Skip to main content

create-evenfire-mcp-server

Build, test, and package an MCP (Model Context Protocol) server for Evenfire so it can live in the org catalog, install on a cluster, and be used by the chat agent. Covers the two server shapes (a local image you build, or a remote wrapper around a hosted endpoint), the streamableHttp/stdio server code contract, Dockerfile hardening, the registry.json schema, the naming rule, local handshake testing, how the platform deploys it (direct vs stdio-bridge vs egress-proxy), and how to attach it to a chat agent via a Context. Use when creating, changing, testing, or onboarding an MCP server/connector.

Informações da origem

Repositório
evenfire-ai/evenfire-skills
Última atividade na origem
12 de agosto de 2026 às 01:08
Idioma detectado do SKILL.md
inglês
Estrelas
1
Forks
0

Opções de instalação

Por padrão, está selecionado o prompt que primeiro revisa a origem. Você pode mudar para um comando direto ou baixar uma cópia local.

Revise os arquivos de origem

Leia o SKILL.md e os arquivos complementares exibidos pelo SkillsMP antes de decidir se vai instalar.

Explorador de arquivos
2 arquivos

Exibindo SKILL.md

SKILL.md
Instruções da origem · Visualização somente leitura
name
create-evenfire-mcp-server
description
Build, test, and package an MCP (Model Context Protocol) server for Evenfire so it can live in the org catalog, install on a cluster, and be used by the chat agent. Covers the two server shapes (a local image you build, or a remote wrapper around a hosted endpoint), the streamableHttp/stdio server code contract, Dockerfile hardening, the registry.json schema, the naming rule, local handshake testing, how the platform deploys it (direct vs stdio-bridge vs egress-proxy), and how to attach it to a chat agent via a Context. Use when creating, changing, testing, or onboarding an MCP server/connector.
# Building an Evenfire MCP server > **Verified against Evenfire `dev` at commit `21d9a7d5d` (2026-08-11).** > `git describe`: `v0.3.0-915-g21d9a7d5d`. CRD API `clerum.io/v1alpha1`, > `clerum-crds` chart `0.7.0`. Field names, enums, endpoints, and commands below > match that revision. See [VERSIONS.md](../VERSIONS.md). An MCP server exposes tools (and optionally resources/prompts) over the Model Context Protocol. On Evenfire it reaches the chat agent through `mcp-host`, which discovers servers a Context allows and prefixes every tool as `serverName__toolName` (double underscore). A server is packaged as one directory carrying a `registry.json` (and, for a local image, source + a Dockerfile). Everything here is verified against the platform code (`host-context-controller`, `mcp-host`, `control-api`, the `clerum.io` McpServer and Context CRDs) and the shipped server collection. To publish what you build, use the `publish-evenfire-plugin` skill. If instead you want the MCP server to ship inside a larger UI plugin, add it as a `transport` workload with the `create-evenfire-plugin` skill. ## 1. Pick the shape | Shape | When | Directory contents | transport | |---|---|---|---| | Local, own code | you write the tools (call a vendor REST API, a chain RPC, etc.) | `src/`, `package.json`, `Dockerfile`, `registry.json` | `streamableHttp` | | Local, reference wrapper (Node) | a thin image around an upstream stdio MCP server | `Dockerfile`, `registry.json` (no `src`) | `stdio` + a `command[]` | | Local, reference wrapper (Python) | an upstream Python stdio server exposed over HTTP via `supergateway` | `Dockerfile`, `registry.json` | `streamableHttp` (no `command`) | | Remote wrapper | the vendor already hosts a stable MCP endpoint | JUST `registry.json` (no image) | `streamableHttp` (never `stdio`) | Prefer a remote wrapper whenever the vendor hosts a stable endpoint: nothing to build or maintain, Evenfire fronts it with an nginx egress proxy. Write your own code only when there is no good upstream. Name the directory `mcp-<vendor>` for a local server, `mcp-<vendor>-remote` for a remote wrapper. ## 2. The own-code server contract (local, streamableHttp) The server MUST listen on `0.0.0.0:PORT` (default 3000) and serve MCP over StreamableHTTP at `/mcp`. Binding `127.0.0.1` makes the pod fail its probe. A minimal, verified `src/index.ts`: ```typescript import express from 'express' import { randomUUID } from 'node:crypto' import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js' import { z } from 'zod' const PORT = Number(process.env.PORT ?? 3000) const API_KEY = process.env.EXAMPLE_API_KEY // must equal a credentialSchema key name function buildServer(): McpServer { const server = new McpServer({ name: 'mcp-example', version: '1.0.0' }) server.tool( 'get_thing', 'Fetch a thing by id from the Example API.', { id: z.string().describe('the thing id') }, async ({ id }) => { const res = await fetch(`https://api.example.com/things/${id}`, { headers: { authorization: `Bearer ${API_KEY}` }, }) if (!res.ok) return { content: [{ type: 'text', text: `error ${res.status}` }], isError: true } return { content: [{ type: 'text', text: JSON.stringify(await res.json()) }] } }, ) return server } const app = express() app.use(express.json()) const transports = new Map<string, StreamableHTTPServerTransport>() app.post('/mcp', async (req, res) => { const sid = req.header('mcp-session-id') let transport = sid ? transports.get(sid) : undefined if (!transport) { transport = new StreamableHTTPServerTransport({ sessionIdGenerator: randomUUID, onsessioninitialized: (id) => transports.set(id, transport!), }) await buildServer().connect(transport) } await transport.handleRequest(req, res, req.body) }) app.get('/mcp', async (req, res) => { const t = transports.get(req.header('mcp-session-id') ?? '') if (!t) return res.status(400).end() await t.handleRequest(req, res) }) app.delete('/mcp', async (req, res) => { const t = transports.get(req.header('mcp-session-id') ?? '') if (!t) return res.status(400).end() await t.handleRequest(req, res) }) app.get('/health', (_req, res) => res.json({ ok: true })) app.listen(PORT, '0.0.0.0', () => console.log(`mcp-example on :${PORT}/mcp`)) ``` Notes verified against the platform: - The MCP session id travels in the `mcp-session-id` request header. - `/health` is a convenience for humans and for stdio-bridge/remote deploys. For a LOCAL streamableHttp server the platform readiness probe is a **tcpSocket** probe on the port, not `httpGet /health`. What matters is that the process opens a TCP listener on the declared port bound to `0.0.0.0`. - A tool handler returns `{ content: [{ type: 'text', text }], isError? }`. - Credential env-var names read by the code (here `EXAMPLE_API_KEY`) MUST match the `credentialSchema.keys[].name` in `registry.json` exactly, or an operator-entered secret never reaches the running server. ## 3. Dockerfile (own-code, hardened) ```dockerfile FROM node:24-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci COPY tsconfig.json ./ COPY src/ ./src/ RUN npm run build FROM node:24-alpine LABEL org.opencontainers.image.title="mcp-example" \ org.opencontainers.image.description="Example MCP server." \ org.opencontainers.image.source="https://github.com/<org>/<repo>" \ org.opencontainers.image.version="1.0.0" \ org.opencontainers.image.licenses="MIT" WORKDIR /app COPY --from=builder /app/dist ./dist COPY --from=builder /app/node_modules ./node_modules COPY package.json ./ ENV PORT=3000 EXPOSE 3000 USER node HEALTHCHECK --interval=30s --timeout=3s --start-period=25s --retries=3 \ CMD node -e "require('net').connect(3000,'127.0.0.1').on('connect',()=>process.exit(0)).on('error',()=>process.exit(1))" CMD ["node", "dist/index.js"] ``` `USER node` maps to uid 1000, which matches the platform default `runAsUser`. Build multi-arch the same way as any plugin image (see `publish-evenfire-plugin`). ### 3.1 Reference-server wrappers (no own code) To ship an existing upstream MCP server, wrap it rather than rewrite it. Node stdio server: a thin image that installs the upstream package with NO `CMD`, plus a `stdio` `registry.json` whose `command[]` points at the installed entrypoint under `/mcp-bin` (the stdio-bridge sidecar copies `/app` into `/mcp-bin` and spawns it): ```dockerfile FROM node:22-alpine WORKDIR /app RUN npm init -y >/dev/null 2>&1 && npm install @modelcontextprotocol/server-memory@<ver> && npm cache clean --force USER node # no CMD: the stdio-bridge sidecar launches the server (see registry.json command[]) ``` ```json { "name": "mcp-memory", "version": "1.0.0", "description": "Knowledge-graph memory (upstream reference server, stdio via the bridge).", "category": "productivity", "serverMode": "local", "transport": "stdio", "command": ["node", "/mcp-bin/node_modules/@modelcontextprotocol/server-memory/dist/index.js"], "port": 3000, "tools": ["create_entities", "read_graph", "search_nodes"] } ``` Python stdio server: the Node stdio-bridge cannot spawn a Python process, so ship `supergateway` in the image to expose the upstream stdio server over StreamableHTTP, and use `transport: streamableHttp` with no `command`. ## 4. registry.json (the authoring artifact) The directory name, `registry.json` `.name`, the pushed image repo segment, and the scoped catalog name MUST all be the same string. This is a hard identity constraint: control-api rejects a mismatch with a 422 at BOTH publish and install (`imageRef repo ... must equal the entry name ... cross-org pull would be denied`), and the collection's `publish.sh` fails even earlier, locally, on a `dir != name` mismatch. The name must be a DNS-1123 label (`^[a-z0-9]([-a-z0-9]*[a-z0-9])?$`). The npm `package.json` `.name` is unrelated and is NOT the catalog name. Local own-code server: ```json { "name": "mcp-example", "description": "Example MCP server: reads things from the Example API.", "category": "data", "tags": ["example", "api"], "transport": "streamableHttp", "port": 3000, "egressSummary": { "domains": ["api.example.com"], "ports": [443] }, "tools": ["get_thing"], "credentialSchema": { "required": true, "authType": "api-key", "keys": [ { "name": "EXAMPLE_API_KEY", "label": "Example API Key", "kind": "api-key", "semanticType": "plain-string", "description": "Get one at example.com/apikey." } ] } } ``` Remote wrapper (no image, `.version` required here because there is no `package.json`): ```json { "name": "mcp-example-remote", "version": "1.0.0", "description": "Example (hosted): evenfire fronts the vendor MCP endpoint via nginx egress proxy. Keyless.", "category": "docs", "tags": ["example", "remote", "wrapper"], "serverMode": "remote", "transport": "streamableHttp", "port": 3000, "tools": ["resolve", "query"], "remoteEndpoints": [{ "url": "https://mcp.example.com/mcp" }], "docsUrl": "https://example.com", "egressSummary": { "domains": ["mcp.example.com"], "ports": [443] } } ``` Field rules verified against the publisher and control-api: - Local own-code: `serverMode` and `version` are optional (default `local`; version comes from `package.json`). Own-code servers use `transport: streamableHttp` and no `command`. - Local stdio reference wrapper: no `src`/`package.json`; `.version` lives in `registry.json`; `transport: stdio` and a non-empty `command[]` array (spawned by the stdio-bridge sidecar). Omitting `command` on stdio, or adding `command` on a non-stdio transport, fails publish. - Remote: `serverMode: remote`, `version` required, `remoteEndpoints: [{url}]` (HTTPS only, at least one), transport must NOT be `stdio`, optional `authHeaders: [{header, valueTemplate}]` (the `${VAR}` placeholder must match a `credentialSchema.keys[].name`). Remote may carry `authHeaders`; local may not. - `egressSummary` has exactly two shapes. Exact-host: `{ "domains": [hosts], "ports": [ints] }` (bare hostnames, no scheme/path/ wildcard/IP). Public-web: exactly `{ "wideCidr": true, "ports": [80, 443] }` (partial or with domains is rejected). Omit it entirely when the server has no external egress. Use public-web ONLY for genuinely dynamic public destinations (a CDN or a runtime-resolved host), not to dodge a finite host list. - `credentialSchema` is `{ required, authType, keys: [{name, label?, kind?, semanticType?, description?}] }` or `null`/omitted for a keyless server. Only `keys[].name` is load-bearing (it must equal the env var the code reads or the `authHeaders` `${VAR}`). `authType` is free-form catalog metadata, not a validated enum, do not rely on a fixed vocabulary. - `tools[]` is catalog metadata for discovery. Keep it in sync with the tools the server actually advertises (`mcp-smoke.sh`'s `tools/list` is the check). A server may also expose MCP resources and prompts, not only tools; the platform advertises whatever the server returns at runtime, `tools[]` is just the catalog listing. - Do not author `transport: sse`. It exists in the CRD but is not a supported authoring path in the collection. ## 5. Test locally before publishing Two levels, both runnable without a cluster: 1. Run the server and drive the MCP handshake against it. The bundled `scripts/mcp-smoke.sh` does `initialize` -> `tools/list` -> optional `tools/call` over StreamableHTTP and prints each response. Point it at a local `npm run dev` (`http://localhost:3000/mcp`), a running container, or any live remote endpoint: ```bash
Ver no GitHub
Este SKILL.md e muito grande, entao o SkillsMP mostra aqui apenas a primeira secao. Ver no GitHub