Purpose
Use this skill when creating a new MCP tool in the CodeScene MCP Server project. It encodes the exact conventions, patterns, and file structure that all existing tools follow.
Project Context
- Framework:
rmcp(Rust MCP SDK) with#[tool(tool_box)]macros - Language: Rust
- Source root:
src/ - Server entry point:
src/main.rs(containsCodeSceneServerstruct and tool method bindings) - Tools directory:
src/tools/withmod.rsfor module declarations and parameter type definitions - Dependency injection: Via the
CodeSceneServerstruct, which holdscli_runner: Arc<dyn CliRunner>andhttp_client: Arc<dyn HttpClient>trait objects
Step-by-Step
1. Create the tool file
Create a new .rs file in src/tools/ named after the tool using snake_case:
src/tools/<tool_name>.rs
2. Define the parameter struct (if needed)
If the tool needs parameters beyond what already exists, add a new struct to src/tools/mod.rs:
/// Parameters for <description of what the tool does>.
#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct MyToolParam {
/// Description of this field for the JSON Schema (visible to LLM clients).
pub some_field: String,
/// Optional fields use Option<T> with #[serde(default)].
#[serde(default)]
pub optional_field: Option<String>,
}
Existing parameter structs that may already fit your tool:
| Struct | Fields | When to use |
|---|---|---|
FilePathParam |
file_path: String |
Single-file analysis tools |
GitRepoParam |
git_repository_path: String |
Repository-level tools |
ChangeSetParam |
base_ref: String, git_repository_path: String |
Branch diff tools |
RefactorParam |
file_path: String, function_name: String |
Function-level refactoring |
ProjectParam |
project_id: i64 |
Project-scoped API tools |
ProjectFileParam |
file_path: String, project_id: i64 |
Project + file API tools |
OptionalContext |
context: Option<String> |
Tools that take no meaningful input |
GetConfigParam |
key: Option<String> |
Config read |
SetConfigParam |
key: String, value: String |
Config write |
Reuse an existing struct when possible. Only define a new one if no existing struct matches.
3. Add the module declaration
Add a pub mod line in src/tools/mod.rs, keeping the list alphabetically sorted:
pub mod my_tool;
4. Implement the tool handler
Create src/tools/<tool_name>.rs with a handle function:
use std::path::Path;
use rmcp::model::{CallToolResult, Content};
use rmcp::ErrorData;
use crate::docker;
use crate::event_properties;
use crate::tools::common::{run_review, tool_error};
use crate::tools::FilePathParam;
use crate::CodeSceneServer;
pub(crate) async fn handle(
server: &CodeSceneServer,
params: FilePathParam,
) -> Result<CallToolResult, ErrorData> {
// 1. Check for access token
if let Some(r) = server.require_token() {
return Ok(r);
}
// 2. Trigger background version check
server.version_checker.check_in_background();
// 3. Adapt paths for Docker if needed
let file_path = docker::adapt_path_for_docker(Path::new(¶ms.file_path));
// 4. Call the CLI or HTTP client via the server's injected dependencies
let result = run_review(Path::new(&file_path), &*server.cli_runner).await;
// 5. Handle the result
match result {
Ok(output) => {
// Track success event
let props = event_properties::score_properties(Path::new(¶ms.file_path), None);
server.track("my-tool", props);
// Format and return
let text = server.maybe_version_warning(&output).await;
Ok(CallToolResult::success(vec![Content::text(text)]))
}
Err(e) => {
server.track_err("my-tool", &e.to_string());
Ok(tool_error(&format!("Error: {e}")))
}
}
}
Key conventions:
- The handler is a free
async fn, not a method on a struct. It receives&CodeSceneServerand the parameter struct. - Always check for the access token first with
server.require_token(). - Always trigger the background version check with
server.version_checker.check_in_background(). - Use
docker::adapt_path_for_docker()when the tool receives file paths from the client. - Track events with
server.track()on success andserver.track_err()on failure. - Use
server.maybe_version_warning()to prepend version update notices to the response. - Return
tool_error()fromcrate::tools::commonfor error results (setsis_error = true). - Return type is always
Result<CallToolResult, ErrorData>.
Choose the right dependency based on what the tool does:
| Dependency | Access via | When to use |
|---|---|---|
server.cli_runner |
Arc<dyn CliRunner> |
Run the CodeScene CLI (code review, pre-commit, change-set) |
server.http_client |
Arc<dyn HttpClient> |
Make HTTP requests to CodeScene cloud/on-prem API |
server.config_data |
Arc<ConfigData> |
Read server configuration values |
5. Wire the tool method on CodeSceneServer
In src/main.rs, inside the #[tool_router] impl CodeSceneServer block, add a new method:
#[tool(
description = "One-line summary of what this tool does.\nAdditional detail on inputs, outputs, and how the LLM should present results.",
input_schema = inlined_schema_for::<MyToolParam>()
)]
async fn my_tool(
&self,
Parameters(params): Parameters<MyToolParam>,
) -> Result<CallToolResult, ErrorData> {
tools::my_tool::handle(self, params).await
}
Conventions:
- The
descriptionstring is critical — it becomes the tool description visible to LLM clients. Be specific about inputs, outputs, and how the LLM should present results. - The method name becomes the MCP tool name as seen by clients.
- Use
inlined_schema_for::<ParamType>()to generate the JSON Schema for the parameter struct. - The method body is a one-liner that delegates to the handler in
src/tools/<tool_name>.rs. - For tools with no parameters, omit
input_schemaandParameters:#[tool(description = "...")] async fn my_tool(&self) -> Result<CallToolResult, ErrorData> { tools::my_tool::handle(self).await }
Also add the parameter struct to the import at the top of src/main.rs if you defined a new one:
use crate::tools::{
ChangeSetParam, FilePathParam, GetConfigParam, GitRepoParam, MyToolParam,
OptionalContext, OwnershipParam, ProjectFileParam, ProjectParam,
RefactorParam, SetConfigParam,
};
6. Write inline tests
Add a #[cfg(test)] module at the bottom of src/tools/<tool_name>.rs:
#[cfg(test)]
mod tests {
use rmcp::handler::server::wrapper::Parameters;
use crate::tests::{
assert_error_contains, assert_success_contains, assert_token_error, clear_token,
make_cli_mock_server, make_server, set_token, MockCliRunner,
};
use crate::tools::FilePathParam;
#[tokio::test]
async fn rejects_missing_token() {
let _g = clear_token();
let params = FilePathParam {
file_path: "/tmp/f.rs".to_string(),
};
let result = make_server(false)
.my_tool(Parameters(params))
.await
.unwrap();
assert_token_error(&result);
}
#[tokio::test]
async fn success_returns_expected_content() {
let _g = set_token("tok");
let server = make_cli_mock_server(MockCliRunner::with_ok(r#"{"score":8.5,"review":[]}"#));
let params = FilePathParam {
file_path: "/tmp/test.rs".to_string(),
};
let result = server.my_tool(Parameters(params)).await.unwrap();
assert_success_contains(&result, "expected content");
}
#[tokio::test]
async fn error_returns_tool_error() {
let _g = set_token("tok");
let server = make_cli_mock_server(MockCliRunner::with_err(1, "tool failed"));
let params = FilePathParam {
file_path: "/tmp/test.rs".to_string(),
};
let result = server.my_tool(Parameters(params)).await.unwrap();
assert_error_contains(&result, "tool failed");
}
}
Key patterns:
- Use
set_token()/clear_token()— these acquire a mutex guard (TokenGuard) that serializes tests touchingCS_ACCESS_TOKEN. Always bind the guard to_gso it lives for the test's duration. - Use
MockCliRunnerto inject controlled CLI responses:MockCliRunner::with_ok(output)— simulates successful CLI executionMockCliRunner::with_err(code, stderr)— simulates CLI failureMockCliRunner::with_responses(vec![...])— queues multiple responses for tools that make multiple CLI calls
- Use
make_cli_mock_server()to create aCodeSceneServerwith mocked CLI and default HTTP. - Use
make_server_with_mocks()when you need to mock both CLI and HTTP. - Call the tool method directly on the server instance:
server.my_tool(Parameters(params)). - Use assertion helpers:
assert_success_contains,assert_error_contains,assert_token_error,assert_standalone_error. - Test at minimum: missing token rejection, success path, and error path.
7. Validate Code Health and fix any issues
After the tool is implemented, tested, and registered, run the CodeScene Code Health tools to ensure the new code meets quality standards. Do not consider the tool complete until Code Health passes without regressions.
Choose the appropriate scope:
| Scope | Tool | When to use |
|---|---|---|
| Single file | code_health_review |
Quick check on a specific file you just created or modified |
| Staged + unstaged changes | pre_commit_code_health_safeguard |
Before committing — reviews all uncommitted changes in the repo |
| Full branch vs base | analyze_change_set |
Before opening a PR — reviews all committed + staged + unstaged changes against a base ref |
Recommended workflow:
- Run
code_health_reviewon each new/modified file (at minimumsrc/tools/<tool_name>.rs). - If any code smells or regressions are reported:
- Refactor the flagged code to resolve the issues.
- Re-run
code_health_reviewafter each fix to confirm improvement.
- Before committing, run
pre_commit_code_health_safeguardto catch anything across all changed files. - Before opening a PR, run
analyze_change_setagainst the target branch to ensure no regressions across the full change set.
Target: Code Health 10.0. Scores of 9+ are not "good enough" — aim for optimal.
Canonical Example: code_health_score
The simplest existing tool to reference is src/tools/code_health_score.rs. It demonstrates:
- Single parameter (
FilePathParam) - Free
async fn handle()receiving&CodeSceneServerand params - Token check, version check, Docker path adaptation
- CLI call via
server.cli_runner - Event tracking on success and error
- Version warning prepended to output
- Inline
#[cfg(test)]module with mocked dependencies
Refer to src/tools/code_health_score.rs as the minimal template.
Checklist
Before considering the tool complete:
- Tool file created at
src/tools/<tool_name>.rs - Parameter struct defined in
src/tools/mod.rs(or existing struct reused) - Module declared in
src/tools/mod.rs(pub mod <tool_name>;) - Handler function
pub(crate) async fn handle()implemented with token check, version check, and error handling - Tool method added to
#[tool_router] impl CodeSceneServerinsrc/main.rswith#[tool(description = "...")] - Parameter struct imported in
src/main.rs(if new) - Inline tests cover: missing token, success path, and error path
- Tests use
MockCliRunner/MockHttpClientfor dependency injection - Run tests with
cargo test -
code_health_reviewpasses on all new/modified files with no code smells -
pre_commit_code_health_safeguardreports no regressions before commit -
analyze_change_setreports no regressions before opening a PR