PR #21942 Research: Shared Operations Layer for Internal and External AI Agents
PR Overview
| Field | Value |
|---|---|
| Author | dannon |
| State | MERGED |
| Created | 2026-02-26 |
| Merged | 2026-04-03 |
| Merge SHA | c8e1732a5a |
| Branch | agents |
Verified against origin/dev | 7765fae934 |
| Labels | area/API, area/admin, area/dependencies, area/documentation, area/testing, area/testing/api, area/testing/integration |
Builds on PR 21434 - AI Agent Framework and ChatGXY.
Summary
Adds AgentOperationsManager (lib/galaxy/agents/operations.py, ~875 lines) — a single layer wrapping Galaxy service-layer calls (tools, histories, jobs, datasets, workflows, invocations) for AI consumers — and exposes it externally via an in-process FastMCP server mounted on Galaxy’s FastAPI app at /api/mcp (configurable). Auth is API-key based via UserManager.by_api_key, with the key passed as a parameter on every MCP tool call rather than via transport headers. Internally, only the new HistoryAgent consumes the manager directly; other agents (router, error_analysis, custom_tool, orchestrator, tools) were slimmed but still reach Galaxy through GalaxyAgentDependencies. Enable via enable_mcp_server: true in galaxy.yml.
Architecture
AgentOperationsManager — lib/galaxy/agents/operations.py (NEW)
class AgentOperationsManager(app: MinimalManagerApp, trans: ProvidesUserContext)— line 50.- Lazy service properties (lines 89–149) resolve
tools_service,histories_service,jobs_service,datasets_service,workflows_service,invocations_service,hda_manager,dataset_collections_serviceviaself.app[ServiceClass]. - ID encoding helpers
_encode_id/_encode_ids_in_response(lines 65–87) walk dicts/lists and rewrite numeric values for keys inID_FIELDS(line 35:id,history_id,dataset_id,job_id,workflow_id,invocation_id,user_id,hda_id,hdca_id,collection_id,creating_job). - Operation methods (line numbers in
operations.py):connect151,search_tools172,get_tool_details194,list_histories233,run_tool260,get_job_status270,create_history281,get_history_details293,get_history_contents308,get_dataset_details358,get_collection_details374,upload_file_from_url395,list_workflows424,get_workflow_details460,invoke_workflow476,get_invocations504,get_invocation_details547,cancel_workflow_invocation562,get_tool_panel578,get_tool_run_examples586,get_tool_citations622,search_tools_by_keywords650,list_history_ids695,get_job_details717,get_job_errors745,peek_dataset_content780,download_dataset815,get_server_info842,get_user861.
MCP endpoint — lib/galaxy/webapps/galaxy/api/mcp.py (NEW, 436 lines)
get_mcp_app(gx_app)(line 87) setsfastmcp_settings.stateless_http = True(line 89) — required for multi-worker Gunicorn — instantiatesFastMCP("Galaxy")(line 91), and registers ~28@mcp.tool()callables.get_operations_manager(api_key, ctx)(line 96):UserManager.by_api_key→ on success buildsWorkRequestContext(app=gx_app, user=user, url_builder=url_builder)→ returnsAgentOperationsManager. Invalid/missing key raisesValueError.get_mcp_url_builder(fallback_base_url)(line 29) reads fastmcp’s private_current_http_requestContextVar to get the live request and produce a realUrlBuilder; falls back to a hand-writtenMCPUrlBuilder(line 40) with hardcoded shapes forhistory,history_contents,datasetand a generic/api/{name}fallback._mcp_error_handler(operation)(line 76) — context manager mappingValueErrorto operation-named errors.- Exposed MCP tools:
connect,search_tools,get_tool_details,list_histories,run_tool,get_job_status,create_history,get_history_details,get_history_contents,get_dataset_details,get_collection_details,upload_file_from_url,list_workflows,get_workflow_details,invoke_workflow,get_invocations,get_invocation_details,cancel_workflow_invocation,get_tool_panel,get_tool_run_examples,get_tool_citations,search_tools_by_keywords,list_history_ids,get_job_details,download_dataset,get_server_info,get_user. Note:peek_dataset_contentis on the manager but not exposed via MCP — only used by the internalHistoryAgent.
FastAPI integration — lib/galaxy/webapps/galaxy/fast_app.py
get_mcp_lifespan(gx_app)(line 264) early-returns(None, None)whenenable_mcp_serveris falsy. On success returns(mcp_app, mcp_app.lifespan). CatchesImportError(fastmcp missing) and genericException(line 277) so a broken MCP doesn’t crash startup.include_mcp(app, gx_app, mcp_app)(line 282):app.mount(gx_app.config.mcp_server_path, mcp_app).initialize_fast_app(line 295) wires MCP into acombined_lifespanasync context manager (lines 302–305) so FastMCP runs alongside Galaxy’s lifespan, then callsinclude_mcp(line 325) afterinclude_all_package_routersand before the WSGI mount at/.
Internal agents
lib/galaxy/agents/base.py(refactor +56/−257; current 628 lines):AgentTypeconstants (line 145, including newHISTORY = "history"at line 153),AgentResponse(157),GalaxyAgentDependenciesdataclass (180) carryingtrans,user,config,get_agentcallback, optional managers (job_manager,dataset_manager,workflow_manager,tool_cache,toolbox,model_factory).BaseGalaxyAgentABC at 197 with_create_agent,get_system_prompt,_validate_query(prompt-injection patterns at 229–237),_run_with_retryexponential backoff. Helpers:extract_result_content86,extract_usage_info95,extract_structured_output110,normalize_llm_text137.lib/galaxy/agents/history.py(NEW, 142 lines) —HistoryAgent(line 27) is the only internal consumer ofAgentOperationsManager.__init__(line 32) buildsself.ops = AgentOperationsManager(app=deps.trans.app, trans=deps.trans). pydantic-ai@agent.toolcallbacks wrap ops methods:list_user_histories → ops.list_histories,get_history_info → ops.get_history_details,list_datasets → ops.get_history_contents(limit=500, order=“hid-asc”),get_dataset_info → ops.get_dataset_details,get_job_for_dataset → ops.get_job_details,get_job_errors → ops.get_job_errors,get_tool_citations → ops.get_tool_citations,get_tool_info → ops.get_tool_details,peek_dataset_content → ops.peek_dataset_content.MalformedIdis caught and converted to{"error": ...}for the LLM.lib/galaxy/agents/router.py(+136/−171):QueryRouterAgent(line 37), pydantic-ai output-functions design with a deepseek branch (line 45) for models without structured output. Does not useAgentOperationsManager.lib/galaxy/agents/orchestrator.py(+88/−71):WorkflowOrchestratorAgent(line 47),AgentPlanBaseModel (39), sequential (189) and parallel (226) execution paths viadeps.get_agent. Calls sub-agents, not the ops manager.lib/galaxy/agents/error_analysis.py(+32/−154):ErrorAnalysisResult(line 35),ErrorAnalysisAgent(47) — structured-output vs text-fallback branches.lib/galaxy/agents/tools.py(+13/−74):ToolRecommendationAgent(line 48), reaches the toolbox viadeps.lib/galaxy/agents/custom_tool.py(+13/−30):CustomToolAgent(line 35), structured output required (_requires_structured_outputline 45).lib/galaxy/agents/registry.py(+4/−38):AgentRegistry(line 18).build_default_registry(config=None)(88) registersHistoryAgent(131); other agents gated by_is_enabled(108) reading config keys.
Config & dependency
lib/galaxy/config/schemas/config_schema.ymllines 4309–4331:enable_mcp_server(bool, defaultfalse, line 4309) andmcp_server_path(str, default/api/mcp, line 4323).lib/galaxy/config/sample/galaxy.yml.samplelines 3152–3159: commented examples for both keys.lib/galaxy/dependencies/conditional-requirements.txtline 19:fastmcp>=2.13.0(note: PR body imprecisely references “mcp” — the dependency isfastmcp, not the lower-levelmcppackage).lib/galaxy/dependencies/__init__.pylines 313–314:check_fastmcp(self): return asbool(self.config.get("enable_mcp_server", False)).
Tests
test/unit/app/managers/test_AgentOperationsManager.py(NEW, 318 lines):TestAgentOperationsManagerBasic(line 9) coversconnect,connect_requires_user,get_user,get_server_info.TestAgentOperationsManagerWithMockedServices(49) covers create/list_histories (with name filter),get_collection_detailselement truncation @ 500,get_workflow_detailswith version,invoke_workflowwith history-name + history-required validation,get_history_contentsfilters,get_tool_run_exampleswith version,search_tools,get_tool_details(incl. not-found),run_tool,get_job_status— all against mocked services.test/integration/test_agents.py(+263 lines, current 404): pre-existingTestAgentsApi(47) andTestAgentsApiLiveLLM(73, gated on live LLM env). NewTestAgentOperationsManagerEncoding(175) asserts ID encoding (nested ints, preserves non-ID fields, idempotent on already-encoded IDs). NewTestMCPServerSmoke(274) setsenable_mcp_server: True(284) andtest_mcp_server_initializes(305) confirms the FastMCP instance exists when enabled.test/unit/app/test_agents.py(+70/−23, 651 lines): config fallback chain, custom_tool structured output, registry build/disabled-agent behavior, error_analysis admin suggestions, router/orchestrator withTestModel, prompt-injection rejection, sequential/parallel orchestration. No MCP cases.
Cross-checks vs PR body
- Endpoint at
/api/mcp: VERIFIED. Defaultmcp_server_path/api/mcp(config_schema.yml:4325); mount atfast_app.py:288. enable_mcp_server/mcp_server_pathconfig keys: VERIFIED.- “Shared layer used by both internal and external agents”: PARTIALLY TRUE. Used by
api/mcp.pyandagents/history.py. Router, orchestrator, error_analysis, custom_tool, and tool_recommendation were slimmed but continue to access Galaxy viaGalaxyAgentDependencies, notAgentOperationsManager. PR body reads broader than reality. - Conditional dep is
mcppackage: FALSE. Added line isfastmcp>=2.13.0—fastmcp(a higher-level wrapper), not the lower-levelmcppackage. - Stateless HTTP for multi-worker: VERIFIED at
mcp.py:89. - MCP “inherits Galaxy auth/CORS/rate-limiting”: PARTIALLY VERIFIED. Auth is API-key only (passed as a tool parameter, not transport header). CORS inheritance via
app.mountis plausible. Rate limiting (slowapiLimiter) hooks routes by name and is unlikely to apply to mounted sub-app endpoints — claim should be treated skeptically; not asserted by tests.
Unresolved questions
- Why does the PR title promise a shared layer “for internal and external” while only
HistoryAgentactually adoptsAgentOperationsManager? Are router/error_analysis/orchestrator scheduled to migrate, or is the manager intentionally external-facing? peek_dataset_contentis on the ops manager but not exposed via MCP — intentional content-leak guard, or oversight?MCPUrlBuilderfallback only handleshistory,history_contents,dataset; everything else collapses to/api/{name}. What breaks when ops methods need other route names off-request (background tasks)?- Reliance on
fastmcp.server.http._current_http_request(private API;mcp.py:32comment acknowledges) — maintenance risk on fastmcp >2.13? - API-key-as-parameter on every MCP tool call vs header auth — most MCP clients pass auth via transport. Rationale?
fastmcp_settings.stateless_http = Trueis a module-level flag — does this leak into other potential MCP integrations or shared-process tests?- Rate-limiting/CORS coverage of the mounted MCP sub-app is unverified by tests; smoke test only checks server initialization.
fastmcp>=2.13.0floor is recent — documented compatibility window?- Local file upload doesn’t translate (per PR body); is
upload_file_from_urlthe only intended ingest path, or is there a tracking issue?
Changes since merge
git log c8e1732a5a..origin/dev -- lib/galaxy/agents/ lib/galaxy/webapps/galaxy/api/mcp.py returns one commit:
dbfcdb8bc9— “python packages: convert to pure namespace packages” (mechanical packaging-only; no behavioral or rename impact).
fast_app.py and config_schema.yml saw subsequent activity (statsd middleware, webdav refactor, aiocop middleware, celery rate-limiting) but none touched the MCP-mount path or the two new config keys.
Related
- PR 21434 - AI Agent Framework and ChatGXY — original framework refactored here.
- PR 21692 - Standardize Agent API Schemas — agent response schemas; ops manager returns dicts that bypass these schemas.
- PR 21706 - Data Analysis Agent Integration — sibling agent addition (DSPy / Pyodide).
- PR 21463 - Jupyternaut Adapter for JupyterLite — comparable external AI integration mounting/auth pattern.
- Component - Agents Backend — backend agent architecture; needs revision for ops manager + HistoryAgent.
- Component - Agents UX — UX surfaces; MCP is a new external surface alongside ChatGXY/wizard/Jupyternaut.
- Component - Agents ChatGXY Persistence — ChatGXY persistence; MCP is the non-persistent peer entry point.