Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/codeql/reusables/supported-frameworks.rst
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,8 @@ and the CodeQL library pack ``codeql/python-all`` (`changelog <https://github.co
Tornado, Web framework
Twisted, Web framework
Gradio, Web framework
fastmcp, MCP server framework
mcp, MCP server framework
starlette, Asynchronous Server Gateway Interface (ASGI)
ldap3, Lightweight Directory Access Protocol (LDAP)
python-ldap, Lightweight Directory Access Protocol (LDAP)
Expand Down
4 changes: 4 additions & 0 deletions python/ql/lib/change-notes/2026-10-04-mcp-remote-sources.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
category: minorAnalysis
---
* Added `RemoteFlowSource` modeling for parameters of MCP server handlers registered via the `mcp` and `fastmcp` PyPI packages (`tool`, `prompt`, `resource`, and `add_tool`).
1 change: 1 addition & 0 deletions python/ql/lib/semmle/python/Frameworks.qll
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ private import semmle.python.frameworks.Libxml2
private import semmle.python.frameworks.Lxml
private import semmle.python.frameworks.Mako
private import semmle.python.frameworks.MarkupSafe
private import semmle.python.frameworks.Mcp
private import semmle.python.frameworks.Multidict
private import semmle.python.frameworks.Mysql
private import semmle.python.frameworks.MySQLdb
Expand Down
137 changes: 137 additions & 0 deletions python/ql/lib/semmle/python/frameworks/Mcp.qll
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/**
* Provides classes modeling security-relevant aspects of the `mcp` and `fastmcp` PyPI packages
* (Model Context Protocol server SDKs).
*/

private import python
private import semmle.python.dataflow.new.DataFlow
private import semmle.python.dataflow.new.RemoteFlowSources
private import semmle.python.ApiGraphs

/**
* Provides models for MCP server handler parameters as remote flow sources.
*/
module Mcp {
private string registrationMethod() { result in ["tool", "prompt", "resource"] }

/** Gets API nodes for MCP / FastMCP server classes used to register handlers. */
private API::Node mcpServerClass() {
result =
[
API::moduleImport("fastmcp").getMember("FastMCP"),
API::moduleImport("fastmcp").getMember("server").getMember("FastMCP"),
API::moduleImport("fastmcp").getMember("server").getMember("server").getMember("FastMCP"),
API::moduleImport("mcp").getMember("server").getMember("fastmcp").getMember("FastMCP"),
API::moduleImport("mcp")
.getMember("server")
.getMember("fastmcp")
.getMember("server")
.getMember("FastMCP"),
API::moduleImport("mcp").getMember("server").getMember("MCPServer"),
API::moduleImport("mcp").getMember("server").getMember("mcpserver").getMember("MCPServer"),
API::moduleImport("mcp")
.getMember("server")
.getMember("mcpserver")
.getMember("server")
.getMember("MCPServer"),
]
}

/** Gets API nodes for module-level `fastmcp` registration decorators (`@tool`, etc.). */
private API::Node fastmcpModuleRegistration(string name) {
name = registrationMethod() and result = API::moduleImport("fastmcp").getMember(name)
or
name = "tool" and result = API::moduleImport("fastmcp").getMember("tools").getMember("tool")
}

private predicate isMcpHandler(Function handler) {
exists(API::Node cls | cls = mcpServerClass() |
exists(API::CallNode call |
call = cls.getAnInstance().getMember(registrationMethod()).getACall() and
call.getNode().getNode() = handler.getADecorator()
)
or
exists(string name | name = registrationMethod() |
handler.getADecorator() =
cls.getAnInstance().getMember(name).getAValueReachableFromSource().asExpr()
)
or
exists(API::CallNode addCall |
addCall = cls.getAnInstance().getMember("add_tool").getACall() and
(
exists(DataFlow::LocalSourceNode funcSrc |
(
funcSrc.flowsTo(addCall.getArg(0)) or
funcSrc.flowsTo(addCall.getArgByName("fn")) or
funcSrc.flowsTo(addCall.getArgByName("tool"))
) and
funcSrc.asExpr() = handler.getDefinition()
)
or
exists(DataFlow::Node arg |
arg in [addCall.getArg(0), addCall.getArgByName("fn"), addCall.getArgByName("tool")] and
arg.asExpr().(Name).getId() = handler.getName() and
arg.getScope() = handler.getScope()
)
)
)
)
or
exists(string name | name = registrationMethod() |
exists(API::Node reg | reg = fastmcpModuleRegistration(name) |
exists(API::CallNode call |
call = reg.getACall() and call.getNode().getNode() = handler.getADecorator()
)
or
handler.getADecorator() = reg.getAValueReachableFromSource().asExpr()
)
)
}

private predicate isContextAnnotation(Expr ann) {
exists(API::Node ctx |
ctx in [
API::moduleImport("fastmcp").getMember("Context"),
API::moduleImport("mcp").getMember("server").getMember("fastmcp").getMember("Context"),
] and
ann = ctx.getAValueReachableFromSource().asExpr()
)
}

private predicate isMcpContextParameter(Parameter p) {
exists(Expr ann | ann = p.getAnnotation() |
isContextAnnotation(ann)
or
exists(Subscript sub | sub = ann | isContextAnnotation(sub.getValue()))
or
exists(BinaryExpr bin | bin = ann and bin.getOp() instanceof BitOr |
isContextAnnotation(bin.getLeft()) or isContextAnnotation(bin.getRight())
)
)
}

private predicate isHandlerParameter(Function handler, Parameter p) {
(
p = handler.getAnArg()
or
p = handler.getAKeywordOnlyArg()
or
p = handler.getVararg()
or
p = handler.getKwarg()
) and
not (handler.isMethod() and p = handler.getArg(0)) and
not isMcpContextParameter(p)
}

private class McpHandlerParameter extends RemoteFlowSource::Range, DataFlow::ParameterNode {
McpHandlerParameter() {
exists(Function handler |
isMcpHandler(handler) and
isHandlerParameter(handler, this.getParameter())
)
}

override string getSourceType() { result = "MCP server handler parameter" }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
| mcp_test.py:16:15:16:21 | ControlFlowNode for command | MCP server handler parameter |
| mcp_test.py:21:15:21:21 | ControlFlowNode for command | MCP server handler parameter |
| mcp_test.py:26:16:26:20 | ControlFlowNode for topic | MCP server handler parameter |
| mcp_test.py:26:31:26:35 | ControlFlowNode for style | MCP server handler parameter |
| mcp_test.py:31:17:31:20 | ControlFlowNode for name | MCP server handler parameter |
| mcp_test.py:35:23:35:25 | ControlFlowNode for url | MCP server handler parameter |
| mcp_test.py:42:19:42:21 | ControlFlowNode for url | MCP server handler parameter |
| mcp_test.py:50:17:50:19 | ControlFlowNode for cmd | MCP server handler parameter |
| mcp_test.py:55:24:55:26 | ControlFlowNode for cmd | MCP server handler parameter |
| mcp_test.py:61:23:61:25 | ControlFlowNode for cmd | MCP server handler parameter |
| mcp_test.py:70:32:70:35 | ControlFlowNode for data | MCP server handler parameter |
| mcp_test.py:80:29:80:32 | ControlFlowNode for host | MCP server handler parameter |
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import python
import semmle.python.dataflow.new.RemoteFlowSources

from RemoteFlowSource src
where exists(src.getLocation().getFile().getRelativePath())
select src, src.getSourceType()
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
from fastmcp import Context, FastMCP
from fastmcp import tool as standalone_tool
from mcp.server.fastmcp import FastMCP as McpFastMCP
from mcp.server import MCPServer

mcp_app = FastMCP("test")
mcp_sdk = McpFastMCP("test")
mcp_v2 = MCPServer("test")


def other_decorator(fn):
return fn


@mcp_app.tool()
def sync_tool(command: str) -> str:
return command


@mcp_app.tool
def bare_tool(command: str) -> str:
return command


@mcp_sdk.prompt()
def sdk_prompt(topic: str, *, style: str = "short") -> str:
return topic + style


@mcp_v2.resource("notes://{name}")
def v2_resource(name: str) -> str:
return name


async def async_fetch(url: str) -> str:
return url


mcp_app.add_tool(async_fetch)


def added_via_add(url: str) -> str:
return url


mcp_sdk.add_tool(fn=added_via_add)


@standalone_tool
def module_tool(cmd: str) -> str:
return cmd


@standalone_tool()
def module_tool_called(cmd: str) -> str:
return cmd


@other_decorator
@mcp_app.tool()
def stacked_decorator(cmd: str) -> str:
return cmd


def not_registered(value: str) -> str:
return value


@mcp_app.tool()
def with_context(ctx: Context, data: str) -> str:
return data


class Service:
def __init__(self):
self.mcp = FastMCP("svc")

def register(self):
@self.mcp.tool()
def instance_lookup(host: str) -> str:
return host