-
Notifications
You must be signed in to change notification settings - Fork 19
feat(ensapi): introduce Model Context Protocol (MCP) server for Omnigraph API #2301
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
b419590
ed71208
fbade93
1d67e55
d7f01d0
d397758
9bddeb2
605d015
d793f4f
8b4c1f8
ab4d17d
9836b3e
8ce8d07
891db57
ba25bec
1509598
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "ensapi": patch | ||
| --- | ||
|
|
||
| Add Omnigraph MCP server at `/api/mcp` (streamable HTTP). MCP clients can run vetted example queries or custom GraphQL via `omnigraph_query`, with schema and example resources. Server name is `ENS`; task-specific MCP prompts removed. Initialize instructions require `exampleId` first and inline the full example catalog. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@ensnode/ensnode-sdk": patch | ||
| --- | ||
|
|
||
| Add offline Omnigraph schema lookup helpers (`lookupOmnigraphSchema`, `buildCondensedSchemaReference`) on `@ensnode/ensnode-sdk/internal` for enscli and ensskills. Bundle Omnigraph SDL in ensnode-sdk (generated from `enssdk/omnigraph/schema.graphql`) so schema loading works in Vite/Rollup builds without Node `createRequire`. | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -20,6 +20,7 @@ dist | |||||||||||||
| # Ponder | ||||||||||||||
| generated | ||||||||||||||
| !packages/enssdk/src/omnigraph/generated/ | ||||||||||||||
| !packages/ensnode-sdk/src/omnigraph-api/generated/ | ||||||||||||||
| .ponder | ||||||||||||||
|
|
||||||||||||||
| #jetbrains elements | ||||||||||||||
|
|
@@ -46,3 +47,7 @@ apps/ensrainbow/test-* | |||||||||||||
| apps/fallback-ensapi/dist | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| # Agent skills from npm packages (managed by skills-npm) | ||||||||||||||
| **/skills/npm-* | ||||||||||||||
| data-* | ||||||||||||||
|
Comment on lines
+51
to
+53
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Scope the new As written, Proposed fix- data-*
+ **/skills/npm-*/data-*📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| import { Client } from "@modelcontextprotocol/sdk/client/index.js"; | ||
| import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; | ||
| import { afterEach, beforeEach, describe, expect, it } from "vitest"; | ||
|
|
||
| const ensnodeUrl = process.env.ENSNODE_URL; | ||
| if (!ensnodeUrl) { | ||
| throw new Error( | ||
| "ENSNODE_URL environment variable must be configured for Omnigraph MCP integration tests to run.", | ||
| ); | ||
| } | ||
|
|
||
| const MCP_URL = new URL("/api/mcp", ensnodeUrl); | ||
|
|
||
| type TextContent = { type: "text"; text: string }; | ||
|
|
||
| /** Connects an MCP client to the live ENSApi `/api/mcp` endpoint over Streamable HTTP. */ | ||
| async function connect() { | ||
| const client = new Client({ name: "ensapi-integration-test", version: "0.0.0" }); | ||
| const transport = new StreamableHTTPClientTransport(MCP_URL); | ||
| await client.connect(transport); | ||
| return client; | ||
| } | ||
|
|
||
| /** Calls `omnigraph_query` and parses the single text content block as GraphQL JSON. */ | ||
| async function omnigraphQuery<T = unknown>( | ||
| client: Client, | ||
| query: string, | ||
| variables?: Record<string, unknown>, | ||
| ): Promise<{ data?: T; errors?: Array<{ message: string }> }> { | ||
| const result = await client.callTool({ | ||
| name: "omnigraph_query", | ||
| arguments: { query, variables }, | ||
| }); | ||
|
|
||
| const content = result.content as TextContent[]; | ||
| expect(content).toHaveLength(1); | ||
| expect(content[0].type).toBe("text"); | ||
| return JSON.parse(content[0].text); | ||
| } | ||
|
|
||
| describe("Omnigraph MCP API (/api/mcp)", () => { | ||
| let client: Client; | ||
|
|
||
| beforeEach(async () => { | ||
| client = await connect(); | ||
| }); | ||
|
|
||
| afterEach(async () => { | ||
| await client.close(); | ||
| }); | ||
|
|
||
| it("advertises the omnigraph_query tool", async () => { | ||
| const { tools } = await client.listTools(); | ||
| expect(tools.map((t) => t.name)).toEqual( | ||
| expect.arrayContaining(["omnigraph_query", "omnigraph_schema"]), | ||
| ); | ||
| }); | ||
|
|
||
| it("executes a trivial query end-to-end through Yoga", async () => { | ||
| const { data, errors } = await omnigraphQuery<{ __typename: string }>(client, "{ __typename }"); | ||
|
|
||
| expect(errors).toBeUndefined(); | ||
| expect(data).toEqual({ __typename: "Query" }); | ||
| }); | ||
|
|
||
| it("resolves a seeded devnet domain via Query.domain", async () => { | ||
| const { data, errors } = await omnigraphQuery<{ | ||
| domain: { canonical: { name: { interpreted: string } } } | null; | ||
| }>( | ||
| client, | ||
| /* GraphQL */ ` | ||
| query DomainByName($name: InterpretedName!) { | ||
| domain(by: { name: $name }) { | ||
| canonical { name { interpreted } } | ||
| } | ||
| } | ||
| `, | ||
| { name: "test.eth" }, | ||
| ); | ||
|
|
||
| expect(errors).toBeUndefined(); | ||
| expect(data).toMatchObject({ | ||
| domain: { canonical: { name: { interpreted: "test.eth" } } }, | ||
| }); | ||
| }); | ||
|
|
||
| it("surfaces GraphQL errors for invalid queries in the response payload", async () => { | ||
| const { errors } = await omnigraphQuery(client, "{ thisFieldDoesNotExist }"); | ||
| expect(errors).toBeDefined(); | ||
| expect(errors?.length ?? 0).toBeGreaterThan(0); | ||
| }); | ||
|
|
||
| it("runs a vetted example query by exampleId", async () => { | ||
| const result = await client.callTool({ | ||
| name: "omnigraph_query", | ||
| arguments: { | ||
| exampleId: "domain-by-name", | ||
| variables: { name: "test.eth" }, | ||
| }, | ||
| }); | ||
|
|
||
| const content = result.content as TextContent[]; | ||
| const { data, errors } = JSON.parse(content[0].text) as { | ||
| data?: { domain: { canonical: { name: { interpreted: string } } } | null }; | ||
| errors?: unknown[]; | ||
| }; | ||
|
|
||
| expect(errors).toBeUndefined(); | ||
| expect(data).toMatchObject({ | ||
| domain: { canonical: { name: { interpreted: "test.eth" } } }, | ||
| }); | ||
| }); | ||
| }); |
Uh oh!
There was an error while loading. Please reload this page.