diff --git a/README.md b/README.md index 6426efe6..a099734e 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,15 @@

One registry. HTTP RPC, interactive docs, OpenAPI, MCP, and a typed client.

-callspec replaces the old express-typed-rpc + Swagger + separate MCP tool list stack with a single `defineRegistry` object. Add a route once; it shows up everywhere. +One `defineRegistry` powers HTTP RPC, OpenAPI, callsheet docs, MCP tools, and a typed client. Add a route once; it shows up everywhere. + +

+ + callsheet home — Chirp demo API with Connect MCP panel showing Cursor config + +

+ +

callsheet — built-in docs UI with a Connect MCP panel. Copy the endpoint and a ready-made config for Cursor, Claude, VS Code, Windsurf, or Pi.

```typescript import {defineRegistry, defineRoute, mountRegistry, mountMcp, client} from 'callspec'; @@ -22,7 +30,7 @@ export const api = defineRegistry({ description: 'Returns pipelines for a team.', tags: ['pipelines'], }, - mcp: true, // → tools/list + mcp: true, // → MCP tools/list handler: getPipelines, }), }); @@ -31,22 +39,44 @@ mountRegistry(app, api, { contextResolver: getUserContext, docs: { openApi: {title: 'My API', version: '1.0.0'}, - // exposeOpenApi: true → GET /openapi.json - // exposeUi: true → GET /docs (callsheet UI) + exposeUi: true, // → /docs (callsheet + Connect MCP) + exposeOpenApi: true, // → /openapi.json }, }); mountMcp(app, api, {path: '/mcp', contextResolver: getUserContext}); ``` +Try the demo locally: `npm run build && npm run dev:docs` → [http://127.0.0.1:3456/v1/docs](http://127.0.0.1:3456/v1/docs) (Chirp API sample; use `Bearer demo` for private tools). + +## Built-in MCP server + +No separate MCP process. No hand-maintained tool manifest. No stdio bridge. + +1. **Opt in per route** — `mcp: true` on any `defineRoute`. Input/output schemas come from the same runtyp preds as HTTP. +2. **Mount once** — `mountMcp(app, api, { path: '/mcp' })` on the same Express app. Streamable HTTP at `/mcp`. +3. **Connect from callsheet** — at `/docs`, the **Connect MCP** panel shows your endpoint and copy-paste configs for Cursor (`.cursor/mcp.json`), Claude Desktop, Claude Code CLI, VS Code, Windsurf, and Pi — including `Authorization` headers when you have private tools. + +```typescript +searchRecent: defineRoute({ + input: p.object({query: p.string()}), + meta: {summary: 'Search recent', description: '…', tags: ['tweets']}, + access: 'private', + mcp: true, // this route is now an MCP tool — same handler as POST /v1/searchRecent + handler: searchRecent, +}), +``` + +Agents call the **same handlers** as your HTTP RPC. Auth uses the same `contextResolver` (e.g. `Authorization: Bearer …`). Public tools work without a token; private tools return 401 without one. + ## Why callspec | Surface | How you get it | |---------|----------------| -| **HTTP RPC** | `POST /v1/` — same URLs as before | +| **HTTP RPC** | `POST /v1/` | | **Interactive docs** | Built-in **callsheet** UI at `/docs` | | **OpenAPI 3.1** | `GET /openapi.json` from the same registry | -| **MCP tools** | `mcp: true` on a route → automatic `tools/list` | +| **MCP tools** | `mcp: true` on a route → `tools/list` + `tools/call` at `/mcp` | | **Typed client** | `client('searchLogs', input)` | No second schema. No duplicate handler layer. No separate MCP subprocess. @@ -55,15 +85,20 @@ No second schema. No duplicate handler layer. No separate MCP subprocess. ### callsheet — interactive docs -Minimal, fast docs UI baked into callspec. Env-gated in production; flip on with `exposeDocs: true` or: +Minimal, fast docs UI baked into callspec. Browse routes, try RPCs, read OpenAPI, and **connect MCP clients** from the home page. Env-gated in production; flip on with: ```typescript docs: { openApi: {title: 'Logfox API', version: '1.0.0'}, exposeOpenApi: true, // machine-readable spec - exposeUi: true, // human-readable /docs + exposeUi: true, // human-readable /docs + Connect MCP openApiPath: '/openapi.json', uiPath: '/docs', + callsheet: { + mcp: { + authHint: 'Use Authorization: Bearer for private tools.', + }, + }, } ``` @@ -77,17 +112,13 @@ Toggle each surface independently — spec only, UI only, both, or neither (`doc Private gate runs **before** validation so unauthenticated callers never see field-level errors. -### MCP on the same Express process - -`mountMcp` at `/mcp` on the same app — not a stdio subprocess. Routes opt in with `mcp: true`. - ### runtyp + OpenAPI -Field `{ description }` on runtyp preds flows to JSON Schema → OpenAPI → callsheet. Route-level `meta` (summary, tags) is callspec-only. +Field `{ description }` on runtyp preds flows to JSON Schema → OpenAPI → callsheet → MCP `inputSchema`. Route-level `meta` (summary, tags) is callspec-only. ## Client -Fetch-only — works in the browser and in Node 18+ (global `fetch`). No `http`/`https`/`express` in the client entry, same split as `express-typed-rpc/dist/client` vs `client-node`. +Fetch-only — works in the browser and in Node 18+ (global `fetch`). The `callspec/client` entry has no `http`, `https`, or Express imports, so it is safe in frontend bundles. **Browser or frontend bundler** — import the client subpath so you do not pull server code: @@ -110,15 +141,16 @@ export const api = defineRegistry({ /* ... */ }); export type API = InferRegistry; ``` -Drop-in replacement for `express-typed-rpc/dist/client`. Same Date wire format (`deserializeResponse` on read). +Responses deserialize ISO date strings back to `Date` on read (`deserializeResponse`). ## Development ```bash npm run validate # build server + callsheet UI, lint, test (incl. integration) +npm run dev:docs # Chirp demo API + callsheet at :3456/v1/docs ``` -Integration tests spin up Express in-process and verify OpenAPI, `/docs`, auth, and RPC end-to-end. +Integration tests spin up Express in-process and verify OpenAPI, `/docs`, auth, MCP schemas, and RPC end-to-end. ## Package layout @@ -129,6 +161,7 @@ src/ executeRoute.ts # shared HTTP + MCP pipeline mountRegistry.ts # POST routes + openapi + callsheet mountMcp.ts # MCP on Express + mcpTools.ts # tools/list schemas from runtyp openapi.ts # OpenAPI 3.1 emitter client.ts # typed fetch client callsheet/ # built-in docs UI (bundled to dist/callsheet/ui) diff --git a/assets/callsheet-chirp-demo.png b/assets/callsheet-chirp-demo.png new file mode 100644 index 00000000..c1340d81 Binary files /dev/null and b/assets/callsheet-chirp-demo.png differ diff --git a/assets/callspec-lockup-dark.svg b/assets/callspec-lockup-dark.svg index f6755493..ab107e09 100644 --- a/assets/callspec-lockup-dark.svg +++ b/assets/callspec-lockup-dark.svg @@ -1,11 +1,11 @@ - - - - - - - - - callspec + + + + + + + + + callspec diff --git a/assets/callspec-lockup-light.svg b/assets/callspec-lockup-light.svg index aafa2a6c..499dd6b9 100644 --- a/assets/callspec-lockup-light.svg +++ b/assets/callspec-lockup-light.svg @@ -1,11 +1,11 @@ - - - - - - - - - callspec + + + + + + + + + callspec diff --git a/assets/callspec-mark-dark.svg b/assets/callspec-mark-dark.svg index 1478ba5b..51f1b063 100644 --- a/assets/callspec-mark-dark.svg +++ b/assets/callspec-mark-dark.svg @@ -1,10 +1,10 @@ - - - - - - - - + + + + + + + + diff --git a/assets/callspec-mark-light.svg b/assets/callspec-mark-light.svg index 26739969..08c3d694 100644 --- a/assets/callspec-mark-light.svg +++ b/assets/callspec-mark-light.svg @@ -1,10 +1,10 @@ - - - - - - - - + + + + + + + + diff --git a/src/callsheet/ui/styles.css b/src/callsheet/ui/styles.css index 18dbc4d5..e8b6a6b9 100644 --- a/src/callsheet/ui/styles.css +++ b/src/callsheet/ui/styles.css @@ -883,7 +883,7 @@ html, body { display: block; width: 100%; height: 100%; - object-fit: cover; + object-fit: contain; } .brand-mark-dark { display: none; }