Repository navigation
Conversation
2771182 to
ec38c23
Compare
|
Hi @geoffw0 — hope you are well. You reviewed and merged my earlier C++ MMIO PR (#22438); thank you again for that. When you have a moment, I would appreciate a look at this Python follow-up for #22702. It models MCP / FastMCP server handler parameters as There is already open MaD coverage in #22703; this PR is intentionally QL-only and complementary. Library tests are under No rush — happy to adjust based on team preference (QL vs MaD, scope, naming). Thanks for any pointers. |
| */ | ||
|
|
||
| private import python | ||
| private import semmle.python.dataflow.new.DataFlow |
|
Hi, this looks reasonable at a glance (though I was a little surprised to see QL-modelled sources rather than MaD models). In any case I've started the tests and am handing over to another member of the team. |
c909145 to
1ae3030
Compare
|
I was also surprised by the assertion that QL-modelling was needed. I think it's correct though. We should fix it - I think changing those queries (and any others) to use |
|
Thanks for the review and for driving this upstream architectural fix in #22822, @owen-mc — migrating those queries to I would love to collaborate directly on #22822. I can help benchmark query recall across the affected legacy rules, run test suites against edge cases, or review changes alongside the team. Once #22822 lands, I will rebase and align this PR (#22750):
With @Alex-Hofer already referencing this work in For background on my work in AST/CPG query lowering and static analysis compiler infrastructure:
Let me know the best way to coordinate on #22822—I'm happy to push validation runs or draft test cases against your branch whenever helpful! |
410350c to
b33c676
Compare
|
Awesome to see #22822 land, @owen-mc! Great work resolving that across the query suite. I have rebased this PR onto main and narrowed Mcp.qll strictly to the dynamic AST patterns that MaD cannot currently express—specifically stacked custom project decorators (getADecorator()) and instance registrations (@self.mcp.tool()). This keeps the PR lightweight and complementary to #22703 without duplicating the baseline YAML definitions. Ready for review whenever the team has bandwidth! |
b33c676 to
86d2f8e
Compare
86d2f8e to
d0a798c
Compare
|
Thanks for updating the PR. Now that it's ready for review, please don't force-push again, as that makes it harder to review. |
|
Understood, @owen-mc! Leaving git history as-is and won't force-push. Thanks for taking a look whenever you have the bandwidth. |
| not isMcpContextParameter(p) | ||
| } | ||
|
|
||
| private class McpHandlerParameter extends RemoteFlowSource::Range, DataFlow::ParameterNode { |
There was a problem hiding this comment.
| private class McpHandlerParameter extends RemoteFlowSource::Range, DataFlow::ParameterNode { | |
| private class McpHandlerParameter extends ThreatModelSource::Range, DataFlow::ParameterNode { |
I assume we want this to have kind ai, instead of remote. So, instead you should extend ThreatModelSource::Range and add the predicate override string getThreatModel() { result = "ai" }.
| or | ||
| p = handler.getKwarg() | ||
| ) and | ||
| not (handler.isMethod() and p = handler.getArg(0)) and |
There was a problem hiding this comment.
We do want the first argument to static methods. (isMethod holds if handler is declared inside a class.) Add private import semmle.python.dataflow.new.internal.DataFlowDispatch at the top and make this change:
| not (handler.isMethod() and p = handler.getArg(0)) and | |
| not ( | |
| handler.isMethod() and | |
| not isStaticmethod(handler) and | |
| p = handler.getArg(0) | |
| ) and |
Ideally you'd add a test for this as well.
| exists(API::Node ctx | | ||
| ctx in [ | ||
| API::moduleImport("fastmcp").getMember("Context"), | ||
| API::moduleImport("mcp").getMember("server").getMember("fastmcp").getMember("Context"), |
There was a problem hiding this comment.
| API::moduleImport("mcp").getMember("server").getMember("fastmcp").getMember("Context"), | |
| API::moduleImport("fastmcp").getMember("server").getMember("context").getMember("Context"), | |
| API::moduleImport("mcp").getMember("server").getMember("fastmcp").getMember("Context"), | |
| API::moduleImport("mcp") | |
| .getMember("server") | |
| .getMember("fastmcp") | |
| .getMember("server") | |
| .getMember("Context"), |
Apply review feedback: ThreatModelSource kind ai, expanded Context API paths, isStaticmethod parameter handling, and staticmethod test coverage.
|
Thanks for the clear feedback, @owen-mc! All items have been addressed in
Also merged latest |
There was a problem hiding this comment.
🟡 Changes recommended
The MCP model misclassifies injected Context parameters and drops valid handler inputs, while the regression query would produce unexpected rows when merged with the companion AI-source changes.
6 open issues
Filter and project native source types in the query · New Add MCP 2.x Context import paths to the exclusion · New Resolve string forward references for Context annotations · New Exclude Optional and Union Context annotations · New Preserve parameters on directly decorated class methods · New Test MCP handlers hidden by wrapping custom decorators · New
What changed in this PR
Adds native Python AST modeling for complex mcp and fastmcp handler registrations, complementing #22703’s Models-as-Data coverage under the opt-in ai threat model.
Changes:
- Recognizes stacked decorators, instance lookups, and static handlers.
- Models handler parameters as sources while excluding recognized
Contextannotations. - Adds regression fixtures, expected results, and framework-support documentation.
| File | Description |
|---|---|
python/​ql/​test/​library-tests/​dataflow/​remote-flow-sources/​RemoteFlowSources.ql |
Queries AI sources for regression testing. |
python/​ql/​test/​library-tests/​dataflow/​remote-flow-sources/​RemoteFlowSources.expected |
Records four expected native parameter sources. |
python/​ql/​test/​library-tests/​dataflow/​remote-flow-sources/​mcp_test.py |
Exercises handler registrations and Context exclusions. |
python/​ql/​lib/​semmle/​python/​frameworks/​Mcp.qll |
Implements handler recognition and parameter-source modeling. |
python/​ql/​lib/​semmle/​python/​Frameworks.qll |
Loads the MCP model with existing framework models. |
python/​ql/​lib/​change-notes/​2026-10-04-mcp-remote-sources.md |
Announces complementary opt-in AI-source coverage. |
docs/​codeql/​reusables/​supported-frameworks.rst |
Lists both MCP packages as supported frameworks. |
Review effort: Max (auto)
| from ThreatModelSource src | ||
| where src.getThreatModel() = "ai" and exists(src.getLocation().getFile().getRelativePath()) | ||
| select src, src.getSourceType() |
| .getMember("server") | ||
| .getMember("fastmcp") | ||
| .getMember("server") | ||
| .getMember("Context"), |
| exists(Expr ann | ann = p.getAnnotation() | | ||
| isContextAnnotation(ann) |
| exists(Expr ann | ann = p.getAnnotation() | | ||
| isContextAnnotation(ann) | ||
| or | ||
| exists(Subscript sub | sub = ann | isContextAnnotation(sub.getValue())) |
| ) and | ||
| not ( | ||
| handler.isMethod() and | ||
| not isStaticmethod(handler) and | ||
| p = handler.getArg(0) | ||
| ) and | ||
| not isMcpContextParameter(p) |
| def other_decorator(fn): | ||
| return fn |



Summary
This PR provides the native QL AST modeling for Python MCP and
fastmcpframeworks to handle complex programmatic edge cases that currently fall outside the scope of Models-as-Data (MaD) YAML.Context & Positioning
Following the excellent global engine update in #22822 (which upgraded legacy queries to accept
ActiveThreatModelSource), MaD YAML PR #22703 is now perfectly positioned to handle the 80% baseline of standard MCP handlers and headers.However, as noted in the mcp-vulnbench benchmarks, YAML currently drops handlers if they are wrapped behind stacked custom project decorators (e.g.,
@custom_telemetryabove@mcp.tool()).This PR is designed to be merged alongside #22703. It acts as a lightweight AST extension extending
RemoteFlowSource::Rangeto capture:getADecorator()).@self.mcp.tool()).Contextparameters to prevent false positives.Testing
Rebased onto
main. All local unit tests underpython/ql/test/library-tests/dataflow/remote-flow-sources/pass cleanly.