| name | create-mcp-tool |
| description | Scaffold a new MCP tool for the CodeScene MCP Server following the project's established patterns for directory structure, dependency injection, decorators, testing, and registration. |
| metadata | {"audience":"contributors","language":"rust"} |
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 (contains CodeSceneServer struct and tool method bindings)
- Tools directory:
src/tools/ with mod.rs for module declarations and parameter type definitions
- Dependency injection: Via the
CodeSceneServer struct, which holds cli_runner: Arc<dyn CliRunner> and http_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:
#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct MyToolParam {
pub some_field: String,
#[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> {
if let Some(r) = server.require_token() {
return Ok(r);
}
server.version_checker.check_in_background();
let file_path = docker::adapt_path_for_docker(Path::new(¶ms.file_path));
let result = run_review(Path::new(&file_path), &*server.cli_runner).await;
match result {
Ok(output) => {
let props = event_properties::score_properties(Path::new(¶ms.file_path), None);
server.track("my-tool", props);
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 &CodeSceneServer and 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 and server.track_err() on failure.
- Use
server.maybe_version_warning() to prepend version update notices to the response.
- Return
tool_error() from crate::tools::common for error results (sets is_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:
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 touching CS_ACCESS_TOKEN. Always bind the guard to _g so it lives for the test's duration.
- Use
MockCliRunner to inject controlled CLI responses:
MockCliRunner::with_ok(output) โ simulates successful CLI execution
MockCliRunner::with_err(code, stderr) โ simulates CLI failure
MockCliRunner::with_responses(vec![...]) โ queues multiple responses for tools that make multiple CLI calls
- Use
make_cli_mock_server() to create a CodeSceneServer with 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_review on each new/modified file (at minimum src/tools/<tool_name>.rs).
- If any code smells or regressions are reported:
- Refactor the flagged code to resolve the issues.
- Re-run
code_health_review after each fix to confirm improvement.
- Before committing, run
pre_commit_code_health_safeguard to catch anything across all changed files.
- Before opening a PR, run
analyze_change_set against 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 &CodeSceneServer and 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: