feat: enable built-in MCP server — Phase-1 MCP-first reference#30
Draft
ashpreetbedi wants to merge 1 commit into
Draft
feat: enable built-in MCP server — Phase-1 MCP-first reference#30ashpreetbedi wants to merge 1 commit into
ashpreetbedi wants to merge 1 commit into
Conversation
Dash now serves agno's built-in 8-tool MCP server at /mcp
(enable_mcp_server=True), making the warehouse callable by any MCP
client via run_team("dash", ...). Stacked on the agno 2.7.0a6 bump
branch (the [mcp] extra and the 421 host-guard fix require >=2.7.0a5).
- pyproject: agno[os] -> agno[os,mcp]; drop the standalone mcp dep
(fastmcp 3.4.3 supplies mcp==1.27.0 transitively); regenerated
requirements.txt
- app/main.py: enable_mcp_server=True on the AgentOS ctor — auth,
scheduler, lifespan unchanged
- scripts/mcp_check.sh: ported from agentos-railway, retargeted to
run_team("dash", ...) with a tool-free default question
- README: MCP section (endpoint, uvx agno connect, connector setup,
auth model, smoke check)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Contributor
Author
|
🤖 Automated re-review No blocking issues found. Re-reviewed the current diff for correctness bugs and reuse/simplification/efficiency issues; nothing blocking surfaced. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Phase-1 reference implementation for the Apps → MCP-first direction (see
agentos-family/docs/proposals/apps-mcp-first-spec.md). Stacked onchore/update-2026-07-07— the built-in MCP server needs agno ≥ 2.7.0a5 ([mcp]extra + the 421 host-guard fix); do not retarget tomain(still 2.5.14).What changed
app/main.py—enable_mcp_server=Trueon theAgentOS(...)ctor. Nothing else: auth, scheduler, lifespan unchanged. Dash now serves the built-in 8-tool streamable-HTTP MCP server at/mcp, so any MCP client (Claude Code, Cursor, claude.ai connectors, other Apps) canrun_team("dash", ...)against the warehouse.pyproject.toml—agno[os]→agno[os,mcp]at the same pin (2.7.0a6); dropped the standalonemcpdep (the extra suppliesfastmcp==3.4.3→mcp==1.27.0transitively).requirements.txtregenerated via./scripts/generate_requirements.sh.scripts/mcp_check.sh— ported fromagentos-railway, retargeted torun_team("dash", ...)with a tool-free default question; expects "MCP OK — 8 tools".README.md— MCP section: endpoint,uvx agno connect/claude mcp add, chat-app connector setup, auth model, smoke check.Identity spike — does the built-in 8-tool server honor per-request identity/role scoping?
Verified against the installed agno 2.7.0a6 source (
agno/os/mcp.py,agno/os/app.py,agno/os/middleware/jwt.py).Per-request identity: YES, natively.
/mcp. The parent app's singleAuthMiddleware(app.py::_add_auth_middleware) runs before Starlette dispatches to the MCP mount — JWTs, service-account PATs, internal service token, and security key are all verified there, and the identity lands onrequest.state.user_id(+scopes,admin_scope,authorization_enabled). No auth code lives on the mounted sub-app.user_id. Every run tool calls_resolve_user_id()(agno/os/mcp.py:158), which reads the in-flight request viafastmcp.server.dependencies.get_http_request()and replaces any client-passeduser_idwithrequest.state.user_id. MCP callers cannot spoof attribution in prd._require_tool_scopes()(mcp.py:230) maps each tool call onto its equivalent REST route (run_team→POST /teams/{id}/runs, etc.) and runscheck_route_scopeswith the caller's token scopes — so a PAT scoped to a subset of resources gets "insufficient permissions" per call, identically to REST.get_agentos_configadditionally filters the visible agent/team/workflow roster by per-resource scopes (filter_resources_by_access).AuthorizationConfig(user_isolation=True),_scoped_read_user_id()pins non-adminget_sessions/get_session_runsto the caller's own id, andcontinue_run/cancel_runverify run ownership. Dash (like coda/scout) does not setuser_isolation, so a valid org token can read other users' sessions over MCP — the same exposure the REST API already has (no new privilege; tracked as a follow-up in the standardization proposal).Role scoping of the tool list: NO — static, not per-request. The tool surface is fixed at server build time (
build_mcp_server):MCPServerConfig.include_tags/exclude_tags/enable_builtin_toolsscope tools at registration, for all callers alike.tools/listreturns the same set to every authenticated caller; role differences surface as call-time authorization errors, not different tool lists. There is no per-request dynamic tool-filtering hook.The path for role-scoped surfaces (context is the template): context achieves owner/guest scoping not by filtering the built-in list per caller but by (a)
MCPServerConfig.authorize=_caller_is_owner— a per-call predicate middleware that 401s non-owners after JWT, before any tool runs (mcp.py:1034), and (b) a custom tool whoseuser_idparameter agno fills with the verified JWT subject and hides from the client-facing schema (_inject_user_id,mcp.py:121) — spoof-proof, with an in-tool role check as defense-in-depth. Apps that want per-role tool surfaces should follow that pattern (custom tools +authorizegate), not the generic 8. For dash's v1, the default config is correct: JWT gates/mcpin prd, the verified subject owns attribution, and scopes gate each call.One more spike finding worth recording: agno 2.7 disables fastmcp ≥3.4.3's built-in Host/Origin guard (
http_app(host_origin_protection=False),mcp.py:1140) and makes DNS-rebinding protection opt-in viaMCPServerConfig.allowed_hosts. Default config (dash) therefore has no host guard → no 421 risk behind Railway's proxy, but also no rebinding protection — same posture as theagentos-railwaystarter. If the public-domain probe below 4xxs anyway, copy context'sAGENTOS_URL-derivedallowed_hostspattern.Validation done (no secrets, no compose up)
./scripts/validate.sh— ruff + mypy clean (36 files).docker compose config— parses.RUNTIME_ENV=dev python -c "import app.main"against a throwaway pgvector — imports clean,enable_mcp_server=True, MCP app mounted (inner route/mcp), andbuild_mcp_server(agent_os)registers exactly the 8 expected tools:cancel_run, continue_run, get_agentos_config, get_session_runs, get_sessions, run_agent, run_team, run_workflow.docker compose up(no secrets in the fresh clone; shared ports), evals, deploy.Pre-existing note (bump branch, not this PR): import logs
Registry: multiple distinct databases share id 'dash-db'—get_postgres_db()constructs several instances with one id.Reviewer checklist — live verification
docker compose up -d --build, then./scripts/mcp_check.sh→ "MCP OK — 8 tools" + a real team answer./mcpover the PUBLIC domain, not localhost — the fastmcp 421 host-guard episode made localhost-green/prod-broken the signature failure mode. Handshake +list_toolsmust return the 8 tools viahttps://<railway-domain>/mcp.RUNTIME_ENV=prd, an unauthenticated/mcpcall is rejected; an authenticated call (PAT / JWT from os.agno.com) handshakes andrun_team("dash", ...)succeeds, with the run attributed to the token subject (check the session'suser_id).user_idin arun_agent/run_teamcall with a JWT — the run must be attributed to the JWT subject, not the passed id.allowed_hostspattern)./docs200, validate, smoke evals) before merging into the bump PR.🤖 Generated with Claude Code