Files
operations/test/from_mcp.test.ts
glm-5.1 b0283aa662 feat(mcp-envelope-integration): update from_mcp adapter to use mcpEnvelope, structuredContent, and outputSchema extraction
- Add mapMCPContentBlocks() helper mapping SDK ContentBlock[] to MCPContentBlock[]
- Extract tool.outputSchema via FromSchema() when present, fall back to Type.Unknown()
- Handler returns mcpEnvelope() with structured/legacy data path
- structuredContent preferred as data when present, Value.Cast() when outputSchema is known
- isError: true wrapped in envelope meta, NOT thrown
- Transport-level config errors throw CallError
- Unknown MCP content block types fall back to { type: 'text', text: JSON.stringify(block) }
- Add 20 tests for mapMCPContentBlocks and envelope detection
2026-05-11 01:59:30 +00:00

281 lines
8.4 KiB
TypeScript

import { describe, it, expect } from "vitest";
import { Type } from "@alkdev/typebox";
import { Value } from "@alkdev/typebox/value";
import { mapMCPContentBlocks } from "../src/from_mcp.js";
import { isResponseEnvelope } from "../src/response-envelope.js";
import type { MCPContentBlock } from "../src/response-envelope.js";
describe("mapMCPContentBlocks", () => {
it("maps text content blocks", () => {
const sdkBlocks = [
{ type: "text", text: "hello world" },
];
const result = mapMCPContentBlocks(sdkBlocks);
expect(result).toHaveLength(1);
expect(result[0]).toEqual({ type: "text", text: "hello world" });
});
it("maps text content blocks with annotations", () => {
const sdkBlocks = [
{ type: "text", text: "hello", annotations: { audience: ["user"], priority: 1 } },
];
const result = mapMCPContentBlocks(sdkBlocks);
expect(result).toHaveLength(1);
expect(result[0]).toEqual({
type: "text",
text: "hello",
annotations: { audience: ["user"], priority: 1 },
});
});
it("maps image content blocks", () => {
const sdkBlocks = [
{ type: "image", data: "base64data", mimeType: "image/png" },
];
const result = mapMCPContentBlocks(sdkBlocks);
expect(result).toHaveLength(1);
expect(result[0]).toEqual({
type: "image",
data: "base64data",
mimeType: "image/png",
});
});
it("maps audio content blocks", () => {
const sdkBlocks = [
{ type: "audio", data: "base64audio", mimeType: "audio/wav" },
];
const result = mapMCPContentBlocks(sdkBlocks);
expect(result).toHaveLength(1);
expect(result[0]).toEqual({
type: "audio",
data: "base64audio",
mimeType: "audio/wav",
});
});
it("maps resource content blocks", () => {
const sdkBlocks = [
{
type: "resource",
resource: {
uri: "file:///test.txt",
mimeType: "text/plain",
text: "file content",
},
},
];
const result = mapMCPContentBlocks(sdkBlocks);
expect(result).toHaveLength(1);
expect(result[0]).toEqual({
type: "resource",
resource: {
uri: "file:///test.txt",
mimeType: "text/plain",
text: "file content",
},
});
});
it("maps resource_link content blocks", () => {
const sdkBlocks = [
{
type: "resource_link",
uri: "file:///data.json",
name: "data",
description: "A data file",
mimeType: "application/json",
},
];
const result = mapMCPContentBlocks(sdkBlocks);
expect(result).toHaveLength(1);
expect(result[0]).toEqual({
type: "resource_link",
uri: "file:///data.json",
name: "data",
description: "A data file",
mimeType: "application/json",
});
});
it("maps resource_link content blocks without optional fields", () => {
const sdkBlocks = [
{
type: "resource_link",
uri: "file:///data.json",
name: "data",
},
];
const result = mapMCPContentBlocks(sdkBlocks);
expect(result).toHaveLength(1);
expect(result[0]).toEqual({
type: "resource_link",
uri: "file:///data.json",
name: "data",
});
expect(result[0]).not.toHaveProperty("description");
expect(result[0]).not.toHaveProperty("mimeType");
});
it("falls back to text JSON.stringify for unknown block types", () => {
const sdkBlocks = [
{ type: "custom_type", foo: "bar", baz: 42 },
];
const result = mapMCPContentBlocks(sdkBlocks);
expect(result).toHaveLength(1);
expect(result[0]).toEqual({
type: "text",
text: JSON.stringify({ type: "custom_type", foo: "bar", baz: 42 }),
});
});
it("falls back to text JSON.stringify for non-object blocks", () => {
const result = mapMCPContentBlocks([null, 42, "hello", true]);
expect(result).toHaveLength(4);
expect(result[0]).toEqual({ type: "text", text: "null" });
expect(result[1]).toEqual({ type: "text", text: "42" });
expect(result[2]).toEqual({ type: "text", text: "\"hello\"" });
expect(result[3]).toEqual({ type: "text", text: "true" });
});
it("handles multiple mixed content blocks", () => {
const sdkBlocks = [
{ type: "text", text: "result" },
{ type: "image", data: "abc", mimeType: "image/png" },
{ type: "resource", resource: { uri: "f.txt", text: "hi" } },
];
const result = mapMCPContentBlocks(sdkBlocks);
expect(result).toHaveLength(3);
expect(result[0].type).toBe("text");
expect(result[1].type).toBe("image");
expect(result[2].type).toBe("resource");
});
it("handles empty array", () => {
const result = mapMCPContentBlocks([]);
expect(result).toEqual([]);
});
it("maps resource blocks with blob instead of text", () => {
const sdkBlocks = [
{
type: "resource",
resource: {
uri: "file:///binary.bin",
mimeType: "application/octet-stream",
blob: "base64blob",
},
},
];
const result = mapMCPContentBlocks(sdkBlocks);
expect(result).toHaveLength(1);
expect(result[0]).toEqual({
type: "resource",
resource: {
uri: "file:///binary.bin",
mimeType: "application/octet-stream",
blob: "base64blob",
},
});
});
it("maps image content blocks with annotations", () => {
const sdkBlocks = [
{ type: "image", data: "base64data", mimeType: "image/png", annotations: { priority: 5 } },
];
const result = mapMCPContentBlocks(sdkBlocks);
expect(result).toHaveLength(1);
expect(result[0]).toEqual({
type: "image",
data: "base64data",
mimeType: "image/png",
annotations: { priority: 5 },
});
});
it("maps audio content blocks with annotations", () => {
const sdkBlocks = [
{ type: "audio", data: "base64audio", mimeType: "audio/wav", annotations: { audience: ["assistant"] } },
];
const result = mapMCPContentBlocks(sdkBlocks);
expect(result).toHaveLength(1);
expect(result[0]).toEqual({
type: "audio",
data: "base64audio",
mimeType: "audio/wav",
annotations: { audience: ["assistant"] },
});
});
it("maps resource content blocks with annotations", () => {
const sdkBlocks = [
{
type: "resource",
resource: { uri: "file:///test.txt", text: "hi" },
annotations: { priority: 1 },
},
];
const result = mapMCPContentBlocks(sdkBlocks);
expect(result).toHaveLength(1);
expect(result[0]).toEqual({
type: "resource",
resource: { uri: "file:///test.txt", text: "hi" },
annotations: { priority: 1 },
});
});
});
describe("MCP handler envelope wrapping (unit)", () => {
it("isResponseEnvelope detects mcpEnvelope results", () => {
const envelope = {
data: [{ type: "text" as const, text: "result" }],
meta: {
source: "mcp" as const,
isError: false,
content: [{ type: "text" as const, text: "result" }],
},
};
expect(isResponseEnvelope(envelope)).toBe(true);
});
it("isResponseEnvelope detects mcpEnvelope with isError: true", () => {
const envelope = {
data: [{ type: "text" as const, text: "error details" }],
meta: {
source: "mcp" as const,
isError: true,
content: [{ type: "text" as const, text: "error details" }],
},
};
expect(isResponseEnvelope(envelope)).toBe(true);
});
it("Value.Cast normalizes structuredContent against outputSchema", () => {
const outputSchema = Type.Object({
name: Type.String(),
count: Type.Number(),
});
const structuredContent = { name: "test", count: 42, extra: "removed" };
const cast = Value.Cast(outputSchema, structuredContent);
expect(cast.name).toBe("test");
expect(cast.count).toBe(42);
});
it("Value.Cast with Type.Unknown passes through any value", () => {
const unknownSchema = Type.Unknown();
const data = { anything: "goes", nested: [1, 2, 3] };
const cast = Value.Cast(unknownSchema, data);
expect(cast).toEqual(data);
});
it("isResponseEnvelope rejects non-envelope values", () => {
expect(isResponseEnvelope(null)).toBe(false);
expect(isResponseEnvelope(undefined)).toBe(false);
expect(isResponseEnvelope("string")).toBe(false);
expect(isResponseEnvelope({})).toBe(false);
expect(isResponseEnvelope({ data: "test" })).toBe(false);
expect(isResponseEnvelope({ meta: { source: "mcp" } })).toBe(false);
});
});