Scan TypeScript projects for @ExposeTool decorated methods and automatically generate VS Code contributes.languageModelTools entries in package.json.
# Global (provides `mcp-scanner` CLI)
npm install -g @codearchitects/mcp-scanner
# As a project dependency
npm install @codearchitects/mcp-scannerimport { ExposeTool } from '@codearchitects/mcp-scanner';
interface IGreetParams {
/** The user's name. */
name: string;
/** Optional greeting style. */
style?: 'formal' | 'casual';
}
class MyService {
@ExposeTool({
name: 'greetUser',
displayName: 'Greet User',
modelDescription: 'Say hello to a user by name with a chosen style.',
icon: '$(smiley)',
})
greetUser(params: IGreetParams): string {
return params.style === 'formal'
? `Good day, ${params.name}.`
: `Hey ${params.name}!`;
}
}# From your project root
mcp-scanner
# Or with options
mcp-scanner --project /path/to/project --tsconfig tsconfig.json
# Restrict scan only to a folder/subtree
mcp-scanner --project /path/to/project --tools-path src/tools
# Exclude one or more folders/subtrees from scan
mcp-scanner --project /path/to/project --exclude-path src/generated --exclude-path src/legacy
# Tag-scoped patching: only tools with this tag are replaced on rerun
mcp-scanner --project /path/to/project --tools-tag core
# Write to a specific package.json
mcp-scanner --project /path/to/project --package-json ../apps/vscode-ext/package.json
# Generate proxy methods into another library file
mcp-scanner --project /path/to/source-lib \
--proxy-file ../apps/vscode-ext/src/services/generated-proxies.ts \
--scaffold-template ../apps/vscode-ext/proxy-scaffold.ejs \
--proxy-class ModelerToolsProxy
# Copy the default proxy scaffold template to customize it
mcp-scanner --init-proxy-file ./proxy-scaffold.ejs
# Dry run (preview without writing)
mcp-scanner --dry-run{
"contributes": {
"languageModelTools": [
{ "name": "existingManualTool", "..." : "..." },
{
"name": "greetUser",
"displayName": "Greet User",
"modelDescription": "Say hello to a user by name with a chosen style.",
"canBeReferencedInPrompt": true,
"toolReferenceName": "greetUser",
"icon": "$(smiley)",
"inputSchema": {
"type": "object",
"properties": {
"name": { "type": "string", "description": "The user's name." },
"style": { "type": "string", "enum": ["formal", "casual"], "description": "Optional greeting style." }
},
"required": ["name"]
}
}
]
}
}mcp-scanner also writes .mcp-scanner.state.json in the project root to track which tools were generated in the previous run.
On re-run, only those previously generated tools are replaced, while manually-added tools are preserved.
If legacy marker strings are present (____AUTOGEN_TOOLS_START____ / ____AUTOGEN_TOOLS_END____), they are migrated automatically on the next run.
In your VS Code extension, register the Language Model Tool so users can invoke it from Copilot Chat:
import { registerScanProjectToolsLmTool } from '@codearchitects/mcp-scanner/vscode';
export function activate(context: vscode.ExtensionContext) {
// ... your extension setup ...
// Register #scanProjectTools LM tool
registerScanProjectToolsLmTool(context);
}Then declare it in your extension's package.json:
{
"contributes": {
"languageModelTools": [
{
"name": "scanProjectTools",
"displayName": "Scan Project: @ExposeTool → package.json",
"modelDescription": "Scan the current TypeScript project for methods decorated with @ExposeTool, generate JSON Schema from parameter interfaces, and update contributes.languageModelTools in package.json.",
"canBeReferencedInPrompt": true,
"toolReferenceName": "scanProjectTools",
"icon": "$(search)",
"inputSchema": {
"type": "object",
"properties": {
"tsconfigPath": { "type": "string", "description": "tsconfig file name. Defaults to tsconfig.json." },
"autoApply": { "type": "boolean", "description": "Apply changes without confirmation." }
}
}
}
]
}
}| Option | Type | Required | Description |
|---|---|---|---|
name |
string |
✅ | Unique tool name |
displayName |
string |
✅ | Human-readable label |
modelDescription |
string |
✅ | Description for the language model |
icon |
string |
VS Code codicon, e.g. $(search). Default: $(tools) |
|
canBeReferencedInPrompt |
boolean |
Allow #tool references. Default: true |
Used by code generators (e.g., proxy generation) to preserve tool metadata without exposing the method.
Accepts the same options as @ExposeTool but does not register the method as a tool.
scanProject recognises @Tool and derives inputSchema from the method parameter types exactly like @ExposeTool — so Library B (the proxy side) produces a fully-populated contributes.languageModelTools entry without any extra configuration.
| Option | Type | Required | Description |
|---|---|---|---|
name |
string |
✅ | Unique tool name |
displayName |
string |
✅ | Human-readable label |
modelDescription |
string |
✅ | Description for the language model |
icon |
string |
VS Code codicon, e.g. $(search). Default: $(tools) |
|
canBeReferencedInPrompt |
boolean |
Allow #tool references. Default: true |
Registers methods decorated with either @ExposeTool or @Tool as VS Code Language Model Tool handlers at runtime.
Duplicate tool names within the same activation are skipped with a warning.
This is useful for proxy-based architectures where generated proxy methods carry @Tool metadata.
Returns IScanResult with discovered tools, file count, and diagnostics.
Patches package.json on disk and updates .mcp-scanner.state.json.
When options.toolTag is set, patching is tag-scoped and state-file ownership is not used.
Patches raw JSON string (for use with VS Code fs API or other runtimes). Accepts an optional third argument with previously generated tool names.
Signature:
patchPackageJsonContent(
raw: string,
generatedTools: IScannedTool[],
previousGeneratedToolNames?: string[],
options?: { toolTag?: string },
): {
content: string;
result: IPatchResult;
nextGeneratedToolNames: string[];
}mcp-scanner [options]
--project, -p <path> Project root (default: cwd)
--tsconfig, -t <name> tsconfig file name (default: tsconfig.json)
--tools-path, -s <path>
Restrict scanning to this path subtree.
Relative paths are resolved from --project.
--exclude-path, -i <path>
Exclude this path subtree from scanning.
Can be repeated. Relative paths are resolved from --project.
--tools-tag, -g <tag>
Tag generated tools and patch only tools with this tag.
If omitted, legacy state-based patching is used.
--package-json, -j <path>
package.json path to patch.
Relative paths are resolved from --project.
(default: <project>/package.json)
--proxy-file, -o <path>
Generate proxy methods into this file.
Relative paths are resolved from --project.
--proxy-class, -c <name>
Class name for generated proxies.
(default: GeneratedExposeToolProxies)
--scaffold-template, -x <path>
Custom EJS scaffold template for proxy generation.
Relative paths are resolved from --project.
--init-proxy-file <path>
Copy default complete proxy scaffold template.
Recommended extension: .ejs
--extra, -e <path> [tsconfig]
Additional project root to scan (repeatable)
--dry-run, -d Preview without writing
--help, -h Show help
When @ExposeTool methods live in one library but runtime command handlers live in another,
you can generate a proxy class in the target library.
# Copy the default proxy scaffold template to start with
mcp-scanner --init-proxy-file ./proxy-scaffold.ejs
# Edit proxy-scaffold.ejs to customize (class layout, method body, imports)
# Then generate/update the TypeScript proxy file from that template
mcp-scanner --proxy-file ./MyProxyClass.ts \
--proxy-class MyCustomClassName \
--scaffold-template ./proxy-scaffold.ejs- One proxy method for each decorated source method.
- Original method JSDoc copied to each generated proxy method.
- Type imports required by method parameters/return type.
- Method body includes TODO comment with context and example.
- Reserved injection zones, so only generated sections are refreshed on re-run.
The scaffold template (EJS) has access to:
className: The class name (set via--proxy-classor from template)methods: Array of method objects with:toolName: Tool name from@ExposeTool({ name })toolOptions: Complete metadata object from@ExposeTool({ displayName, modelDescription, icon, canBeReferencedInPrompt })methodName: Source method namereturnTypeText: Return type textjsDoc: JSDoc comment from sourceparameters: Array of{ name, typeText, optional }firstParameterName: First parameter name if presentimportStatements: Array of required import statements
Default scaffold template (view/customize with --init-proxy-file):
/* eslint-disable @typescript-eslint/no-unused-vars */
/* This file is auto-generated by mcp-scanner. */
// <mcp-scanner:proxy-imports:start>
import { Tool } from 'mcp-scanner';
<%- methods.flatMap(m => m.importStatements).filter((v, i, a) => a.indexOf(v) === i).sort().join('\n') %>
// <mcp-scanner:proxy-imports:end>
// <mcp-scanner:proxy-class:start>
export class <%- className %> {
// <mcp-scanner:proxy-class:end>
// <mcp-scanner:proxy-methods:start>
// JSDoc comment
// @Tool decorator with metadata
// public async method(...) { ... }
// <mcp-scanner:proxy-methods:end>
}You can also build your own template using the same EJS syntax and variable context.
Notes:
- Use
--package-jsonand--proxy-filetogether when source and target libraries differ. - Use
--tools-tagwhen multiple generators write to the samelanguageModelToolsarray. - On first run, if the proxy file does not exist,
mcp-scannercreates it with markers. - On subsequent runs, only marker sections (imports, class, methods) are updated; manual code outside markers is preserved.
- If the target file exists but has no method markers, generation stops to avoid destructive overwrite.
- For local exported types declared in source files, generation uses package imports when source and target are in different packages (derived from the nearest source
package.jsonname per file, including--extraprojects), otherwise relative imports are used. - Local types must be
exported by the source package entrypoint to be importable from another package.
Injection markers used by the generator:
// <mcp-scanner:proxy-imports:start>
// ...auto-generated imports...
// <mcp-scanner:proxy-imports:end>
// <mcp-scanner:proxy-class:start>
// ...class declaration...
// <mcp-scanner:proxy-class:end>
// <mcp-scanner:proxy-methods:start>
// ...auto-generated methods...
// <mcp-scanner:proxy-methods:end>When generating proxy methods, mcp-scanner automatically applies the @Tool decorator to preserve all original tool metadata (displayName, modelDescription, icon, canBeReferencedInPrompt).
@ExposeToolexposes the method as a tool, which would cause the proxy method to appear inlanguageModelToolsin addition to the original. This is not desired.@Toolpreserves metadata without exposure, allowing language model tools and MCP servers to access the complete tool definition without the proxy being registered as a separate tool.
Given a source library method:
class MyLibrary {
@ExposeTool({
name: 'processData',
displayName: 'Process Data',
modelDescription: 'Transforms data according to rules.',
icon: '$(gear)',
})
processData(params: IDataParams): Promise<string> { ... }
}The generated proxy preserves the metadata:
class MyProxyClass {
/**
* Transforms data according to rules.
*/
@Tool({
name: 'processData',
displayName: 'Process Data',
modelDescription: 'Transforms data according to rules.',
icon: '$(gear)',
})
public async processData(params: IDataParams): Promise<string> {
// TODO: Replace with your implementation
return await this._bridge.dispatch('processData', params);
}
}The @Tool decorator is automatically imported from mcp-scanner in the proxy file's imports section.
- Loads the project's
tsconfig.jsonand creates a TypeScript program - Walks the AST of every non-declaration source file
- Finds
@ExposeTool(...)decorated methods on classes - Extracts the decorator's options object (name, displayName, modelDescription, icon)
- Resolves the first parameter's type into a JSON Schema (
inputSchema)- Interfaces →
{ type: "object", properties: {...}, required: [...] } - String unions →
{ type: "string", enum: [...] } - Arrays, nested types, JSDoc descriptions — all handled
- Interfaces →
- Replaces previously generated tools in
package.jsoncontributes.languageModelTools - Persists generated ownership to
.mcp-scanner.state.json
MIT - see LICENSE.