Skip to content

Commit d57ad3c

Browse files
feat(mcp): parse query string as mcp client options in mcp server
1 parent 5f7ef32 commit d57ad3c

File tree

8 files changed

+499
-32
lines changed

8 files changed

+499
-32
lines changed

packages/mcp-server/README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,19 @@ A configuration JSON for this server might look like this, assuming the server i
152152
}
153153
```
154154

155+
The command-line arguments for filtering tools and specifying clients can also be used as query parameters in the URL.
156+
For example, to exclude specific tools while including others, use the URL:
157+
158+
```
159+
http://localhost:3000?resource=cards&resource=accounts&no_tool=create_cards
160+
```
161+
162+
Or, to configure for the Cursor client, with a custom max tool name length, use the URL:
163+
164+
```
165+
http://localhost:3000?client=cursor&capability=tool-name-length%3D40
166+
```
167+
155168
## Importing the tools and server individually
156169

157170
```js

packages/mcp-server/package.json

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,20 +28,22 @@
2828
},
2929
"dependencies": {
3030
"isaacus": "file:../../dist/",
31+
"@cloudflare/cabidela": "^0.2.4",
3132
"@modelcontextprotocol/sdk": "^1.11.5",
3233
"express": "^5.1.0",
3334
"jq-web": "https://github.com/stainless-api/jq-web/releases/download/v0.8.6/jq-web.tar.gz",
35+
"qs": "^6.14.0",
3436
"yargs": "^17.7.2",
35-
"@cloudflare/cabidela": "^0.2.4",
3637
"zod": "^3.25.20",
37-
"zod-to-json-schema": "^3.24.5"
38+
"zod-to-json-schema": "^3.24.5",
39+
"zod-validation-error": "^4.0.1"
3840
},
3941
"bin": {
4042
"mcp-server": "dist/index.js"
4143
},
4244
"devDependencies": {
43-
"@types/jest": "^29.4.0",
4445
"@types/express": "^5.0.3",
46+
"@types/jest": "^29.4.0",
4547
"@types/yargs": "^17.0.8",
4648
"@typescript-eslint/eslint-plugin": "8.31.1",
4749
"@typescript-eslint/parser": "8.31.1",

packages/mcp-server/src/compat.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Tool } from '@modelcontextprotocol/sdk/types.js';
2+
import { z } from 'zod';
23
import { Endpoint } from './tools';
34

