@outfitter/mcp 0.1.0 → 0.3.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 +61 -3
- package/dist/index.d.ts +401 -1
- package/dist/index.js +471 -18
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -54,9 +54,10 @@ Creates an MCP server instance.
|
|
|
54
54
|
|
|
55
55
|
```typescript
|
|
56
56
|
interface McpServerOptions {
|
|
57
|
-
name: string;
|
|
58
|
-
version: string;
|
|
59
|
-
logger?: Logger;
|
|
57
|
+
name: string; // Server name for MCP handshake
|
|
58
|
+
version: string; // Server version (semver)
|
|
59
|
+
logger?: Logger; // Optional structured logger
|
|
60
|
+
defaultLogLevel?: McpLogLevel | null; // Default log forwarding level
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
const server = createMcpServer({
|
|
@@ -66,6 +67,41 @@ const server = createMcpServer({
|
|
|
66
67
|
});
|
|
67
68
|
```
|
|
68
69
|
|
|
70
|
+
### Log Forwarding
|
|
71
|
+
|
|
72
|
+
MCP servers can forward log messages to the connected client. The default log level is resolved from environment configuration:
|
|
73
|
+
|
|
74
|
+
**Precedence** (highest wins):
|
|
75
|
+
1. `OUTFITTER_LOG_LEVEL` environment variable
|
|
76
|
+
2. `options.defaultLogLevel`
|
|
77
|
+
3. `OUTFITTER_ENV` profile defaults (`"debug"` in development, `null` otherwise)
|
|
78
|
+
4. `null` (no forwarding)
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
const server = createMcpServer({
|
|
82
|
+
name: "my-server",
|
|
83
|
+
version: "1.0.0",
|
|
84
|
+
// Forwarding level auto-resolved from OUTFITTER_ENV
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// With OUTFITTER_ENV=development → forwards at "debug"
|
|
88
|
+
// With OUTFITTER_ENV=production → no forwarding (null)
|
|
89
|
+
// With OUTFITTER_LOG_LEVEL=error → forwards at "error"
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Set `defaultLogLevel: null` to explicitly disable forwarding regardless of environment. The MCP client can always override via `logging/setLevel`.
|
|
93
|
+
|
|
94
|
+
#### `sendLogMessage(level, data, loggerName?)`
|
|
95
|
+
|
|
96
|
+
Send a log message to the connected MCP client.
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
server.sendLogMessage("info", "Indexing complete", "my-server");
|
|
100
|
+
server.sendLogMessage("warning", { message: "Rate limited", retryAfter: 30 });
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Only sends if the message level meets or exceeds the current client log level threshold.
|
|
104
|
+
|
|
69
105
|
### defineTool(definition)
|
|
70
106
|
|
|
71
107
|
Helper for defining typed tools with better type inference.
|
|
@@ -103,6 +139,7 @@ interface ResourceDefinition {
|
|
|
103
139
|
name: string; // Human-readable name
|
|
104
140
|
description?: string; // Optional description
|
|
105
141
|
mimeType?: string; // Content MIME type
|
|
142
|
+
handler?: ResourceReadHandler; // Optional resources/read handler
|
|
106
143
|
}
|
|
107
144
|
|
|
108
145
|
const configResource = defineResource({
|
|
@@ -110,9 +147,27 @@ const configResource = defineResource({
|
|
|
110
147
|
name: "Application Config",
|
|
111
148
|
description: "Main configuration file",
|
|
112
149
|
mimeType: "application/json",
|
|
150
|
+
handler: async (uri, ctx) => {
|
|
151
|
+
ctx.logger.debug("Reading config resource", { uri });
|
|
152
|
+
return Result.ok([
|
|
153
|
+
{
|
|
154
|
+
uri,
|
|
155
|
+
mimeType: "application/json",
|
|
156
|
+
text: JSON.stringify({ debug: true }),
|
|
157
|
+
},
|
|
158
|
+
]);
|
|
159
|
+
},
|
|
113
160
|
});
|
|
114
161
|
```
|
|
115
162
|
|
|
163
|
+
Registered resources with handlers are exposed through MCP `resources/read`.
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
server.registerResource(configResource);
|
|
167
|
+
|
|
168
|
+
const contentResult = await server.readResource("file:///etc/app/config.json");
|
|
169
|
+
```
|
|
170
|
+
|
|
116
171
|
### Server Methods
|
|
117
172
|
|
|
118
173
|
```typescript
|
|
@@ -123,12 +178,15 @@ interface McpServer {
|
|
|
123
178
|
// Registration
|
|
124
179
|
registerTool<TInput, TOutput, TError>(tool: ToolDefinition): void;
|
|
125
180
|
registerResource(resource: ResourceDefinition): void;
|
|
181
|
+
registerResourceTemplate(template: ResourceTemplateDefinition): void;
|
|
126
182
|
|
|
127
183
|
// Introspection
|
|
128
184
|
getTools(): SerializedTool[];
|
|
129
185
|
getResources(): ResourceDefinition[];
|
|
186
|
+
getResourceTemplates(): ResourceTemplateDefinition[];
|
|
130
187
|
|
|
131
188
|
// Invocation
|
|
189
|
+
readResource(uri: string): Promise<Result<ResourceContent[], McpError>>;
|
|
132
190
|
invokeTool<T>(name: string, input: unknown, options?: InvokeToolOptions): Promise<Result<T, McpError>>;
|
|
133
191
|
|
|
134
192
|
// Lifecycle
|
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,32 @@ import { ActionRegistry, ActionSurface, AnyActionSpec } from "@outfitter/contrac
|
|
|
2
2
|
import { Handler, HandlerContext, Logger, OutfitterError, Result, TaggedErrorClass } from "@outfitter/contracts";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
/**
|
|
5
|
+
* @outfitter/mcp - Logging Bridge
|
|
6
|
+
*
|
|
7
|
+
* Maps Outfitter log levels to MCP log levels for
|
|
8
|
+
* server-to-client log message notifications.
|
|
9
|
+
*
|
|
10
|
+
* @packageDocumentation
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* MCP log levels as defined in the MCP specification.
|
|
14
|
+
* Ordered from least to most severe.
|
|
15
|
+
*/
|
|
16
|
+
type McpLogLevel = "debug" | "info" | "notice" | "warning" | "error" | "critical" | "alert" | "emergency";
|
|
17
|
+
/**
|
|
18
|
+
* Outfitter log levels.
|
|
19
|
+
*/
|
|
20
|
+
type OutfitterLogLevel = "trace" | "debug" | "info" | "warn" | "error" | "fatal";
|
|
21
|
+
/**
|
|
22
|
+
* Map an Outfitter log level to the corresponding MCP log level.
|
|
23
|
+
*/
|
|
24
|
+
declare function mapLogLevelToMcp(level: OutfitterLogLevel): McpLogLevel;
|
|
25
|
+
/**
|
|
26
|
+
* Check whether a message at the given level should be emitted
|
|
27
|
+
* based on the client-requested threshold.
|
|
28
|
+
*/
|
|
29
|
+
declare function shouldEmitLog(messageLevel: McpLogLevel, threshold: McpLogLevel): boolean;
|
|
30
|
+
/**
|
|
5
31
|
* Configuration options for creating an MCP server.
|
|
6
32
|
*
|
|
7
33
|
* @example
|
|
@@ -31,6 +57,37 @@ interface McpServerOptions {
|
|
|
31
57
|
* If not provided, a no-op logger is used.
|
|
32
58
|
*/
|
|
33
59
|
logger?: Logger;
|
|
60
|
+
/**
|
|
61
|
+
* Default MCP log level for client-facing log forwarding.
|
|
62
|
+
*
|
|
63
|
+
* Precedence (highest wins):
|
|
64
|
+
* 1. `OUTFITTER_LOG_LEVEL` environment variable
|
|
65
|
+
* 2. This option
|
|
66
|
+
* 3. Environment profile (`OUTFITTER_ENV`)
|
|
67
|
+
* 4. `null` (no forwarding until client opts in)
|
|
68
|
+
*
|
|
69
|
+
* Set to `null` to explicitly disable forwarding regardless of environment.
|
|
70
|
+
* The MCP client can always override via `logging/setLevel`.
|
|
71
|
+
*/
|
|
72
|
+
defaultLogLevel?: McpLogLevel | null;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Behavioral hints for MCP tools.
|
|
76
|
+
*
|
|
77
|
+
* Annotations help clients understand tool behavior without invoking them.
|
|
78
|
+
* All fields are optional — only include hints that apply.
|
|
79
|
+
*
|
|
80
|
+
* @see https://spec.modelcontextprotocol.io/specification/2025-03-26/server/tools/#annotations
|
|
81
|
+
*/
|
|
82
|
+
interface ToolAnnotations {
|
|
83
|
+
/** When true, the tool does not modify any state. */
|
|
84
|
+
readOnlyHint?: boolean;
|
|
85
|
+
/** When true, the tool may perform destructive operations (e.g., deleting data). */
|
|
86
|
+
destructiveHint?: boolean;
|
|
87
|
+
/** When true, calling the tool multiple times with the same input has the same effect. */
|
|
88
|
+
idempotentHint?: boolean;
|
|
89
|
+
/** When true, the tool may interact with external systems beyond the server. */
|
|
90
|
+
openWorldHint?: boolean;
|
|
34
91
|
}
|
|
35
92
|
/**
|
|
36
93
|
* Definition of an MCP tool that can be invoked by clients.
|
|
@@ -94,6 +151,11 @@ interface ToolDefinition<
|
|
|
94
151
|
*/
|
|
95
152
|
inputSchema: z.ZodType<TInput>;
|
|
96
153
|
/**
|
|
154
|
+
* Optional behavioral annotations for the tool.
|
|
155
|
+
* Helps clients understand tool behavior without invoking it.
|
|
156
|
+
*/
|
|
157
|
+
annotations?: ToolAnnotations;
|
|
158
|
+
/**
|
|
97
159
|
* Handler function that processes the tool invocation.
|
|
98
160
|
* Receives validated input and HandlerContext, returns Result.
|
|
99
161
|
*/
|
|
@@ -112,8 +174,66 @@ interface SerializedTool {
|
|
|
112
174
|
inputSchema: Record<string, unknown>;
|
|
113
175
|
/** MCP tool-search hint: whether tool is deferred */
|
|
114
176
|
defer_loading?: boolean;
|
|
177
|
+
/** Behavioral annotations for the tool */
|
|
178
|
+
annotations?: ToolAnnotations;
|
|
115
179
|
}
|
|
116
180
|
/**
|
|
181
|
+
* Annotations for content items (resource content, prompt messages).
|
|
182
|
+
*
|
|
183
|
+
* Provides hints about content audience and priority.
|
|
184
|
+
*
|
|
185
|
+
* @see https://spec.modelcontextprotocol.io/specification/2025-03-26/server/utilities/annotations/
|
|
186
|
+
*/
|
|
187
|
+
interface ContentAnnotations {
|
|
188
|
+
/**
|
|
189
|
+
* Who the content is intended for.
|
|
190
|
+
* Can include "user", "assistant", or both.
|
|
191
|
+
*/
|
|
192
|
+
audience?: Array<"user" | "assistant">;
|
|
193
|
+
/**
|
|
194
|
+
* Priority level from 0.0 (least) to 1.0 (most important).
|
|
195
|
+
*/
|
|
196
|
+
priority?: number;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Text content returned from a resource read.
|
|
200
|
+
*/
|
|
201
|
+
interface TextResourceContent {
|
|
202
|
+
/** Resource URI */
|
|
203
|
+
uri: string;
|
|
204
|
+
/** Text content */
|
|
205
|
+
text: string;
|
|
206
|
+
/** Optional MIME type */
|
|
207
|
+
mimeType?: string;
|
|
208
|
+
/** Optional content annotations */
|
|
209
|
+
annotations?: ContentAnnotations;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Binary (base64-encoded) content returned from a resource read.
|
|
213
|
+
*/
|
|
214
|
+
interface BlobResourceContent {
|
|
215
|
+
/** Resource URI */
|
|
216
|
+
uri: string;
|
|
217
|
+
/** Base64-encoded binary content */
|
|
218
|
+
blob: string;
|
|
219
|
+
/** Optional MIME type */
|
|
220
|
+
mimeType?: string;
|
|
221
|
+
/** Optional content annotations */
|
|
222
|
+
annotations?: ContentAnnotations;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Content returned from reading a resource.
|
|
226
|
+
*/
|
|
227
|
+
type ResourceContent = TextResourceContent | BlobResourceContent;
|
|
228
|
+
/**
|
|
229
|
+
* Handler for reading a resource's content.
|
|
230
|
+
*
|
|
231
|
+
* @param uri - The resource URI being read
|
|
232
|
+
* @param ctx - Handler context with logger and requestId
|
|
233
|
+
* @returns Array of resource content items
|
|
234
|
+
*/
|
|
235
|
+
type ResourceReadHandler = (uri: string, ctx: HandlerContext) => Promise<Result<ResourceContent[], OutfitterError>>;
|
|
236
|
+
/**
|
|
117
237
|
* Definition of an MCP resource that can be read by clients.
|
|
118
238
|
*
|
|
119
239
|
* Resources represent data that clients can access, such as files,
|
|
@@ -126,6 +246,10 @@ interface SerializedTool {
|
|
|
126
246
|
* name: "Application Config",
|
|
127
247
|
* description: "Main application configuration file",
|
|
128
248
|
* mimeType: "application/json",
|
|
249
|
+
* handler: async (uri, ctx) => {
|
|
250
|
+
* const content = await readFile(uri);
|
|
251
|
+
* return Result.ok([{ uri, text: content }]);
|
|
252
|
+
* },
|
|
129
253
|
* };
|
|
130
254
|
* ```
|
|
131
255
|
*/
|
|
@@ -150,6 +274,151 @@ interface ResourceDefinition {
|
|
|
150
274
|
* Helps clients understand how to process the resource.
|
|
151
275
|
*/
|
|
152
276
|
mimeType?: string;
|
|
277
|
+
/**
|
|
278
|
+
* Optional handler for reading the resource content.
|
|
279
|
+
* If not provided, the resource is metadata-only.
|
|
280
|
+
*/
|
|
281
|
+
handler?: ResourceReadHandler;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Handler for reading a resource template's content.
|
|
285
|
+
*
|
|
286
|
+
* @param uri - The matched URI
|
|
287
|
+
* @param variables - Extracted template variables
|
|
288
|
+
* @param ctx - Handler context
|
|
289
|
+
*/
|
|
290
|
+
type ResourceTemplateReadHandler = (uri: string, variables: Record<string, string>, ctx: HandlerContext) => Promise<Result<ResourceContent[], OutfitterError>>;
|
|
291
|
+
/**
|
|
292
|
+
* Definition of an MCP resource template with URI pattern matching.
|
|
293
|
+
*
|
|
294
|
+
* Templates use RFC 6570 Level 1 URI templates (e.g., `{param}`)
|
|
295
|
+
* to match and extract variables from URIs.
|
|
296
|
+
*
|
|
297
|
+
* @example
|
|
298
|
+
* ```typescript
|
|
299
|
+
* const userTemplate: ResourceTemplateDefinition = {
|
|
300
|
+
* uriTemplate: "db:///users/{userId}/profile",
|
|
301
|
+
* name: "User Profile",
|
|
302
|
+
* handler: async (uri, variables) => {
|
|
303
|
+
* const profile = await getProfile(variables.userId);
|
|
304
|
+
* return Result.ok([{ uri, text: JSON.stringify(profile) }]);
|
|
305
|
+
* },
|
|
306
|
+
* };
|
|
307
|
+
* ```
|
|
308
|
+
*/
|
|
309
|
+
interface ResourceTemplateDefinition {
|
|
310
|
+
/** URI template with `{param}` placeholders (RFC 6570 Level 1). */
|
|
311
|
+
uriTemplate: string;
|
|
312
|
+
/** Human-readable name for the template. */
|
|
313
|
+
name: string;
|
|
314
|
+
/** Optional description. */
|
|
315
|
+
description?: string;
|
|
316
|
+
/** Optional MIME type. */
|
|
317
|
+
mimeType?: string;
|
|
318
|
+
/** Optional completion handlers keyed by parameter name. */
|
|
319
|
+
complete?: Record<string, CompletionHandler>;
|
|
320
|
+
/** Handler for reading matched resources. */
|
|
321
|
+
handler: ResourceTemplateReadHandler;
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Result of a completion request.
|
|
325
|
+
*/
|
|
326
|
+
interface CompletionResult {
|
|
327
|
+
/** Completion values */
|
|
328
|
+
values: string[];
|
|
329
|
+
/** Total number of available values (for pagination) */
|
|
330
|
+
total?: number;
|
|
331
|
+
/** Whether there are more values */
|
|
332
|
+
hasMore?: boolean;
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Handler for generating completions.
|
|
336
|
+
*/
|
|
337
|
+
type CompletionHandler = (value: string) => Promise<CompletionResult>;
|
|
338
|
+
/**
|
|
339
|
+
* Reference to a prompt or resource for completion.
|
|
340
|
+
*/
|
|
341
|
+
type CompletionRef = {
|
|
342
|
+
type: "ref/prompt";
|
|
343
|
+
name: string;
|
|
344
|
+
} | {
|
|
345
|
+
type: "ref/resource";
|
|
346
|
+
uri: string;
|
|
347
|
+
};
|
|
348
|
+
/**
|
|
349
|
+
* Argument definition for a prompt.
|
|
350
|
+
*/
|
|
351
|
+
interface PromptArgument {
|
|
352
|
+
/** Argument name */
|
|
353
|
+
name: string;
|
|
354
|
+
/** Human-readable description */
|
|
355
|
+
description?: string;
|
|
356
|
+
/** Whether this argument is required */
|
|
357
|
+
required?: boolean;
|
|
358
|
+
/** Optional completion handler for this argument */
|
|
359
|
+
complete?: CompletionHandler;
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Content block within a prompt message.
|
|
363
|
+
*/
|
|
364
|
+
interface PromptMessageContent {
|
|
365
|
+
/** Content type */
|
|
366
|
+
type: "text";
|
|
367
|
+
/** Text content */
|
|
368
|
+
text: string;
|
|
369
|
+
/** Optional content annotations */
|
|
370
|
+
annotations?: ContentAnnotations;
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* A message in a prompt response.
|
|
374
|
+
*/
|
|
375
|
+
interface PromptMessage {
|
|
376
|
+
/** Message role */
|
|
377
|
+
role: "user" | "assistant";
|
|
378
|
+
/** Message content */
|
|
379
|
+
content: PromptMessageContent;
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Result returned from getting a prompt.
|
|
383
|
+
*/
|
|
384
|
+
interface PromptResult {
|
|
385
|
+
/** Prompt messages */
|
|
386
|
+
messages: PromptMessage[];
|
|
387
|
+
/** Optional description override */
|
|
388
|
+
description?: string;
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Handler for generating prompt messages.
|
|
392
|
+
*/
|
|
393
|
+
type PromptHandler = (args: Record<string, string | undefined>) => Promise<Result<PromptResult, OutfitterError>>;
|
|
394
|
+
/**
|
|
395
|
+
* Definition of an MCP prompt.
|
|
396
|
+
*
|
|
397
|
+
* Prompts are reusable templates that generate messages for LLMs.
|
|
398
|
+
*
|
|
399
|
+
* @example
|
|
400
|
+
* ```typescript
|
|
401
|
+
* const reviewPrompt: PromptDefinition = {
|
|
402
|
+
* name: "code-review",
|
|
403
|
+
* description: "Review code changes",
|
|
404
|
+
* arguments: [
|
|
405
|
+
* { name: "language", description: "Programming language", required: true },
|
|
406
|
+
* ],
|
|
407
|
+
* handler: async (args) => Result.ok({
|
|
408
|
+
* messages: [{ role: "user", content: { type: "text", text: `Review this ${args.language} code` } }],
|
|
409
|
+
* }),
|
|
410
|
+
* };
|
|
411
|
+
* ```
|
|
412
|
+
*/
|
|
413
|
+
interface PromptDefinition {
|
|
414
|
+
/** Unique prompt name */
|
|
415
|
+
name: string;
|
|
416
|
+
/** Human-readable description */
|
|
417
|
+
description?: string;
|
|
418
|
+
/** Prompt arguments */
|
|
419
|
+
arguments: PromptArgument[];
|
|
420
|
+
/** Handler to generate messages */
|
|
421
|
+
handler: PromptHandler;
|
|
153
422
|
}
|
|
154
423
|
declare const McpErrorBase: TaggedErrorClass<"McpError", {
|
|
155
424
|
message: string;
|
|
@@ -191,6 +460,8 @@ interface InvokeToolOptions {
|
|
|
191
460
|
signal?: AbortSignal;
|
|
192
461
|
/** Custom request ID (auto-generated if not provided) */
|
|
193
462
|
requestId?: string;
|
|
463
|
+
/** Progress token from client for tracking progress */
|
|
464
|
+
progressToken?: string | number;
|
|
194
465
|
}
|
|
195
466
|
/**
|
|
196
467
|
* MCP Server instance.
|
|
@@ -236,11 +507,56 @@ interface McpServer {
|
|
|
236
507
|
*/
|
|
237
508
|
getTools(): SerializedTool[];
|
|
238
509
|
/**
|
|
510
|
+
* Register a resource template with the server.
|
|
511
|
+
* @param template - Resource template definition to register
|
|
512
|
+
*/
|
|
513
|
+
registerResourceTemplate(template: ResourceTemplateDefinition): void;
|
|
514
|
+
/**
|
|
239
515
|
* Get all registered resources.
|
|
240
516
|
* @returns Array of resource definitions
|
|
241
517
|
*/
|
|
242
518
|
getResources(): ResourceDefinition[];
|
|
243
519
|
/**
|
|
520
|
+
* Get all registered resource templates.
|
|
521
|
+
* @returns Array of resource template definitions
|
|
522
|
+
*/
|
|
523
|
+
getResourceTemplates(): ResourceTemplateDefinition[];
|
|
524
|
+
/**
|
|
525
|
+
* Complete an argument value.
|
|
526
|
+
* @param ref - Reference to the prompt or resource template
|
|
527
|
+
* @param argumentName - Name of the argument to complete
|
|
528
|
+
* @param value - Current value to complete
|
|
529
|
+
* @returns Result with completion values or McpError
|
|
530
|
+
*/
|
|
531
|
+
complete(ref: CompletionRef, argumentName: string, value: string): Promise<Result<CompletionResult, InstanceType<typeof McpError>>>;
|
|
532
|
+
/**
|
|
533
|
+
* Register a prompt with the server.
|
|
534
|
+
* @param prompt - Prompt definition to register
|
|
535
|
+
*/
|
|
536
|
+
registerPrompt(prompt: PromptDefinition): void;
|
|
537
|
+
/**
|
|
538
|
+
* Get all registered prompts.
|
|
539
|
+
* @returns Array of prompt definitions (without handlers)
|
|
540
|
+
*/
|
|
541
|
+
getPrompts(): Array<{
|
|
542
|
+
name: string;
|
|
543
|
+
description?: string;
|
|
544
|
+
arguments: PromptArgument[];
|
|
545
|
+
}>;
|
|
546
|
+
/**
|
|
547
|
+
* Get a specific prompt's messages.
|
|
548
|
+
* @param name - Prompt name
|
|
549
|
+
* @param args - Prompt arguments
|
|
550
|
+
* @returns Result with prompt result or McpError
|
|
551
|
+
*/
|
|
552
|
+
getPrompt(name: string, args: Record<string, string | undefined>): Promise<Result<PromptResult, InstanceType<typeof McpError>>>;
|
|
553
|
+
/**
|
|
554
|
+
* Read a resource by URI.
|
|
555
|
+
* @param uri - Resource URI
|
|
556
|
+
* @returns Result with resource content or McpError
|
|
557
|
+
*/
|
|
558
|
+
readResource(uri: string): Promise<Result<ResourceContent[], InstanceType<typeof McpError>>>;
|
|
559
|
+
/**
|
|
244
560
|
* Invoke a tool by name.
|
|
245
561
|
* @param name - Tool name
|
|
246
562
|
* @param input - Tool input (will be validated)
|
|
@@ -249,6 +565,56 @@ interface McpServer {
|
|
|
249
565
|
*/
|
|
250
566
|
invokeTool<T = unknown>(name: string, input: unknown, options?: InvokeToolOptions): Promise<Result<T, InstanceType<typeof McpError>>>;
|
|
251
567
|
/**
|
|
568
|
+
* Subscribe to updates for a resource URI.
|
|
569
|
+
* @param uri - Resource URI to subscribe to
|
|
570
|
+
*/
|
|
571
|
+
subscribe(uri: string): void;
|
|
572
|
+
/**
|
|
573
|
+
* Unsubscribe from updates for a resource URI.
|
|
574
|
+
* @param uri - Resource URI to unsubscribe from
|
|
575
|
+
*/
|
|
576
|
+
unsubscribe(uri: string): void;
|
|
577
|
+
/**
|
|
578
|
+
* Notify connected clients that a specific resource has been updated.
|
|
579
|
+
* Only emits for subscribed URIs.
|
|
580
|
+
* @param uri - URI of the updated resource
|
|
581
|
+
*/
|
|
582
|
+
notifyResourceUpdated(uri: string): void;
|
|
583
|
+
/**
|
|
584
|
+
* Notify connected clients that the tool list has changed.
|
|
585
|
+
*/
|
|
586
|
+
notifyToolsChanged(): void;
|
|
587
|
+
/**
|
|
588
|
+
* Notify connected clients that the resource list has changed.
|
|
589
|
+
*/
|
|
590
|
+
notifyResourcesChanged(): void;
|
|
591
|
+
/**
|
|
592
|
+
* Notify connected clients that the prompt list has changed.
|
|
593
|
+
*/
|
|
594
|
+
notifyPromptsChanged(): void;
|
|
595
|
+
/**
|
|
596
|
+
* Set the client-requested log level.
|
|
597
|
+
* Only log messages at or above this level will be forwarded.
|
|
598
|
+
* @param level - MCP log level string
|
|
599
|
+
*/
|
|
600
|
+
setLogLevel?(level: string): void;
|
|
601
|
+
/**
|
|
602
|
+
* Send a log message to connected clients.
|
|
603
|
+
* Filters by the client-requested log level threshold.
|
|
604
|
+
* No-op if no SDK server is bound or if the message is below the threshold.
|
|
605
|
+
*
|
|
606
|
+
* @param level - MCP log level for the message
|
|
607
|
+
* @param data - Log data (string, object, or any serializable value)
|
|
608
|
+
* @param loggerName - Optional logger name for client-side filtering
|
|
609
|
+
*/
|
|
610
|
+
sendLogMessage(level: McpLogLevel, data: unknown, loggerName?: string): void;
|
|
611
|
+
/**
|
|
612
|
+
* Bind the SDK server instance for notifications.
|
|
613
|
+
* Called internally by the transport layer.
|
|
614
|
+
* @param sdkServer - The MCP SDK Server instance
|
|
615
|
+
*/
|
|
616
|
+
bindSdkServer?(sdkServer: any): void;
|
|
617
|
+
/**
|
|
252
618
|
* Start the MCP server.
|
|
253
619
|
* Begins listening for client connections.
|
|
254
620
|
*/
|
|
@@ -260,12 +626,26 @@ interface McpServer {
|
|
|
260
626
|
stop(): Promise<void>;
|
|
261
627
|
}
|
|
262
628
|
/**
|
|
629
|
+
* Reporter for sending progress updates to clients.
|
|
630
|
+
*/
|
|
631
|
+
interface ProgressReporter {
|
|
632
|
+
/**
|
|
633
|
+
* Report progress for the current operation.
|
|
634
|
+
* @param progress - Current progress value
|
|
635
|
+
* @param total - Optional total value (for percentage calculation)
|
|
636
|
+
* @param message - Optional human-readable status message
|
|
637
|
+
*/
|
|
638
|
+
report(progress: number, total?: number, message?: string): void;
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
263
641
|
* Extended handler context for MCP tools.
|
|
264
642
|
* Includes MCP-specific information in addition to standard HandlerContext.
|
|
265
643
|
*/
|
|
266
644
|
interface McpHandlerContext extends HandlerContext {
|
|
267
645
|
/** The name of the tool being invoked */
|
|
268
646
|
toolName?: string;
|
|
647
|
+
/** Progress reporter, present when client provides a progressToken */
|
|
648
|
+
progress?: ProgressReporter;
|
|
269
649
|
}
|
|
270
650
|
interface BuildMcpToolsOptions {
|
|
271
651
|
readonly includeSurfaces?: readonly ActionSurface[];
|
|
@@ -507,6 +887,26 @@ declare function defineTool<
|
|
|
507
887
|
* ```
|
|
508
888
|
*/
|
|
509
889
|
declare function defineResource(definition: ResourceDefinition): ResourceDefinition;
|
|
890
|
+
/**
|
|
891
|
+
* Define a resource template.
|
|
892
|
+
*
|
|
893
|
+
* Helper function for creating resource template definitions
|
|
894
|
+
* with URI pattern matching.
|
|
895
|
+
*
|
|
896
|
+
* @param definition - Resource template definition object
|
|
897
|
+
* @returns The same resource template definition
|
|
898
|
+
*/
|
|
899
|
+
declare function defineResourceTemplate(definition: ResourceTemplateDefinition): ResourceTemplateDefinition;
|
|
900
|
+
/**
|
|
901
|
+
* Define a prompt.
|
|
902
|
+
*
|
|
903
|
+
* Helper function for creating prompt definitions
|
|
904
|
+
* with consistent typing.
|
|
905
|
+
*
|
|
906
|
+
* @param definition - Prompt definition object
|
|
907
|
+
* @returns The same prompt definition
|
|
908
|
+
*/
|
|
909
|
+
declare function definePrompt(definition: PromptDefinition): PromptDefinition;
|
|
510
910
|
import { Server } from "@modelcontextprotocol/sdk/server/index";
|
|
511
911
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio";
|
|
512
912
|
import { CallToolResult } from "@modelcontextprotocol/sdk/types";
|
|
@@ -519,4 +919,4 @@ declare function createSdkServer(server: McpServer): Server;
|
|
|
519
919
|
* Connect an MCP server over stdio transport.
|
|
520
920
|
*/
|
|
521
921
|
declare function connectStdio(server: McpServer, transport?: StdioServerTransport): Promise<Server>;
|
|
522
|
-
export { zodToJsonSchema, defineTool, defineResource, defineQueryTool, defineDocsTool, defineConfigTool, createSdkServer, createMcpServer, createCoreTools, connectStdio, buildMcpTools, ToolDefinition, SerializedTool, ResourceDefinition, QueryToolResponse, QueryToolOptions, QueryToolInput, McpToolResponse, McpServerOptions, McpServer, McpHandlerContext, McpError, JsonSchema, InvokeToolOptions, DocsToolResponse, DocsToolOptions, DocsToolInput, DocsToolEntry, DocsSection, CoreToolsOptions, ConfigToolResponse, ConfigToolOptions, ConfigToolInput, ConfigStore, ConfigAction, BuildMcpToolsOptions };
|
|
922
|
+
export { zodToJsonSchema, shouldEmitLog, mapLogLevelToMcp, defineTool, defineResourceTemplate, defineResource, defineQueryTool, definePrompt, defineDocsTool, defineConfigTool, createSdkServer, createMcpServer, createCoreTools, connectStdio, buildMcpTools, ToolDefinition, ToolAnnotations, TextResourceContent, SerializedTool, ResourceTemplateReadHandler, ResourceTemplateDefinition, ResourceReadHandler, ResourceDefinition, ResourceContent, QueryToolResponse, QueryToolOptions, QueryToolInput, PromptResult, PromptMessageContent, PromptMessage, PromptHandler, PromptDefinition, PromptArgument, ProgressReporter, McpToolResponse, McpServerOptions, McpServer, McpLogLevel, McpHandlerContext, McpError, JsonSchema, InvokeToolOptions, DocsToolResponse, DocsToolOptions, DocsToolInput, DocsToolEntry, DocsSection, CoreToolsOptions, ContentAnnotations, ConfigToolResponse, ConfigToolOptions, ConfigToolInput, ConfigStore, ConfigAction, CompletionResult, CompletionRef, CompletionHandler, BuildMcpToolsOptions, BlobResourceContent };
|
package/dist/index.js
CHANGED
|
@@ -2,8 +2,43 @@
|
|
|
2
2
|
import { DEFAULT_REGISTRY_SURFACES } from "@outfitter/contracts";
|
|
3
3
|
|
|
4
4
|
// src/server.ts
|
|
5
|
+
import { getEnvironment, getEnvironmentDefaults } from "@outfitter/config";
|
|
5
6
|
import { generateRequestId, Result } from "@outfitter/contracts";
|
|
6
7
|
|
|
8
|
+
// src/logging.ts
|
|
9
|
+
var MCP_LEVEL_ORDER = [
|
|
10
|
+
"debug",
|
|
11
|
+
"info",
|
|
12
|
+
"notice",
|
|
13
|
+
"warning",
|
|
14
|
+
"error",
|
|
15
|
+
"critical",
|
|
16
|
+
"alert",
|
|
17
|
+
"emergency"
|
|
18
|
+
];
|
|
19
|
+
function mapLogLevelToMcp(level) {
|
|
20
|
+
switch (level) {
|
|
21
|
+
case "trace":
|
|
22
|
+
case "debug":
|
|
23
|
+
return "debug";
|
|
24
|
+
case "info":
|
|
25
|
+
return "info";
|
|
26
|
+
case "warn":
|
|
27
|
+
return "warning";
|
|
28
|
+
case "error":
|
|
29
|
+
return "error";
|
|
30
|
+
case "fatal":
|
|
31
|
+
return "emergency";
|
|
32
|
+
default: {
|
|
33
|
+
const _exhaustiveCheck = level;
|
|
34
|
+
return _exhaustiveCheck;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function shouldEmitLog(messageLevel, threshold) {
|
|
39
|
+
return MCP_LEVEL_ORDER.indexOf(messageLevel) >= MCP_LEVEL_ORDER.indexOf(threshold);
|
|
40
|
+
}
|
|
41
|
+
|
|
7
42
|
// src/schema.ts
|
|
8
43
|
function zodToJsonSchema(schema) {
|
|
9
44
|
return convertZodType(schema);
|
|
@@ -357,23 +392,61 @@ class McpError extends McpErrorBase {
|
|
|
357
392
|
|
|
358
393
|
// src/server.ts
|
|
359
394
|
function createNoOpLogger() {
|
|
360
|
-
const noop = () => {};
|
|
361
395
|
return {
|
|
362
|
-
trace:
|
|
363
|
-
debug:
|
|
364
|
-
info:
|
|
365
|
-
warn:
|
|
366
|
-
error:
|
|
367
|
-
fatal:
|
|
396
|
+
trace: (..._args) => {},
|
|
397
|
+
debug: (..._args) => {},
|
|
398
|
+
info: (..._args) => {},
|
|
399
|
+
warn: (..._args) => {},
|
|
400
|
+
error: (..._args) => {},
|
|
401
|
+
fatal: (..._args) => {},
|
|
368
402
|
child: () => createNoOpLogger()
|
|
369
403
|
};
|
|
370
404
|
}
|
|
405
|
+
var VALID_MCP_LOG_LEVELS = new Set([
|
|
406
|
+
"debug",
|
|
407
|
+
"info",
|
|
408
|
+
"notice",
|
|
409
|
+
"warning",
|
|
410
|
+
"error",
|
|
411
|
+
"critical",
|
|
412
|
+
"alert",
|
|
413
|
+
"emergency"
|
|
414
|
+
]);
|
|
415
|
+
var DEFAULTS_TO_MCP = {
|
|
416
|
+
debug: "debug",
|
|
417
|
+
info: "info",
|
|
418
|
+
warn: "warning",
|
|
419
|
+
error: "error"
|
|
420
|
+
};
|
|
421
|
+
function resolveDefaultLogLevel(options) {
|
|
422
|
+
const envLogLevel = process.env["OUTFITTER_LOG_LEVEL"];
|
|
423
|
+
if (envLogLevel !== undefined && VALID_MCP_LOG_LEVELS.has(envLogLevel)) {
|
|
424
|
+
return envLogLevel;
|
|
425
|
+
}
|
|
426
|
+
if (options.defaultLogLevel !== undefined && (options.defaultLogLevel === null || VALID_MCP_LOG_LEVELS.has(options.defaultLogLevel))) {
|
|
427
|
+
return options.defaultLogLevel;
|
|
428
|
+
}
|
|
429
|
+
const env = getEnvironment();
|
|
430
|
+
const defaults = getEnvironmentDefaults(env);
|
|
431
|
+
if (defaults.logLevel !== null) {
|
|
432
|
+
const mapped = DEFAULTS_TO_MCP[defaults.logLevel];
|
|
433
|
+
if (mapped !== undefined) {
|
|
434
|
+
return mapped;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
371
439
|
function createMcpServer(options) {
|
|
372
440
|
const { name, version, logger: providedLogger } = options;
|
|
373
441
|
const logger = providedLogger ?? createNoOpLogger();
|
|
374
442
|
const tools = new Map;
|
|
375
|
-
const resources =
|
|
376
|
-
|
|
443
|
+
const resources = new Map;
|
|
444
|
+
const resourceTemplates = new Map;
|
|
445
|
+
const prompts = new Map;
|
|
446
|
+
let sdkServer = null;
|
|
447
|
+
const subscriptions = new Set;
|
|
448
|
+
let clientLogLevel = resolveDefaultLogLevel(options);
|
|
449
|
+
function createHandlerContext(toolName, requestId, signal, progressToken) {
|
|
377
450
|
const ctx = {
|
|
378
451
|
requestId,
|
|
379
452
|
logger: logger.child({ tool: toolName, requestId }),
|
|
@@ -383,6 +456,21 @@ function createMcpServer(options) {
|
|
|
383
456
|
if (signal !== undefined) {
|
|
384
457
|
ctx.signal = signal;
|
|
385
458
|
}
|
|
459
|
+
if (progressToken !== undefined && sdkServer) {
|
|
460
|
+
ctx.progress = {
|
|
461
|
+
report(progress, total, message) {
|
|
462
|
+
sdkServer?.notification?.({
|
|
463
|
+
method: "notifications/progress",
|
|
464
|
+
params: {
|
|
465
|
+
progressToken,
|
|
466
|
+
progress,
|
|
467
|
+
...total !== undefined ? { total } : {},
|
|
468
|
+
...message ? { message } : {}
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
}
|
|
386
474
|
return ctx;
|
|
387
475
|
}
|
|
388
476
|
function translateError(error) {
|
|
@@ -423,34 +511,232 @@ function createMcpServer(options) {
|
|
|
423
511
|
const jsonSchema = zodToJsonSchema(tool.inputSchema);
|
|
424
512
|
const handler = (input, ctx) => tool.handler(input, ctx);
|
|
425
513
|
const deferLoading = tool.deferLoading ?? true;
|
|
426
|
-
|
|
514
|
+
const stored = {
|
|
427
515
|
name: tool.name,
|
|
428
516
|
description,
|
|
429
517
|
inputSchema: jsonSchema,
|
|
430
518
|
deferLoading,
|
|
431
519
|
handler,
|
|
432
520
|
zodSchema: tool.inputSchema
|
|
433
|
-
}
|
|
521
|
+
};
|
|
522
|
+
if (tool.annotations !== undefined) {
|
|
523
|
+
stored.annotations = tool.annotations;
|
|
524
|
+
}
|
|
525
|
+
tools.set(tool.name, stored);
|
|
434
526
|
logger.info("Tool registered", { name: tool.name });
|
|
527
|
+
if (sdkServer) {
|
|
528
|
+
sdkServer.sendToolListChanged?.();
|
|
529
|
+
}
|
|
435
530
|
},
|
|
436
531
|
registerResource(resource) {
|
|
437
532
|
logger.debug("Registering resource", {
|
|
438
533
|
uri: resource.uri,
|
|
439
534
|
name: resource.name
|
|
440
535
|
});
|
|
441
|
-
resources.
|
|
536
|
+
resources.set(resource.uri, resource);
|
|
442
537
|
logger.info("Resource registered", { uri: resource.uri });
|
|
538
|
+
if (sdkServer) {
|
|
539
|
+
sdkServer.sendResourceListChanged?.();
|
|
540
|
+
}
|
|
541
|
+
},
|
|
542
|
+
registerResourceTemplate(template) {
|
|
543
|
+
logger.debug("Registering resource template", {
|
|
544
|
+
uriTemplate: template.uriTemplate,
|
|
545
|
+
name: template.name
|
|
546
|
+
});
|
|
547
|
+
resourceTemplates.set(template.uriTemplate, template);
|
|
548
|
+
logger.info("Resource template registered", {
|
|
549
|
+
uriTemplate: template.uriTemplate
|
|
550
|
+
});
|
|
551
|
+
if (sdkServer) {
|
|
552
|
+
sdkServer.sendResourceListChanged?.();
|
|
553
|
+
}
|
|
443
554
|
},
|
|
444
555
|
getTools() {
|
|
445
556
|
return Array.from(tools.values()).map((tool) => ({
|
|
446
557
|
name: tool.name,
|
|
447
558
|
description: tool.description,
|
|
448
559
|
inputSchema: tool.inputSchema,
|
|
449
|
-
defer_loading: tool.deferLoading
|
|
560
|
+
defer_loading: tool.deferLoading,
|
|
561
|
+
...tool.annotations ? { annotations: tool.annotations } : {}
|
|
450
562
|
}));
|
|
451
563
|
},
|
|
452
564
|
getResources() {
|
|
453
|
-
return
|
|
565
|
+
return Array.from(resources.values());
|
|
566
|
+
},
|
|
567
|
+
getResourceTemplates() {
|
|
568
|
+
return Array.from(resourceTemplates.values());
|
|
569
|
+
},
|
|
570
|
+
async complete(ref, argumentName, value) {
|
|
571
|
+
if (ref.type === "ref/prompt") {
|
|
572
|
+
const prompt = prompts.get(ref.name);
|
|
573
|
+
if (!prompt) {
|
|
574
|
+
return Result.err(new McpError({
|
|
575
|
+
message: `Prompt not found: ${ref.name}`,
|
|
576
|
+
code: -32601,
|
|
577
|
+
context: { prompt: ref.name }
|
|
578
|
+
}));
|
|
579
|
+
}
|
|
580
|
+
const arg = prompt.arguments.find((a) => a.name === argumentName);
|
|
581
|
+
if (!arg?.complete) {
|
|
582
|
+
return Result.ok({ values: [] });
|
|
583
|
+
}
|
|
584
|
+
try {
|
|
585
|
+
const result = await arg.complete(value);
|
|
586
|
+
return Result.ok(result);
|
|
587
|
+
} catch (error) {
|
|
588
|
+
return Result.err(new McpError({
|
|
589
|
+
message: error instanceof Error ? error.message : "Unknown error",
|
|
590
|
+
code: -32603,
|
|
591
|
+
context: {
|
|
592
|
+
prompt: ref.name,
|
|
593
|
+
argument: argumentName,
|
|
594
|
+
thrown: true
|
|
595
|
+
}
|
|
596
|
+
}));
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
if (ref.type === "ref/resource") {
|
|
600
|
+
const template = resourceTemplates.get(ref.uri);
|
|
601
|
+
if (!template) {
|
|
602
|
+
return Result.err(new McpError({
|
|
603
|
+
message: `Resource template not found: ${ref.uri}`,
|
|
604
|
+
code: -32601,
|
|
605
|
+
context: { uri: ref.uri }
|
|
606
|
+
}));
|
|
607
|
+
}
|
|
608
|
+
const handler = template.complete?.[argumentName];
|
|
609
|
+
if (!handler) {
|
|
610
|
+
return Result.ok({ values: [] });
|
|
611
|
+
}
|
|
612
|
+
try {
|
|
613
|
+
const result = await handler(value);
|
|
614
|
+
return Result.ok(result);
|
|
615
|
+
} catch (error) {
|
|
616
|
+
return Result.err(new McpError({
|
|
617
|
+
message: error instanceof Error ? error.message : "Unknown error",
|
|
618
|
+
code: -32603,
|
|
619
|
+
context: { uri: ref.uri, argument: argumentName, thrown: true }
|
|
620
|
+
}));
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
return Result.err(new McpError({
|
|
624
|
+
message: "Invalid completion reference type",
|
|
625
|
+
code: -32602,
|
|
626
|
+
context: { ref }
|
|
627
|
+
}));
|
|
628
|
+
},
|
|
629
|
+
registerPrompt(prompt) {
|
|
630
|
+
logger.debug("Registering prompt", { name: prompt.name });
|
|
631
|
+
prompts.set(prompt.name, prompt);
|
|
632
|
+
logger.info("Prompt registered", { name: prompt.name });
|
|
633
|
+
if (sdkServer) {
|
|
634
|
+
sdkServer.sendPromptListChanged?.();
|
|
635
|
+
}
|
|
636
|
+
},
|
|
637
|
+
getPrompts() {
|
|
638
|
+
return Array.from(prompts.values()).map((p) => ({
|
|
639
|
+
name: p.name,
|
|
640
|
+
...p.description ? { description: p.description } : {},
|
|
641
|
+
arguments: p.arguments
|
|
642
|
+
}));
|
|
643
|
+
},
|
|
644
|
+
async getPrompt(promptName, args) {
|
|
645
|
+
const prompt = prompts.get(promptName);
|
|
646
|
+
if (!prompt) {
|
|
647
|
+
return Result.err(new McpError({
|
|
648
|
+
message: `Prompt not found: ${promptName}`,
|
|
649
|
+
code: -32601,
|
|
650
|
+
context: { prompt: promptName }
|
|
651
|
+
}));
|
|
652
|
+
}
|
|
653
|
+
for (const arg of prompt.arguments) {
|
|
654
|
+
if (arg.required && (args[arg.name] === undefined || args[arg.name] === "")) {
|
|
655
|
+
return Result.err(new McpError({
|
|
656
|
+
message: `Missing required argument: ${arg.name}`,
|
|
657
|
+
code: -32602,
|
|
658
|
+
context: { prompt: promptName, argument: arg.name }
|
|
659
|
+
}));
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
try {
|
|
663
|
+
const result = await prompt.handler(args);
|
|
664
|
+
if (result.isErr()) {
|
|
665
|
+
return Result.err(translateError(result.error));
|
|
666
|
+
}
|
|
667
|
+
return Result.ok(result.value);
|
|
668
|
+
} catch (error) {
|
|
669
|
+
return Result.err(new McpError({
|
|
670
|
+
message: error instanceof Error ? error.message : "Unknown error",
|
|
671
|
+
code: -32603,
|
|
672
|
+
context: { prompt: promptName, thrown: true }
|
|
673
|
+
}));
|
|
674
|
+
}
|
|
675
|
+
},
|
|
676
|
+
async readResource(uri) {
|
|
677
|
+
const resource = resources.get(uri);
|
|
678
|
+
if (resource) {
|
|
679
|
+
if (!resource.handler) {
|
|
680
|
+
return Result.err(new McpError({
|
|
681
|
+
message: `Resource not readable: ${uri}`,
|
|
682
|
+
code: -32002,
|
|
683
|
+
context: { uri }
|
|
684
|
+
}));
|
|
685
|
+
}
|
|
686
|
+
const requestId = generateRequestId();
|
|
687
|
+
const ctx = {
|
|
688
|
+
requestId,
|
|
689
|
+
logger: logger.child({ resource: uri, requestId }),
|
|
690
|
+
cwd: process.cwd(),
|
|
691
|
+
env: process.env
|
|
692
|
+
};
|
|
693
|
+
try {
|
|
694
|
+
const result = await resource.handler(uri, ctx);
|
|
695
|
+
if (result.isErr()) {
|
|
696
|
+
return Result.err(translateError(result.error));
|
|
697
|
+
}
|
|
698
|
+
return Result.ok(result.value);
|
|
699
|
+
} catch (error) {
|
|
700
|
+
return Result.err(new McpError({
|
|
701
|
+
message: error instanceof Error ? error.message : "Unknown error",
|
|
702
|
+
code: -32603,
|
|
703
|
+
context: { uri, thrown: true }
|
|
704
|
+
}));
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
for (const template of resourceTemplates.values()) {
|
|
708
|
+
const variables = matchUriTemplate(template.uriTemplate, uri);
|
|
709
|
+
if (variables) {
|
|
710
|
+
const templateRequestId = generateRequestId();
|
|
711
|
+
const templateCtx = {
|
|
712
|
+
requestId: templateRequestId,
|
|
713
|
+
logger: logger.child({
|
|
714
|
+
resource: uri,
|
|
715
|
+
requestId: templateRequestId
|
|
716
|
+
}),
|
|
717
|
+
cwd: process.cwd(),
|
|
718
|
+
env: process.env
|
|
719
|
+
};
|
|
720
|
+
try {
|
|
721
|
+
const result = await template.handler(uri, variables, templateCtx);
|
|
722
|
+
if (result.isErr()) {
|
|
723
|
+
return Result.err(translateError(result.error));
|
|
724
|
+
}
|
|
725
|
+
return Result.ok(result.value);
|
|
726
|
+
} catch (error) {
|
|
727
|
+
return Result.err(new McpError({
|
|
728
|
+
message: error instanceof Error ? error.message : "Unknown error",
|
|
729
|
+
code: -32603,
|
|
730
|
+
context: { uri, thrown: true }
|
|
731
|
+
}));
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
return Result.err(new McpError({
|
|
736
|
+
message: `Resource not found: ${uri}`,
|
|
737
|
+
code: -32002,
|
|
738
|
+
context: { uri }
|
|
739
|
+
}));
|
|
454
740
|
},
|
|
455
741
|
async invokeTool(toolName, input, invokeOptions) {
|
|
456
742
|
const requestId = invokeOptions?.requestId ?? generateRequestId();
|
|
@@ -481,7 +767,7 @@ function createMcpServer(options) {
|
|
|
481
767
|
}
|
|
482
768
|
}));
|
|
483
769
|
}
|
|
484
|
-
const ctx = createHandlerContext(toolName, requestId, invokeOptions?.signal);
|
|
770
|
+
const ctx = createHandlerContext(toolName, requestId, invokeOptions?.signal, invokeOptions?.progressToken);
|
|
485
771
|
try {
|
|
486
772
|
const result = await tool.handler(parseResult.data, ctx);
|
|
487
773
|
if (result.isErr()) {
|
|
@@ -513,6 +799,50 @@ function createMcpServer(options) {
|
|
|
513
799
|
}));
|
|
514
800
|
}
|
|
515
801
|
},
|
|
802
|
+
subscribe(uri) {
|
|
803
|
+
subscriptions.add(uri);
|
|
804
|
+
logger.debug("Resource subscription added", { uri });
|
|
805
|
+
},
|
|
806
|
+
unsubscribe(uri) {
|
|
807
|
+
subscriptions.delete(uri);
|
|
808
|
+
logger.debug("Resource subscription removed", { uri });
|
|
809
|
+
},
|
|
810
|
+
notifyResourceUpdated(uri) {
|
|
811
|
+
if (subscriptions.has(uri)) {
|
|
812
|
+
sdkServer?.sendResourceUpdated?.({ uri });
|
|
813
|
+
}
|
|
814
|
+
},
|
|
815
|
+
notifyToolsChanged() {
|
|
816
|
+
sdkServer?.sendToolListChanged?.();
|
|
817
|
+
},
|
|
818
|
+
notifyResourcesChanged() {
|
|
819
|
+
sdkServer?.sendResourceListChanged?.();
|
|
820
|
+
},
|
|
821
|
+
notifyPromptsChanged() {
|
|
822
|
+
sdkServer?.sendPromptListChanged?.();
|
|
823
|
+
},
|
|
824
|
+
setLogLevel(level) {
|
|
825
|
+
clientLogLevel = level;
|
|
826
|
+
logger.debug("Client log level set", { level });
|
|
827
|
+
},
|
|
828
|
+
sendLogMessage(level, data, loggerName) {
|
|
829
|
+
if (!sdkServer || clientLogLevel === null || !shouldEmitLog(level, clientLogLevel)) {
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
const params = {
|
|
833
|
+
level,
|
|
834
|
+
data
|
|
835
|
+
};
|
|
836
|
+
if (loggerName !== undefined) {
|
|
837
|
+
params.logger = loggerName;
|
|
838
|
+
}
|
|
839
|
+
sdkServer.sendLoggingMessage?.(params);
|
|
840
|
+
},
|
|
841
|
+
bindSdkServer(server2) {
|
|
842
|
+
sdkServer = server2;
|
|
843
|
+
clientLogLevel = resolveDefaultLogLevel(options);
|
|
844
|
+
logger.debug("SDK server bound for notifications");
|
|
845
|
+
},
|
|
516
846
|
async start() {
|
|
517
847
|
logger.info("MCP server starting", { name, version, tools: tools.size });
|
|
518
848
|
},
|
|
@@ -525,9 +855,44 @@ function createMcpServer(options) {
|
|
|
525
855
|
function defineTool(definition) {
|
|
526
856
|
return definition;
|
|
527
857
|
}
|
|
858
|
+
function escapeRegex(str) {
|
|
859
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
860
|
+
}
|
|
861
|
+
function matchUriTemplate(template, uri) {
|
|
862
|
+
const paramNames = [];
|
|
863
|
+
const parts = template.split(/(\{[^}]+\})/);
|
|
864
|
+
const regexSource = parts.map((part) => {
|
|
865
|
+
const paramMatch = part.match(/^\{([^}]+)\}$/);
|
|
866
|
+
if (paramMatch?.[1]) {
|
|
867
|
+
paramNames.push(paramMatch[1]);
|
|
868
|
+
return "([^/]+)";
|
|
869
|
+
}
|
|
870
|
+
return escapeRegex(part);
|
|
871
|
+
}).join("");
|
|
872
|
+
const regex = new RegExp(`^${regexSource}$`);
|
|
873
|
+
const match = uri.match(regex);
|
|
874
|
+
if (!match) {
|
|
875
|
+
return null;
|
|
876
|
+
}
|
|
877
|
+
const variables = {};
|
|
878
|
+
for (let i = 0;i < paramNames.length; i++) {
|
|
879
|
+
const name = paramNames[i];
|
|
880
|
+
const value = match[i + 1];
|
|
881
|
+
if (name !== undefined && value !== undefined) {
|
|
882
|
+
variables[name] = value;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
return variables;
|
|
886
|
+
}
|
|
528
887
|
function defineResource(definition) {
|
|
529
888
|
return definition;
|
|
530
889
|
}
|
|
890
|
+
function defineResourceTemplate(definition) {
|
|
891
|
+
return definition;
|
|
892
|
+
}
|
|
893
|
+
function definePrompt(definition) {
|
|
894
|
+
return definition;
|
|
895
|
+
}
|
|
531
896
|
|
|
532
897
|
// src/actions.ts
|
|
533
898
|
function isActionRegistry(source) {
|
|
@@ -694,7 +1059,17 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
|
694
1059
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
695
1060
|
import {
|
|
696
1061
|
CallToolRequestSchema,
|
|
697
|
-
|
|
1062
|
+
CompleteRequestSchema,
|
|
1063
|
+
GetPromptRequestSchema,
|
|
1064
|
+
ListPromptsRequestSchema,
|
|
1065
|
+
ListResourcesRequestSchema,
|
|
1066
|
+
ListResourceTemplatesRequestSchema,
|
|
1067
|
+
ListToolsRequestSchema,
|
|
1068
|
+
ReadResourceRequestSchema,
|
|
1069
|
+
McpError as SdkMcpError,
|
|
1070
|
+
SetLevelRequestSchema,
|
|
1071
|
+
SubscribeRequestSchema,
|
|
1072
|
+
UnsubscribeRequestSchema
|
|
698
1073
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
699
1074
|
import { safeStringify } from "@outfitter/contracts";
|
|
700
1075
|
function isMcpToolResponse(value) {
|
|
@@ -751,19 +1126,93 @@ function wrapToolError(error) {
|
|
|
751
1126
|
isError: true
|
|
752
1127
|
};
|
|
753
1128
|
}
|
|
1129
|
+
function toSdkError(error) {
|
|
1130
|
+
return new SdkMcpError(error.code, error.message, error.context);
|
|
1131
|
+
}
|
|
754
1132
|
function createSdkServer(server) {
|
|
755
|
-
const
|
|
1133
|
+
const capabilities = {
|
|
1134
|
+
tools: { listChanged: true }
|
|
1135
|
+
};
|
|
1136
|
+
if (server.getResources().length > 0 || server.getResourceTemplates().length > 0) {
|
|
1137
|
+
capabilities["resources"] = { listChanged: true, subscribe: true };
|
|
1138
|
+
}
|
|
1139
|
+
if (server.getPrompts().length > 0) {
|
|
1140
|
+
capabilities["prompts"] = { listChanged: true };
|
|
1141
|
+
}
|
|
1142
|
+
capabilities["completions"] = {};
|
|
1143
|
+
capabilities["logging"] = {};
|
|
1144
|
+
const sdkServer = new Server({ name: server.name, version: server.version }, { capabilities });
|
|
756
1145
|
sdkServer.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
757
1146
|
tools: server.getTools()
|
|
758
1147
|
}));
|
|
759
1148
|
sdkServer.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
760
1149
|
const { name, arguments: args } = request.params;
|
|
761
|
-
const
|
|
1150
|
+
const progressToken = request.params._meta?.progressToken;
|
|
1151
|
+
const options = progressToken !== undefined ? { progressToken } : undefined;
|
|
1152
|
+
const result = await server.invokeTool(name, args ?? {}, options);
|
|
762
1153
|
if (result.isErr()) {
|
|
763
1154
|
return wrapToolError(result.error);
|
|
764
1155
|
}
|
|
765
1156
|
return wrapToolResult(result.value);
|
|
766
1157
|
});
|
|
1158
|
+
sdkServer.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
|
1159
|
+
resources: server.getResources().map((r) => ({
|
|
1160
|
+
uri: r.uri,
|
|
1161
|
+
name: r.name,
|
|
1162
|
+
...r.description ? { description: r.description } : {},
|
|
1163
|
+
...r.mimeType ? { mimeType: r.mimeType } : {}
|
|
1164
|
+
}))
|
|
1165
|
+
}));
|
|
1166
|
+
sdkServer.setRequestHandler(ListResourceTemplatesRequestSchema, async () => ({
|
|
1167
|
+
resourceTemplates: server.getResourceTemplates().map((t) => ({
|
|
1168
|
+
uriTemplate: t.uriTemplate,
|
|
1169
|
+
name: t.name,
|
|
1170
|
+
...t.description ? { description: t.description } : {},
|
|
1171
|
+
...t.mimeType ? { mimeType: t.mimeType } : {}
|
|
1172
|
+
}))
|
|
1173
|
+
}));
|
|
1174
|
+
sdkServer.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
1175
|
+
const { uri } = request.params;
|
|
1176
|
+
const result = await server.readResource(uri);
|
|
1177
|
+
if (result.isErr()) {
|
|
1178
|
+
throw toSdkError(result.error);
|
|
1179
|
+
}
|
|
1180
|
+
return { contents: result.value };
|
|
1181
|
+
});
|
|
1182
|
+
sdkServer.setRequestHandler(SubscribeRequestSchema, async (request) => {
|
|
1183
|
+
server.subscribe(request.params.uri);
|
|
1184
|
+
return {};
|
|
1185
|
+
});
|
|
1186
|
+
sdkServer.setRequestHandler(UnsubscribeRequestSchema, async (request) => {
|
|
1187
|
+
server.unsubscribe(request.params.uri);
|
|
1188
|
+
return {};
|
|
1189
|
+
});
|
|
1190
|
+
sdkServer.setRequestHandler(ListPromptsRequestSchema, async () => ({
|
|
1191
|
+
prompts: server.getPrompts()
|
|
1192
|
+
}));
|
|
1193
|
+
sdkServer.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
1194
|
+
const { name, arguments: args } = request.params;
|
|
1195
|
+
const result = await server.getPrompt(name, args ?? {});
|
|
1196
|
+
if (result.isErr()) {
|
|
1197
|
+
throw toSdkError(result.error);
|
|
1198
|
+
}
|
|
1199
|
+
return { ...result.value };
|
|
1200
|
+
});
|
|
1201
|
+
sdkServer.setRequestHandler(CompleteRequestSchema, async (request) => {
|
|
1202
|
+
const { ref, argument } = request.params;
|
|
1203
|
+
const completionRef = ref.type === "ref/prompt" ? { type: "ref/prompt", name: ref.name } : { type: "ref/resource", uri: ref.uri };
|
|
1204
|
+
const result = await server.complete(completionRef, argument.name, argument.value);
|
|
1205
|
+
if (result.isErr()) {
|
|
1206
|
+
throw toSdkError(result.error);
|
|
1207
|
+
}
|
|
1208
|
+
return { completion: result.value };
|
|
1209
|
+
});
|
|
1210
|
+
sdkServer.setRequestHandler(SetLevelRequestSchema, async (request) => {
|
|
1211
|
+
const level = request.params.level;
|
|
1212
|
+
server.setLogLevel?.(level);
|
|
1213
|
+
return {};
|
|
1214
|
+
});
|
|
1215
|
+
server.bindSdkServer?.(sdkServer);
|
|
767
1216
|
return sdkServer;
|
|
768
1217
|
}
|
|
769
1218
|
async function connectStdio(server, transport = new StdioServerTransport) {
|
|
@@ -773,9 +1222,13 @@ async function connectStdio(server, transport = new StdioServerTransport) {
|
|
|
773
1222
|
}
|
|
774
1223
|
export {
|
|
775
1224
|
zodToJsonSchema,
|
|
1225
|
+
shouldEmitLog,
|
|
1226
|
+
mapLogLevelToMcp,
|
|
776
1227
|
defineTool,
|
|
1228
|
+
defineResourceTemplate,
|
|
777
1229
|
defineResource,
|
|
778
1230
|
defineQueryTool,
|
|
1231
|
+
definePrompt,
|
|
779
1232
|
defineDocsTool,
|
|
780
1233
|
defineConfigTool,
|
|
781
1234
|
createSdkServer,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@outfitter/mcp",
|
|
3
3
|
"description": "MCP server framework with typed tools for Outfitter",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.3.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
7
7
|
"dist"
|
|
@@ -40,8 +40,9 @@
|
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
43
|
-
"@outfitter/
|
|
44
|
-
"@outfitter/
|
|
43
|
+
"@outfitter/config": "0.3.0",
|
|
44
|
+
"@outfitter/contracts": "0.2.0",
|
|
45
|
+
"@outfitter/logging": "0.3.0",
|
|
45
46
|
"zod": "^4.3.5"
|
|
46
47
|
},
|
|
47
48
|
"devDependencies": {
|