formatting, more version rollbacks
This commit is contained in:
parent
37dcf753af
commit
9686fc2ead
8 changed files with 51 additions and 52 deletions
|
|
@ -442,7 +442,7 @@ export class Client<
|
|||
try {
|
||||
// Validate the structured content (which is already an object) against the schema
|
||||
const validationResult = outputSchema.safeParse(result.structuredContent);
|
||||
|
||||
|
||||
if (!validationResult.success) {
|
||||
throw new McpError(
|
||||
ErrorCode.InvalidParams,
|
||||
|
|
@ -485,10 +485,10 @@ export class Client<
|
|||
// Cache the tools and their output schemas for future validation
|
||||
this._cachedTools.clear();
|
||||
this._cachedToolOutputSchemas.clear();
|
||||
|
||||
|
||||
for (const tool of result.tools) {
|
||||
this._cachedTools.set(tool.name, tool);
|
||||
|
||||
|
||||
// If the tool has an outputSchema, create and cache the Zod schema
|
||||
if (tool.outputSchema) {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ server.tool(
|
|||
},
|
||||
required: ["celsius", "fahrenheit"]
|
||||
},
|
||||
conditions: {
|
||||
conditions: {
|
||||
type: "string",
|
||||
enum: ["sunny", "cloudy", "rainy", "stormy", "snowy"]
|
||||
},
|
||||
|
|
@ -57,7 +57,7 @@ server.tool(
|
|||
// Simulate weather API call
|
||||
const temp_c = Math.round((Math.random() * 35 - 5) * 10) / 10;
|
||||
const conditions = ["sunny", "cloudy", "rainy", "stormy", "snowy"][Math.floor(Math.random() * 5)];
|
||||
|
||||
|
||||
return {
|
||||
structuredContent: {
|
||||
temperature: {
|
||||
|
|
@ -118,15 +118,15 @@ server.tool(
|
|||
const lines = csv_data.trim().split('\n');
|
||||
const headers = lines[0].split(delimiter).map(h => h.trim());
|
||||
const data = lines.slice(1).map(line => line.split(delimiter).map(cell => cell.trim()));
|
||||
|
||||
|
||||
// Infer data types
|
||||
const dataTypes: { [key: string]: string } = {};
|
||||
const summary: { [key: string]: unknown } = {};
|
||||
|
||||
|
||||
headers.forEach((header, idx) => {
|
||||
const values = data.map(row => row[idx]);
|
||||
const numericValues = values.filter(v => !isNaN(Number(v)) && v !== '');
|
||||
|
||||
|
||||
if (numericValues.length === values.length) {
|
||||
dataTypes[header] = "number";
|
||||
const numbers = numericValues.map(Number);
|
||||
|
|
@ -140,7 +140,7 @@ server.tool(
|
|||
dataTypes[header] = "string";
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
return {
|
||||
structuredContent: {
|
||||
row_count: data.length,
|
||||
|
|
@ -174,7 +174,7 @@ server.tool(
|
|||
|
||||
// Tool that can return both structured and unstructured content
|
||||
server.tool(
|
||||
"hybrid_tool",
|
||||
"hybrid_tool",
|
||||
"Tool that returns both structured and readable content",
|
||||
{
|
||||
data: z.array(z.number()).describe("Array of numbers to analyze")
|
||||
|
|
@ -196,12 +196,12 @@ server.tool(
|
|||
async ({ data }) => {
|
||||
const mean = data.reduce((a, b) => a + b, 0) / data.length;
|
||||
const sorted = [...data].sort((a, b) => a - b);
|
||||
const median = sorted.length % 2 === 0
|
||||
const median = sorted.length % 2 === 0
|
||||
? (sorted[sorted.length / 2 - 1] + sorted[sorted.length / 2]) / 2
|
||||
: sorted[Math.floor(sorted.length / 2)];
|
||||
const variance = data.reduce((acc, val) => acc + Math.pow(val - mean, 2), 0) / data.length;
|
||||
const std_dev = Math.sqrt(variance);
|
||||
|
||||
|
||||
return {
|
||||
structuredContent: {
|
||||
stats: {
|
||||
|
|
|
|||
|
|
@ -38,8 +38,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|||
type: "object",
|
||||
properties: {
|
||||
bmi: { type: "number", description: "Body Mass Index" },
|
||||
category: {
|
||||
type: "string",
|
||||
category: {
|
||||
type: "string",
|
||||
enum: ["underweight", "normal", "overweight", "obese"],
|
||||
description: "BMI category"
|
||||
},
|
||||
|
|
@ -102,22 +102,22 @@ server.setRequestHandler(CallToolRequestSchema, async (request: CallToolRequest)
|
|||
switch (request.params.name) {
|
||||
case "calculate_bmi": {
|
||||
const { height_cm, weight_kg } = request.params.arguments as { height_cm: number; weight_kg: number };
|
||||
|
||||
|
||||
const height_m = height_cm / 100;
|
||||
const bmi = weight_kg / (height_m * height_m);
|
||||
|
||||
|
||||
let category: string;
|
||||
if (bmi < 18.5) category = "underweight";
|
||||
else if (bmi < 25) category = "normal";
|
||||
else if (bmi < 30) category = "overweight";
|
||||
else category = "obese";
|
||||
|
||||
|
||||
// Calculate healthy weight range for normal BMI (18.5-24.9)
|
||||
const min_healthy_bmi = 18.5;
|
||||
const max_healthy_bmi = 24.9;
|
||||
const min_healthy_weight = min_healthy_bmi * height_m * height_m;
|
||||
const max_healthy_weight = max_healthy_bmi * height_m * height_m;
|
||||
|
||||
|
||||
// Return structured content matching the outputSchema
|
||||
return {
|
||||
structuredContent: {
|
||||
|
|
@ -130,31 +130,31 @@ server.setRequestHandler(CallToolRequestSchema, async (request: CallToolRequest)
|
|||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
case "analyze_text": {
|
||||
const { text } = request.params.arguments as { text: string };
|
||||
|
||||
|
||||
// Simple text analysis
|
||||
const words = text.trim().split(/\s+/);
|
||||
const sentences = text.split(/[.!?]+/).filter(s => s.trim().length > 0);
|
||||
const wordsPerMinute = 200; // Average reading speed
|
||||
|
||||
|
||||
// Very simple sentiment analysis (for demo purposes)
|
||||
const positiveWords = ["good", "great", "excellent", "happy", "positive", "amazing"];
|
||||
const negativeWords = ["bad", "poor", "terrible", "sad", "negative", "awful"];
|
||||
|
||||
|
||||
let positiveCount = 0;
|
||||
let negativeCount = 0;
|
||||
words.forEach(word => {
|
||||
if (positiveWords.includes(word.toLowerCase())) positiveCount++;
|
||||
if (negativeWords.includes(word.toLowerCase())) negativeCount++;
|
||||
});
|
||||
|
||||
|
||||
let sentiment: string;
|
||||
if (positiveCount > negativeCount) sentiment = "positive";
|
||||
else if (negativeCount > positiveCount) sentiment = "negative";
|
||||
else sentiment = "neutral";
|
||||
|
||||
|
||||
// Extract key phrases (simple approach - just common bigrams)
|
||||
const keyPhrases: string[] = [];
|
||||
for (let i = 0; i < words.length - 1; i++) {
|
||||
|
|
@ -162,7 +162,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request: CallToolRequest)
|
|||
keyPhrases.push(`${words[i]} ${words[i + 1]}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
structuredContent: {
|
||||
word_count: words.length,
|
||||
|
|
@ -174,21 +174,21 @@ server.setRequestHandler(CallToolRequestSchema, async (request: CallToolRequest)
|
|||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
case "traditional_tool": {
|
||||
const { message } = request.params.arguments as { message: string };
|
||||
|
||||
|
||||
// Traditional tool returns content array
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Processed message: ${message.toUpperCase()}`
|
||||
{
|
||||
type: "text",
|
||||
text: `Processed message: ${message.toUpperCase()}`
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
default:
|
||||
throw new McpError(
|
||||
ErrorCode.MethodNotFound,
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ describe('McpServer outputSchema support', () => {
|
|||
beforeEach(async () => {
|
||||
server = new McpServer({ name: 'test', version: '1.0' });
|
||||
client = new Client({ name: 'test-client', version: '1.0' });
|
||||
|
||||
|
||||
[clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
||||
});
|
||||
|
||||
|
|
@ -85,7 +85,7 @@ describe('McpServer outputSchema support', () => {
|
|||
'structured-tool',
|
||||
{ input: z.string() },
|
||||
outputSchema,
|
||||
() => ({
|
||||
() => ({
|
||||
structuredContent: { result: 'test', count: 42 }
|
||||
})
|
||||
);
|
||||
|
|
@ -118,7 +118,7 @@ describe('McpServer outputSchema support', () => {
|
|||
'structured-tool',
|
||||
{ input: z.string() },
|
||||
outputSchema,
|
||||
() => ({
|
||||
() => ({
|
||||
structuredContent: { result: 'test' },
|
||||
content: [{ type: 'text', text: 'Custom text' }]
|
||||
})
|
||||
|
|
@ -149,7 +149,7 @@ describe('McpServer outputSchema support', () => {
|
|||
'broken-tool',
|
||||
{ input: z.string() },
|
||||
outputSchema,
|
||||
() => ({
|
||||
() => ({
|
||||
content: [{ type: 'text', text: 'No structured content' }]
|
||||
})
|
||||
);
|
||||
|
|
@ -169,7 +169,7 @@ describe('McpServer outputSchema support', () => {
|
|||
server.tool(
|
||||
'broken-tool',
|
||||
{ input: z.string() },
|
||||
() => ({
|
||||
() => ({
|
||||
structuredContent: { result: 'test' }
|
||||
})
|
||||
);
|
||||
|
|
|
|||
|
|
@ -860,8 +860,8 @@ describe("tool()", () => {
|
|||
expect(result.structuredContent).toBeDefined();
|
||||
// For backward compatibility, content is auto-generated from structuredContent
|
||||
expect(result.content).toBeDefined();
|
||||
|
||||
const structuredContent = result.structuredContent as {
|
||||
|
||||
const structuredContent = result.structuredContent as {
|
||||
processedInput: string;
|
||||
resultType: string;
|
||||
timestamp: string;
|
||||
|
|
|
|||
|
|
@ -121,12 +121,12 @@ export class McpServer {
|
|||
: EMPTY_OBJECT_JSON_SCHEMA,
|
||||
annotations: tool.annotations,
|
||||
};
|
||||
|
||||
|
||||
// Only include outputSchema if it's defined
|
||||
if (tool.outputSchema) {
|
||||
toolDefinition.outputSchema = tool.outputSchema;
|
||||
}
|
||||
|
||||
|
||||
return toolDefinition;
|
||||
},
|
||||
),
|
||||
|
|
@ -205,7 +205,7 @@ export class McpServer {
|
|||
`Tool ${request.params.name} has outputSchema but returned no structuredContent`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// For backward compatibility, if structuredContent is provided but no content,
|
||||
// automatically serialize the structured content to text
|
||||
if (result.structuredContent && !result.content) {
|
||||
|
|
@ -224,7 +224,7 @@ export class McpServer {
|
|||
`Tool ${request.params.name} has no outputSchema but returned structuredContent`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Tool must have content if no outputSchema
|
||||
if (!result.content && !result.isError) {
|
||||
throw new McpError(
|
||||
|
|
@ -774,11 +774,11 @@ export class McpServer {
|
|||
// Check if the next arg is potentially annotations or outputSchema
|
||||
if (rest.length > 1 && typeof rest[0] === "object" && rest[0] !== null) {
|
||||
const nextArg = rest[0];
|
||||
|
||||
|
||||
// Check if it's a JSON Schema (outputSchema)
|
||||
if (typeof nextArg === "object" && "type" in nextArg) {
|
||||
outputSchema = rest.shift() as Tool["outputSchema"];
|
||||
|
||||
|
||||
// Check if there's still an annotations object
|
||||
if (rest.length > 1 && typeof rest[0] === "object" && rest[0] !== null && !(isZodRawShape(rest[0]))) {
|
||||
annotations = rest.shift() as ToolAnnotations;
|
||||
|
|
@ -1011,7 +1011,7 @@ export class ResourceTemplate {
|
|||
* Callback for a tool handler registered with Server.tool().
|
||||
*
|
||||
* Parameters will include tool arguments, if applicable, as well as other request handler context.
|
||||
*
|
||||
*
|
||||
* The callback should return:
|
||||
* - `structuredContent` if the tool has an outputSchema defined
|
||||
* - `content` if the tool does not have an outputSchema
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ describe("Types", () => {
|
|||
|
||||
test("should have correct latest protocol version", () => {
|
||||
expect(LATEST_PROTOCOL_VERSION).toBeDefined();
|
||||
expect(LATEST_PROTOCOL_VERSION).toBe("DRAFT-2025-v2");
|
||||
expect(LATEST_PROTOCOL_VERSION).toBe("2025-03-26");
|
||||
});
|
||||
test("should have correct supported protocol versions", () => {
|
||||
expect(SUPPORTED_PROTOCOL_VERSIONS).toBeDefined();
|
||||
|
|
|
|||
11
src/types.ts
11
src/types.ts
|
|
@ -1,9 +1,8 @@
|
|||
import { z, ZodTypeAny } from "zod";
|
||||
|
||||
export const LATEST_PROTOCOL_VERSION = "DRAFT-2025-v2";
|
||||
export const LATEST_PROTOCOL_VERSION = "2025-03-26";
|
||||
export const SUPPORTED_PROTOCOL_VERSIONS = [
|
||||
LATEST_PROTOCOL_VERSION,
|
||||
"2025-03-26",
|
||||
"2024-11-05",
|
||||
"2024-10-07",
|
||||
];
|
||||
|
|
@ -889,12 +888,12 @@ export const CallToolUnstructuredResultSchema = ResultSchema.extend({
|
|||
* If the Tool does not define an outputSchema, this field MUST be present in the result.
|
||||
*/
|
||||
content: ContentListSchema,
|
||||
|
||||
|
||||
/**
|
||||
* Structured output must not be provided in an unstructured tool result.
|
||||
*/
|
||||
structuredContent: z.never().optional(),
|
||||
|
||||
|
||||
/**
|
||||
* Whether the tool call ended in an error.
|
||||
*
|
||||
|
|
@ -910,7 +909,7 @@ export const CallToolStructuredResultSchema = ResultSchema.extend({
|
|||
* If the Tool defines an outputSchema, this field MUST be present in the result, and contain a JSON object that matches the schema.
|
||||
*/
|
||||
structuredContent: z.object({}).passthrough(),
|
||||
|
||||
|
||||
/**
|
||||
* A list of content objects that represent the result of the tool call.
|
||||
*
|
||||
|
|
@ -919,7 +918,7 @@ export const CallToolStructuredResultSchema = ResultSchema.extend({
|
|||
* Clients that support structured content should ignore this field.
|
||||
*/
|
||||
content: z.optional(ContentListSchema),
|
||||
|
||||
|
||||
/**
|
||||
* Whether the tool call ended in an error.
|
||||
*
|
||||
|
|
|
|||
Loading…
Reference in a new issue