45
export interface ClientCapabilities {
@@ -19,7 +20,8 @@ export const defaultClientCapabilities: ClientCapabilities = {
1920
toolNameLength: undefined,
2021
};
2122

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

2426
// Client presets for compatibility
2527
// Note that these could change over time as models get better, so this is

packages/mcp-server/src/http.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,33 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp';
44
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
55

66
import express from 'express';
7-
import { McpOptions } from './options';
7+
import { fromError } from 'zod-validation-error/v3';
8+
import { McpOptions, parseQueryOptions } from './options';
89
import { initMcpServer, newMcpServer } from './server';
910
import { parseAuthHeaders } from './headers';
1011
import { Endpoint } from './tools';
1112

12-
const newServer = (mcpOptions: McpOptions, req: express.Request, res: express.Response): McpServer | null => {
13+
const newServer = (
14+
defaultMcpOptions: McpOptions,
15+
req: express.Request,
16+
res: express.Response,
17+
): McpServer | null => {
1318
const server = newMcpServer();
19+
20+
let mcpOptions: McpOptions;
21+
try {
22+
mcpOptions = parseQueryOptions(defaultMcpOptions, req.query);
23+
} catch (error) {
24+
res.status(400).json({
25+
jsonrpc: '2.0',
26+
error: {
27+
code: -32000,
28+
message: `Invalid request: ${fromError(error)}`,
29+
},
30+
});
31+
return null;
32+
}
33+
1434
try {
1535
const authOptions = parseAuthHeaders(req);
1636
initMcpServer({
@@ -71,6 +91,7 @@ const del = async (req: express.Request, res: express.Response) => {
7191

7292
export const streamableHTTPApp = (options: McpOptions): express.Express => {
7393
const app = express();
94+
app.set('query parser', 'extended');
7495
app.use(express.json());
7596

7697
app.get('/', get);

packages/mcp-server/src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import { selectTools } from './server';
44
import { Endpoint, endpoints } from './tools';
5-
import { McpOptions, parseOptions } from './options';
5+
import { McpOptions, parseCLIOptions } from './options';
66
import { launchStdioServer } from './stdio';
77
import { launchStreamableHTTPServer } from './http';
88

@@ -40,7 +40,7 @@ if (require.main === module) {
4040

4141
function parseOptionsOrError() {
4242
try {
43-
return parseOptions();
43+
return parseCLIOptions();
4444
} catch (error) {
4545
console.error('Error parsing options:', error);
4646
process.exit(1);

packages/mcp-server/src/options.ts

Lines changed: 120 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import qs from 'qs';
12
import yargs from 'yargs';
23
import { hideBin } from 'yargs/helpers';
4+
import z from 'zod';
35
import { endpoints, Filter } from './tools';
46
import { ClientCapabilities, knownClients, ClientType } from './compat';
57

@@ -47,7 +49,7 @@ function parseCapabilityValue(cap: string): { name: Capability; value?: number }
4749
return { name: cap as Capability };
4850
}
4951

50-
export function parseOptions(): CLIOptions {
52+
export function parseCLIOptions(): CLIOptions {
5153
const opts = yargs(hideBin(process.argv))
5254
.option('tools', {
5355
type: 'string',
@@ -271,6 +273,123 @@ export function parseOptions(): CLIOptions {
271273
};
272274
}
273275

276+
const coerceArray = <T extends z.ZodTypeAny>(zodType: T) =>
277+
z.preprocess(
278+
(val) =>
279+
Array.isArray(val) ? val
280+
: val ? [val]
281+
: val,
282+
z.array(zodType).optional(),
283+
);
284+
285+
const QueryOptions = z.object({
286+
tools: coerceArray(z.enum(['dynamic', 'all'])).describe('Use dynamic tools or all tools'),
287+
no_tools: coerceArray(z.enum(['dynamic', 'all'])).describe('Do not use dynamic tools or all tools'),
288+
tool: coerceArray(z.string()).describe('Include tools matching the specified names'),
289+
resource: coerceArray(z.string()).describe('Include tools matching the specified resources'),
290+
operation: coerceArray(z.enum(['read', 'write'])).describe(
291+
'Include tools matching the specified operations',
292+
),
293+
tag: coerceArray(z.string()).describe('Include tools with the specified tags'),
294+
no_tool: coerceArray(z.string()).describe('Exclude tools matching the specified names'),
295+
no_resource: coerceArray(z.string()).describe('Exclude tools matching the specified resources'),
296+
no_operation: coerceArray(z.enum(['read', 'write'])).describe(
297+
'Exclude tools matching the specified operations',
298+
),
299+
no_tag: coerceArray(z.string()).describe('Exclude tools with the specified tags'),
300+
client: ClientType.optional().describe('Specify the MCP client being used'),
301+
capability: coerceArray(z.string()).describe('Specify client capabilities'),
302+
no_capability: coerceArray(z.enum(CAPABILITY_CHOICES)).describe('Unset client capabilities'),
303+
});
304+
305+
export function parseQueryOptions(defaultOptions: McpOptions, query: unknown): McpOptions {
306+
const queryObject = typeof query === 'string' ? qs.parse(query) : query;
307+
const queryOptions = QueryOptions.parse(queryObject);
308+
309+
const filters: Filter[] = [...defaultOptions.filters];
310+
311+
for (const resource of queryOptions.resource || []) {
312+
filters.push({ type: 'resource', op: 'include', value: resource });
313+
}
314+
for (const operation of queryOptions.operation || []) {
315+
filters.push({ type: 'operation', op: 'include', value: operation });
316+
}
317+
for (const tag of queryOptions.tag || []) {
318+
filters.push({ type: 'tag', op: 'include', value: tag });
319+
}
320+
for (const tool of queryOptions.tool || []) {
321+
filters.push({ type: 'tool', op: 'include', value: tool });
322+
}
323+
for (const resource of queryOptions.no_resource || []) {
324+
filters.push({ type: 'resource', op: 'exclude', value: resource });
325+
}
326+
for (const operation of queryOptions.no_operation || []) {
327+
filters.push({ type: 'operation', op: 'exclude', value: operation });
328+
}
329+
for (const tag of queryOptions.no_tag || []) {
330+
filters.push({ type: 'tag', op: 'exclude', value: tag });
331+
}
332+
for (const tool of queryOptions.no_tool || []) {
333+
filters.push({ type: 'tool', op: 'exclude', value: tool });
334+
}
335+
336+
// Parse client capabilities
337+
const clientCapabilities: ClientCapabilities = {
338+
topLevelUnions: true,
339+
validJson: true,
340+
refs: true,
341+
unions: true,
342+
formats: true,
343+
toolNameLength: undefined,
344+
...defaultOptions.capabilities,
345+
};
346+
347+
for (const cap of queryOptions.capability || []) {
348+
const parsed = parseCapabilityValue(cap);
349+
if (parsed.name === 'top-level-unions') {
350+
clientCapabilities.topLevelUnions = true;
351+
} else if (parsed.name === 'valid-json') {
352+
clientCapabilities.validJson = true;
353+
} else if (parsed.name === 'refs') {
354+
clientCapabilities.refs = true;
355+
} else if (parsed.name === 'unions') {
356+
clientCapabilities.unions = true;
357+
} else if (parsed.name === 'formats') {
358+
clientCapabilities.formats = true;
359+
} else if (parsed.name === 'tool-name-length') {
360+
clientCapabilities.toolNameLength = parsed.value;
361+
}
362+
}
363+
364+
for (const cap of queryOptions.no_capability || []) {
365+
if (cap === 'top-level-unions') {
366+
clientCapabilities.topLevelUnions = false;
367+
} else if (cap === 'valid-json') {
368+
clientCapabilities.validJson = false;
369+
} else if (cap === 'refs') {
370+
clientCapabilities.refs = false;
371+
} else if (cap === 'unions') {
372+
clientCapabilities.unions = false;
373+
} else if (cap === 'formats') {
374+
clientCapabilities.formats = false;
375+
} else if (cap === 'tool-name-length') {
376+
clientCapabilities.toolNameLength = undefined;
377+
}
378+
}
379+
380+
return {
381+
client: queryOptions.client ?? defaultOptions.client,
382+
includeDynamicTools:
383+
defaultOptions.includeDynamicTools ??
384+
(queryOptions.tools?.includes('dynamic') && !queryOptions.no_tools?.includes('dynamic')),
385+
includeAllTools:
386+
defaultOptions.includeAllTools ??
387+
(queryOptions.tools?.includes('all') && !queryOptions.no_tools?.includes('all')),
388+
filters,
389+
capabilities: clientCapabilities,
390+
};
391+
}
392+
274393
function getCapabilitiesExplanation(): string {
275394
return `
276395
Client Capabilities Explanation:

0 commit comments

Comments
 (0)