Skip to content

Commit

Permalink
feat: Add completion and resource template support
Browse files Browse the repository at this point in the history
- Add support for MCP completions
- Implement URI templates for dynamic resources
- Add completion handlers for prompts and resources
- Update documentation with new features and examples

This implementation follows the MCP specification for completions and
resource templates while maintaining backwards compatibility.
  • Loading branch information
Gavin Aboulhosn committed Dec 12, 2024
1 parent 89a323a commit b87d2d6
Show file tree
Hide file tree
Showing 4 changed files with 167 additions and 30 deletions.
52 changes: 47 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
```

Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -203,6 +205,21 @@ class GreetingPrompt extends MCPPrompt<GreetingInput> {
},
};

// 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 [
{
Expand All @@ -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";
Expand All @@ -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<ResourceContent[]> {
const config = {
theme: "dark",
language: "en",
theme: {
mode: "dark",
language: "en",
},
network: {
proxy: "none"
}
};

return [
Expand Down Expand Up @@ -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

Expand Down
87 changes: 64 additions & 23 deletions src/core/MCPServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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);
Expand All @@ -77,7 +79,7 @@ export class MCPServer {
prompts: { enabled: false },
resources: { enabled: false },
},
}
},
);

this.setupHandlers();
Expand Down Expand Up @@ -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,
),
};
});
Expand All @@ -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(", ")}`,
);
}

Expand All @@ -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,
),
};
});
Expand All @@ -166,8 +168,8 @@ export class MCPServer {
`Unknown prompt: ${
request.params.name
}. Available prompts: ${Array.from(this.promptsMap.keys()).join(
", "
)}`
", ",
)}`,
);
}

Expand All @@ -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<typeof template> =>
Boolean(template),
);

return { resourceTemplates: templates };
},
);

this.server.setRequestHandler(
ReadResourceRequestSchema,
async (request) => {
Expand All @@ -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) => {
Expand All @@ -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`,
);
}

Expand All @@ -228,13 +243,39 @@ 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);
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);
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<ServerCapabilities> {
Expand Down Expand Up @@ -262,17 +303,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();
Expand All @@ -285,22 +326,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) {
Expand Down
22 changes: 20 additions & 2 deletions src/prompts/BasePrompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ export type PromptArguments<T extends PromptArgumentSchema<any>> = {
[K in keyof T]: z.infer<T[K]["type"]>;
};

export type PromptCompletion = {
values: string[];
total?: number;
hasMore?: boolean;
};

export interface PromptProtocol {
name: string;
description: string;
Expand All @@ -38,6 +44,7 @@ export interface PromptProtocol {
};
}>
>;
complete?(argument: string, name: string): Promise<PromptCompletion>;
}

export abstract class MCPPrompt<TArgs extends Record<string, any> = {}>
Expand Down Expand Up @@ -77,14 +84,25 @@ export abstract class MCPPrompt<TArgs extends Record<string, any> = {}>
async getMessages(args: Record<string, unknown> = {}) {
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: string,
value: string,
): Promise<PromptCompletion> {
return {
values: [],
total: 0,
hasMore: false,
};
}

protected async fetch<T>(url: string, init?: RequestInit): Promise<T> {
const response = await fetch(url, init);
if (!response.ok) {
Expand Down
Loading

0 comments on commit b87d2d6

Please sign in to comment.