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
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { McpRouteConfig, Registry, RouteDef } from './types';
|
|
2
|
+
export declare function isMcpEnabled(route: RouteDef<any, any, any>): boolean;
|
|
3
|
+
export declare function routeMcpName(routeKey: string, route: RouteDef<any, any, any>): string;
|
|
4
|
+
export declare function mcpAnnotations(route: RouteDef<any, any, any>): Record<string, unknown> | undefined;
|
|
5
|
+
export type McpToolListEntry = {
|
|
6
|
+
name: string;
|
|
7
|
+
title?: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
inputSchema: Record<string, unknown>;
|
|
10
|
+
outputSchema?: Record<string, unknown>;
|
|
11
|
+
annotations?: Record<string, unknown>;
|
|
12
|
+
};
|
|
13
|
+
export declare function listMcpTools(registry: Registry<any>): McpToolListEntry[];
|
|
14
|
+
export type { McpRouteConfig };
|
package/dist/mcpTools.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isMcpEnabled = isMcpEnabled;
|
|
4
|
+
exports.routeMcpName = routeMcpName;
|
|
5
|
+
exports.mcpAnnotations = mcpAnnotations;
|
|
6
|
+
exports.listMcpTools = listMcpTools;
|
|
7
|
+
const runtyp_1 = require("runtyp");
|
|
8
|
+
function isMcpEnabled(route) {
|
|
9
|
+
return route.mcp !== undefined;
|
|
10
|
+
}
|
|
11
|
+
function routeMcpName(routeKey, route) {
|
|
12
|
+
if (route.mcp && typeof route.mcp === 'object' && route.mcp.name) {
|
|
13
|
+
return route.mcp.name;
|
|
14
|
+
}
|
|
15
|
+
return routeKey;
|
|
16
|
+
}
|
|
17
|
+
function mcpAnnotations(route) {
|
|
18
|
+
if (route.mcp && typeof route.mcp === 'object') {
|
|
19
|
+
return route.mcp.annotations;
|
|
20
|
+
}
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
function listMcpTools(registry) {
|
|
24
|
+
return Object.entries(registry)
|
|
25
|
+
.filter(([, route]) => isMcpEnabled(route))
|
|
26
|
+
.map(([key, route]) => {
|
|
27
|
+
const entry = {
|
|
28
|
+
name: routeMcpName(key, route),
|
|
29
|
+
title: route.meta.summary,
|
|
30
|
+
description: route.meta.description,
|
|
31
|
+
inputSchema: (0, runtyp_1.toJsonSchema)(route.input),
|
|
32
|
+
};
|
|
33
|
+
if (route.output) {
|
|
34
|
+
entry.outputSchema = (0, runtyp_1.toJsonSchema)(route.output);
|
|
35
|
+
}
|
|
36
|
+
const annotations = mcpAnnotations(route);
|
|
37
|
+
if (annotations)
|
|
38
|
+
entry.annotations = annotations;
|
|
39
|
+
return entry;
|
|
40
|
+
});
|
|
41
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Router } from 'express';
|
|
2
|
+
import type { ContextResolver, Registry } from './types';
|
|
3
|
+
export type MountMcpOptions<Ctx> = {
|
|
4
|
+
path?: string;
|
|
5
|
+
contextResolver?: ContextResolver<Ctx>;
|
|
6
|
+
expose?: boolean;
|
|
7
|
+
serverInfo: {
|
|
8
|
+
name: string;
|
|
9
|
+
version: string;
|
|
10
|
+
};
|
|
11
|
+
instructions?: string;
|
|
12
|
+
};
|
|
13
|
+
export declare function mountMcp<Ctx>(router: Router, registry: Registry<Ctx>, options: MountMcpOptions<Ctx>): void;
|
package/dist/mountMcp.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.mountMcp = mountMcp;
|
|
4
|
+
const errors_1 = require("./errors");
|
|
5
|
+
const executeRoute_1 = require("./executeRoute");
|
|
6
|
+
const mcpTools_1 = require("./mcpTools");
|
|
7
|
+
function toolError(message) {
|
|
8
|
+
return {
|
|
9
|
+
content: [{ type: 'text', text: message }],
|
|
10
|
+
isError: true,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
function mountMcp(router, registry, options) {
|
|
14
|
+
if (options.expose === false)
|
|
15
|
+
return;
|
|
16
|
+
const mcpPath = options.path ?? '/mcp';
|
|
17
|
+
router.all(mcpPath, (async (req, res) => {
|
|
18
|
+
const body = req.body;
|
|
19
|
+
const respond = (result) => {
|
|
20
|
+
res.json({
|
|
21
|
+
jsonrpc: '2.0',
|
|
22
|
+
id: body?.id ?? null,
|
|
23
|
+
result,
|
|
24
|
+
});
|
|
25
|
+
};
|
|
26
|
+
const respondError = (code, message) => {
|
|
27
|
+
res.status(code >= 400 ? code : 500).json({
|
|
28
|
+
jsonrpc: '2.0',
|
|
29
|
+
id: body?.id ?? null,
|
|
30
|
+
error: { code, message },
|
|
31
|
+
});
|
|
32
|
+
};
|
|
33
|
+
try {
|
|
34
|
+
if (body?.method === 'initialize') {
|
|
35
|
+
respond({
|
|
36
|
+
protocolVersion: '2024-11-05',
|
|
37
|
+
capabilities: { tools: {} },
|
|
38
|
+
serverInfo: options.serverInfo,
|
|
39
|
+
instructions: options.instructions,
|
|
40
|
+
});
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (body?.method === 'tools/list') {
|
|
44
|
+
respond({ tools: (0, mcpTools_1.listMcpTools)(registry) });
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (body?.method === 'tools/call') {
|
|
48
|
+
const toolName = body.params?.name;
|
|
49
|
+
if (!toolName) {
|
|
50
|
+
respond(toolError('Missing tool name'));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const routeEntry = Object.entries(registry).find(([key, route]) => (0, mcpTools_1.routeMcpName)(key, route) === toolName);
|
|
54
|
+
if (!routeEntry) {
|
|
55
|
+
respond(toolError(`Unknown tool: ${toolName}`));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const [, route] = routeEntry;
|
|
59
|
+
if (!(0, mcpTools_1.isMcpEnabled)(route)) {
|
|
60
|
+
respond(toolError(`Tool not exposed: ${toolName}`));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const ctx = options.contextResolver
|
|
64
|
+
? await options.contextResolver(req)
|
|
65
|
+
: undefined;
|
|
66
|
+
try {
|
|
67
|
+
const result = await (0, executeRoute_1.executeRoute)(route, body.params?.arguments ?? {}, ctx);
|
|
68
|
+
respond({
|
|
69
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
70
|
+
structuredContent: result,
|
|
71
|
+
});
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
if (err instanceof errors_1.CallspecUnauthorizedError) {
|
|
76
|
+
respond(toolError('Unauthorized — Bearer token required'));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (err instanceof errors_1.CallspecValidationError) {
|
|
80
|
+
respond(toolError(JSON.stringify(err.errors)));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
throw err;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (body?.method === 'notifications/initialized') {
|
|
87
|
+
res.status(204).end();
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
respondError(400, `Unsupported method: ${body?.method ?? 'unknown'}`);
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
if (err instanceof errors_1.CallspecNotFoundError) {
|
|
94
|
+
respondError(404, err.message);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
respondError(500, err instanceof Error ? err.message : 'Internal error');
|
|
98
|
+
}
|
|
99
|
+
}));
|
|
100
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Router } from 'express';
|
|
2
|
+
import { type OpenApiOptions } from './openapi';
|
|
3
|
+
import type { ContextResolver, Registry } from './types';
|
|
4
|
+
import { type MountCallsheetOptions } from './callsheet/mountCallsheet';
|
|
5
|
+
export type MountRegistryDocsOptions = {
|
|
6
|
+
/** OpenAPI document metadata. Required to expose docs. */
|
|
7
|
+
openApi?: OpenApiOptions;
|
|
8
|
+
/** Serve GET …/openapi.json. Default: true when docs are enabled. */
|
|
9
|
+
exposeOpenApi?: boolean;
|
|
10
|
+
/** Serve the callsheet UI at `/docs`. Default: true when docs are enabled. */
|
|
11
|
+
exposeUi?: boolean;
|
|
12
|
+
/** OpenAPI JSON path on this router. Default `/openapi.json`. */
|
|
13
|
+
openApiPath?: string;
|
|
14
|
+
/** callsheet UI mount path. Default `/docs`. */
|
|
15
|
+
uiPath?: string;
|
|
16
|
+
/** Options passed to callsheet (rpcBase override, branding, MCP, etc.). */
|
|
17
|
+
callsheet?: Pick<MountCallsheetOptions, 'rpcBase' | 'branding' | 'mcpPath' | 'mcp' | 'brandAssetsDir'>;
|
|
18
|
+
};
|
|
19
|
+
export type MountRegistryOptions<Ctx> = {
|
|
20
|
+
contextResolver?: ContextResolver<Ctx>;
|
|
21
|
+
/**
|
|
22
|
+
* @deprecated Use `docs.openApi` with `docs.exposeOpenApi` / `docs.exposeUi`.
|
|
23
|
+
* When true and `openApi` or `docs.openApi` is set, enables docs surfaces.
|
|
24
|
+
*/
|
|
25
|
+
exposeDocs?: boolean;
|
|
26
|
+
/** @deprecated Use `docs.openApi`. */
|
|
27
|
+
openApi?: OpenApiOptions;
|
|
28
|
+
/** Docs configuration — OpenAPI spec and/or callsheet UI, each toggled independently. */
|
|
29
|
+
docs?: MountRegistryDocsOptions | false;
|
|
30
|
+
basePath?: string;
|
|
31
|
+
};
|
|
32
|
+
export declare function mountRegistry<Ctx>(router: Router, registry: Registry<Ctx>, options?: MountRegistryOptions<Ctx>): void;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.mountRegistry = mountRegistry;
|
|
4
|
+
const errors_1 = require("./errors");
|
|
5
|
+
const executeRoute_1 = require("./executeRoute");
|
|
6
|
+
const openapi_1 = require("./openapi");
|
|
7
|
+
const mountCallsheet_1 = require("./callsheet/mountCallsheet");
|
|
8
|
+
function sendError(res, err) {
|
|
9
|
+
if (err instanceof errors_1.CallspecValidationError) {
|
|
10
|
+
res.status(400).json({ error: err.message, errors: err.errors });
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
if (err instanceof errors_1.CallspecUnauthorizedError) {
|
|
14
|
+
res.status(401).send('Unauthorized');
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
throw err;
|
|
18
|
+
}
|
|
19
|
+
function resolveDocsOptions(options) {
|
|
20
|
+
if (options.docs === false)
|
|
21
|
+
return false;
|
|
22
|
+
const legacyEnabled = options.exposeDocs === true;
|
|
23
|
+
const merged = options.docs ?? {};
|
|
24
|
+
const openApi = merged.openApi ?? options.openApi;
|
|
25
|
+
if (!openApi)
|
|
26
|
+
return false;
|
|
27
|
+
if (!legacyEnabled && options.docs === undefined && options.exposeDocs === undefined) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
const docsEnabled = legacyEnabled || options.docs !== undefined;
|
|
31
|
+
return {
|
|
32
|
+
openApi,
|
|
33
|
+
exposeOpenApi: merged.exposeOpenApi ?? (docsEnabled ? true : false),
|
|
34
|
+
exposeUi: merged.exposeUi ?? (docsEnabled ? true : false),
|
|
35
|
+
openApiPath: merged.openApiPath ?? '/openapi.json',
|
|
36
|
+
uiPath: merged.uiPath ?? '/docs',
|
|
37
|
+
callsheet: merged.callsheet,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function mountRegistry(router, registry, options = {}) {
|
|
41
|
+
const basePath = options.basePath ?? '';
|
|
42
|
+
const docs = resolveDocsOptions(options);
|
|
43
|
+
if (docs && docs.openApi && docs.exposeOpenApi !== false) {
|
|
44
|
+
const openApiPath = docs.openApiPath ?? '/openapi.json';
|
|
45
|
+
router.get(`${basePath}${openApiPath}`, (_req, res) => {
|
|
46
|
+
res.json((0, openapi_1.emitOpenApi)(registry, {
|
|
47
|
+
...docs.openApi,
|
|
48
|
+
basePath,
|
|
49
|
+
}));
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
if (docs && docs.exposeUi !== false) {
|
|
53
|
+
const uiPath = docs.uiPath ?? '/docs';
|
|
54
|
+
const openApiPath = docs.openApiPath ?? '/openapi.json';
|
|
55
|
+
const specPath = openApiPath.startsWith('/')
|
|
56
|
+
? `..${openApiPath}`
|
|
57
|
+
: openApiPath;
|
|
58
|
+
(0, mountCallsheet_1.mountCallsheet)(router, {
|
|
59
|
+
path: `${basePath}${uiPath}`.replace(/\/{2,}/g, '/') || '/docs',
|
|
60
|
+
specPath,
|
|
61
|
+
rpcBase: docs.callsheet?.rpcBase ?? '..',
|
|
62
|
+
title: docs.openApi?.title,
|
|
63
|
+
branding: docs.callsheet?.branding,
|
|
64
|
+
mcpPath: docs.callsheet?.mcpPath,
|
|
65
|
+
mcp: docs.callsheet?.mcp,
|
|
66
|
+
brandAssetsDir: docs.callsheet?.brandAssetsDir,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
for (const [name, route] of Object.entries(registry)) {
|
|
70
|
+
router.post(`${basePath}/${name}`, (async (req, res, next) => {
|
|
71
|
+
try {
|
|
72
|
+
const ctx = options.contextResolver
|
|
73
|
+
? await options.contextResolver(req)
|
|
74
|
+
: undefined;
|
|
75
|
+
const response = await (0, executeRoute_1.executeRoute)(route, req.body, ctx);
|
|
76
|
+
res.json(response);
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
try {
|
|
80
|
+
sendError(res, err);
|
|
81
|
+
}
|
|
82
|
+
catch (rethrow) {
|
|
83
|
+
next(rethrow);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Registry } from './types';
|
|
2
|
+
export type OpenApiOptions = {
|
|
3
|
+
title: string;
|
|
4
|
+
version: string;
|
|
5
|
+
basePath?: string;
|
|
6
|
+
security?: Array<Record<string, string[]>>;
|
|
7
|
+
};
|
|
8
|
+
export declare function emitOpenApi(registry: Registry<any>, options: OpenApiOptions): Record<string, unknown>;
|
package/dist/openapi.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.emitOpenApi = emitOpenApi;
|
|
4
|
+
const runtyp_1 = require("runtyp");
|
|
5
|
+
function routePath(basePath, name) {
|
|
6
|
+
return `${basePath}/${name}`.replace(/\/{2,}/g, '/');
|
|
7
|
+
}
|
|
8
|
+
function emitOpenApi(registry, options) {
|
|
9
|
+
const paths = {};
|
|
10
|
+
const basePath = options.basePath ?? '';
|
|
11
|
+
for (const [name, route] of Object.entries(registry)) {
|
|
12
|
+
paths[routePath(basePath, name)] = {
|
|
13
|
+
post: {
|
|
14
|
+
operationId: name,
|
|
15
|
+
summary: route.meta.summary,
|
|
16
|
+
description: route.meta.description,
|
|
17
|
+
tags: [...route.meta.tags],
|
|
18
|
+
requestBody: {
|
|
19
|
+
required: true,
|
|
20
|
+
content: {
|
|
21
|
+
'application/json': {
|
|
22
|
+
schema: (0, runtyp_1.toJsonSchema)(route.input),
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
responses: {
|
|
27
|
+
200: {
|
|
28
|
+
description: 'Success',
|
|
29
|
+
content: route.output ? {
|
|
30
|
+
'application/json': {
|
|
31
|
+
schema: (0, runtyp_1.toJsonSchema)(route.output),
|
|
32
|
+
},
|
|
33
|
+
} : {
|
|
34
|
+
'application/json': {
|
|
35
|
+
schema: { type: 'object' },
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
401: { description: 'Unauthorized' },
|
|
40
|
+
400: { description: 'Validation error' },
|
|
41
|
+
},
|
|
42
|
+
...(route.access === 'private' && options.security ? { security: options.security } : {}),
|
|
43
|
+
'x-callspec-access': route.access,
|
|
44
|
+
...(route.mcp ? { 'x-callspec-mcp': true } : {}),
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
openapi: '3.1.0',
|
|
50
|
+
info: {
|
|
51
|
+
title: options.title,
|
|
52
|
+
version: options.version,
|
|
53
|
+
},
|
|
54
|
+
paths,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.serializeResponse = serializeResponse;
|
|
4
|
+
exports.deserializeResponse = deserializeResponse;
|
|
5
|
+
function serializeResponse(data) {
|
|
6
|
+
if (data === null || data === undefined)
|
|
7
|
+
return data;
|
|
8
|
+
if (data instanceof Date) {
|
|
9
|
+
return {
|
|
10
|
+
__type: 'Date',
|
|
11
|
+
value: data.toISOString(),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
if (Array.isArray(data))
|
|
15
|
+
return data.map(serializeResponse);
|
|
16
|
+
if (typeof data === 'object') {
|
|
17
|
+
const serialized = {};
|
|
18
|
+
for (const [key, value] of Object.entries(data)) {
|
|
19
|
+
serialized[key] = serializeResponse(value);
|
|
20
|
+
}
|
|
21
|
+
return serialized;
|
|
22
|
+
}
|
|
23
|
+
return data;
|
|
24
|
+
}
|
|
25
|
+
function deserializeResponse(data) {
|
|
26
|
+
if (data === null || data === undefined)
|
|
27
|
+
return data;
|
|
28
|
+
if (typeof data === 'object'
|
|
29
|
+
&& data !== null
|
|
30
|
+
&& '__type' in data
|
|
31
|
+
&& data.__type === 'Date'
|
|
32
|
+
&& 'value' in data) {
|
|
33
|
+
return new Date(data.value);
|
|
34
|
+
}
|
|
35
|
+
if (Array.isArray(data))
|
|
36
|
+
return data.map(deserializeResponse);
|
|
37
|
+
if (typeof data === 'object') {
|
|
38
|
+
const deserialized = {};
|
|
39
|
+
for (const [key, value] of Object.entries(data)) {
|
|
40
|
+
deserialized[key] = deserializeResponse(value);
|
|
41
|
+
}
|
|
42
|
+
return deserialized;
|
|
43
|
+
}
|
|
44
|
+
return data;
|
|
45
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Request } from 'express';
|
|
2
|
+
import type { Pred } from 'runtyp';
|
|
3
|
+
export type RouteMeta = {
|
|
4
|
+
summary: string;
|
|
5
|
+
description: string;
|
|
6
|
+
tags: readonly string[];
|
|
7
|
+
};
|
|
8
|
+
export type McpRouteConfig = true | {
|
|
9
|
+
name?: string;
|
|
10
|
+
annotations?: Record<string, unknown>;
|
|
11
|
+
};
|
|
12
|
+
export type RouteAccess = 'public' | 'private';
|
|
13
|
+
export type RouteHandler<TInput, TOutput, Ctx> = (input: TInput, ctx: Ctx) => Promise<TOutput> | TOutput;
|
|
14
|
+
export type RouteDef<TInput = unknown, TOutput = unknown, Ctx = unknown> = {
|
|
15
|
+
input: Pred<TInput>;
|
|
16
|
+
output?: Pred<TOutput>;
|
|
17
|
+
meta: RouteMeta;
|
|
18
|
+
access: RouteAccess;
|
|
19
|
+
mcp?: McpRouteConfig;
|
|
20
|
+
handler: RouteHandler<TInput, TOutput, Ctx>;
|
|
21
|
+
};
|
|
22
|
+
export type Registry<Ctx = unknown> = Record<string, RouteDef<any, any, Ctx>>;
|
|
23
|
+
export type ContextResolver<Ctx> = (req: Request) => Ctx | Promise<Ctx | undefined> | Ctx | undefined;
|
|
24
|
+
export type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
|
|
25
|
+
export type InferRouteInput<R extends RouteDef<any, any, any>> = R extends RouteDef<infer I, any, any> ? I : never;
|
|
26
|
+
export type InferRouteOutput<R extends RouteDef<any, any, any>> = R extends {
|
|
27
|
+
output: Pred<infer O>;
|
|
28
|
+
} ? O : UnwrapPromise<ReturnType<R['handler']>>;
|
|
29
|
+
export type InferRegistry<T extends Registry<any>> = {
|
|
30
|
+
[K in keyof T]: {
|
|
31
|
+
name: K & string;
|
|
32
|
+
input: InferRouteInput<T[K]>;
|
|
33
|
+
output: InferRouteOutput<T[K]>;
|
|
34
|
+
};
|
|
35
|
+
};
|
package/dist/types.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "callspec",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Registry-first RPC: HTTP, OpenAPI docs, MCP, and typed client from one defineRoute registry",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"rpc",
|
|
7
|
+
"openapi",
|
|
8
|
+
"mcp",
|
|
9
|
+
"express",
|
|
10
|
+
"typescript",
|
|
11
|
+
"api",
|
|
12
|
+
"registry"
|
|
13
|
+
],
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/logfoxai/callspec.git"
|
|
17
|
+
},
|
|
18
|
+
"homepage": "https://github.com/logfoxai/callspec",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"author": "Marc H. Weiner <mhweiner234@gmail.com>",
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"main": "dist/index.js",
|
|
25
|
+
"types": "dist/index.d.ts",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"default": "./dist/index.js"
|
|
30
|
+
},
|
|
31
|
+
"./client": {
|
|
32
|
+
"types": "./dist/client.d.ts",
|
|
33
|
+
"default": "./dist/client.js"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"sideEffects": false,
|
|
37
|
+
"files": [
|
|
38
|
+
"dist",
|
|
39
|
+
"assets",
|
|
40
|
+
"!dist/**/*.spec.*"
|
|
41
|
+
],
|
|
42
|
+
"scripts": {
|
|
43
|
+
"prepare": "npm run build",
|
|
44
|
+
"build": "npm run build:server && npm run build:ui",
|
|
45
|
+
"build:server": "tsc",
|
|
46
|
+
"build:ui": "node scripts/build-callsheet-ui.mjs",
|
|
47
|
+
"lint": "eslint . --fix",
|
|
48
|
+
"lint:check": "eslint .",
|
|
49
|
+
"test": "kizu 'src/**/*.spec.ts'",
|
|
50
|
+
"dev:docs": "node scripts/dev-docs.cjs",
|
|
51
|
+
"validate": "npm run check:package-json && npm run build && npm run lint && npm test",
|
|
52
|
+
"check:package-json": "node scripts/check-no-file-deps.mjs"
|
|
53
|
+
},
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"@modelcontextprotocol/sdk": "1.29.0",
|
|
56
|
+
"highlight.js": "11.11.1",
|
|
57
|
+
"runtyp": "2.5.0"
|
|
58
|
+
},
|
|
59
|
+
"peerDependencies": {
|
|
60
|
+
"express": "^4.18.0"
|
|
61
|
+
},
|
|
62
|
+
"peerDependenciesMeta": {
|
|
63
|
+
"express": {
|
|
64
|
+
"optional": false
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
"devDependencies": {
|
|
68
|
+
"@types/express": "^4.17.17",
|
|
69
|
+
"@types/node": "^25.5.0",
|
|
70
|
+
"eslint": "^10.0.0",
|
|
71
|
+
"express": "^4.18.2",
|
|
72
|
+
"kizu": "^4.0.0",
|
|
73
|
+
"typescript": "^5.6.0",
|
|
74
|
+
"typescript-eslint": "^8.0.0",
|
|
75
|
+
"vite": "^8.0.0"
|
|
76
|
+
}
|
|
77
|
+
}
|