Skip to content

Commit b419590

Browse files
committed
feat(ensapi): add MCP server for Omnigraph queries and update documentation
- Introduced a new MCP server for handling Omnigraph queries at the `/api/mcp` endpoint. - Updated OpenAPI document to exclude the new MCP endpoint from generated documentation. - Added documentation for integrating with the Omnigraph MCP, including usage instructions for clients like Cursor and Claude. - Enhanced existing documentation with a new link card for the Omnigraph MCP.
1 parent b41afb4 commit b419590

5 files changed

Lines changed: 263 additions & 0 deletions

File tree

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,7 @@ apps/ensrainbow/test-*
4646
apps/fallback-ensapi/dist
4747

4848

49+
50+
# Agent skills from npm packages (managed by skills-npm)
51+
**/skills/npm-*
52+
data-*
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import packageJson from "@/../package.json" with { type: "json" };
2+
3+
import { StreamableHTTPTransport } from "@hono/mcp";
4+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5+
import { Hono } from "hono";
6+
import { z } from "zod/v4";
7+
8+
const OmnigraphQueryInputSchema = z.object({
9+
query: z.string().describe("The GraphQL query document to execute."),
10+
variables: z
11+
.record(z.string(), z.unknown())
12+
.optional()
13+
.describe("Optional GraphQL variables, keyed by variable name."),
14+
});
15+
16+
/** GraphQL-over-HTTP request body; always includes `variables` (`null` when omitted). */
17+
function buildGraphQLRequestBody(query: string, variables?: Record<string, unknown>): string {
18+
return JSON.stringify({ query, variables: variables ?? null });
19+
}
20+
21+
/**
22+
* Builds the ENSNode Omnigraph MCP server and registers its tools.
23+
*
24+
* Exported so tests can drive the server over an in-memory transport without the HTTP layer.
25+
*/
26+
export function createOmnigraphMcpServer(): McpServer {
27+
const server = new McpServer({
28+
name: "ensnode-omnigraph",
29+
version: packageJson.version,
30+
});
31+
32+
server.registerTool(
33+
"omnigraph_query",
34+
{
35+
title: "Run an ENS Omnigraph GraphQL query",
36+
description:
37+
"Execute a read-only GraphQL query against this ENSNode instance's ENS Omnigraph API - the " +
38+
"unified ENSv1 + ENSv2 data model. Returns the raw GraphQL JSON response (`{ data, errors }`). " +
39+
"Use the `omnigraph` agent skill and the GraphiQL playground at `/api/omnigraph` to discover " +
40+
"the schema and author queries.",
41+
inputSchema: OmnigraphQueryInputSchema,
42+
},
43+
async ({ query, variables }) => {
44+
// Defer importing yoga to runtime (matching @/handlers/api/omnigraph/omnigraph-api) so the
45+
// module's Namechain datasource requirement is only triggered at request time.
46+
const { yoga } = await import("@/omnigraph-api/yoga");
47+
48+
// Execute against the in-process Yoga instance rather than looping back over HTTP. This reuses
49+
// the Pothos schema, per-request context creation, and Yoga's error masking.
50+
//
51+
// NOTE: this bypasses the indexing-status middleware on the `/api/omnigraph` HTTP route, so
52+
// queries run against whatever the index currently holds. `canAccelerate` only affects the
53+
// Resolution API, so `false` is correct for generic Omnigraph queries.
54+
const response = await yoga.fetch(
55+
new Request("http://ensapi.internal/api/omnigraph", {
56+
method: "POST",
57+
headers: { "content-type": "application/json", accept: "application/json" },
58+
body: buildGraphQLRequestBody(query, variables),
59+
}),
60+
{ canAccelerate: false },
61+
);
62+
63+
return {
64+
content: [{ type: "text", text: await response.text() }],
65+
};
66+
},
67+
);
68+
69+
return server;
70+
}
71+
72+
/**
73+
* The single, stateless MCP server + transport for this ENSApi process.
74+
*
75+
* Stateless mode (no `sessionIdGenerator`) lets one server and one transport be shared across all
76+
* requests, which is appropriate for read-only query tools.
77+
*/
78+
const mcpServer = createOmnigraphMcpServer();
79+
const transport = new StreamableHTTPTransport();
80+
81+
/** In-flight `connect()` shared across concurrent cold-start requests. */
82+
let mcpConnectPromise: Promise<void> | undefined;
83+
84+
function ensureMcpConnected(): Promise<void> {
85+
if (mcpServer.isConnected()) {
86+
return Promise.resolve();
87+
}
88+
89+
mcpConnectPromise ??= mcpServer.connect(transport).catch((error) => {
90+
mcpConnectPromise = undefined;
91+
throw error;
92+
});
93+
94+
return mcpConnectPromise;
95+
}
96+
97+
const app = new Hono();
98+
99+
app.all("/", async (c) => {
100+
await ensureMcpConnected();
101+
102+
// `handleRequest` may return undefined for messages that have no HTTP body (e.g. notifications).
103+
const response = await transport.handleRequest(c);
104+
return response ?? c.body(null, 202);
105+
});
106+
107+
export default app;

