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.
- package/LICENSE +21 -0
- package/README.md +170 -0
- package/assets/callsheet-chirp-demo.png +0 -0
- package/assets/callspec-lockup-dark.svg +11 -0
- package/assets/callspec-lockup-light.svg +11 -0
- package/assets/callspec-mark-dark.svg +10 -0
- package/assets/callspec-mark-light.svg +10 -0
- package/assets/chirp/mark-dark.svg +7 -0
- package/assets/chirp/mark.png +0 -0
- package/assets/chirp/mark.svg +7 -0
- package/assets/chirp/mark@2x.png +0 -0
- package/dist/callsheet/branding.d.ts +33 -0
- package/dist/callsheet/branding.js +2 -0
- package/dist/callsheet/index.d.ts +4 -0
- package/dist/callsheet/index.js +8 -0
- package/dist/callsheet/mountCallsheet.d.ts +22 -0
- package/dist/callsheet/mountCallsheet.js +62 -0
- package/dist/callsheet/parseOpenApi.d.ts +17 -0
- package/dist/callsheet/parseOpenApi.js +58 -0
- package/dist/callsheet/ui/assets/app.js +226 -0
- package/dist/callsheet/ui/assets/callspec-mark-dark.svg +10 -0
- package/dist/callsheet/ui/assets/callspec-mark-light.svg +10 -0
- package/dist/callsheet/ui/assets/style.css +2 -0
- package/dist/callsheet/ui/index.html +25 -0
- package/dist/client.d.ts +15 -0
- package/dist/client.js +49 -0
- package/dist/defineRegistry.d.ts +3 -0
- package/dist/defineRegistry.js +11 -0
- package/dist/defineRoute.d.ts +9 -0
- package/dist/defineRoute.js +16 -0
- package/dist/errors.d.ts +10 -0
- package/dist/errors.js +25 -0
- package/dist/executeRoute.d.ts +2 -0
- package/dist/executeRoute.js +16 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +29 -0
- package/dist/mcpTools.d.ts +14 -0
- package/dist/mcpTools.js +41 -0
- package/dist/mountMcp.d.ts +13 -0
- package/dist/mountMcp.js +100 -0
- package/dist/mountRegistry.d.ts +32 -0
- package/dist/mountRegistry.js +88 -0
- package/dist/openapi.d.ts +8 -0
- package/dist/openapi.js +56 -0
- package/dist/serializer.d.ts +2 -0
- package/dist/serializer.js +45 -0
- package/dist/types.d.ts +35 -0
- package/dist/types.js +2 -0
- 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,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
|
+
}
|