MCP 2025-06-18 で追加された structured tool output を試す
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() というメソッドが追加されている。
今後は特別な理由がない限りこのメソッドを利用することが推奨される。
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(),
};
// Using server.registerTool(), added in this PR:
// https://github.com/modelcontextprotocol/typescript-sdk/pull/454
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` の全体像はこちら
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(),
};
// Using server.registerTool(), added in this PR:
// https://github.com/modelcontextprotocol/typescript-sdk/pull/454
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 を使うと安心
} satisfies z.infer<z.ZodObject<typeof divOutputSchema>>;
return {
content: [
{
type: 'text',
text: JSON.stringify(structuredContent),
},
],
isError,
structuredContent,
};
}
正常に除算できる方の分岐でも同様に記述すると、最終的にこのような実装になる。
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(),
};
// Using server.registerTool(), added in this PR:
// https://github.com/modelcontextprotocol/typescript-sdk/pull/454
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.infer<z.ZodObject<typeof divOutputSchema>>;
return {
content: [
{
type: 'text',
text: JSON.stringify(structuredContent),
},
],
isError,
structuredContent,
};
}
const isError = false;
const structuredContent = {
isError,
result: dividend / divisor,
} satisfies z.infer<z.ZodObject<typeof 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.infer<TResult>): 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,
};
},
};
};
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 サーバーを実装されている方々は、ツール呼び出し結果の成功 / 失敗にかかわらず同じスキーマを用いる必要がある 点に注意されたい。
参考
- https://modelcontextprotocol.io/introduction
- https://modelcontextprotocol.io/specification/2025-06-18/server/tools#structured-content
- https://modelcontextprotocol.io/specification/2025-06-18/server/tools#output-schema
- https://github.com/modelcontextprotocol/modelcontextprotocol/pull/371
- https://github.com/modelcontextprotocol/modelcontextprotocol/pull/559
- https://github.com/modelcontextprotocol/typescript-sdk/pull/454
Discussion
大変参考になる記事をありがとうございます!
この記事を参考に実装を進めていて実際に運用していたのですが、エラーが起きているのにちゃんと読み込まれずに成功したとみなされることがあり、
MCP の公式仕様を確認したところ、エラーハンドリングの構造について重要な点が判明したので共有させてください。
つまり、エラー情報は
resultオブジェクトの中に含めるのが推奨されています。推奨される構造
記事内の実装例:
以上、参考になれば幸いです。
ご指摘ありがとうございます!
具体的に推奨されるスキーマについて調査が不十分なまま執筆していた部分があり、あらためて調査を行いました。
まず 'Any errors that originate from the tool SHOULD be reported inside the result object,' における result object が指しているものは
JSONRPCResponseにおける result、つまり tool 実行結果の payload そのものであると推測しています。よってこの記述は「tool の規約に違反しているので tool call が失敗した場合は payload にエラー内容を含めてください、さもなくば LLM にエラー内容が伝わりません」という意味合いで、構造化レスポンスのスキーマ構造に言及しているものではないと考えています!
逆に避けるべきとされている 'MCP protocol-level error' は
JSONRPCErrorのような error code を返すものとみられ、これは tool handler 内で例外を throw することに該当していそうでした。結論として、MCP Specification 2025-06-18 は構造化レスポンスの具体的なスキーマについては言及していないとみられるため、本記事で紹介した事例の修正は不要だと判断しております!これで答えになっておりますでしょうか?