Model Context Protocol の Version 2025-06-18 で structured tool output という仕様が追加された。
これは簡単に説明すると、 tool 定義にレスポンスのスキーマを事前に含めることで、レスポンス構造の検証などを可能にする仕様だ。
既に TypeScript SDK ではこの仕様が実装されているので、本記事では実際に structured tool output を試す。
簡易的な MCP サーバーを準備する
TypeScript プロジェクトをセットアップして、MCP SDK をインストールする。
npm i @modelcontextprotocol/sdk zod@3
今回は簡易的な除算する MCP サーバーを例として、以下の実装を作成する。
なお、structured output のサポートと同時に、引数が増えすぎた McpServer.tool()
をリファクタした McpServer.registerTool()
というメソッドが追加されている。
今後は特別な理由がない限りこのメソッドを利用することが推奨される。
src/index.ts
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import z from 'zod';
const server = new McpServer({
name: 'math-mcp-with-structured-output',
version: '0.0.0',
});
const divInputSchema = {
divisor: z.number(),
dividend: z.number(),
};
server.registerTool(
'div',
{
title: 'Divide',
description: 'Divide two numbers together',
annotations: {
openWorldHint: false,
readOnlyHint: true,
},
inputSchema: divInputSchema,
},
({ divisor, dividend }) => {
if (divisor === 0) {
return {
content: [
{
type: 'text',
text: 'Cannot divide by zero',
},
],
isError: true,
};
}
return {
content: [
{
type: 'text',
text: `${dividend} / ${divisor} = ${dividend / divisor}`,
},
],
};
}
);
const run = async () => {
const transport = new StdioServerTransport();
await server.connect(transport);
};
run().catch(console.error);
ここで一度 MCP Inspector を起動し、tool が正常に動作することを確認する。
npx @modelcontextprotocol/inspector npx tsx src/index.ts
では、ここから structured output にしていく。
Structured Output を返すようにする
スキーマを定義する
まず、structured output の構造を表すスキーマを定義する。
重要な点として、ツール呼び出し結果の成功 / 失敗にかかわらず同じスキーマを用いる必要がある。
そのため、成功と失敗の両方を表現できる、以下のようなスキーマを定義する。
const divOutputSchema = {
isError: z.boolean(),
result: z.number().optional(),
error: z.string().optional(),
};
次に、このスキーマを McpServer.registerTool()
の第二引数に渡す。
server.registerTool(
'div',
{
title: 'Divide',
description: 'Divide two numbers together',
annotations: {
openWorldHint: false,
readOnlyHint: true,
},
inputSchema: divInputSchema,
outputSchema: divOutputSchema,
},
)
この時点の `src/index.ts` の全体像はこちら
src/index.ts
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import z from 'zod';
const server = new McpServer({
name: 'math-mcp-with-structured-output',
version: '0.0.0',
});
const divInputSchema = {
divisor: z.number(),
dividend: z.number(),
};
const divOutputSchema = {
isError: z.boolean(),
result: z.number().optional(),
error: z.string().optional(),
};
server.registerTool(
'div',
{
title: 'Divide',
description: 'Divide two numbers together',
annotations: {
openWorldHint: false,
readOnlyHint: true,
},
inputSchema: divInputSchema,
outputSchema: divOutputSchema,
},
({ divisor, dividend }) => {
if (divisor === 0) {
return {
content: [
{
type: 'text',
text: 'Cannot divide by zero',
},
],
isError: true,
};
}
return {
content: [
{
type: 'text',
text: `${dividend} / ${divisor} = ${dividend / divisor}`,
},
],
};
}
);
const run = async () => {
const transport = new StdioServerTransport();
await server.connect(transport);
};
run().catch(console.error);
ここでもう一度 MCP Inspector を起動ないし再起動すると、div tool の output schema を見ることができる。
npx @modelcontextprotocol/inspector npx tsx src/index.ts
そして、この状態で div tool を呼び出すとこのようなエラーが出ることを確認する。
MCP error -32602: MCP error -32602:
Tool div has an output schema but no structured content was provided
tool call の結果として構造を宣言しているにもかかわらず、実際には返していないため、このエラーが発生するのは自然な動作だ。
次は実際に構造化されたコンテンツを返す。
実際に構造化されたレスポンスを返す
構造化されたコンテンツを返すには、tool の callback から返すオブジェクトに structuredContent
というプロパティを含める必要がある。
なお、後方互換性を考慮して、従来の text
には structuredContent
を JSON.stringify()
したものを格納することが推奨されている。
これを踏まえ、たとえば 0 除算エラーを返す分岐は以下のように記述できる。
if (divisor === 0) {
const isError = true;
const structuredContent = {
isError,
error: 'Cannot divide by zero',
} satisfies z.inferz.ZodObjecttypeof divOutputSchema>>;
return {
content: [
{
type: 'text',
text: JSON.stringify(structuredContent),
},
],
isError,
structuredContent,
};
}
正常に除算できる方の分岐でも同様に記述すると、最終的にこのような実装になる。
src/index.ts
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import z from 'zod';
const server = new McpServer({
name: 'math-mcp-with-structured-output',
version: '0.0.0',
});
const divInputSchema = {
divisor: z.number(),
dividend: z.number(),
};
const divOutputSchema = {
isError: z.boolean(),
result: z.number().optional(),
error: z.string().optional(),
};
server.registerTool(
'div',
{
title: 'Divide',
description: 'Divide two numbers together',
annotations: {
openWorldHint: false,
readOnlyHint: true,
},
inputSchema: divInputSchema,
outputSchema: divOutputSchema,
},
({ divisor, dividend }) => {
if (divisor === 0) {
const isError = true;
const structuredContent = {
isError,
error: 'Cannot divide by zero',
} satisfies z.inferz.ZodObjecttypeof divOutputSchema>>;
return {
content: [
{
type: 'text',
text: JSON.stringify(structuredContent),
},
],
isError,
structuredContent,
};
}
const isError = false;
const structuredContent = {
isError,
result: dividend / divisor,
} satisfies z.inferz.ZodObjecttypeof divOutputSchema>>;
return {
content: [
{
type: 'text',
text: JSON.stringify(structuredContent),
},
],
isError,
structuredContent,
};
}
);
const run = async () => {
const transport = new StdioServerTransport();
await server.connect(transport);
};
run().catch(console.error);
構造化されたレスポンスを確認してみる
もう一度 MCP Inspector を起動、または再起動してレスポンスを確認する。
npx @modelcontextprotocol/inspector npx tsx src/index.ts
以下の画像のように、成功時と失敗時の両方で、出力がスキーマに準拠しており、かつ従来の非構造化コンテンツが構造化コンテンツと同じ内容になっていることが確認できる。
これで、structured output を返す MCP サーバーを実装できた。
より効率的に記述する
前述したように、structured output を返す場合従来の text
には structured output と同等な JSON 文字列を返すことが推奨される。
そこで、以下のようなユーティリティを定義することで、tool 本体の実装をより簡潔かつ型安全に記述できる。
const createStructuredOutput = TResult extends z.ZodTypeAny>(
expectedResult: TResult
) => {
const outputSchema = {
isError: z.boolean(),
result: expectedResult.optional(),
error: z.string().optional(),
};
return {
schema: outputSchema,
success: (data: z.inferTResult>): CallToolResult => {
const structuredContent = {
isError: false,
result: data,
};
return {
content: [
{
text: JSON.stringify(structuredContent),
type: 'text',
},
],
isError: false,
structuredContent: structuredContent,
};
},
error: (error: string): CallToolResult => {
const structuredContent = {
isError: true,
error,
};
return {
content: [
{
text: JSON.stringify(structuredContent),
type: 'text',
},
],
isError: true,
structuredContent,
};
},
};
};
src/index.ts
const structuredDivOutput = createStructuredOutput(z.number());
server.registerTool(
'div',
{
title: 'Divide',
description: 'Divide two numbers together',
annotations: {
openWorldHint: false,
readOnlyHint: true,
},
inputSchema: divInputSchema,
outputSchema: structuredDivOutput.schema,
},
({ divisor, dividend }) => {
if (divisor === 0) {
return structuredDivOutput.error('Cannot divide by zero');
}
return structuredDivOutput.success(dividend / divisor);
}
);
まとめ
本記事では、Model Context Protocol Version 2025-06-18 で追加された structured tool output を試した。
この仕様を用いると、ツール呼び出しによって得られるデータの構造を事前に AI へ伝えることが可能になる。これにより、さらに効率的なツール呼び出しが実現できると期待される。
MCP サーバーを実装されている方々は、ツール呼び出し結果の成功 / 失敗にかかわらず同じスキーマを用いる必要がある 点に注意されたい。
参考
Views: 0