Merge branch 'main' into main
This commit is contained in:
commit
b92243bbd5
18 changed files with 1770 additions and 73 deletions
0
.git-blame-ignore-revs
Normal file
0
.git-blame-ignore-revs
Normal file
198
README.md
198
README.md
|
|
@ -11,6 +11,7 @@
|
|||
- [Resources](#resources)
|
||||
- [Tools](#tools)
|
||||
- [Prompts](#prompts)
|
||||
- [Completions](#completions)
|
||||
- [Running Your Server](#running-your-server)
|
||||
- [stdio](#stdio)
|
||||
- [Streamable HTTP](#streamable-http)
|
||||
|
|
@ -150,6 +151,33 @@ server.registerResource(
|
|||
}]
|
||||
})
|
||||
);
|
||||
|
||||
// Resource with context-aware completion
|
||||
server.registerResource(
|
||||
"repository",
|
||||
new ResourceTemplate("github://repos/{owner}/{repo}", {
|
||||
list: undefined,
|
||||
complete: {
|
||||
// Provide intelligent completions based on previously resolved parameters
|
||||
repo: (value, context) => {
|
||||
if (context?.arguments?.["owner"] === "org1") {
|
||||
return ["project1", "project2", "project3"].filter(r => r.startsWith(value));
|
||||
}
|
||||
return ["default-repo"].filter(r => r.startsWith(value));
|
||||
}
|
||||
}
|
||||
}),
|
||||
{
|
||||
title: "GitHub Repository",
|
||||
description: "Repository information"
|
||||
},
|
||||
async (uri, { owner, repo }) => ({
|
||||
contents: [{
|
||||
uri: uri.href,
|
||||
text: `Repository: ${owner}/${repo}`
|
||||
}]
|
||||
})
|
||||
);
|
||||
```
|
||||
|
||||
### Tools
|
||||
|
|
@ -233,12 +261,14 @@ Tools can return `ResourceLink` objects to reference resources without embedding
|
|||
Prompts are reusable templates that help LLMs interact with your server effectively:
|
||||
|
||||
```typescript
|
||||
import { completable } from "@modelcontextprotocol/sdk/server/completable.js";
|
||||
|
||||
server.registerPrompt(
|
||||
"review-code",
|
||||
{
|
||||
title: "Code Review",
|
||||
description: "Review code for best practices and potential issues",
|
||||
arguments: { code: z.string() }
|
||||
argsSchema: { code: z.string() }
|
||||
},
|
||||
({ code }) => ({
|
||||
messages: [{
|
||||
|
|
@ -250,6 +280,68 @@ server.registerPrompt(
|
|||
}]
|
||||
})
|
||||
);
|
||||
|
||||
// Prompt with context-aware completion
|
||||
server.registerPrompt(
|
||||
"team-greeting",
|
||||
{
|
||||
title: "Team Greeting",
|
||||
description: "Generate a greeting for team members",
|
||||
argsSchema: {
|
||||
department: completable(z.string(), (value) => {
|
||||
// Department suggestions
|
||||
return ["engineering", "sales", "marketing", "support"].filter(d => d.startsWith(value));
|
||||
}),
|
||||
name: completable(z.string(), (value, context) => {
|
||||
// Name suggestions based on selected department
|
||||
const department = context?.arguments?.["department"];
|
||||
if (department === "engineering") {
|
||||
return ["Alice", "Bob", "Charlie"].filter(n => n.startsWith(value));
|
||||
} else if (department === "sales") {
|
||||
return ["David", "Eve", "Frank"].filter(n => n.startsWith(value));
|
||||
} else if (department === "marketing") {
|
||||
return ["Grace", "Henry", "Iris"].filter(n => n.startsWith(value));
|
||||
}
|
||||
return ["Guest"].filter(n => n.startsWith(value));
|
||||
})
|
||||
}
|
||||
},
|
||||
({ department, name }) => ({
|
||||
messages: [{
|
||||
role: "assistant",
|
||||
content: {
|
||||
type: "text",
|
||||
text: `Hello ${name}, welcome to the ${department} team!`
|
||||
}
|
||||
}]
|
||||
})
|
||||
);
|
||||
```
|
||||
|
||||
### Completions
|
||||
|
||||
MCP supports argument completions to help users fill in prompt arguments and resource template parameters. See the examples above for [resource completions](#resources) and [prompt completions](#prompts).
|
||||
|
||||
#### Client Usage
|
||||
|
||||
```typescript
|
||||
// Request completions for any argument
|
||||
const result = await client.complete({
|
||||
ref: {
|
||||
type: "ref/prompt", // or "ref/resource"
|
||||
name: "example" // or uri: "template://..."
|
||||
},
|
||||
argument: {
|
||||
name: "argumentName",
|
||||
value: "partial" // What the user has typed so far
|
||||
},
|
||||
context: { // Optional: Include previously resolved arguments
|
||||
arguments: {
|
||||
previousArg: "value"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
### Display Names and Metadata
|
||||
|
|
@ -772,6 +864,109 @@ const transport = new StdioServerTransport();
|
|||
await server.connect(transport);
|
||||
```
|
||||
|
||||
### Eliciting User Input
|
||||
|
||||
MCP servers can request additional information from users through the elicitation feature. This is useful for interactive workflows where the server needs user input or confirmation:
|
||||
|
||||
```typescript
|
||||
// Server-side: Restaurant booking tool that asks for alternatives
|
||||
server.tool(
|
||||
"book-restaurant",
|
||||
{
|
||||
restaurant: z.string(),
|
||||
date: z.string(),
|
||||
partySize: z.number()
|
||||
},
|
||||
async ({ restaurant, date, partySize }) => {
|
||||
// Check availability
|
||||
const available = await checkAvailability(restaurant, date, partySize);
|
||||
|
||||
if (!available) {
|
||||
// Ask user if they want to try alternative dates
|
||||
const result = await server.server.elicitInput({
|
||||
message: `No tables available at ${restaurant} on ${date}. Would you like to check alternative dates?`,
|
||||
requestedSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
checkAlternatives: {
|
||||
type: "boolean",
|
||||
title: "Check alternative dates",
|
||||
description: "Would you like me to check other dates?"
|
||||
},
|
||||
flexibleDates: {
|
||||
type: "string",
|
||||
title: "Date flexibility",
|
||||
description: "How flexible are your dates?",
|
||||
enum: ["next_day", "same_week", "next_week"],
|
||||
enumNames: ["Next day", "Same week", "Next week"]
|
||||
}
|
||||
},
|
||||
required: ["checkAlternatives"]
|
||||
}
|
||||
});
|
||||
|
||||
if (result.action === "accept" && result.content?.checkAlternatives) {
|
||||
const alternatives = await findAlternatives(
|
||||
restaurant,
|
||||
date,
|
||||
partySize,
|
||||
result.content.flexibleDates as string
|
||||
);
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `Found these alternatives: ${alternatives.join(", ")}`
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: "No booking made. Original date not available."
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
// Book the table
|
||||
await makeBooking(restaurant, date, partySize);
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `Booked table for ${partySize} at ${restaurant} on ${date}`
|
||||
}]
|
||||
};
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
Client-side: Handle elicitation requests
|
||||
|
||||
```typescript
|
||||
// This is a placeholder - implement based on your UI framework
|
||||
async function getInputFromUser(message: string, schema: any): Promise<{
|
||||
action: "accept" | "decline" | "cancel";
|
||||
data?: Record<string, any>;
|
||||
}> {
|
||||
// This should be implemented depending on the app
|
||||
throw new Error("getInputFromUser must be implemented for your platform");
|
||||
}
|
||||
|
||||
client.setRequestHandler(ElicitRequestSchema, async (request) => {
|
||||
const userResponse = await getInputFromUser(
|
||||
request.params.message,
|
||||
request.params.requestedSchema
|
||||
);
|
||||
|
||||
return {
|
||||
action: userResponse.action,
|
||||
content: userResponse.action === "accept" ? userResponse.data : undefined
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
**Note**: Elicitation requires client support. Clients must declare the `elicitation` capability during initialization.
|
||||
|
||||
### Writing MCP Clients
|
||||
|
||||
The SDK provides a high-level client interface:
|
||||
|
|
@ -820,6 +1015,7 @@ const result = await client.callTool({
|
|||
arg1: "value"
|
||||
}
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
### Proxy Authorization Requests Upstream
|
||||
|
|
|
|||
4
package-lock.json
generated
4
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "@modelcontextprotocol/sdk",
|
||||
"version": "1.11.4",
|
||||
"version": "1.12.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@modelcontextprotocol/sdk",
|
||||
"version": "1.11.4",
|
||||
"version": "1.12.3",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ajv": "^6.12.6",
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { LATEST_PROTOCOL_VERSION } from '../types.js';
|
||||
import {
|
||||
discoverOAuthMetadata,
|
||||
startAuthorization,
|
||||
|
|
@ -202,7 +203,7 @@ describe("OAuth Authorization", () => {
|
|||
const [url, options] = calls[0];
|
||||
expect(url.toString()).toBe("https://auth.example.com/.well-known/oauth-authorization-server");
|
||||
expect(options.headers).toEqual({
|
||||
"MCP-Protocol-Version": "2025-03-26"
|
||||
"MCP-Protocol-Version": LATEST_PROTOCOL_VERSION
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import {
|
|||
ListToolsRequestSchema,
|
||||
CallToolRequestSchema,
|
||||
CreateMessageRequestSchema,
|
||||
ElicitRequestSchema,
|
||||
ListRootsRequestSchema,
|
||||
ErrorCode,
|
||||
} from "../types.js";
|
||||
|
|
@ -597,6 +598,43 @@ test("should only allow setRequestHandler for declared capabilities", () => {
|
|||
}).toThrow("Client does not support roots capability");
|
||||
});
|
||||
|
||||
test("should allow setRequestHandler for declared elicitation capability", () => {
|
||||
const client = new Client(
|
||||
{
|
||||
name: "test-client",
|
||||
version: "1.0.0",
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
elicitation: {},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// This should work because elicitation is a declared capability
|
||||
expect(() => {
|
||||
client.setRequestHandler(ElicitRequestSchema, () => ({
|
||||
action: "accept",
|
||||
content: {
|
||||
username: "test-user",
|
||||
confirmed: true,
|
||||
},
|
||||
}));
|
||||
}).not.toThrow();
|
||||
|
||||
// This should throw because sampling is not a declared capability
|
||||
expect(() => {
|
||||
client.setRequestHandler(CreateMessageRequestSchema, () => ({
|
||||
model: "test-model",
|
||||
role: "assistant",
|
||||
content: {
|
||||
type: "text",
|
||||
text: "Test response",
|
||||
},
|
||||
}));
|
||||
}).toThrow("Client does not support sampling capability");
|
||||
});
|
||||
|
||||
/***
|
||||
* Test: Type Checking
|
||||
* Test that custom request/notification/result schemas can be used with the Client class.
|
||||
|
|
|
|||
|
|
@ -307,6 +307,14 @@ export class Client<
|
|||
}
|
||||
break;
|
||||
|
||||
case "elicitation/create":
|
||||
if (!this._capabilities.elicitation) {
|
||||
throw new Error(
|
||||
`Client does not support elicitation capability (required for ${method})`,
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case "roots/list":
|
||||
if (!this._capabilities.roots) {
|
||||
throw new Error(
|
||||
|
|
|
|||
|
|
@ -14,11 +14,13 @@ import {
|
|||
ListResourcesResultSchema,
|
||||
LoggingMessageNotificationSchema,
|
||||
ResourceListChangedNotificationSchema,
|
||||
ElicitRequestSchema,
|
||||
ResourceLink,
|
||||
ReadResourceRequest,
|
||||
ReadResourceResultSchema,
|
||||
ResourceLink,
|
||||
} from '../../types.js';
|
||||
import { getDisplayName } from '../../shared/metadataUtils.js';
|
||||
import Ajv from "ajv";
|
||||
|
||||
// Create readline interface for user input
|
||||
const readline = createInterface({
|
||||
|
|
@ -58,6 +60,7 @@ function printHelp(): void {
|
|||
console.log(' call-tool <name> [args] - Call a tool with optional JSON arguments');
|
||||
console.log(' greet [name] - Call the greet tool');
|
||||
console.log(' multi-greet [name] - Call the multi-greet tool with notifications');
|
||||
console.log(' collect-info [type] - Test elicitation with collect-user-info tool (contact/preferences/feedback)');
|
||||
console.log(' start-notifications [interval] [count] - Start periodic notifications');
|
||||
console.log(' run-notifications-tool-with-resumability [interval] [count] - Run notification tool with resumability');
|
||||
console.log(' list-prompts - List available prompts');
|
||||
|
|
@ -120,6 +123,10 @@ function commandLoop(): void {
|
|||
await callMultiGreetTool(args[1] || 'MCP User');
|
||||
break;
|
||||
|
||||
case 'collect-info':
|
||||
await callCollectInfoTool(args[1] || 'contact');
|
||||
break;
|
||||
|
||||
case 'start-notifications': {
|
||||
const interval = args[1] ? parseInt(args[1], 10) : 2000;
|
||||
const count = args[2] ? parseInt(args[2], 10) : 10;
|
||||
|
|
@ -204,15 +211,212 @@ async function connect(url?: string): Promise<void> {
|
|||
console.log(`Connecting to ${serverUrl}...`);
|
||||
|
||||
try {
|
||||
// Create a new client
|
||||
// Create a new client with elicitation capability
|
||||
client = new Client({
|
||||
name: 'example-client',
|
||||
version: '1.0.0'
|
||||
}, {
|
||||
capabilities: {
|
||||
elicitation: {},
|
||||
},
|
||||
});
|
||||
client.onerror = (error) => {
|
||||
console.error('\x1b[31mClient error:', error, '\x1b[0m');
|
||||
}
|
||||
|
||||
// Set up elicitation request handler with proper validation
|
||||
client.setRequestHandler(ElicitRequestSchema, async (request) => {
|
||||
console.log('\n🔔 Elicitation Request Received:');
|
||||
console.log(`Message: ${request.params.message}`);
|
||||
console.log('Requested Schema:');
|
||||
console.log(JSON.stringify(request.params.requestedSchema, null, 2));
|
||||
|
||||
const schema = request.params.requestedSchema;
|
||||
const properties = schema.properties;
|
||||
const required = schema.required || [];
|
||||
|
||||
// Set up AJV validator for the requested schema
|
||||
const ajv = new Ajv();
|
||||
const validate = ajv.compile(schema);
|
||||
|
||||
let attempts = 0;
|
||||
const maxAttempts = 3;
|
||||
|
||||
while (attempts < maxAttempts) {
|
||||
attempts++;
|
||||
console.log(`\nPlease provide the following information (attempt ${attempts}/${maxAttempts}):`);
|
||||
|
||||
const content: Record<string, unknown> = {};
|
||||
let inputCancelled = false;
|
||||
|
||||
// Collect input for each field
|
||||
for (const [fieldName, fieldSchema] of Object.entries(properties)) {
|
||||
const field = fieldSchema as {
|
||||
type?: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
default?: unknown;
|
||||
enum?: string[];
|
||||
minimum?: number;
|
||||
maximum?: number;
|
||||
minLength?: number;
|
||||
maxLength?: number;
|
||||
format?: string;
|
||||
};
|
||||
|
||||
const isRequired = required.includes(fieldName);
|
||||
let prompt = `${field.title || fieldName}`;
|
||||
|
||||
// Add helpful information to the prompt
|
||||
if (field.description) {
|
||||
prompt += ` (${field.description})`;
|
||||
}
|
||||
if (field.enum) {
|
||||
prompt += ` [options: ${field.enum.join(', ')}]`;
|
||||
}
|
||||
if (field.type === 'number' || field.type === 'integer') {
|
||||
if (field.minimum !== undefined && field.maximum !== undefined) {
|
||||
prompt += ` [${field.minimum}-${field.maximum}]`;
|
||||
} else if (field.minimum !== undefined) {
|
||||
prompt += ` [min: ${field.minimum}]`;
|
||||
} else if (field.maximum !== undefined) {
|
||||
prompt += ` [max: ${field.maximum}]`;
|
||||
}
|
||||
}
|
||||
if (field.type === 'string' && field.format) {
|
||||
prompt += ` [format: ${field.format}]`;
|
||||
}
|
||||
if (isRequired) {
|
||||
prompt += ' *required*';
|
||||
}
|
||||
if (field.default !== undefined) {
|
||||
prompt += ` [default: ${field.default}]`;
|
||||
}
|
||||
|
||||
prompt += ': ';
|
||||
|
||||
const answer = await new Promise<string>((resolve) => {
|
||||
readline.question(prompt, (input) => {
|
||||
resolve(input.trim());
|
||||
});
|
||||
});
|
||||
|
||||
// Check for cancellation
|
||||
if (answer.toLowerCase() === 'cancel' || answer.toLowerCase() === 'c') {
|
||||
inputCancelled = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// Parse and validate the input
|
||||
try {
|
||||
if (answer === '' && field.default !== undefined) {
|
||||
content[fieldName] = field.default;
|
||||
} else if (answer === '' && !isRequired) {
|
||||
// Skip optional empty fields
|
||||
continue;
|
||||
} else if (answer === '') {
|
||||
throw new Error(`${fieldName} is required`);
|
||||
} else {
|
||||
// Parse the value based on type
|
||||
let parsedValue: unknown;
|
||||
|
||||
if (field.type === 'boolean') {
|
||||
parsedValue = answer.toLowerCase() === 'true' || answer.toLowerCase() === 'yes' || answer === '1';
|
||||
} else if (field.type === 'number') {
|
||||
parsedValue = parseFloat(answer);
|
||||
if (isNaN(parsedValue as number)) {
|
||||
throw new Error(`${fieldName} must be a valid number`);
|
||||
}
|
||||
} else if (field.type === 'integer') {
|
||||
parsedValue = parseInt(answer, 10);
|
||||
if (isNaN(parsedValue as number)) {
|
||||
throw new Error(`${fieldName} must be a valid integer`);
|
||||
}
|
||||
} else if (field.enum) {
|
||||
if (!field.enum.includes(answer)) {
|
||||
throw new Error(`${fieldName} must be one of: ${field.enum.join(', ')}`);
|
||||
}
|
||||
parsedValue = answer;
|
||||
} else {
|
||||
parsedValue = answer;
|
||||
}
|
||||
|
||||
content[fieldName] = parsedValue;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`❌ Error: ${error}`);
|
||||
// Continue to next attempt
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (inputCancelled) {
|
||||
return { action: 'cancel' };
|
||||
}
|
||||
|
||||
// If we didn't complete all fields due to an error, try again
|
||||
if (Object.keys(content).length !== Object.keys(properties).filter(name =>
|
||||
required.includes(name) || content[name] !== undefined
|
||||
).length) {
|
||||
if (attempts < maxAttempts) {
|
||||
console.log('Please try again...');
|
||||
continue;
|
||||
} else {
|
||||
console.log('Maximum attempts reached. Declining request.');
|
||||
return { action: 'decline' };
|
||||
}
|
||||
}
|
||||
|
||||
// Validate the complete object against the schema
|
||||
const isValid = validate(content);
|
||||
|
||||
if (!isValid) {
|
||||
console.log('❌ Validation errors:');
|
||||
validate.errors?.forEach(error => {
|
||||
console.log(` - ${error.dataPath || 'root'}: ${error.message}`);
|
||||
});
|
||||
|
||||
if (attempts < maxAttempts) {
|
||||
console.log('Please correct the errors and try again...');
|
||||
continue;
|
||||
} else {
|
||||
console.log('Maximum attempts reached. Declining request.');
|
||||
return { action: 'decline' };
|
||||
}
|
||||
}
|
||||
|
||||
// Show the collected data and ask for confirmation
|
||||
console.log('\n✅ Collected data:');
|
||||
console.log(JSON.stringify(content, null, 2));
|
||||
|
||||
const confirmAnswer = await new Promise<string>((resolve) => {
|
||||
readline.question('\nSubmit this information? (yes/no/cancel): ', (input) => {
|
||||
resolve(input.trim().toLowerCase());
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
if (confirmAnswer === 'yes' || confirmAnswer === 'y') {
|
||||
return {
|
||||
action: 'accept',
|
||||
content,
|
||||
};
|
||||
} else if (confirmAnswer === 'cancel' || confirmAnswer === 'c') {
|
||||
return { action: 'cancel' };
|
||||
} else if (confirmAnswer === 'no' || confirmAnswer === 'n') {
|
||||
if (attempts < maxAttempts) {
|
||||
console.log('Please re-enter the information...');
|
||||
continue;
|
||||
} else {
|
||||
return { action: 'decline' };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Maximum attempts reached. Declining request.');
|
||||
return { action: 'decline' };
|
||||
});
|
||||
|
||||
transport = new StreamableHTTPClientTransport(
|
||||
new URL(serverUrl),
|
||||
{
|
||||
|
|
@ -358,7 +562,7 @@ async function callTool(name: string, args: Record<string, unknown>): Promise<vo
|
|||
|
||||
console.log('Tool result:');
|
||||
const resourceLinks: ResourceLink[] = [];
|
||||
|
||||
|
||||
result.content.forEach(item => {
|
||||
if (item.type === 'text') {
|
||||
console.log(` ${item.text}`);
|
||||
|
|
@ -383,7 +587,7 @@ async function callTool(name: string, args: Record<string, unknown>): Promise<vo
|
|||
console.log(` [Unknown content type]:`, item);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Offer to read resource links
|
||||
if (resourceLinks.length > 0) {
|
||||
console.log(`\nFound ${resourceLinks.length} resource link(s). Use 'read-resource <uri>' to read their content.`);
|
||||
|
|
@ -403,6 +607,11 @@ async function callMultiGreetTool(name: string): Promise<void> {
|
|||
await callTool('multi-greet', { name });
|
||||
}
|
||||
|
||||
async function callCollectInfoTool(infoType: string): Promise<void> {
|
||||
console.log(`Testing elicitation with collect-user-info tool (${infoType})...`);
|
||||
await callTool('collect-user-info', { infoType });
|
||||
}
|
||||
|
||||
async function startNotifications(interval: number, count: number): Promise<void> {
|
||||
console.log(`Starting notification stream: interval=${interval}ms, count=${count || 'unlimited'}`);
|
||||
await callTool('start-notification-stream', { interval, count });
|
||||
|
|
@ -546,7 +755,7 @@ async function readResource(uri: string): Promise<void> {
|
|||
if (content.mimeType) {
|
||||
console.log(` Type: ${content.mimeType}`);
|
||||
}
|
||||
|
||||
|
||||
if ('text' in content && typeof content.text === 'string') {
|
||||
console.log(' Content:');
|
||||
console.log(' ---');
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { McpServer } from '../../server/mcp.js';
|
|||
import { StreamableHTTPServerTransport } from '../../server/streamableHttp.js';
|
||||
import { getOAuthProtectedResourceMetadataUrl, mcpAuthMetadataRouter } from '../../server/auth/router.js';
|
||||
import { requireBearerAuth } from '../../server/auth/middleware/bearerAuth.js';
|
||||
import { CallToolResult, GetPromptResult, isInitializeRequest, ReadResourceResult, ResourceLink } from '../../types.js';
|
||||
import { CallToolResult, GetPromptResult, isInitializeRequest, PrimitiveSchemaDefinition, ReadResourceResult, ResourceLink } from '../../types.js';
|
||||
import { InMemoryEventStore } from '../shared/inMemoryEventStore.js';
|
||||
import { setupAuthServer } from './demoInMemoryOAuthProvider.js';
|
||||
import { OAuthMetadata } from 'src/shared/auth.js';
|
||||
|
|
@ -86,6 +86,155 @@ const getServer = () => {
|
|||
};
|
||||
}
|
||||
);
|
||||
// Register a tool that demonstrates elicitation (user input collection)
|
||||
// This creates a closure that captures the server instance
|
||||
server.tool(
|
||||
'collect-user-info',
|
||||
'A tool that collects user information through elicitation',
|
||||
{
|
||||
infoType: z.enum(['contact', 'preferences', 'feedback']).describe('Type of information to collect'),
|
||||
},
|
||||
async ({ infoType }): Promise<CallToolResult> => {
|
||||
let message: string;
|
||||
let requestedSchema: {
|
||||
type: 'object';
|
||||
properties: Record<string, PrimitiveSchemaDefinition>;
|
||||
required?: string[];
|
||||
};
|
||||
|
||||
switch (infoType) {
|
||||
case 'contact':
|
||||
message = 'Please provide your contact information';
|
||||
requestedSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
title: 'Full Name',
|
||||
description: 'Your full name',
|
||||
},
|
||||
email: {
|
||||
type: 'string',
|
||||
title: 'Email Address',
|
||||
description: 'Your email address',
|
||||
format: 'email',
|
||||
},
|
||||
phone: {
|
||||
type: 'string',
|
||||
title: 'Phone Number',
|
||||
description: 'Your phone number (optional)',
|
||||
},
|
||||
},
|
||||
required: ['name', 'email'],
|
||||
};
|
||||
break;
|
||||
case 'preferences':
|
||||
message = 'Please set your preferences';
|
||||
requestedSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
theme: {
|
||||
type: 'string',
|
||||
title: 'Theme',
|
||||
description: 'Choose your preferred theme',
|
||||
enum: ['light', 'dark', 'auto'],
|
||||
enumNames: ['Light', 'Dark', 'Auto'],
|
||||
},
|
||||
notifications: {
|
||||
type: 'boolean',
|
||||
title: 'Enable Notifications',
|
||||
description: 'Would you like to receive notifications?',
|
||||
default: true,
|
||||
},
|
||||
frequency: {
|
||||
type: 'string',
|
||||
title: 'Notification Frequency',
|
||||
description: 'How often would you like notifications?',
|
||||
enum: ['daily', 'weekly', 'monthly'],
|
||||
enumNames: ['Daily', 'Weekly', 'Monthly'],
|
||||
},
|
||||
},
|
||||
required: ['theme'],
|
||||
};
|
||||
break;
|
||||
case 'feedback':
|
||||
message = 'Please provide your feedback';
|
||||
requestedSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
rating: {
|
||||
type: 'integer',
|
||||
title: 'Rating',
|
||||
description: 'Rate your experience (1-5)',
|
||||
minimum: 1,
|
||||
maximum: 5,
|
||||
},
|
||||
comments: {
|
||||
type: 'string',
|
||||
title: 'Comments',
|
||||
description: 'Additional comments (optional)',
|
||||
maxLength: 500,
|
||||
},
|
||||
recommend: {
|
||||
type: 'boolean',
|
||||
title: 'Would you recommend this?',
|
||||
description: 'Would you recommend this to others?',
|
||||
},
|
||||
},
|
||||
required: ['rating', 'recommend'],
|
||||
};
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown info type: ${infoType}`);
|
||||
}
|
||||
|
||||
try {
|
||||
// Use the underlying server instance to elicit input from the client
|
||||
const result = await server.server.elicitInput({
|
||||
message,
|
||||
requestedSchema,
|
||||
});
|
||||
|
||||
if (result.action === 'accept') {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Thank you! Collected ${infoType} information: ${JSON.stringify(result.content, null, 2)}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
} else if (result.action === 'decline') {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `No information was collected. User declined to provide ${infoType} information.`,
|
||||
},
|
||||
],
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Information collection was cancelled by the user.`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Error collecting ${infoType} information: ${error}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Register a simple prompt with title
|
||||
server.registerPrompt(
|
||||
|
|
@ -178,10 +327,10 @@ const getServer = () => {
|
|||
server.registerResource(
|
||||
'example-file-1',
|
||||
'file:///example/file1.txt',
|
||||
{
|
||||
{
|
||||
title: 'Example File 1',
|
||||
description: 'First example file for ResourceLink demonstration',
|
||||
mimeType: 'text/plain'
|
||||
mimeType: 'text/plain'
|
||||
},
|
||||
async (): Promise<ReadResourceResult> => {
|
||||
return {
|
||||
|
|
@ -198,10 +347,10 @@ const getServer = () => {
|
|||
server.registerResource(
|
||||
'example-file-2',
|
||||
'file:///example/file2.txt',
|
||||
{
|
||||
{
|
||||
title: 'Example File 2',
|
||||
description: 'Second example file for ResourceLink demonstration',
|
||||
mimeType: 'text/plain'
|
||||
mimeType: 'text/plain'
|
||||
},
|
||||
async (): Promise<ReadResourceResult> => {
|
||||
return {
|
||||
|
|
@ -328,7 +477,7 @@ if (useOAuth) {
|
|||
|
||||
authMiddleware = requireBearerAuth({
|
||||
verifier: tokenVerifier,
|
||||
requiredScopes: ['mcp:tools'],
|
||||
requiredScopes: [],
|
||||
resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl),
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { Client } from '../client/index.js';
|
|||
import { StreamableHTTPClientTransport } from '../client/streamableHttp.js';
|
||||
import { McpServer } from '../server/mcp.js';
|
||||
import { StreamableHTTPServerTransport } from '../server/streamableHttp.js';
|
||||
import { CallToolResultSchema, ListToolsResultSchema, ListResourcesResultSchema, ListPromptsResultSchema, DEFAULT_NEGOTIATED_PROTOCOL_VERSION } from '../types.js';
|
||||
import { CallToolResultSchema, ListToolsResultSchema, ListResourcesResultSchema, ListPromptsResultSchema, LATEST_PROTOCOL_VERSION } from '../types.js';
|
||||
import { z } from 'zod';
|
||||
|
||||
describe('Streamable HTTP Transport Session Management', () => {
|
||||
|
|
@ -145,7 +145,7 @@ describe('Streamable HTTP Transport Session Management', () => {
|
|||
params: {}
|
||||
}, ListToolsResultSchema);
|
||||
|
||||
|
||||
|
||||
});
|
||||
it('should operate without session management', async () => {
|
||||
// Create and connect a client
|
||||
|
|
@ -220,15 +220,15 @@ describe('Streamable HTTP Transport Session Management', () => {
|
|||
});
|
||||
|
||||
const transport = new StreamableHTTPClientTransport(baseUrl);
|
||||
|
||||
|
||||
// Verify protocol version is not set before connecting
|
||||
expect(transport.protocolVersion).toBeUndefined();
|
||||
|
||||
|
||||
await client.connect(transport);
|
||||
|
||||
|
||||
// Verify protocol version is set after connecting
|
||||
expect(transport.protocolVersion).toBe(DEFAULT_NEGOTIATED_PROTOCOL_VERSION);
|
||||
|
||||
expect(transport.protocolVersion).toBe(LATEST_PROTOCOL_VERSION);
|
||||
|
||||
// Clean up
|
||||
await transport.close();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@ export enum McpZodTypeKind {
|
|||
|
||||
export type CompleteCallback<T extends ZodTypeAny = ZodTypeAny> = (
|
||||
value: T["_input"],
|
||||
context?: {
|
||||
arguments?: Record<string, string>;
|
||||
},
|
||||
) => T["_input"][] | Promise<T["_input"][]>;
|
||||
|
||||
export interface CompletableDef<T extends ZodTypeAny = ZodTypeAny>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
LATEST_PROTOCOL_VERSION,
|
||||
SUPPORTED_PROTOCOL_VERSIONS,
|
||||
CreateMessageRequestSchema,
|
||||
ElicitRequestSchema,
|
||||
ListPromptsRequestSchema,
|
||||
ListResourcesRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
|
|
@ -267,6 +268,318 @@ test("should respect client capabilities", async () => {
|
|||
await expect(server.listRoots()).rejects.toThrow(/^Client does not support/);
|
||||
});
|
||||
|
||||
test("should respect client elicitation capabilities", async () => {
|
||||
const server = new Server(
|
||||
{
|
||||
name: "test server",
|
||||
version: "1.0",
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
prompts: {},
|
||||
resources: {},
|
||||
tools: {},
|
||||
logging: {},
|
||||
},
|
||||
enforceStrictCapabilities: true,
|
||||
},
|
||||
);
|
||||
|
||||
const client = new Client(
|
||||
{
|
||||
name: "test client",
|
||||
version: "1.0",
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
elicitation: {},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
client.setRequestHandler(ElicitRequestSchema, (params) => ({
|
||||
action: "accept",
|
||||
content: {
|
||||
username: params.params.message.includes("username") ? "test-user" : undefined,
|
||||
confirmed: true,
|
||||
},
|
||||
}));
|
||||
|
||||
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
||||
|
||||
await Promise.all([
|
||||
client.connect(clientTransport),
|
||||
server.connect(serverTransport),
|
||||
]);
|
||||
|
||||
expect(server.getClientCapabilities()).toEqual({ elicitation: {} });
|
||||
|
||||
// This should work because elicitation is supported by the client
|
||||
await expect(
|
||||
server.elicitInput({
|
||||
message: "Please provide your username",
|
||||
requestedSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
username: {
|
||||
type: "string",
|
||||
title: "Username",
|
||||
description: "Your username",
|
||||
},
|
||||
confirmed: {
|
||||
type: "boolean",
|
||||
title: "Confirm",
|
||||
description: "Please confirm",
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
required: ["username"],
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
action: "accept",
|
||||
content: {
|
||||
username: "test-user",
|
||||
confirmed: true,
|
||||
},
|
||||
});
|
||||
|
||||
// This should still throw because sampling is not supported by the client
|
||||
await expect(
|
||||
server.createMessage({
|
||||
messages: [],
|
||||
maxTokens: 10,
|
||||
}),
|
||||
).rejects.toThrow(/^Client does not support/);
|
||||
});
|
||||
|
||||
test("should validate elicitation response against requested schema", async () => {
|
||||
const server = new Server(
|
||||
{
|
||||
name: "test server",
|
||||
version: "1.0",
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
prompts: {},
|
||||
resources: {},
|
||||
tools: {},
|
||||
logging: {},
|
||||
},
|
||||
enforceStrictCapabilities: true,
|
||||
},
|
||||
);
|
||||
|
||||
const client = new Client(
|
||||
{
|
||||
name: "test client",
|
||||
version: "1.0",
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
elicitation: {},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// Set up client to return valid response
|
||||
client.setRequestHandler(ElicitRequestSchema, (request) => ({
|
||||
action: "accept",
|
||||
content: {
|
||||
name: "John Doe",
|
||||
email: "john@example.com",
|
||||
age: 30,
|
||||
},
|
||||
}));
|
||||
|
||||
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
||||
|
||||
await Promise.all([
|
||||
client.connect(clientTransport),
|
||||
server.connect(serverTransport),
|
||||
]);
|
||||
|
||||
// Test with valid response
|
||||
await expect(
|
||||
server.elicitInput({
|
||||
message: "Please provide your information",
|
||||
requestedSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: {
|
||||
type: "string",
|
||||
minLength: 1,
|
||||
},
|
||||
email: {
|
||||
type: "string",
|
||||
minLength: 1,
|
||||
},
|
||||
age: {
|
||||
type: "integer",
|
||||
minimum: 0,
|
||||
maximum: 150,
|
||||
},
|
||||
},
|
||||
required: ["name", "email"],
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
action: "accept",
|
||||
content: {
|
||||
name: "John Doe",
|
||||
email: "john@example.com",
|
||||
age: 30,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("should reject elicitation response with invalid data", async () => {
|
||||
const server = new Server(
|
||||
{
|
||||
name: "test server",
|
||||
version: "1.0",
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
prompts: {},
|
||||
resources: {},
|
||||
tools: {},
|
||||
logging: {},
|
||||
},
|
||||
enforceStrictCapabilities: true,
|
||||
},
|
||||
);
|
||||
|
||||
const client = new Client(
|
||||
{
|
||||
name: "test client",
|
||||
version: "1.0",
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
elicitation: {},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// Set up client to return invalid response (missing required field, invalid age)
|
||||
client.setRequestHandler(ElicitRequestSchema, (request) => ({
|
||||
action: "accept",
|
||||
content: {
|
||||
email: "", // Invalid - too short
|
||||
age: -5, // Invalid age
|
||||
},
|
||||
}));
|
||||
|
||||
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
||||
|
||||
await Promise.all([
|
||||
client.connect(clientTransport),
|
||||
server.connect(serverTransport),
|
||||
]);
|
||||
|
||||
// Test with invalid response
|
||||
await expect(
|
||||
server.elicitInput({
|
||||
message: "Please provide your information",
|
||||
requestedSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: {
|
||||
type: "string",
|
||||
minLength: 1,
|
||||
},
|
||||
email: {
|
||||
type: "string",
|
||||
minLength: 1,
|
||||
},
|
||||
age: {
|
||||
type: "integer",
|
||||
minimum: 0,
|
||||
maximum: 150,
|
||||
},
|
||||
},
|
||||
required: ["name", "email"],
|
||||
},
|
||||
}),
|
||||
).rejects.toThrow(/does not match requested schema/);
|
||||
});
|
||||
|
||||
test("should allow elicitation decline and cancel without validation", async () => {
|
||||
const server = new Server(
|
||||
{
|
||||
name: "test server",
|
||||
version: "1.0",
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
prompts: {},
|
||||
resources: {},
|
||||
tools: {},
|
||||
logging: {},
|
||||
},
|
||||
enforceStrictCapabilities: true,
|
||||
},
|
||||
);
|
||||
|
||||
const client = new Client(
|
||||
{
|
||||
name: "test client",
|
||||
version: "1.0",
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
elicitation: {},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
let requestCount = 0;
|
||||
client.setRequestHandler(ElicitRequestSchema, (request) => {
|
||||
requestCount++;
|
||||
if (requestCount === 1) {
|
||||
return { action: "decline" };
|
||||
} else {
|
||||
return { action: "cancel" };
|
||||
}
|
||||
});
|
||||
|
||||
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
||||
|
||||
await Promise.all([
|
||||
client.connect(clientTransport),
|
||||
server.connect(serverTransport),
|
||||
]);
|
||||
|
||||
const schema = {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
name: { type: "string" as const },
|
||||
},
|
||||
required: ["name"],
|
||||
};
|
||||
|
||||
// Test decline - should not validate
|
||||
await expect(
|
||||
server.elicitInput({
|
||||
message: "Please provide your name",
|
||||
requestedSchema: schema,
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
action: "decline",
|
||||
});
|
||||
|
||||
// Test cancel - should not validate
|
||||
await expect(
|
||||
server.elicitInput({
|
||||
message: "Please provide your name",
|
||||
requestedSchema: schema,
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
action: "cancel",
|
||||
});
|
||||
});
|
||||
|
||||
test("should respect server notification capabilities", async () => {
|
||||
const server = new Server(
|
||||
{
|
||||
|
|
|
|||
|
|
@ -8,6 +8,9 @@ import {
|
|||
ClientCapabilities,
|
||||
CreateMessageRequest,
|
||||
CreateMessageResultSchema,
|
||||
ElicitRequest,
|
||||
ElicitResult,
|
||||
ElicitResultSchema,
|
||||
EmptyResultSchema,
|
||||
Implementation,
|
||||
InitializedNotificationSchema,
|
||||
|
|
@ -18,6 +21,8 @@ import {
|
|||
ListRootsRequest,
|
||||
ListRootsResultSchema,
|
||||
LoggingMessageNotification,
|
||||
McpError,
|
||||
ErrorCode,
|
||||
Notification,
|
||||
Request,
|
||||
ResourceUpdatedNotification,
|
||||
|
|
@ -28,6 +33,7 @@ import {
|
|||
ServerResult,
|
||||
SUPPORTED_PROTOCOL_VERSIONS,
|
||||
} from "../types.js";
|
||||
import Ajv from "ajv";
|
||||
|
||||
export type ServerOptions = ProtocolOptions & {
|
||||
/**
|
||||
|
|
@ -129,6 +135,14 @@ export class Server<
|
|||
}
|
||||
break;
|
||||
|
||||
case "elicitation/create":
|
||||
if (!this._clientCapabilities?.elicitation) {
|
||||
throw new Error(
|
||||
`Client does not support elicitation (required for ${method})`,
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case "roots/list":
|
||||
if (!this._clientCapabilities?.roots) {
|
||||
throw new Error(
|
||||
|
|
@ -296,6 +310,44 @@ export class Server<
|
|||
);
|
||||
}
|
||||
|
||||
async elicitInput(
|
||||
params: ElicitRequest["params"],
|
||||
options?: RequestOptions,
|
||||
): Promise<ElicitResult> {
|
||||
const result = await this.request(
|
||||
{ method: "elicitation/create", params },
|
||||
ElicitResultSchema,
|
||||
options,
|
||||
);
|
||||
|
||||
// Validate the response content against the requested schema if action is "accept"
|
||||
if (result.action === "accept" && result.content) {
|
||||
try {
|
||||
const ajv = new Ajv();
|
||||
|
||||
const validate = ajv.compile(params.requestedSchema);
|
||||
const isValid = validate(result.content);
|
||||
|
||||
if (!isValid) {
|
||||
throw new McpError(
|
||||
ErrorCode.InvalidParams,
|
||||
`Elicitation response content does not match requested schema: ${ajv.errorsText(validate.errors)}`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof McpError) {
|
||||
throw error;
|
||||
}
|
||||
throw new McpError(
|
||||
ErrorCode.InternalError,
|
||||
`Error validating elicitation response: ${error}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async listRoots(
|
||||
params?: ListRootsRequest["params"],
|
||||
options?: RequestOptions,
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import {
|
|||
LoggingMessageNotificationSchema,
|
||||
Notification,
|
||||
TextContent,
|
||||
ElicitRequestSchema,
|
||||
} from "../types.js";
|
||||
import { ResourceTemplate } from "./mcp.js";
|
||||
import { completable } from "./completable.js";
|
||||
|
|
@ -3521,12 +3522,12 @@ describe("prompt()", () => {
|
|||
);
|
||||
|
||||
expect(result.resources).toHaveLength(2);
|
||||
|
||||
|
||||
// Resource 1 should have its own metadata
|
||||
expect(result.resources[0].name).toBe("Resource 1");
|
||||
expect(result.resources[0].description).toBe("Individual resource description");
|
||||
expect(result.resources[0].mimeType).toBe("text/plain");
|
||||
|
||||
|
||||
// Resource 2 should inherit template metadata
|
||||
expect(result.resources[1].name).toBe("Resource 2");
|
||||
expect(result.resources[1].description).toBe("Template description");
|
||||
|
|
@ -3592,7 +3593,7 @@ describe("prompt()", () => {
|
|||
);
|
||||
|
||||
expect(result.resources).toHaveLength(1);
|
||||
|
||||
|
||||
// All fields should be from the individual resource, not the template
|
||||
expect(result.resources[0].name).toBe("Overridden Name");
|
||||
expect(result.resources[0].description).toBe("Overridden description");
|
||||
|
|
@ -3698,41 +3699,532 @@ describe("Tool title precedence", () => {
|
|||
});
|
||||
|
||||
test("getDisplayName unit tests for title precedence", () => {
|
||||
|
||||
|
||||
// Test 1: Only name
|
||||
expect(getDisplayName({ name: "tool_name" })).toBe("tool_name");
|
||||
|
||||
|
||||
// Test 2: Name and title - title wins
|
||||
expect(getDisplayName({
|
||||
name: "tool_name",
|
||||
title: "Tool Title"
|
||||
expect(getDisplayName({
|
||||
name: "tool_name",
|
||||
title: "Tool Title"
|
||||
})).toBe("Tool Title");
|
||||
|
||||
|
||||
// Test 3: Name and annotations.title - annotations.title wins
|
||||
expect(getDisplayName({
|
||||
expect(getDisplayName({
|
||||
name: "tool_name",
|
||||
annotations: { title: "Annotations Title" }
|
||||
})).toBe("Annotations Title");
|
||||
|
||||
|
||||
// Test 4: All three - title wins (correct precedence)
|
||||
expect(getDisplayName({
|
||||
name: "tool_name",
|
||||
expect(getDisplayName({
|
||||
name: "tool_name",
|
||||
title: "Regular Title",
|
||||
annotations: { title: "Annotations Title" }
|
||||
})).toBe("Regular Title");
|
||||
|
||||
|
||||
// Test 5: Empty title should not be used
|
||||
expect(getDisplayName({
|
||||
name: "tool_name",
|
||||
expect(getDisplayName({
|
||||
name: "tool_name",
|
||||
title: "",
|
||||
annotations: { title: "Annotations Title" }
|
||||
})).toBe("Annotations Title");
|
||||
|
||||
|
||||
// Test 6: Undefined vs null handling
|
||||
expect(getDisplayName({
|
||||
name: "tool_name",
|
||||
expect(getDisplayName({
|
||||
name: "tool_name",
|
||||
title: undefined,
|
||||
annotations: { title: "Annotations Title" }
|
||||
})).toBe("Annotations Title");
|
||||
});
|
||||
|
||||
test("should support resource template completion with resolved context", async () => {
|
||||
const mcpServer = new McpServer({
|
||||
name: "test server",
|
||||
version: "1.0",
|
||||
});
|
||||
|
||||
const client = new Client({
|
||||
name: "test client",
|
||||
version: "1.0",
|
||||
});
|
||||
|
||||
mcpServer.registerResource(
|
||||
"test",
|
||||
new ResourceTemplate("github://repos/{owner}/{repo}", {
|
||||
list: undefined,
|
||||
complete: {
|
||||
repo: (value, context) => {
|
||||
if (context?.arguments?.["owner"] === "org1") {
|
||||
return ["project1", "project2", "project3"].filter(r => r.startsWith(value));
|
||||
} else if (context?.arguments?.["owner"] === "org2") {
|
||||
return ["repo1", "repo2", "repo3"].filter(r => r.startsWith(value));
|
||||
}
|
||||
return [];
|
||||
},
|
||||
},
|
||||
}),
|
||||
{
|
||||
title: "GitHub Repository",
|
||||
description: "Repository information"
|
||||
},
|
||||
async () => ({
|
||||
contents: [
|
||||
{
|
||||
uri: "github://repos/test/test",
|
||||
text: "Test content",
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
const [clientTransport, serverTransport] =
|
||||
InMemoryTransport.createLinkedPair();
|
||||
|
||||
await Promise.all([
|
||||
client.connect(clientTransport),
|
||||
mcpServer.server.connect(serverTransport),
|
||||
]);
|
||||
|
||||
// Test with microsoft owner
|
||||
const result1 = await client.request(
|
||||
{
|
||||
method: "completion/complete",
|
||||
params: {
|
||||
ref: {
|
||||
type: "ref/resource",
|
||||
uri: "github://repos/{owner}/{repo}",
|
||||
},
|
||||
argument: {
|
||||
name: "repo",
|
||||
value: "p",
|
||||
},
|
||||
context: {
|
||||
arguments: {
|
||||
owner: "org1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
CompleteResultSchema,
|
||||
);
|
||||
|
||||
expect(result1.completion.values).toEqual(["project1", "project2", "project3"]);
|
||||
expect(result1.completion.total).toBe(3);
|
||||
|
||||
// Test with facebook owner
|
||||
const result2 = await client.request(
|
||||
{
|
||||
method: "completion/complete",
|
||||
params: {
|
||||
ref: {
|
||||
type: "ref/resource",
|
||||
uri: "github://repos/{owner}/{repo}",
|
||||
},
|
||||
argument: {
|
||||
name: "repo",
|
||||
value: "r",
|
||||
},
|
||||
context: {
|
||||
arguments: {
|
||||
owner: "org2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
CompleteResultSchema,
|
||||
);
|
||||
|
||||
expect(result2.completion.values).toEqual(["repo1", "repo2", "repo3"]);
|
||||
expect(result2.completion.total).toBe(3);
|
||||
|
||||
// Test with no resolved context
|
||||
const result3 = await client.request(
|
||||
{
|
||||
method: "completion/complete",
|
||||
params: {
|
||||
ref: {
|
||||
type: "ref/resource",
|
||||
uri: "github://repos/{owner}/{repo}",
|
||||
},
|
||||
argument: {
|
||||
name: "repo",
|
||||
value: "t",
|
||||
},
|
||||
},
|
||||
},
|
||||
CompleteResultSchema,
|
||||
);
|
||||
|
||||
expect(result3.completion.values).toEqual([]);
|
||||
expect(result3.completion.total).toBe(0);
|
||||
});
|
||||
|
||||
test("should support prompt argument completion with resolved context", async () => {
|
||||
const mcpServer = new McpServer({
|
||||
name: "test server",
|
||||
version: "1.0",
|
||||
});
|
||||
|
||||
const client = new Client({
|
||||
name: "test client",
|
||||
version: "1.0",
|
||||
});
|
||||
|
||||
mcpServer.registerPrompt(
|
||||
"test-prompt",
|
||||
{
|
||||
title: "Team Greeting",
|
||||
description: "Generate a greeting for team members",
|
||||
argsSchema: {
|
||||
department: completable(z.string(), (value) => {
|
||||
return ["engineering", "sales", "marketing", "support"].filter(d => d.startsWith(value));
|
||||
}),
|
||||
name: completable(z.string(), (value, context) => {
|
||||
const department = context?.arguments?.["department"];
|
||||
if (department === "engineering") {
|
||||
return ["Alice", "Bob", "Charlie"].filter(n => n.startsWith(value));
|
||||
} else if (department === "sales") {
|
||||
return ["David", "Eve", "Frank"].filter(n => n.startsWith(value));
|
||||
} else if (department === "marketing") {
|
||||
return ["Grace", "Henry", "Iris"].filter(n => n.startsWith(value));
|
||||
}
|
||||
return ["Guest"].filter(n => n.startsWith(value));
|
||||
}),
|
||||
}
|
||||
},
|
||||
async ({ department, name }) => ({
|
||||
messages: [
|
||||
{
|
||||
role: "assistant",
|
||||
content: {
|
||||
type: "text",
|
||||
text: `Hello ${name}, welcome to the ${department} team!`,
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
const [clientTransport, serverTransport] =
|
||||
InMemoryTransport.createLinkedPair();
|
||||
|
||||
await Promise.all([
|
||||
client.connect(clientTransport),
|
||||
mcpServer.server.connect(serverTransport),
|
||||
]);
|
||||
|
||||
// Test with engineering department
|
||||
const result1 = await client.request(
|
||||
{
|
||||
method: "completion/complete",
|
||||
params: {
|
||||
ref: {
|
||||
type: "ref/prompt",
|
||||
name: "test-prompt",
|
||||
},
|
||||
argument: {
|
||||
name: "name",
|
||||
value: "A",
|
||||
},
|
||||
context: {
|
||||
arguments: {
|
||||
department: "engineering",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
CompleteResultSchema,
|
||||
);
|
||||
|
||||
expect(result1.completion.values).toEqual(["Alice"]);
|
||||
|
||||
// Test with sales department
|
||||
const result2 = await client.request(
|
||||
{
|
||||
method: "completion/complete",
|
||||
params: {
|
||||
ref: {
|
||||
type: "ref/prompt",
|
||||
name: "test-prompt",
|
||||
},
|
||||
argument: {
|
||||
name: "name",
|
||||
value: "D",
|
||||
},
|
||||
context: {
|
||||
arguments: {
|
||||
department: "sales",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
CompleteResultSchema,
|
||||
);
|
||||
|
||||
expect(result2.completion.values).toEqual(["David"]);
|
||||
|
||||
// Test with marketing department
|
||||
const result3 = await client.request(
|
||||
{
|
||||
method: "completion/complete",
|
||||
params: {
|
||||
ref: {
|
||||
type: "ref/prompt",
|
||||
name: "test-prompt",
|
||||
},
|
||||
argument: {
|
||||
name: "name",
|
||||
value: "G",
|
||||
},
|
||||
context: {
|
||||
arguments: {
|
||||
department: "marketing",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
CompleteResultSchema,
|
||||
);
|
||||
|
||||
expect(result3.completion.values).toEqual(["Grace"]);
|
||||
|
||||
// Test with no resolved context
|
||||
const result4 = await client.request(
|
||||
{
|
||||
method: "completion/complete",
|
||||
params: {
|
||||
ref: {
|
||||
type: "ref/prompt",
|
||||
name: "test-prompt",
|
||||
},
|
||||
argument: {
|
||||
name: "name",
|
||||
value: "G",
|
||||
},
|
||||
},
|
||||
},
|
||||
CompleteResultSchema,
|
||||
);
|
||||
|
||||
expect(result4.completion.values).toEqual(["Guest"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("elicitInput()", () => {
|
||||
|
||||
const checkAvailability = jest.fn().mockResolvedValue(false);
|
||||
const findAlternatives = jest.fn().mockResolvedValue([]);
|
||||
const makeBooking = jest.fn().mockResolvedValue("BOOKING-123");
|
||||
|
||||
let mcpServer: McpServer;
|
||||
let client: Client;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Create server with restaurant booking tool
|
||||
mcpServer = new McpServer({
|
||||
name: "restaurant-booking-server",
|
||||
version: "1.0.0",
|
||||
});
|
||||
|
||||
// Register the restaurant booking tool from README example
|
||||
mcpServer.tool(
|
||||
"book-restaurant",
|
||||
{
|
||||
restaurant: z.string(),
|
||||
date: z.string(),
|
||||
partySize: z.number()
|
||||
},
|
||||
async ({ restaurant, date, partySize }) => {
|
||||
// Check availability
|
||||
const available = await checkAvailability(restaurant, date, partySize);
|
||||
|
||||
if (!available) {
|
||||
// Ask user if they want to try alternative dates
|
||||
const result = await mcpServer.server.elicitInput({
|
||||
message: `No tables available at ${restaurant} on ${date}. Would you like to check alternative dates?`,
|
||||
requestedSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
checkAlternatives: {
|
||||
type: "boolean",
|
||||
title: "Check alternative dates",
|
||||
description: "Would you like me to check other dates?"
|
||||
},
|
||||
flexibleDates: {
|
||||
type: "string",
|
||||
title: "Date flexibility",
|
||||
description: "How flexible are your dates?",
|
||||
enum: ["next_day", "same_week", "next_week"],
|
||||
enumNames: ["Next day", "Same week", "Next week"]
|
||||
}
|
||||
},
|
||||
required: ["checkAlternatives"]
|
||||
}
|
||||
});
|
||||
|
||||
if (result.action === "accept" && result.content?.checkAlternatives) {
|
||||
const alternatives = await findAlternatives(
|
||||
restaurant,
|
||||
date,
|
||||
partySize,
|
||||
result.content.flexibleDates as string
|
||||
);
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `Found these alternatives: ${alternatives.join(", ")}`
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: "No booking made. Original date not available."
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
await makeBooking(restaurant, date, partySize);
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `Booked table for ${partySize} at ${restaurant} on ${date}`
|
||||
}]
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// Create client with elicitation capability
|
||||
client = new Client(
|
||||
{
|
||||
name: "test-client",
|
||||
version: "1.0.0",
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
elicitation: {},
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test("should successfully elicit additional information", async () => {
|
||||
// Mock availability check to return false
|
||||
checkAvailability.mockResolvedValue(false);
|
||||
findAlternatives.mockResolvedValue(["2024-12-26", "2024-12-27", "2024-12-28"]);
|
||||
|
||||
// Set up client to accept alternative date checking
|
||||
client.setRequestHandler(ElicitRequestSchema, async (request) => {
|
||||
expect(request.params.message).toContain("No tables available at ABC Restaurant on 2024-12-25");
|
||||
return {
|
||||
action: "accept",
|
||||
content: {
|
||||
checkAlternatives: true,
|
||||
flexibleDates: "same_week"
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
||||
|
||||
await Promise.all([
|
||||
client.connect(clientTransport),
|
||||
mcpServer.server.connect(serverTransport),
|
||||
]);
|
||||
|
||||
// Call the tool
|
||||
const result = await client.callTool({
|
||||
name: "book-restaurant",
|
||||
arguments: {
|
||||
restaurant: "ABC Restaurant",
|
||||
date: "2024-12-25",
|
||||
partySize: 2
|
||||
}
|
||||
});
|
||||
|
||||
expect(checkAvailability).toHaveBeenCalledWith("ABC Restaurant", "2024-12-25", 2);
|
||||
expect(findAlternatives).toHaveBeenCalledWith("ABC Restaurant", "2024-12-25", 2, "same_week");
|
||||
expect(result.content).toEqual([{
|
||||
type: "text",
|
||||
text: "Found these alternatives: 2024-12-26, 2024-12-27, 2024-12-28"
|
||||
}]);
|
||||
});
|
||||
|
||||
test("should handle user declining to elicitation request", async () => {
|
||||
// Mock availability check to return false
|
||||
checkAvailability.mockResolvedValue(false);
|
||||
|
||||
// Set up client to decline alternative date checking
|
||||
client.setRequestHandler(ElicitRequestSchema, async () => {
|
||||
return {
|
||||
action: "accept",
|
||||
content: {
|
||||
checkAlternatives: false
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
||||
|
||||
await Promise.all([
|
||||
client.connect(clientTransport),
|
||||
mcpServer.server.connect(serverTransport),
|
||||
]);
|
||||
|
||||
// Call the tool
|
||||
const result = await client.callTool({
|
||||
name: "book-restaurant",
|
||||
arguments: {
|
||||
restaurant: "ABC Restaurant",
|
||||
date: "2024-12-25",
|
||||
partySize: 2
|
||||
}
|
||||
});
|
||||
|
||||
expect(checkAvailability).toHaveBeenCalledWith("ABC Restaurant", "2024-12-25", 2);
|
||||
expect(findAlternatives).not.toHaveBeenCalled();
|
||||
expect(result.content).toEqual([{
|
||||
type: "text",
|
||||
text: "No booking made. Original date not available."
|
||||
}]);
|
||||
});
|
||||
|
||||
test("should handle user cancelling the elicitation", async () => {
|
||||
// Mock availability check to return false
|
||||
checkAvailability.mockResolvedValue(false);
|
||||
|
||||
// Set up client to cancel the elicitation
|
||||
client.setRequestHandler(ElicitRequestSchema, async () => {
|
||||
return {
|
||||
action: "cancel"
|
||||
};
|
||||
});
|
||||
|
||||
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
||||
|
||||
await Promise.all([
|
||||
client.connect(clientTransport),
|
||||
mcpServer.server.connect(serverTransport),
|
||||
]);
|
||||
|
||||
// Call the tool
|
||||
const result = await client.callTool({
|
||||
name: "book-restaurant",
|
||||
arguments: {
|
||||
restaurant: "ABC Restaurant",
|
||||
date: "2024-12-25",
|
||||
partySize: 2
|
||||
}
|
||||
});
|
||||
|
||||
expect(checkAvailability).toHaveBeenCalledWith("ABC Restaurant", "2024-12-25", 2);
|
||||
expect(findAlternatives).not.toHaveBeenCalled();
|
||||
expect(result.content).toEqual([{
|
||||
type: "text",
|
||||
text: "No booking made. Original date not available."
|
||||
}]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -293,7 +293,7 @@ export class McpServer {
|
|||
}
|
||||
|
||||
const def: CompletableDef<ZodString> = field._def;
|
||||
const suggestions = await def.complete(request.params.argument.value);
|
||||
const suggestions = await def.complete(request.params.argument.value, request.params.context);
|
||||
return createCompletionResult(suggestions);
|
||||
}
|
||||
|
||||
|
|
@ -324,7 +324,7 @@ export class McpServer {
|
|||
return EMPTY_COMPLETION_RESULT;
|
||||
}
|
||||
|
||||
const suggestions = await completer(request.params.argument.value);
|
||||
const suggestions = await completer(request.params.argument.value, request.params.context);
|
||||
return createCompletionResult(suggestions);
|
||||
}
|
||||
|
||||
|
|
@ -1068,6 +1068,9 @@ export class McpServer {
|
|||
*/
|
||||
export type CompleteResourceTemplateCallback = (
|
||||
value: string,
|
||||
context?: {
|
||||
arguments?: Record<string, string>;
|
||||
},
|
||||
) => string[] | Promise<string[]>;
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export function getDisplayName(metadata: BaseMetadata): string {
|
|||
if (metadata.title !== undefined && metadata.title !== '') {
|
||||
return metadata.title;
|
||||
}
|
||||
|
||||
|
||||
// Then check for annotations.title (only present in Tool objects)
|
||||
if ('annotations' in metadata) {
|
||||
const metadataWithAnnotations = metadata as BaseMetadata & { annotations?: { title?: string } };
|
||||
|
|
@ -23,14 +23,7 @@ export function getDisplayName(metadata: BaseMetadata): string {
|
|||
return metadataWithAnnotations.annotations.title;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Finally fall back to name
|
||||
return metadata.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an object has a custom title different from its name.
|
||||
*/
|
||||
export function hasCustomTitle(metadata: BaseMetadata): boolean {
|
||||
return metadata.title !== undefined && metadata.title !== metadata.name;
|
||||
}
|
||||
|
|
@ -465,6 +465,7 @@ describe("mergeCapabilities", () => {
|
|||
experimental: {
|
||||
feature: true,
|
||||
},
|
||||
elicitation: {},
|
||||
roots: {
|
||||
newProp: true,
|
||||
},
|
||||
|
|
@ -473,6 +474,7 @@ describe("mergeCapabilities", () => {
|
|||
const merged = mergeCapabilities(base, additional);
|
||||
expect(merged).toEqual({
|
||||
sampling: {},
|
||||
elicitation: {},
|
||||
roots: {
|
||||
listChanged: true,
|
||||
newProp: true,
|
||||
|
|
|
|||
|
|
@ -4,14 +4,15 @@ import {
|
|||
ResourceLinkSchema,
|
||||
ContentBlockSchema,
|
||||
PromptMessageSchema,
|
||||
CallToolResultSchema
|
||||
CallToolResultSchema,
|
||||
CompleteRequestSchema
|
||||
} from "./types.js";
|
||||
|
||||
describe("Types", () => {
|
||||
|
||||
test("should have correct latest protocol version", () => {
|
||||
expect(LATEST_PROTOCOL_VERSION).toBeDefined();
|
||||
expect(LATEST_PROTOCOL_VERSION).toBe("2025-03-26");
|
||||
expect(LATEST_PROTOCOL_VERSION).toBe("2025-06-18");
|
||||
});
|
||||
test("should have correct supported protocol versions", () => {
|
||||
expect(SUPPORTED_PROTOCOL_VERSIONS).toBeDefined();
|
||||
|
|
@ -19,6 +20,7 @@ describe("Types", () => {
|
|||
expect(SUPPORTED_PROTOCOL_VERSIONS).toContain(LATEST_PROTOCOL_VERSION);
|
||||
expect(SUPPORTED_PROTOCOL_VERSIONS).toContain("2024-11-05");
|
||||
expect(SUPPORTED_PROTOCOL_VERSIONS).toContain("2024-10-07");
|
||||
expect(SUPPORTED_PROTOCOL_VERSIONS).toContain("2025-03-26");
|
||||
});
|
||||
|
||||
describe("ResourceLink", () => {
|
||||
|
|
@ -223,4 +225,91 @@ describe("Types", () => {
|
|||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("CompleteRequest", () => {
|
||||
test("should validate a CompleteRequest without resolved field", () => {
|
||||
const request = {
|
||||
method: "completion/complete",
|
||||
params: {
|
||||
ref: { type: "ref/prompt", name: "greeting" },
|
||||
argument: { name: "name", value: "A" }
|
||||
}
|
||||
};
|
||||
|
||||
const result = CompleteRequestSchema.safeParse(request);
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.method).toBe("completion/complete");
|
||||
expect(result.data.params.ref.type).toBe("ref/prompt");
|
||||
expect(result.data.params.context).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
test("should validate a CompleteRequest with resolved field", () => {
|
||||
const request = {
|
||||
method: "completion/complete",
|
||||
params: {
|
||||
ref: { type: "ref/resource", uri: "github://repos/{owner}/{repo}" },
|
||||
argument: { name: "repo", value: "t" },
|
||||
context: {
|
||||
arguments: {
|
||||
"{owner}": "microsoft"
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const result = CompleteRequestSchema.safeParse(request);
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.params.context?.arguments).toEqual({
|
||||
"{owner}": "microsoft"
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test("should validate a CompleteRequest with empty resolved field", () => {
|
||||
const request = {
|
||||
method: "completion/complete",
|
||||
params: {
|
||||
ref: { type: "ref/prompt", name: "test" },
|
||||
argument: { name: "arg", value: "" },
|
||||
context: {
|
||||
arguments: {}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const result = CompleteRequestSchema.safeParse(request);
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.params.context?.arguments).toEqual({});
|
||||
}
|
||||
});
|
||||
|
||||
test("should validate a CompleteRequest with multiple resolved variables", () => {
|
||||
const request = {
|
||||
method: "completion/complete",
|
||||
params: {
|
||||
ref: { type: "ref/resource", uri: "api://v1/{tenant}/{resource}/{id}" },
|
||||
argument: { name: "id", value: "123" },
|
||||
context: {
|
||||
arguments: {
|
||||
"{tenant}": "acme-corp",
|
||||
"{resource}": "users"
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const result = CompleteRequestSchema.safeParse(request);
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.params.context?.arguments).toEqual({
|
||||
"{tenant}": "acme-corp",
|
||||
"{resource}": "users"
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
187
src/types.ts
187
src/types.ts
|
|
@ -1,9 +1,10 @@
|
|||
import { z, ZodTypeAny } from "zod";
|
||||
|
||||
export const LATEST_PROTOCOL_VERSION = "2025-03-26";
|
||||
export const LATEST_PROTOCOL_VERSION = "2025-06-18";
|
||||
export const DEFAULT_NEGOTIATED_PROTOCOL_VERSION = "2025-03-26";
|
||||
export const SUPPORTED_PROTOCOL_VERSIONS = [
|
||||
LATEST_PROTOCOL_VERSION,
|
||||
"2025-03-26",
|
||||
"2024-11-05",
|
||||
"2024-10-07",
|
||||
];
|
||||
|
|
@ -44,8 +45,9 @@ export const RequestSchema = z.object({
|
|||
const BaseNotificationParamsSchema = z
|
||||
.object({
|
||||
/**
|
||||
* Reserved by MCP for protocol-level metadata; implementations must not make assumptions about its contents.
|
||||
*/
|
||||
* See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
|
||||
* for notes on _meta usage.
|
||||
*/
|
||||
_meta: z.optional(z.object({}).passthrough()),
|
||||
})
|
||||
.passthrough();
|
||||
|
|
@ -58,8 +60,9 @@ export const NotificationSchema = z.object({
|
|||
export const ResultSchema = z
|
||||
.object({
|
||||
/**
|
||||
* Reserved by MCP for protocol-level metadata; implementations must not make assumptions about its contents.
|
||||
*/
|
||||
* See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
|
||||
* for notes on _meta usage.
|
||||
*/
|
||||
_meta: z.optional(z.object({}).passthrough()),
|
||||
})
|
||||
.passthrough();
|
||||
|
|
@ -236,6 +239,10 @@ export const ClientCapabilitiesSchema = z
|
|||
* Present if the client supports sampling from an LLM.
|
||||
*/
|
||||
sampling: z.optional(z.object({}).passthrough()),
|
||||
/**
|
||||
* Present if the client supports eliciting user input.
|
||||
*/
|
||||
elicitation: z.optional(z.object({}).passthrough()),
|
||||
/**
|
||||
* Present if the client supports listing roots.
|
||||
*/
|
||||
|
|
@ -435,6 +442,11 @@ export const ResourceContentsSchema = z
|
|||
* The MIME type of this resource, if known.
|
||||
*/
|
||||
mimeType: z.optional(z.string()),
|
||||
/**
|
||||
* See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
|
||||
* for notes on _meta usage.
|
||||
*/
|
||||
_meta: z.optional(z.object({}).passthrough()),
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
|
|
@ -474,8 +486,9 @@ export const ResourceSchema = BaseMetadataSchema.extend({
|
|||
mimeType: z.optional(z.string()),
|
||||
|
||||
/**
|
||||
* Reserved by MCP for protocol-level metadata; implementations must not make assumptions about its contents.
|
||||
*/
|
||||
* See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
|
||||
* for notes on _meta usage.
|
||||
*/
|
||||
_meta: z.optional(z.object({}).passthrough()),
|
||||
});
|
||||
|
||||
|
|
@ -501,8 +514,9 @@ export const ResourceTemplateSchema = BaseMetadataSchema.extend({
|
|||
mimeType: z.optional(z.string()),
|
||||
|
||||
/**
|
||||
* Reserved by MCP for protocol-level metadata; implementations must not make assumptions about its contents.
|
||||
*/
|
||||
* See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
|
||||
* for notes on _meta usage.
|
||||
*/
|
||||
_meta: z.optional(z.object({}).passthrough()),
|
||||
});
|
||||
|
||||
|
|
@ -637,6 +651,11 @@ export const PromptSchema = BaseMetadataSchema.extend({
|
|||
* A list of arguments to use for templating the prompt.
|
||||
*/
|
||||
arguments: z.optional(z.array(PromptArgumentSchema)),
|
||||
/**
|
||||
* See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
|
||||
* for notes on _meta usage.
|
||||
*/
|
||||
_meta: z.optional(z.object({}).passthrough()),
|
||||
});
|
||||
|
||||
/**
|
||||
|
|
@ -682,8 +701,9 @@ export const TextContentSchema = z
|
|||
text: z.string(),
|
||||
|
||||
/**
|
||||
* Reserved by MCP for protocol-level metadata; implementations must not make assumptions about its contents.
|
||||
*/
|
||||
* See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
|
||||
* for notes on _meta usage.
|
||||
*/
|
||||
_meta: z.optional(z.object({}).passthrough()),
|
||||
})
|
||||
.passthrough();
|
||||
|
|
@ -704,8 +724,9 @@ export const ImageContentSchema = z
|
|||
mimeType: z.string(),
|
||||
|
||||
/**
|
||||
* Reserved by MCP for protocol-level metadata; implementations must not make assumptions about its contents.
|
||||
*/
|
||||
* See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
|
||||
* for notes on _meta usage.
|
||||
*/
|
||||
_meta: z.optional(z.object({}).passthrough()),
|
||||
})
|
||||
.passthrough();
|
||||
|
|
@ -726,8 +747,9 @@ export const AudioContentSchema = z
|
|||
mimeType: z.string(),
|
||||
|
||||
/**
|
||||
* Reserved by MCP for protocol-level metadata; implementations must not make assumptions about its contents.
|
||||
*/
|
||||
* See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
|
||||
* for notes on _meta usage.
|
||||
*/
|
||||
_meta: z.optional(z.object({}).passthrough()),
|
||||
})
|
||||
.passthrough();
|
||||
|
|
@ -739,6 +761,11 @@ export const EmbeddedResourceSchema = z
|
|||
.object({
|
||||
type: z.literal("resource"),
|
||||
resource: z.union([TextResourceContentsSchema, BlobResourceContentsSchema]),
|
||||
/**
|
||||
* See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
|
||||
* for notes on _meta usage.
|
||||
*/
|
||||
_meta: z.optional(z.object({}).passthrough()),
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
|
|
@ -883,8 +910,9 @@ export const ToolSchema = BaseMetadataSchema.extend({
|
|||
annotations: z.optional(ToolAnnotationsSchema),
|
||||
|
||||
/**
|
||||
* Reserved by MCP for protocol-level metadata; implementations must not make assumptions about its contents.
|
||||
*/
|
||||
* See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
|
||||
* for notes on _meta usage.
|
||||
*/
|
||||
_meta: z.optional(z.object({}).passthrough()),
|
||||
});
|
||||
|
||||
|
|
@ -1115,6 +1143,107 @@ export const CreateMessageResultSchema = ResultSchema.extend({
|
|||
]),
|
||||
});
|
||||
|
||||
/* Elicitation */
|
||||
/**
|
||||
* Primitive schema definition for boolean fields.
|
||||
*/
|
||||
export const BooleanSchemaSchema = z
|
||||
.object({
|
||||
type: z.literal("boolean"),
|
||||
title: z.optional(z.string()),
|
||||
description: z.optional(z.string()),
|
||||
default: z.optional(z.boolean()),
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
/**
|
||||
* Primitive schema definition for string fields.
|
||||
*/
|
||||
export const StringSchemaSchema = z
|
||||
.object({
|
||||
type: z.literal("string"),
|
||||
title: z.optional(z.string()),
|
||||
description: z.optional(z.string()),
|
||||
minLength: z.optional(z.number()),
|
||||
maxLength: z.optional(z.number()),
|
||||
format: z.optional(z.enum(["email", "uri", "date", "date-time"])),
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
/**
|
||||
* Primitive schema definition for number fields.
|
||||
*/
|
||||
export const NumberSchemaSchema = z
|
||||
.object({
|
||||
type: z.enum(["number", "integer"]),
|
||||
title: z.optional(z.string()),
|
||||
description: z.optional(z.string()),
|
||||
minimum: z.optional(z.number()),
|
||||
maximum: z.optional(z.number()),
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
/**
|
||||
* Primitive schema definition for enum fields.
|
||||
*/
|
||||
export const EnumSchemaSchema = z
|
||||
.object({
|
||||
type: z.literal("string"),
|
||||
title: z.optional(z.string()),
|
||||
description: z.optional(z.string()),
|
||||
enum: z.array(z.string()),
|
||||
enumNames: z.optional(z.array(z.string())),
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
/**
|
||||
* Union of all primitive schema definitions.
|
||||
*/
|
||||
export const PrimitiveSchemaDefinitionSchema = z.union([
|
||||
BooleanSchemaSchema,
|
||||
StringSchemaSchema,
|
||||
NumberSchemaSchema,
|
||||
EnumSchemaSchema,
|
||||
]);
|
||||
|
||||
/**
|
||||
* A request from the server to elicit user input via the client.
|
||||
* The client should present the message and form fields to the user.
|
||||
*/
|
||||
export const ElicitRequestSchema = RequestSchema.extend({
|
||||
method: z.literal("elicitation/create"),
|
||||
params: BaseRequestParamsSchema.extend({
|
||||
/**
|
||||
* The message to present to the user.
|
||||
*/
|
||||
message: z.string(),
|
||||
/**
|
||||
* The schema for the requested user input.
|
||||
*/
|
||||
requestedSchema: z
|
||||
.object({
|
||||
type: z.literal("object"),
|
||||
properties: z.record(z.string(), PrimitiveSchemaDefinitionSchema),
|
||||
required: z.optional(z.array(z.string())),
|
||||
})
|
||||
.passthrough(),
|
||||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
* The client's response to an elicitation/create request from the server.
|
||||
*/
|
||||
export const ElicitResultSchema = ResultSchema.extend({
|
||||
/**
|
||||
* The user's response action.
|
||||
*/
|
||||
action: z.enum(["accept", "decline", "cancel"]),
|
||||
/**
|
||||
* The collected user input content (only present if action is "accept").
|
||||
*/
|
||||
content: z.optional(z.record(z.string(), z.unknown())),
|
||||
});
|
||||
|
||||
/* Autocomplete */
|
||||
/**
|
||||
* A reference to a resource or resource template definition.
|
||||
|
|
@ -1169,6 +1298,14 @@ export const CompleteRequestSchema = RequestSchema.extend({
|
|||
value: z.string(),
|
||||
})
|
||||
.passthrough(),
|
||||
context: z.optional(
|
||||
z.object({
|
||||
/**
|
||||
* Previously-resolved variables in a URI template or prompt.
|
||||
*/
|
||||
arguments: z.optional(z.record(z.string(), z.string())),
|
||||
})
|
||||
),
|
||||
}),
|
||||
});
|
||||
|
||||
|
|
@ -1210,8 +1347,9 @@ export const RootSchema = z
|
|||
name: z.optional(z.string()),
|
||||
|
||||
/**
|
||||
* Reserved by MCP for protocol-level metadata; implementations must not make assumptions about its contents.
|
||||
*/
|
||||
* See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
|
||||
* for notes on _meta usage.
|
||||
*/
|
||||
_meta: z.optional(z.object({}).passthrough()),
|
||||
})
|
||||
.passthrough();
|
||||
|
|
@ -1264,6 +1402,7 @@ export const ClientNotificationSchema = z.union([
|
|||
export const ClientResultSchema = z.union([
|
||||
EmptyResultSchema,
|
||||
CreateMessageResultSchema,
|
||||
ElicitResultSchema,
|
||||
ListRootsResultSchema,
|
||||
]);
|
||||
|
||||
|
|
@ -1271,6 +1410,7 @@ export const ClientResultSchema = z.union([
|
|||
export const ServerRequestSchema = z.union([
|
||||
PingRequestSchema,
|
||||
CreateMessageRequestSchema,
|
||||
ElicitRequestSchema,
|
||||
ListRootsRequestSchema,
|
||||
]);
|
||||
|
||||
|
|
@ -1418,6 +1558,15 @@ export type SamplingMessage = Infer<typeof SamplingMessageSchema>;
|
|||
export type CreateMessageRequest = Infer<typeof CreateMessageRequestSchema>;
|
||||
export type CreateMessageResult = Infer<typeof CreateMessageResultSchema>;
|
||||
|
||||
/* Elicitation */
|
||||
export type BooleanSchema = Infer<typeof BooleanSchemaSchema>;
|
||||
export type StringSchema = Infer<typeof StringSchemaSchema>;
|
||||
export type NumberSchema = Infer<typeof NumberSchemaSchema>;
|
||||
export type EnumSchema = Infer<typeof EnumSchemaSchema>;
|
||||
export type PrimitiveSchemaDefinition = Infer<typeof PrimitiveSchemaDefinitionSchema>;
|
||||
export type ElicitRequest = Infer<typeof ElicitRequestSchema>;
|
||||
export type ElicitResult = Infer<typeof ElicitResultSchema>;
|
||||
|
||||
/* Autocomplete */
|
||||
export type ResourceTemplateReference = Infer<typeof ResourceTemplateReferenceSchema>;
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in a new issue