@outfitter/mcp 0.2.0 → 0.4.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.
@@ -0,0 +1,30 @@
1
+ import { McpServer } from "./mcp-h2twz77x";
2
+ import { Server } from "@modelcontextprotocol/sdk/server/index";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio";
4
+ import { CallToolResult } from "@modelcontextprotocol/sdk/types";
5
+ type McpToolResponse = CallToolResult;
6
+ /**
7
+ * Wrap a handler success value into an MCP CallToolResult.
8
+ *
9
+ * If the value is already a valid McpToolResponse (has a `content` array),
10
+ * it is returned as-is. Otherwise it is wrapped in a text content block.
11
+ * Plain objects are also attached as `structuredContent` for SDK clients
12
+ * that support structured output.
13
+ */
14
+ declare function wrapToolResult(value: unknown): McpToolResponse;
15
+ /**
16
+ * Wrap an error into an MCP CallToolResult with `isError: true`.
17
+ *
18
+ * Serializes the error (preserving `_tag`, `message`, `code`, `context` if
19
+ * present) and wraps it as a text content block.
20
+ */
21
+ declare function wrapToolError(error: unknown): McpToolResponse;
22
+ /**
23
+ * Create an MCP SDK server from an Outfitter MCP server.
24
+ */
25
+ declare function createSdkServer(server: McpServer): Server;
26
+ /**
27
+ * Connect an MCP server over stdio transport.
28
+ */
29
+ declare function connectStdio(server: McpServer, transport?: StdioServerTransport): Promise<Server>;
30
+ export { McpToolResponse, wrapToolResult, wrapToolError, createSdkServer, connectStdio };
@@ -0,0 +1,538 @@
1
+ // @bun
2
+ import {
3
+ shouldEmitLog
4
+ } from "./mcp-fjtxsa0x.js";
5
+ import {
6
+ McpError
7
+ } from "./mcp-9m5hs2z0.js";
8
+ import {
9
+ zodToJsonSchema
10
+ } from "./mcp-f91wbr49.js";
11
+
12
+ // packages/mcp/src/server.ts
13
+ import { getEnvironment, getEnvironmentDefaults } from "@outfitter/config";
14
+ import { generateRequestId, Result } from "@outfitter/contracts";
15
+ import {
16
+ createOutfitterLoggerFactory,
17
+ createPrettyFormatter
18
+ } from "@outfitter/logging";
19
+ var VALID_MCP_LOG_LEVELS = new Set([
20
+ "debug",
21
+ "info",
22
+ "notice",
23
+ "warning",
24
+ "error",
25
+ "critical",
26
+ "alert",
27
+ "emergency"
28
+ ]);
29
+ var DEFAULTS_TO_MCP = {
30
+ debug: "debug",
31
+ info: "info",
32
+ warn: "warning",
33
+ error: "error"
34
+ };
35
+ function createDefaultMcpSink() {
36
+ const formatter = createPrettyFormatter({ colors: false });
37
+ return {
38
+ formatter,
39
+ write(record, formatted) {
40
+ const serialized = formatted ?? formatter.format(record);
41
+ const line = serialized.endsWith(`
42
+ `) ? serialized : `${serialized}
43
+ `;
44
+ if (typeof process !== "undefined" && process.stderr?.write) {
45
+ process.stderr.write(line);
46
+ }
47
+ }
48
+ };
49
+ }
50
+ function resolveDefaultLogLevel(options) {
51
+ const envLogLevel = process.env["OUTFITTER_LOG_LEVEL"];
52
+ if (envLogLevel !== undefined && VALID_MCP_LOG_LEVELS.has(envLogLevel)) {
53
+ return envLogLevel;
54
+ }
55
+ if (options.defaultLogLevel !== undefined && (options.defaultLogLevel === null || VALID_MCP_LOG_LEVELS.has(options.defaultLogLevel))) {
56
+ return options.defaultLogLevel;
57
+ }
58
+ const env = getEnvironment();
59
+ const defaults = getEnvironmentDefaults(env);
60
+ if (defaults.logLevel !== null) {
61
+ const mapped = DEFAULTS_TO_MCP[defaults.logLevel];
62
+ if (mapped !== undefined) {
63
+ return mapped;
64
+ }
65
+ }
66
+ return null;
67
+ }
68
+ function createMcpServer(options) {
69
+ const { name, version, logger: providedLogger } = options;
70
+ let loggerFactory = null;
71
+ const logger = providedLogger ?? (() => {
72
+ loggerFactory = createOutfitterLoggerFactory({
73
+ defaults: { sinks: [createDefaultMcpSink()] }
74
+ });
75
+ return loggerFactory.createLogger({
76
+ name: "mcp",
77
+ context: { serverName: name, serverVersion: version, surface: "mcp" }
78
+ });
79
+ })();
80
+ const tools = new Map;
81
+ const resources = new Map;
82
+ const resourceTemplates = new Map;
83
+ const prompts = new Map;
84
+ let sdkServer = null;
85
+ const subscriptions = new Set;
86
+ let clientLogLevel = resolveDefaultLogLevel(options);
87
+ function createHandlerContext(toolName, requestId, signal, progressToken) {
88
+ const ctx = {
89
+ requestId,
90
+ logger: logger.child({ tool: toolName, requestId }),
91
+ cwd: process.cwd(),
92
+ env: process.env
93
+ };
94
+ if (signal !== undefined) {
95
+ ctx.signal = signal;
96
+ }
97
+ if (progressToken !== undefined && sdkServer) {
98
+ ctx.progress = {
99
+ report(progress, total, message) {
100
+ sdkServer?.notification?.({
101
+ method: "notifications/progress",
102
+ params: {
103
+ progressToken,
104
+ progress,
105
+ ...total !== undefined ? { total } : {},
106
+ ...message ? { message } : {}
107
+ }
108
+ });
109
+ }
110
+ };
111
+ }
112
+ return ctx;
113
+ }
114
+ function translateError(error) {
115
+ const codeMap = {
116
+ validation: -32602,
117
+ not_found: -32601,
118
+ permission: -32600,
119
+ internal: -32603,
120
+ timeout: -32603,
121
+ network: -32603,
122
+ rate_limit: -32603,
123
+ auth: -32600,
124
+ conflict: -32603,
125
+ cancelled: -32603
126
+ };
127
+ const code = codeMap[error.category] ?? -32603;
128
+ return new McpError({
129
+ message: error.message,
130
+ code,
131
+ context: {
132
+ originalTag: error._tag,
133
+ category: error.category
134
+ }
135
+ });
136
+ }
137
+ const server = {
138
+ name,
139
+ version,
140
+ registerTool(tool) {
141
+ logger.debug("Registering tool", { name: tool.name });
142
+ const description = tool.description?.trim() ?? "";
143
+ if (description.length < 8) {
144
+ logger.warn("Tool description may be too short for search discovery", {
145
+ name: tool.name,
146
+ description
147
+ });
148
+ }
149
+ const jsonSchema = zodToJsonSchema(tool.inputSchema);
150
+ const handler = (input, ctx) => tool.handler(input, ctx);
151
+ const deferLoading = tool.deferLoading ?? true;
152
+ const stored = {
153
+ name: tool.name,
154
+ description,
155
+ inputSchema: jsonSchema,
156
+ deferLoading,
157
+ handler,
158
+ zodSchema: tool.inputSchema
159
+ };
160
+ if (tool.annotations !== undefined) {
161
+ stored.annotations = tool.annotations;
162
+ }
163
+ tools.set(tool.name, stored);
164
+ logger.info("Tool registered", { name: tool.name });
165
+ if (sdkServer) {
166
+ sdkServer.sendToolListChanged?.();
167
+ }
168
+ },
169
+ registerResource(resource) {
170
+ logger.debug("Registering resource", {
171
+ uri: resource.uri,
172
+ name: resource.name
173
+ });
174
+ resources.set(resource.uri, resource);
175
+ logger.info("Resource registered", { uri: resource.uri });
176
+ if (sdkServer) {
177
+ sdkServer.sendResourceListChanged?.();
178
+ }
179
+ },
180
+ registerResourceTemplate(template) {
181
+ logger.debug("Registering resource template", {
182
+ uriTemplate: template.uriTemplate,
183
+ name: template.name
184
+ });
185
+ resourceTemplates.set(template.uriTemplate, template);
186
+ logger.info("Resource template registered", {
187
+ uriTemplate: template.uriTemplate
188
+ });
189
+ if (sdkServer) {
190
+ sdkServer.sendResourceListChanged?.();
191
+ }
192
+ },
193
+ getTools() {
194
+ return Array.from(tools.values()).map((tool) => ({
195
+ name: tool.name,
196
+ description: tool.description,
197
+ inputSchema: tool.inputSchema,
198
+ defer_loading: tool.deferLoading,
199
+ ...tool.annotations ? { annotations: tool.annotations } : {}
200
+ }));
201
+ },
202
+ getResources() {
203
+ return Array.from(resources.values());
204
+ },
205
+ getResourceTemplates() {
206
+ return Array.from(resourceTemplates.values());
207
+ },
208
+ async complete(ref, argumentName, value) {
209
+ if (ref.type === "ref/prompt") {
210
+ const prompt = prompts.get(ref.name);
211
+ if (!prompt) {
212
+ return Result.err(new McpError({
213
+ message: `Prompt not found: ${ref.name}`,
214
+ code: -32601,
215
+ context: { prompt: ref.name }
216
+ }));
217
+ }
218
+ const arg = prompt.arguments.find((a) => a.name === argumentName);
219
+ if (!arg?.complete) {
220
+ return Result.ok({ values: [] });
221
+ }
222
+ try {
223
+ const result = await arg.complete(value);
224
+ return Result.ok(result);
225
+ } catch (error) {
226
+ return Result.err(new McpError({
227
+ message: error instanceof Error ? error.message : "Unknown error",
228
+ code: -32603,
229
+ context: {
230
+ prompt: ref.name,
231
+ argument: argumentName,
232
+ thrown: true
233
+ }
234
+ }));
235
+ }
236
+ }
237
+ if (ref.type === "ref/resource") {
238
+ const template = resourceTemplates.get(ref.uri);
239
+ if (!template) {
240
+ return Result.err(new McpError({
241
+ message: `Resource template not found: ${ref.uri}`,
242
+ code: -32601,
243
+ context: { uri: ref.uri }
244
+ }));
245
+ }
246
+ const handler = template.complete?.[argumentName];
247
+ if (!handler) {
248
+ return Result.ok({ values: [] });
249
+ }
250
+ try {
251
+ const result = await handler(value);
252
+ return Result.ok(result);
253
+ } catch (error) {
254
+ return Result.err(new McpError({
255
+ message: error instanceof Error ? error.message : "Unknown error",
256
+ code: -32603,
257
+ context: { uri: ref.uri, argument: argumentName, thrown: true }
258
+ }));
259
+ }
260
+ }
261
+ return Result.err(new McpError({
262
+ message: "Invalid completion reference type",
263
+ code: -32602,
264
+ context: { ref }
265
+ }));
266
+ },
267
+ registerPrompt(prompt) {
268
+ logger.debug("Registering prompt", { name: prompt.name });
269
+ prompts.set(prompt.name, prompt);
270
+ logger.info("Prompt registered", { name: prompt.name });
271
+ if (sdkServer) {
272
+ sdkServer.sendPromptListChanged?.();
273
+ }
274
+ },
275
+ getPrompts() {
276
+ return Array.from(prompts.values()).map((p) => ({
277
+ name: p.name,
278
+ ...p.description ? { description: p.description } : {},
279
+ arguments: p.arguments
280
+ }));
281
+ },
282
+ async getPrompt(promptName, args) {
283
+ const prompt = prompts.get(promptName);
284
+ if (!prompt) {
285
+ return Result.err(new McpError({
286
+ message: `Prompt not found: ${promptName}`,
287
+ code: -32601,
288
+ context: { prompt: promptName }
289
+ }));
290
+ }
291
+ for (const arg of prompt.arguments) {
292
+ if (arg.required && (args[arg.name] === undefined || args[arg.name] === "")) {
293
+ return Result.err(new McpError({
294
+ message: `Missing required argument: ${arg.name}`,
295
+ code: -32602,
296
+ context: { prompt: promptName, argument: arg.name }
297
+ }));
298
+ }
299
+ }
300
+ try {
301
+ const result = await prompt.handler(args);
302
+ if (result.isErr()) {
303
+ return Result.err(translateError(result.error));
304
+ }
305
+ return Result.ok(result.value);
306
+ } catch (error) {
307
+ return Result.err(new McpError({
308
+ message: error instanceof Error ? error.message : "Unknown error",
309
+ code: -32603,
310
+ context: { prompt: promptName, thrown: true }
311
+ }));
312
+ }
313
+ },
314
+ async readResource(uri) {
315
+ const resource = resources.get(uri);
316
+ if (resource) {
317
+ if (!resource.handler) {
318
+ return Result.err(new McpError({
319
+ message: `Resource not readable: ${uri}`,
320
+ code: -32002,
321
+ context: { uri }
322
+ }));
323
+ }
324
+ const requestId = generateRequestId();
325
+ const ctx = {
326
+ requestId,
327
+ logger: logger.child({ resource: uri, requestId }),
328
+ cwd: process.cwd(),
329
+ env: process.env
330
+ };
331
+ try {
332
+ const result = await resource.handler(uri, ctx);
333
+ if (result.isErr()) {
334
+ return Result.err(translateError(result.error));
335
+ }
336
+ return Result.ok(result.value);
337
+ } catch (error) {
338
+ return Result.err(new McpError({
339
+ message: error instanceof Error ? error.message : "Unknown error",
340
+ code: -32603,
341
+ context: { uri, thrown: true }
342
+ }));
343
+ }
344
+ }
345
+ for (const template of resourceTemplates.values()) {
346
+ const variables = matchUriTemplate(template.uriTemplate, uri);
347
+ if (variables) {
348
+ const templateRequestId = generateRequestId();
349
+ const templateCtx = {
350
+ requestId: templateRequestId,
351
+ logger: logger.child({
352
+ resource: uri,
353
+ requestId: templateRequestId
354
+ }),
355
+ cwd: process.cwd(),
356
+ env: process.env
357
+ };
358
+ try {
359
+ const result = await template.handler(uri, variables, templateCtx);
360
+ if (result.isErr()) {
361
+ return Result.err(translateError(result.error));
362
+ }
363
+ return Result.ok(result.value);
364
+ } catch (error) {
365
+ return Result.err(new McpError({
366
+ message: error instanceof Error ? error.message : "Unknown error",
367
+ code: -32603,
368
+ context: { uri, thrown: true }
369
+ }));
370
+ }
371
+ }
372
+ }
373
+ return Result.err(new McpError({
374
+ message: `Resource not found: ${uri}`,
375
+ code: -32002,
376
+ context: { uri }
377
+ }));
378
+ },
379
+ async invokeTool(toolName, input, invokeOptions) {
380
+ const requestId = invokeOptions?.requestId ?? generateRequestId();
381
+ logger.debug("Invoking tool", { tool: toolName, requestId });
382
+ const tool = tools.get(toolName);
383
+ if (!tool) {
384
+ logger.warn("Tool not found", { tool: toolName, requestId });
385
+ return Result.err(new McpError({
386
+ message: `Tool not found: ${toolName}`,
387
+ code: -32601,
388
+ context: { tool: toolName }
389
+ }));
390
+ }
391
+ const parseResult = tool.zodSchema.safeParse(input);
392
+ if (!parseResult.success) {
393
+ const errorMessages = parseResult.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ");
394
+ logger.warn("Input validation failed", {
395
+ tool: toolName,
396
+ requestId,
397
+ errors: errorMessages
398
+ });
399
+ return Result.err(new McpError({
400
+ message: `Invalid input: ${errorMessages}`,
401
+ code: -32602,
402
+ context: {
403
+ tool: toolName,
404
+ validationErrors: parseResult.error.issues
405
+ }
406
+ }));
407
+ }
408
+ const ctx = createHandlerContext(toolName, requestId, invokeOptions?.signal, invokeOptions?.progressToken);
409
+ try {
410
+ const result = await tool.handler(parseResult.data, ctx);
411
+ if (result.isErr()) {
412
+ logger.debug("Tool returned error", {
413
+ tool: toolName,
414
+ requestId,
415
+ error: result.error._tag
416
+ });
417
+ return Result.err(translateError(result.error));
418
+ }
419
+ logger.debug("Tool completed successfully", {
420
+ tool: toolName,
421
+ requestId
422
+ });
423
+ return Result.ok(result.value);
424
+ } catch (error) {
425
+ logger.error("Tool threw exception", {
426
+ tool: toolName,
427
+ requestId,
428
+ error: error instanceof Error ? error.message : String(error)
429
+ });
430
+ return Result.err(new McpError({
431
+ message: error instanceof Error ? error.message : "Unknown error",
432
+ code: -32603,
433
+ context: {
434
+ tool: toolName,
435
+ thrown: true
436
+ }
437
+ }));
438
+ }
439
+ },
440
+ subscribe(uri) {
441
+ subscriptions.add(uri);
442
+ logger.debug("Resource subscription added", { uri });
443
+ },
444
+ unsubscribe(uri) {
445
+ subscriptions.delete(uri);
446
+ logger.debug("Resource subscription removed", { uri });
447
+ },
448
+ notifyResourceUpdated(uri) {
449
+ if (subscriptions.has(uri)) {
450
+ sdkServer?.sendResourceUpdated?.({ uri });
451
+ }
452
+ },
453
+ notifyToolsChanged() {
454
+ sdkServer?.sendToolListChanged?.();
455
+ },
456
+ notifyResourcesChanged() {
457
+ sdkServer?.sendResourceListChanged?.();
458
+ },
459
+ notifyPromptsChanged() {
460
+ sdkServer?.sendPromptListChanged?.();
461
+ },
462
+ setLogLevel(level) {
463
+ clientLogLevel = level;
464
+ logger.debug("Client log level set", { level });
465
+ },
466
+ sendLogMessage(level, data, loggerName) {
467
+ if (!sdkServer || clientLogLevel === null || !shouldEmitLog(level, clientLogLevel)) {
468
+ return;
469
+ }
470
+ const params = {
471
+ level,
472
+ data
473
+ };
474
+ if (loggerName !== undefined) {
475
+ params.logger = loggerName;
476
+ }
477
+ sdkServer.sendLoggingMessage?.(params);
478
+ },
479
+ bindSdkServer(server2) {
480
+ sdkServer = server2;
481
+ clientLogLevel = resolveDefaultLogLevel(options);
482
+ logger.debug("SDK server bound for notifications");
483
+ },
484
+ async start() {
485
+ logger.info("MCP server starting", { name, version, tools: tools.size });
486
+ },
487
+ async stop() {
488
+ logger.info("MCP server stopping", { name, version });
489
+ if (loggerFactory !== null) {
490
+ await loggerFactory.flush();
491
+ }
492
+ }
493
+ };
494
+ return server;
495
+ }
496
+ function defineTool(definition) {
497
+ return definition;
498
+ }
499
+ function escapeRegex(str) {
500
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
501
+ }
502
+ function matchUriTemplate(template, uri) {
503
+ const paramNames = [];
504
+ const parts = template.split(/(\{[^}]+\})/);
505
+ const regexSource = parts.map((part) => {
506
+ const paramMatch = part.match(/^\{([^}]+)\}$/);
507
+ if (paramMatch?.[1]) {
508
+ paramNames.push(paramMatch[1]);
509
+ return "([^/]+)";
510
+ }
511
+ return escapeRegex(part);
512
+ }).join("");
513
+ const regex = new RegExp(`^${regexSource}$`);
514
+ const match = uri.match(regex);
515
+ if (!match) {
516
+ return null;
517
+ }
518
+ const variables = {};
519
+ for (let i = 0;i < paramNames.length; i++) {
520
+ const name = paramNames[i];
521
+ const value = match[i + 1];
522
+ if (name !== undefined && value !== undefined) {
523
+ variables[name] = value;
524
+ }
525
+ }
526
+ return variables;
527
+ }
528
+ function defineResource(definition) {
529
+ return definition;
530
+ }
531
+ function defineResourceTemplate(definition) {
532
+ return definition;
533
+ }
534
+ function definePrompt(definition) {
535
+ return definition;
536
+ }
537
+
538
+ export { createMcpServer, defineTool, defineResource, defineResourceTemplate, definePrompt };