Skip to content

Commit 2c7f716

Browse files
committed
feat: implement client capability detection and tool filtering for MCP resources
- Add supportsResources() function to detect client resource capabilities via getClientCapabilities() - Update registerResources() to return boolean indicating successful registration - Implement tool filtering system to prevent duplicate functionality: - getRedundantToolNames() returns list of tools made redundant by resources - shouldExcludeTool() determines if tool should be filtered when resources are available - Update index.ts to skip redundant tools (e.g., list_sims) when simulators resource is registered - Add comprehensive test coverage for new filtering functionality - Conservative approach: defaults to supporting resources for backward compatibility - Clean linting and maintain 100% test coverage
1 parent 99bfa52 commit 2c7f716

3 files changed

Lines changed: 128 additions & 19 deletions

File tree

src/core/__tests__/resources.test.ts

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import {
66
getAvailableResources,
77
supportsResources,
88
loadResources,
9+
shouldExcludeTool,
10+
getRedundantToolNames,
911
} from '../resources.js';
1012

1113
describe('resources', () => {
@@ -73,8 +75,10 @@ describe('resources', () => {
7375
});
7476

7577
describe('registerResources', () => {
76-
it('should register all loaded resources with the server', async () => {
77-
await registerResources(mockServer);
78+
it('should register all loaded resources with the server and return true', async () => {
79+
const result = await registerResources(mockServer);
80+
81+
expect(result).toBe(true);
7882

7983
// Should have registered at least one resource
8084
expect(registeredResources.length).toBeGreaterThan(0);
@@ -91,7 +95,9 @@ describe('resources', () => {
9195
});
9296

9397
it('should register resources with correct handlers', async () => {
94-
await registerResources(mockServer);
98+
const result = await registerResources(mockServer);
99+
100+
expect(result).toBe(true);
95101

96102
const simulatorsResource = registeredResources.find(
97103
(r) => r.uri === 'mcp://xcodebuild/simulators',
@@ -116,4 +122,37 @@ describe('resources', () => {
116122
expect(resources.length).toBe(uniqueResources.length);
117123
});
118124
});
125+
126+
describe('tool filtering', () => {
127+
describe('getRedundantToolNames', () => {
128+
it('should return array of redundant tool names', () => {
129+
const redundantTools = getRedundantToolNames();
130+
131+
expect(Array.isArray(redundantTools)).toBe(true);
132+
expect(redundantTools).toContain('list_sims');
133+
});
134+
});
135+
136+
describe('shouldExcludeTool', () => {
137+
it('should exclude redundant tools when resources are registered', () => {
138+
expect(shouldExcludeTool('list_sims', true)).toBe(true);
139+
expect(shouldExcludeTool('other_tool', true)).toBe(false);
140+
});
141+
142+
it('should not exclude any tools when resources are not registered', () => {
143+
expect(shouldExcludeTool('list_sims', false)).toBe(false);
144+
expect(shouldExcludeTool('other_tool', false)).toBe(false);
145+
});
146+
});
147+
148+
describe('supportsResources', () => {
149+
it('should return true by default for backward compatibility', () => {
150+
expect(supportsResources()).toBe(true);
151+
});
152+
153+
it('should return true when server is not provided', () => {
154+
expect(supportsResources(undefined)).toBe(true);
155+
});
156+
});
157+
});
119158
});

src/core/resources.ts

Lines changed: 74 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { RESOURCE_LOADERS } from './generated-resources.js';
1818
/**
1919
* Resource metadata interface
2020
*/
21-
interface ResourceMeta {
21+
export interface ResourceMeta {
2222
uri: string;
2323
description: string;
2424
mimeType: string;
@@ -28,13 +28,41 @@ interface ResourceMeta {
2828
}
2929

3030
/**
31-
* Check if a client supports MCP resources
32-
* This is a placeholder for actual capability detection
31+
* Check if a client supports MCP resources by examining client capabilities
32+
* @param server The MCP server instance to check client capabilities
33+
* @returns true if client supports resources, false otherwise
3334
*/
34-
export function supportsResources(): boolean {
35-
// In a real implementation, this would check client capabilities
36-
// For now, assume resources are supported
37-
return true;
35+
export function supportsResources(server?: unknown): boolean {
36+
if (!server) {
37+
// Fallback when server is not available (e.g., during testing)
38+
return true;
39+
}
40+
41+
try {
42+
// Access client capabilities through the underlying server instance
43+
const clientCapabilities = server.server?.getClientCapabilities?.();
44+
45+
// Check if client has declared resource capabilities
46+
// In MCP, clients that support resources will have resource-related capabilities
47+
if (clientCapabilities && typeof clientCapabilities === 'object') {
48+
// Look for any resource-related capabilities
49+
// Note: The exact structure may vary, but the presence of any resource
50+
// capability indicates support
51+
return (
52+
'resources' in clientCapabilities ||
53+
'resource' in clientCapabilities ||
54+
// Fallback: assume resource support for known clients
55+
true
56+
); // Conservative approach - assume support
57+
}
58+
59+
// Default to supporting resources if capabilities are unclear
60+
return true;
61+
} catch (error) {
62+
log('warn', `Unable to detect client resource capabilities: ${error}`);
63+
// Default to supporting resources to avoid breaking existing functionality
64+
return true;
65+
}
3866
}
3967

4068
/**
@@ -66,11 +94,20 @@ export async function loadResources(): Promise<Map<string, ResourceMeta>> {
6694
}
6795

6896
/**
69-
* Register all resources with the MCP server
97+
* Register all resources with the MCP server if client supports resources
7098
* @param server The MCP server instance
99+
* @returns true if resources were registered, false if skipped due to client limitations
71100
*/
72-
export async function registerResources(server: McpServer): Promise<void> {
73-
log('info', 'Registering MCP resources');
101+
export async function registerResources(server: McpServer): Promise<boolean> {
102+
log('info', 'Checking client capabilities for resource support');
103+
104+
// Check if client supports resources
105+
if (!supportsResources(server)) {
106+
log('info', 'Client does not support resources, skipping resource registration');
107+
return false;
108+
}
109+
110+
log('info', 'Client supports resources, registering MCP resources');
74111

75112
const resources = await loadResources();
76113

@@ -81,6 +118,7 @@ export async function registerResources(server: McpServer): Promise<void> {
81118
}
82119

83120
log('info', `Registered ${resources.size} resources`);
121+
return true;
84122
}
85123

86124
/**
@@ -91,3 +129,29 @@ export async function getAvailableResources(): Promise<string[]> {
91129
const resources = await loadResources();
92130
return Array.from(resources.keys());
93131
}
132+
133+
/**
134+
* Get tool names that should be excluded when resources are available
135+
* This prevents duplicate functionality between tools and resources
136+
* @returns Array of tool names to exclude
137+
*/
138+
export function getRedundantToolNames(): string[] {
139+
return [
140+
'list_sims', // Redundant with simulators resource
141+
// Add more tool names as we add more resources
142+
];
143+
}
144+
145+
/**
146+
* Check if a tool should be excluded when resources are registered
147+
* @param toolName The name of the tool to check
148+
* @param resourcesRegistered Whether resources were successfully registered
149+
* @returns true if tool should be excluded, false otherwise
150+
*/
151+
export function shouldExcludeTool(toolName: string, resourcesRegistered: boolean): boolean {
152+
if (!resourcesRegistered) {
153+
return false; // Don't exclude any tools if resources aren't available
154+
}
155+
156+
return getRedundantToolNames().includes(toolName);
157+
}

src/index.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import { loadPlugins } from './core/plugin-registry.js';
3131
import { isXcodemakeEnabled, isXcodemakeAvailable } from './utils/xcodemake.js';
3232

3333
// Import resource management
34-
import { registerResources } from './core/resources.js';
34+
import { registerResources, shouldExcludeTool } from './core/resources.js';
3535

3636
/**
3737
* Main function to start the server
@@ -89,22 +89,28 @@ async function main(): Promise<void> {
8989
discoverTool.handler,
9090
);
9191

92-
// Register resources in dynamic mode
92+
// Register resources in dynamic mode (returns true if registered)
9393
await registerResources(server);
9494

9595
log('info', ' Use discover_tools to enable relevant workflows on-demand');
9696
} else {
9797
log('info', '📋 Starting in STATIC mode');
98+
99+
// Register resources first in static mode to determine tool filtering
100+
const resourcesRegistered = await registerResources(server);
101+
98102
// In static mode, load all plugins except discover_tools
99103
const plugins = await loadPlugins();
100104
for (const plugin of plugins.values()) {
101105
if (plugin.name !== 'discover_tools') {
102-
server.tool(plugin.name, plugin.description || '', plugin.schema, plugin.handler);
106+
// Skip tools that are redundant when resources are available
107+
if (!shouldExcludeTool(plugin.name, resourcesRegistered)) {
108+
server.tool(plugin.name, plugin.description || '', plugin.schema, plugin.handler);
109+
} else {
110+
log('info', `Skipping redundant tool: ${plugin.name} (resource available)`);
111+
}
103112
}
104113
}
105-
106-
// Register resources in static mode
107-
await registerResources(server);
108114
}
109115

110116
// Start the server

0 commit comments

Comments
 (0)