Skip to content

Commit 79eb017

Browse files
feat(mcp): add option to infer mcp client
1 parent dedddc8 commit 79eb017

File tree

7 files changed

+78
-100
lines changed

7 files changed

+78
-100
lines changed

packages/mcp-server/src/compat.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,13 @@ export const defaultClientCapabilities: ClientCapabilities = {
2020
toolNameLength: undefined,
2121
};
2222

23-
export const ClientType = z.enum(['openai-agents', 'claude', 'claude-code', 'cursor']);
23+
export const ClientType = z.enum(['openai-agents', 'claude', 'claude-code', 'cursor', 'infer']);
2424
export type ClientType = z.infer<typeof ClientType>;
2525

2626
// Client presets for compatibility
2727
// Note that these could change over time as models get better, so this is
2828
// a best effort.
29-
export const knownClients: Record<ClientType, ClientCapabilities> = {
29+
export const knownClients: Record<Exclude<ClientType, 'infer'>, ClientCapabilities> = {
3030
'openai-agents': {
3131
topLevelUnions: false,
3232
validJson: true,

packages/mcp-server/src/http.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import { fromError } from 'zod-validation-error/v3';
88
import { McpOptions, parseQueryOptions } from './options';
99
import { initMcpServer, newMcpServer } from './server';
1010
import { parseAuthHeaders } from './headers';
11-
import { Endpoint } from './tools';
1211

1312
const newServer = (
1413
defaultMcpOptions: McpOptions,
@@ -101,11 +100,7 @@ export const streamableHTTPApp = (options: McpOptions): express.Express => {
101100
return app;
102101
};
103102

104-
export const launchStreamableHTTPServer = async (
105-
options: McpOptions,
106-
endpoints: Endpoint[],
107-
port: number | string | undefined,
108-
) => {
103+
export const launchStreamableHTTPServer = async (options: McpOptions, port: number | string | undefined) => {
109104
const app = streamableHTTPApp(options);
110105
const server = app.listen(port);
111106
const address = server.address();

packages/mcp-server/src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,10 @@ async function main() {
2323

2424
switch (options.transport) {
2525
case 'stdio':
26-
await launchStdioServer(options, selectedTools);
26+
await launchStdioServer(options);
2727
break;
2828
case 'http':
29-
await launchStreamableHTTPServer(options, selectedTools, options.port ?? options.socket);
29+
await launchStreamableHTTPServer(options, options.port ?? options.socket);
3030
break;
3131
}
3232
}

packages/mcp-server/src/options.ts

Lines changed: 16 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@ export type CLIOptions = McpOptions & {
1313
};
1414

1515
export type McpOptions = {
16-
client: ClientType | undefined;
17-
includeDynamicTools: boolean | undefined;
18-
includeAllTools: boolean | undefined;
19-
includeCodeTools: boolean | undefined;
20-
filters: Filter[];
21-
capabilities?: Partial<ClientCapabilities>;
16+
client?: ClientType | undefined;
17+
includeDynamicTools?: boolean | undefined;
18+
includeAllTools?: boolean | undefined;
19+
includeCodeTools?: boolean | undefined;
20+
filters?: Filter[] | undefined;
21+
capabilities?: Partial<ClientCapabilities> | undefined;
2222
};
2323

2424
const CAPABILITY_CHOICES = [
@@ -204,14 +204,7 @@ export function parseCLIOptions(): CLIOptions {
204204
}
205205

206206
// Parse client capabilities
207-
const clientCapabilities: ClientCapabilities = {
208-
topLevelUnions: true,
209-
validJson: true,
210-
refs: true,
211-
unions: true,
212-
formats: true,
213-
toolNameLength: undefined,
214-
};
207+
const clientCapabilities: Partial<ClientCapabilities> = {};
215208

216209
// Apply individual capability overrides
217210
if (Array.isArray(argv.capability)) {
@@ -264,7 +257,7 @@ export function parseCLIOptions(): CLIOptions {
264257

265258
const client = argv.client as ClientType;
266259
return {
267-
client: client && knownClients[client] ? client : undefined,
260+
client: client && client !== 'infer' && knownClients[client] ? client : undefined,
268261
includeDynamicTools,
269262
includeAllTools,
270263
includeCodeTools,
@@ -310,7 +303,7 @@ export function parseQueryOptions(defaultOptions: McpOptions, query: unknown): M
310303
const queryObject = typeof query === 'string' ? qs.parse(query) : query;
311304
const queryOptions = QueryOptions.parse(queryObject);
312305

313-
const filters: Filter[] = [...defaultOptions.filters];
306+
const filters: Filter[] = [...(defaultOptions.filters ?? [])];
314307

315308
for (const resource of queryOptions.resource || []) {
316309
filters.push({ type: 'resource', op: 'include', value: resource });
@@ -338,15 +331,7 @@ export function parseQueryOptions(defaultOptions: McpOptions, query: unknown): M
338331
}
339332

340333
// Parse client capabilities
341-
const clientCapabilities: ClientCapabilities = {
342-
topLevelUnions: true,
343-
validJson: true,
344-
refs: true,
345-
unions: true,
346-
formats: true,
347-
toolNameLength: undefined,
348-
...defaultOptions.capabilities,
349-
};
334+
const clientCapabilities: Partial<ClientCapabilities> = { ...defaultOptions.capabilities };
350335

351336
for (const cap of queryOptions.capability || []) {
352337
const parsed = parseCapabilityValue(cap);
@@ -384,12 +369,13 @@ export function parseQueryOptions(defaultOptions: McpOptions, query: unknown): M
384369
return {
385370
client: queryOptions.client ?? defaultOptions.client,
386371
includeDynamicTools:
387-
defaultOptions.includeDynamicTools ??
388-
(queryOptions.tools?.includes('dynamic') && !queryOptions.no_tools?.includes('dynamic')),
372+
defaultOptions.includeDynamicTools === false ?
373+
false
374+
: queryOptions.tools?.includes('dynamic') && !queryOptions.no_tools?.includes('dynamic'),
389375
includeAllTools:
390-
defaultOptions.includeAllTools ??
391-
(queryOptions.tools?.includes('all') && !queryOptions.no_tools?.includes('all')),
392-
// Never include code tools on remote server.
376+
defaultOptions.includeAllTools === false ?
377+
false
378+
: queryOptions.tools?.includes('all') && !queryOptions.no_tools?.includes('all'),
393379
includeCodeTools: undefined,
394380
filters,
395381
capabilities: clientCapabilities,

packages/mcp-server/src/server.ts

Lines changed: 51 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@
33
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
44
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
55
import { Endpoint, endpoints, HandlerFunction, query } from './tools';
6-
import { CallToolRequestSchema, ListToolsRequestSchema, Tool } from '@modelcontextprotocol/sdk/types.js';
6+
import {
7+
CallToolRequestSchema,
8+
Implementation,
9+
ListToolsRequestSchema,
10+
Tool,
11+
} from '@modelcontextprotocol/sdk/types.js';
712
import { ClientOptions } from '@flowglad/node';
813
import Flowglad from '@flowglad/node';
914
import {
@@ -41,29 +46,29 @@ export const server = newMcpServer();
4146
*/
4247
export function initMcpServer(params: {
4348
server: Server | McpServer;
44-
clientOptions: ClientOptions;
45-
mcpOptions: McpOptions;
46-
endpoints?: { tool: Tool; handler: HandlerFunction }[];
47-
}) {
48-
const transformedEndpoints = selectTools(endpoints, params.mcpOptions);
49-
const client = new Flowglad(params.clientOptions);
50-
const capabilities = {
51-
...defaultClientCapabilities,
52-
...(params.mcpOptions.client ? knownClients[params.mcpOptions.client] : params.mcpOptions.capabilities),
53-
};
54-
init({ server: params.server, client, endpoints: transformedEndpoints, capabilities });
55-
}
56-
57-
export function init(params: {
58-
server: Server | McpServer;
59-
client?: Flowglad;
60-
endpoints?: { tool: Tool; handler: HandlerFunction }[];
61-
capabilities?: Partial<ClientCapabilities>;
49+
clientOptions?: ClientOptions;
50+
mcpOptions?: McpOptions;
6251
}) {
6352
const server = params.server instanceof McpServer ? params.server.server : params.server;
64-
const providedEndpoints = params.endpoints || endpoints;
65-
66-
const endpointMap = Object.fromEntries(providedEndpoints.map((endpoint) => [endpoint.tool.name, endpoint]));
53+
const mcpOptions = params.mcpOptions ?? {};
54+
55+
let providedEndpoints: Endpoint[] | null = null;
56+
let endpointMap: Record<string, Endpoint> | null = null;
57+
58+
const initTools = (implementation?: Implementation) => {
59+
if (implementation && (!mcpOptions.client || mcpOptions.client === 'infer')) {
60+
mcpOptions.client =
61+
implementation.name.toLowerCase().includes('claude') ? 'claude'
62+
: implementation.name.toLowerCase().includes('cursor') ? 'cursor'
63+
: undefined;
64+
mcpOptions.capabilities = {
65+
...(mcpOptions.client && knownClients[mcpOptions.client]),
66+
...mcpOptions.capabilities,
67+
};
68+
}
69+
providedEndpoints = selectTools(endpoints, mcpOptions);
70+
endpointMap = Object.fromEntries(providedEndpoints.map((endpoint) => [endpoint.tool.name, endpoint]));
71+
};
6772

6873
const logAtLevel =
6974
(level: 'debug' | 'info' | 'warning' | 'error') =>
@@ -80,51 +85,63 @@ export function init(params: {
8085
error: logAtLevel('error'),
8186
};
8287

83-
const client =
84-
params.client || new Flowglad({ defaultHeaders: { 'X-Stainless-MCP': 'true' }, logger: logger });
88+
const client = new Flowglad({
89+
logger,
90+
...params.clientOptions,
91+
defaultHeaders: {
92+
...params.clientOptions?.defaultHeaders,
93+
'X-Stainless-MCP': 'true',
94+
},
95+
});
8596

8697
server.setRequestHandler(ListToolsRequestSchema, async () => {
98+
if (providedEndpoints === null) {
99+
initTools(server.getClientVersion());
100+
}
87101
return {
88-
tools: providedEndpoints.map((endpoint) => endpoint.tool),
102+
tools: providedEndpoints!.map((endpoint) => endpoint.tool),
89103
};
90104
});
91105

92106
server.setRequestHandler(CallToolRequestSchema, async (request) => {
107+
if (endpointMap === null) {
108+
initTools(server.getClientVersion());
109+
}
93110
const { name, arguments: args } = request.params;
94-
const endpoint = endpointMap[name];
111+
const endpoint = endpointMap![name];
95112
if (!endpoint) {
96113
throw new Error(`Unknown tool: ${name}`);
97114
}
98115

99-
return executeHandler(endpoint.tool, endpoint.handler, client, args, params.capabilities);
116+
return executeHandler(endpoint.tool, endpoint.handler, client, args, mcpOptions.capabilities);
100117
});
101118
}
102119

103120
/**
104121
* Selects the tools to include in the MCP Server based on the provided options.
105122
*/
106-
export function selectTools(endpoints: Endpoint[], options: McpOptions): Endpoint[] {
107-
const filteredEndpoints = query(options.filters, endpoints);
123+
export function selectTools(endpoints: Endpoint[], options?: McpOptions): Endpoint[] {
124+
const filteredEndpoints = query(options?.filters ?? [], endpoints);
108125

109126
let includedTools = filteredEndpoints;
110127

111128
if (includedTools.length > 0) {
112-
if (options.includeDynamicTools) {
129+
if (options?.includeDynamicTools) {
113130
includedTools = dynamicTools(includedTools);
114131
}
115132
} else {
116-
if (options.includeAllTools) {
133+
if (options?.includeAllTools) {
117134
includedTools = endpoints;
118-
} else if (options.includeDynamicTools) {
135+
} else if (options?.includeDynamicTools) {
119136
includedTools = dynamicTools(endpoints);
120-
} else if (options.includeCodeTools) {
137+
} else if (options?.includeCodeTools) {
121138
includedTools = [codeTool()];
122139
} else {
123140
includedTools = endpoints;
124141
}
125142
}
126143

127-
const capabilities = { ...defaultClientCapabilities, ...options.capabilities };
144+
const capabilities = { ...defaultClientCapabilities, ...options?.capabilities };
128145
return applyCompatibilityTransformations(includedTools, capabilities);
129146
}
130147

packages/mcp-server/src/stdio.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
2-
import { init, newMcpServer } from './server';
3-
import { Endpoint } from './tools';
2+
import { initMcpServer, newMcpServer } from './server';
43
import { McpOptions } from './options';
54

6-
export const launchStdioServer = async (options: McpOptions, endpoints: Endpoint[]) => {
5+
export const launchStdioServer = async (options: McpOptions) => {
76
const server = newMcpServer();
87

9-
init({ server, endpoints });
8+
initMcpServer({ server, mcpOptions: options });
109

1110
const transport = new StdioServerTransport();
1211
await server.connect(transport);

packages/mcp-server/tests/options.test.ts

Lines changed: 3 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,7 @@ describe('parseCLIOptions', () => {
2929
{ type: 'operation', op: 'include', value: 'read' },
3030
] as Filter[]);
3131

32-
// Default client capabilities
33-
expect(result.capabilities).toEqual({
34-
topLevelUnions: true,
35-
validJson: true,
36-
refs: true,
37-
unions: true,
38-
formats: true,
39-
toolNameLength: undefined,
40-
});
32+
expect(result.capabilities).toEqual({});
4133

4234
expect(result.list).toBe(false);
4335

@@ -61,14 +53,7 @@ describe('parseCLIOptions', () => {
6153
{ type: 'operation', op: 'exclude', value: 'write' },
6254
] as Filter[]);
6355

64-
expect(result.capabilities).toEqual({
65-
topLevelUnions: true,
66-
validJson: true,
67-
refs: true,
68-
unions: true,
69-
formats: true,
70-
toolNameLength: undefined,
71-
});
56+
expect(result.capabilities).toEqual({});
7257

7358
cleanup();
7459
});
@@ -99,7 +84,6 @@ describe('parseCLIOptions', () => {
9984
validJson: true,
10085
refs: true,
10186
unions: true,
102-
formats: true,
10387
toolNameLength: 40,
10488
});
10589

@@ -150,10 +134,7 @@ describe('parseCLIOptions', () => {
150134
expect(result.capabilities).toEqual({
151135
topLevelUnions: true,
152136
validJson: true,
153-
refs: true,
154137
unions: true,
155-
formats: true,
156-
toolNameLength: undefined,
157138
});
158139

159140
cleanup();
@@ -316,7 +297,7 @@ describe('parseQueryOptions', () => {
316297
]);
317298

318299
expect(result.client).toBe('cursor');
319-
expect(result.includeDynamicTools).toBe(true);
300+
expect(result.includeDynamicTools).toBe(undefined);
320301
});
321302

322303
it('should override client from default options', () => {

0 commit comments

Comments
 (0)