@contractspec/lib.contracts-runtime-server-mcp 2.0.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/README.md +3 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +186 -0
- package/dist/mcp/createMcpServer.d.ts +6 -0
- package/dist/mcp/createMcpServer.js +182 -0
- package/dist/mcp/mcpTypes.d.ts +18 -0
- package/dist/mcp/mcpTypes.js +1 -0
- package/dist/mcp/registerPresentations.d.ts +3 -0
- package/dist/mcp/registerPresentations.js +62 -0
- package/dist/mcp/registerPrompts.d.ts +4 -0
- package/dist/mcp/registerPrompts.js +46 -0
- package/dist/mcp/registerResources.d.ts +4 -0
- package/dist/mcp/registerResources.js +45 -0
- package/dist/mcp/registerTools.d.ts +4 -0
- package/dist/mcp/registerTools.js +22 -0
- package/dist/node/index.js +185 -0
- package/dist/node/mcp/createMcpServer.js +181 -0
- package/dist/node/mcp/mcpTypes.js +0 -0
- package/dist/node/mcp/registerPresentations.js +61 -0
- package/dist/node/mcp/registerPrompts.js +45 -0
- package/dist/node/mcp/registerResources.js +44 -0
- package/dist/node/mcp/registerTools.js +21 -0
- package/dist/node/provider-mcp.js +181 -0
- package/dist/provider-mcp.d.ts +1 -0
- package/dist/provider-mcp.js +182 -0
- package/package.json +140 -0
package/README.md
ADDED
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export * from './provider-mcp';
|
|
2
|
+
export * from './mcp/createMcpServer';
|
|
3
|
+
export * from './mcp/mcpTypes';
|
|
4
|
+
export * from './mcp/registerPresentations';
|
|
5
|
+
export * from './mcp/registerPrompts';
|
|
6
|
+
export * from './mcp/registerResources';
|
|
7
|
+
export * from './mcp/registerTools';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/mcp/registerTools.ts
|
|
3
|
+
import { defaultMcpTool } from "@contractspec/lib.contracts-spec/jsonschema";
|
|
4
|
+
function registerMcpTools(server, ops, ctx) {
|
|
5
|
+
for (const spec of ops.list()) {
|
|
6
|
+
if (spec.meta.kind !== "command")
|
|
7
|
+
continue;
|
|
8
|
+
const toolName = spec.transport?.mcp?.toolName ?? defaultMcpTool(spec.meta.key, spec.meta.version);
|
|
9
|
+
server.registerTool(toolName, {
|
|
10
|
+
description: spec.meta.description,
|
|
11
|
+
inputSchema: spec.io.input?.getZod()
|
|
12
|
+
}, async (args) => {
|
|
13
|
+
const result = await ops.execute(spec.meta.key, spec.meta.version, args ?? {}, ctx.toolCtx());
|
|
14
|
+
return {
|
|
15
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 4) }]
|
|
16
|
+
};
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// src/mcp/registerResources.ts
|
|
22
|
+
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
23
|
+
import { Buffer } from "buffer";
|
|
24
|
+
function mcpResourceMeta(resource) {
|
|
25
|
+
return {
|
|
26
|
+
title: resource.meta.title,
|
|
27
|
+
description: resource.meta.description,
|
|
28
|
+
mimeType: resource.meta.mimeType,
|
|
29
|
+
_meta: { tags: resource.meta.tags ?? [] }
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function registerMcpResources(server, resources, ctx) {
|
|
33
|
+
for (const resource of resources.listTemplates()) {
|
|
34
|
+
ctx.logger.debug("Registering resource: " + resource.meta.uriTemplate);
|
|
35
|
+
server.registerResource(resource.meta.uriTemplate, new ResourceTemplate(resource.meta.uriTemplate, { list: undefined }), mcpResourceMeta(resource), async (_uri, variables) => {
|
|
36
|
+
const parsedArgs = resource.input.parse(variables);
|
|
37
|
+
const out = await resource.resolve(parsedArgs, ctx.resourceCtx());
|
|
38
|
+
if (typeof out.data === "string") {
|
|
39
|
+
return {
|
|
40
|
+
contents: [
|
|
41
|
+
{
|
|
42
|
+
uri: out.uri,
|
|
43
|
+
mimeType: out.mimeType ?? resource.meta.mimeType,
|
|
44
|
+
text: out.data
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
contents: [
|
|
51
|
+
{
|
|
52
|
+
uri: out.uri,
|
|
53
|
+
mimeType: out.mimeType ?? resource.meta.mimeType,
|
|
54
|
+
blob: Buffer.from(out.data).toString("base64")
|
|
55
|
+
}
|
|
56
|
+
]
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
ctx.logger.debug("Registered resource: " + resource.meta.uriTemplate);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// src/mcp/registerPrompts.ts
|
|
64
|
+
import { defaultMcpPrompt } from "@contractspec/lib.contracts-spec/jsonschema";
|
|
65
|
+
function promptArgsSchemaFromPromptArgs(args) {
|
|
66
|
+
const shape = {};
|
|
67
|
+
for (const a of args)
|
|
68
|
+
shape[a.name] = a.schema;
|
|
69
|
+
return shape;
|
|
70
|
+
}
|
|
71
|
+
function registerMcpPrompts(server, prompts, ctx) {
|
|
72
|
+
for (const prompt of prompts.list()) {
|
|
73
|
+
const promptName = defaultMcpPrompt(prompt.meta.key);
|
|
74
|
+
server.registerPrompt(promptName, {
|
|
75
|
+
title: prompt.meta.title,
|
|
76
|
+
description: prompt.meta.description,
|
|
77
|
+
argsSchema: promptArgsSchemaFromPromptArgs(prompt.args)
|
|
78
|
+
}, async (args) => {
|
|
79
|
+
const link = (tpl, vars) => {
|
|
80
|
+
let out = tpl;
|
|
81
|
+
for (const [k, v] of Object.entries(vars)) {
|
|
82
|
+
out = out.replace(new RegExp(`\\{${k}\\}`, "g"), encodeURIComponent(String(v)));
|
|
83
|
+
}
|
|
84
|
+
return out;
|
|
85
|
+
};
|
|
86
|
+
const parts = await prompt.render(prompt.input.parse(args), {
|
|
87
|
+
...ctx.promptCtx(),
|
|
88
|
+
link
|
|
89
|
+
});
|
|
90
|
+
const text = parts.map((p) => p.type === "text" ? p.text : `See resource: ${p.title ?? p.uri}
|
|
91
|
+
URI: ${p.uri}`).join(`
|
|
92
|
+
|
|
93
|
+
`);
|
|
94
|
+
const contents = {
|
|
95
|
+
type: "text",
|
|
96
|
+
text
|
|
97
|
+
};
|
|
98
|
+
return {
|
|
99
|
+
messages: [{ role: "assistant", content: contents }],
|
|
100
|
+
description: prompt.meta.description
|
|
101
|
+
};
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// src/mcp/registerPresentations.ts
|
|
107
|
+
import"@contractspec/lib.contracts-spec/presentations";
|
|
108
|
+
import {
|
|
109
|
+
createDefaultTransformEngine,
|
|
110
|
+
registerBasicValidation,
|
|
111
|
+
registerDefaultReactRenderer
|
|
112
|
+
} from "@contractspec/lib.contracts-spec/presentations/transform-engine";
|
|
113
|
+
import { sanitizeMcpName } from "@contractspec/lib.contracts-spec/jsonschema";
|
|
114
|
+
function isEngineRenderOutput(x) {
|
|
115
|
+
if (!x || typeof x !== "object")
|
|
116
|
+
return false;
|
|
117
|
+
return "body" in x && typeof x.body === "string";
|
|
118
|
+
}
|
|
119
|
+
function registerMcpPresentations(server, ctx) {
|
|
120
|
+
if (!ctx.presentations?.count()) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const engine = registerBasicValidation(registerDefaultReactRenderer(createDefaultTransformEngine()));
|
|
124
|
+
for (const presentationSpec of ctx.presentations.list()) {
|
|
125
|
+
const baseKey = sanitizeMcpName(`presentation_${presentationSpec.meta.key}_v${presentationSpec.meta.version}`);
|
|
126
|
+
const baseUri = `presentation://${presentationSpec.meta.key}/v${presentationSpec.meta.version}`;
|
|
127
|
+
ctx.logger.debug(`Registering presentation ${baseUri} for ${baseKey}`);
|
|
128
|
+
server.registerResource(baseKey, baseUri, {
|
|
129
|
+
title: `${presentationSpec.meta.key} v${presentationSpec.meta.version}`,
|
|
130
|
+
description: presentationSpec.meta.description ?? "Presentation",
|
|
131
|
+
mimeType: "application/json"
|
|
132
|
+
}, async () => {
|
|
133
|
+
const jsonText = JSON.stringify({
|
|
134
|
+
meta: presentationSpec.meta,
|
|
135
|
+
source: presentationSpec.source,
|
|
136
|
+
targets: presentationSpec.targets
|
|
137
|
+
}, null, 2);
|
|
138
|
+
return {
|
|
139
|
+
contents: [
|
|
140
|
+
{
|
|
141
|
+
uri: baseUri,
|
|
142
|
+
mimeType: "application/json",
|
|
143
|
+
text: jsonText
|
|
144
|
+
}
|
|
145
|
+
]
|
|
146
|
+
};
|
|
147
|
+
});
|
|
148
|
+
const variants = [{ ext: ".md", target: "markdown" }];
|
|
149
|
+
for (const v of variants) {
|
|
150
|
+
const key = `${baseKey}${v.ext}`;
|
|
151
|
+
const uri = `${baseUri}${v.ext}`;
|
|
152
|
+
server.registerResource(key, uri, {
|
|
153
|
+
title: `${presentationSpec.meta.key} v${presentationSpec.meta.version} (${v.ext})`,
|
|
154
|
+
description: `${presentationSpec.meta.description ?? "Presentation"} (${v.ext})`
|
|
155
|
+
}, async () => {
|
|
156
|
+
const out = await engine.render(v.target, presentationSpec);
|
|
157
|
+
const mimeType = isEngineRenderOutput(out) && out.mimeType ? out.mimeType : v.target === "markdown" ? "text/markdown" : v.target;
|
|
158
|
+
const text = isEngineRenderOutput(out) && typeof out.body === "string" ? out.body : String(out);
|
|
159
|
+
return { contents: [{ uri, mimeType, text }] };
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// src/mcp/createMcpServer.ts
|
|
166
|
+
function createMcpServer(server, ops, resources, prompts, ctxFactories) {
|
|
167
|
+
ctxFactories.logger.debug("Creating MCP server");
|
|
168
|
+
registerMcpTools(server, ops, { toolCtx: ctxFactories.toolCtx });
|
|
169
|
+
registerMcpResources(server, resources, {
|
|
170
|
+
logger: ctxFactories.logger,
|
|
171
|
+
resourceCtx: ctxFactories.resourceCtx
|
|
172
|
+
});
|
|
173
|
+
registerMcpPresentations(server, {
|
|
174
|
+
logger: ctxFactories.logger,
|
|
175
|
+
presentations: ctxFactories.presentations
|
|
176
|
+
});
|
|
177
|
+
registerMcpPrompts(server, prompts, { promptCtx: ctxFactories.promptCtx });
|
|
178
|
+
return server;
|
|
179
|
+
}
|
|
180
|
+
export {
|
|
181
|
+
registerMcpTools,
|
|
182
|
+
registerMcpResources,
|
|
183
|
+
registerMcpPrompts,
|
|
184
|
+
registerMcpPresentations,
|
|
185
|
+
createMcpServer
|
|
186
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import type { OperationSpecRegistry } from '@contractspec/lib.contracts-spec/operations/registry';
|
|
3
|
+
import type { ResourceRegistry } from '@contractspec/lib.contracts-spec/resources';
|
|
4
|
+
import type { PromptRegistry } from '@contractspec/lib.contracts-spec/promptRegistry';
|
|
5
|
+
import type { McpCtxFactories } from './mcpTypes';
|
|
6
|
+
export declare function createMcpServer(server: McpServer, ops: OperationSpecRegistry, resources: ResourceRegistry, prompts: PromptRegistry, ctxFactories: McpCtxFactories): McpServer;
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/mcp/registerTools.ts
|
|
3
|
+
import { defaultMcpTool } from "@contractspec/lib.contracts-spec/jsonschema";
|
|
4
|
+
function registerMcpTools(server, ops, ctx) {
|
|
5
|
+
for (const spec of ops.list()) {
|
|
6
|
+
if (spec.meta.kind !== "command")
|
|
7
|
+
continue;
|
|
8
|
+
const toolName = spec.transport?.mcp?.toolName ?? defaultMcpTool(spec.meta.key, spec.meta.version);
|
|
9
|
+
server.registerTool(toolName, {
|
|
10
|
+
description: spec.meta.description,
|
|
11
|
+
inputSchema: spec.io.input?.getZod()
|
|
12
|
+
}, async (args) => {
|
|
13
|
+
const result = await ops.execute(spec.meta.key, spec.meta.version, args ?? {}, ctx.toolCtx());
|
|
14
|
+
return {
|
|
15
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 4) }]
|
|
16
|
+
};
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// src/mcp/registerResources.ts
|
|
22
|
+
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
23
|
+
import { Buffer } from "buffer";
|
|
24
|
+
function mcpResourceMeta(resource) {
|
|
25
|
+
return {
|
|
26
|
+
title: resource.meta.title,
|
|
27
|
+
description: resource.meta.description,
|
|
28
|
+
mimeType: resource.meta.mimeType,
|
|
29
|
+
_meta: { tags: resource.meta.tags ?? [] }
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function registerMcpResources(server, resources, ctx) {
|
|
33
|
+
for (const resource of resources.listTemplates()) {
|
|
34
|
+
ctx.logger.debug("Registering resource: " + resource.meta.uriTemplate);
|
|
35
|
+
server.registerResource(resource.meta.uriTemplate, new ResourceTemplate(resource.meta.uriTemplate, { list: undefined }), mcpResourceMeta(resource), async (_uri, variables) => {
|
|
36
|
+
const parsedArgs = resource.input.parse(variables);
|
|
37
|
+
const out = await resource.resolve(parsedArgs, ctx.resourceCtx());
|
|
38
|
+
if (typeof out.data === "string") {
|
|
39
|
+
return {
|
|
40
|
+
contents: [
|
|
41
|
+
{
|
|
42
|
+
uri: out.uri,
|
|
43
|
+
mimeType: out.mimeType ?? resource.meta.mimeType,
|
|
44
|
+
text: out.data
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
contents: [
|
|
51
|
+
{
|
|
52
|
+
uri: out.uri,
|
|
53
|
+
mimeType: out.mimeType ?? resource.meta.mimeType,
|
|
54
|
+
blob: Buffer.from(out.data).toString("base64")
|
|
55
|
+
}
|
|
56
|
+
]
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
ctx.logger.debug("Registered resource: " + resource.meta.uriTemplate);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// src/mcp/registerPrompts.ts
|
|
64
|
+
import { defaultMcpPrompt } from "@contractspec/lib.contracts-spec/jsonschema";
|
|
65
|
+
function promptArgsSchemaFromPromptArgs(args) {
|
|
66
|
+
const shape = {};
|
|
67
|
+
for (const a of args)
|
|
68
|
+
shape[a.name] = a.schema;
|
|
69
|
+
return shape;
|
|
70
|
+
}
|
|
71
|
+
function registerMcpPrompts(server, prompts, ctx) {
|
|
72
|
+
for (const prompt of prompts.list()) {
|
|
73
|
+
const promptName = defaultMcpPrompt(prompt.meta.key);
|
|
74
|
+
server.registerPrompt(promptName, {
|
|
75
|
+
title: prompt.meta.title,
|
|
76
|
+
description: prompt.meta.description,
|
|
77
|
+
argsSchema: promptArgsSchemaFromPromptArgs(prompt.args)
|
|
78
|
+
}, async (args) => {
|
|
79
|
+
const link = (tpl, vars) => {
|
|
80
|
+
let out = tpl;
|
|
81
|
+
for (const [k, v] of Object.entries(vars)) {
|
|
82
|
+
out = out.replace(new RegExp(`\\{${k}\\}`, "g"), encodeURIComponent(String(v)));
|
|
83
|
+
}
|
|
84
|
+
return out;
|
|
85
|
+
};
|
|
86
|
+
const parts = await prompt.render(prompt.input.parse(args), {
|
|
87
|
+
...ctx.promptCtx(),
|
|
88
|
+
link
|
|
89
|
+
});
|
|
90
|
+
const text = parts.map((p) => p.type === "text" ? p.text : `See resource: ${p.title ?? p.uri}
|
|
91
|
+
URI: ${p.uri}`).join(`
|
|
92
|
+
|
|
93
|
+
`);
|
|
94
|
+
const contents = {
|
|
95
|
+
type: "text",
|
|
96
|
+
text
|
|
97
|
+
};
|
|
98
|
+
return {
|
|
99
|
+
messages: [{ role: "assistant", content: contents }],
|
|
100
|
+
description: prompt.meta.description
|
|
101
|
+
};
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// src/mcp/registerPresentations.ts
|
|
107
|
+
import"@contractspec/lib.contracts-spec/presentations";
|
|
108
|
+
import {
|
|
109
|
+
createDefaultTransformEngine,
|
|
110
|
+
registerBasicValidation,
|
|
111
|
+
registerDefaultReactRenderer
|
|
112
|
+
} from "@contractspec/lib.contracts-spec/presentations/transform-engine";
|
|
113
|
+
import { sanitizeMcpName } from "@contractspec/lib.contracts-spec/jsonschema";
|
|
114
|
+
function isEngineRenderOutput(x) {
|
|
115
|
+
if (!x || typeof x !== "object")
|
|
116
|
+
return false;
|
|
117
|
+
return "body" in x && typeof x.body === "string";
|
|
118
|
+
}
|
|
119
|
+
function registerMcpPresentations(server, ctx) {
|
|
120
|
+
if (!ctx.presentations?.count()) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const engine = registerBasicValidation(registerDefaultReactRenderer(createDefaultTransformEngine()));
|
|
124
|
+
for (const presentationSpec of ctx.presentations.list()) {
|
|
125
|
+
const baseKey = sanitizeMcpName(`presentation_${presentationSpec.meta.key}_v${presentationSpec.meta.version}`);
|
|
126
|
+
const baseUri = `presentation://${presentationSpec.meta.key}/v${presentationSpec.meta.version}`;
|
|
127
|
+
ctx.logger.debug(`Registering presentation ${baseUri} for ${baseKey}`);
|
|
128
|
+
server.registerResource(baseKey, baseUri, {
|
|
129
|
+
title: `${presentationSpec.meta.key} v${presentationSpec.meta.version}`,
|
|
130
|
+
description: presentationSpec.meta.description ?? "Presentation",
|
|
131
|
+
mimeType: "application/json"
|
|
132
|
+
}, async () => {
|
|
133
|
+
const jsonText = JSON.stringify({
|
|
134
|
+
meta: presentationSpec.meta,
|
|
135
|
+
source: presentationSpec.source,
|
|
136
|
+
targets: presentationSpec.targets
|
|
137
|
+
}, null, 2);
|
|
138
|
+
return {
|
|
139
|
+
contents: [
|
|
140
|
+
{
|
|
141
|
+
uri: baseUri,
|
|
142
|
+
mimeType: "application/json",
|
|
143
|
+
text: jsonText
|
|
144
|
+
}
|
|
145
|
+
]
|
|
146
|
+
};
|
|
147
|
+
});
|
|
148
|
+
const variants = [{ ext: ".md", target: "markdown" }];
|
|
149
|
+
for (const v of variants) {
|
|
150
|
+
const key = `${baseKey}${v.ext}`;
|
|
151
|
+
const uri = `${baseUri}${v.ext}`;
|
|
152
|
+
server.registerResource(key, uri, {
|
|
153
|
+
title: `${presentationSpec.meta.key} v${presentationSpec.meta.version} (${v.ext})`,
|
|
154
|
+
description: `${presentationSpec.meta.description ?? "Presentation"} (${v.ext})`
|
|
155
|
+
}, async () => {
|
|
156
|
+
const out = await engine.render(v.target, presentationSpec);
|
|
157
|
+
const mimeType = isEngineRenderOutput(out) && out.mimeType ? out.mimeType : v.target === "markdown" ? "text/markdown" : v.target;
|
|
158
|
+
const text = isEngineRenderOutput(out) && typeof out.body === "string" ? out.body : String(out);
|
|
159
|
+
return { contents: [{ uri, mimeType, text }] };
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// src/mcp/createMcpServer.ts
|
|
166
|
+
function createMcpServer(server, ops, resources, prompts, ctxFactories) {
|
|
167
|
+
ctxFactories.logger.debug("Creating MCP server");
|
|
168
|
+
registerMcpTools(server, ops, { toolCtx: ctxFactories.toolCtx });
|
|
169
|
+
registerMcpResources(server, resources, {
|
|
170
|
+
logger: ctxFactories.logger,
|
|
171
|
+
resourceCtx: ctxFactories.resourceCtx
|
|
172
|
+
});
|
|
173
|
+
registerMcpPresentations(server, {
|
|
174
|
+
logger: ctxFactories.logger,
|
|
175
|
+
presentations: ctxFactories.presentations
|
|
176
|
+
});
|
|
177
|
+
registerMcpPrompts(server, prompts, { promptCtx: ctxFactories.promptCtx });
|
|
178
|
+
return server;
|
|
179
|
+
}
|
|
180
|
+
export {
|
|
181
|
+
createMcpServer
|
|
182
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Logger } from '@contractspec/lib.logger';
|
|
2
|
+
import type { HandlerCtx } from '@contractspec/lib.contracts-spec/types';
|
|
3
|
+
import type { PresentationRegistry } from '@contractspec/lib.contracts-spec/presentations';
|
|
4
|
+
export interface McpCtxFactories {
|
|
5
|
+
logger: Logger;
|
|
6
|
+
toolCtx: () => HandlerCtx;
|
|
7
|
+
promptCtx: () => {
|
|
8
|
+
userId?: string | null;
|
|
9
|
+
orgId?: string | null;
|
|
10
|
+
locale?: string;
|
|
11
|
+
};
|
|
12
|
+
resourceCtx: () => {
|
|
13
|
+
userId?: string | null;
|
|
14
|
+
orgId?: string | null;
|
|
15
|
+
locale?: string;
|
|
16
|
+
};
|
|
17
|
+
presentations?: PresentationRegistry;
|
|
18
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
// @bun
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/mcp/registerPresentations.ts
|
|
3
|
+
import"@contractspec/lib.contracts-spec/presentations";
|
|
4
|
+
import {
|
|
5
|
+
createDefaultTransformEngine,
|
|
6
|
+
registerBasicValidation,
|
|
7
|
+
registerDefaultReactRenderer
|
|
8
|
+
} from "@contractspec/lib.contracts-spec/presentations/transform-engine";
|
|
9
|
+
import { sanitizeMcpName } from "@contractspec/lib.contracts-spec/jsonschema";
|
|
10
|
+
function isEngineRenderOutput(x) {
|
|
11
|
+
if (!x || typeof x !== "object")
|
|
12
|
+
return false;
|
|
13
|
+
return "body" in x && typeof x.body === "string";
|
|
14
|
+
}
|
|
15
|
+
function registerMcpPresentations(server, ctx) {
|
|
16
|
+
if (!ctx.presentations?.count()) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const engine = registerBasicValidation(registerDefaultReactRenderer(createDefaultTransformEngine()));
|
|
20
|
+
for (const presentationSpec of ctx.presentations.list()) {
|
|
21
|
+
const baseKey = sanitizeMcpName(`presentation_${presentationSpec.meta.key}_v${presentationSpec.meta.version}`);
|
|
22
|
+
const baseUri = `presentation://${presentationSpec.meta.key}/v${presentationSpec.meta.version}`;
|
|
23
|
+
ctx.logger.debug(`Registering presentation ${baseUri} for ${baseKey}`);
|
|
24
|
+
server.registerResource(baseKey, baseUri, {
|
|
25
|
+
title: `${presentationSpec.meta.key} v${presentationSpec.meta.version}`,
|
|
26
|
+
description: presentationSpec.meta.description ?? "Presentation",
|
|
27
|
+
mimeType: "application/json"
|
|
28
|
+
}, async () => {
|
|
29
|
+
const jsonText = JSON.stringify({
|
|
30
|
+
meta: presentationSpec.meta,
|
|
31
|
+
source: presentationSpec.source,
|
|
32
|
+
targets: presentationSpec.targets
|
|
33
|
+
}, null, 2);
|
|
34
|
+
return {
|
|
35
|
+
contents: [
|
|
36
|
+
{
|
|
37
|
+
uri: baseUri,
|
|
38
|
+
mimeType: "application/json",
|
|
39
|
+
text: jsonText
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
const variants = [{ ext: ".md", target: "markdown" }];
|
|
45
|
+
for (const v of variants) {
|
|
46
|
+
const key = `${baseKey}${v.ext}`;
|
|
47
|
+
const uri = `${baseUri}${v.ext}`;
|
|
48
|
+
server.registerResource(key, uri, {
|
|
49
|
+
title: `${presentationSpec.meta.key} v${presentationSpec.meta.version} (${v.ext})`,
|
|
50
|
+
description: `${presentationSpec.meta.description ?? "Presentation"} (${v.ext})`
|
|
51
|
+
}, async () => {
|
|
52
|
+
const out = await engine.render(v.target, presentationSpec);
|
|
53
|
+
const mimeType = isEngineRenderOutput(out) && out.mimeType ? out.mimeType : v.target === "markdown" ? "text/markdown" : v.target;
|
|
54
|
+
const text = isEngineRenderOutput(out) && typeof out.body === "string" ? out.body : String(out);
|
|
55
|
+
return { contents: [{ uri, mimeType, text }] };
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export {
|
|
61
|
+
registerMcpPresentations
|
|
62
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import type { PromptRegistry } from '@contractspec/lib.contracts-spec/promptRegistry';
|
|
3
|
+
import type { McpCtxFactories } from './mcpTypes';
|
|
4
|
+
export declare function registerMcpPrompts(server: McpServer, prompts: PromptRegistry, ctx: Pick<McpCtxFactories, 'promptCtx'>): void;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/mcp/registerPrompts.ts
|
|
3
|
+
import { defaultMcpPrompt } from "@contractspec/lib.contracts-spec/jsonschema";
|
|
4
|
+
function promptArgsSchemaFromPromptArgs(args) {
|
|
5
|
+
const shape = {};
|
|
6
|
+
for (const a of args)
|
|
7
|
+
shape[a.name] = a.schema;
|
|
8
|
+
return shape;
|
|
9
|
+
}
|
|
10
|
+
function registerMcpPrompts(server, prompts, ctx) {
|
|
11
|
+
for (const prompt of prompts.list()) {
|
|
12
|
+
const promptName = defaultMcpPrompt(prompt.meta.key);
|
|
13
|
+
server.registerPrompt(promptName, {
|
|
14
|
+
title: prompt.meta.title,
|
|
15
|
+
description: prompt.meta.description,
|
|
16
|
+
argsSchema: promptArgsSchemaFromPromptArgs(prompt.args)
|
|
17
|
+
}, async (args) => {
|
|
18
|
+
const link = (tpl, vars) => {
|
|
19
|
+
let out = tpl;
|
|
20
|
+
for (const [k, v] of Object.entries(vars)) {
|
|
21
|
+
out = out.replace(new RegExp(`\\{${k}\\}`, "g"), encodeURIComponent(String(v)));
|
|
22
|
+
}
|
|
23
|
+
return out;
|
|
24
|
+
};
|
|
25
|
+
const parts = await prompt.render(prompt.input.parse(args), {
|
|
26
|
+
...ctx.promptCtx(),
|
|
27
|
+
link
|
|
28
|
+
});
|
|
29
|
+
const text = parts.map((p) => p.type === "text" ? p.text : `See resource: ${p.title ?? p.uri}
|
|
30
|
+
URI: ${p.uri}`).join(`
|
|
31
|
+
|
|
32
|
+
`);
|
|
33
|
+
const contents = {
|
|
34
|
+
type: "text",
|
|
35
|
+
text
|
|
36
|
+
};
|
|
37
|
+
return {
|
|
38
|
+
messages: [{ role: "assistant", content: contents }],
|
|
39
|
+
description: prompt.meta.description
|
|
40
|
+
};
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export {
|
|
45
|
+
registerMcpPrompts
|
|
46
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import type { ResourceRegistry } from '@contractspec/lib.contracts-spec/resources';
|
|
3
|
+
import type { McpCtxFactories } from './mcpTypes';
|
|
4
|
+
export declare function registerMcpResources(server: McpServer, resources: ResourceRegistry, ctx: Pick<McpCtxFactories, 'logger' | 'resourceCtx'>): void;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/mcp/registerResources.ts
|
|
3
|
+
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import { Buffer } from "buffer";
|
|
5
|
+
function mcpResourceMeta(resource) {
|
|
6
|
+
return {
|
|
7
|
+
title: resource.meta.title,
|
|
8
|
+
description: resource.meta.description,
|
|
9
|
+
mimeType: resource.meta.mimeType,
|
|
10
|
+
_meta: { tags: resource.meta.tags ?? [] }
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
function registerMcpResources(server, resources, ctx) {
|
|
14
|
+
for (const resource of resources.listTemplates()) {
|
|
15
|
+
ctx.logger.debug("Registering resource: " + resource.meta.uriTemplate);
|
|
16
|
+
server.registerResource(resource.meta.uriTemplate, new ResourceTemplate(resource.meta.uriTemplate, { list: undefined }), mcpResourceMeta(resource), async (_uri, variables) => {
|
|
17
|
+
const parsedArgs = resource.input.parse(variables);
|
|
18
|
+
const out = await resource.resolve(parsedArgs, ctx.resourceCtx());
|
|
19
|
+
if (typeof out.data === "string") {
|
|
20
|
+
return {
|
|
21
|
+
contents: [
|
|
22
|
+
{
|
|
23
|
+
uri: out.uri,
|
|
24
|
+
mimeType: out.mimeType ?? resource.meta.mimeType,
|
|
25
|
+
text: out.data
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
contents: [
|
|
32
|
+
{
|
|
33
|
+
uri: out.uri,
|
|
34
|
+
mimeType: out.mimeType ?? resource.meta.mimeType,
|
|
35
|
+
blob: Buffer.from(out.data).toString("base64")
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
ctx.logger.debug("Registered resource: " + resource.meta.uriTemplate);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export {
|
|
44
|
+
registerMcpResources
|
|
45
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import type { OperationSpecRegistry } from '@contractspec/lib.contracts-spec/operations/registry';
|
|
3
|
+
import type { McpCtxFactories } from './mcpTypes';
|
|
4
|
+
export declare function registerMcpTools(server: McpServer, ops: OperationSpecRegistry, ctx: Pick<McpCtxFactories, 'toolCtx'>): void;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/mcp/registerTools.ts
|
|
3
|
+
import { defaultMcpTool } from "@contractspec/lib.contracts-spec/jsonschema";
|
|
4
|
+
function registerMcpTools(server, ops, ctx) {
|
|
5
|
+
for (const spec of ops.list()) {
|
|
6
|
+
if (spec.meta.kind !== "command")
|
|
7
|
+
continue;
|
|
8
|
+
const toolName = spec.transport?.mcp?.toolName ?? defaultMcpTool(spec.meta.key, spec.meta.version);
|
|
9
|
+
server.registerTool(toolName, {
|
|
10
|
+
description: spec.meta.description,
|
|
11
|
+
inputSchema: spec.io.input?.getZod()
|
|
12
|
+
}, async (args) => {
|
|
13
|
+
const result = await ops.execute(spec.meta.key, spec.meta.version, args ?? {}, ctx.toolCtx());
|
|
14
|
+
return {
|
|
15
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 4) }]
|
|
16
|
+
};
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export {
|
|
21
|
+
registerMcpTools
|
|
22
|
+
};
|