apps/ensapi/src/openapi-document.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import { openapiMeta } from "@/openapi-meta";
66
* Deprecated endpoints to exclude from the generated OpenAPI document.
77
*/
88
const HIDE_OPENAPI_ENDPOINTS: string[] = [
9+
// MCP (Model Context Protocol) over streamable HTTP — not a REST/OpenAPI surface.
10+
"/api/mcp",
911
// TODO: remove /amirealtime once the legacy endpoint is deleted.
1012
"/amirealtime",
1113
// TODO: remove all other endpoints from this list once the legacy endpoints are deleted.

docs/ensnode.io/src/content/docs/docs/integrate/ai-llm.mdx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,12 @@ and how many other domains do they own?
6161
href="/docs/integrate/integration-options/enscli"
6262
/>
6363

64+
<LinkCard
65+
title="ENS Omnigraph MCP"
66+
description="Query the Omnigraph from Cursor, Claude, or any MCP client — built into every ENSNode instance at /api/mcp."
67+
href="/docs/integrate/integration-options/omnigraph-mcp"
68+
/>
69+
6470
<LinkCard
6571
title="ensskills"
6672
description="Skill bundles that give AI agents an opinionated contract for ENS."
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
---
2+
title: ENS Omnigraph MCP (AI agents)
3+
description: Query the ENS Omnigraph from any MCP client — first-party, built into every ENSNode instance at /api/mcp.
4+
---
5+
6+
import { Aside, LinkCard } from "@astrojs/starlight/components";
7+
import IntegrateHostedEnsNodeTip from "@components/molecules/IntegrateHostedEnsNodeTip.astro";
8+
9+
Every ENSNode instance exposes the [ENS Omnigraph API](/docs/integrate/integration-options/omnigraph-graphql-api) over the [Model Context Protocol](https://modelcontextprotocol.io/) at **`/api/mcp`**, using the streamable-HTTP transport. This lets MCP clients — [Cursor](https://www.cursor.com/), [Claude Desktop](https://claude.ai/download), and any agent that speaks MCP — run Omnigraph queries directly.
10+
11+
It's first-party and built in: the same endpoint works locally (`http://localhost:4334/api/mcp`) and against any hosted instance, so there's nothing to install or deploy — point your client at a URL and go.
12+
13+
<IntegrateHostedEnsNodeTip />
14+
15+
## The tool
16+
17+
The server exposes a single, read-only tool:
18+
19+
- **`omnigraph_query`** — accepts a GraphQL `query` (and optional `variables`) and returns the raw `{ data, errors }` JSON, exactly as the [HTTP endpoint](/docs/integrate/integration-options/omnigraph-graphql-api#1-the-endpoint) does. Because it's the full Omnigraph behind one tool, an agent can answer any question the Omnigraph can — resolve records, search Domains, list a user's Domains, and much more — without a fixed, hand-written tool per use case.
20+
21+
## Cursor
22+
23+
Cursor supports streamable-HTTP MCP servers natively via a `url` in `mcp.json`.
24+
25+
**Project-scoped** (recommended when working in one repo) — create `.cursor/mcp.json` in the project root:
26+
27+
```json title=".cursor/mcp.json"
28+
{
29+
"mcpServers": {
30+
"ensnode-omnigraph": {
31+
"url": "https://api.v2-sepolia.ensnode.io/api/mcp"
32+
}
33+
}
34+
}
35+
```
36+
37+
**User-scoped** (all projects) — use `~/.cursor/mcp.json` with the same shape.
38+
39+
For a local ENSApi instance:
40+
41+
```json title=".cursor/mcp.json"
42+
{
43+
"mcpServers": {
44+
"ensnode-omnigraph": {
45+
"url": "http://localhost:4334/api/mcp"
46+
}
47+
}
48+
}
49+
```
50+
51+
Then:
52+
53+
1. Open **Cursor Settings → Tools & MCP** (or **Features → MCP**).
54+
2. Confirm `ensnode-omnigraph` is listed with a green status and the `omnigraph_query` tool.
55+
3. In chat, ask ENS questions in plain language — e.g. _"Who owns vitalik.eth?"_ or _"List domains owned by 0x…"_ — and let the agent call `omnigraph_query`.
56+
57+
<Aside type="tip" title="Pair with ensskills">
58+
MCP gives the agent **execution**; [`ensskills`](/docs/integrate/integration-options/ensskills) gives it **query authoring**. Install the **`omnigraph`** skill so the agent writes correct GraphQL on the first try. In this monorepo, `pnpm install` already symlinks `ensskills` into `.cursor/skills/`.
59+
</Aside>
60+
61+
## Claude Desktop
62+
63+
Claude Desktop configures MCP servers in a JSON file (restart Claude after editing):
64+
65+
| OS | Config path |
66+
| ------- | ----------------------------------------------------------------- |
67+
| macOS | `~/Library/Application Support/Claude/claude_desktop_config.json` |
68+
| Windows | `%APPDATA%\Claude\claude_desktop_config.json` |
69+
70+
Many Claude Desktop builds expect **stdio** MCP servers rather than a remote URL. Bridge to ENSNode's streamable-HTTP endpoint with [`mcp-remote`](https://www.npmjs.com/package/mcp-remote):
71+
72+
```json title="claude_desktop_config.json"
73+
{
74+
"mcpServers": {
75+
"ensnode-omnigraph": {
76+
"command": "npx",
77+
"args": [
78+
"-y",
79+
"mcp-remote",
80+
"https://api.v2-sepolia.ensnode.io/api/mcp"
81+
]
82+
}
83+
}
84+
}
85+
```
86+
87+
For local development, swap the URL for `http://localhost:4334/api/mcp`.
88+
89+
After restart, start a new conversation and ask ENS questions — Claude should invoke `omnigraph_query` when it needs indexed ENS data.
90+
91+
<Aside type="note" title="Claude Code / other MCP clients">
92+
Clients that support streamable HTTP directly (like Cursor) can use the `"url"` form instead of `mcp-remote`. Check your client's MCP docs for whether it accepts a remote `url` or requires a stdio `command`.
93+
</Aside>
94+
95+
## Verify the connection
96+
97+
To smoke-test without an editor, use the [MCP Inspector](https://github.com/modelcontextprotocol/inspector):
98+
99+
```bash
100+
npx @modelcontextprotocol/inspector
101+
```
102+
103+
Set **Transport** to **Streamable HTTP**, **URL** to your `/api/mcp` endpoint, connect, then **List Tools** — you should see `omnigraph_query`. Call it with:
104+
105+
```json
106+
{
107+
"query": "{ __typename }"
108+
}
109+
```
110+
111+
## Authoring queries
112+
113+
The MCP endpoint runs whatever GraphQL you hand it — it does not author queries for you. To discover the schema and write correct queries:
114+
115+
- Explore interactively in the **GraphiQL playground** at `/api/omnigraph`.
116+
- Browse the [Omnigraph Schema Reference](/docs/integrate/omnigraph/schema-reference) and [Omnigraph examples](/docs/integrate/omnigraph/examples).
117+
- Give your agent the [`ensskills`](/docs/integrate/integration-options/ensskills) **`omnigraph`** skill, which teaches the unified ENSv1 + ENSv2 datamodel, resolution, pagination, and vetted example queries — so it writes queries that work the first time.
118+
119+
<Aside type="note" title="Read-only">
120+
The Omnigraph is a read-only API, and so is this MCP server. It indexes and serves ENS state; it does not send transactions or mutate onchain data.
121+
</Aside>
122+
123+
## Where to go next
124+
125+
- Want to call the same API over plain HTTP, `curl`, or your own GraphQL client? See the [ENS Omnigraph API (GraphQL)](/docs/integrate/integration-options/omnigraph-graphql-api) guide.
126+
- Building integration code rather than driving an agent? Use [`enssdk`](/docs/integrate/integration-options/enssdk) (typed client) or [`enskit`](/docs/integrate/integration-options/enskit) (React).
127+
128+
<LinkCard
129+
title="ENS Omnigraph API (GraphQL)"
130+
description="The HTTP GraphQL API this MCP server wraps."
131+
href="/docs/integrate/integration-options/omnigraph-graphql-api"
132+
/>
133+
134+
<LinkCard
135+
title="ensskills (AI agents)"
136+
description="Agent skills that teach how to author Omnigraph queries."
137+
href="/docs/integrate/integration-options/ensskills"
138+
/>
139+
140+
<LinkCard
141+
title="Model Context Protocol"
142+
description="The open protocol this endpoint implements."
143+
href="https://modelcontextprotocol.io/"
144+
/>

0 commit comments

Comments
 (0)