@outfitter/mcp 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -103,6 +103,7 @@ interface ResourceDefinition {
103
103
  name: string; // Human-readable name
104
104
  description?: string; // Optional description
105
105
  mimeType?: string; // Content MIME type
106
+ handler?: ResourceReadHandler; // Optional resources/read handler
106
107
  }
107
108
 
108
109
  const configResource = defineResource({
@@ -110,9 +111,27 @@ const configResource = defineResource({
110
111
  name: "Application Config",
111
112
  description: "Main configuration file",
112
113
  mimeType: "application/json",
114
+ handler: async (uri, ctx) => {
115
+ ctx.logger.debug("Reading config resource", { uri });
116
+ return Result.ok([
117
+ {
118
+ uri,
119
+ mimeType: "application/json",
120
+ text: JSON.stringify({ debug: true }),
121
+ },
122
+ ]);
123
+ },
113
124
  });
114
125
  ```
115
126
 
127
+ Registered resources with handlers are exposed through MCP `resources/read`.
128
+
129
+ ```typescript
130
+ server.registerResource(configResource);
131
+
132
+ const contentResult = await server.readResource("file:///etc/app/config.json");
133
+ ```
134
+
116
135
  ### Server Methods
117
136
 
118
137
  ```typescript
@@ -123,12 +142,15 @@ interface McpServer {
123
142
  // Registration
124
143
  registerTool<TInput, TOutput, TError>(tool: ToolDefinition): void;
125
144
  registerResource(resource: ResourceDefinition): void;
145
+ registerResourceTemplate(template: ResourceTemplateDefinition): void;
126
146
 
127
147
  // Introspection
128
148
  getTools(): SerializedTool[];
129
149
  getResources(): ResourceDefinition[];
150
+ getResourceTemplates(): ResourceTemplateDefinition[];
130
151
 
131
152
  // Invocation
153
+ readResource(uri: string): Promise<Result<ResourceContent[], McpError>>;
132
154
  invokeTool<T>(name: string, input: unknown, options?: InvokeToolOptions): Promise<Result<T, McpError>>;
133
155
 
134
156
  // Lifecycle
package/dist/index.d.ts CHANGED
@@ -33,6 +33,24 @@ interface McpServerOptions {
33
33
  logger?: Logger;
34
34
  }
35
35
  /**
36
+ * Behavioral hints for MCP tools.
37
+ *
38
+ * Annotations help clients understand tool behavior without invoking them.
39
+ * All fields are optional — only include hints that apply.
40
+ *
41
+ * @see https://spec.modelcontextprotocol.io/specification/2025-03-26/server/tools/#annotations
42
+ */
43
+ interface ToolAnnotations {
44
+ /** When true, the tool does not modify any state. */
45
+ readOnlyHint?: boolean;
46
+ /** When true, the tool may perform destructive operations (e.g., deleting data). */
47
+ destructiveHint?: boolean;
48
+ /** When true, calling the tool multiple times with the same input has the same effect. */
49
+ idempotentHint?: boolean;
50
+ /** When true, the tool may interact with external systems beyond the server. */
51
+ openWorldHint?: boolean;
52
+ }
53
+ /**
36
54
  * Definition of an MCP tool that can be invoked by clients.
37
55
  *
38
56
  * Tools are the primary way clients interact with MCP servers.
@@ -94,6 +112,11 @@ interface ToolDefinition<
94
112
  */
95
113
  inputSchema: z.ZodType<TInput>;
96
114
  /**
115
+ * Optional behavioral annotations for the tool.
116
+ * Helps clients understand tool behavior without invoking it.
117
+ */
118
+ annotations?: ToolAnnotations;
119
+ /**
97
120
  * Handler function that processes the tool invocation.
98
121
  * Receives validated input and HandlerContext, returns Result.
99
122
  */
@@ -112,8 +135,66 @@ interface SerializedTool {
112
135
  inputSchema: Record<string, unknown>;
113
136
  /** MCP tool-search hint: whether tool is deferred */
114
137
  defer_loading?: boolean;
138
+ /** Behavioral annotations for the tool */
139
+ annotations?: ToolAnnotations;
115
140
  }
116
141
  /**
142
+ * Annotations for content items (resource content, prompt messages).
143
+ *
144
+ * Provides hints about content audience and priority.
145
+ *
146
+ * @see https://spec.modelcontextprotocol.io/specification/2025-03-26/server/utilities/annotations/
147
+ */
148
+ interface ContentAnnotations {
149
+ /**
150
+ * Who the content is intended for.
151
+ * Can include "user", "assistant", or both.
152
+ */
153
+ audience?: Array<"user" | "assistant">;
154
+ /**
155
+ * Priority level from 0.0 (least) to 1.0 (most important).
156
+ */
157
+ priority?: number;
158
+ }
159
+ /**
160
+ * Text content returned from a resource read.
161
+ */
162
+ interface TextResourceContent {
163
+ /** Resource URI */
164
+ uri: string;
165
+ /** Text content */
166
+ text: string;
167
+ /** Optional MIME type */
168
+ mimeType?: string;
169
+ /** Optional content annotations */
170
+ annotations?: ContentAnnotations;
171
+ }
172
+ /**
173
+ * Binary (base64-encoded) content returned from a resource read.
174
+ */
175
+ interface BlobResourceContent {
176
+ /** Resource URI */
177
+ uri: string;
178
+ /** Base64-encoded binary content */
179
+ blob: string;
180
+ /** Optional MIME type */
181
+ mimeType?: string;
182
+ /** Optional content annotations */
183
+ annotations?: ContentAnnotations;
184
+ }
185
+ /**
186
+ * Content returned from reading a resource.
187
+ */
188
+ type ResourceContent = TextResourceContent | BlobResourceContent;
189
+ /**
190
+ * Handler for reading a resource's content.
191
+ *
192
+ * @param uri - The resource URI being read
193
+ * @param ctx - Handler context with logger and requestId
194
+ * @returns Array of resource content items
195
+ */
196
+ type ResourceReadHandler = (uri: string, ctx: HandlerContext) => Promise<Result<ResourceContent[], OutfitterError>>;
197
+ /**
117
198
  * Definition of an MCP resource that can be read by clients.
118
199
  *
119
200
  * Resources represent data that clients can access, such as files,
@@ -126,6 +207,10 @@ interface SerializedTool {
126
207
  * name: "Application Config",
127
208
  * description: "Main application configuration file",
128
209
  * mimeType: "application/json",
210
+ * handler: async (uri, ctx) => {
211
+ * const content = await readFile(uri);
212
+ * return Result.ok([{ uri, text: content }]);
213
+ * },
129
214
  * };
130
215
  * ```
131
216
  */
@@ -150,6 +235,151 @@ interface ResourceDefinition {
150
235
  * Helps clients understand how to process the resource.
151
236
  */
152
237
  mimeType?: string;
238
+ /**
239
+ * Optional handler for reading the resource content.
240
+ * If not provided, the resource is metadata-only.
241
+ */
242
+ handler?: ResourceReadHandler;
243
+ }
244
+ /**
245
+ * Handler for reading a resource template's content.
246
+ *
247
+ * @param uri - The matched URI
248
+ * @param variables - Extracted template variables
249
+ * @param ctx - Handler context
250
+ */
251
+ type ResourceTemplateReadHandler = (uri: string, variables: Record<string, string>, ctx: HandlerContext) => Promise<Result<ResourceContent[], OutfitterError>>;
252
+ /**
253
+ * Definition of an MCP resource template with URI pattern matching.
254
+ *
255
+ * Templates use RFC 6570 Level 1 URI templates (e.g., `{param}`)
256
+ * to match and extract variables from URIs.
257
+ *
258
+ * @example
259
+ * ```typescript
260
+ * const userTemplate: ResourceTemplateDefinition = {
261
+ * uriTemplate: "db:///users/{userId}/profile",
262
+ * name: "User Profile",
263
+ * handler: async (uri, variables) => {
264
+ * const profile = await getProfile(variables.userId);
265
+ * return Result.ok([{ uri, text: JSON.stringify(profile) }]);
266
+ * },
267
+ * };
268
+ * ```
269
+ */
270
+ interface ResourceTemplateDefinition {
271
+ /** URI template with `{param}` placeholders (RFC 6570 Level 1). */
272
+ uriTemplate: string;
273
+ /** Human-readable name for the template. */
274
+ name: string;
275
+ /** Optional description. */
276
+ description?: string;
277
+ /** Optional MIME type. */
278
+ mimeType?: string;
279
+ /** Optional completion handlers keyed by parameter name. */
280
+ complete?: Record<string, CompletionHandler>;
281
+ /** Handler for reading matched resources. */
282
+ handler: ResourceTemplateReadHandler;
283
+ }
284
+ /**
285
+ * Result of a completion request.
286
+ */
287
+ interface CompletionResult {
288
+ /** Completion values */
289
+ values: string[];
290
+ /** Total number of available values (for pagination) */
291
+ total?: number;
292
+ /** Whether there are more values */
293
+ hasMore?: boolean;
294
+ }
295
+ /**
296
+ * Handler for generating completions.
297
+ */
298
+ type CompletionHandler = (value: string) => Promise<CompletionResult>;
299
+ /**
300
+ * Reference to a prompt or resource for completion.
301
+ */
302
+ type CompletionRef = {
303
+ type: "ref/prompt";
304
+ name: string;
305
+ } | {
306
+ type: "ref/resource";
307
+ uri: string;
308
+ };
309
+ /**
310
+ * Argument definition for a prompt.
311
+ */
312
+ interface PromptArgument {
313
+ /** Argument name */
314
+ name: string;
315
+ /** Human-readable description */
316
+ description?: string;
317
+ /** Whether this argument is required */
318
+ required?: boolean;
319
+ /** Optional completion handler for this argument */
320
+ complete?: CompletionHandler;
321
+ }
322
+ /**
323
+ * Content block within a prompt message.
324
+ */
325
+ interface PromptMessageContent {
326
+ /** Content type */
327
+ type: "text";
328
+ /** Text content */
329
+ text: string;
330
+ /** Optional content annotations */
331
+ annotations?: ContentAnnotations;
332
+ }
333
+ /**
334
+ * A message in a prompt response.
335
+ */
336
+ interface PromptMessage {
337
+ /** Message role */
338
+ role: "user" | "assistant";
339
+ /** Message content */
340
+ content: PromptMessageContent;
341
+ }
342
+ /**
343
+ * Result returned from getting a prompt.
344
+ */
345
+ interface PromptResult {
346
+ /** Prompt messages */
347
+ messages: PromptMessage[];
348
+ /** Optional description override */
349
+ description?: string;
350
+ }
351
+ /**
352
+ * Handler for generating prompt messages.
353
+ */
354
+ type PromptHandler = (args: Record<string, string | undefined>) => Promise<Result<PromptResult, OutfitterError>>;
355
+ /**
356
+ * Definition of an MCP prompt.
357
+ *
358
+ * Prompts are reusable templates that generate messages for LLMs.
359
+ *
360
+ * @example
361
+ * ```typescript
362
+ * const reviewPrompt: PromptDefinition = {
363
+ * name: "code-review",
364
+ * description: "Review code changes",
365
+ * arguments: [
366
+ * { name: "language", description: "Programming language", required: true },
367
+ * ],
368
+ * handler: async (args) => Result.ok({
369
+ * messages: [{ role: "user", content: { type: "text", text: `Review this ${args.language} code` } }],
370
+ * }),
371
+ * };
372
+ * ```
373
+ */
374
+ interface PromptDefinition {
375
+ /** Unique prompt name */
376
+ name: string;
377
+ /** Human-readable description */
378
+ description?: string;
379
+ /** Prompt arguments */
380
+ arguments: PromptArgument[];
381
+ /** Handler to generate messages */
382
+ handler: PromptHandler;
153
383
  }
154
384
  declare const McpErrorBase: TaggedErrorClass<"McpError", {
155
385
  message: string;
@@ -191,6 +421,8 @@ interface InvokeToolOptions {
191
421
  signal?: AbortSignal;
192
422
  /** Custom request ID (auto-generated if not provided) */
193
423
  requestId?: string;
424
+ /** Progress token from client for tracking progress */
425
+ progressToken?: string | number;
194
426
  }
195
427
  /**
196
428
  * MCP Server instance.
@@ -236,11 +468,56 @@ interface McpServer {
236
468
  */
237
469
  getTools(): SerializedTool[];
238
470
  /**
471
+ * Register a resource template with the server.
472
+ * @param template - Resource template definition to register
473
+ */
474
+ registerResourceTemplate(template: ResourceTemplateDefinition): void;
475
+ /**
239
476
  * Get all registered resources.
240
477
  * @returns Array of resource definitions
241
478
  */
242
479
  getResources(): ResourceDefinition[];
243
480
  /**
481
+ * Get all registered resource templates.
482
+ * @returns Array of resource template definitions
483
+ */
484
+ getResourceTemplates(): ResourceTemplateDefinition[];
485
+ /**
486
+ * Complete an argument value.
487
+ * @param ref - Reference to the prompt or resource template
488
+ * @param argumentName - Name of the argument to complete
489
+ * @param value - Current value to complete
490
+ * @returns Result with completion values or McpError
491
+ */
492
+ complete(ref: CompletionRef, argumentName: string, value: string): Promise<Result<CompletionResult, InstanceType<typeof McpError>>>;
493
+ /**
494
+ * Register a prompt with the server.
495
+ * @param prompt - Prompt definition to register
496
+ */
497
+ registerPrompt(prompt: PromptDefinition): void;
498
+ /**
499
+ * Get all registered prompts.
500
+ * @returns Array of prompt definitions (without handlers)
501
+ */
502
+ getPrompts(): Array<{
503
+ name: string;
504
+ description?: string;
505
+ arguments: PromptArgument[];
506
+ }>;
507
+ /**
508
+ * Get a specific prompt's messages.
509
+ * @param name - Prompt name
510
+ * @param args - Prompt arguments
511
+ * @returns Result with prompt result or McpError
512
+ */
513
+ getPrompt(name: string, args: Record<string, string | undefined>): Promise<Result<PromptResult, InstanceType<typeof McpError>>>;
514
+ /**
515
+ * Read a resource by URI.
516
+ * @param uri - Resource URI
517
+ * @returns Result with resource content or McpError
518
+ */
519
+ readResource(uri: string): Promise<Result<ResourceContent[], InstanceType<typeof McpError>>>;
520
+ /**
244
521
  * Invoke a tool by name.
245
522
  * @param name - Tool name
246
523
  * @param input - Tool input (will be validated)
@@ -249,6 +526,46 @@ interface McpServer {
249
526
  */
250
527
  invokeTool<T = unknown>(name: string, input: unknown, options?: InvokeToolOptions): Promise<Result<T, InstanceType<typeof McpError>>>;
251
528
  /**
529
+ * Subscribe to updates for a resource URI.
530
+ * @param uri - Resource URI to subscribe to
531
+ */
532
+ subscribe(uri: string): void;
533
+ /**
534
+ * Unsubscribe from updates for a resource URI.
535
+ * @param uri - Resource URI to unsubscribe from
536
+ */
537
+ unsubscribe(uri: string): void;
538
+ /**
539
+ * Notify connected clients that a specific resource has been updated.
540
+ * Only emits for subscribed URIs.
541
+ * @param uri - URI of the updated resource
542
+ */
543
+ notifyResourceUpdated(uri: string): void;
544
+ /**
545
+ * Notify connected clients that the tool list has changed.
546
+ */
547
+ notifyToolsChanged(): void;
548
+ /**
549
+ * Notify connected clients that the resource list has changed.
550
+ */
551
+ notifyResourcesChanged(): void;
552
+ /**
553
+ * Notify connected clients that the prompt list has changed.
554
+ */
555
+ notifyPromptsChanged(): void;
556
+ /**
557
+ * Set the client-requested log level.
558
+ * Only log messages at or above this level will be forwarded.
559
+ * @param level - MCP log level string
560
+ */
561
+ setLogLevel?(level: string): void;
562
+ /**
563
+ * Bind the SDK server instance for notifications.
564
+ * Called internally by the transport layer.
565
+ * @param sdkServer - The MCP SDK Server instance
566
+ */
567
+ bindSdkServer?(sdkServer: any): void;
568
+ /**
252
569
  * Start the MCP server.
253
570
  * Begins listening for client connections.
254
571
  */
@@ -260,12 +577,26 @@ interface McpServer {
260
577
  stop(): Promise<void>;
261
578
  }
262
579
  /**
580
+ * Reporter for sending progress updates to clients.
581
+ */
582
+ interface ProgressReporter {
583
+ /**
584
+ * Report progress for the current operation.
585
+ * @param progress - Current progress value
586
+ * @param total - Optional total value (for percentage calculation)
587
+ * @param message - Optional human-readable status message
588
+ */
589
+ report(progress: number, total?: number, message?: string): void;
590
+ }
591
+ /**
263
592
  * Extended handler context for MCP tools.
264
593
  * Includes MCP-specific information in addition to standard HandlerContext.
265
594
  */
266
595
  interface McpHandlerContext extends HandlerContext {
267
596
  /** The name of the tool being invoked */
268
597
  toolName?: string;
598
+ /** Progress reporter, present when client provides a progressToken */
599
+ progress?: ProgressReporter;
269
600
  }
270
601
  interface BuildMcpToolsOptions {
271
602
  readonly includeSurfaces?: readonly ActionSurface[];
@@ -366,6 +697,32 @@ interface CoreToolsOptions {
366
697
  type NormalizedQueryInput = Required<Pick<QueryToolInput, "q">> & Omit<QueryToolInput, "q">;
367
698
  type CoreToolDefinition = ToolDefinition<DocsToolInput, DocsToolResponse> | ToolDefinition<ConfigToolInput, ConfigToolResponse> | ToolDefinition<QueryToolInput, QueryToolResponse>;
368
699
  declare function createCoreTools(options?: CoreToolsOptions): CoreToolDefinition[];
700
+ /**
701
+ * @outfitter/mcp - Logging Bridge
702
+ *
703
+ * Maps Outfitter log levels to MCP log levels for
704
+ * server-to-client log message notifications.
705
+ *
706
+ * @packageDocumentation
707
+ */
708
+ /**
709
+ * MCP log levels as defined in the MCP specification.
710
+ * Ordered from least to most severe.
711
+ */
712
+ type McpLogLevel = "debug" | "info" | "notice" | "warning" | "error" | "critical" | "alert" | "emergency";
713
+ /**
714
+ * Outfitter log levels.
715
+ */
716
+ type OutfitterLogLevel = "trace" | "debug" | "info" | "warn" | "error" | "fatal";
717
+ /**
718
+ * Map an Outfitter log level to the corresponding MCP log level.
719
+ */
720
+ declare function mapLogLevelToMcp(level: OutfitterLogLevel): McpLogLevel;
721
+ /**
722
+ * Check whether a message at the given level should be emitted
723
+ * based on the client-requested threshold.
724
+ */
725
+ declare function shouldEmitLog(messageLevel: McpLogLevel, threshold: McpLogLevel): boolean;
369
726
  import { z as z2 } from "zod";
370
727
  /**
371
728
  * JSON Schema representation.
@@ -507,6 +864,26 @@ declare function defineTool<
507
864
  * ```
508
865
  */
509
866
  declare function defineResource(definition: ResourceDefinition): ResourceDefinition;
867
+ /**
868
+ * Define a resource template.
869
+ *
870
+ * Helper function for creating resource template definitions
871
+ * with URI pattern matching.
872
+ *
873
+ * @param definition - Resource template definition object
874
+ * @returns The same resource template definition
875
+ */
876
+ declare function defineResourceTemplate(definition: ResourceTemplateDefinition): ResourceTemplateDefinition;
877
+ /**
878
+ * Define a prompt.
879
+ *
880
+ * Helper function for creating prompt definitions
881
+ * with consistent typing.
882
+ *
883
+ * @param definition - Prompt definition object
884
+ * @returns The same prompt definition
885
+ */
886
+ declare function definePrompt(definition: PromptDefinition): PromptDefinition;
510
887
  import { Server } from "@modelcontextprotocol/sdk/server/index";
511
888
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio";
512
889
  import { CallToolResult } from "@modelcontextprotocol/sdk/types";
@@ -519,4 +896,4 @@ declare function createSdkServer(server: McpServer): Server;
519
896
  * Connect an MCP server over stdio transport.
520
897
  */
521
898
  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 };
899
+ 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
@@ -357,14 +357,13 @@ class McpError extends McpErrorBase {
357
357
 
358
358
  // src/server.ts
359
359
  function createNoOpLogger() {
360
- const noop = () => {};
361
360
  return {
362
- trace: noop,
363
- debug: noop,
364
- info: noop,
365
- warn: noop,
366
- error: noop,
367
- fatal: noop,
361
+ trace: (..._args) => {},
362
+ debug: (..._args) => {},
363
+ info: (..._args) => {},
364
+ warn: (..._args) => {},
365
+ error: (..._args) => {},
366
+ fatal: (..._args) => {},
368
367
  child: () => createNoOpLogger()
369
368
  };
370
369
  }
@@ -372,8 +371,12 @@ function createMcpServer(options) {
372
371
  const { name, version, logger: providedLogger } = options;
373
372
  const logger = providedLogger ?? createNoOpLogger();
374
373
  const tools = new Map;
375
- const resources = [];
376
- function createHandlerContext(toolName, requestId, signal) {
374
+ const resources = new Map;
375
+ const resourceTemplates = new Map;
376
+ const prompts = new Map;
377
+ let sdkServer = null;
378
+ const subscriptions = new Set;
379
+ function createHandlerContext(toolName, requestId, signal, progressToken) {
377
380
  const ctx = {
378
381
  requestId,
379
382
  logger: logger.child({ tool: toolName, requestId }),
@@ -383,6 +386,21 @@ function createMcpServer(options) {
383
386
  if (signal !== undefined) {
384
387
  ctx.signal = signal;
385
388
  }
389
+ if (progressToken !== undefined && sdkServer) {
390
+ ctx.progress = {
391
+ report(progress, total, message) {
392
+ sdkServer?.notification?.({
393
+ method: "notifications/progress",
394
+ params: {
395
+ progressToken,
396
+ progress,
397
+ ...total !== undefined ? { total } : {},
398
+ ...message ? { message } : {}
399
+ }
400
+ });
401
+ }
402
+ };
403
+ }
386
404
  return ctx;
387
405
  }
388
406
  function translateError(error) {
@@ -423,34 +441,232 @@ function createMcpServer(options) {
423
441
  const jsonSchema = zodToJsonSchema(tool.inputSchema);
424
442
  const handler = (input, ctx) => tool.handler(input, ctx);
425
443
  const deferLoading = tool.deferLoading ?? true;
426
- tools.set(tool.name, {
444
+ const stored = {
427
445
  name: tool.name,
428
446
  description,
429
447
  inputSchema: jsonSchema,
430
448
  deferLoading,
431
449
  handler,
432
450
  zodSchema: tool.inputSchema
433
- });
451
+ };
452
+ if (tool.annotations !== undefined) {
453
+ stored.annotations = tool.annotations;
454
+ }
455
+ tools.set(tool.name, stored);
434
456
  logger.info("Tool registered", { name: tool.name });
457
+ if (sdkServer) {
458
+ sdkServer.sendToolListChanged?.();
459
+ }
435
460
  },
436
461
  registerResource(resource) {
437
462
  logger.debug("Registering resource", {
438
463
  uri: resource.uri,
439
464
  name: resource.name
440
465
  });
441
- resources.push(resource);
466
+ resources.set(resource.uri, resource);
442
467
  logger.info("Resource registered", { uri: resource.uri });
468
+ if (sdkServer) {
469
+ sdkServer.sendResourceListChanged?.();
470
+ }
471
+ },
472
+ registerResourceTemplate(template) {
473
+ logger.debug("Registering resource template", {
474
+ uriTemplate: template.uriTemplate,
475
+ name: template.name
476
+ });
477
+ resourceTemplates.set(template.uriTemplate, template);
478
+ logger.info("Resource template registered", {
479
+ uriTemplate: template.uriTemplate
480
+ });
481
+ if (sdkServer) {
482
+ sdkServer.sendResourceListChanged?.();
483
+ }
443
484
  },
444
485
  getTools() {
445
486
  return Array.from(tools.values()).map((tool) => ({
446
487
  name: tool.name,
447
488
  description: tool.description,
448
489
  inputSchema: tool.inputSchema,
449
- defer_loading: tool.deferLoading
490
+ defer_loading: tool.deferLoading,
491
+ ...tool.annotations ? { annotations: tool.annotations } : {}
450
492
  }));
451
493
  },
452
494
  getResources() {
453
- return [...resources];
495
+ return Array.from(resources.values());
496
+ },
497
+ getResourceTemplates() {
498
+ return Array.from(resourceTemplates.values());
499
+ },
500
+ async complete(ref, argumentName, value) {
501
+ if (ref.type === "ref/prompt") {
502
+ const prompt = prompts.get(ref.name);
503
+ if (!prompt) {
504
+ return Result.err(new McpError({
505
+ message: `Prompt not found: ${ref.name}`,
506
+ code: -32601,
507
+ context: { prompt: ref.name }
508
+ }));
509
+ }
510
+ const arg = prompt.arguments.find((a) => a.name === argumentName);
511
+ if (!arg?.complete) {
512
+ return Result.ok({ values: [] });
513
+ }
514
+ try {
515
+ const result = await arg.complete(value);
516
+ return Result.ok(result);
517
+ } catch (error) {
518
+ return Result.err(new McpError({
519
+ message: error instanceof Error ? error.message : "Unknown error",
520
+ code: -32603,
521
+ context: {
522
+ prompt: ref.name,
523
+ argument: argumentName,
524
+ thrown: true
525
+ }
526
+ }));
527
+ }
528
+ }
529
+ if (ref.type === "ref/resource") {
530
+ const template = resourceTemplates.get(ref.uri);
531
+ if (!template) {
532
+ return Result.err(new McpError({
533
+ message: `Resource template not found: ${ref.uri}`,
534
+ code: -32601,
535
+ context: { uri: ref.uri }
536
+ }));
537
+ }
538
+ const handler = template.complete?.[argumentName];
539
+ if (!handler) {
540
+ return Result.ok({ values: [] });
541
+ }
542
+ try {
543
+ const result = await handler(value);
544
+ return Result.ok(result);
545
+ } catch (error) {
546
+ return Result.err(new McpError({
547
+ message: error instanceof Error ? error.message : "Unknown error",
548
+ code: -32603,
549
+ context: { uri: ref.uri, argument: argumentName, thrown: true }
550
+ }));
551
+ }
552
+ }
553
+ return Result.err(new McpError({
554
+ message: "Invalid completion reference type",
555
+ code: -32602,
556
+ context: { ref }
557
+ }));
558
+ },
559
+ registerPrompt(prompt) {
560
+ logger.debug("Registering prompt", { name: prompt.name });
561
+ prompts.set(prompt.name, prompt);
562
+ logger.info("Prompt registered", { name: prompt.name });
563
+ if (sdkServer) {
564
+ sdkServer.sendPromptListChanged?.();
565
+ }
566
+ },
567
+ getPrompts() {
568
+ return Array.from(prompts.values()).map((p) => ({
569
+ name: p.name,
570
+ ...p.description ? { description: p.description } : {},
571
+ arguments: p.arguments
572
+ }));
573
+ },
574
+ async getPrompt(promptName, args) {
575
+ const prompt = prompts.get(promptName);
576
+ if (!prompt) {
577
+ return Result.err(new McpError({
578
+ message: `Prompt not found: ${promptName}`,
579
+ code: -32601,
580
+ context: { prompt: promptName }
581
+ }));
582
+ }
583
+ for (const arg of prompt.arguments) {
584
+ if (arg.required && (args[arg.name] === undefined || args[arg.name] === "")) {
585
+ return Result.err(new McpError({
586
+ message: `Missing required argument: ${arg.name}`,
587
+ code: -32602,
588
+ context: { prompt: promptName, argument: arg.name }
589
+ }));
590
+ }
591
+ }
592
+ try {
593
+ const result = await prompt.handler(args);
594
+ if (result.isErr()) {
595
+ return Result.err(translateError(result.error));
596
+ }
597
+ return Result.ok(result.value);
598
+ } catch (error) {
599
+ return Result.err(new McpError({
600
+ message: error instanceof Error ? error.message : "Unknown error",
601
+ code: -32603,
602
+ context: { prompt: promptName, thrown: true }
603
+ }));
604
+ }
605
+ },
606
+ async readResource(uri) {
607
+ const resource = resources.get(uri);
608
+ if (resource) {
609
+ if (!resource.handler) {
610
+ return Result.err(new McpError({
611
+ message: `Resource not readable: ${uri}`,
612
+ code: -32002,
613
+ context: { uri }
614
+ }));
615
+ }
616
+ const requestId = generateRequestId();
617
+ const ctx = {
618
+ requestId,
619
+ logger: logger.child({ resource: uri, requestId }),
620
+ cwd: process.cwd(),
621
+ env: process.env
622
+ };
623
+ try {
624
+ const result = await resource.handler(uri, ctx);
625
+ if (result.isErr()) {
626
+ return Result.err(translateError(result.error));
627
+ }
628
+ return Result.ok(result.value);
629
+ } catch (error) {
630
+ return Result.err(new McpError({
631
+ message: error instanceof Error ? error.message : "Unknown error",
632
+ code: -32603,
633
+ context: { uri, thrown: true }
634
+ }));
635
+ }
636
+ }
637
+ for (const template of resourceTemplates.values()) {
638
+ const variables = matchUriTemplate(template.uriTemplate, uri);
639
+ if (variables) {
640
+ const templateRequestId = generateRequestId();
641
+ const templateCtx = {
642
+ requestId: templateRequestId,
643
+ logger: logger.child({
644
+ resource: uri,
645
+ requestId: templateRequestId
646
+ }),
647
+ cwd: process.cwd(),
648
+ env: process.env
649
+ };
650
+ try {
651
+ const result = await template.handler(uri, variables, templateCtx);
652
+ if (result.isErr()) {
653
+ return Result.err(translateError(result.error));
654
+ }
655
+ return Result.ok(result.value);
656
+ } catch (error) {
657
+ return Result.err(new McpError({
658
+ message: error instanceof Error ? error.message : "Unknown error",
659
+ code: -32603,
660
+ context: { uri, thrown: true }
661
+ }));
662
+ }
663
+ }
664
+ }
665
+ return Result.err(new McpError({
666
+ message: `Resource not found: ${uri}`,
667
+ code: -32002,
668
+ context: { uri }
669
+ }));
454
670
  },
455
671
  async invokeTool(toolName, input, invokeOptions) {
456
672
  const requestId = invokeOptions?.requestId ?? generateRequestId();
@@ -481,7 +697,7 @@ function createMcpServer(options) {
481
697
  }
482
698
  }));
483
699
  }
484
- const ctx = createHandlerContext(toolName, requestId, invokeOptions?.signal);
700
+ const ctx = createHandlerContext(toolName, requestId, invokeOptions?.signal, invokeOptions?.progressToken);
485
701
  try {
486
702
  const result = await tool.handler(parseResult.data, ctx);
487
703
  if (result.isErr()) {
@@ -513,6 +729,35 @@ function createMcpServer(options) {
513
729
  }));
514
730
  }
515
731
  },
732
+ subscribe(uri) {
733
+ subscriptions.add(uri);
734
+ logger.debug("Resource subscription added", { uri });
735
+ },
736
+ unsubscribe(uri) {
737
+ subscriptions.delete(uri);
738
+ logger.debug("Resource subscription removed", { uri });
739
+ },
740
+ notifyResourceUpdated(uri) {
741
+ if (subscriptions.has(uri)) {
742
+ sdkServer?.sendResourceUpdated?.({ uri });
743
+ }
744
+ },
745
+ notifyToolsChanged() {
746
+ sdkServer?.sendToolListChanged?.();
747
+ },
748
+ notifyResourcesChanged() {
749
+ sdkServer?.sendResourceListChanged?.();
750
+ },
751
+ notifyPromptsChanged() {
752
+ sdkServer?.sendPromptListChanged?.();
753
+ },
754
+ setLogLevel(level) {
755
+ logger.debug("Client log level set", { level });
756
+ },
757
+ bindSdkServer(server2) {
758
+ sdkServer = server2;
759
+ logger.debug("SDK server bound for notifications");
760
+ },
516
761
  async start() {
517
762
  logger.info("MCP server starting", { name, version, tools: tools.size });
518
763
  },
@@ -525,9 +770,44 @@ function createMcpServer(options) {
525
770
  function defineTool(definition) {
526
771
  return definition;
527
772
  }
773
+ function escapeRegex(str) {
774
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
775
+ }
776
+ function matchUriTemplate(template, uri) {
777
+ const paramNames = [];
778
+ const parts = template.split(/(\{[^}]+\})/);
779
+ const regexSource = parts.map((part) => {
780
+ const paramMatch = part.match(/^\{([^}]+)\}$/);
781
+ if (paramMatch?.[1]) {
782
+ paramNames.push(paramMatch[1]);
783
+ return "([^/]+)";
784
+ }
785
+ return escapeRegex(part);
786
+ }).join("");
787
+ const regex = new RegExp(`^${regexSource}$`);
788
+ const match = uri.match(regex);
789
+ if (!match) {
790
+ return null;
791
+ }
792
+ const variables = {};
793
+ for (let i = 0;i < paramNames.length; i++) {
794
+ const name = paramNames[i];
795
+ const value = match[i + 1];
796
+ if (name !== undefined && value !== undefined) {
797
+ variables[name] = value;
798
+ }
799
+ }
800
+ return variables;
801
+ }
528
802
  function defineResource(definition) {
529
803
  return definition;
530
804
  }
805
+ function defineResourceTemplate(definition) {
806
+ return definition;
807
+ }
808
+ function definePrompt(definition) {
809
+ return definition;
810
+ }
531
811
 
532
812
  // src/actions.ts
533
813
  function isActionRegistry(source) {
@@ -689,12 +969,55 @@ function createCoreTools(options = {}) {
689
969
  defineQueryTool(options.query)
690
970
  ];
691
971
  }
972
+ // src/logging.ts
973
+ var MCP_LEVEL_ORDER = [
974
+ "debug",
975
+ "info",
976
+ "notice",
977
+ "warning",
978
+ "error",
979
+ "critical",
980
+ "alert",
981
+ "emergency"
982
+ ];
983
+ function mapLogLevelToMcp(level) {
984
+ switch (level) {
985
+ case "trace":
986
+ case "debug":
987
+ return "debug";
988
+ case "info":
989
+ return "info";
990
+ case "warn":
991
+ return "warning";
992
+ case "error":
993
+ return "error";
994
+ case "fatal":
995
+ return "emergency";
996
+ default: {
997
+ const _exhaustiveCheck = level;
998
+ return _exhaustiveCheck;
999
+ }
1000
+ }
1001
+ }
1002
+ function shouldEmitLog(messageLevel, threshold) {
1003
+ return MCP_LEVEL_ORDER.indexOf(messageLevel) >= MCP_LEVEL_ORDER.indexOf(threshold);
1004
+ }
692
1005
  // src/transport.ts
693
1006
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
694
1007
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
695
1008
  import {
696
1009
  CallToolRequestSchema,
697
- ListToolsRequestSchema
1010
+ CompleteRequestSchema,
1011
+ GetPromptRequestSchema,
1012
+ ListPromptsRequestSchema,
1013
+ ListResourcesRequestSchema,
1014
+ ListResourceTemplatesRequestSchema,
1015
+ ListToolsRequestSchema,
1016
+ ReadResourceRequestSchema,
1017
+ McpError as SdkMcpError,
1018
+ SetLevelRequestSchema,
1019
+ SubscribeRequestSchema,
1020
+ UnsubscribeRequestSchema
698
1021
  } from "@modelcontextprotocol/sdk/types.js";
699
1022
  import { safeStringify } from "@outfitter/contracts";
700
1023
  function isMcpToolResponse(value) {
@@ -751,19 +1074,93 @@ function wrapToolError(error) {
751
1074
  isError: true
752
1075
  };
753
1076
  }
1077
+ function toSdkError(error) {
1078
+ return new SdkMcpError(error.code, error.message, error.context);
1079
+ }
754
1080
  function createSdkServer(server) {
755
- const sdkServer = new Server({ name: server.name, version: server.version }, { capabilities: { tools: {} } });
1081
+ const capabilities = {
1082
+ tools: { listChanged: true }
1083
+ };
1084
+ if (server.getResources().length > 0 || server.getResourceTemplates().length > 0) {
1085
+ capabilities["resources"] = { listChanged: true, subscribe: true };
1086
+ }
1087
+ if (server.getPrompts().length > 0) {
1088
+ capabilities["prompts"] = { listChanged: true };
1089
+ }
1090
+ capabilities["completions"] = {};
1091
+ capabilities["logging"] = {};
1092
+ const sdkServer = new Server({ name: server.name, version: server.version }, { capabilities });
756
1093
  sdkServer.setRequestHandler(ListToolsRequestSchema, async () => ({
757
1094
  tools: server.getTools()
758
1095
  }));
759
1096
  sdkServer.setRequestHandler(CallToolRequestSchema, async (request) => {
760
1097
  const { name, arguments: args } = request.params;
761
- const result = await server.invokeTool(name, args ?? {});
1098
+ const progressToken = request.params._meta?.progressToken;
1099
+ const options = progressToken !== undefined ? { progressToken } : undefined;
1100
+ const result = await server.invokeTool(name, args ?? {}, options);
762
1101
  if (result.isErr()) {
763
1102
  return wrapToolError(result.error);
764
1103
  }
765
1104
  return wrapToolResult(result.value);
766
1105
  });
1106
+ sdkServer.setRequestHandler(ListResourcesRequestSchema, async () => ({
1107
+ resources: server.getResources().map((r) => ({
1108
+ uri: r.uri,
1109
+ name: r.name,
1110
+ ...r.description ? { description: r.description } : {},
1111
+ ...r.mimeType ? { mimeType: r.mimeType } : {}
1112
+ }))
1113
+ }));
1114
+ sdkServer.setRequestHandler(ListResourceTemplatesRequestSchema, async () => ({
1115
+ resourceTemplates: server.getResourceTemplates().map((t) => ({
1116
+ uriTemplate: t.uriTemplate,
1117
+ name: t.name,
1118
+ ...t.description ? { description: t.description } : {},
1119
+ ...t.mimeType ? { mimeType: t.mimeType } : {}
1120
+ }))
1121
+ }));
1122
+ sdkServer.setRequestHandler(ReadResourceRequestSchema, async (request) => {
1123
+ const { uri } = request.params;
1124
+ const result = await server.readResource(uri);
1125
+ if (result.isErr()) {
1126
+ throw toSdkError(result.error);
1127
+ }
1128
+ return { contents: result.value };
1129
+ });
1130
+ sdkServer.setRequestHandler(SubscribeRequestSchema, async (request) => {
1131
+ server.subscribe(request.params.uri);
1132
+ return {};
1133
+ });
1134
+ sdkServer.setRequestHandler(UnsubscribeRequestSchema, async (request) => {
1135
+ server.unsubscribe(request.params.uri);
1136
+ return {};
1137
+ });
1138
+ sdkServer.setRequestHandler(ListPromptsRequestSchema, async () => ({
1139
+ prompts: server.getPrompts()
1140
+ }));
1141
+ sdkServer.setRequestHandler(GetPromptRequestSchema, async (request) => {
1142
+ const { name, arguments: args } = request.params;
1143
+ const result = await server.getPrompt(name, args ?? {});
1144
+ if (result.isErr()) {
1145
+ throw toSdkError(result.error);
1146
+ }
1147
+ return { ...result.value };
1148
+ });
1149
+ sdkServer.setRequestHandler(CompleteRequestSchema, async (request) => {
1150
+ const { ref, argument } = request.params;
1151
+ const completionRef = ref.type === "ref/prompt" ? { type: "ref/prompt", name: ref.name } : { type: "ref/resource", uri: ref.uri };
1152
+ const result = await server.complete(completionRef, argument.name, argument.value);
1153
+ if (result.isErr()) {
1154
+ throw toSdkError(result.error);
1155
+ }
1156
+ return { completion: result.value };
1157
+ });
1158
+ sdkServer.setRequestHandler(SetLevelRequestSchema, async (request) => {
1159
+ const level = request.params.level;
1160
+ server.setLogLevel?.(level);
1161
+ return {};
1162
+ });
1163
+ server.bindSdkServer?.(sdkServer);
767
1164
  return sdkServer;
768
1165
  }
769
1166
  async function connectStdio(server, transport = new StdioServerTransport) {
@@ -773,9 +1170,13 @@ async function connectStdio(server, transport = new StdioServerTransport) {
773
1170
  }
774
1171
  export {
775
1172
  zodToJsonSchema,
1173
+ shouldEmitLog,
1174
+ mapLogLevelToMcp,
776
1175
  defineTool,
1176
+ defineResourceTemplate,
777
1177
  defineResource,
778
1178
  defineQueryTool,
1179
+ definePrompt,
779
1180
  defineDocsTool,
780
1181
  defineConfigTool,
781
1182
  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.1.0",
4
+ "version": "0.2.0",
5
5
  "type": "module",
6
6
  "files": [
7
7
  "dist"
@@ -40,8 +40,8 @@
40
40
  },
41
41
  "dependencies": {
42
42
  "@modelcontextprotocol/sdk": "^1.12.1",
43
- "@outfitter/contracts": "0.1.0",
44
- "@outfitter/logging": "0.1.0",
43
+ "@outfitter/contracts": "0.2.0",
44
+ "@outfitter/logging": "0.2.0",
45
45
  "zod": "^4.3.5"
46
46
  },
47
47
  "devDependencies": {