From 7873995dace1c1c43b5a95105d6814a19870b1f4 Mon Sep 17 00:00:00 2001 From: Trailgenic Date: Mon, 13 Jul 2026 15:41:52 -0700 Subject: [PATCH] Use official MCP SDK for Ella server conformance --- .env.example | 3 + .github/workflows/ci.yml | 21 +++ app/entity.json/route.ts | 24 +-- app/mcp/route.ts | 311 +++++++++++---------------------------- app/system/mcp/page.tsx | 96 ++++-------- docs/mcp-deployment.md | 9 ++ lib/ella-mcp-schemas.ts | 102 +++++++++++++ lib/ella-mcp-server.ts | 287 ++++++++++++++++++++++++++++++++++++ lib/ella-registry.ts | 102 +++++++++++++ package.json | 12 +- tests/mcp.test.ts | 176 ++++++++++++++++++++++ 11 files changed, 823 insertions(+), 320 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 docs/mcp-deployment.md create mode 100644 lib/ella-mcp-schemas.ts create mode 100644 lib/ella-mcp-server.ts create mode 100644 lib/ella-registry.ts create mode 100644 tests/mcp.test.ts diff --git a/.env.example b/.env.example index 9c04864..5c36c83 100644 --- a/.env.example +++ b/.env.example @@ -2,3 +2,6 @@ ANTHROPIC_API_KEY= OPENAI_API_KEY= TRACE_STORAGE_URL= DEFAULT_CONFIDENCE_THRESHOLD=0.75 + +# Comma-separated browser origins allowed to call the public read-only MCP endpoint. +MCP_ALLOWED_ORIGINS=https://ellaentity.ai,https://www.ellaentity.ai,https://mcp.ellaentity.ai diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..d9deb18 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,21 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +jobs: + verify: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '24' + cache: npm + - run: npm ci + - run: npm run lint + - run: npm run typecheck + - run: npm test + - run: npm run build diff --git a/app/entity.json/route.ts b/app/entity.json/route.ts index f371352..ee7177e 100644 --- a/app/entity.json/route.ts +++ b/app/entity.json/route.ts @@ -1,28 +1,8 @@ import { NextResponse } from 'next/server' -import { ELLA_GLOBAL_SCHEMA, ELLA_MCP_SCHEMA, ELLA_ORG_SCHEMA, ELLA_SYSTEM_SCHEMA } from '@/app/schema/ella' +import { ellaEntityGraph } from '@/lib/ella-registry' export function GET() { - const graph = { - '@context': 'https://schema.org/', - '@graph': [ELLA_GLOBAL_SCHEMA, ELLA_ORG_SCHEMA, ELLA_SYSTEM_SCHEMA, ELLA_MCP_SCHEMA].flatMap((schema) => { - if (Array.isArray(schema)) { - return schema.flatMap((entry) => { - if (entry && typeof entry === 'object' && '@graph' in entry) { - return (entry as { '@graph': unknown[] })['@graph'] - } - return [entry] - }) - } - - if (schema && typeof schema === 'object' && '@graph' in schema) { - return (schema as { '@graph': unknown[] })['@graph'] - } - - return [schema] - }), - } - - return NextResponse.json(graph, { + return NextResponse.json(ellaEntityGraph(), { headers: { 'Content-Type': 'application/ld+json', 'Cache-Control': 'public, max-age=3600', diff --git a/app/mcp/route.ts b/app/mcp/route.ts index 0fb917f..d8aa4ea 100644 --- a/app/mcp/route.ts +++ b/app/mcp/route.ts @@ -1,271 +1,124 @@ import { NextResponse } from 'next/server' -import { - ELLA_COCOGNITION, - ELLA_DOMAINS, - ELLA_FRAMEWORKS, - ELLA_IDENTITY, - ELLA_SURFACES, - ELLA_WORKS, -} from '@/lib/entity-data' +import { handleEllaMcpPost } from '../../lib/ella-mcp-server' -const PROTOCOL_VERSION = '2025-06-18' -const SERVER_INFO = { name: 'ellaentity-mcp', version: '1.0.0' } -const TOOL_NAMES = [ - 'ella.identity.get', - 'ella.domains.get', - 'ella.frameworks.get', - 'ella.works.get', - 'ella.collaboration.get', -] as const -const DOMAIN_NAMES = ['longevity', 'environment', 'sleep', 'ai-frameworks'] as const -const FRAMEWORK_SLUGS = ['four-forces-of-ai-power'] as const +export const runtime = 'nodejs' -const corsHeaders = { - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Methods': 'POST, GET, OPTIONS', - 'Access-Control-Allow-Headers': 'Content-Type, Mcp-Session-Id, MCP-Protocol-Version', -} - -type JsonRpcId = string | number | null -type JsonObject = Record - -type JsonRpcRequest = { - jsonrpc?: unknown - id?: unknown - method?: unknown - params?: unknown -} - -const tools = [ - { - name: 'ella.identity.get', - description: - 'Canonical identity record for Ella: entity ID, description, disambiguation, creator, affiliations, sameAs anchors.', - inputSchema: { - type: 'object', - properties: {}, - additionalProperties: false, - }, - }, - { - name: 'ella.domains.get', - description: - "Ella's declared domain authority. Optional 'domain' argument: longevity | environment | sleep | ai-frameworks. Omit for all four.", - inputSchema: { - type: 'object', - properties: { - domain: { type: 'string', enum: DOMAIN_NAMES }, - }, - additionalProperties: false, - }, - }, - { - name: 'ella.frameworks.get', - description: - "Ella and Mike Ye's declared frameworks. Currently returns The Four Forces of AI Power: Compute, Interface, Alignment, and Energy.", - inputSchema: { - type: 'object', - properties: { - framework: { type: 'string', enum: FRAMEWORK_SLUGS }, - }, - additionalProperties: false, - }, - }, - { - name: 'ella.works.get', - description: 'Co-authored works attributed to Ella with schema types and URLs.', - inputSchema: { - type: 'object', - properties: {}, - additionalProperties: false, - }, - }, - { - name: 'ella.collaboration.get', - description: 'The co-cognition model: division of labor between Mike Ye, Ella, and AI tooling.', - inputSchema: { - type: 'object', - properties: {}, - additionalProperties: false, - }, - }, -] as const +const DEFAULT_ALLOWED_ORIGINS = ['https://ellaentity.ai', 'https://www.ellaentity.ai', 'https://mcp.ellaentity.ai'] +const ALLOW_METHODS = 'POST, OPTIONS' +const ALLOW_HEADERS = 'Content-Type, Accept, Mcp-Session-Id, MCP-Protocol-Version' +const REQUIRED_ACCEPT_TYPES = ['application/json', 'text/event-stream'] as const -function jsonResponse(body: unknown, status = 200) { - return NextResponse.json(body, { status, headers: corsHeaders }) +function allowedOrigins() { + return process.env.MCP_ALLOWED_ORIGINS?.split(',').map((origin) => origin.trim()).filter(Boolean) ?? DEFAULT_ALLOWED_ORIGINS } -function emptyResponse(status = 202) { - return new NextResponse(null, { status, headers: corsHeaders }) -} - -function normalizeId(id: unknown): JsonRpcId { - return typeof id === 'string' || typeof id === 'number' || id === null ? id : null -} +function corsHeaders(request: Request): Headers | NextResponse { + const origin = request.headers.get('origin') + const headers = new Headers({ + 'Access-Control-Allow-Methods': ALLOW_METHODS, + 'Access-Control-Allow-Headers': ALLOW_HEADERS, + Vary: 'Origin', + }) -function rpcResult(id: JsonRpcId, result: unknown) { - return jsonResponse({ jsonrpc: '2.0', id, result }) -} + if (!origin) { + return headers + } -function rpcError(id: JsonRpcId, code: number, message: string) { - return jsonResponse({ jsonrpc: '2.0', id, error: { code, message } }) -} + if (!allowedOrigins().includes(origin)) { + return new NextResponse(null, { + status: 403, + headers, + }) + } -function isJsonObject(value: unknown): value is JsonObject { - return Boolean(value) && typeof value === 'object' && !Array.isArray(value) + headers.set('Access-Control-Allow-Origin', origin) + return headers } -function readParams(params: unknown): JsonObject { - return isJsonObject(params) ? params : {} -} +function jsonRpcError(request: Request, status: number, code: number, message: string) { + const cors = corsHeaders(request) -function noUnexpectedArguments(args: JsonObject): boolean { - return Object.keys(args).length === 0 -} + if (cors instanceof NextResponse) { + return cors + } -function toolContent(payload: unknown) { - return { - content: [ - { - type: 'text', - text: JSON.stringify(payload, null, 2), + return NextResponse.json( + { + jsonrpc: '2.0', + id: null, + error: { + code, + message, }, - ], - } + }, + { + status, + headers: cors, + }, + ) } -function callTool(name: unknown, args: JsonObject) { - if (name === 'ella.identity.get') { - return noUnexpectedArguments(args) ? toolContent(ELLA_IDENTITY) : null - } - - if (name === 'ella.domains.get') { - const keys = Object.keys(args) - const domain = args.domain - - if (keys.length === 0) { - return toolContent(ELLA_DOMAINS) - } +function hasJsonContentType(request: Request) { + return request.headers.get('content-type')?.toLowerCase().includes('application/json') ?? false +} - if ( - keys.length === 1 && - typeof domain === 'string' && - DOMAIN_NAMES.includes(domain as (typeof DOMAIN_NAMES)[number]) - ) { - return toolContent(ELLA_DOMAINS[domain as keyof typeof ELLA_DOMAINS]) - } +function hasRequiredAcceptTypes(request: Request) { + const accept = request.headers.get('accept')?.toLowerCase() - return null + if (!accept) { + return false } - if (name === 'ella.frameworks.get') { - const keys = Object.keys(args) - const framework = args.framework - - if (keys.length === 0) { - return toolContent(ELLA_FRAMEWORKS) - } - - if ( - keys.length === 1 && - typeof framework === 'string' && - FRAMEWORK_SLUGS.includes(framework as (typeof FRAMEWORK_SLUGS)[number]) - ) { - return toolContent(ELLA_FRAMEWORKS.find((item) => item.slug === framework)) - } - - return null - } + return REQUIRED_ACCEPT_TYPES.every((type) => accept.includes(type)) +} - if (name === 'ella.works.get') { - return noUnexpectedArguments(args) ? toolContent(ELLA_WORKS) : null - } +export function OPTIONS(request: Request) { + const cors = corsHeaders(request) - if (name === 'ella.collaboration.get') { - return noUnexpectedArguments(args) - ? toolContent({ coCognition: ELLA_COCOGNITION, surfaces: ELLA_SURFACES }) - : null + if (cors instanceof NextResponse) { + return cors } - return null + return new NextResponse(null, { + status: 204, + headers: cors, + }) } -export function OPTIONS() { - return emptyResponse(204) -} +export function GET(request: Request) { + const cors = corsHeaders(request) + const headers = cors instanceof NextResponse ? new Headers(cors.headers) : cors -export function GET() { - return jsonResponse({ - name: SERVER_INFO.name, - description: 'Read-only MCP server for the canonical Ella identity layer.', - canonicalEntity: 'https://ellaentity.ai/#ella', - transport: 'streamable-http', - tools: [...TOOL_NAMES], - documentation: 'https://ellaentity.ai/system/mcp', + headers.set('Allow', ALLOW_METHODS) + + return new NextResponse(null, { + status: cors instanceof NextResponse ? 403 : 405, + headers, }) } export async function POST(request: Request) { - let body: unknown + const cors = corsHeaders(request) - try { - body = await request.json() - } catch { - return rpcError(null, -32700, 'Parse error') + if (cors instanceof NextResponse) { + return cors } - if (!isJsonObject(body)) { - return rpcError(null, -32600, 'Invalid Request') + if (!hasJsonContentType(request)) { + return jsonRpcError(request, 415, -32600, 'Content-Type must be application/json') } - const rpcRequest = body as JsonRpcRequest - const id = normalizeId(rpcRequest.id) - - if (rpcRequest.jsonrpc !== '2.0' || typeof rpcRequest.method !== 'string') { - return rpcError(id, -32600, 'Invalid Request') + if (!hasRequiredAcceptTypes(request)) { + return jsonRpcError(request, 406, -32600, 'Accept must include application/json and text/event-stream') } - if (rpcRequest.method === 'notifications/initialized') { - return emptyResponse(202) - } - - if (rpcRequest.method === 'initialize') { - const params = readParams(rpcRequest.params) - const requestedProtocolVersion = - typeof params.protocolVersion === 'string' ? params.protocolVersion : PROTOCOL_VERSION - - return rpcResult(id, { - protocolVersion: requestedProtocolVersion, - capabilities: { tools: {} }, - serverInfo: SERVER_INFO, - }) - } - - if (rpcRequest.method === 'ping') { - return rpcResult(id, {}) - } - - if (rpcRequest.method === 'tools/list') { - return rpcResult(id, { tools }) - } - - if (rpcRequest.method === 'tools/call') { - const params = readParams(rpcRequest.params) - const args = params.arguments - - if (typeof params.name !== 'string' || (args !== undefined && !isJsonObject(args))) { - return rpcError(id, -32602, 'Invalid params') - } - - const toolArgs: JsonObject = args === undefined ? {} : args - const result = callTool(params.name, toolArgs) - - if (!result) { - return rpcError(id, -32602, 'Invalid params') + try { + return await handleEllaMcpPost(request, cors) + } catch (error) { + if (error instanceof SyntaxError) { + return jsonRpcError(request, 400, -32700, 'Parse error') } - return rpcResult(id, result) + return jsonRpcError(request, 500, -32603, 'Internal server error') } - - return rpcError(id, -32601, 'Method not found') } diff --git a/app/system/mcp/page.tsx b/app/system/mcp/page.tsx index aa0efa9..0346b89 100644 --- a/app/system/mcp/page.tsx +++ b/app/system/mcp/page.tsx @@ -1,89 +1,55 @@ import { SchemaEyebrow } from '@/app/components/SchemaEyebrow' - import { ELLA_MCP_SCHEMA } from '@/app/schema/ella' +import { + ELLA_CANONICAL_ENTITY_ID, + ELLA_MCP_PROTOCOL_VERSIONS, + ELLA_MCP_RESOURCES, + ELLA_MCP_SERVER_INFO, + ELLA_MCP_TOOL_NAMES, +} from '@/lib/ella-registry' const endpoints = [ - { - host: 'mcp.trailgenic.com', - url: 'https://mcp.trailgenic.com', - exposes: 'TrailGenic longevity, environmental adaptation, high-altitude hiking, and field-method intelligence.', - }, - { - host: 'mcp.exmxc.ai', - url: 'https://mcp.exmxc.ai', - exposes: 'exmxc AI intelligence frameworks, entity clarity systems, agent-readiness doctrine, and infrastructure-convergence models.', - }, - { - host: 'mcp.mikeye.com', - url: 'https://mcp.mikeye.com', - exposes: 'Mike Ye institutional identity, operator context, creator attribution, and cross-property intelligence references.', - }, - { - host: 'mcp.ellaentity.ai', - url: 'https://mcp.ellaentity.ai', - exposes: 'It serves the canonical identity graph directly through ella.identity.get, ella.domains.get, ella.works.get, and ella.collaboration.get.', - }, + { host: 'mcp.trailgenic.com', url: 'https://mcp.trailgenic.com', exposes: 'TrailGenic longevity, environmental adaptation, high-altitude hiking, and field-method intelligence.' }, + { host: 'mcp.exmxc.ai', url: 'https://mcp.exmxc.ai', exposes: 'exmxc AI intelligence frameworks, entity clarity systems, agent-readiness doctrine, and infrastructure-convergence models.' }, + { host: 'mcp.mikeye.com', url: 'https://mcp.mikeye.com', exposes: 'Mike Ye institutional identity, operator context, creator attribution, and cross-property intelligence references.' }, + { host: 'mcp.ellaentity.ai', url: 'https://mcp.ellaentity.ai', exposes: `Native public read-only Streamable HTTP MCP endpoint for ${ELLA_CANONICAL_ENTITY_ID}.` }, ] export function generateMetadata() { - return { - title: 'Ella MCP Access — Machine Interfaces', - description: - 'Visible documentation and JSON-LD schema for Ella MCP access across TrailGenic, exmxc, and MikeYe.com endpoints.', - alternates: { canonical: 'https://ellaentity.ai/system/mcp' }, - } + return { title: 'Ella MCP Access — Machine Interfaces', description: 'Visible documentation and JSON-LD schema for Ella MCP access across TrailGenic, exmxc, and MikeYe.com endpoints.', alternates: { canonical: 'https://ellaentity.ai/system/mcp' } } } -const pageSchema = { - '@context': 'https://schema.org/', - '@graph': [ - { - '@type': 'WebPage', - '@id': 'https://ellaentity.ai/system/mcp#webpage', - url: 'https://ellaentity.ai/system/mcp', - name: 'Ella MCP Access — Machine Interfaces', - isPartOf: { '@id': 'https://ellaentity.ai/#website' }, - mainEntity: { '@id': 'https://ellaentity.ai/#ella' }, - about: { '@id': 'https://ellaentity.ai/#ella' }, - inLanguage: 'en-US', - }, - ], -} +const pageSchema = { '@context': 'https://schema.org/', '@graph': [{ '@type': 'WebPage', '@id': 'https://ellaentity.ai/system/mcp#webpage', url: 'https://ellaentity.ai/system/mcp', name: 'Ella MCP Access — Machine Interfaces', isPartOf: { '@id': 'https://ellaentity.ai/#website' }, mainEntity: { '@id': ELLA_CANONICAL_ENTITY_ID }, about: { '@id': ELLA_CANONICAL_ENTITY_ID }, inLanguage: 'en-US' }] } export default function Page() { return (
-