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 — 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 @@
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 @@
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; }