Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
b419590
feat(ensapi): add MCP server for Omnigraph queries and update documen…
djstrong Jun 15, 2026
ed71208
feat(ensapi): integrate MCP API and add tests for Omnigraph queries
djstrong Jun 15, 2026
fbade93
feat(ensapi): update MCP API with new Omnigraph features and tests
djstrong Jun 16, 2026
1d67e55
feat(ensapi): add offline Omnigraph schema lookup helpers and enhance…
djstrong Jun 16, 2026
d7f01d0
feat(ensapi): refine Omnigraph schema input validation and enhance er…
djstrong Jun 16, 2026
d397758
feat(ensapi): add auto-generated Omnigraph schema SDL file for enhanc…
djstrong Jun 16, 2026
9bddeb2
fix(ensapi): improve error handling in MCP API and update schema SDL …
djstrong Jun 16, 2026
605d015
refactor(ensapi): update Omnigraph schema handling and CI configuration
djstrong Jun 17, 2026
d793f4f
feat(ensapi): enhance MCP API tests and update example queries
djstrong Jun 18, 2026
8b4c1f8
feat(ensapi): enhance MCP API with new filter input types and validat…
djstrong Jun 18, 2026
ab4d17d
feat(ensnode-sdk): add offline Omnigraph schema lookup helpers and bu…
djstrong Jun 18, 2026
9836b3e
feat(ensapi): add Omnigraph MCP server and enable custom GraphQL queries
djstrong Jun 18, 2026
8ce8d07
feat(ensapi): improve MCP API session management and enhance validati…
djstrong Jun 18, 2026
891db57
feat(ensapi): implement least-recently-used session eviction and upda…
djstrong Jun 18, 2026
ba25bec
feat(ensapi): update Omnigraph MCP server to use plain-language promp…
djstrong Jun 18, 2026
1509598
feat(ensapi): refine Omnigraph MCP server instructions and enhance in…
djstrong Jun 19, 2026
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
5 changes: 5 additions & 0 deletions .changeset/omnigraph-mcp.md
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.
5 changes: 5 additions & 0 deletions .changeset/omnigraph-schema-reference.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ensnode/ensnode-sdk": patch
---
Comment thread
djstrong marked this conversation as resolved.

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`.
4 changes: 2 additions & 2 deletions .github/workflows/test_ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,11 @@ jobs:

- name: Verify generated files are committed
run: |
if ! git diff --quiet packages/enssdk/src/omnigraph/generated/; then
if ! git diff --quiet packages/enssdk/src/omnigraph/generated/ packages/ensnode-sdk/src/omnigraph-api/generated/; then
echo "Error: Generated files are out of sync"
echo ""
echo "The following generated files differ from what is committed:"
git diff --name-status packages/enssdk/src/omnigraph/generated/
git diff --name-status packages/enssdk/src/omnigraph/generated/ packages/ensnode-sdk/src/omnigraph-api/generated/
echo ""
echo "To fix, run:"
echo " pnpm generate:gqlschema"
Expand Down
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ dist
# Ponder
generated
!packages/enssdk/src/omnigraph/generated/
!packages/ensnode-sdk/src/omnigraph-api/generated/
.ponder

#jetbrains elements
Expand All @@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Scope the new data-* ignore entry more narrowly.

As written, data-* ignores any matching file or directory anywhere in the repo, which can accidentally hide legitimate fixtures or docs assets. If these artifacts are only generated under the npm-managed skill tree, keep the pattern scoped there.

Proposed fix
- data-*
+ **/skills/npm-*/data-*
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Agent skills from npm packages (managed by skills-npm)
**/skills/npm-*
data-*
# Agent skills from npm packages (managed by skills-npm)
**/skills/npm-*
**/skills/npm-*/data-*
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.gitignore around lines 50 - 52, The `data-*` pattern on line 52 is too
broad and will ignore matching files or directories anywhere in the repository,
potentially hiding legitimate assets or fixtures. Scope this pattern more
narrowly by placing it under the same directory constraint as the npm skills
entry above it (the `**/skills/npm-*` pattern), so that `data-*` files are only
ignored when they exist within the npm-managed skills directory tree, not
globally across the entire repository.

2 changes: 2 additions & 0 deletions apps/ensapi/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,11 @@
"@ensnode/ensdb-sdk": "workspace:*",
"@ensnode/ensnode-sdk": "workspace:*",
"@ensnode/ponder-subgraph": "workspace:*",
"@hono/mcp": "^0.3.0",
"@hono/node-server": "catalog:",
"@hono/otel": "^0.2.2",
"@hono/zod-openapi": "^1.4.0",
"@modelcontextprotocol/sdk": "^1.29.0",
"@namehash/ens-referrals": "workspace:*",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/core": "^2.7.1",
Expand Down
113 changes: 113 additions & 0 deletions apps/ensapi/src/handlers/api/mcp/mcp-api.integration.test.ts
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" } } },
});
});
});
Loading