diff --git a/README.md b/README.md index 1a8a79d..96fc05c 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,8 @@ Get started fast with mcp-framework โšกโšกโšก - ๐Ÿ—๏ธ Powerful abstractions with full type safety - ๐Ÿš€ Simple server setup and configuration - ๐Ÿ“ฆ CLI for rapid development and project scaffolding +- ๐Ÿ” Built-in support for autocompletion +- ๐Ÿ“ URI templates for dynamic resources ## Quick Start @@ -66,7 +68,7 @@ mcp add prompt price-analysis ### Adding a Resource ```bash -# Add a new prompt +# Add a new resource mcp add resource market-data ``` @@ -175,7 +177,7 @@ export default ExampleTool; ### 2. Prompts (Optional) -Prompts help structure conversations with Claude: +Prompts help structure conversations with Claude and can provide completion suggestions: ```typescript import { MCPPrompt } from "mcp-framework"; @@ -203,6 +205,21 @@ class GreetingPrompt extends MCPPrompt { }, }; + // Provide auto-completions for arguments + async complete(argumentName: string, value: string) { + if (argumentName === "language") { + const languages = ["English", "Spanish", "French", "German"]; + const matches = languages.filter(lang => + lang.toLowerCase().startsWith(value.toLowerCase()) + ); + return { + values: matches, + total: matches.length + }; + } + return { values: [] }; + } + async generateMessages({ name, language = "English" }: GreetingInput) { return [ { @@ -221,7 +238,7 @@ export default GreetingPrompt; ### 3. Resources (Optional) -Resources provide data access capabilities: +Resources provide data access capabilities with support for dynamic URIs and completions: ```typescript import { MCPResource, ResourceContent } from "mcp-framework"; @@ -232,10 +249,32 @@ class ConfigResource extends MCPResource { description = "Current application configuration"; mimeType = "application/json"; + protected template = { + uriTemplate: "config://app/{section}", + description: "Access settings by section" + }; + + // Optional: Provide completions for URI template arguments + async complete(argumentName: string, value: string) { + if (argumentName === "section") { + const sections = ["theme", "network"]; + return { + values: sections.filter(s => s.startsWith(value)), + total: sections.length + }; + } + return { values: [] }; + } + async read(): Promise { const config = { - theme: "dark", - language: "en", + theme: { + mode: "dark", + language: "en", + }, + network: { + proxy: "none" + } }; return [ @@ -294,12 +333,15 @@ Each feature should be in its own file and export a default class that extends t - Manages prompt arguments and validation - Generates message sequences for LLM interactions - Supports dynamic prompt templates +- Optional completion support for arguments #### MCPResource - Exposes data through URI-based system - Supports text and binary content - Optional subscription capabilities for real-time updates +- Optional URI templates for dynamic access +- Optional completion support for template arguments ## Type Safety diff --git a/src/core/MCPServer.ts b/src/core/MCPServer.ts index 57d2391..5ea8ad0 100644 --- a/src/core/MCPServer.ts +++ b/src/core/MCPServer.ts @@ -9,6 +9,8 @@ import { ReadResourceRequestSchema, SubscribeRequestSchema, UnsubscribeRequestSchema, + CompleteRequestSchema, + ListResourceTemplatesRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import { ToolProtocol } from "../tools/BaseTool.js"; import { PromptProtocol } from "../prompts/BasePrompt.js"; @@ -59,7 +61,7 @@ export class MCPServer { this.serverVersion = config.version ?? this.getDefaultVersion(); logger.info( - `Initializing MCP Server: ${this.serverName}@${this.serverVersion}` + `Initializing MCP Server: ${this.serverName}@${this.serverVersion}`, ); this.toolLoader = new ToolLoader(this.basePath); @@ -77,7 +79,7 @@ export class MCPServer { prompts: { enabled: false }, resources: { enabled: false }, }, - } + }, ); this.setupHandlers(); @@ -128,7 +130,7 @@ export class MCPServer { this.server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: Array.from(this.toolsMap.values()).map( - (tool) => tool.toolDefinition + (tool) => tool.toolDefinition, ), }; }); @@ -138,8 +140,8 @@ export class MCPServer { if (!tool) { throw new Error( `Unknown tool: ${request.params.name}. Available tools: ${Array.from( - this.toolsMap.keys() - ).join(", ")}` + this.toolsMap.keys(), + ).join(", ")}`, ); } @@ -154,7 +156,7 @@ export class MCPServer { this.server.setRequestHandler(ListPromptsRequestSchema, async () => { return { prompts: Array.from(this.promptsMap.values()).map( - (prompt) => prompt.promptDefinition + (prompt) => prompt.promptDefinition, ), }; }); @@ -166,8 +168,8 @@ export class MCPServer { `Unknown prompt: ${ request.params.name }. Available prompts: ${Array.from(this.promptsMap.keys()).join( - ", " - )}` + ", ", + )}`, ); } @@ -179,11 +181,24 @@ export class MCPServer { this.server.setRequestHandler(ListResourcesRequestSchema, async () => { return { resources: Array.from(this.resourcesMap.values()).map( - (resource) => resource.resourceDefinition + (resource) => resource.resourceDefinition, ), }; }); + this.server.setRequestHandler( + ListResourceTemplatesRequestSchema, + async () => { + const templates = Array.from(this.resourcesMap.values()) + .map((resource) => resource.templateDefinition) + .filter((template): template is NonNullable => + Boolean(template), + ); + + return { resourceTemplates: templates }; + }, + ); + this.server.setRequestHandler( ReadResourceRequestSchema, async (request) => { @@ -193,15 +208,15 @@ export class MCPServer { `Unknown resource: ${ request.params.uri }. Available resources: ${Array.from(this.resourcesMap.keys()).join( - ", " - )}` + ", ", + )}`, ); } return { contents: await resource.read(), }; - } + }, ); this.server.setRequestHandler(SubscribeRequestSchema, async (request) => { @@ -212,7 +227,7 @@ export class MCPServer { if (!resource.subscribe) { throw new Error( - `Resource ${request.params.uri} does not support subscriptions` + `Resource ${request.params.uri} does not support subscriptions`, ); } @@ -228,13 +243,43 @@ export class MCPServer { if (!resource.unsubscribe) { throw new Error( - `Resource ${request.params.uri} does not support subscriptions` + `Resource ${request.params.uri} does not support subscriptions`, ); } await resource.unsubscribe(); return {}; }); + + this.server.setRequestHandler(CompleteRequestSchema, async (request) => { + const { ref, argument } = request.params; + + if (ref.type === "ref/prompt") { + const prompt = this.promptsMap.get(ref.name) as + | PromptProtocol + | undefined; + if (!prompt?.complete) { + return { completion: { values: [] } }; + } + return { + completion: await prompt.complete(argument.name, argument.value), + }; + } + + if (ref.type === "ref/resource") { + const resource = this.resourcesMap.get(ref.uri) as + | ResourceProtocol + | undefined; + if (!resource?.complete) { + return { completion: { values: [] } }; + } + return { + completion: await resource.complete(argument.name, argument.value), + }; + } + + throw new Error(`Unknown reference type: ${ref}`); + }); } private async detectCapabilities(): Promise { @@ -262,17 +307,17 @@ export class MCPServer { try { const tools = await this.toolLoader.loadTools(); this.toolsMap = new Map( - tools.map((tool: ToolProtocol) => [tool.name, tool]) + tools.map((tool: ToolProtocol) => [tool.name, tool]), ); const prompts = await this.promptLoader.loadPrompts(); this.promptsMap = new Map( - prompts.map((prompt: PromptProtocol) => [prompt.name, prompt]) + prompts.map((prompt: PromptProtocol) => [prompt.name, prompt]), ); const resources = await this.resourceLoader.loadResources(); this.resourcesMap = new Map( - resources.map((resource: ResourceProtocol) => [resource.uri, resource]) + resources.map((resource: ResourceProtocol) => [resource.uri, resource]), ); await this.detectCapabilities(); @@ -285,22 +330,22 @@ export class MCPServer { if (tools.length > 0) { logger.info( `Tools (${tools.length}): ${Array.from(this.toolsMap.keys()).join( - ", " - )}` + ", ", + )}`, ); } if (prompts.length > 0) { logger.info( `Prompts (${prompts.length}): ${Array.from( - this.promptsMap.keys() - ).join(", ")}` + this.promptsMap.keys(), + ).join(", ")}`, ); } if (resources.length > 0) { logger.info( `Resources (${resources.length}): ${Array.from( - this.resourcesMap.keys() - ).join(", ")}` + this.resourcesMap.keys(), + ).join(", ")}`, ); } } catch (error) { diff --git a/src/prompts/BasePrompt.ts b/src/prompts/BasePrompt.ts index cf10e6a..8f24993 100644 --- a/src/prompts/BasePrompt.ts +++ b/src/prompts/BasePrompt.ts @@ -12,7 +12,13 @@ export type PromptArguments> = { [K in keyof T]: z.infer; }; -export interface PromptProtocol { +export type PromptCompletion = { + values: string[]; + total?: number; + hasMore?: boolean; +}; + +export interface PromptProtocol = {}> { name: string; description: string; promptDefinition: { @@ -38,10 +44,14 @@ export interface PromptProtocol { }; }> >; + complete?( + argumentName: K, + value: string, + ): Promise; } export abstract class MCPPrompt = {}> - implements PromptProtocol + implements PromptProtocol { abstract name: string; abstract description: string; @@ -77,14 +87,29 @@ export abstract class MCPPrompt = {}> async getMessages(args: Record = {}) { const zodSchema = z.object( Object.fromEntries( - Object.entries(this.schema).map(([key, schema]) => [key, schema.type]) - ) + Object.entries(this.schema).map(([key, schema]) => [key, schema.type]), + ), ); const validatedArgs = (await zodSchema.parse(args)) as TArgs; return this.generateMessages(validatedArgs); } + async complete( + argumentName: K, + value: string, + ): Promise { + if (!this.schema[argumentName].type) { + throw new Error(`No schema found for argument: ${argumentName}`); + } + + return { + values: [], + total: 0, + hasMore: false, + }; + } + protected async fetch(url: string, init?: RequestInit): Promise { const response = await fetch(url, init); if (!response.ok) { diff --git a/src/resources/BaseResource.ts b/src/resources/BaseResource.ts index c681ed9..e723419 100644 --- a/src/resources/BaseResource.ts +++ b/src/resources/BaseResource.ts @@ -19,22 +19,39 @@ export type ResourceTemplateDefinition = { mimeType?: string; }; -export interface ResourceProtocol { +export type ResourceCompletion = { + values: string[]; + total?: number; + hasMore?: boolean; +}; + +export interface ResourceProtocol = {}> { uri: string; name: string; description?: string; mimeType?: string; resourceDefinition: ResourceDefinition; + templateDefinition?: ResourceTemplateDefinition; read(): Promise; subscribe?(): Promise; unsubscribe?(): Promise; + complete?( + argumentName: K, + value: TArgs[K], + ): Promise; } -export abstract class MCPResource implements ResourceProtocol { +export abstract class MCPResource = {}> + implements ResourceProtocol +{ abstract uri: string; abstract name: string; description?: string; mimeType?: string; + protected template?: { + uriTemplate: string; + description?: string; + }; get resourceDefinition(): ResourceDefinition { return { @@ -45,6 +62,19 @@ export abstract class MCPResource implements ResourceProtocol { }; } + get templateDefinition(): ResourceTemplateDefinition | undefined { + if (!this.template) { + return undefined; + } + + return { + uriTemplate: this.template.uriTemplate, + name: this.name, + description: this.template.description ?? this.description, + mimeType: this.mimeType, + }; + } + abstract read(): Promise; async subscribe?(): Promise { @@ -55,6 +85,17 @@ export abstract class MCPResource implements ResourceProtocol { throw new Error("Unsubscription not implemented for this resource"); } + async complete?( + argument: string, + value: string, + ): Promise { + return { + values: [], + total: 0, + hasMore: false, + }; + } + protected async fetch(url: string, init?: RequestInit): Promise { const response = await fetch(url, init); if (!response.ok) {