Skip to content

Python: Add models for the MCP server SDKs (mcp, fastmcp) - #22703

Open
Alex-Hofer wants to merge 4 commits into
github:mainfrom
Alex-Hofer:python-mcp-models
Open

Alex-Hofer wants to merge 4 commits into
github:mainfrom
Alex-Hofer:python-mcp-models

Conversation

@Alex-Hofer

@Alex-Hofer Alex-Hofer commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Implements the proposal in #22702. The observation there about py/xxe, py/xml-bomb and
py/nosql-injection taking only RemoteFlowSource has since been fixed by #22822.

MCP servers expose tools to LLM agents. Every parameter of a tool, resource or prompt handler is
chosen by the client, that is by a model that prompt injection can steer, or by anyone who can
reach the server over HTTP. This PR models that input for the two Python SDKs as Models-as-Data
next to the existing Stdlib.model.yml sources: the handler parameters as sources of the opt-in
ai threat model (#22810), the transport headers and the bearer token as sources of the remote
threat model.

Covered:

  • mcp 1.x FastMCP and 2.x MCPServer: tool(), resource(), prompt(), add_tool(fn).
  • The low-level Server: the 1.x decorators call_tool(), read_resource(), get_prompt(), and
    the 2.x constructor handlers on_call_tool, on_read_resource, on_get_prompt, where
    params.arguments / params.uri is the source.
  • fastmcp 2.x to 4.x: tool and prompt bare, called or as a plain call, resource(),
    add_tool, add_prompt, Tool.from_function, fastmcp.tools.tool, the transport headers via
    get_http_headers() and get_http_request().headers.
  • Both SDKs: the bearer token of the Authorization header, in verify_token of TokenVerifier
    (and AuthProvider) subclasses and from get_access_token().token.
  • typeModel rows for the re-exported import paths of the classes.

Tests: library-tests/frameworks/mcp and library-tests/frameworks/fastmcp use MaDTest
(mad-source__ai, mad-source__remote); every row has a handler with an inline expectation, and classes with the
same method name but no SDK base class have none.

Evidence: with these rows (as a model pack) CodeQL 2.27.1 finds 13 of 28 real, publicly disclosed
MCP server vulnerabilities in mcp-vulnbench
instead of 1; on the half of the cases that was held out while writing the rows, 9 of 16 instead
of 0, at 0.27 to 1.09 alarms per KLOC. Known limits of this form (handlers behind project
decorators, Pydantic-typed parameters, headers read through the Context object) are documented
there and in the issue. The measurement used the rows as remote, which is on by default; with
ai the same sources apply once that threat model is enabled.

Handler parameters are produced by a language model, so they are sources of the opt-in ai threat model added in github#22810. HTTP headers and bearer tokens come from the HTTP client and stay sources of the remote threat model.
@Alex-Hofer

Copy link
Copy Markdown
Contributor Author

Following the suggestion in #22749, the handler parameters are now sources of the opt-in ai threat model (#22810); the HTTP headers and bearer tokens stay remote, since they come from the HTTP client. Rebased on main. With #22822 merged, the note in the description about py/xxe, py/xml-bomb and py/nosql-injection is settled: these queries now see sources from data extensions.

@Alex-Hofer

Copy link
Copy Markdown
Contributor Author

I have merged main into the branch to pick up the ai source kind in the model validation from #22749; without it, library-tests/frameworks/data fails on these rows with Invalid kind "ai" in source model. The diff of this PR is unchanged. Locally (CodeQL 2.27.1), frameworks/data, frameworks/mcp and frameworks/fastmcp pass.

Comment on lines +8 to +12
- ['fastmcp.FastMCP', 'Member[tool,prompt].Argument[0,name_or_fn:].Parameter[any]', 'ai']
- ['fastmcp.FastMCP', 'Member[tool,resource,prompt].ReturnValue.Argument[0].Parameter[any]', 'ai']
- ['fastmcp.FastMCP', 'Member[add_tool].Argument[0,tool:].Parameter[any]', 'ai']
- ['fastmcp.FastMCP', 'Member[add_prompt].Argument[0,prompt:].Parameter[any]', 'ai']
- ['fastmcp.tools.Tool!', 'Member[from_function].Argument[0,fn:].Parameter[any]', 'ai']

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Parameter[any] includes ctx: Context, although the SDK injects this rather than taking it from client arguments. I think the only fix is to use QL for these models.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in f215487: the handlers of FastMCP, MCPServer and fastmcp are now modeled in Mcp.qll, which leaves out a parameter annotated with Context (also as Context[...], Context | None, Optional[...], Union[...], Annotated[...], or a string annotation in a module that imports Context) and the receiver of non-static methods; the tests have a negative case for each. The low-level server keeps its rows, since its handlers take (name, arguments) or (ctx, params) and no Context. #22750 also adds a Mcp.qll, for handlers behind a project decorator; I used the same name so that both end up in one module, and I'm happy to do that merge on whichever side lands second.

Comment on lines +9 to +12
- ['mcp.server.fastmcp.FastMCP', 'Member[tool,resource,prompt].ReturnValue.Argument[0].Parameter[any]', 'ai']
- ['mcp.server.fastmcp.FastMCP', 'Member[add_tool].Argument[0,fn:].Parameter[any]', 'ai']
- ['mcp.server.mcpserver.MCPServer', 'Member[tool,resource,prompt].ReturnValue.Argument[0].Parameter[any]', 'ai']
- ['mcp.server.mcpserver.MCPServer', 'Member[add_tool].Argument[0,fn:].Parameter[any]', 'ai']

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Parameter[any] includes ctx: Context, although the SDK injects this rather than taking it from client arguments. I think the only fix is to use QL for these models.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same change, see Mcp.qll; mcp.model.yml now only has the low-level server and the bearer token.

- ['mcp.server.mcpserver.MCPServer', 'Member[add_tool].Argument[0,fn:].Parameter[any]', 'ai']
# The low-level server: decorators in 1.x, constructor handlers taking `(ctx, params)` in 2.x.
- ['mcp.server.lowlevel.Server', 'Member[call_tool,read_resource,get_prompt].ReturnValue.Argument[0].Parameter[any]', 'ai']
- ['mcp.server.lowlevel.Server!', 'Argument[on_call_tool:,on_get_prompt:].Parameter[1].Member[arguments]', 'ai']

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- ['mcp.server.lowlevel.Server!', 'Argument[on_call_tool:,on_get_prompt:].Parameter[1].Member[arguments]', 'ai']
- ['mcp.server.lowlevel.Server!', 'Argument[on_call_tool:,on_get_prompt:].Parameter[1].Member[arguments,name]', 'ai']

And add tests for `name.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied, with tests for params.name in lowlevel_v2.py.

- ['fastmcp.FastMCP', 'Member[tool,prompt].Argument[0,name_or_fn:].Parameter[any]', 'ai']
- ['fastmcp.FastMCP', 'Member[tool,resource,prompt].ReturnValue.Argument[0].Parameter[any]', 'ai']
- ['fastmcp.FastMCP', 'Member[add_tool].Argument[0,tool:].Parameter[any]', 'ai']
- ['fastmcp.FastMCP', 'Member[add_prompt].Argument[0,prompt:].Parameter[any]', 'ai']

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently we will not track flow through code like app.add_prompt(Prompt.from_function(handler)). Add models for Prompt.from_function and FunctionPrompt.from_function.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added Prompt.from_function and FunctionPrompt.from_function for the import paths of 2.x to 4.x (fastmcp.prompts, fastmcp.prompts.prompt, fastmcp.prompts.base, fastmcp.prompts.function_prompt), with a test for app.add_prompt(Prompt.from_function(handler)). I also added the standalone prompt decorator, the counterpart of tool.

Comment on lines +31 to +33
- ['fastmcp.tools.Tool!', 'fastmcp', 'Member[tools].Member[FunctionTool]']
- ['fastmcp.tools.Tool!', 'fastmcp', 'Member[tools].Member[base].Member[Tool]']
- ['fastmcp.tools.Tool!', 'fastmcp', 'Member[tools].Member[function_tool].Member[FunctionTool]']

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tool-class aliases omit fastmcp.tools.tool.Tool and fastmcp.tools.tool.FunctionTool. Add aliases here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added fastmcp.tools.tool.Tool and fastmcp.tools.tool.FunctionTool, with tests.

Comment on lines +13 to +14
- ['fastmcp', 'Member[tools].Member[tool].Argument[0,name_or_fn:].Parameter[any]', 'ai']
- ['fastmcp', 'Member[tools].Member[tool].ReturnValue.Argument[0].Parameter[any]', 'ai']

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The standalone decorator models cover fastmcp.tools.tool, but not the defining-module import fastmcp.tools.function_tool.tool. Add coverage for that path.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added fastmcp.tools.function_tool.tool, with and without parentheses.

`Parameter[any]` also matched the `Context` parameter that the SDKs inject,
so the handlers of `FastMCP`, `MCPServer` and `fastmcp` are now modeled in
`Mcp.qll`, which leaves it out (also as `Context | None`, `Optional[...]`,
`Annotated[...]` or a string annotation) as well as the receiver of methods.
This also covers `Prompt.from_function`, `FunctionPrompt.from_function`, the
`Tool` classes in `fastmcp.tools.tool` and the standalone decorators in
`fastmcp.tools.function_tool` and `fastmcp.prompts`. The low-level server
keeps its data extensions and now also models `params.name`.
Copilot AI auto review requested due to automatic review settings November 4, 2026 18:12

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

MCP parameter classification incorrectly excludes client-controlled inputs and includes aliased SDK-injected context, risking both missed security findings and false positives.

3 open issues
What changed in this PR

Adds Python CodeQL models for the mcp and fastmcp server SDKs so existing security queries can track client-controlled handler input, headers, and bearer tokens.

Changes:

  • Models high-level handler parameters in QL under the opt-in ai threat model, excluding injected context parameters.
  • Adds data-extension sources for low-level handlers, transport headers, and authentication tokens.
  • Registers the framework model and adds regression tests and a change note.
File Description
python/​ql/​test/​library-tests/​frameworks/​mcp/​mcpserver_v2.py Tests MCPServer handlers and context exclusions.
python/​ql/​test/​library-tests/​frameworks/​mcp/​MaDTest.ql Runs data-extension source checks.
python/​ql/​test/​library-tests/​frameworks/​mcp/​MaDTest.expected Records expected data-extension results.
python/​ql/​test/​library-tests/​frameworks/​mcp/​lowlevel_v2.py Tests constructor-based low-level handlers.
python/​ql/​test/​library-tests/​frameworks/​mcp/​lowlevel_v1.py Tests decorator-based low-level handlers.
python/​ql/​test/​library-tests/​frameworks/​mcp/​fastmcp_v1.py Tests official SDK FastMCP registrations.
python/​ql/​test/​library-tests/​frameworks/​mcp/​ConceptsTest.ql Runs framework source-concept checks.
python/​ql/​test/​library-tests/​frameworks/​mcp/​ConceptsTest.expected Records expected framework source results.
python/​ql/​test/​library-tests/​frameworks/​mcp/​auth.py Tests token-verifier and access-token sources.
python/​ql/​test/​library-tests/​frameworks/​fastmcp/​test.py Tests standalone FastMCP handlers, context, headers, and authentication.
python/​ql/​test/​library-tests/​frameworks/​fastmcp/​MaDTest.ql Runs FastMCP data-extension checks.
python/​ql/​test/​library-tests/​frameworks/​fastmcp/​MaDTest.expected Records expected data-extension results.
python/​ql/​test/​library-tests/​frameworks/​fastmcp/​ConceptsTest.ql Runs FastMCP source-concept checks.
python/​ql/​test/​library-tests/​frameworks/​fastmcp/​ConceptsTest.expected Records expected handler-source results.
python/​ql/​lib/​semmle/​python/​frameworks/​Mcp.qll Identifies handler parameters and excludes injected context.
python/​ql/​lib/​semmle/​python/​frameworks/​mcp.model.yml Models low-level MCP handlers and token sources.
python/​ql/​lib/​semmle/​python/​frameworks/​fastmcp.model.yml Models FastMCP headers, tokens, and authentication aliases.
python/​ql/​lib/​semmle/​python/​Frameworks.qll Loads the MCP framework model.
python/​ql/​lib/​change-notes/​2026-09-30-mcp-models.md Documents SDK coverage and threat-model behavior.

Review effort: Max (auto)

Comment on lines +154 to +156
isContextAnnotation(e.(Subscript).getIndex())
or
isContextAnnotation(e.(Tuple).getAnElt())
Comment on lines +163 to +165
e.(StringLiteral).getText().regexpMatch(".*\\bContext\\b.*") and
contextClass().getAValueReachableFromSource().asExpr().getEnclosingModule() =
e.getEnclosingModule()
Comment on lines +181 to +183
handler.isMethod() and
not isStaticmethod(handler) and
p = handler.getArg(0)
@Alex-Hofer
Alex-Hofer requested a review from owen-mc November 4, 2026 22:47

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants