Skip to content

Commit be69a25

Browse files
Dynamic tool registration based on MCP client capabilities (#61)
* Implement dynamic tool registration based on MCP client capabilities This commit introduces capability-aware tool registration that adapts to client features, ensuring users only see tools that work with their client. Key changes: 1. Tool Categorization (src/tools/toolRegistry.ts): - Split tools into three categories by capability requirements: * CORE_TOOLS (23 tools): Work in all MCP clients * ELICITATION_TOOLS (2 tools): Require elicitation capability - PreviewStyleTool - StyleComparisonTool * RESOURCE_FALLBACK_TOOLS (1 tool): Bridge for missing resource support - GetReferenceTool - Added new getter functions: * getCoreTools() * getElicitationTools() * getResourceFallbackTools() - Deprecated getAllTools() in favor of capability-aware functions - Maintained ALL_TOOLS for backward compatibility and testing 2. Dynamic Registration (src/index.ts): - Register CORE tools before connection (always available) - After connection, check client capabilities via getClientCapabilities() - Conditionally register ELICITATION_TOOLS if client supports elicitation - Conditionally register RESOURCE_FALLBACK_TOOLS if client lacks resource support - Send notifications/tools/list_changed notification after dynamic registration - Added comprehensive logging for registration decisions - Advertise listChanged: true capability to inform clients that tool list can change dynamically Benefits: - Users only see tools that actually work with their client - No confusing runtime capability errors - Cleaner UX for capability-limited clients - Backward compatible with existing tool configuration system - Zero breaking changes (all tools still work, just registered conditionally) All 520 tests pass. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * Tests: Add unit tests for tool categorization Add comprehensive unit tests to verify: - Tool categorization correctness - Getter functions return expected tools - No overlap between categories - Tool counts are correct 15 tests covering: - getCoreTools() - 4 tests - getElicitationTools() - 3 tests - getResourceFallbackTools() - 2 tests - getAllTools() - 3 tests - Tool categorization consistency - 3 tests All tests pass. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * Fix: Move preview/comparison tools to CORE_TOOLS (elicitation not merged yet) The elicitation support for preview_style_tool and style_comparison_tool exists only in the add-preview-token-elicitation branch, which hasn't been merged to main yet. Moving these tools to ELICITATION_TOOLS made them unavailable to all clients. Changes: - Move preview_style_tool back to CORE_TOOLS - Move style_comparison_tool back to CORE_TOOLS - Keep ELICITATION_TOOLS as empty array (ready for future use) - Update tests to reflect empty ELICITATION_TOOLS - Add comments explaining this is temporary until elicitation support merges When elicitation support is merged in the future, these tools can be moved to ELICITATION_TOOLS in a follow-up PR. All 535 tests pass. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * Move GetMapboxDocSourceTool to RESOURCE_FALLBACK_TOOLS The get_latest_mapbox_docs_tool duplicates functionality of the resource://mapbox-documentation resource. It should only be available to clients that don't properly support resources. Changes: - Move GetMapboxDocSourceTool from CORE_TOOLS to RESOURCE_FALLBACK_TOOLS - CORE_TOOLS now has 24 tools (down from 25) - RESOURCE_FALLBACK_TOOLS now has 2 tools (up from 1): * get_reference_tool (reference resources) * get_latest_mapbox_docs_tool (documentation resource) - Update tests to reflect new categorization - Add test for get_latest_mapbox_docs_tool in resource fallback tools Clients that properly support resources will no longer see this tool, reducing clutter in their tool list. All 536 tests pass. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * Fix: Correct resource fallback tool detection logic The previous logic incorrectly checked clientCapabilities.resources which doesn't exist (resources is a SERVER capability, not a CLIENT capability). Now correctly detects clients by name using getClientVersion() and only registers fallback tools for clients with known resource support issues (e.g., Claude Desktop). Result: MCP Inspector and other proper clients now see 23 tools instead of 26, with resource fallback tools only appearing for Claude clients. MCP Inspector Output After Fix: Tools (23): [ "bounding_box_tool", "check_color_contrast_tool", "compare_styles_tool", "coordinate_conversion_tool", "country_bounding_box_tool", "create_style_tool", "create_token_tool", "delete_style_tool", "geojson_preview_tool", "get_feedback_tool", "list_feedback_tool", "list_styles_tool", "list_tokens_tool", "optimize_style_tool", "preview_style_tool", "retrieve_style_tool", "style_builder_tool", "style_comparison_tool", "tilequery_tool", "update_style_tool", "validate_expression_tool", "validate_geojson_tool", "validate_style_tool" ] Resources (5): [ "resource://mapbox-documentation", "resource://mapbox-layer-type-mapping", "resource://mapbox-streets-v8-fields", "resource://mapbox-style-layers", "resource://mapbox-token-scopes" ] Fixes dynamic tool registration to work as intended. * Address code review: Invert resource fallback logic for safer client compatibility Per @Valiunia's review feedback, invert the resource fallback tool logic to use an allowlist approach instead of blocklist. This is safer for unknown clients. Changes: - OLD: Only provide fallback tools if client name includes 'claude' (blocklist) - NEW: Provide fallback tools to ALL clients UNLESS we know they support resources (allowlist) Known clients with proper resource support (skip fallback tools): - inspector - vscode Benefits: - Unknown/new clients get fallback tools by default (safer) - No risk of missing clients that need resource fallback - More defensive approach for ecosystem compatibility Related: PR #61 review comments All tests passing (536 tests). * Clarify resource fallback logic: for clients without resource support (like smolagents) Update comments and logic to reflect the real use case: resource fallback tools are for clients that don't support resources at all (like smolagents), not for buggy resource implementations. Changes: - Add 'claude' to allowlist (Claude Desktop now supports resources properly) - Update comments to clarify this is for clients without resource support - Change from "may not support properly" to "don't support resources" - Document smolagents as the primary use case Known clients WITH resource support (skip fallback tools): - inspector - vscode - claude (Claude Desktop/Code) Clients WITHOUT resource support (get fallback tools): - smolagents - unknown/new clients (safer default) All tests passing (536 tests). * Replace client detection with CLIENT_NEEDS_RESOURCE_FALLBACK env var (opt-in) Remove fragile name-based client detection and replace with explicit environment variable configuration. Use opt-in approach since most MCP clients support resources. Changes: - Remove client name checking logic entirely - Add CLIENT_NEEDS_RESOURCE_FALLBACK environment variable - Default (unset): Assume client supports resources, skip fallback tools - Set to "true": Client needs fallback tools (e.g., smolagents) Benefits: - No maintenance burden tracking client names/versions - Better default (most clients support resources) - Opt-in only for exceptions (smolagents, etc.) - Explicit configuration by users who know their client Configuration: ```bash # smolagents or clients without resource support export CLIENT_NEEDS_RESOURCE_FALLBACK=true # Claude Desktop, VS Code, Inspector, etc. (default) # Leave unset - assumes resource support ``` Documentation updated in README.md explaining when to set this variable. All tests passing (536 tests). --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 45fa1b3 commit be69a25

4 files changed

Lines changed: 352 additions & 11 deletions

File tree

README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1304,6 +1304,29 @@ node dist/esm/index.js --disable-mcp-ui
13041304

13051305
**Note:** You typically don't need to disable this. The implementation is fully backwards compatible and doesn't affect clients that don't support MCP-UI. See [mcpui.dev](https://mcpui.dev) for compatible clients.
13061306

1307+
#### CLIENT_NEEDS_RESOURCE_FALLBACK
1308+
1309+
**Resource Fallback Tools (Opt-In for Non-Compliant Clients)**
1310+
1311+
Resources are a core MCP feature supported by most clients (Claude Desktop, VS Code, MCP Inspector, etc.). However, some clients (like smolagents) don't support resources at all. For these clients, the server can provide "resource fallback tools" that deliver the same content as resources but via tool calls.
1312+
1313+
**Fallback Tools:**
1314+
1315+
- `get_reference_tool` - Access to style layers, Streets v8 fields, token scopes, layer type mapping
1316+
- `get_latest_mapbox_docs_tool` - Access to Mapbox documentation
1317+
1318+
**By default, these tools are NOT included** (assumes your client supports resources). If your client doesn't support resources, enable the fallback tools:
1319+
1320+
```bash
1321+
export CLIENT_NEEDS_RESOURCE_FALLBACK=true
1322+
```
1323+
1324+
**When to set this:**
1325+
1326+
- ✅ Set to `true` if using smolagents or other clients without resource support
1327+
- ❌ Leave unset (default) if using Claude Desktop, VS Code, MCP Inspector, or any resource-capable client
1328+
- ❌ Leave unset if unsure (most clients support resources)
1329+
13071330
## Troubleshooting
13081331

13091332
**Issue:** Tools fail with authentication errors

src/index.ts

Lines changed: 93 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
1313
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
1414
import { z } from 'zod';
1515
import { parseToolConfigFromArgs, filterTools } from './config/toolConfig.js';
16-
import { getAllTools } from './tools/toolRegistry.js';
16+
import {
17+
getCoreTools,
18+
getElicitationTools,
19+
getResourceFallbackTools
20+
} from './tools/toolRegistry.js';
1721
import { getAllResources } from './resources/resourceRegistry.js';
1822
import { getAllPrompts } from './prompts/promptRegistry.js';
1923
import { getVersionInfo } from './utils/versionUtils.js';
@@ -54,8 +58,14 @@ const versionInfo = getVersionInfo();
5458
const config = parseToolConfigFromArgs();
5559

5660
// Get and filter tools based on configuration
57-
const allTools = getAllTools();
58-
const enabledTools = filterTools(allTools, config);
61+
// Split into categories for capability-aware registration
62+
const coreTools = getCoreTools();
63+
const elicitationTools = getElicitationTools();
64+
const resourceFallbackTools = getResourceFallbackTools();
65+
66+
const enabledCoreTools = filterTools(coreTools, config);
67+
const enabledElicitationTools = filterTools(elicitationTools, config);
68+
const enabledResourceFallbackTools = filterTools(resourceFallbackTools, config);
5969

6070
// Create an MCP server
6171
const server = new McpServer(
@@ -65,15 +75,18 @@ const server = new McpServer(
6575
},
6676
{
6777
capabilities: {
68-
tools: {},
78+
tools: {
79+
listChanged: true // Advertise support for dynamic tool registration
80+
},
6981
resources: {},
7082
prompts: {}
7183
}
7284
}
7385
);
7486

75-
// Register enabled tools to the server
76-
enabledTools.forEach((tool) => {
87+
// Register only core tools before connection
88+
// Capability-dependent tools will be registered dynamically after connection
89+
enabledCoreTools.forEach((tool) => {
7790
tool.installTo(server);
7891
});
7992

@@ -210,6 +223,80 @@ async function main() {
210223
// Start receiving messages on stdin and sending messages on stdout
211224
const transport = new StdioServerTransport();
212225
await server.connect(transport);
226+
227+
// After connection, dynamically register capability-dependent tools
228+
const clientCapabilities = server.server.getClientCapabilities();
229+
230+
// Debug: Log what capabilities we detected
231+
server.server.sendLoggingMessage({
232+
level: 'info',
233+
data: `Client capabilities detected: ${JSON.stringify(clientCapabilities, null, 2)}`
234+
});
235+
236+
let toolsAdded = false;
237+
238+
// Register elicitation tools if client supports elicitation
239+
if (clientCapabilities?.elicitation && enabledElicitationTools.length > 0) {
240+
server.server.sendLoggingMessage({
241+
level: 'info',
242+
data: `Client supports elicitation. Registering ${enabledElicitationTools.length} elicitation-dependent tools`
243+
});
244+
245+
enabledElicitationTools.forEach((tool) => {
246+
tool.installTo(server);
247+
});
248+
toolsAdded = true;
249+
} else if (enabledElicitationTools.length > 0) {
250+
server.server.sendLoggingMessage({
251+
level: 'debug',
252+
data: `Client does not support elicitation. Skipping ${enabledElicitationTools.length} elicitation-dependent tools`
253+
});
254+
}
255+
256+
// Register resource fallback tools for clients that don't support resources
257+
// Note: Resources are a core MCP feature supported by most clients.
258+
// However, some clients (like smolagents) don't support resources at all.
259+
// These fallback tools provide the same content as resources but via tool calls instead.
260+
//
261+
// Configuration via CLIENT_NEEDS_RESOURCE_FALLBACK environment variable:
262+
// - unset (default) = Skip fallback tools (assume client supports resources)
263+
// - "true" = Provide fallback tools (client does NOT support resources)
264+
const clientNeedsResourceFallback =
265+
process.env.CLIENT_NEEDS_RESOURCE_FALLBACK?.toLowerCase() === 'true';
266+
267+
if (clientNeedsResourceFallback && enabledResourceFallbackTools.length > 0) {
268+
server.server.sendLoggingMessage({
269+
level: 'info',
270+
data: `CLIENT_NEEDS_RESOURCE_FALLBACK=true. Registering ${enabledResourceFallbackTools.length} resource fallback tools`
271+
});
272+
273+
enabledResourceFallbackTools.forEach((tool) => {
274+
tool.installTo(server);
275+
});
276+
toolsAdded = true;
277+
} else if (enabledResourceFallbackTools.length > 0) {
278+
server.server.sendLoggingMessage({
279+
level: 'debug',
280+
data: `CLIENT_NEEDS_RESOURCE_FALLBACK not set or false. Skipping ${enabledResourceFallbackTools.length} resource fallback tools (client supports resources)`
281+
});
282+
}
283+
284+
// Notify client about tool list changes if any tools were added
285+
if (toolsAdded) {
286+
try {
287+
server.sendToolListChanged();
288+
289+
server.server.sendLoggingMessage({
290+
level: 'debug',
291+
data: 'Sent notifications/tools/list_changed to client'
292+
});
293+
} catch (error) {
294+
server.server.sendLoggingMessage({
295+
level: 'warning',
296+
data: `Failed to send tool list change notification: ${error instanceof Error ? error.message : String(error)}`
297+
});
298+
}
299+
}
213300
}
214301

215302
// Ensure cleanup interval is cleared when the process exits

src/tools/toolRegistry.ts

Lines changed: 71 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,11 @@ import { ValidateGeojsonTool } from './validate-geojson-tool/ValidateGeojsonTool
2828
import { ValidateStyleTool } from './validate-style-tool/ValidateStyleTool.js';
2929
import { httpRequest } from '../utils/httpPipeline.js';
3030

31-
// Central registry of all tools
32-
export const ALL_TOOLS = [
31+
/**
32+
* Core tools that work in all MCP clients without requiring special capabilities
33+
* These tools are registered immediately during server startup
34+
*/
35+
export const CORE_TOOLS = [
3336
new ListStylesTool({ httpRequest }),
3437
new CreateStyleTool({ httpRequest }),
3538
new RetrieveStyleTool({ httpRequest }),
@@ -41,28 +44,91 @@ export const ALL_TOOLS = [
4144
new CheckColorContrastTool(),
4245
new CompareStylesTool(),
4346
new OptimizeStyleTool(),
47+
new StyleComparisonTool(),
4448
new CreateTokenTool({ httpRequest }),
4549
new ListTokensTool({ httpRequest }),
4650
new BoundingBoxTool(),
4751
new CountryBoundingBoxTool(),
4852
new CoordinateConversionTool(),
4953
new GetFeedbackTool({ httpRequest }),
5054
new ListFeedbackTool({ httpRequest }),
51-
new GetMapboxDocSourceTool({ httpRequest }),
52-
new GetReferenceTool(),
53-
new StyleComparisonTool(),
5455
new TilequeryTool({ httpRequest }),
5556
new ValidateExpressionTool(),
5657
new ValidateGeojsonTool(),
5758
new ValidateStyleTool()
5859
] as const;
5960

61+
/**
62+
* Tools that require elicitation capability for optimal functionality
63+
* These tools use elicitInput() for secure token management
64+
* Registered only if client supports elicitation
65+
*
66+
* Currently empty - elicitation support will be added in a future PR.
67+
* This category is ready for tools that require the elicitation capability.
68+
*/
69+
export const ELICITATION_TOOLS = [] as const;
70+
71+
/**
72+
* Tools that serve as bridges for clients without resource support
73+
* These tools are only registered if CLIENT_NEEDS_RESOURCE_FALLBACK env var is set to "true"
74+
*
75+
* Context: Most MCP clients support resources (Claude Desktop, VS Code, Inspector, etc.).
76+
* However, some clients (like smolagents) don't support resources at all.
77+
* These tools provide the same content as resources but via tool calls instead.
78+
*
79+
* Configuration:
80+
* - Leave unset (default) = Skip these tools (assumes client supports resources)
81+
* - Set CLIENT_NEEDS_RESOURCE_FALLBACK=true = Include these tools (for smolagents, etc.)
82+
*
83+
* Tools:
84+
* - GetReferenceTool: Provides access to reference resources (style layers, Streets v8 fields, token scopes, layer type mapping)
85+
* - GetMapboxDocSourceTool: Provides access to Mapbox documentation (resource://mapbox-documentation)
86+
*/
87+
export const RESOURCE_FALLBACK_TOOLS = [
88+
new GetReferenceTool(),
89+
new GetMapboxDocSourceTool({ httpRequest })
90+
] as const;
91+
92+
/**
93+
* All tools combined (for backward compatibility and testing)
94+
*/
95+
export const ALL_TOOLS = [
96+
...CORE_TOOLS,
97+
...ELICITATION_TOOLS,
98+
...RESOURCE_FALLBACK_TOOLS
99+
] as const;
100+
60101
export type ToolInstance = (typeof ALL_TOOLS)[number];
61102

103+
/**
104+
* Get all tools (for backward compatibility)
105+
* @deprecated Use getCoreTools(), getElicitationTools(), etc. instead for capability-aware registration
106+
*/
62107
export function getAllTools(): readonly ToolInstance[] {
63108
return ALL_TOOLS;
64109
}
65110

111+
/**
112+
* Get tools that work in all MCP clients
113+
*/
114+
export function getCoreTools(): readonly ToolInstance[] {
115+
return CORE_TOOLS;
116+
}
117+
118+
/**
119+
* Get tools that require elicitation capability
120+
*/
121+
export function getElicitationTools(): readonly ToolInstance[] {
122+
return ELICITATION_TOOLS;
123+
}
124+
125+
/**
126+
* Get tools that serve as fallbacks when client doesn't support resources
127+
*/
128+
export function getResourceFallbackTools(): readonly ToolInstance[] {
129+
return RESOURCE_FALLBACK_TOOLS;
130+
}
131+
66132
export function getToolByName(name: string): ToolInstance | undefined {
67133
return ALL_TOOLS.find((tool) => tool.name === name);
68134
}

0 commit comments

Comments
 (0)