@contractspec/bundle.library 3.0.0 → 3.2.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/.turbo/turbo-build.log +178 -166
- package/AGENTS.md +19 -12
- package/CHANGELOG.md +74 -0
- package/dist/application/context-storage/index.d.ts +18 -0
- package/dist/application/context-storage/index.js +29 -0
- package/dist/application/index.d.ts +1 -0
- package/dist/application/index.js +662 -2
- package/dist/application/mcp/cliMcp.js +12 -2
- package/dist/application/mcp/common.d.ts +11 -1
- package/dist/application/mcp/common.js +12 -2
- package/dist/application/mcp/contractsMcp.d.ts +51 -0
- package/dist/application/mcp/contractsMcp.js +531 -0
- package/dist/application/mcp/contractsMcpResources.d.ts +7 -0
- package/dist/application/mcp/contractsMcpResources.js +124 -0
- package/dist/application/mcp/contractsMcpTools.d.ts +9 -0
- package/dist/application/mcp/contractsMcpTools.js +200 -0
- package/dist/application/mcp/contractsMcpTypes.d.ts +50 -0
- package/dist/application/mcp/contractsMcpTypes.js +1 -0
- package/dist/application/mcp/docsMcp.js +12 -2
- package/dist/application/mcp/index.d.ts +2 -0
- package/dist/application/mcp/index.js +635 -2
- package/dist/application/mcp/internalMcp.js +12 -2
- package/dist/application/mcp/providerRankingMcp.d.ts +46 -0
- package/dist/application/mcp/providerRankingMcp.js +494 -0
- package/dist/node/application/context-storage/index.js +28 -0
- package/dist/node/application/index.js +662 -2
- package/dist/node/application/mcp/cliMcp.js +12 -2
- package/dist/node/application/mcp/common.js +12 -2
- package/dist/node/application/mcp/contractsMcp.js +530 -0
- package/dist/node/application/mcp/contractsMcpResources.js +123 -0
- package/dist/node/application/mcp/contractsMcpTools.js +199 -0
- package/dist/node/application/mcp/contractsMcpTypes.js +0 -0
- package/dist/node/application/mcp/docsMcp.js +12 -2
- package/dist/node/application/mcp/index.js +635 -2
- package/dist/node/application/mcp/internalMcp.js +12 -2
- package/dist/node/application/mcp/providerRankingMcp.js +493 -0
- package/package.json +113 -25
- package/src/application/context-storage/index.ts +58 -0
- package/src/application/index.ts +1 -0
- package/src/application/mcp/common.ts +28 -1
- package/src/application/mcp/contractsMcp.ts +34 -0
- package/src/application/mcp/contractsMcpResources.ts +142 -0
- package/src/application/mcp/contractsMcpTools.ts +246 -0
- package/src/application/mcp/contractsMcpTypes.ts +47 -0
- package/src/application/mcp/index.ts +2 -0
- package/src/application/mcp/providerRankingMcp.ts +380 -0
- package/src/components/docs/generated/docs-index._common.json +879 -1
- package/src/components/docs/generated/docs-index.manifest.json +5 -5
- package/src/components/docs/generated/docs-index.metrics.json +8 -0
- package/src/components/docs/generated/docs-index.platform-integrations.json +8 -0
|
@@ -73,9 +73,13 @@ function createMcpElysiaHandler({
|
|
|
73
73
|
ops,
|
|
74
74
|
resources,
|
|
75
75
|
prompts,
|
|
76
|
-
presentations
|
|
76
|
+
presentations,
|
|
77
|
+
validateAuth,
|
|
78
|
+
requiredAuthMethods
|
|
77
79
|
}) {
|
|
78
|
-
logger.info("Setting up MCP handler..."
|
|
80
|
+
logger.info("Setting up MCP handler...", {
|
|
81
|
+
requiredAuthMethods: requiredAuthMethods ?? []
|
|
82
|
+
});
|
|
79
83
|
const isStateful = process.env.CONTRACTSPEC_MCP_STATEFUL === "1";
|
|
80
84
|
const sessions = new Map;
|
|
81
85
|
async function handleStateless(request) {
|
|
@@ -146,6 +150,12 @@ function createMcpElysiaHandler({
|
|
|
146
150
|
}
|
|
147
151
|
return new Elysia({ name: `mcp-${serverName}` }).all(path, async ({ request }) => {
|
|
148
152
|
try {
|
|
153
|
+
if (validateAuth) {
|
|
154
|
+
const authResult = await validateAuth(request);
|
|
155
|
+
if (!authResult.valid) {
|
|
156
|
+
return createJsonRpcErrorResponse(401, -32002, "Authentication failed", authResult.reason);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
149
159
|
if (isStateful) {
|
|
150
160
|
return await handleStateful(request);
|
|
151
161
|
}
|
|
@@ -2,6 +2,12 @@ import type { OperationSpecRegistry, PromptRegistry, ResourceRegistry } from '@c
|
|
|
2
2
|
import type { PresentationSpec } from '@contractspec/lib.contracts-spec/presentations';
|
|
3
3
|
import { Elysia } from 'elysia';
|
|
4
4
|
import { Logger } from '@contractspec/lib.logger';
|
|
5
|
+
import type { IntegrationAuthType } from '@contractspec/lib.contracts-integrations/integrations';
|
|
6
|
+
export interface McpAuthValidationResult {
|
|
7
|
+
valid: boolean;
|
|
8
|
+
actor?: string;
|
|
9
|
+
reason?: string;
|
|
10
|
+
}
|
|
5
11
|
interface McpHttpHandlerConfig {
|
|
6
12
|
path: string;
|
|
7
13
|
serverName: string;
|
|
@@ -10,8 +16,12 @@ interface McpHttpHandlerConfig {
|
|
|
10
16
|
prompts: PromptRegistry;
|
|
11
17
|
presentations?: PresentationSpec[];
|
|
12
18
|
logger: Logger;
|
|
19
|
+
/** Callback to validate auth credentials from the incoming request. */
|
|
20
|
+
validateAuth?: (request: Request) => Promise<McpAuthValidationResult>;
|
|
21
|
+
/** Auth methods this MCP handler requires callers to present. */
|
|
22
|
+
requiredAuthMethods?: IntegrationAuthType[];
|
|
13
23
|
}
|
|
14
|
-
export declare function createMcpElysiaHandler({ logger, path, serverName, ops, resources, prompts, presentations, }: McpHttpHandlerConfig): Elysia<"", {
|
|
24
|
+
export declare function createMcpElysiaHandler({ logger, path, serverName, ops, resources, prompts, presentations, validateAuth, requiredAuthMethods, }: McpHttpHandlerConfig): Elysia<"", {
|
|
15
25
|
decorator: {};
|
|
16
26
|
store: {};
|
|
17
27
|
derive: {};
|
|
@@ -73,9 +73,13 @@ function createMcpElysiaHandler({
|
|
|
73
73
|
ops,
|
|
74
74
|
resources,
|
|
75
75
|
prompts,
|
|
76
|
-
presentations
|
|
76
|
+
presentations,
|
|
77
|
+
validateAuth,
|
|
78
|
+
requiredAuthMethods
|
|
77
79
|
}) {
|
|
78
|
-
logger.info("Setting up MCP handler..."
|
|
80
|
+
logger.info("Setting up MCP handler...", {
|
|
81
|
+
requiredAuthMethods: requiredAuthMethods ?? []
|
|
82
|
+
});
|
|
79
83
|
const isStateful = process.env.CONTRACTSPEC_MCP_STATEFUL === "1";
|
|
80
84
|
const sessions = new Map;
|
|
81
85
|
async function handleStateless(request) {
|
|
@@ -146,6 +150,12 @@ function createMcpElysiaHandler({
|
|
|
146
150
|
}
|
|
147
151
|
return new Elysia({ name: `mcp-${serverName}` }).all(path, async ({ request }) => {
|
|
148
152
|
try {
|
|
153
|
+
if (validateAuth) {
|
|
154
|
+
const authResult = await validateAuth(request);
|
|
155
|
+
if (!authResult.valid) {
|
|
156
|
+
return createJsonRpcErrorResponse(401, -32002, "Authentication failed", authResult.reason);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
149
159
|
if (isStateful) {
|
|
150
160
|
return await handleStateful(request);
|
|
151
161
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contracts MCP server.
|
|
3
|
+
*
|
|
4
|
+
* Exposes contract management operations (list, get, create, update,
|
|
5
|
+
* delete, validate, build) as MCP tools, resources, and prompts.
|
|
6
|
+
*
|
|
7
|
+
* Services are injected at creation time so this bundle stays
|
|
8
|
+
* decoupled from bundle.workspace — the app layer provides real impls.
|
|
9
|
+
*/
|
|
10
|
+
import type { ContractsMcpServices } from './contractsMcpTypes';
|
|
11
|
+
export type { ContractsMcpServices, ContractInfo } from './contractsMcpTypes';
|
|
12
|
+
export declare function createContractsMcpHandler(path: string | undefined, services: ContractsMcpServices): import("elysia").default<"", {
|
|
13
|
+
decorator: {};
|
|
14
|
+
store: {};
|
|
15
|
+
derive: {};
|
|
16
|
+
resolve: {};
|
|
17
|
+
}, {
|
|
18
|
+
typebox: {};
|
|
19
|
+
error: {};
|
|
20
|
+
}, {
|
|
21
|
+
schema: {};
|
|
22
|
+
standaloneSchema: {};
|
|
23
|
+
macro: {};
|
|
24
|
+
macroFn: {};
|
|
25
|
+
parser: {};
|
|
26
|
+
response: {};
|
|
27
|
+
}, {
|
|
28
|
+
[x: string]: {
|
|
29
|
+
[x: string]: {
|
|
30
|
+
body: unknown;
|
|
31
|
+
params: {};
|
|
32
|
+
query: unknown;
|
|
33
|
+
headers: unknown;
|
|
34
|
+
response: {
|
|
35
|
+
200: Response;
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
}, {
|
|
40
|
+
derive: {};
|
|
41
|
+
resolve: {};
|
|
42
|
+
schema: {};
|
|
43
|
+
standaloneSchema: {};
|
|
44
|
+
response: {};
|
|
45
|
+
}, {
|
|
46
|
+
derive: {};
|
|
47
|
+
resolve: {};
|
|
48
|
+
schema: {};
|
|
49
|
+
standaloneSchema: {};
|
|
50
|
+
response: {};
|
|
51
|
+
}>;
|
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/application/mcp/common.ts
|
|
3
|
+
import { PresentationRegistry } from "@contractspec/lib.contracts-spec/presentations";
|
|
4
|
+
import { createMcpServer } from "@contractspec/lib.contracts-runtime-server-mcp/provider-mcp";
|
|
5
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
6
|
+
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
7
|
+
import { Elysia } from "elysia";
|
|
8
|
+
import { randomUUID } from "crypto";
|
|
9
|
+
var baseCtx = {
|
|
10
|
+
actor: "anonymous",
|
|
11
|
+
decide: async () => ({ effect: "allow" })
|
|
12
|
+
};
|
|
13
|
+
function createJsonRpcErrorResponse(status, code, message, data) {
|
|
14
|
+
return new Response(JSON.stringify({
|
|
15
|
+
jsonrpc: "2.0",
|
|
16
|
+
error: {
|
|
17
|
+
code,
|
|
18
|
+
message,
|
|
19
|
+
...data ? { data } : {}
|
|
20
|
+
},
|
|
21
|
+
id: null
|
|
22
|
+
}), {
|
|
23
|
+
status,
|
|
24
|
+
headers: {
|
|
25
|
+
"content-type": "application/json"
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
function createSessionState({
|
|
30
|
+
logger,
|
|
31
|
+
serverName,
|
|
32
|
+
ops,
|
|
33
|
+
resources,
|
|
34
|
+
prompts,
|
|
35
|
+
presentations,
|
|
36
|
+
stateful
|
|
37
|
+
}) {
|
|
38
|
+
const server = new McpServer({
|
|
39
|
+
name: serverName,
|
|
40
|
+
version: "1.0.0"
|
|
41
|
+
}, {
|
|
42
|
+
capabilities: {
|
|
43
|
+
tools: {},
|
|
44
|
+
resources: {},
|
|
45
|
+
prompts: {},
|
|
46
|
+
logging: {}
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
logger.info("Setting up MCP server...");
|
|
50
|
+
createMcpServer(server, ops, resources, prompts, {
|
|
51
|
+
logger,
|
|
52
|
+
toolCtx: () => baseCtx,
|
|
53
|
+
promptCtx: () => ({ locale: "en" }),
|
|
54
|
+
resourceCtx: () => ({ locale: "en" }),
|
|
55
|
+
presentations: new PresentationRegistry(presentations)
|
|
56
|
+
});
|
|
57
|
+
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
58
|
+
sessionIdGenerator: stateful ? () => randomUUID() : undefined,
|
|
59
|
+
enableJsonResponse: true
|
|
60
|
+
});
|
|
61
|
+
return server.connect(transport).then(() => ({ server, transport }));
|
|
62
|
+
}
|
|
63
|
+
async function closeSessionState(state) {
|
|
64
|
+
await Promise.allSettled([state.transport.close(), state.server.close()]);
|
|
65
|
+
}
|
|
66
|
+
function toErrorMessage(error) {
|
|
67
|
+
return error instanceof Error ? error.stack ?? error.message : String(error);
|
|
68
|
+
}
|
|
69
|
+
function createMcpElysiaHandler({
|
|
70
|
+
logger,
|
|
71
|
+
path,
|
|
72
|
+
serverName,
|
|
73
|
+
ops,
|
|
74
|
+
resources,
|
|
75
|
+
prompts,
|
|
76
|
+
presentations,
|
|
77
|
+
validateAuth,
|
|
78
|
+
requiredAuthMethods
|
|
79
|
+
}) {
|
|
80
|
+
logger.info("Setting up MCP handler...", {
|
|
81
|
+
requiredAuthMethods: requiredAuthMethods ?? []
|
|
82
|
+
});
|
|
83
|
+
const isStateful = process.env.CONTRACTSPEC_MCP_STATEFUL === "1";
|
|
84
|
+
const sessions = new Map;
|
|
85
|
+
async function handleStateless(request) {
|
|
86
|
+
const state = await createSessionState({
|
|
87
|
+
logger,
|
|
88
|
+
path,
|
|
89
|
+
serverName,
|
|
90
|
+
ops,
|
|
91
|
+
resources,
|
|
92
|
+
prompts,
|
|
93
|
+
presentations,
|
|
94
|
+
stateful: false
|
|
95
|
+
});
|
|
96
|
+
try {
|
|
97
|
+
return await state.transport.handleRequest(request);
|
|
98
|
+
} finally {
|
|
99
|
+
await closeSessionState(state);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async function closeSession(sessionId) {
|
|
103
|
+
const state = sessions.get(sessionId);
|
|
104
|
+
if (!state)
|
|
105
|
+
return;
|
|
106
|
+
sessions.delete(sessionId);
|
|
107
|
+
await closeSessionState(state);
|
|
108
|
+
}
|
|
109
|
+
async function handleStateful(request) {
|
|
110
|
+
const requestedSessionId = request.headers.get("mcp-session-id");
|
|
111
|
+
let state;
|
|
112
|
+
let createdState = false;
|
|
113
|
+
if (requestedSessionId) {
|
|
114
|
+
const existing = sessions.get(requestedSessionId);
|
|
115
|
+
if (!existing) {
|
|
116
|
+
return createJsonRpcErrorResponse(404, -32001, "Session not found");
|
|
117
|
+
}
|
|
118
|
+
state = existing;
|
|
119
|
+
} else {
|
|
120
|
+
state = await createSessionState({
|
|
121
|
+
logger,
|
|
122
|
+
path,
|
|
123
|
+
serverName,
|
|
124
|
+
ops,
|
|
125
|
+
resources,
|
|
126
|
+
prompts,
|
|
127
|
+
presentations,
|
|
128
|
+
stateful: true
|
|
129
|
+
});
|
|
130
|
+
createdState = true;
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
const response = await state.transport.handleRequest(request);
|
|
134
|
+
const activeSessionId = state.transport.sessionId;
|
|
135
|
+
if (activeSessionId && !sessions.has(activeSessionId)) {
|
|
136
|
+
sessions.set(activeSessionId, state);
|
|
137
|
+
}
|
|
138
|
+
if (request.method === "DELETE" && activeSessionId) {
|
|
139
|
+
await closeSession(activeSessionId);
|
|
140
|
+
} else if (!activeSessionId && createdState) {
|
|
141
|
+
await closeSessionState(state);
|
|
142
|
+
}
|
|
143
|
+
return response;
|
|
144
|
+
} catch (error) {
|
|
145
|
+
if (createdState) {
|
|
146
|
+
await closeSessionState(state);
|
|
147
|
+
}
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return new Elysia({ name: `mcp-${serverName}` }).all(path, async ({ request }) => {
|
|
152
|
+
try {
|
|
153
|
+
if (validateAuth) {
|
|
154
|
+
const authResult = await validateAuth(request);
|
|
155
|
+
if (!authResult.valid) {
|
|
156
|
+
return createJsonRpcErrorResponse(401, -32002, "Authentication failed", authResult.reason);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (isStateful) {
|
|
160
|
+
return await handleStateful(request);
|
|
161
|
+
}
|
|
162
|
+
return await handleStateless(request);
|
|
163
|
+
} catch (error) {
|
|
164
|
+
logger.error("Error handling MCP request", {
|
|
165
|
+
path,
|
|
166
|
+
method: request.method,
|
|
167
|
+
error: toErrorMessage(error)
|
|
168
|
+
});
|
|
169
|
+
return createJsonRpcErrorResponse(500, -32000, "Internal error");
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// src/infrastructure/elysia/logger.ts
|
|
175
|
+
import { Logger, LogLevel } from "@contractspec/lib.logger";
|
|
176
|
+
var createAppLogger = () => new Logger({
|
|
177
|
+
level: LogLevel.DEBUG,
|
|
178
|
+
environment: "development",
|
|
179
|
+
enableTracing: true,
|
|
180
|
+
enableTiming: true,
|
|
181
|
+
enableContext: true,
|
|
182
|
+
enableColors: true
|
|
183
|
+
});
|
|
184
|
+
var appLogger = createAppLogger();
|
|
185
|
+
var dbLogger = new Logger({
|
|
186
|
+
level: LogLevel.DEBUG,
|
|
187
|
+
environment: "development",
|
|
188
|
+
enableTracing: true,
|
|
189
|
+
enableTiming: true,
|
|
190
|
+
enableContext: true,
|
|
191
|
+
enableColors: true
|
|
192
|
+
});
|
|
193
|
+
var authLogger = new Logger({
|
|
194
|
+
level: LogLevel.INFO,
|
|
195
|
+
environment: "development",
|
|
196
|
+
enableTracing: true,
|
|
197
|
+
enableTiming: true,
|
|
198
|
+
enableContext: true,
|
|
199
|
+
enableColors: true
|
|
200
|
+
});
|
|
201
|
+
// src/application/mcp/contractsMcpTools.ts
|
|
202
|
+
import {
|
|
203
|
+
defineCommand,
|
|
204
|
+
installOp,
|
|
205
|
+
OperationSpecRegistry
|
|
206
|
+
} from "@contractspec/lib.contracts-spec";
|
|
207
|
+
import { defineSchemaModel, ScalarTypeEnum } from "@contractspec/lib.schema";
|
|
208
|
+
var OWNERS = ["@contractspec"];
|
|
209
|
+
var TAGS = ["contracts", "mcp"];
|
|
210
|
+
function buildContractsOps(services) {
|
|
211
|
+
const registry = new OperationSpecRegistry;
|
|
212
|
+
const ListInput = defineSchemaModel({
|
|
213
|
+
name: "ContractsListInput",
|
|
214
|
+
fields: {
|
|
215
|
+
pattern: { type: ScalarTypeEnum.String_unsecure(), isOptional: true },
|
|
216
|
+
type: { type: ScalarTypeEnum.String_unsecure(), isOptional: true }
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
const ListOutput = defineSchemaModel({
|
|
220
|
+
name: "ContractsListOutput",
|
|
221
|
+
fields: {
|
|
222
|
+
specs: { type: ScalarTypeEnum.JSON(), isOptional: false },
|
|
223
|
+
total: { type: ScalarTypeEnum.Int_unsecure(), isOptional: false }
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
installOp(registry, defineCommand({
|
|
227
|
+
meta: {
|
|
228
|
+
key: "contracts.list",
|
|
229
|
+
version: "1.0.0",
|
|
230
|
+
stability: "beta",
|
|
231
|
+
owners: OWNERS,
|
|
232
|
+
tags: TAGS,
|
|
233
|
+
description: "List contract specs in the workspace.",
|
|
234
|
+
goal: "Discover available contracts by type, pattern, or owner.",
|
|
235
|
+
context: "Contracts MCP server."
|
|
236
|
+
},
|
|
237
|
+
io: { input: ListInput, output: ListOutput },
|
|
238
|
+
policy: { auth: "anonymous" }
|
|
239
|
+
}), async ({ pattern, type }) => {
|
|
240
|
+
const specs = await services.listSpecs({ pattern, type });
|
|
241
|
+
return { specs, total: specs.length };
|
|
242
|
+
});
|
|
243
|
+
const GetInput = defineSchemaModel({
|
|
244
|
+
name: "ContractsGetInput",
|
|
245
|
+
fields: {
|
|
246
|
+
path: { type: ScalarTypeEnum.String_unsecure(), isOptional: false }
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
const GetOutput = defineSchemaModel({
|
|
250
|
+
name: "ContractsGetOutput",
|
|
251
|
+
fields: {
|
|
252
|
+
content: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
253
|
+
info: { type: ScalarTypeEnum.JSON(), isOptional: false }
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
installOp(registry, defineCommand({
|
|
257
|
+
meta: {
|
|
258
|
+
key: "contracts.get",
|
|
259
|
+
version: "1.0.0",
|
|
260
|
+
stability: "beta",
|
|
261
|
+
owners: OWNERS,
|
|
262
|
+
tags: TAGS,
|
|
263
|
+
description: "Read a single contract spec file.",
|
|
264
|
+
goal: "Fetch spec content and parsed metadata.",
|
|
265
|
+
context: "Contracts MCP server."
|
|
266
|
+
},
|
|
267
|
+
io: { input: GetInput, output: GetOutput },
|
|
268
|
+
policy: { auth: "anonymous" }
|
|
269
|
+
}), async ({ path }) => {
|
|
270
|
+
const result = await services.getSpec(path);
|
|
271
|
+
if (!result)
|
|
272
|
+
throw new Error(`Spec not found: ${path}`);
|
|
273
|
+
return result;
|
|
274
|
+
});
|
|
275
|
+
const ValidateInput = defineSchemaModel({
|
|
276
|
+
name: "ContractsValidateInput",
|
|
277
|
+
fields: {
|
|
278
|
+
path: { type: ScalarTypeEnum.String_unsecure(), isOptional: false }
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
const ValidateOutput = defineSchemaModel({
|
|
282
|
+
name: "ContractsValidateOutput",
|
|
283
|
+
fields: {
|
|
284
|
+
valid: { type: ScalarTypeEnum.Boolean(), isOptional: false },
|
|
285
|
+
errors: { type: ScalarTypeEnum.JSON(), isOptional: false },
|
|
286
|
+
warnings: { type: ScalarTypeEnum.JSON(), isOptional: false }
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
installOp(registry, defineCommand({
|
|
290
|
+
meta: {
|
|
291
|
+
key: "contracts.validate",
|
|
292
|
+
version: "1.0.0",
|
|
293
|
+
stability: "beta",
|
|
294
|
+
owners: OWNERS,
|
|
295
|
+
tags: TAGS,
|
|
296
|
+
description: "Validate a contract spec structure.",
|
|
297
|
+
goal: "Check spec for structural or policy issues.",
|
|
298
|
+
context: "Contracts MCP server."
|
|
299
|
+
},
|
|
300
|
+
io: { input: ValidateInput, output: ValidateOutput },
|
|
301
|
+
policy: { auth: "anonymous" }
|
|
302
|
+
}), async ({ path }) => services.validateSpec(path));
|
|
303
|
+
const BuildInput = defineSchemaModel({
|
|
304
|
+
name: "ContractsBuildInput",
|
|
305
|
+
fields: {
|
|
306
|
+
path: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
307
|
+
dryRun: { type: ScalarTypeEnum.Boolean(), isOptional: true }
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
const BuildOutput = defineSchemaModel({
|
|
311
|
+
name: "ContractsBuildOutput",
|
|
312
|
+
fields: {
|
|
313
|
+
results: { type: ScalarTypeEnum.JSON(), isOptional: false }
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
installOp(registry, defineCommand({
|
|
317
|
+
meta: {
|
|
318
|
+
key: "contracts.build",
|
|
319
|
+
version: "1.0.0",
|
|
320
|
+
stability: "beta",
|
|
321
|
+
owners: OWNERS,
|
|
322
|
+
tags: TAGS,
|
|
323
|
+
description: "Generate implementation code from a contract spec.",
|
|
324
|
+
goal: "Produce handler, component, or test skeletons.",
|
|
325
|
+
context: "Contracts MCP server."
|
|
326
|
+
},
|
|
327
|
+
io: { input: BuildInput, output: BuildOutput },
|
|
328
|
+
policy: { auth: "user" }
|
|
329
|
+
}), async ({ path, dryRun }) => services.buildSpec(path, { dryRun }));
|
|
330
|
+
registerMutationTools(registry, services);
|
|
331
|
+
return registry;
|
|
332
|
+
}
|
|
333
|
+
function registerMutationTools(registry, services) {
|
|
334
|
+
const UpdateInput = defineSchemaModel({
|
|
335
|
+
name: "ContractsUpdateInput",
|
|
336
|
+
fields: {
|
|
337
|
+
path: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
338
|
+
content: { type: ScalarTypeEnum.String_unsecure(), isOptional: true },
|
|
339
|
+
fields: { type: ScalarTypeEnum.JSON(), isOptional: true }
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
const UpdateOutput = defineSchemaModel({
|
|
343
|
+
name: "ContractsUpdateOutput",
|
|
344
|
+
fields: {
|
|
345
|
+
updated: { type: ScalarTypeEnum.Boolean(), isOptional: false },
|
|
346
|
+
errors: { type: ScalarTypeEnum.JSON(), isOptional: false },
|
|
347
|
+
warnings: { type: ScalarTypeEnum.JSON(), isOptional: false }
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
installOp(registry, defineCommand({
|
|
351
|
+
meta: {
|
|
352
|
+
key: "contracts.update",
|
|
353
|
+
version: "1.0.0",
|
|
354
|
+
stability: "beta",
|
|
355
|
+
owners: OWNERS,
|
|
356
|
+
tags: TAGS,
|
|
357
|
+
description: "Update an existing contract spec.",
|
|
358
|
+
goal: "Modify spec content or individual fields with validation.",
|
|
359
|
+
context: "Contracts MCP server."
|
|
360
|
+
},
|
|
361
|
+
io: { input: UpdateInput, output: UpdateOutput },
|
|
362
|
+
policy: { auth: "user" }
|
|
363
|
+
}), async ({ path, content, fields }) => services.updateSpec(path, {
|
|
364
|
+
content,
|
|
365
|
+
fields: Array.isArray(fields) ? fields : undefined
|
|
366
|
+
}));
|
|
367
|
+
const DeleteInput = defineSchemaModel({
|
|
368
|
+
name: "ContractsDeleteInput",
|
|
369
|
+
fields: {
|
|
370
|
+
path: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
371
|
+
clean: { type: ScalarTypeEnum.Boolean(), isOptional: true }
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
const DeleteOutput = defineSchemaModel({
|
|
375
|
+
name: "ContractsDeleteOutput",
|
|
376
|
+
fields: {
|
|
377
|
+
deleted: { type: ScalarTypeEnum.Boolean(), isOptional: false },
|
|
378
|
+
cleanedFiles: { type: ScalarTypeEnum.JSON(), isOptional: false },
|
|
379
|
+
errors: { type: ScalarTypeEnum.JSON(), isOptional: false }
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
installOp(registry, defineCommand({
|
|
383
|
+
meta: {
|
|
384
|
+
key: "contracts.delete",
|
|
385
|
+
version: "1.0.0",
|
|
386
|
+
stability: "beta",
|
|
387
|
+
owners: OWNERS,
|
|
388
|
+
tags: TAGS,
|
|
389
|
+
description: "Delete a contract spec and optionally its artifacts.",
|
|
390
|
+
goal: "Remove a spec file and clean generated handlers/tests.",
|
|
391
|
+
context: "Contracts MCP server."
|
|
392
|
+
},
|
|
393
|
+
io: { input: DeleteInput, output: DeleteOutput },
|
|
394
|
+
policy: { auth: "user" }
|
|
395
|
+
}), async ({ path, clean }) => services.deleteSpec(path, { clean }));
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// src/application/mcp/contractsMcpResources.ts
|
|
399
|
+
import {
|
|
400
|
+
definePrompt,
|
|
401
|
+
defineResourceTemplate,
|
|
402
|
+
PromptRegistry,
|
|
403
|
+
ResourceRegistry
|
|
404
|
+
} from "@contractspec/lib.contracts-spec";
|
|
405
|
+
import z from "zod";
|
|
406
|
+
var OWNERS2 = ["@contractspec"];
|
|
407
|
+
var TAGS2 = ["contracts", "mcp"];
|
|
408
|
+
function buildContractsResources(services) {
|
|
409
|
+
const resources = new ResourceRegistry;
|
|
410
|
+
resources.register(defineResourceTemplate({
|
|
411
|
+
meta: {
|
|
412
|
+
uriTemplate: "contracts://list",
|
|
413
|
+
title: "Contract specs list",
|
|
414
|
+
description: "JSON list of all contract specs in the workspace.",
|
|
415
|
+
mimeType: "application/json",
|
|
416
|
+
tags: TAGS2
|
|
417
|
+
},
|
|
418
|
+
input: z.object({}),
|
|
419
|
+
resolve: async () => {
|
|
420
|
+
const specs = await services.listSpecs();
|
|
421
|
+
return {
|
|
422
|
+
uri: "contracts://list",
|
|
423
|
+
mimeType: "application/json",
|
|
424
|
+
data: JSON.stringify(specs, null, 2)
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
}));
|
|
428
|
+
resources.register(defineResourceTemplate({
|
|
429
|
+
meta: {
|
|
430
|
+
uriTemplate: "contracts://spec/{path}",
|
|
431
|
+
title: "Contract spec content",
|
|
432
|
+
description: "Read a single contract spec file by path.",
|
|
433
|
+
mimeType: "text/plain",
|
|
434
|
+
tags: TAGS2
|
|
435
|
+
},
|
|
436
|
+
input: z.object({ path: z.string() }),
|
|
437
|
+
resolve: async ({ path }) => {
|
|
438
|
+
const result = await services.getSpec(path);
|
|
439
|
+
if (!result) {
|
|
440
|
+
return {
|
|
441
|
+
uri: `contracts://spec/${encodeURIComponent(path)}`,
|
|
442
|
+
mimeType: "text/plain",
|
|
443
|
+
data: `Spec not found: ${path}`
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
return {
|
|
447
|
+
uri: `contracts://spec/${encodeURIComponent(path)}`,
|
|
448
|
+
mimeType: "text/plain",
|
|
449
|
+
data: result.content
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
}));
|
|
453
|
+
resources.register(defineResourceTemplate({
|
|
454
|
+
meta: {
|
|
455
|
+
uriTemplate: "contracts://registry/manifest",
|
|
456
|
+
title: "Remote registry manifest",
|
|
457
|
+
description: "Contract registry manifest from the remote server.",
|
|
458
|
+
mimeType: "application/json",
|
|
459
|
+
tags: TAGS2
|
|
460
|
+
},
|
|
461
|
+
input: z.object({}),
|
|
462
|
+
resolve: async () => {
|
|
463
|
+
const manifest = await services.fetchRegistryManifest();
|
|
464
|
+
return {
|
|
465
|
+
uri: "contracts://registry/manifest",
|
|
466
|
+
mimeType: "application/json",
|
|
467
|
+
data: JSON.stringify(manifest, null, 2)
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
}));
|
|
471
|
+
return resources;
|
|
472
|
+
}
|
|
473
|
+
function buildContractsPrompts() {
|
|
474
|
+
const prompts = new PromptRegistry;
|
|
475
|
+
prompts.register(definePrompt({
|
|
476
|
+
meta: {
|
|
477
|
+
key: "contracts.editor",
|
|
478
|
+
version: "1.0.0",
|
|
479
|
+
title: "Contract editing guide",
|
|
480
|
+
description: "Guide AI agents through reading, editing, and validating contracts.",
|
|
481
|
+
tags: TAGS2,
|
|
482
|
+
stability: "beta",
|
|
483
|
+
owners: OWNERS2
|
|
484
|
+
},
|
|
485
|
+
args: [
|
|
486
|
+
{
|
|
487
|
+
name: "goal",
|
|
488
|
+
description: "What the agent wants to achieve with the contract.",
|
|
489
|
+
required: false,
|
|
490
|
+
schema: z.string().optional()
|
|
491
|
+
}
|
|
492
|
+
],
|
|
493
|
+
input: z.object({ goal: z.string().optional() }),
|
|
494
|
+
render: async ({ goal }) => [
|
|
495
|
+
{
|
|
496
|
+
type: "text",
|
|
497
|
+
text: [
|
|
498
|
+
"Contract editing workflow:",
|
|
499
|
+
"1. Use contracts.list to discover specs",
|
|
500
|
+
"2. Use contracts.get to read a spec",
|
|
501
|
+
"3. Edit content and call contracts.update",
|
|
502
|
+
"4. Run contracts.validate to verify changes",
|
|
503
|
+
"5. Run contracts.build to regenerate artifacts",
|
|
504
|
+
goal ? `Agent goal: ${goal}` : ""
|
|
505
|
+
].filter(Boolean).join(`
|
|
506
|
+
`)
|
|
507
|
+
},
|
|
508
|
+
{
|
|
509
|
+
type: "resource",
|
|
510
|
+
uri: "contracts://list",
|
|
511
|
+
title: "Available contracts"
|
|
512
|
+
}
|
|
513
|
+
]
|
|
514
|
+
}));
|
|
515
|
+
return prompts;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// src/application/mcp/contractsMcp.ts
|
|
519
|
+
function createContractsMcpHandler(path = "/api/mcp/contracts", services) {
|
|
520
|
+
return createMcpElysiaHandler({
|
|
521
|
+
logger: appLogger,
|
|
522
|
+
path,
|
|
523
|
+
serverName: "contractspec-contracts-mcp",
|
|
524
|
+
ops: buildContractsOps(services),
|
|
525
|
+
resources: buildContractsResources(services),
|
|
526
|
+
prompts: buildContractsPrompts()
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
export {
|
|
530
|
+
createContractsMcpHandler
|
|
531
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract management MCP resource and prompt definitions.
|
|
3
|
+
*/
|
|
4
|
+
import { PromptRegistry, ResourceRegistry } from '@contractspec/lib.contracts-spec';
|
|
5
|
+
import type { ContractsMcpServices } from './contractsMcpTypes';
|
|
6
|
+
export declare function buildContractsResources(services: ContractsMcpServices): ResourceRegistry;
|
|
7
|
+
export declare function buildContractsPrompts(): PromptRegistry;
|