Skip to content

Commit b87d2d6

Browse files
author
Gavin Aboulhosn
committed
feat: Add completion and resource template support
- 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.
1 parent 89a323a commit b87d2d6

File tree

4 files changed

+167
-30
lines changed

4 files changed

+167
-30
lines changed

README.md

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ Get started fast with mcp-framework ⚡⚡⚡
1414
- 🏗️ Powerful abstractions with full type safety
1515
- 🚀 Simple server setup and configuration
1616
- 📦 CLI for rapid development and project scaffolding
17+
- 🔍 Built-in support for autocompletion
18+
- 📝 URI templates for dynamic resources
1719

1820
## Quick Start
1921

@@ -66,7 +68,7 @@ mcp add prompt price-analysis
6668
### Adding a Resource
6769

6870
```bash
69-
# Add a new prompt
71+
# Add a new resource
7072
mcp add resource market-data
7173
```
7274

@@ -175,7 +177,7 @@ export default ExampleTool;
175177

176178
### 2. Prompts (Optional)
177179

178-
Prompts help structure conversations with Claude:
180+
Prompts help structure conversations with Claude and can provide completion suggestions:
179181

180182
```typescript
181183
import { MCPPrompt } from "mcp-framework";
@@ -203,6 +205,21 @@ class GreetingPrompt extends MCPPrompt<GreetingInput> {
203205
},
204206
};
205207

208+
// Provide auto-completions for arguments
209+
async complete(argumentName: string, value: string) {
210+
if (argumentName === "language") {
211+
const languages = ["English", "Spanish", "French", "German"];
212+
const matches = languages.filter(lang =>
213+
lang.toLowerCase().startsWith(value.toLowerCase())
214+
);
215+
return {
216+
values: matches,
217+
total: matches.length
218+
};
219+
}
220+
return { values: [] };
221+
}
222+
206223
async generateMessages({ name, language = "English" }: GreetingInput) {
207224
return [
208225
{
@@ -221,7 +238,7 @@ export default GreetingPrompt;
221238

222239
### 3. Resources (Optional)
223240

224-
Resources provide data access capabilities:
241+
Resources provide data access capabilities with support for dynamic URIs and completions:
225242

226243
```typescript
227244
import { MCPResource, ResourceContent } from "mcp-framework";
@@ -232,10 +249,32 @@ class ConfigResource extends MCPResource {
232249
description = "Current application configuration";
233250
mimeType = "application/json";
234251

252+
protected template = {
253+
uriTemplate: "config://app/{section}",
254+
description: "Access settings by section"
255+
};
256+
257+
// Optional: Provide completions for URI template arguments
258+
async complete(argumentName: string, value: string) {
259+
if (argumentName === "section") {
260+
const sections = ["theme", "network"];
261+
return {
262+
values: sections.filter(s => s.startsWith(value)),
263+
total: sections.length
264+
};
265+
}
266+
return { values: [] };
267+
}
268+
235269
async read(): Promise<ResourceContent[]> {
236270
const config = {
237-
theme: "dark",
238-
language: "en",
271+
theme: {
272+
mode: "dark",
273+
language: "en",
274+
},
275+
network: {
276+
proxy: "none"
277+
}
239278
};
240279

241280
return [
@@ -294,12 +333,15 @@ Each feature should be in its own file and export a default class that extends t
294333
- Manages prompt arguments and validation
295334
- Generates message sequences for LLM interactions
296335
- Supports dynamic prompt templates
336+
- Optional completion support for arguments
297337

298338
#### MCPResource
299339

300340
- Exposes data through URI-based system
301341
- Supports text and binary content
302342
- Optional subscription capabilities for real-time updates
343+
- Optional URI templates for dynamic access
344+
- Optional completion support for template arguments
303345

304346
## Type Safety
305347

src/core/MCPServer.ts

Lines changed: 64 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import {
99
ReadResourceRequestSchema,
1010
SubscribeRequestSchema,
1111
UnsubscribeRequestSchema,
12+
CompleteRequestSchema,
13+
ListResourceTemplatesRequestSchema,
1214
} from "@modelcontextprotocol/sdk/types.js";
1315
import { ToolProtocol } from "../tools/BaseTool.js";
1416
import { PromptProtocol } from "../prompts/BasePrompt.js";
@@ -59,7 +61,7 @@ export class MCPServer {
5961
this.serverVersion = config.version ?? this.getDefaultVersion();
6062

6163
logger.info(
62-
`Initializing MCP Server: ${this.serverName}@${this.serverVersion}`
64+
`Initializing MCP Server: ${this.serverName}@${this.serverVersion}`,
6365
);
6466

6567
this.toolLoader = new ToolLoader(this.basePath);
@@ -77,7 +79,7 @@ export class MCPServer {
7779
prompts: { enabled: false },
7880
resources: { enabled: false },
7981
},
80-
}
82+
},
8183
);
8284

8385
this.setupHandlers();
@@ -128,7 +130,7 @@ export class MCPServer {
128130
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
129131
return {
130132
tools: Array.from(this.toolsMap.values()).map(
131-
(tool) => tool.toolDefinition
133+
(tool) => tool.toolDefinition,
132134
),
133135
};
134136
});
@@ -138,8 +140,8 @@ export class MCPServer {
138140
if (!tool) {
139141
throw new Error(
140142
`Unknown tool: ${request.params.name}. Available tools: ${Array.from(
141-
this.toolsMap.keys()
142-
).join(", ")}`
143+
this.toolsMap.keys(),
144+
).join(", ")}`,
143145
);
144146
}
145147

@@ -154,7 +156,7 @@ export class MCPServer {
154156
this.server.setRequestHandler(ListPromptsRequestSchema, async () => {
155157
return {
156158
prompts: Array.from(this.promptsMap.values()).map(
157-
(prompt) => prompt.promptDefinition
159+
(prompt) => prompt.promptDefinition,
158160
),
159161
};
160162
});
@@ -166,8 +168,8 @@ export class MCPServer {
166168
`Unknown prompt: ${
167169
request.params.name
168170
}. Available prompts: ${Array.from(this.promptsMap.keys()).join(
169-
", "
170-
)}`
171+
", ",
172+
)}`,
171173
);
172174
}
173175

