Skip to content

Commit fc6ea80

Browse files
committed
JS: Add models for the MCP server SDKs
1 parent f7617ab commit fc6ea80

5 files changed

Lines changed: 210 additions & 0 deletions

File tree

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
category: minorAnalysis
3+
---
4+
* Added modeling of the `@modelcontextprotocol/sdk` and `@modelcontextprotocol/server` (the official Model Context Protocol SDK) and `fastmcp` npm packages. The arguments of MCP tool, resource and prompt callbacks, the requests that handlers of the low-level `Server` receive, and the request headers and bearer token that the SDK passes to a callback, are now sources of the `remote` threat model.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Sources for MCP servers built with the npm package "fastmcp": the arguments of tools, resource
2+
# templates and prompts.
3+
extensions:
4+
- addsTo:
5+
pack: codeql/javascript-all
6+
extensible: typeModel
7+
data:
8+
- ["fastmcp.FastMCP", "fastmcp", "Member[FastMCP].Instance"]
9+
10+
- addsTo:
11+
pack: codeql/javascript-all
12+
extensible: sourceModel
13+
data:
14+
- ["fastmcp.FastMCP", "Member[addTool].Argument[0].Member[execute].Parameter[0]", "remote"]
15+
- ["fastmcp.FastMCP", "Member[addResourceTemplate,addPrompt].Argument[0].Member[load].Parameter[0]", "remote"]
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Sources for MCP servers built with the TypeScript SDK: "@modelcontextprotocol/sdk" (1.x) and
2+
# "@modelcontextprotocol/server" (2.x). The arguments of tool, prompt and resource callbacks are
3+
# input the server receives through MCP, and so are the transport headers and the bearer token the
4+
# SDK hands to a callback.
5+
#
6+
# A package name that contains a dot must be quoted with single quotes; otherwise CodeQL reads the
7+
# text after the first dot as a type name.
8+
extensions:
9+
- addsTo:
10+
pack: codeql/javascript-all
11+
extensible: typeModel
12+
data:
13+
- ["@modelcontextprotocol/sdk.McpServer", "'@modelcontextprotocol/sdk/server/mcp.js'", "Member[McpServer].Instance"]
14+
- ["@modelcontextprotocol/sdk.McpServer", "@modelcontextprotocol/sdk/server/mcp", "Member[McpServer].Instance"]
15+
- ["@modelcontextprotocol/sdk.McpServer", "@modelcontextprotocol/server", "Member[McpServer].Instance"]
16+
- ["@modelcontextprotocol/sdk.McpServer", "'@modelcontextprotocol/sdk/server/mcp.js'.McpServer", ""]
17+
- ["@modelcontextprotocol/sdk.Server", "'@modelcontextprotocol/sdk/server/index.js'", "Member[Server].Instance"]
18+
- ["@modelcontextprotocol/sdk.Server", "@modelcontextprotocol/sdk/server/index", "Member[Server].Instance"]
19+
- ["@modelcontextprotocol/sdk.Server", "@modelcontextprotocol/server", "Member[Server].Instance"]
20+
- ["@modelcontextprotocol/sdk.Server", "@modelcontextprotocol/sdk.McpServer", "Member[server]"]
21+
22+
- addsTo:
23+
pack: codeql/javascript-all
24+
extensible: sourceModel
25+
data:
26+
# High-level server: the callback is the last argument; its first parameter holds the arguments
27+
- ["@modelcontextprotocol/sdk.McpServer", "Member[tool,registerTool,prompt,registerPrompt].Argument[1..].Parameter[0]", "remote"]
28+
- ["@modelcontextprotocol/sdk.McpServer", "Member[resource,registerResource].Argument[1..].Parameter[0,1]", "remote"]
29+
# Transport headers and the bearer token: `extra` in 1.x, `ctx` in 2.x
30+
- ["@modelcontextprotocol/sdk.McpServer", "Member[tool,registerTool,prompt,registerPrompt].Argument[1..].Parameter[1,2].Member[requestInfo].Member[headers]", "remote"]
31+
- ["@modelcontextprotocol/sdk.McpServer", "Member[tool,registerTool,prompt,registerPrompt].Argument[1..].Parameter[1,2].Member[authInfo].Member[token]", "remote"]
32+
- ["@modelcontextprotocol/sdk.McpServer", "Member[tool,registerTool,prompt,registerPrompt].Argument[1..].Parameter[1,2].Member[http].Member[req].Member[headers].Member[get].ReturnValue", "remote"]
33+
- ["@modelcontextprotocol/sdk.McpServer", "Member[tool,registerTool,prompt,registerPrompt].Argument[1..].Parameter[1,2].Member[http].Member[authInfo].Member[token]", "remote"]
34+
# Low-level server: request handlers receive the request; custom methods (2.x) its params
35+
- ["@modelcontextprotocol/sdk.Server", "Member[setRequestHandler].Argument[1].Parameter[0].Member[params]", "remote"]
36+
- ["@modelcontextprotocol/sdk.Server", "Member[setRequestHandler].Argument[2].Parameter[0]", "remote"]
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { FastMCP } from "fastmcp";
2+
import { z } from "zod";
3+
4+
declare function SINK(value: unknown): void;
5+
6+
const server = new FastMCP({ name: "test", version: "1.0.0" });
7+
8+
server.addTool({
9+
name: "run",
10+
parameters: z.object({ command: z.string() }),
11+
execute: async (args) => { // $ threat-source=remote
12+
SINK(args.command); // $ hasFlow
13+
return "";
14+
},
15+
});
16+
17+
server.addResourceTemplate({
18+
uriTemplate: "note://{name}",
19+
name: "note",
20+
arguments: [{ name: "name" }],
21+
async load({ name }) { // $ threat-source=remote
22+
SINK(name); // $ hasFlow
23+
return { text: "" };
24+
},
25+
});
26+
27+
server.addPrompt({
28+
name: "summarize",
29+
arguments: [{ name: "topic" }],
30+
load: async ({ topic }) => { // $ threat-source=remote
31+
SINK(topic); // $ hasFlow
32+
return "";
33+
},
34+
});
35+
36+
// A static resource takes no arguments: no source.
37+
server.addResource({
38+
uri: "file:///log",
39+
name: "log",
40+
async load() {
41+
return { text: "" };
42+
},
43+
});
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2+
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
3+
import { McpServer as McpServerNoSuffix } from "@modelcontextprotocol/sdk/server/mcp";
4+
import { CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";
5+
import { McpServer as McpServerV2, Server as ServerV2 } from "@modelcontextprotocol/server";
6+
import { z } from "zod";
7+
8+
declare function SINK(value: unknown): void;
9+
10+
const server = new McpServer({ name: "test", version: "1.0.0" });
11+
12+
// Tools and prompts: the first parameter of the callback holds the arguments.
13+
server.tool("run", { command: z.string() }, async ({ command }) => { // $ threat-source=remote
14+
SINK(command); // $ hasFlow
15+
return { content: [] };
16+
});
17+
18+
server.tool("read", "Read a file", { path: z.string() }, async (args) => { // $ threat-source=remote
19+
SINK(args.path); // $ hasFlow
20+
return { content: [] };
21+
});
22+
23+
server.registerTool("fetch", { inputSchema: { url: z.string() } }, async ({ url }) => { // $ threat-source=remote
24+
SINK(url); // $ hasFlow
25+
return { content: [] };
26+
});
27+
28+
async function namedHandler({ target }: { target: string }) { // $ threat-source=remote
29+
SINK(target); // $ hasFlow
30+
return { content: [] };
31+
}
32+
server.tool("remove", { target: z.string() }, namedHandler);
33+
34+
server.prompt("summarize", { topic: z.string() }, ({ topic }) => { // $ threat-source=remote
35+
SINK(topic); // $ hasFlow
36+
return { messages: [] };
37+
});
38+
39+
server.registerPrompt("explain", { argsSchema: { file: z.string() } }, ({ file }) => { // $ threat-source=remote
40+
SINK(file); // $ hasFlow
41+
return { messages: [] };
42+
});
43+
44+
// Resources: the URI and the variables of the template.
45+
server.resource("note", new ResourceTemplate("note://{name}", { list: undefined }), async (uri, { name }) => { // $ threat-source=remote
46+
SINK(uri.href); // $ hasFlow
47+
SINK(name); // $ hasFlow
48+
return { contents: [] };
49+
});
50+
51+
server.registerResource("page", new ResourceTemplate("page://{host}", { list: undefined }), {}, async (uri, variables) => { // $ threat-source=remote
52+
SINK(variables.host); // $ hasFlow
53+
return { contents: [] };
54+
});
55+
56+
// The request headers and the bearer token that the SDK hands to a callback.
57+
server.tool("forward", { id: z.string() }, async (_args, extra) => { // $ threat-source=remote
58+
SINK(extra.requestInfo?.headers["x-target"]); // $ hasFlow threat-source=remote
59+
SINK(extra.authInfo?.token); // $ hasFlow threat-source=remote
60+
SINK(extra.sessionId);
61+
return { content: [] };
62+
});
63+
64+
// The low-level server: handlers receive the request.
65+
const lowLevel = new Server({ name: "test", version: "1.0.0" }, { capabilities: { tools: {} } });
66+
lowLevel.setRequestHandler(CallToolRequestSchema, async (request) => {
67+
SINK(request.params.arguments?.command); // $ hasFlow threat-source=remote
68+
return { content: [] };
69+
});
70+
71+
server.server.setRequestHandler(CallToolRequestSchema, async (request) => {
72+
SINK(request.params.name); // $ hasFlow threat-source=remote
73+
return { content: [] };
74+
});
75+
76+
// An import path without the file suffix, and a server that arrives as a typed parameter.
77+
new McpServerNoSuffix({ name: "test", version: "1.0.0" }).tool("run", { command: z.string() }, async ({ command }) => { // $ threat-source=remote
78+
SINK(command); // $ hasFlow
79+
return { content: [] };
80+
});
81+
82+
export function register(target: McpServer) {
83+
target.tool("run", { command: z.string() }, async ({ command }) => { // $ threat-source=remote
84+
SINK(command); // $ hasFlow
85+
return { content: [] };
86+
});
87+
}
88+
89+
// Version 2 of the SDK: one package, and a context object instead of `extra`.
90+
const serverV2 = new McpServerV2({ name: "test", version: "2.0.0" });
91+
serverV2.registerTool("fetch", { inputSchema: z.object({ url: z.string() }) }, async ({ url }, ctx) => { // $ threat-source=remote
92+
SINK(url); // $ hasFlow
93+
SINK(ctx.http?.req.headers.get("x-target")); // $ hasFlow threat-source=remote
94+
SINK(ctx.http?.authInfo?.token); // $ hasFlow threat-source=remote
95+
return { content: [] };
96+
});
97+
98+
const lowLevelV2 = new ServerV2({ name: "test", version: "2.0.0" });
99+
lowLevelV2.setRequestHandler("tools/call", async (request) => {
100+
SINK(request.params.arguments); // $ hasFlow threat-source=remote
101+
return { content: [] };
102+
});
103+
104+
lowLevelV2.setRequestHandler("acme/search", { params: z.object({ query: z.string() }), result: z.object({}) }, async (params) => { // $ threat-source=remote
105+
SINK(params.query); // $ hasFlow
106+
return {};
107+
});
108+
109+
// Not registered with a server: no source.
110+
export function notAHandler(command: string) {
111+
SINK(command);
112+
}

0 commit comments

Comments
 (0)