callspec 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +170 -0
  3. package/assets/callsheet-chirp-demo.png +0 -0
  4. package/assets/callspec-lockup-dark.svg +11 -0
  5. package/assets/callspec-lockup-light.svg +11 -0
  6. package/assets/callspec-mark-dark.svg +10 -0
  7. package/assets/callspec-mark-light.svg +10 -0
  8. package/assets/chirp/mark-dark.svg +7 -0
  9. package/assets/chirp/mark.png +0 -0
  10. package/assets/chirp/mark.svg +7 -0
  11. package/assets/chirp/mark@2x.png +0 -0
  12. package/dist/callsheet/branding.d.ts +33 -0
  13. package/dist/callsheet/branding.js +2 -0
  14. package/dist/callsheet/index.d.ts +4 -0
  15. package/dist/callsheet/index.js +8 -0
  16. package/dist/callsheet/mountCallsheet.d.ts +22 -0
  17. package/dist/callsheet/mountCallsheet.js +62 -0
  18. package/dist/callsheet/parseOpenApi.d.ts +17 -0
  19. package/dist/callsheet/parseOpenApi.js +58 -0
  20. package/dist/callsheet/ui/assets/app.js +226 -0
  21. package/dist/callsheet/ui/assets/callspec-mark-dark.svg +10 -0
  22. package/dist/callsheet/ui/assets/callspec-mark-light.svg +10 -0
  23. package/dist/callsheet/ui/assets/style.css +2 -0
  24. package/dist/callsheet/ui/index.html +25 -0
  25. package/dist/client.d.ts +15 -0
  26. package/dist/client.js +49 -0
  27. package/dist/defineRegistry.d.ts +3 -0
  28. package/dist/defineRegistry.js +11 -0
  29. package/dist/defineRoute.d.ts +9 -0
  30. package/dist/defineRoute.js +16 -0
  31. package/dist/errors.d.ts +10 -0
  32. package/dist/errors.js +25 -0
  33. package/dist/executeRoute.d.ts +2 -0
  34. package/dist/executeRoute.js +16 -0
  35. package/dist/index.d.ts +17 -0
  36. package/dist/index.js +29 -0
  37. package/dist/mcpTools.d.ts +14 -0
  38. package/dist/mcpTools.js +41 -0
  39. package/dist/mountMcp.d.ts +13 -0
  40. package/dist/mountMcp.js +100 -0
  41. package/dist/mountRegistry.d.ts +32 -0
  42. package/dist/mountRegistry.js +88 -0
  43. package/dist/openapi.d.ts +8 -0
  44. package/dist/openapi.js +56 -0
  45. package/dist/serializer.d.ts +2 -0
  46. package/dist/serializer.js +45 -0
  47. package/dist/types.d.ts +35 -0
  48. package/dist/types.js +2 -0
  49. package/package.json +77 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Marc H. Weiner
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,170 @@
1
+ <div align="center">
2
+ <picture>
3
+ <source srcset="assets/callspec-lockup-light.svg" media="(prefers-color-scheme: light)" />
4
+ <source srcset="assets/callspec-lockup-dark.svg" media="(prefers-color-scheme: dark)" />
5
+ <img src="assets/callspec-lockup-dark.svg" alt="callspec" />
6
+ </picture>
7
+
8
+ <p><strong>One registry. HTTP RPC, interactive docs, OpenAPI, MCP, and a typed client.</strong></p>
9
+ </div>
10
+
11
+ One `defineRegistry` powers HTTP RPC, OpenAPI, callsheet docs, MCP tools, and a typed client. Add a route once; it shows up everywhere.
12
+
13
+ <p align="center">
14
+ <a href="assets/callsheet-chirp-demo.png">
15
+ <img src="assets/callsheet-chirp-demo.png" alt="callsheet home — Chirp demo API with Connect MCP panel showing Cursor config" width="920" />
16
+ </a>
17
+ </p>
18
+
19
+ <p align="center"><sub><strong>callsheet</strong> — built-in docs UI with a <strong>Connect MCP</strong> panel. Copy the endpoint and a ready-made config for Cursor, Claude, VS Code, Windsurf, or Pi.</sub></p>
20
+
21
+ ```typescript
22
+ import {defineRegistry, defineRoute, mountRegistry, mountMcp, client} from 'callspec';
23
+ import {predicates as p} from 'runtyp';
24
+
25
+ export const api = defineRegistry({
26
+ getPipelines: defineRoute({
27
+ input: getPipelinesInput,
28
+ meta: {
29
+ summary: 'List pipelines',
30
+ description: 'Returns pipelines for a team.',
31
+ tags: ['pipelines'],
32
+ },
33
+ mcp: true, // → MCP tools/list
34
+ handler: getPipelines,
35
+ }),
36
+ });
37
+
38
+ mountRegistry(app, api, {
39
+ contextResolver: getUserContext,
40
+ docs: {
41
+ openApi: {title: 'My API', version: '1.0.0'},
42
+ exposeUi: true, // → /docs (callsheet + Connect MCP)
43
+ exposeOpenApi: true, // → /openapi.json
44
+ },
45
+ });
46
+
47
+ mountMcp(app, api, {path: '/mcp', contextResolver: getUserContext});
48
+ ```
49
+
50
+ 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).
51
+
52
+ ## Built-in MCP server
53
+
54
+ No separate MCP process. No hand-maintained tool manifest. No stdio bridge.
55
+
56
+ 1. **Opt in per route** — `mcp: true` on any `defineRoute`. Input/output schemas come from the same runtyp preds as HTTP.
57
+ 2. **Mount once** — `mountMcp(app, api, { path: '/mcp' })` on the same Express app. Streamable HTTP at `/mcp`.
58
+ 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.
59
+
60
+ ```typescript
61
+ searchRecent: defineRoute({
62
+ input: p.object({query: p.string()}),
63
+ meta: {summary: 'Search recent', description: '…', tags: ['tweets']},
64
+ access: 'private',
65
+ mcp: true, // this route is now an MCP tool — same handler as POST /v1/searchRecent
66
+ handler: searchRecent,
67
+ }),
68
+ ```
69
+
70
+ 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.
71
+
72
+ ## Why callspec
73
+
74
+ | Surface | How you get it |
75
+ |---------|----------------|
76
+ | **HTTP RPC** | `POST /v1/<methodName>` |
77
+ | **Interactive docs** | Built-in **callsheet** UI at `/docs` |
78
+ | **OpenAPI 3.1** | `GET /openapi.json` from the same registry |
79
+ | **MCP tools** | `mcp: true` on a route → `tools/list` + `tools/call` at `/mcp` |
80
+ | **Typed client** | `client<API['searchLogs']>('searchLogs', input)` |
81
+
82
+ No second schema. No duplicate handler layer. No separate MCP subprocess.
83
+
84
+ ## Batteries included
85
+
86
+ ### callsheet — interactive docs
87
+
88
+ 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:
89
+
90
+ ```typescript
91
+ docs: {
92
+ openApi: {title: 'Logfox API', version: '1.0.0'},
93
+ exposeOpenApi: true, // machine-readable spec
94
+ exposeUi: true, // human-readable /docs + Connect MCP
95
+ openApiPath: '/openapi.json',
96
+ uiPath: '/docs',
97
+ callsheet: {
98
+ mcp: {
99
+ authHint: 'Use Authorization: Bearer <token> for private tools.',
100
+ },
101
+ },
102
+ }
103
+ ```
104
+
105
+ Toggle each surface independently — spec only, UI only, both, or neither (`docs: false`).
106
+
107
+ ### Auth
108
+
109
+ - **`access: 'public'`** — no credentials required
110
+ - **`access: 'private'`** (default) — 401 without `contextResolver` result
111
+ - Team/role rules stay in your resolver `assert*` helpers
112
+
113
+ Private gate runs **before** validation so unauthenticated callers never see field-level errors.
114
+
115
+ ### runtyp + OpenAPI
116
+
117
+ Field `{ description }` on runtyp preds flows to JSON Schema → OpenAPI → callsheet → MCP `inputSchema`. Route-level `meta` (summary, tags) is callspec-only.
118
+
119
+ ## Client
120
+
121
+ 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.
122
+
123
+ **Browser or frontend bundler** — import the client subpath so you do not pull server code:
124
+
125
+ ```typescript
126
+ import type {API} from '@logfoxai/my-service'; // InferRegistry<typeof api> from the service package
127
+ import {client} from 'callspec/client';
128
+
129
+ const logs = await client<API['searchLogs']>('searchLogs', {teamId}, {
130
+ endpoint: 'https://api.example.com/v1',
131
+ fetchOptions: {headers: {Authorization: `Bearer ${token}`}},
132
+ });
133
+ ```
134
+
135
+ **Service package** — export the API type once from your registry:
136
+
137
+ ```typescript
138
+ import type {InferRegistry} from 'callspec';
139
+
140
+ export const api = defineRegistry({ /* ... */ });
141
+ export type API = InferRegistry<typeof api>;
142
+ ```
143
+
144
+ Responses deserialize ISO date strings back to `Date` on read (`deserializeResponse`).
145
+
146
+ ## Development
147
+
148
+ ```bash
149
+ npm run validate # build server + callsheet UI, lint, test (incl. integration)
150
+ npm run dev:docs # Chirp demo API + callsheet at :3456/v1/docs
151
+ ```
152
+
153
+ Integration tests spin up Express in-process and verify OpenAPI, `/docs`, auth, MCP schemas, and RPC end-to-end.
154
+
155
+ ## Package layout
156
+
157
+ ```
158
+ src/
159
+ defineRoute.ts # route definition + arity guard
160
+ defineRegistry.ts # named route map
161
+ executeRoute.ts # shared HTTP + MCP pipeline
162
+ mountRegistry.ts # POST routes + openapi + callsheet
163
+ mountMcp.ts # MCP on Express
164
+ mcpTools.ts # tools/list schemas from runtyp
165
+ openapi.ts # OpenAPI 3.1 emitter
166
+ client.ts # typed fetch client
167
+ callsheet/ # built-in docs UI (bundled to dist/callsheet/ui)
168
+ ```
169
+
170
+ Powered by [runtyp](https://github.com/logfoxai/runtyp) for validation.
Binary file
@@ -0,0 +1,11 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="248" height="66" viewBox="0 0 248 66" role="img" aria-label="callspec">
2
+ <rect x="4" y="9" width="44" height="44" rx="9" fill="#1a1a1a" stroke="#333" stroke-width="1"/>
3
+ <circle cx="26" cy="36" r="4" fill="#fafafa"/>
4
+ <path d="M26 32V24" stroke="#fafafa" stroke-width="2" stroke-linecap="round"/>
5
+ <circle cx="26" cy="22.5" r="2.25" fill="#fafafa"/>
6
+ <path d="M22.5 38L16.1 41.2" stroke="#fafafa" stroke-width="2" stroke-linecap="round"/>
7
+ <circle cx="15.2" cy="41.8" r="2.25" fill="#fafafa"/>
8
+ <path d="M29.5 38L35.9 41.2" stroke="#fafafa" stroke-width="2" stroke-linecap="round"/>
9
+ <circle cx="36.8" cy="41.8" r="2.25" fill="#fafafa"/>
10
+ <text x="62" y="33" dominant-baseline="middle" font-family="ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="32" font-weight="600" letter-spacing="-0.02em" fill="#fafafa">callspec</text>
11
+ </svg>
@@ -0,0 +1,11 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="248" height="66" viewBox="0 0 248 66" role="img" aria-label="callspec">
2
+ <rect x="4" y="9" width="44" height="44" rx="9" fill="none" stroke="#171717" stroke-width="2"/>
3
+ <circle cx="26" cy="36" r="4" fill="#171717"/>
4
+ <path d="M26 32V24" stroke="#171717" stroke-width="2" stroke-linecap="round"/>
5
+ <circle cx="26" cy="22.5" r="2.25" fill="#171717"/>
6
+ <path d="M22.5 38L16.1 41.2" stroke="#171717" stroke-width="2" stroke-linecap="round"/>
7
+ <circle cx="15.2" cy="41.8" r="2.25" fill="#171717"/>
8
+ <path d="M29.5 38L35.9 41.2" stroke="#171717" stroke-width="2" stroke-linecap="round"/>
9
+ <circle cx="36.8" cy="41.8" r="2.25" fill="#171717"/>
10
+ <text x="62" y="33" dominant-baseline="middle" font-family="ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="32" font-weight="600" letter-spacing="-0.02em" fill="#171717">callspec</text>
11
+ </svg>
@@ -0,0 +1,10 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" role="img" aria-label="callspec">
2
+ <rect x="2" y="2" width="44" height="44" rx="9" fill="#1a1a1a" stroke="#333" stroke-width="1"/>
3
+ <circle cx="24" cy="27" r="4" fill="#fafafa"/>
4
+ <path d="M24 23V15" stroke="#fafafa" stroke-width="2" stroke-linecap="round"/>
5
+ <circle cx="24" cy="13.5" r="2.25" fill="#fafafa"/>
6
+ <path d="M20.5 29L14.1 32.2" stroke="#fafafa" stroke-width="2" stroke-linecap="round"/>
7
+ <circle cx="13.2" cy="32.8" r="2.25" fill="#fafafa"/>
8
+ <path d="M27.5 29L33.9 32.2" stroke="#fafafa" stroke-width="2" stroke-linecap="round"/>
9
+ <circle cx="34.8" cy="32.8" r="2.25" fill="#fafafa"/>
10
+ </svg>
@@ -0,0 +1,10 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" role="img" aria-label="callspec">
2
+ <rect x="2" y="2" width="44" height="44" rx="9" fill="none" stroke="#171717" stroke-width="2"/>
3
+ <circle cx="24" cy="27" r="4" fill="#171717"/>
4
+ <path d="M24 23V15" stroke="#171717" stroke-width="2" stroke-linecap="round"/>
5
+ <circle cx="24" cy="13.5" r="2.25" fill="#171717"/>
6
+ <path d="M20.5 29L14.1 32.2" stroke="#171717" stroke-width="2" stroke-linecap="round"/>
7
+ <circle cx="13.2" cy="32.8" r="2.25" fill="#171717"/>
8
+ <path d="M27.5 29L33.9 32.2" stroke="#171717" stroke-width="2" stroke-linecap="round"/>
9
+ <circle cx="34.8" cy="32.8" r="2.25" fill="#171717"/>
10
+ </svg>
@@ -0,0 +1,7 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none" role="img" aria-label="Chirp">
2
+ <rect width="64" height="64" rx="14" fill="#134e4a"/>
3
+ <circle cx="33" cy="31" r="12" fill="#fafafa"/>
4
+ <path fill="#fafafa" d="M19 21l-5 6 8 2 4-5z"/>
5
+ <circle cx="36.5" cy="26.5" r="2" fill="#171717"/>
6
+ <path fill="#fb923c" d="M45 28.5 51.5 31.5 45 34.5z"/>
7
+ </svg>
Binary file
@@ -0,0 +1,7 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none" role="img" aria-label="Chirp">
2
+ <rect width="64" height="64" fill="#0f766e"/>
3
+ <circle cx="33" cy="31" r="12" fill="#f5f2eb"/>
4
+ <path fill="#f5f2eb" d="M19 21l-5 6 8 2 4-5z"/>
5
+ <circle cx="36.5" cy="26.5" r="2" fill="#171717"/>
6
+ <path fill="#f97316" d="M45 28.5 51.5 31.5 45 34.5z"/>
7
+ </svg>
Binary file
@@ -0,0 +1,33 @@
1
+ export type CallsheetBranding = {
2
+ /** Display name; defaults to OpenAPI info.title */
3
+ name?: string;
4
+ /** Short welcome paragraph on the home page */
5
+ intro?: string;
6
+ /** Company or product website */
7
+ websiteUrl?: string;
8
+ /** Link label; defaults to hostname or "Learn more" */
9
+ websiteLabel?: string;
10
+ /** Logo URL (relative to docs mount or absolute) */
11
+ logoUrl?: string;
12
+ /** Dark-mode logo; falls back to logoUrl */
13
+ logoUrlDark?: string;
14
+ /** Optional srcset, e.g. "./brand/mark.png 1x, ./brand/mark@2x.png 2x" */
15
+ logoSrcSet?: string;
16
+ /** Display size in px. Default 80 */
17
+ logoSize?: number;
18
+ };
19
+ export type CallsheetMcp = {
20
+ /** MCP endpoint (absolute or relative). Default: sibling of docs at mcpPath */
21
+ url?: string;
22
+ /** Auth note shown when private MCP tools exist */
23
+ authHint?: string;
24
+ };
25
+ export type CallsheetConfig = {
26
+ specUrl: string;
27
+ rpcBase: string;
28
+ title?: string;
29
+ branding?: CallsheetBranding;
30
+ /** Relative path from docs to MCP endpoint. Default `../mcp` */
31
+ mcpPath?: string;
32
+ mcp?: CallsheetMcp;
33
+ };
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,4 @@
1
+ export { parseCallspecOpenApi } from './parseOpenApi';
2
+ export type { CallsheetAccess, CallsheetRoute, CallsheetSpec } from './parseOpenApi';
3
+ export { mountCallsheet, renderCallsheetPage } from './mountCallsheet';
4
+ export type { MountCallsheetOptions, CallsheetConfig, CallsheetBranding, CallsheetMcp } from './mountCallsheet';
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.renderCallsheetPage = exports.mountCallsheet = exports.parseCallspecOpenApi = void 0;
4
+ var parseOpenApi_1 = require("./parseOpenApi");
5
+ Object.defineProperty(exports, "parseCallspecOpenApi", { enumerable: true, get: function () { return parseOpenApi_1.parseCallspecOpenApi; } });
6
+ var mountCallsheet_1 = require("./mountCallsheet");
7
+ Object.defineProperty(exports, "mountCallsheet", { enumerable: true, get: function () { return mountCallsheet_1.mountCallsheet; } });
8
+ Object.defineProperty(exports, "renderCallsheetPage", { enumerable: true, get: function () { return mountCallsheet_1.renderCallsheetPage; } });
@@ -0,0 +1,22 @@
1
+ import type { Router } from 'express';
2
+ import type { CallsheetBranding, CallsheetConfig, CallsheetMcp } from './branding';
3
+ export type { CallsheetBranding, CallsheetConfig, CallsheetMcp } from './branding';
4
+ export type MountCallsheetOptions = {
5
+ /** Mount path for the UI. Default `/docs`. */
6
+ path?: string;
7
+ /** OpenAPI JSON URL for the UI to fetch. Default `../openapi.json`. */
8
+ specPath?: string;
9
+ /** RPC base for try-it requests. Default `..` (sibling of the docs path). */
10
+ rpcBase?: string;
11
+ /** Page title override. */
12
+ title?: string;
13
+ /** Whitelabel home page content */
14
+ branding?: CallsheetBranding;
15
+ /** Relative path from docs to MCP endpoint. Default `../mcp` */
16
+ mcpPath?: string;
17
+ mcp?: CallsheetMcp;
18
+ /** Directory of tenant logos/static files served at `{path}/brand/` */
19
+ brandAssetsDir?: string;
20
+ };
21
+ export declare function renderCallsheetPage(config: CallsheetConfig): string;
22
+ export declare function mountCallsheet(router: Router, options?: MountCallsheetOptions): void;
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.renderCallsheetPage = renderCallsheetPage;
7
+ exports.mountCallsheet = mountCallsheet;
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const path_1 = __importDefault(require("path"));
10
+ const express_1 = __importDefault(require("express"));
11
+ const CONFIG_PLACEHOLDER = '<!--CALLSHEET_CONFIG-->';
12
+ function uiDir() {
13
+ const candidates = [
14
+ path_1.default.join(__dirname, 'ui'),
15
+ path_1.default.resolve(__dirname, '../../dist/callsheet/ui'),
16
+ path_1.default.resolve(process.cwd(), 'dist/callsheet/ui'),
17
+ ];
18
+ for (const candidate of candidates) {
19
+ if (fs_1.default.existsSync(path_1.default.join(candidate, 'index.html'))) {
20
+ return candidate;
21
+ }
22
+ }
23
+ throw new Error('callsheet UI assets missing — run `npm run build` in callspec');
24
+ }
25
+ function readIndexHtml() {
26
+ const file = path_1.default.join(uiDir(), 'index.html');
27
+ return fs_1.default.readFileSync(file, 'utf8');
28
+ }
29
+ function renderCallsheetPage(config) {
30
+ const html = readIndexHtml();
31
+ const script = `<script>window.__CALLSHEET__=${JSON.stringify(config)};</script>`;
32
+ if (html.includes(CONFIG_PLACEHOLDER)) {
33
+ return html.replace(CONFIG_PLACEHOLDER, script);
34
+ }
35
+ return html.replace('</head>', `${script}</head>`);
36
+ }
37
+ function mountCallsheet(router, options = {}) {
38
+ const mountPath = options.path ?? '/docs';
39
+ const mountPathWithSlash = mountPath.endsWith('/') ? mountPath : `${mountPath}/`;
40
+ const specUrl = options.specPath ?? '../openapi.json';
41
+ const rpcBase = options.rpcBase ?? '..';
42
+ const assetsDir = uiDir();
43
+ const servePage = (req, res) => {
44
+ if (!req.path.endsWith('/')) {
45
+ res.redirect(301, `${req.baseUrl}${mountPathWithSlash}`);
46
+ return;
47
+ }
48
+ res.type('html').send(renderCallsheetPage({
49
+ specUrl,
50
+ rpcBase,
51
+ title: options.title,
52
+ branding: options.branding,
53
+ mcpPath: options.mcpPath ?? '../mcp',
54
+ mcp: options.mcp,
55
+ }));
56
+ };
57
+ router.get(mountPath, servePage);
58
+ router.use(mountPathWithSlash, express_1.default.static(assetsDir, { index: false }));
59
+ if (options.brandAssetsDir && fs_1.default.existsSync(options.brandAssetsDir)) {
60
+ router.use(`${mountPathWithSlash}brand`, express_1.default.static(options.brandAssetsDir, { index: false }));
61
+ }
62
+ }
@@ -0,0 +1,17 @@
1
+ export type CallsheetAccess = 'public' | 'private';
2
+ export type CallsheetRoute = {
3
+ name: string;
4
+ summary: string;
5
+ description: string;
6
+ tags: string[];
7
+ access: CallsheetAccess;
8
+ mcp: boolean;
9
+ inputSchema: unknown;
10
+ outputSchema: unknown;
11
+ };
12
+ export type CallsheetSpec = {
13
+ title: string;
14
+ version: string;
15
+ routes: CallsheetRoute[];
16
+ };
17
+ export declare function parseCallspecOpenApi(doc: Record<string, unknown>): CallsheetSpec;
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseCallspecOpenApi = parseCallspecOpenApi;
4
+ function readAccess(operation) {
5
+ const ext = operation['x-callspec-access'];
6
+ if (ext === 'public' || ext === 'private')
7
+ return ext;
8
+ const security = operation.security;
9
+ if (Array.isArray(security) && security.length > 0)
10
+ return 'private';
11
+ return 'public';
12
+ }
13
+ function readMcp(operation) {
14
+ return operation['x-callspec-mcp'] === true;
15
+ }
16
+ function readJsonSchema(content) {
17
+ if (!content)
18
+ return { type: 'object' };
19
+ const appJson = content['application/json'];
20
+ if (!appJson || typeof appJson !== 'object')
21
+ return { type: 'object' };
22
+ return appJson.schema ?? { type: 'object' };
23
+ }
24
+ function parseCallspecOpenApi(doc) {
25
+ const info = doc.info;
26
+ const paths = doc.paths;
27
+ const routes = [];
28
+ if (paths) {
29
+ for (const [pathKey, methods] of Object.entries(paths)) {
30
+ const post = methods.post;
31
+ if (!post)
32
+ continue;
33
+ const name = post.operationId
34
+ ?? pathKey.replace(/^\//, '');
35
+ const requestBody = post.requestBody;
36
+ const content = requestBody?.content;
37
+ const responses = post.responses;
38
+ const ok = responses?.['200'];
39
+ const okContent = ok?.content;
40
+ routes.push({
41
+ name,
42
+ summary: post.summary ?? name,
43
+ description: post.description ?? '',
44
+ tags: Array.isArray(post.tags) ? post.tags.map(String) : [],
45
+ access: readAccess(post),
46
+ mcp: readMcp(post),
47
+ inputSchema: readJsonSchema(content),
48
+ outputSchema: readJsonSchema(okContent),
49
+ });
50
+ }
51
+ }
52
+ routes.sort((a, b) => a.name.localeCompare(b.name));
53
+ return {
54
+ title: info?.title ?? 'API',
55
+ version: info?.version ?? '0.0.0',
56
+ routes,
57
+ };
58
+ }