@outfitter/mcp 0.1.0-rc.1
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 +342 -0
- package/dist/index.d.ts +522 -0
- package/dist/index.js +787 -0
- package/package.json +67 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,787 @@
|
|
|
1
|
+
// src/actions.ts
|
|
2
|
+
import { DEFAULT_REGISTRY_SURFACES } from "@outfitter/contracts";
|
|
3
|
+
|
|
4
|
+
// src/server.ts
|
|
5
|
+
import { generateRequestId, Result } from "@outfitter/contracts";
|
|
6
|
+
|
|
7
|
+
// src/schema.ts
|
|
8
|
+
function zodToJsonSchema(schema) {
|
|
9
|
+
return convertZodType(schema);
|
|
10
|
+
}
|
|
11
|
+
function getDef(schemaOrDef) {
|
|
12
|
+
if (!schemaOrDef) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
if (schemaOrDef._def) {
|
|
16
|
+
return schemaOrDef._def;
|
|
17
|
+
}
|
|
18
|
+
if (schemaOrDef.def) {
|
|
19
|
+
return schemaOrDef.def;
|
|
20
|
+
}
|
|
21
|
+
return schemaOrDef;
|
|
22
|
+
}
|
|
23
|
+
function getDescription(schema, def) {
|
|
24
|
+
if (typeof schema?.description === "string") {
|
|
25
|
+
return schema.description;
|
|
26
|
+
}
|
|
27
|
+
if (typeof def?.description === "string") {
|
|
28
|
+
return def.description;
|
|
29
|
+
}
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
function convertZodType(schema) {
|
|
33
|
+
const def = getDef(schema);
|
|
34
|
+
if (!def) {
|
|
35
|
+
return {};
|
|
36
|
+
}
|
|
37
|
+
const typeName = def.typeName ?? def.type;
|
|
38
|
+
let jsonSchema;
|
|
39
|
+
switch (typeName) {
|
|
40
|
+
case "ZodString":
|
|
41
|
+
case "string":
|
|
42
|
+
jsonSchema = convertString(def);
|
|
43
|
+
break;
|
|
44
|
+
case "ZodNumber":
|
|
45
|
+
case "number":
|
|
46
|
+
jsonSchema = convertNumber(def);
|
|
47
|
+
break;
|
|
48
|
+
case "ZodBoolean":
|
|
49
|
+
case "boolean":
|
|
50
|
+
jsonSchema = { type: "boolean" };
|
|
51
|
+
break;
|
|
52
|
+
case "ZodNull":
|
|
53
|
+
case "null":
|
|
54
|
+
jsonSchema = { type: "null" };
|
|
55
|
+
break;
|
|
56
|
+
case "ZodUndefined":
|
|
57
|
+
case "undefined":
|
|
58
|
+
jsonSchema = {};
|
|
59
|
+
break;
|
|
60
|
+
case "ZodArray":
|
|
61
|
+
case "array":
|
|
62
|
+
jsonSchema = convertArray(def);
|
|
63
|
+
break;
|
|
64
|
+
case "ZodObject":
|
|
65
|
+
case "object":
|
|
66
|
+
jsonSchema = convertObject(def);
|
|
67
|
+
break;
|
|
68
|
+
case "ZodOptional":
|
|
69
|
+
case "optional":
|
|
70
|
+
jsonSchema = convertZodType(def.innerType);
|
|
71
|
+
break;
|
|
72
|
+
case "ZodNullable":
|
|
73
|
+
case "nullable":
|
|
74
|
+
jsonSchema = {
|
|
75
|
+
anyOf: [convertZodType(def.innerType), { type: "null" }]
|
|
76
|
+
};
|
|
77
|
+
break;
|
|
78
|
+
case "ZodDefault":
|
|
79
|
+
case "default": {
|
|
80
|
+
const defaultValue = typeof def.defaultValue === "function" ? def.defaultValue() : def.defaultValue;
|
|
81
|
+
jsonSchema = {
|
|
82
|
+
...convertZodType(def.innerType),
|
|
83
|
+
default: defaultValue
|
|
84
|
+
};
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
case "ZodEnum":
|
|
88
|
+
case "enum": {
|
|
89
|
+
const values = def.values ?? Object.values(def.entries ?? {});
|
|
90
|
+
jsonSchema = {
|
|
91
|
+
type: "string",
|
|
92
|
+
enum: values
|
|
93
|
+
};
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
case "ZodNativeEnum":
|
|
97
|
+
jsonSchema = {
|
|
98
|
+
enum: Object.values(def.values ?? def.entries ?? {})
|
|
99
|
+
};
|
|
100
|
+
break;
|
|
101
|
+
case "ZodLiteral":
|
|
102
|
+
case "literal": {
|
|
103
|
+
const literalValues = Array.isArray(def.values) ? def.values : [def.value].filter((value) => value !== undefined);
|
|
104
|
+
if (literalValues.length > 1) {
|
|
105
|
+
jsonSchema = {
|
|
106
|
+
enum: literalValues
|
|
107
|
+
};
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
jsonSchema = literalValues.length ? {
|
|
111
|
+
const: literalValues[0]
|
|
112
|
+
} : {};
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
case "ZodUnion":
|
|
116
|
+
case "union":
|
|
117
|
+
jsonSchema = {
|
|
118
|
+
anyOf: def.options.map(convertZodType)
|
|
119
|
+
};
|
|
120
|
+
break;
|
|
121
|
+
case "ZodIntersection":
|
|
122
|
+
case "intersection":
|
|
123
|
+
jsonSchema = {
|
|
124
|
+
allOf: [convertZodType(def.left), convertZodType(def.right)]
|
|
125
|
+
};
|
|
126
|
+
break;
|
|
127
|
+
case "ZodRecord":
|
|
128
|
+
case "record":
|
|
129
|
+
jsonSchema = {
|
|
130
|
+
type: "object",
|
|
131
|
+
additionalProperties: def.valueType ? convertZodType(def.valueType) : {}
|
|
132
|
+
};
|
|
133
|
+
break;
|
|
134
|
+
case "ZodTuple":
|
|
135
|
+
case "tuple":
|
|
136
|
+
jsonSchema = {
|
|
137
|
+
type: "array",
|
|
138
|
+
items: def.items.map(convertZodType)
|
|
139
|
+
};
|
|
140
|
+
break;
|
|
141
|
+
case "ZodAny":
|
|
142
|
+
case "any":
|
|
143
|
+
jsonSchema = {};
|
|
144
|
+
break;
|
|
145
|
+
case "ZodUnknown":
|
|
146
|
+
case "unknown":
|
|
147
|
+
jsonSchema = {};
|
|
148
|
+
break;
|
|
149
|
+
case "ZodVoid":
|
|
150
|
+
case "void":
|
|
151
|
+
jsonSchema = {};
|
|
152
|
+
break;
|
|
153
|
+
case "ZodNever":
|
|
154
|
+
case "never":
|
|
155
|
+
jsonSchema = { not: {} };
|
|
156
|
+
break;
|
|
157
|
+
case "ZodEffects":
|
|
158
|
+
jsonSchema = convertZodType(def.schema);
|
|
159
|
+
break;
|
|
160
|
+
case "ZodPipeline":
|
|
161
|
+
case "pipe": {
|
|
162
|
+
const outputDef = getDef(def.out);
|
|
163
|
+
const outputType = outputDef?.typeName ?? outputDef?.type;
|
|
164
|
+
jsonSchema = outputType === "transform" ? convertZodType(def.in) : convertZodType(def.out);
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
case "ZodLazy":
|
|
168
|
+
case "lazy":
|
|
169
|
+
jsonSchema = {};
|
|
170
|
+
break;
|
|
171
|
+
default:
|
|
172
|
+
jsonSchema = {};
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
const description = getDescription(schema, def);
|
|
176
|
+
if (description && !jsonSchema.description) {
|
|
177
|
+
jsonSchema.description = description;
|
|
178
|
+
}
|
|
179
|
+
return jsonSchema;
|
|
180
|
+
}
|
|
181
|
+
function convertString(def) {
|
|
182
|
+
const schema = { type: "string" };
|
|
183
|
+
if (def.checks) {
|
|
184
|
+
for (const check of def.checks) {
|
|
185
|
+
const normalizedCheck = check?._zod?.def ?? check?.def ?? check;
|
|
186
|
+
if (normalizedCheck?.kind) {
|
|
187
|
+
switch (normalizedCheck.kind) {
|
|
188
|
+
case "min":
|
|
189
|
+
schema.minLength = normalizedCheck.value;
|
|
190
|
+
break;
|
|
191
|
+
case "max":
|
|
192
|
+
schema.maxLength = normalizedCheck.value;
|
|
193
|
+
break;
|
|
194
|
+
case "length":
|
|
195
|
+
schema.minLength = normalizedCheck.value;
|
|
196
|
+
schema.maxLength = normalizedCheck.value;
|
|
197
|
+
break;
|
|
198
|
+
case "email":
|
|
199
|
+
schema.pattern = "^[^@]+@[^@]+\\.[^@]+$";
|
|
200
|
+
break;
|
|
201
|
+
case "url":
|
|
202
|
+
schema.pattern = "^https?://";
|
|
203
|
+
break;
|
|
204
|
+
case "uuid":
|
|
205
|
+
schema.pattern = "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$";
|
|
206
|
+
break;
|
|
207
|
+
case "regex":
|
|
208
|
+
schema.pattern = normalizedCheck.regex?.source ?? normalizedCheck.pattern?.source ?? (typeof normalizedCheck.pattern === "string" ? normalizedCheck.pattern : undefined);
|
|
209
|
+
break;
|
|
210
|
+
default:
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (!normalizedCheck?.check) {
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
switch (normalizedCheck.check) {
|
|
219
|
+
case "min_length":
|
|
220
|
+
schema.minLength = normalizedCheck.minimum;
|
|
221
|
+
break;
|
|
222
|
+
case "max_length":
|
|
223
|
+
schema.maxLength = normalizedCheck.maximum;
|
|
224
|
+
break;
|
|
225
|
+
case "string_format":
|
|
226
|
+
if (normalizedCheck.pattern) {
|
|
227
|
+
schema.pattern = typeof normalizedCheck.pattern === "string" ? normalizedCheck.pattern : normalizedCheck.pattern.source;
|
|
228
|
+
}
|
|
229
|
+
if (normalizedCheck.format && normalizedCheck.format !== "regex") {
|
|
230
|
+
schema.format = normalizedCheck.format;
|
|
231
|
+
}
|
|
232
|
+
break;
|
|
233
|
+
default:
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return schema;
|
|
239
|
+
}
|
|
240
|
+
function convertNumber(def) {
|
|
241
|
+
const schema = { type: "number" };
|
|
242
|
+
if (def.checks) {
|
|
243
|
+
for (const check of def.checks) {
|
|
244
|
+
const normalizedCheck = check?._zod?.def ?? check?.def ?? check;
|
|
245
|
+
if (normalizedCheck?.kind) {
|
|
246
|
+
switch (normalizedCheck.kind) {
|
|
247
|
+
case "min":
|
|
248
|
+
schema.minimum = normalizedCheck.value;
|
|
249
|
+
break;
|
|
250
|
+
case "max":
|
|
251
|
+
schema.maximum = normalizedCheck.value;
|
|
252
|
+
break;
|
|
253
|
+
case "int":
|
|
254
|
+
schema.type = "integer";
|
|
255
|
+
break;
|
|
256
|
+
default:
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
if (!normalizedCheck?.check) {
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
switch (normalizedCheck.check) {
|
|
265
|
+
case "greater_than":
|
|
266
|
+
if (normalizedCheck.inclusive) {
|
|
267
|
+
schema.minimum = normalizedCheck.value;
|
|
268
|
+
} else {
|
|
269
|
+
schema.exclusiveMinimum = normalizedCheck.value;
|
|
270
|
+
}
|
|
271
|
+
break;
|
|
272
|
+
case "less_than":
|
|
273
|
+
if (normalizedCheck.inclusive) {
|
|
274
|
+
schema.maximum = normalizedCheck.value;
|
|
275
|
+
} else {
|
|
276
|
+
schema.exclusiveMaximum = normalizedCheck.value;
|
|
277
|
+
}
|
|
278
|
+
break;
|
|
279
|
+
case "number_format":
|
|
280
|
+
if (normalizedCheck.format === "int" || normalizedCheck.format === "safeint") {
|
|
281
|
+
schema.type = "integer";
|
|
282
|
+
}
|
|
283
|
+
break;
|
|
284
|
+
default:
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return schema;
|
|
290
|
+
}
|
|
291
|
+
function convertArray(def) {
|
|
292
|
+
const element = def.element ?? def.type;
|
|
293
|
+
const schema = {
|
|
294
|
+
type: "array",
|
|
295
|
+
items: element ? convertZodType(element) : {}
|
|
296
|
+
};
|
|
297
|
+
return schema;
|
|
298
|
+
}
|
|
299
|
+
function isFieldOptional(fieldDef) {
|
|
300
|
+
if (!(fieldDef?.typeName || fieldDef?.type)) {
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
const typeName = fieldDef.typeName ?? fieldDef.type;
|
|
304
|
+
if (typeName === "ZodOptional" || typeName === "ZodDefault" || typeName === "optional" || typeName === "default") {
|
|
305
|
+
return true;
|
|
306
|
+
}
|
|
307
|
+
if (typeName === "ZodEffects") {
|
|
308
|
+
return isFieldOptional(getDef(fieldDef.schema));
|
|
309
|
+
}
|
|
310
|
+
if (typeName === "ZodPipeline" || typeName === "pipe") {
|
|
311
|
+
const inputOptional = isFieldOptional(getDef(fieldDef.in));
|
|
312
|
+
const outputDef = getDef(fieldDef.out);
|
|
313
|
+
const outputType = outputDef?.typeName ?? outputDef?.type;
|
|
314
|
+
if (outputType === "transform") {
|
|
315
|
+
return inputOptional;
|
|
316
|
+
}
|
|
317
|
+
const outputOptional = isFieldOptional(outputDef);
|
|
318
|
+
return inputOptional && outputOptional;
|
|
319
|
+
}
|
|
320
|
+
if (typeName === "ZodNullable" || typeName === "nullable") {
|
|
321
|
+
return isFieldOptional(getDef(fieldDef.innerType));
|
|
322
|
+
}
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
function convertObject(def) {
|
|
326
|
+
const properties = {};
|
|
327
|
+
const required = [];
|
|
328
|
+
const shape = typeof def.shape === "function" ? def.shape() : def.shape;
|
|
329
|
+
for (const [key, value] of Object.entries(shape ?? {})) {
|
|
330
|
+
properties[key] = convertZodType(value);
|
|
331
|
+
const fieldDef = getDef(value);
|
|
332
|
+
if (!isFieldOptional(fieldDef)) {
|
|
333
|
+
required.push(key);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
const schema = {
|
|
337
|
+
type: "object",
|
|
338
|
+
properties
|
|
339
|
+
};
|
|
340
|
+
if (required.length > 0) {
|
|
341
|
+
schema.required = required;
|
|
342
|
+
}
|
|
343
|
+
return schema;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// src/types.ts
|
|
347
|
+
import {
|
|
348
|
+
TaggedError as TaggedErrorImpl
|
|
349
|
+
} from "@outfitter/contracts";
|
|
350
|
+
import { TaggedError } from "@outfitter/contracts";
|
|
351
|
+
var TaggedError2 = TaggedErrorImpl;
|
|
352
|
+
var McpErrorBase = TaggedError2("McpError")();
|
|
353
|
+
|
|
354
|
+
class McpError extends McpErrorBase {
|
|
355
|
+
category = "internal";
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// src/server.ts
|
|
359
|
+
function createNoOpLogger() {
|
|
360
|
+
const noop = () => {};
|
|
361
|
+
return {
|
|
362
|
+
trace: noop,
|
|
363
|
+
debug: noop,
|
|
364
|
+
info: noop,
|
|
365
|
+
warn: noop,
|
|
366
|
+
error: noop,
|
|
367
|
+
fatal: noop,
|
|
368
|
+
child: () => createNoOpLogger()
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
function createMcpServer(options) {
|
|
372
|
+
const { name, version, logger: providedLogger } = options;
|
|
373
|
+
const logger = providedLogger ?? createNoOpLogger();
|
|
374
|
+
const tools = new Map;
|
|
375
|
+
const resources = [];
|
|
376
|
+
function createHandlerContext(toolName, requestId, signal) {
|
|
377
|
+
const ctx = {
|
|
378
|
+
requestId,
|
|
379
|
+
logger: logger.child({ tool: toolName, requestId }),
|
|
380
|
+
cwd: process.cwd(),
|
|
381
|
+
env: process.env
|
|
382
|
+
};
|
|
383
|
+
if (signal !== undefined) {
|
|
384
|
+
ctx.signal = signal;
|
|
385
|
+
}
|
|
386
|
+
return ctx;
|
|
387
|
+
}
|
|
388
|
+
function translateError(error) {
|
|
389
|
+
const codeMap = {
|
|
390
|
+
validation: -32602,
|
|
391
|
+
not_found: -32601,
|
|
392
|
+
permission: -32600,
|
|
393
|
+
internal: -32603,
|
|
394
|
+
timeout: -32603,
|
|
395
|
+
network: -32603,
|
|
396
|
+
rate_limit: -32603,
|
|
397
|
+
auth: -32600,
|
|
398
|
+
conflict: -32603,
|
|
399
|
+
cancelled: -32603
|
|
400
|
+
};
|
|
401
|
+
const code = codeMap[error.category] ?? -32603;
|
|
402
|
+
return new McpError({
|
|
403
|
+
message: error.message,
|
|
404
|
+
code,
|
|
405
|
+
context: {
|
|
406
|
+
originalTag: error._tag,
|
|
407
|
+
category: error.category
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
const server = {
|
|
412
|
+
name,
|
|
413
|
+
version,
|
|
414
|
+
registerTool(tool) {
|
|
415
|
+
logger.debug("Registering tool", { name: tool.name });
|
|
416
|
+
const description = tool.description?.trim() ?? "";
|
|
417
|
+
if (description.length < 8) {
|
|
418
|
+
logger.warn("Tool description may be too short for search discovery", {
|
|
419
|
+
name: tool.name,
|
|
420
|
+
description
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
const jsonSchema = zodToJsonSchema(tool.inputSchema);
|
|
424
|
+
const handler = (input, ctx) => tool.handler(input, ctx);
|
|
425
|
+
const deferLoading = tool.deferLoading ?? true;
|
|
426
|
+
tools.set(tool.name, {
|
|
427
|
+
name: tool.name,
|
|
428
|
+
description,
|
|
429
|
+
inputSchema: jsonSchema,
|
|
430
|
+
deferLoading,
|
|
431
|
+
handler,
|
|
432
|
+
zodSchema: tool.inputSchema
|
|
433
|
+
});
|
|
434
|
+
logger.info("Tool registered", { name: tool.name });
|
|
435
|
+
},
|
|
436
|
+
registerResource(resource) {
|
|
437
|
+
logger.debug("Registering resource", {
|
|
438
|
+
uri: resource.uri,
|
|
439
|
+
name: resource.name
|
|
440
|
+
});
|
|
441
|
+
resources.push(resource);
|
|
442
|
+
logger.info("Resource registered", { uri: resource.uri });
|
|
443
|
+
},
|
|
444
|
+
getTools() {
|
|
445
|
+
return Array.from(tools.values()).map((tool) => ({
|
|
446
|
+
name: tool.name,
|
|
447
|
+
description: tool.description,
|
|
448
|
+
inputSchema: tool.inputSchema,
|
|
449
|
+
defer_loading: tool.deferLoading
|
|
450
|
+
}));
|
|
451
|
+
},
|
|
452
|
+
getResources() {
|
|
453
|
+
return [...resources];
|
|
454
|
+
},
|
|
455
|
+
async invokeTool(toolName, input, invokeOptions) {
|
|
456
|
+
const requestId = invokeOptions?.requestId ?? generateRequestId();
|
|
457
|
+
logger.debug("Invoking tool", { tool: toolName, requestId });
|
|
458
|
+
const tool = tools.get(toolName);
|
|
459
|
+
if (!tool) {
|
|
460
|
+
logger.warn("Tool not found", { tool: toolName, requestId });
|
|
461
|
+
return Result.err(new McpError({
|
|
462
|
+
message: `Tool not found: ${toolName}`,
|
|
463
|
+
code: -32601,
|
|
464
|
+
context: { tool: toolName }
|
|
465
|
+
}));
|
|
466
|
+
}
|
|
467
|
+
const parseResult = tool.zodSchema.safeParse(input);
|
|
468
|
+
if (!parseResult.success) {
|
|
469
|
+
const errorMessages = parseResult.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ");
|
|
470
|
+
logger.warn("Input validation failed", {
|
|
471
|
+
tool: toolName,
|
|
472
|
+
requestId,
|
|
473
|
+
errors: errorMessages
|
|
474
|
+
});
|
|
475
|
+
return Result.err(new McpError({
|
|
476
|
+
message: `Invalid input: ${errorMessages}`,
|
|
477
|
+
code: -32602,
|
|
478
|
+
context: {
|
|
479
|
+
tool: toolName,
|
|
480
|
+
validationErrors: parseResult.error.issues
|
|
481
|
+
}
|
|
482
|
+
}));
|
|
483
|
+
}
|
|
484
|
+
const ctx = createHandlerContext(toolName, requestId, invokeOptions?.signal);
|
|
485
|
+
try {
|
|
486
|
+
const result = await tool.handler(parseResult.data, ctx);
|
|
487
|
+
if (result.isErr()) {
|
|
488
|
+
logger.debug("Tool returned error", {
|
|
489
|
+
tool: toolName,
|
|
490
|
+
requestId,
|
|
491
|
+
error: result.error._tag
|
|
492
|
+
});
|
|
493
|
+
return Result.err(translateError(result.error));
|
|
494
|
+
}
|
|
495
|
+
logger.debug("Tool completed successfully", {
|
|
496
|
+
tool: toolName,
|
|
497
|
+
requestId
|
|
498
|
+
});
|
|
499
|
+
return Result.ok(result.value);
|
|
500
|
+
} catch (error) {
|
|
501
|
+
logger.error("Tool threw exception", {
|
|
502
|
+
tool: toolName,
|
|
503
|
+
requestId,
|
|
504
|
+
error: error instanceof Error ? error.message : String(error)
|
|
505
|
+
});
|
|
506
|
+
return Result.err(new McpError({
|
|
507
|
+
message: error instanceof Error ? error.message : "Unknown error",
|
|
508
|
+
code: -32603,
|
|
509
|
+
context: {
|
|
510
|
+
tool: toolName,
|
|
511
|
+
thrown: true
|
|
512
|
+
}
|
|
513
|
+
}));
|
|
514
|
+
}
|
|
515
|
+
},
|
|
516
|
+
async start() {
|
|
517
|
+
logger.info("MCP server starting", { name, version, tools: tools.size });
|
|
518
|
+
},
|
|
519
|
+
async stop() {
|
|
520
|
+
logger.info("MCP server stopping", { name, version });
|
|
521
|
+
}
|
|
522
|
+
};
|
|
523
|
+
return server;
|
|
524
|
+
}
|
|
525
|
+
function defineTool(definition) {
|
|
526
|
+
return definition;
|
|
527
|
+
}
|
|
528
|
+
function defineResource(definition) {
|
|
529
|
+
return definition;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// src/actions.ts
|
|
533
|
+
function isActionRegistry(source) {
|
|
534
|
+
return "list" in source;
|
|
535
|
+
}
|
|
536
|
+
function buildMcpTools(source, options = {}) {
|
|
537
|
+
const actions = isActionRegistry(source) ? source.list() : source;
|
|
538
|
+
const includeSurfaces = options.includeSurfaces ?? [
|
|
539
|
+
"mcp"
|
|
540
|
+
];
|
|
541
|
+
return actions.filter((action) => {
|
|
542
|
+
const surfaces = action.surfaces ?? DEFAULT_REGISTRY_SURFACES;
|
|
543
|
+
return surfaces.some((surface) => includeSurfaces.includes(surface));
|
|
544
|
+
}).map((action) => defineTool({
|
|
545
|
+
name: action.mcp?.tool ?? action.id,
|
|
546
|
+
description: action.mcp?.description ?? action.description ?? action.id,
|
|
547
|
+
inputSchema: action.input,
|
|
548
|
+
handler: async (input, ctx) => action.handler(input, ctx),
|
|
549
|
+
...action.mcp?.deferLoading !== undefined ? { deferLoading: action.mcp.deferLoading } : {}
|
|
550
|
+
}));
|
|
551
|
+
}
|
|
552
|
+
// src/core-tools.ts
|
|
553
|
+
import { Result as Result2, ValidationError } from "@outfitter/contracts";
|
|
554
|
+
import { z } from "zod";
|
|
555
|
+
var DEFAULT_DOCS = {
|
|
556
|
+
overview: "No documentation configured yet.",
|
|
557
|
+
tools: [],
|
|
558
|
+
examples: [],
|
|
559
|
+
schemas: {}
|
|
560
|
+
};
|
|
561
|
+
var docsSchema = z.object({
|
|
562
|
+
section: z.enum(["overview", "tools", "examples", "schemas"]).optional()
|
|
563
|
+
});
|
|
564
|
+
function pickDocsSection(payload, section) {
|
|
565
|
+
if (!section) {
|
|
566
|
+
return payload;
|
|
567
|
+
}
|
|
568
|
+
return {
|
|
569
|
+
[section]: payload[section]
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
function defineDocsTool(options = {}) {
|
|
573
|
+
return {
|
|
574
|
+
name: "docs",
|
|
575
|
+
description: options.description ?? "Documentation, usage patterns, and examples for this MCP server.",
|
|
576
|
+
deferLoading: false,
|
|
577
|
+
inputSchema: docsSchema,
|
|
578
|
+
handler: async (input) => {
|
|
579
|
+
const payload = options.getDocs ? await options.getDocs(input.section) : options.docs ?? DEFAULT_DOCS;
|
|
580
|
+
return Result2.ok(pickDocsSection(payload, input.section));
|
|
581
|
+
}
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
var configSchema = z.object({
|
|
585
|
+
action: z.enum(["get", "set", "list"]),
|
|
586
|
+
key: z.string().optional(),
|
|
587
|
+
value: z.unknown().optional()
|
|
588
|
+
});
|
|
589
|
+
function createInMemoryStore(initial = {}) {
|
|
590
|
+
const store = new Map(Object.entries(initial));
|
|
591
|
+
return {
|
|
592
|
+
get(key) {
|
|
593
|
+
return { value: store.get(key), found: store.has(key) };
|
|
594
|
+
},
|
|
595
|
+
set(key, value) {
|
|
596
|
+
store.set(key, value);
|
|
597
|
+
},
|
|
598
|
+
list() {
|
|
599
|
+
return Object.fromEntries(store.entries());
|
|
600
|
+
}
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
function defineConfigTool(options = {}) {
|
|
604
|
+
const store = options.store ?? createInMemoryStore(options.initial);
|
|
605
|
+
return {
|
|
606
|
+
name: "config",
|
|
607
|
+
description: options.description ?? "Read or modify server configuration values.",
|
|
608
|
+
deferLoading: false,
|
|
609
|
+
inputSchema: configSchema,
|
|
610
|
+
handler: async (input) => {
|
|
611
|
+
switch (input.action) {
|
|
612
|
+
case "list": {
|
|
613
|
+
const config = await store.list();
|
|
614
|
+
return Result2.ok({ action: "list", config });
|
|
615
|
+
}
|
|
616
|
+
case "get": {
|
|
617
|
+
if (!input.key) {
|
|
618
|
+
return Result2.err(new ValidationError({
|
|
619
|
+
message: "Config key is required for action 'get'.",
|
|
620
|
+
field: "key"
|
|
621
|
+
}));
|
|
622
|
+
}
|
|
623
|
+
const { value, found } = await store.get(input.key);
|
|
624
|
+
return Result2.ok({ action: "get", key: input.key, value, found });
|
|
625
|
+
}
|
|
626
|
+
case "set": {
|
|
627
|
+
if (!input.key) {
|
|
628
|
+
return Result2.err(new ValidationError({
|
|
629
|
+
message: "Config key is required for action 'set'.",
|
|
630
|
+
field: "key"
|
|
631
|
+
}));
|
|
632
|
+
}
|
|
633
|
+
await store.set(input.key, input.value);
|
|
634
|
+
return Result2.ok({
|
|
635
|
+
action: "set",
|
|
636
|
+
key: input.key,
|
|
637
|
+
value: input.value
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
default:
|
|
641
|
+
return Result2.err(new ValidationError({
|
|
642
|
+
message: `Unknown action: ${input.action}`,
|
|
643
|
+
field: "action"
|
|
644
|
+
}));
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
var querySchema = z.object({
|
|
650
|
+
q: z.string().min(1).describe("Search query. Supports natural language or filter syntax.").optional(),
|
|
651
|
+
query: z.string().min(1).describe("Alias for q. Supports natural language or filter syntax.").optional(),
|
|
652
|
+
limit: z.number().int().positive().optional(),
|
|
653
|
+
cursor: z.string().optional(),
|
|
654
|
+
filters: z.record(z.string(), z.unknown()).optional()
|
|
655
|
+
}).refine((value) => {
|
|
656
|
+
const queryValue = (value.q ?? value.query)?.trim();
|
|
657
|
+
return typeof queryValue === "string" && queryValue.length > 0;
|
|
658
|
+
}, {
|
|
659
|
+
message: "Query is required.",
|
|
660
|
+
path: ["q"]
|
|
661
|
+
});
|
|
662
|
+
function defineQueryTool(options = {}) {
|
|
663
|
+
return {
|
|
664
|
+
name: "query",
|
|
665
|
+
description: options.description ?? "Search and discover resources with filters and pagination.",
|
|
666
|
+
deferLoading: false,
|
|
667
|
+
inputSchema: querySchema,
|
|
668
|
+
handler: (input, ctx) => {
|
|
669
|
+
const normalized = {
|
|
670
|
+
...input,
|
|
671
|
+
q: (input.q ?? input.query ?? "").trim()
|
|
672
|
+
};
|
|
673
|
+
if (options.handler) {
|
|
674
|
+
return options.handler(normalized, ctx);
|
|
675
|
+
}
|
|
676
|
+
return Promise.resolve(Result2.ok({
|
|
677
|
+
results: [],
|
|
678
|
+
_meta: {
|
|
679
|
+
note: "No query handler configured."
|
|
680
|
+
}
|
|
681
|
+
}));
|
|
682
|
+
}
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
function createCoreTools(options = {}) {
|
|
686
|
+
return [
|
|
687
|
+
defineDocsTool(options.docs),
|
|
688
|
+
defineConfigTool(options.config),
|
|
689
|
+
defineQueryTool(options.query)
|
|
690
|
+
];
|
|
691
|
+
}
|
|
692
|
+
// src/transport.ts
|
|
693
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
694
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
695
|
+
import {
|
|
696
|
+
CallToolRequestSchema,
|
|
697
|
+
ListToolsRequestSchema
|
|
698
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
699
|
+
import { safeStringify } from "@outfitter/contracts";
|
|
700
|
+
function isMcpToolResponse(value) {
|
|
701
|
+
if (!value || typeof value !== "object") {
|
|
702
|
+
return false;
|
|
703
|
+
}
|
|
704
|
+
const content = value.content;
|
|
705
|
+
return Array.isArray(content);
|
|
706
|
+
}
|
|
707
|
+
function toTextPayload(value) {
|
|
708
|
+
if (typeof value === "string") {
|
|
709
|
+
return value;
|
|
710
|
+
}
|
|
711
|
+
return safeStringify(value);
|
|
712
|
+
}
|
|
713
|
+
function serializeError(error) {
|
|
714
|
+
if (error && typeof error === "object") {
|
|
715
|
+
const record = error;
|
|
716
|
+
return {
|
|
717
|
+
_tag: record._tag ?? "McpError",
|
|
718
|
+
message: record.message ?? "Unknown error",
|
|
719
|
+
code: record.code,
|
|
720
|
+
context: record.context
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
return {
|
|
724
|
+
_tag: "McpError",
|
|
725
|
+
message: String(error)
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
function wrapToolResult(value) {
|
|
729
|
+
if (isMcpToolResponse(value)) {
|
|
730
|
+
return value;
|
|
731
|
+
}
|
|
732
|
+
const structuredContent = value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
733
|
+
return {
|
|
734
|
+
content: [
|
|
735
|
+
{
|
|
736
|
+
type: "text",
|
|
737
|
+
text: toTextPayload(value)
|
|
738
|
+
}
|
|
739
|
+
],
|
|
740
|
+
...structuredContent ? { structuredContent } : {}
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
function wrapToolError(error) {
|
|
744
|
+
return {
|
|
745
|
+
content: [
|
|
746
|
+
{
|
|
747
|
+
type: "text",
|
|
748
|
+
text: toTextPayload(serializeError(error))
|
|
749
|
+
}
|
|
750
|
+
],
|
|
751
|
+
isError: true
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
function createSdkServer(server) {
|
|
755
|
+
const sdkServer = new Server({ name: server.name, version: server.version }, { capabilities: { tools: {} } });
|
|
756
|
+
sdkServer.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
757
|
+
tools: server.getTools()
|
|
758
|
+
}));
|
|
759
|
+
sdkServer.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
760
|
+
const { name, arguments: args } = request.params;
|
|
761
|
+
const result = await server.invokeTool(name, args ?? {});
|
|
762
|
+
if (result.isErr()) {
|
|
763
|
+
return wrapToolError(result.error);
|
|
764
|
+
}
|
|
765
|
+
return wrapToolResult(result.value);
|
|
766
|
+
});
|
|
767
|
+
return sdkServer;
|
|
768
|
+
}
|
|
769
|
+
async function connectStdio(server, transport = new StdioServerTransport) {
|
|
770
|
+
const sdkServer = createSdkServer(server);
|
|
771
|
+
await sdkServer.connect(transport);
|
|
772
|
+
return sdkServer;
|
|
773
|
+
}
|
|
774
|
+
export {
|
|
775
|
+
zodToJsonSchema,
|
|
776
|
+
defineTool,
|
|
777
|
+
defineResource,
|
|
778
|
+
defineQueryTool,
|
|
779
|
+
defineDocsTool,
|
|
780
|
+
defineConfigTool,
|
|
781
|
+
createSdkServer,
|
|
782
|
+
createMcpServer,
|
|
783
|
+
createCoreTools,
|
|
784
|
+
connectStdio,
|
|
785
|
+
buildMcpTools,
|
|
786
|
+
McpError
|
|
787
|
+
};
|