From cc8d95c22bd69a76abf48276df306302e371b219 Mon Sep 17 00:00:00 2001 From: Matthew Podwysocki Date: Tue, 27 Jan 2026 12:12:49 -0500 Subject: [PATCH 1/8] 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 --- src/index.ts | 85 ++++++++++++++++++++++++++++++++++++--- src/tools/toolRegistry.ts | 64 ++++++++++++++++++++++++++--- 2 files changed, 138 insertions(+), 11 deletions(-) diff --git a/src/index.ts b/src/index.ts index e0d6150..18683d7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,7 +13,11 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { z } from 'zod'; import { parseToolConfigFromArgs, filterTools } from './config/toolConfig.js'; -import { getAllTools } from './tools/toolRegistry.js'; +import { + getCoreTools, + getElicitationTools, + getResourceFallbackTools +} from './tools/toolRegistry.js'; import { getAllResources } from './resources/resourceRegistry.js'; import { getAllPrompts } from './prompts/promptRegistry.js'; import { getVersionInfo } from './utils/versionUtils.js'; @@ -54,8 +58,14 @@ const versionInfo = getVersionInfo(); const config = parseToolConfigFromArgs(); // Get and filter tools based on configuration -const allTools = getAllTools(); -const enabledTools = filterTools(allTools, config); +// Split into categories for capability-aware registration +const coreTools = getCoreTools(); +const elicitationTools = getElicitationTools(); +const resourceFallbackTools = getResourceFallbackTools(); + +const enabledCoreTools = filterTools(coreTools, config); +const enabledElicitationTools = filterTools(elicitationTools, config); +const enabledResourceFallbackTools = filterTools(resourceFallbackTools, config); // Create an MCP server const server = new McpServer( @@ -65,15 +75,18 @@ const server = new McpServer( }, { capabilities: { - tools: {}, + tools: { + listChanged: true // Advertise support for dynamic tool registration + }, resources: {}, prompts: {} } } ); -// Register enabled tools to the server -enabledTools.forEach((tool) => { +// Register only core tools before connection +// Capability-dependent tools will be registered dynamically after connection +enabledCoreTools.forEach((tool) => { tool.installTo(server); }); @@ -210,6 +223,66 @@ async function main() { // Start receiving messages on stdin and sending messages on stdout const transport = new StdioServerTransport(); await server.connect(transport); + + // After connection, dynamically register capability-dependent tools + const clientCapabilities = server.server.getClientCapabilities(); + let toolsAdded = false; + + // Register elicitation tools if client supports elicitation + if (clientCapabilities?.elicitation && enabledElicitationTools.length > 0) { + server.server.sendLoggingMessage({ + level: 'info', + data: `Client supports elicitation. Registering ${enabledElicitationTools.length} elicitation-dependent tools` + }); + + enabledElicitationTools.forEach((tool) => { + tool.installTo(server); + }); + toolsAdded = true; + } else if (enabledElicitationTools.length > 0) { + server.server.sendLoggingMessage({ + level: 'debug', + data: `Client does not support elicitation. Skipping ${enabledElicitationTools.length} elicitation-dependent tools` + }); + } + + // Register resource fallback tools if client doesn't properly support resources + // Note: GetReferenceTool exists as a workaround for Claude Desktop which lists resources + // but doesn't automatically fetch them. Most modern clients support resources properly. + const supportsResources = clientCapabilities?.resources !== undefined; + if (!supportsResources && enabledResourceFallbackTools.length > 0) { + server.server.sendLoggingMessage({ + level: 'info', + data: `Client lacks full resource support. Registering ${enabledResourceFallbackTools.length} resource fallback tools` + }); + + enabledResourceFallbackTools.forEach((tool) => { + tool.installTo(server); + }); + toolsAdded = true; + } else if (enabledResourceFallbackTools.length > 0) { + server.server.sendLoggingMessage({ + level: 'debug', + data: `Client supports resources properly. Skipping ${enabledResourceFallbackTools.length} resource fallback tools` + }); + } + + // Notify client about tool list changes if any tools were added + if (toolsAdded) { + try { + server.sendToolListChanged(); + + server.server.sendLoggingMessage({ + level: 'debug', + data: 'Sent notifications/tools/list_changed to client' + }); + } catch (error) { + server.server.sendLoggingMessage({ + level: 'warning', + data: `Failed to send tool list change notification: ${error instanceof Error ? error.message : String(error)}` + }); + } + } } // Ensure cleanup interval is cleared when the process exits diff --git a/src/tools/toolRegistry.ts b/src/tools/toolRegistry.ts index 61ac7d1..a79c0b9 100644 --- a/src/tools/toolRegistry.ts +++ b/src/tools/toolRegistry.ts @@ -28,14 +28,16 @@ import { ValidateGeojsonTool } from './validate-geojson-tool/ValidateGeojsonTool import { ValidateStyleTool } from './validate-style-tool/ValidateStyleTool.js'; import { httpRequest } from '../utils/httpPipeline.js'; -// Central registry of all tools -export const ALL_TOOLS = [ +/** + * Core tools that work in all MCP clients without requiring special capabilities + * These tools are registered immediately during server startup + */ +export const CORE_TOOLS = [ new ListStylesTool({ httpRequest }), new CreateStyleTool({ httpRequest }), new RetrieveStyleTool({ httpRequest }), new UpdateStyleTool({ httpRequest }), new DeleteStyleTool({ httpRequest }), - new PreviewStyleTool(), new StyleBuilderTool(), new GeojsonPreviewTool(), new CheckColorContrastTool(), @@ -49,20 +51,72 @@ export const ALL_TOOLS = [ new GetFeedbackTool({ httpRequest }), new ListFeedbackTool({ httpRequest }), new GetMapboxDocSourceTool({ httpRequest }), - new GetReferenceTool(), - new StyleComparisonTool(), new TilequeryTool({ httpRequest }), new ValidateExpressionTool(), new ValidateGeojsonTool(), new ValidateStyleTool() ] as const; +/** + * Tools that require elicitation capability for optimal functionality + * These tools use elicitInput() for secure token management + * Registered only if client supports elicitation + */ +export const ELICITATION_TOOLS = [ + new PreviewStyleTool(), + new StyleComparisonTool() +] as const; + +/** + * Tools that serve as bridges/workarounds for missing resource support + * These tools are only registered if client does NOT support resources properly + * + * Context: GetReferenceTool exists as a workaround for Claude Desktop's limitation + * where it can list resources but doesn't automatically fetch them. Clients that + * properly support resources don't need this bridge tool. + */ +export const RESOURCE_FALLBACK_TOOLS = [new GetReferenceTool()] as const; + +/** + * All tools combined (for backward compatibility and testing) + */ +export const ALL_TOOLS = [ + ...CORE_TOOLS, + ...ELICITATION_TOOLS, + ...RESOURCE_FALLBACK_TOOLS +] as const; + export type ToolInstance = (typeof ALL_TOOLS)[number]; +/** + * Get all tools (for backward compatibility) + * @deprecated Use getCoreTools(), getElicitationTools(), etc. instead for capability-aware registration + */ export function getAllTools(): readonly ToolInstance[] { return ALL_TOOLS; } +/** + * Get tools that work in all MCP clients + */ +export function getCoreTools(): readonly ToolInstance[] { + return CORE_TOOLS; +} + +/** + * Get tools that require elicitation capability + */ +export function getElicitationTools(): readonly ToolInstance[] { + return ELICITATION_TOOLS; +} + +/** + * Get tools that serve as fallbacks when client doesn't support resources + */ +export function getResourceFallbackTools(): readonly ToolInstance[] { + return RESOURCE_FALLBACK_TOOLS; +} + export function getToolByName(name: string): ToolInstance | undefined { return ALL_TOOLS.find((tool) => tool.name === name); } From d80c0bb5584de4502ac274a6081256d56c5e8d49 Mon Sep 17 00:00:00 2001 From: Matthew Podwysocki Date: Tue, 27 Jan 2026 12:21:14 -0500 Subject: [PATCH 2/8] 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 --- test/tools/toolRegistry.test.ts | 157 ++++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 test/tools/toolRegistry.test.ts diff --git a/test/tools/toolRegistry.test.ts b/test/tools/toolRegistry.test.ts new file mode 100644 index 0000000..dd94f57 --- /dev/null +++ b/test/tools/toolRegistry.test.ts @@ -0,0 +1,157 @@ +// Copyright (c) Mapbox, Inc. +// Licensed under the MIT License. + +import { describe, it, expect } from 'vitest'; +import { + getCoreTools, + getElicitationTools, + getResourceFallbackTools, + getAllTools +} from '../../src/tools/toolRegistry.js'; + +describe('Tool Registry', () => { + describe('getCoreTools', () => { + it('should return an array of core tools', () => { + const coreTools = getCoreTools(); + expect(Array.isArray(coreTools)).toBe(true); + expect(coreTools.length).toBeGreaterThan(0); + }); + + it('should include expected core tools', () => { + const coreTools = getCoreTools(); + const toolNames = coreTools.map((tool) => tool.name); + + // Verify some expected core tools + expect(toolNames).toContain('list_styles_tool'); + expect(toolNames).toContain('create_style_tool'); + expect(toolNames).toContain('style_builder_tool'); + expect(toolNames).toContain('validate_style_tool'); + }); + + it('should not include elicitation-dependent tools', () => { + const coreTools = getCoreTools(); + const toolNames = coreTools.map((tool) => tool.name); + + // Elicitation tools should not be in core + expect(toolNames).not.toContain('preview_style_tool'); + expect(toolNames).not.toContain('style_comparison_tool'); + }); + + it('should not include resource fallback tools', () => { + const coreTools = getCoreTools(); + const toolNames = coreTools.map((tool) => tool.name); + + // Resource fallback tools should not be in core + expect(toolNames).not.toContain('get_reference_tool'); + }); + }); + + describe('getElicitationTools', () => { + it('should return an array of elicitation tools', () => { + const elicitationTools = getElicitationTools(); + expect(Array.isArray(elicitationTools)).toBe(true); + expect(elicitationTools.length).toBe(2); + }); + + it('should include preview_style_tool', () => { + const elicitationTools = getElicitationTools(); + const toolNames = elicitationTools.map((tool) => tool.name); + expect(toolNames).toContain('preview_style_tool'); + }); + + it('should include style_comparison_tool', () => { + const elicitationTools = getElicitationTools(); + const toolNames = elicitationTools.map((tool) => tool.name); + expect(toolNames).toContain('style_comparison_tool'); + }); + }); + + describe('getResourceFallbackTools', () => { + it('should return an array of resource fallback tools', () => { + const resourceFallbackTools = getResourceFallbackTools(); + expect(Array.isArray(resourceFallbackTools)).toBe(true); + expect(resourceFallbackTools.length).toBe(1); + }); + + it('should include get_reference_tool', () => { + const resourceFallbackTools = getResourceFallbackTools(); + const toolNames = resourceFallbackTools.map((tool) => tool.name); + expect(toolNames).toContain('get_reference_tool'); + }); + }); + + describe('getAllTools', () => { + it('should return all tools combined', () => { + const allTools = getAllTools(); + const coreTools = getCoreTools(); + const elicitationTools = getElicitationTools(); + const resourceFallbackTools = getResourceFallbackTools(); + + expect(allTools.length).toBe( + coreTools.length + + elicitationTools.length + + resourceFallbackTools.length + ); + }); + + it('should have no duplicate tools', () => { + const allTools = getAllTools(); + const toolNames = allTools.map((tool) => tool.name); + const uniqueToolNames = new Set(toolNames); + + expect(toolNames.length).toBe(uniqueToolNames.size); + }); + + it('should include tools from all categories', () => { + const allTools = getAllTools(); + const toolNames = allTools.map((tool) => tool.name); + + // Core tool + expect(toolNames).toContain('list_styles_tool'); + // Elicitation tool + expect(toolNames).toContain('preview_style_tool'); + // Resource fallback tool + expect(toolNames).toContain('get_reference_tool'); + }); + }); + + describe('Tool categorization consistency', () => { + it('should have no overlap between core and elicitation tools', () => { + const coreToolNames = getCoreTools().map((tool) => tool.name); + const elicitationToolNames = getElicitationTools().map( + (tool) => tool.name + ); + + const overlap = coreToolNames.filter((name) => + elicitationToolNames.includes(name) + ); + expect(overlap).toEqual([]); + }); + + it('should have no overlap between core and resource fallback tools', () => { + const coreToolNames = getCoreTools().map((tool) => tool.name); + const resourceFallbackToolNames = getResourceFallbackTools().map( + (tool) => tool.name + ); + + const overlap = coreToolNames.filter((name) => + resourceFallbackToolNames.includes(name) + ); + expect(overlap).toEqual([]); + }); + + it('should have no overlap between elicitation and resource fallback tools', () => { + const elicitationToolNames = getElicitationTools().map( + (tool) => tool.name + ); + const resourceFallbackToolNames = getResourceFallbackTools().map( + (tool) => tool.name + ); + + const overlap = elicitationToolNames.filter((name) => + resourceFallbackToolNames.includes(name) + ); + expect(overlap).toEqual([]); + }); + }); +}); From 5a59eb278108689b1a5e5544bce611acb665b464 Mon Sep 17 00:00:00 2001 From: Matthew Podwysocki Date: Tue, 27 Jan 2026 12:29:46 -0500 Subject: [PATCH 3/8] 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 --- src/tools/toolRegistry.ts | 10 ++++++---- test/tools/toolRegistry.test.ts | 26 +++++++++++++------------- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/src/tools/toolRegistry.ts b/src/tools/toolRegistry.ts index a79c0b9..f3b0d1e 100644 --- a/src/tools/toolRegistry.ts +++ b/src/tools/toolRegistry.ts @@ -38,11 +38,13 @@ export const CORE_TOOLS = [ new RetrieveStyleTool({ httpRequest }), new UpdateStyleTool({ httpRequest }), new DeleteStyleTool({ httpRequest }), + new PreviewStyleTool(), new StyleBuilderTool(), new GeojsonPreviewTool(), new CheckColorContrastTool(), new CompareStylesTool(), new OptimizeStyleTool(), + new StyleComparisonTool(), new CreateTokenTool({ httpRequest }), new ListTokensTool({ httpRequest }), new BoundingBoxTool(), @@ -61,11 +63,11 @@ export const CORE_TOOLS = [ * Tools that require elicitation capability for optimal functionality * These tools use elicitInput() for secure token management * Registered only if client supports elicitation + * + * Currently empty - elicitation support will be added in a future PR. + * This category is ready for tools that require the elicitation capability. */ -export const ELICITATION_TOOLS = [ - new PreviewStyleTool(), - new StyleComparisonTool() -] as const; +export const ELICITATION_TOOLS = [] as const; /** * Tools that serve as bridges/workarounds for missing resource support diff --git a/test/tools/toolRegistry.test.ts b/test/tools/toolRegistry.test.ts index dd94f57..9517242 100644 --- a/test/tools/toolRegistry.test.ts +++ b/test/tools/toolRegistry.test.ts @@ -28,13 +28,14 @@ describe('Tool Registry', () => { expect(toolNames).toContain('validate_style_tool'); }); - it('should not include elicitation-dependent tools', () => { + it('should include preview and comparison tools (until elicitation support is added)', () => { const coreTools = getCoreTools(); const toolNames = coreTools.map((tool) => tool.name); - // Elicitation tools should not be in core - expect(toolNames).not.toContain('preview_style_tool'); - expect(toolNames).not.toContain('style_comparison_tool'); + // These tools are currently in CORE_TOOLS + // They will move to ELICITATION_TOOLS when elicitation support is added + expect(toolNames).toContain('preview_style_tool'); + expect(toolNames).toContain('style_comparison_tool'); }); it('should not include resource fallback tools', () => { @@ -50,19 +51,18 @@ describe('Tool Registry', () => { it('should return an array of elicitation tools', () => { const elicitationTools = getElicitationTools(); expect(Array.isArray(elicitationTools)).toBe(true); - expect(elicitationTools.length).toBe(2); }); - it('should include preview_style_tool', () => { + it('should currently be empty (elicitation support pending)', () => { const elicitationTools = getElicitationTools(); - const toolNames = elicitationTools.map((tool) => tool.name); - expect(toolNames).toContain('preview_style_tool'); + expect(elicitationTools.length).toBe(0); }); - it('should include style_comparison_tool', () => { + it('should be ready for future elicitation-dependent tools', () => { + // This test documents that the infrastructure is in place + // When elicitation support is added, tools can be moved here const elicitationTools = getElicitationTools(); - const toolNames = elicitationTools.map((tool) => tool.name); - expect(toolNames).toContain('style_comparison_tool'); + expect(Array.isArray(elicitationTools)).toBe(true); }); }); @@ -106,12 +106,12 @@ describe('Tool Registry', () => { const allTools = getAllTools(); const toolNames = allTools.map((tool) => tool.name); - // Core tool + // Core tools expect(toolNames).toContain('list_styles_tool'); - // Elicitation tool expect(toolNames).toContain('preview_style_tool'); // Resource fallback tool expect(toolNames).toContain('get_reference_tool'); + // Note: No elicitation tools yet (empty array) }); }); From 0b57cb9c2f2b416ed349e86d553551a076b62730 Mon Sep 17 00:00:00 2001 From: Matthew Podwysocki Date: Tue, 27 Jan 2026 12:43:09 -0500 Subject: [PATCH 4/8] 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 --- src/tools/toolRegistry.ts | 15 ++++++++++----- test/tools/toolRegistry.test.ts | 12 ++++++++++-- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/tools/toolRegistry.ts b/src/tools/toolRegistry.ts index f3b0d1e..1c1b417 100644 --- a/src/tools/toolRegistry.ts +++ b/src/tools/toolRegistry.ts @@ -52,7 +52,6 @@ export const CORE_TOOLS = [ new CoordinateConversionTool(), new GetFeedbackTool({ httpRequest }), new ListFeedbackTool({ httpRequest }), - new GetMapboxDocSourceTool({ httpRequest }), new TilequeryTool({ httpRequest }), new ValidateExpressionTool(), new ValidateGeojsonTool(), @@ -73,11 +72,17 @@ export const ELICITATION_TOOLS = [] as const; * Tools that serve as bridges/workarounds for missing resource support * These tools are only registered if client does NOT support resources properly * - * Context: GetReferenceTool exists as a workaround for Claude Desktop's limitation - * where it can list resources but doesn't automatically fetch them. Clients that - * properly support resources don't need this bridge tool. + * Context: These tools exist as workarounds for clients (like Claude Desktop) that + * can list resources but don't automatically fetch them. Clients that properly + * support resources don't need these bridge tools. + * + * - GetReferenceTool: Provides access to reference resources (style layers, Streets v8 fields, token scopes, layer type mapping) + * - GetMapboxDocSourceTool: Provides access to Mapbox documentation (resource://mapbox-documentation) */ -export const RESOURCE_FALLBACK_TOOLS = [new GetReferenceTool()] as const; +export const RESOURCE_FALLBACK_TOOLS = [ + new GetReferenceTool(), + new GetMapboxDocSourceTool({ httpRequest }) +] as const; /** * All tools combined (for backward compatibility and testing) diff --git a/test/tools/toolRegistry.test.ts b/test/tools/toolRegistry.test.ts index 9517242..0acc821 100644 --- a/test/tools/toolRegistry.test.ts +++ b/test/tools/toolRegistry.test.ts @@ -44,6 +44,7 @@ describe('Tool Registry', () => { // Resource fallback tools should not be in core expect(toolNames).not.toContain('get_reference_tool'); + expect(toolNames).not.toContain('get_latest_mapbox_docs_tool'); }); }); @@ -70,7 +71,7 @@ describe('Tool Registry', () => { it('should return an array of resource fallback tools', () => { const resourceFallbackTools = getResourceFallbackTools(); expect(Array.isArray(resourceFallbackTools)).toBe(true); - expect(resourceFallbackTools.length).toBe(1); + expect(resourceFallbackTools.length).toBe(2); }); it('should include get_reference_tool', () => { @@ -78,6 +79,12 @@ describe('Tool Registry', () => { const toolNames = resourceFallbackTools.map((tool) => tool.name); expect(toolNames).toContain('get_reference_tool'); }); + + it('should include get_latest_mapbox_docs_tool', () => { + const resourceFallbackTools = getResourceFallbackTools(); + const toolNames = resourceFallbackTools.map((tool) => tool.name); + expect(toolNames).toContain('get_latest_mapbox_docs_tool'); + }); }); describe('getAllTools', () => { @@ -109,8 +116,9 @@ describe('Tool Registry', () => { // Core tools expect(toolNames).toContain('list_styles_tool'); expect(toolNames).toContain('preview_style_tool'); - // Resource fallback tool + // Resource fallback tools expect(toolNames).toContain('get_reference_tool'); + expect(toolNames).toContain('get_latest_mapbox_docs_tool'); // Note: No elicitation tools yet (empty array) }); }); From b29c23f1db73f62263e8343b30616856d0a355ba Mon Sep 17 00:00:00 2001 From: Matthew Podwysocki Date: Tue, 27 Jan 2026 12:57:59 -0500 Subject: [PATCH 5/8] 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. --- src/index.ts | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/src/index.ts b/src/index.ts index 18683d7..6b258ed 100644 --- a/src/index.ts +++ b/src/index.ts @@ -226,6 +226,13 @@ async function main() { // After connection, dynamically register capability-dependent tools const clientCapabilities = server.server.getClientCapabilities(); + + // Debug: Log what capabilities we detected + server.server.sendLoggingMessage({ + level: 'info', + data: `Client capabilities detected: ${JSON.stringify(clientCapabilities, null, 2)}` + }); + let toolsAdded = false; // Register elicitation tools if client supports elicitation @@ -246,14 +253,20 @@ async function main() { }); } - // Register resource fallback tools if client doesn't properly support resources - // Note: GetReferenceTool exists as a workaround for Claude Desktop which lists resources - // but doesn't automatically fetch them. Most modern clients support resources properly. - const supportsResources = clientCapabilities?.resources !== undefined; - if (!supportsResources && enabledResourceFallbackTools.length > 0) { + // Register resource fallback tools for clients with known resource support issues + // Note: Resources are a core MCP feature, but some clients (like Claude Desktop) can list + // resources but don't automatically fetch them. We detect these clients by name. + // Most modern MCP clients (Inspector, VS Code, etc.) support resources properly. + const clientVersion = server.server.getClientVersion(); + const clientName = clientVersion?.name?.toLowerCase() || ''; + + // Known clients with resource support issues + const needsResourceFallback = clientName.includes('claude'); + + if (needsResourceFallback && enabledResourceFallbackTools.length > 0) { server.server.sendLoggingMessage({ level: 'info', - data: `Client lacks full resource support. Registering ${enabledResourceFallbackTools.length} resource fallback tools` + data: `Client "${clientVersion?.name}" has known resource issues. Registering ${enabledResourceFallbackTools.length} resource fallback tools` }); enabledResourceFallbackTools.forEach((tool) => { @@ -263,7 +276,7 @@ async function main() { } else if (enabledResourceFallbackTools.length > 0) { server.server.sendLoggingMessage({ level: 'debug', - data: `Client supports resources properly. Skipping ${enabledResourceFallbackTools.length} resource fallback tools` + data: `Client "${clientVersion?.name}" supports resources properly. Skipping ${enabledResourceFallbackTools.length} resource fallback tools` }); } From 5e89269577617deb08ae9e46502c31ac8da5c4a0 Mon Sep 17 00:00:00 2001 From: Matthew Podwysocki Date: Wed, 28 Jan 2026 10:29:54 -0500 Subject: [PATCH 6/8] 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). --- src/index.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/index.ts b/src/index.ts index 6b258ed..e68edf4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -253,20 +253,21 @@ async function main() { }); } - // Register resource fallback tools for clients with known resource support issues - // Note: Resources are a core MCP feature, but some clients (like Claude Desktop) can list - // resources but don't automatically fetch them. We detect these clients by name. - // Most modern MCP clients (Inspector, VS Code, etc.) support resources properly. + // Register resource fallback tools for clients that may not support resources properly + // Note: Resources are a core MCP feature, but not all clients support them perfectly. + // We use an allowlist approach: only skip fallback tools for clients we KNOW support resources. + // This is safer than blocklisting - unknown clients get fallback tools by default. const clientVersion = server.server.getClientVersion(); const clientName = clientVersion?.name?.toLowerCase() || ''; - // Known clients with resource support issues - const needsResourceFallback = clientName.includes('claude'); + // Known clients with proper resource support (can skip fallback tools) + const supportsResourcesProperly = + clientName.includes('inspector') || clientName.includes('vscode'); - if (needsResourceFallback && enabledResourceFallbackTools.length > 0) { + if (!supportsResourcesProperly && enabledResourceFallbackTools.length > 0) { server.server.sendLoggingMessage({ level: 'info', - data: `Client "${clientVersion?.name}" has known resource issues. Registering ${enabledResourceFallbackTools.length} resource fallback tools` + data: `Client "${clientVersion?.name}" may need resource fallback tools. Registering ${enabledResourceFallbackTools.length} resource fallback tools` }); enabledResourceFallbackTools.forEach((tool) => { From 185e067b601d3968589adb023c44c74201ab58db Mon Sep 17 00:00:00 2001 From: Matthew Podwysocki Date: Wed, 28 Jan 2026 10:32:13 -0500 Subject: [PATCH 7/8] 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). --- src/index.ts | 19 +++++++++++-------- src/tools/toolRegistry.ts | 11 ++++++----- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/index.ts b/src/index.ts index e68edf4..0bb46d8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -253,21 +253,24 @@ async function main() { }); } - // Register resource fallback tools for clients that may not support resources properly - // Note: Resources are a core MCP feature, but not all clients support them perfectly. + // Register resource fallback tools for clients that don't support resources + // Note: Resources are a core MCP feature, but some clients (like smolagents) don't support them. + // These fallback tools provide the same content as resources but via tool calls instead. // We use an allowlist approach: only skip fallback tools for clients we KNOW support resources. // This is safer than blocklisting - unknown clients get fallback tools by default. const clientVersion = server.server.getClientVersion(); const clientName = clientVersion?.name?.toLowerCase() || ''; - // Known clients with proper resource support (can skip fallback tools) - const supportsResourcesProperly = - clientName.includes('inspector') || clientName.includes('vscode'); + // Known clients with resource support (can skip fallback tools) + const supportsResources = + clientName.includes('inspector') || + clientName.includes('vscode') || + clientName.includes('claude'); - if (!supportsResourcesProperly && enabledResourceFallbackTools.length > 0) { + if (!supportsResources && enabledResourceFallbackTools.length > 0) { server.server.sendLoggingMessage({ level: 'info', - data: `Client "${clientVersion?.name}" may need resource fallback tools. Registering ${enabledResourceFallbackTools.length} resource fallback tools` + data: `Client "${clientVersion?.name}" does not support resources. Registering ${enabledResourceFallbackTools.length} resource fallback tools` }); enabledResourceFallbackTools.forEach((tool) => { @@ -277,7 +280,7 @@ async function main() { } else if (enabledResourceFallbackTools.length > 0) { server.server.sendLoggingMessage({ level: 'debug', - data: `Client "${clientVersion?.name}" supports resources properly. Skipping ${enabledResourceFallbackTools.length} resource fallback tools` + data: `Client "${clientVersion?.name}" supports resources. Skipping ${enabledResourceFallbackTools.length} resource fallback tools` }); } diff --git a/src/tools/toolRegistry.ts b/src/tools/toolRegistry.ts index 1c1b417..38aa085 100644 --- a/src/tools/toolRegistry.ts +++ b/src/tools/toolRegistry.ts @@ -69,12 +69,13 @@ export const CORE_TOOLS = [ export const ELICITATION_TOOLS = [] as const; /** - * Tools that serve as bridges/workarounds for missing resource support - * These tools are only registered if client does NOT support resources properly + * Tools that serve as bridges for clients without resource support + * These tools are only registered if client does NOT support resources * - * Context: These tools exist as workarounds for clients (like Claude Desktop) that - * can list resources but don't automatically fetch them. Clients that properly - * support resources don't need these bridge tools. + * Context: Some MCP clients (like smolagents) don't support resources at all. + * These tools provide the same content as resources but via tool calls instead, + * allowing these clients to access reference data and documentation. + * Clients that support resources (Claude Desktop, VS Code, Inspector) don't need these. * * - GetReferenceTool: Provides access to reference resources (style layers, Streets v8 fields, token scopes, layer type mapping) * - GetMapboxDocSourceTool: Provides access to Mapbox documentation (resource://mapbox-documentation) From 638569dc341754938c716219fabc9e9ce84cb6eb Mon Sep 17 00:00:00 2001 From: Matthew Podwysocki Date: Wed, 28 Jan 2026 10:38:19 -0500 Subject: [PATCH 8/8] 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). --- README.md | 23 +++++++++++++++++++++++ src/index.ts | 27 ++++++++++++--------------- src/tools/toolRegistry.ts | 14 +++++++++----- 3 files changed, 44 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 8d7a47e..85143d6 100644 --- a/README.md +++ b/README.md @@ -1304,6 +1304,29 @@ node dist/esm/index.js --disable-mcp-ui **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. +#### CLIENT_NEEDS_RESOURCE_FALLBACK + +**Resource Fallback Tools (Opt-In for Non-Compliant Clients)** + +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. + +**Fallback Tools:** + +- `get_reference_tool` - Access to style layers, Streets v8 fields, token scopes, layer type mapping +- `get_latest_mapbox_docs_tool` - Access to Mapbox documentation + +**By default, these tools are NOT included** (assumes your client supports resources). If your client doesn't support resources, enable the fallback tools: + +```bash +export CLIENT_NEEDS_RESOURCE_FALLBACK=true +``` + +**When to set this:** + +- ✅ Set to `true` if using smolagents or other clients without resource support +- ❌ Leave unset (default) if using Claude Desktop, VS Code, MCP Inspector, or any resource-capable client +- ❌ Leave unset if unsure (most clients support resources) + ## Troubleshooting **Issue:** Tools fail with authentication errors diff --git a/src/index.ts b/src/index.ts index 0bb46d8..627b1a1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -254,23 +254,20 @@ async function main() { } // Register resource fallback tools for clients that don't support resources - // Note: Resources are a core MCP feature, but some clients (like smolagents) don't support them. + // Note: Resources are a core MCP feature supported by most clients. + // However, some clients (like smolagents) don't support resources at all. // These fallback tools provide the same content as resources but via tool calls instead. - // We use an allowlist approach: only skip fallback tools for clients we KNOW support resources. - // This is safer than blocklisting - unknown clients get fallback tools by default. - const clientVersion = server.server.getClientVersion(); - const clientName = clientVersion?.name?.toLowerCase() || ''; - - // Known clients with resource support (can skip fallback tools) - const supportsResources = - clientName.includes('inspector') || - clientName.includes('vscode') || - clientName.includes('claude'); - - if (!supportsResources && enabledResourceFallbackTools.length > 0) { + // + // Configuration via CLIENT_NEEDS_RESOURCE_FALLBACK environment variable: + // - unset (default) = Skip fallback tools (assume client supports resources) + // - "true" = Provide fallback tools (client does NOT support resources) + const clientNeedsResourceFallback = + process.env.CLIENT_NEEDS_RESOURCE_FALLBACK?.toLowerCase() === 'true'; + + if (clientNeedsResourceFallback && enabledResourceFallbackTools.length > 0) { server.server.sendLoggingMessage({ level: 'info', - data: `Client "${clientVersion?.name}" does not support resources. Registering ${enabledResourceFallbackTools.length} resource fallback tools` + data: `CLIENT_NEEDS_RESOURCE_FALLBACK=true. Registering ${enabledResourceFallbackTools.length} resource fallback tools` }); enabledResourceFallbackTools.forEach((tool) => { @@ -280,7 +277,7 @@ async function main() { } else if (enabledResourceFallbackTools.length > 0) { server.server.sendLoggingMessage({ level: 'debug', - data: `Client "${clientVersion?.name}" supports resources. Skipping ${enabledResourceFallbackTools.length} resource fallback tools` + data: `CLIENT_NEEDS_RESOURCE_FALLBACK not set or false. Skipping ${enabledResourceFallbackTools.length} resource fallback tools (client supports resources)` }); } diff --git a/src/tools/toolRegistry.ts b/src/tools/toolRegistry.ts index 38aa085..481ea7e 100644 --- a/src/tools/toolRegistry.ts +++ b/src/tools/toolRegistry.ts @@ -70,13 +70,17 @@ export const ELICITATION_TOOLS = [] as const; /** * Tools that serve as bridges for clients without resource support - * These tools are only registered if client does NOT support resources + * These tools are only registered if CLIENT_NEEDS_RESOURCE_FALLBACK env var is set to "true" * - * Context: Some MCP clients (like smolagents) don't support resources at all. - * These tools provide the same content as resources but via tool calls instead, - * allowing these clients to access reference data and documentation. - * Clients that support resources (Claude Desktop, VS Code, Inspector) don't need these. + * Context: Most MCP clients support resources (Claude Desktop, VS Code, Inspector, etc.). + * However, some clients (like smolagents) don't support resources at all. + * These tools provide the same content as resources but via tool calls instead. * + * Configuration: + * - Leave unset (default) = Skip these tools (assumes client supports resources) + * - Set CLIENT_NEEDS_RESOURCE_FALLBACK=true = Include these tools (for smolagents, etc.) + * + * Tools: * - GetReferenceTool: Provides access to reference resources (style layers, Streets v8 fields, token scopes, layer type mapping) * - GetMapboxDocSourceTool: Provides access to Mapbox documentation (resource://mapbox-documentation) */