Register Orbit tools in the main MCP server
What does this MR do and why?
This MR adds the Orbit tools to the main GitLab MCP server at POST /api/v4/mcp. The tools are in a new opt-in toolset, orbit. It is not a default toolset. Amanda Rueda made this decision on 2026-10-06 (see the Slack thread copy).
Terms:
- Orbit command: a function of the Orbit server, for example
query_graphorget_graph_schema. The Orbit server returns the list of commands fromListAgentCommands. - Orbit tool: one MCP tool for each Orbit command. The Orbit server sets the name, the description, and the input schema. Rails hard-codes no Orbit tool.
The Orbit gate uses the same checks as the Orbit endpoints. The tools are listed and run only when all of these conditions are true.
- Orbit is set up on the instance, and the user has Orbit access (
accessible_for?). This covers the licence, the plan tier on GitLab.com, theorbit_promoadd-on, and Orbit trials. - The user has at least the Reporter role in a group that has Orbit turned on (
has_enabled_namespaces?). - A fine-grained token has the
read_orbitpermission, as on the Orbit endpoints.
A user who fails the gate does not see the tools. A direct tools/call returns Tool 'query_graph' not found.
Changes
Mcp::Tools::RequestContextis a typedDataobject withcurrent_user,access_token, andrequest.lib/api/mcp/base.rbbuilds it once for each request and passes it ascontext:to the handlers and the manager.Mcp::Tools::Managergets auser_tools(context, toolsets)hook. EE fills it only when the request selects theorbittoolset, or whentools/callnames a tool that is not static. Static tools win on a name or alias clash. Callers with no context get only the static tools.CommandCatalogruns the gate and builds oneCommandToolfor each command. It caches the command list inRails.cachefor 10 minutes, one entry per query language. If the Orbit server is down, the other tools are still listed.CommandInterceptor#invoke_orbit_commandis now the one place that runs an Orbit command:query_graphreturns anOrbitQuery, and other commands callInvokeAgentCommand. The new tools,/api/v4/orbit/mcp, and the/api/v4/orbitagent command routes all use it. This removes two copies of the same code and theInterceptResultandCommandResponsestructs.render_mcp_resultinlib/api/mcp/base.rbrenders the JSON-RPC result. The EE override sends anOrbitQuerytorender_orbit_query!, as the Orbit endpoints do. Soorbit_query_in_railsdecides if Rails or Workhorse runs the query stream.RequestContext.for_mcp_requestsets the billingsource_typethe same way for both MCP endpoints. A token with theai_workflowsscope counts asdws.ConnectionError::GENERIC_MESSAGEholds the generic gRPC error text in one place. A client error with details returns the Orbit error details. Any other error returns the generic message.ListAgentCommandshas a 2 second deadline, so a slow Orbit server does not holdtools/list.CommandTooldecodes a JSON DSLquerythat a client sends as a JSON string, as/api/v4/orbit/mcpdoes.
Toolset headers filter only tools/list, as for the other opt-in toolsets. A direct tools/call of an Orbit tool still runs the full gate.
Duo Workflow Service is not changed. It keeps using /api/v4/orbit/mcp. It builds its gitlab tool list from the static Manager#list_tools and sends no toolset header, so it never gets Orbit tools from /api/v4/mcp.
#631630 plans to pass the access token to MCP tools. With this MR, that work can read context.access_token.
References
- Issue: gitlab-org/orbit/knowledge-graph#1294 (closed)
- Epic: gitlab-org#23430 (closed)
- Toolsets: #607755 (closed)
- Availability pattern: !259715 (merged)
Screenshots or screen recordings
No UI changes.
How to set up and validate locally
Tested end to end on a GDK with the full Orbit stack (Orbit server, Siphon, ClickHouse, Workhorse) in SaaS mode. No stubs.
-
Make two personal access tokens with the
apiandmcpscopes: one for a user with Orbit, and one for a user with no Orbit-enabled group. -
Send
tools/listwith and without the toolset header:curl -sk https://gdk.test:3443/api/v4/mcp -H "Authorization: Bearer $TOKEN" \ -H 'Content-Type: application/json' -H 'X-Gitlab-Enabled-Mcp-Server-Toolsets: orbit' \ -d '{"jsonrpc":"2.0","method":"tools/list","id":"1"}' | jq '[.result.tools[].name]' -
Call
query_graph:curl -sk https://gdk.test:3443/api/v4/mcp -H "Authorization: Bearer $TOKEN" \ -H 'Content-Type: application/json' \ -d '{"jsonrpc":"2.0","method":"tools/call","id":7,"params":{"name":"query_graph","arguments":{"query":{"query_type":"traversal","nodes":[{"id":"p","entity":"Project","filters":{"full_path":{"starts_with":"gitlab-org/"}},"columns":["name","full_path"]}],"limit":3}}}}'
End-to-end results
tools/list:
| Token | No header | orbit |
all |
|---|---|---|---|
| Admin with Orbit | 41 tools, no Orbit tools | 5 tools: meta + 4 Orbit tools |
50 tools, 4 Orbit tools |
| User with no Orbit-enabled group | 36 tools, no Orbit tools | 1 tool (meta) |
41 tools, no Orbit tools |
The 4 Orbit tools are query_graph, get_graph_schema, get_query_dsl, and get_response_format. Each has the readOnlyHint annotation. Each description is 268 characters or less.
tools/call:
query_graphwith integer id7returns real rows:{"jsonrpc":"2.0","id":7,"result":{"isError":false,...}}with 3 projects. This is true withorbit_query_in_railson and off, and with the JSON DSL and GQL. The send-data header does not reach the client.query_graphwith the JSON DSL query as a string returns the same 3 projects.- A class pushed to a new project is found through
query_graphabout 60 seconds after the push. get_graph_schemawith noargumentsreturns the 1,310-character schema.- The user with no Orbit-enabled group gets
-32602 Tool 'query_graph' not found. - A missing
queryreturns the tool errorValidation error: query is missing.
Gate states for a non-admin owner of one group, with the live stack. Each row checks tools/list and tools/call.
| State | Tools listed | Call |
|---|---|---|
| No Orbit-enabled group | No | Not found |
| Orbit turned on, promo gate off | Yes | Runs |
| Promo gate on, no add-on, no trial | No | Not found |
| Promo gate on, trial in progress | Yes | Runs |
| Promo gate on, trial ended | No | Not found |
Promo gate on, active orbit_promo add-on |
Yes | Runs |
Promo gate on, expired orbit_promo add-on |
No | Not found |
Fine-grained token without read_orbit |
No | Not found |
Fine-grained token with read_orbit |
Yes | Runs |
This table shows the database queries for tools/list with no header, from api_json.log in the steady state.
| Token | master |
This branch |
|---|---|---|
| Admin with Orbit | 24 | 24 |
| User with no Orbit-enabled group | 12 | 12 |
Specs after the rebase: 550 examples, 0 failures across the MCP, Orbit MCP, Orbit REST, and Orbit tool specs.
I checked the existing paths again on this commit with real Orbit data.
/api/v4/orbit/mcp:initialize,tools/list,list_commands,get_graph_schema, andquery_graphall return 200 with real rows, with anmcptoken and with anmcp_orbittoken./api/v4/orbitagent commands:query_graph,get_graph_schema, andget_query_dslreturn 200 with the same body shapes as onmaster.- Duo Workflow Service agent chat: the agent calls
orbit_list_commands, theninvoke_commandforget_graph_schema,get_query_dsl, andquery_graph, and answers with real projects. All Orbit calls go to/api/v4/orbit/mcp. - A replay of the Duo Workflow Service
tools/listcall to/api/v4/mcp, with a realai_workflowstoken, returns no Orbit tools. This holds with no tools header and with the header for an agent that selects all 56 tools. It also holds when the 4 Orbit names are added to that header.
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.