Skip to content
Draft
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
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ generate:

generate-go:
rm -rf router/gen && buf generate --path proto/wg/cosmo/node --path proto/wg/cosmo/common --path proto/wg/cosmo/graphqlmetrics --template buf.router.go.gen.yaml
buf generate router/proto --template buf.router.go.gen.yaml
rm -rf graphqlmetrics/gen && buf generate --path proto/wg/cosmo/graphqlmetrics --path proto/wg/cosmo/common --template buf.graphqlmetrics.go.gen.yaml
rm -rf connect-go/wg && buf generate --path proto/wg/cosmo/platform --path proto/wg/cosmo/notifications --path proto/wg/cosmo/common --path proto/wg/cosmo/node --template buf.connect-go.go.gen.yaml

Expand Down
4 changes: 4 additions & 0 deletions buf.router.go.gen.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ managed:
disable:
- file_option: go_package
module: buf.build/googleapis/googleapis
# Keep protovalidate on its published Go module. Without this the build
# fails on a missing gen/buf/validate package.
- file_option: go_package
module: buf.build/bufbuild/protovalidate
override:
- file_option: go_package_prefix
value: github.com/wundergraph/cosmo/router/gen/proto
Expand Down
11 changes: 11 additions & 0 deletions docs-website/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,17 @@
"router/mcp/quickstart",
"router/mcp/tools",
"router/mcp/configuration",
{
"group": "Schema Discovery",
"icon": "compass",
"pages": [
"router/mcp/schema-discovery/overview",
"router/mcp/schema-discovery/quickstart",
"router/mcp/schema-discovery/guides",
"router/mcp/schema-discovery/tools",
"router/mcp/schema-discovery/configuration"
]
},
{
"group": "OAuth 2.1",
"icon": "shield-check",
Expand Down
3 changes: 3 additions & 0 deletions docs-website/router/mcp.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ The Cosmo MCP Server builds on top of the concept of persisted operations (also
<Card title="AI Empowerment" icon="robot">
Empower AI assistants to work with your application's data through a standardized interface
</Card>
<Card title="Schema Discovery" icon="compass" href="/router/mcp/schema-discovery/overview">
Let a model search a large schema and generate a valid operation from a prompt, without the schema in its context
</Card>
</CardGroup>

## Get Started
Expand Down
4 changes: 4 additions & 0 deletions docs-website/router/mcp/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,12 @@ storage_providers:
| `expose_schema` | Enables the `get_schema` built-in tool, exposing the full GraphQL schema to MCP clients. | `false` |
| `omit_tool_name_prefix` | When enabled, MCP tool names omit the `execute_operation_` prefix. For example, `GetUser` becomes `get_user` instead of `execute_operation_get_user`. See [Tools - Omitting the Tool Name Prefix](/router/mcp/tools#omitting-the-tool-name-prefix). | `false` |

| `schema_discovery.enabled` | Enables the `search_schema`, `get_symbols` and `generate_query` built-in tools. They let a model search a large schema and generate a valid operation, without the schema in its context. See [Schema Discovery](/router/mcp/schema-discovery/configuration). | `false` |

For OAuth-specific configuration, see [OAuth 2.1 Authorization](/router/mcp/oauth/overview).

For schema discovery configuration, see [Schema Discovery - Configuration](/router/mcp/schema-discovery/configuration).

## Environment Variables

All MCP options can also be set via environment variables:
Expand Down
112 changes: 112 additions & 0 deletions docs-website/router/mcp/schema-discovery/configuration.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
---
title: 'Configuration'
description: 'Every configuration key and environment variable for MCP schema discovery.'
icon: 'sliders'
---

Schema discovery is configured under `mcp.schema_discovery`.

```yaml router.config.yaml
mcp:
enabled: true
expose_schema: false
schema_discovery:
enabled: true
url: 'https://discovery.example.com'
token: 'your-token'
request_timeout: 90s
index_poll_interval: 2s
index_timeout: 10m
```

## Keys

| Key | Type | Default | Environment variable |
| --------------------- | -------- | ------- | ------------------------------------------ |
| `enabled` | boolean | `false` | `MCP_SCHEMA_DISCOVERY_ENABLED` |
| `url` | string | | `MCP_SCHEMA_DISCOVERY_URL` |
| `token` | string | | `MCP_SCHEMA_DISCOVERY_TOKEN` |
| `request_timeout` | duration | `90s` | `MCP_SCHEMA_DISCOVERY_REQUEST_TIMEOUT` |
| `index_poll_interval` | duration | `2s` | `MCP_SCHEMA_DISCOVERY_INDEX_POLL_INTERVAL` |
| `index_timeout` | duration | `10m` | `MCP_SCHEMA_DISCOVERY_INDEX_TIMEOUT` |

### enabled

Turns schema discovery on. The router then indexes the client schema and registers the `search_schema`, `get_symbols` and `generate_query` tools.

### url

The base URL of the schema discovery service. Include the scheme.

The service speaks Connect over HTTP/1.1.

<Warning>
The router does not start when `enabled` is `true` and `url` is empty. This stops a server whose tools always fail.
</Warning>

### token

The bearer token for the service. The router sends it as `Authorization: Bearer <token>`.

An empty token sends no `Authorization` header. Use an empty token when your service runs with authentication off.

The router never writes the token to a log or to an error message.

### request_timeout

The timeout for one call to the service.

Query generation takes 10 to 30 seconds. A value below `60s` is too low.

This value also raises the write timeout of the MCP HTTP server, so a slow generation still reaches the caller.

### index_poll_interval

The wait between two index status reads while the router waits for a build.

### index_timeout

The router stops waiting for an index that does not become ready within this time.

A schema of 16,000 lines indexes in about 24 seconds. The default of `10m` only trips on a real fault.

## Interaction with other keys

### expose_schema

Set `expose_schema` to `false`.

`get_schema` returns the full schema. That is the context cost that schema discovery removes. The two settings do not fail together, but they work against each other.

### enable_arbitrary_operations

Set this to `true` when you want the agent to run the operation that `generate_query` returns.

Set it to `false` in production. Use the curated path instead: generate the operation in a development router, review it, publish it as a persisted operation, then deploy that. See [Guides](/router/mcp/schema-discovery/guides).

## Startup behaviour

The router does not wait for the index. It serves GraphQL from the first moment.

Read the log to follow the build.

```
INFO MCP schema discovery enabled url=https://discovery.example.com authenticated=true
INFO schema index is building index_id=sha256:6926769e...
INFO schema index is ready index_id=sha256:6926769e... symbol_count=318
```

The router reindexes after every schema change. An unchanged schema costs nothing, because the router compares the hash locally and makes no network call.

During a rebuild the previous index keeps serving. The router adopts the new index only when it is ready.

## Failure behaviour

| Condition | Result |
| ------------------------------------- | ------------------------------------------------------------------------- |
| The service is unreachable at startup | The router starts. The tools report that the index is not ready. |
| The build fails | The router logs the reason. The tools report that the index is not ready. |
| The build exceeds `index_timeout` | The router stops waiting and logs the reason. |
| The service drops an unused index | The router rebuilds it at the same address on the next tool call. |

A fault in the discovery service never stops the router from serving GraphQL.
158 changes: 158 additions & 0 deletions docs-website/router/mcp/schema-discovery/guides.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
---
title: 'Guides'
description: 'Find duplicate work before you build it, curate a generated operation into a tool, or ship one in a BFF.'
icon: 'list-check'
---

These guides assume a router that runs with schema discovery enabled. To set one up, see the [Quickstart](/router/mcp/schema-discovery/quickstart).

## Find out if a capability already exists

Two teams in a large organisation often add the same capability under different names. Search before you build a new field, a new resolver, or a new subgraph. Schema discovery finds the first one before you build the second.

### Step 1 - Search by intent

Describe the capability in your own words. Do not guess field names.

```json
{ "query": "customer billing address", "kinds": ["field"], "limit": 5 }
```

The search matches meaning, not text. It finds `Customer.invoiceAddress` and `Account.billingAddr` even though neither name contains your words. A text search over the schema finds neither.

### Step 2 - Ask for the operation you were about to build

```json
{ "prompt": "get the billing address and payment status for a customer" }
```

Read the result. It gives you a decision.

| Result | Meaning | What you do |
| -------------------------------------------- | ------------------------------ | ----------------------------------- |
| One or more `queries` | The capability exists today. | Use the operation. Do not build it. |
| Empty `queries` and one `unsatisfied` reason | The schema cannot answer this. | Build the capability. |

### Step 3 - Read the reason

The `unsatisfied` reason names what is missing.

```json
{
"unsatisfied": [
"The indexed schema exposes products, employees, and locations, but no invoice entity, billing address, or payment status."
]
}
```

Collect these reasons across your teams. They tell you what consumers want and your graph does not have.

### Limits

The index holds the composed schema. It cannot show a subgraph that nobody published yet. Another team can be halfway through the same work.

Check your schema registry as well, before you commit to a build.

## Turn a generated operation into a tool

Give an agent a curated tool instead of an open prompt.

The router generates an operation. The router never publishes it. You review the operation first, then publish it yourself. Your production router then exposes it as its own MCP tool.

### Step 1 - Generate in a development router

Run schema discovery in a development router. Send the prompt.

```json
{ "prompt": "list employees with their id, first name, last name and current mood" }
```

### Step 2 - Review the document

Read the `document` field. Check three things:

- The operation reads only the fields that you intend to expose.
- The operation is a `query` when you expect no side effect.
- The variables carry the filters that you want the caller to control.

Give the operation a clear name. The name becomes the tool name.

### Step 3 - Save the operation

Write the document to your MCP operations directory.

```graphql operations/ListEmployees.graphql
"""
Lists every employee with their id, name, and current mood.
"""
query ListEmployees {
employees {
id
details {
forename
surname
}
currentMood
}
}
```

The operation name sets the tool name. A file name is used only when the operation has no name. The `"""` docstring above the operation sets the tool description, so write it for the agent. A `#` comment does not work.

### Step 4 - Deploy to production

Deploy the operation to your production router. Turn schema discovery off there, and turn arbitrary operations off.

```yaml production.config.yaml
mcp:
enabled: true
enable_arbitrary_operations: false
expose_schema: false
schema_discovery:
enabled: false
```

Your production router now exposes one typed tool. It runs no arbitrary GraphQL, and it sends no schema to an external service.

This is the curated path. Discovery happens in development. Production runs only what you reviewed.

## Use a generated operation in a BFF

A generated operation drops straight into an application. The document is the request, and the variables schema types the inputs.

### Step 1 - Take both fields

A generated operation gives you two things:

- `document` is the operation text.
- `variablesSchema` is a JSON Schema for the variables.

### Step 2 - Send the document and the variables

```javascript
const response = await fetch('https://router.example.com/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: document,
variables: { limit: 10 },
}),
});
```

The operation is parameterized. Your prompt selected the shape. You supply the values at run time, so one operation serves many requests.

### Step 3 - Use the variables schema to type the inputs

The variables schema carries the descriptions and the allowed values from your GraphQL schema. Use it in two ways:

- Generate types for your application.
- Register the operation as a tool for a language model. The tool name comes from `operationName`, the description from `description`, and the input schema from `variablesSchema`.

A model then fills the variables correctly. It cannot invent a value for an enum, because the schema lists only the allowed names.

### Step 4 - Generate one time

Generation takes 10 to 30 seconds and uses a language model. Do not call it in your request path.

Generate the operation one time. Store the document. Ship it with your application.
Loading
Loading