@@ -179,11 +181,24 @@ export class MCPServer {
179181
this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
180182
return {
181183
resources: Array.from(this.resourcesMap.values()).map(
182-
(resource) => resource.resourceDefinition
184+
(resource) => resource.resourceDefinition,
183185
),
184186
};
185187
});
186188

189+
this.server.setRequestHandler(
190+
ListResourceTemplatesRequestSchema,
191+
async () => {
192+
const templates = Array.from(this.resourcesMap.values())
193+
.map((resource) => resource.templateDefinition)
194+
.filter((template): template is NonNullable<typeof template> =>
195+
Boolean(template),
196+
);
197+
198+
return { resourceTemplates: templates };
199+
},
200+
);
201+
187202
this.server.setRequestHandler(
188203
ReadResourceRequestSchema,
189204
async (request) => {
@@ -193,15 +208,15 @@ export class MCPServer {
193208
`Unknown resource: ${
194209
request.params.uri
195210
}. Available resources: ${Array.from(this.resourcesMap.keys()).join(
196-
", "
197-
)}`
211+
", ",
212+
)}`,
198213
);
199214
}
200215

201216
return {
202217
contents: await resource.read(),
203218
};
204-
}
219+
},
205220
);
206221

207222
this.server.setRequestHandler(SubscribeRequestSchema, async (request) => {
@@ -212,7 +227,7 @@ export class MCPServer {
212227

213228
if (!resource.subscribe) {
214229
throw new Error(
215-
`Resource ${request.params.uri} does not support subscriptions`
230+
`Resource ${request.params.uri} does not support subscriptions`,
216231
);
217232
}
218233

@@ -228,13 +243,39 @@ export class MCPServer {
228243

229244
if (!resource.unsubscribe) {
230245
throw new Error(
231-
`Resource ${request.params.uri} does not support subscriptions`
246+
`Resource ${request.params.uri} does not support subscriptions`,
232247
);
233248
}
234249

235250
await resource.unsubscribe();
236251
return {};
237252
});
253+
254+
this.server.setRequestHandler(CompleteRequestSchema, async (request) => {
255+
const { ref, argument } = request.params;
256+
257+
if (ref.type === "ref/prompt") {
258+
const prompt = this.promptsMap.get(ref.name);
259+
if (!prompt?.complete) {
260+
return { completion: { values: [] } };
261+
}
262+
return {
263+
completion: await prompt.complete(argument.name, argument.value),
264+
};
265+
}
266+
267+
if (ref.type === "ref/resource") {
268+
const resource = this.resourcesMap.get(ref.uri);
269+
if (!resource?.complete) {
270+
return { completion: { values: [] } };
271+
}
272+
return {
273+
completion: await resource.complete(argument.name, argument.value),
274+
};
275+
}
276+
277+
throw new Error(`Unknown reference type: ${ref}`);
278+
});
238279
}
239280

240281
private async detectCapabilities(): Promise<ServerCapabilities> {
@@ -262,17 +303,17 @@ export class MCPServer {
262303
try {
263304
const tools = await this.toolLoader.loadTools();
264305
this.toolsMap = new Map(
265-
tools.map((tool: ToolProtocol) => [tool.name, tool])
306+
tools.map((tool: ToolProtocol) => [tool.name, tool]),
266307
);
267308

268309
const prompts = await this.promptLoader.loadPrompts();
269310
this.promptsMap = new Map(
270-
prompts.map((prompt: PromptProtocol) => [prompt.name, prompt])
311+
prompts.map((prompt: PromptProtocol) => [prompt.name, prompt]),
271312
);
272313

273314
const resources = await this.resourceLoader.loadResources();
274315
this.resourcesMap = new Map(
275-
resources.map((resource: ResourceProtocol) => [resource.uri, resource])
316+
resources.map((resource: ResourceProtocol) => [resource.uri, resource]),
276317
);
277318

278319
await this.detectCapabilities();
@@ -285,22 +326,22 @@ export class MCPServer {
285326
if (tools.length > 0) {
286327
logger.info(
287328
`Tools (${tools.length}): ${Array.from(this.toolsMap.keys()).join(
288-
", "
289-
)}`
329+
", ",
330+
)}`,
290331
);
291332
}
292333
if (prompts.length > 0) {
293334
logger.info(
294335
`Prompts (${prompts.length}): ${Array.from(
295-
this.promptsMap.keys()
296-
).join(", ")}`
336+
this.promptsMap.keys(),
337+
).join(", ")}`,
297338
);
298339
}
299340
if (resources.length > 0) {
300341
logger.info(
301342
`Resources (${resources.length}): ${Array.from(
302-
this.resourcesMap.keys()
303-
).join(", ")}`
343+
this.resourcesMap.keys(),
344+
).join(", ")}`,
304345
);
305346
}
306347
} catch (error) {

src/prompts/BasePrompt.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ export type PromptArguments<T extends PromptArgumentSchema<any>> = {
1212
[K in keyof T]: z.infer<T[K]["type"]>;
1313
};
1414

15+
export type PromptCompletion = {
16+
values: string[];
17+
total?: number;
18+
hasMore?: boolean;
19+
};
20+
1521
export interface PromptProtocol {
1622
name: string;
1723
description: string;
@@ -38,6 +44,7 @@ export interface PromptProtocol {
3844
};
3945
}>
4046
>;
47+
complete?(argument: string, name: string): Promise<PromptCompletion>;
4148
}
4249

4350
export abstract class MCPPrompt<TArgs extends Record<string, any> = {}>
@@ -77,14 +84,25 @@ export abstract class MCPPrompt<TArgs extends Record<string, any> = {}>
7784
async getMessages(args: Record<string, unknown> = {}) {
7885
const zodSchema = z.object(
7986
Object.fromEntries(
80-
Object.entries(this.schema).map(([key, schema]) => [key, schema.type])
81-
)
87+
Object.entries(this.schema).map(([key, schema]) => [key, schema.type]),
88+
),
8289
);
8390

8491
const validatedArgs = (await zodSchema.parse(args)) as TArgs;
8592
return this.generateMessages(validatedArgs);
8693
}
8794

95+
async complete?(
96+
argumentName: string,
97+
value: string,
98+
): Promise<PromptCompletion> {
99+
return {
100+
values: [],
101+
total: 0,
102+
hasMore: false,
103+
};
104+
}
105+
88106
protected async fetch<T>(url: string, init?: RequestInit): Promise<T> {
89107
const response = await fetch(url, init);
90108
if (!response.ok) {

0 commit comments

Comments
 (0)