コンテンツへスキップ
27/30第27章 / 全30章

MCP ServerをShipする:TypeScriptとPythonを実測

同じserverを2度実装。3つのtool、resource、promptを計測。94 packages対28、cold startは145 ms対709 ms。

このページの内容

議論を始める前に、言語論争の全体を実測値で示します。

spawn → tools/list answered, median of 25 launchesTEXT
node ./incidents.js         144.5 ms
python incidents.py         709.4 ms
npx incidents-mcp           712.6 ms

最初の2行は、誰もが見たい比較です。3行目は1行目と同じTypeScript serverを、実際に配布される形で起動したものです。そしてPythonとの差は3ミリ秒に収まります。

第26章では、Model Context Protocolを生のJSON-RPCで仕様そのものに照らして読みました。生のJSON-RPCには言語がないからです。この章には2つの言語があります。そして論点の重みはここにあります。同じserverを、2度書く。 3つのtool、1つのresource、1つのprompt、両方のSDK、どちらにも近道なし。そのうえでtransports、inspector、401、そして誰も公開していない数値を見ます。

インシデントログです。toolは3つ。第18章の読み取りと書き込みの分離を見える形にする必要があるからです。search_incidentsは読み取り、open_incidentは書き込みを行ってhandleを返し、resolve_incidentはそのhandleを受け取ってcloseします。resourceは1つ、incidents://openです。現在のリストを読むことはアプリケーションが紐づけるものだからです。promptは1つ、postmortemです。「これをまとめて」は人間のslash commandだからです。これが第26章の制御階層――model、application、person――を5つのregistrationにしたものです。

handleは見た目以上に重要です。第26章では、状態をmodule-level arrayに置いたおもちゃのカレンダーを壊しました。protocolにはsessionがないため、作成toolはopaque identifierを返し、それ以降の呼び出しはすべてそれを普通の引数として受け取ります。どちらのファイルも、callerがそれを開いたprocessであるとは仮定していません。

同じtoolを両言語で、並べて登録するとこうなります。

incidents.tsTS
server.registerTool(
  "resolve_incident",
  {
    description:
      "Close an incident by handle and record its cause.",
    inputSchema: {
      id: z.string().describe(
        "The handle returned by open_incident, e.g. INC-3."),
      cause: z.string().describe(
        "One sentence. What actually broke."),
    },
    annotations: {
      readOnlyHint: false,
      destructiveHint: true,
      idempotentHint: true,
    },
  },
  async ({ id, cause }) => {
    const at = OPEN.findIndex((i) => i.id === id);
    if (at < 0) {
      return { isError: true, content: [{ type: "text",
        text: `No open incident ${id}. ` +
              `Call search_incidents first.` }] };
    }
    const [done] = OPEN.splice(at, 1);
    return { content: [{ type: "text",
      text: JSON.stringify({ ...done, cause }) }] };
  },
);
incidents.pyPYTHON
@server.tool(
    description=
      "Close an incident by handle and record its cause.",
    annotations=ToolAnnotations(
        readOnlyHint=False,
        destructiveHint=True,
        idempotentHint=True,
    ),
)
def resolve_incident(
    id: Annotated[str, Field(description=
        "The handle returned by open_incident, e.g. INC-3.")],
    cause: Annotated[str, Field(description=
        "One sentence. What actually broke.")],
) -> Incident:
    for at, i in enumerate(OPEN):
        if i["id"] == id:
            done = OPEN.pop(at)
            return {**done, "cause": cause}
    raise ValueError(
        f"No open incident {id}. Call search_incidents first.")

まず同じところを読んでください。それこそが発見です。どちらもname、description、説明付きの2つのstring argument、3つのannotationを宣言しています。どちらも1つのfunctionです。どちらにもJSON-RPC、framing、stdout、protocol versionは出てきません。2つのSDKは同じ形に収束しています。それが「Tier 1」の意味であるはずです。1

本当に違う点は2つあり、どちらも後で戻ってきます。TypeScriptはschema library――ここではZod――で引数を記述し、そのschemaは自分で書くvalueです。Pythonはfunction自身のtype hintsで引数を記述し、import時にそれを読みます。そのため、TypeScriptのファイルが伝えていないfunctionの情報まで知っています。そしてerror pathです。TypeScriptはisError付きのtool resultを返し、Pythonはraiseします。覚えておいてください。

残り4つのregistrationには構造上の違いはありません。resourceはserver.registerResource("open-incidents", "incidents://open", …)@server.resource("incidents://open", …)、promptはregisterPrompt@server.promptです。各ファイルの最後の行はtransportで、await server.connect(new StdioServerTransport())server.run()です。

ファイル全体では、空行を除いてTypeScriptが81行・3,060 bytes、Pythonが63行・2,555 bytesです。ただし、この数字は相応の割引をして受け取ってください。行数は言語と同じくらいformatterを測ってしまうからです。そのため、下のheadline tableにはどちらの数字も入れていません。

言語が見えないことの証明は、同じclientを2回走らせることです。11行で済みます。

client.tsTS
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";

const client = new Client({ name: "incident-cli", version: "1.0.0" });
await client.connect(new StdioClientTransport({
  command: process.argv[2], args: process.argv.slice(3) }));

const { tools } = await client.listTools();
console.log("tools:", tools.map((t) => t.name).join(", "));

const opened = await client.callTool({ name: "open_incident",
  arguments: { title: "Queue backed up", severity: "sev2" } });
console.log("open_incident ->", JSON.stringify(opened.content));

それぞれのserverを順番に指定します。実際の出力を短くするとこうです。

TEXT
$ node client.ts node incidents.ts
tools: search_incidents, open_incident, resolve_incident
open_incident -> [{"type":"text","text":"{\"id\":\"INC-3\"}"}]

$ node client.ts ./py/.venv/bin/python ./py/incidents.py
tools: search_incidents, open_incident, resolve_incident
open_incident -> [{"type":"text","text":"{\n  \"id\": \"INC-3\"\n}"}]

同じtool、同じ順序、同じhandleです。TypeScript clientにはserverが何で書かれているか分かりませんし、尋ねることもありません。protocolの約束は、ここで保たれています。

次に2つ目の結果のwhitespaceを見てください。これは見た目の問題ではありません。Python SDKはpayloadをpydantic_core.to_json(result, fallback=str, indent=2)でserialiseします。リストに2件のincidentがあるresource readでは、TypeScript bodyは136 characters、37 o200k_base tokensです。Python bodyは185 characters、62 tokensです。同一の行に対して68%多いtokensが、resourceをpromptに読み込む人によって毎回支払われます。

catalogueにも同じ話があり、原因はさらに大きくなります。両server、同じ3つのtoolについて、tools/listをkeyごとに量りました。

keyTypeScriptPython
name2121
description4646
annotations4646
inputSchema211192
outputSchema187
execution27
total342480

Pythonのinput schemasは安く済みます。TypeScriptのZod bridgeは各schemaに$schemaadditionalPropertiesを押すからです。138-tokenの差全体は、誰も書いていないoutput schemaです。resolve_incidentには-> Incidentというannotationがあるため、SDKがreturn typeからJSON Schemaを導出して出荷しました。これは本当に有用です。clientがstructuredContentをvalidateできるのはそのおかげです。そして、type hintのせいでcontext windowに187 tokensがやって来ます。第24章で述べた「definitionが重要な材料を押しのける」という規則は、自分が持っていると知らなかったschemaにも当てはまります。

上の2つのerror pathはstyleの選択ではありません。各serverに、実際のintegrationが失敗するようなtoolを渡して、modelに何が届くかを読みます。

tools/call on a tool that raisesTEXT
TypeScript  {"content":[{"type":"text","text":
              "connect ECONNREFUSED 10.0.3.7:5432 (db-prod-eu, user=reporting)"}],
             "isError":true}

Python      {"content":[{"text":"Error executing tool boom","type":"text"}],
             "isError":true}

TypeScript SDKはinternal address、port、database name、service accountをmodelのcontextに入れました。Python SDKはそのどれも入れませんでした。tracebackはstderrへ行き、serverに留まりました。

どちらもbugではありません。どちらも決定です。そしてPython側の決定は、自身のdocstringに書かれています。ToolErrorは「予期していたfailure」であり、そのmessageは「modelが読むためにcontentで」返されます。それ以外は「crashとして扱われ、modelにはError executing tool <name>だけが見え、serverはtracebackをERRORにlogする」。crash caseのclassは残りをはっきり言っています。「originalからは何もclientに届かない」。

どちらの挙動も、半分の場面では間違いです。第18章では、validation errorはmodelが読んで修正できるtool resultとして返すべきだと論じました。多くのintegrationで、そこが最もleverageの高い行だからです。Python側ではそのためにToolErrorを明示的にraiseする必要があり、bare ValueErrorは有用な文を捨ててしまいます。第30章の議論は逆方向です。toolが返すものはすべて、後続のprompt injectionが読み戻そうとするcontextに入ります。そして未reviewのexception stringは、system内で最も監査されていないtextです。

両方を経ても残る規則はこれです。toolごとに、failureが何を言ってよいかを決め、その文字列を自分で書くこと。 どちらの言語でも、exceptionのdefault textに決めさせてはいけません。

公式tutorialは、逃げ道なく規則を述べています。「STDIO-based serversの場合、stdoutに絶対に書き込んではいけません。stdoutに書き込むとJSON-RPC messagesが壊れ、serverが破損します。print() functionはdefaultでstdoutに書き込むため、STDIO serverからは完全に外しておくべきです。」1 第26章ではnormative versionを引用しました。serverは「valid MCP messageではないものをstdoutに一切書いてはならない(MUST NOT)」です。2

各serverに1行を追加して、raw streamを読みます。

raw stdout, first two linesTEXT
TypeScript  incidents server starting
            {"result":{"protocolVersion":"2025-11-25", … },"jsonrpc":"2.0","id":1}

Python      {"jsonrpc":"2.0","id":1,"result":{ … }}
            incidents server starting

Pythonのほうが悪く、その理由はMCPではありません。stdoutがterminalではなくpipeであるprocessはblock-buffered streamを持つため、紛れ込んだ行はbufferが決めたタイミングでflushされます。ここではexit時、つまり書かれた順序としては前だったresponseのです。corruptionはbugのある場所には現れません。flush=Trueを追加するか、flushするlibraryを入れると、場所が動きます。

次に、なぜこれが出荷されるのかを説明する部分です。壊れたserverを3つのclientに食わせます。

TEXT
naive parser, dirty server   SyntaxError: Unexpected token 'i',
                             "incidents "... is not valid JSON
SDK client, dirty server     tools: search_incidents, open_incident, resolve_incident
MCP Inspector, dirty server  full catalogue, no warning

7行のparserは即座に死にます。official clientとInspectorはどちらも平然とし、その行をskipして続行します。誰も使わないclientだけを壊す規則は、そのままproductionに届きます。だからこそ、customerのlogで壊すのではなく、ここでわざと壊す価値があります。

InspectorのCLI modeは忘れられがちな半分です。npx @modelcontextprotocol/inspector --cli <command> --method tools/listはcatalogueを出力してexitするため、browser UIとは違ってscriptableです。3

両SDKは、それぞれ専用のdirectoryにclean installしました。共有はありません。

TypeScriptPython
package@modelcontextprotocol/sdk 1.30.0 + zod 3.25.76mcp 2.1.1
latest protocol revision implemented2025-11-252026-07-28
transitive packages installed9428
installed size13.9 MiB44.3 MiB
files on disk3,3862,018
third-party packages loaded to serve stdio8 of 9418 of 28
bare interpreter start, median19.4 ms11.1 ms
spawn → tools/list answered, median of 25144.5 ms709.4 ms
tools/list catalogue, o200k_base tokens342480

どの行も違う方向に驚かせます。だからこそ、この比較は推測ではなく実行する価値があります。

TypeScriptは3倍以上のpackagesをinstallし、bytesは3分の1未満です。 94 dependenciesはnpm ecosystemらしさそのものです――fast-deep-equales-errorsdunder-proto。Pythonの28個は数が少なく巨大です。cryptographypydantic-coreuvicornはcompiled artefactsです。心配すべきはdependencyのだという直感があるなら、この行はcounter-exampleです。

PythonのinterpreterはNodeより速く起動し、差は小さくありません――empty programで11.1 ms対19.4 msです。つまりcold-start行の565 msは言語ではありません。SDKです。そしてloaded-packages行がその理由を示しています。

third-party modules loaded to answer one tools/list over stdioTEXT
TypeScript   8 of 94   sdk, zod, zod-to-json-schema, ajv, ajv-formats,
                       fast-deep-equal, fast-uri, json-schema-traverse

Python      18 of 28   mcp, mcp_types, pydantic, pydantic_core, anyio,
                       starlette, uvicorn, sse_starlette, httpx2,
                       cryptography, _cffi_backend, opentelemetry, click, …

I/Oがpipeだけのserverが、最初の行を読む前にASGI web server、HTTP client、TLS libraryをimportしています。TypeScript SDKもExpress、Hono、joseeventsourceをshipしています。しかしpackage boundaryがそれらをserver/stdio.js importから外しているため、disk上に未読のまま残ります。Pythonのpackageは1つのimport graphなので、import mcpが全部です。python -X importtimeimport mcp.server.mcpserverに727 msを帰属させています。これはimport profiler下で測った値なので、unprofiled runがspawnからanswerまでに要する709 msを上回ります。そしてそのうち269 msはmcp.types subtreeだけです。wire typesはPydantic modelsで、protocol messageごと、revisionごとに1 classあり、それらの構築はimport時に行われるworkです。これはdesign tradeであって雑さではありません。eager importsのおかげで、Python SDKは追加installなしで次の行にrun(transport="streamable-http")を渡せます。

そしてopening blockの最後の行が、その議論をひっくり返します。 TypeScript serverを適切にpackageします。bin entry、shebang、npm link、download不要。そしてpublished stdio serverが実際に起動される方法であるnpx--no-installを付けてlaunchします。

median of 25, spawn → tools/list answeredTEXT
node ./incidents.js       144.5 ms
npx incidents-mcp         712.6 ms      (+568.1 ms of launcher)
python incidents.py       709.4 ms

launcherはstartごとに568 msかかります。TypeScript SDK import全体の4.5倍です。そしてMCP hostはstdio serverをそのcommandの実行で起動するため、launchのたびに支払われます。つまり「TypeScriptは5倍速く起動する」の正直な形は、通常の方法で配布するまでは、その通りです。同じ注意はおそらくuvxにも当てはまります。このmachineにはuvがinstallされていなかったため、その行は存在しません。測っていないものは表に入れません。

第26章ではstdioのframingを扱いました。ここに残したことが2つあります。

1つ目。npxまたはuvxでserverを実行すること自体がstdio transportです。 別の「package mode」はありません。hostのconfigurationはcommandとargumentsを指定します。hostはそれをspawnし、pipes越しに話します。だからlocalでは「これをどう配布するか」と「どのtransportを話すか」は同じ問いであり、launcherのcostはshippingについての章に属します。

2つ目。stdioにはauthorization sectionがまったくありません。仕様はそれを1行で述べています。stdioを使うimplementationは「この仕様に従うべきではなく、代わりにenvironmentからcredentialsを取得すべき」です。4 そのsecurity modelはoperating systemのものであり、限界も同じです。local subprocessがserviceできるのは、ちょうど1台のmachineと1人のuserです。

もう1つの現行transportはStreamable HTTPです。POSTを受け付けるsingle endpoint、JSON-RPC messageごとに1 HTTP request、そしてAccept headerにはapplication/jsontext/event-streamの両方を列挙しなければなりません。serverがrequestごとにどちらでanswerするかを選ぶからです。5 第14章ではそのevent streamを手でparseしたので、wire formatに新しいものはありません。新しいのは、それを包むものだけです。現行revisionの3つの義務は見落としやすく、3つともtestableです。

すべてのPOSTはMCP-Protocol-Versionを持ち、その値はrequest自身の_meta内にあるprotocolVersionと一致しなければなりません。不一致はheader-mismatch error付きの400であり、黙認ではありません。5

Mcp-Methodはすべてのrequestでmethodをmirrorsします。Mcp-Nametools/callresources/readprompts/get上のparams.nameまたはparams.uriをmirrorsします。proxyがbodyをparseせずにrouteできるようにするためです。5

GET stream、Mcp-Session-IdLast-Event-ID resumptionはすべて削除されました。このrevisionだけを話すserverはGETまたはDELETEに405 Method Not Allowedでanswerし、session headerをmintせずに無視し、Last-Event-IDを無視すべきです。5

ここで章全体の見方を変えるmeasurementです。現行revisionのrequestをHTTP経由で各serverに送ります。

POST /mcp, MCP-Protocol-Version: 2026-07-28TEXT
Python   200  {"result":{"resultType":"complete","cacheScope":"private","ttlMs":0,
              "tools":[…],"_meta":{"io.modelcontextprotocol/serverInfo":{…}}}}

TypeScript    {"error":{"code":-32000,"message":"Bad Request: Unsupported protocol
              version: 2026-07-28 (supported versions: 2025-11-25, 2025-06-18,
              2025-03-26, 2024-11-05, 2024-10-07)"}}

constantsはbehaviorと一致します。Python SDKのLATEST_PROTOCOL_VERSION2026-07-28を読み、TypeScript SDKのものは2025-11-25を読みます。上のstepからheader-mismatch requestを送ると、Python serverはerror -32020とmessage「mcp-protocol-version header does not match the request envelope's protocol version」を含む400でanswerします。TypeScript SDKにはそのようなcodeがありません。それを定義するrevisionをimplementしていないからです。

両方をTier 1として載せるページには、「Each SDK provides the same functionality」とも書かれています。1 下の日付時点で、現行revisionについては、その文はaspirationalです。これからinstallするSDKでLATEST_PROTOCOL_VERSIONを確認してください。1行で済み、1年後にもこの章で唯一意味を持ち続けるclaimです。

serverをlaptopの外に移すと、見知らぬclientがtokenを持って現れます。これは第26章で触れなかった半分であり、multi-user productがskipできない半分です。

仕様はMCP serverをOAuth 2.1 roleに置き、その名前を付けています。protected MCP serverはresource serverであり、clientはOAuth client、authorization serverは誰か別の問題です。4 そのroleから、4つのmandatory clausesをまるごと引用します。言い換えることが間違いの始まりだからです。

MCP servers, acting in their role as an OAuth 2.1 resource server, MUST validate access tokens as described in OAuth 2.1 Section 5.2. MCP servers MUST validate that access tokens were issued specifically for them as the intended audience, according to RFC 8707 Section 2. […] MCP clients MUST NOT send tokens to the MCP server other than ones issued by the MCP server's authorization server. MCP servers MUST only accept tokens that are valid for use with their own resources. MCP servers MUST NOT accept or transit any other tokens.4

「Must not accept or transit」がanti-passthrough ruleであり、audience apparatus全体が存在する理由です。渡されたbearer tokenをthird-party APIでreplayするserverはconfused deputyです。呼び出した相手に自分のtrustを貸してしまいます。この規則が禁じているのはstorageだけではなく、reuseです。

それをenforce可能にするには4つのRFCが必要で、それぞれに1つずつ役割があります。6 RFC 9728は、clientがauthorization serverをそもそも見つける方法です。MCP serverがprotected-resource-metadata documentをserveし、401がそれを指します。RFC 8707resource parameterです。clientはserverのcanonical URIをauthorization requestとtoken requestの両方で送らなければなりません。「authorization serversがそれをsupportするかどうかにかかわらず」です。そうしてissued tokenがaudienceを名指しします。RFC 9207は反対側からloopを閉じます。clientはredirect前にissuerを記録し、返されたissを正確なstringで比較します。normalisationはありません。case foldingも、default-port elisionも、trailing slashもありません。そしてRFC 7591、Dynamic Client Registrationは、Client ID Metadata Documentsを優先する形で現在deprecatedです。それらをsupportしないauthorization serversとの「backwards compatibilityのために retained」されています。4

audienceだけをcheckするtoken verifierで、両serverにこれを配線します。TypeScriptのladderです。

POST /mcp — TypeScript, with requireBearerAuthTEXT
no token            401  WWW-Authenticate: Bearer error="invalid_token",
                         error_description="Missing Authorization header",
                         scope="incidents:read",
                         resource_metadata="…/.well-known/oauth-protected-resource/mcp"
aud=other server    401  error_description="token audience is not this server"
no exp claim        401  error_description="Token has no expiration time"
right aud, no scope 403  error="insufficient_scope", scope="incidents:read"
right aud + scope   200  {"result":{"tools":[…]}}
GET /.well-known/oauth-protected-resource/mcpTEXT
{"resource":"http://127.0.0.1:8931/mcp",
 "authorization_servers":["https://auth.example.com/"],
 "scopes_supported":["incidents:read","incidents:write"],
 "resource_name":"Incidents"}

どちらのSDKもそのdocumentをserveし、どちらも401でそれを指します。これがdiscovery storyのすべてです。あなたのserverを一度も見たことがないclientは、refusalからどこでauthenticateすべきかを学びます。403は別物です。tokenは問題ないがscopeが違います。challengeは欠けているものを名指しするため、clientは最初からやり直すのではなくstep upできます。

2つのrungsに違いがあり、どちらの違いも仕様にはありません。TypeScript SDKはexpiry claimのないtokenを拒否します。Python側は200を返します。AccessToken上のexpires_atがoptionalであり、Noneは「no opinion」を意味するからです。そしてPythonの403は、仕様がserverは含めるべきだとするscope parameterなしでerror_description="Required scope: incidents:read"を運びます。verifierはlibrary defaultを受け入れる場所ではありません。audience checkはどちらの言語でもあなたが書くものです。expiryも同じです。

同じrunから、正直な細部を1つ。endpointへのGETはExpress wiringで404、Python側で400 Bad Request: Missing session IDをanswerしました。仕様が求めるのは405 Method Not Allowedであり、「session ID」はこのrevisionが削除した語彙です。どちらも危険ではありません。どちらもmid-migrationなecosystemの形です。

shippingの最後の要素は、どこにpublishするかです。そしてそれには数字付きの答えがあります。本日crawlした、official registry内の全serverのlatest versionです。7

servers
total (latest version, not deleted)28,170
active / deprecated27,853 / 317
ship at least one installable package13,065
remote only — a URL, nothing to install14,696
npm8,275
PyPI3,603
OCI images867
mcpb bundles706
NuGet / Cargo107 / 43

読み方は2つあり、逆方向を指します。published serversで見ると、npmは2.3対1でリードしています――ecosystemはTypeScriptだと言うときに人々が引用する数字です。downloadsで見ると、Pythonがリードしています。直近30日でmcpは286.7 million、@modelcontextprotocol/sdkは194.7 millionで、fastmcpの72.1 millionを加える前です。7 どちらもTier 1であり、normative schemaはschema.tsであり、official「Build an MCP server」tutorialはPython tabから始まります。1 そのどちらか片方を頭に入れていたとしても、もう片方も同時に真実です。

そして、どちらよりも重要な行があります。registryの半分以上――28,170件中14,696件――にはinstallするものがありません。 それらはweb servicesです。transport talliesも反対側から同じことを示します。14,290のpackage entriesのうち13,787がstdioをdeclareし、16,640のremote entriesのうち15,570がStreamable HTTPをdeclareし、1,070はまだdeprecated HTTP+SSEをdeclareしています。つまり「MCP serverはlaptop上のsubprocessである」という説明は縮小しつつある少数派であり、14,696件すべてに必要なのはenvironment variableではなく、上のsectionです。

詳細を表示

意図的なbilingual、そしてその前例。

これはcourseで唯一のbilingual chapterです。正直な答えが分かれるからです。registryはnpm-firstであり、downloadsはPython-firstです。同時に、今日の時点で。2つのうち一方だけを書くと、問いの半分を手放し、ecosystemを誤って描写することになります。公開された前例もあります。Hugging Face MCP Courseはprerequisitesに「Experience with at least one programming language (Python or TypeScript examples will be shown)」を挙げ、両方を教えています。8 protocolの価値がimplementationの数にあるなら、monolingualでいるには向かない場所です。

2026年9月7日に、protocol revision 2026-07-28に対して読み、測定しました。

value
@modelcontextprotocol/sdk1.30.0, published 27 July 2026; 4,322,438 bytes unpacked, 693 files, 17 direct dependencies
latest revision it implements2025-11-25
mcp (PyPI)2.1.1, published 25 August 2026; 357,912-byte wheel, plus mcp-types 2.1.1 at 69,656 bytes
latest revision it implements2026-07-28
SDK tiersTypeScript, Python, C#, Go, Rust at Tier 1; Java, Ruby at Tier 2; Swift, PHP, Kotlin at Tier 3
registry servers28,170
downloads, last 30 daysmcp 286,653,871 · fastmcp 72,097,269 · @modelcontextprotocol/sdk 194,679,333

数字ではないmigration noteを1つ。mcp 2.xでは、FastMCPMCPServerにrenameされました。そしてonlineのtutorialのほとんどは、まだ古いimportで始まります。SDKはその説明だけを目的にしたmoduleをshipしています。この章で最も思いやりのあるdeprecationです。

from mcp.server.fastmcp import FastMCPTEXT
ModuleNotFoundError: No module named 'mcp.server.fastmcp'. This is mcp 2.x,
where FastMCP was renamed to MCPServer (from mcp.server.mcpserver import
MCPServer) and other APIs changed; see the migration guide … or pin 'mcp<2'
to keep running v1 code.

表を目の前に置くと、recommendationは退屈です。それは良い兆候です。

serverがすでに運用しているweb applicationの中に住むなら、TypeScriptで書いてください。 同じprocess、同じdeploy、同じrequest handlerです。Streamable HTTPは他のendpointの隣に追加するendpointです。そして13.9 MiBと145 msは、runtimeがすでに起動しているため無料です。14,696件のremote serversの大半がこれです。

serverがdata toolingをwrapするなら、Pythonで書いてください。 exposeしているのはpandas、warehouse client、notebook相当のtransformsです。別言語のserverはschemaを身に着けたsubprocess callになります。1度だけ起動するserviceでは700 msのimportはcostではありません。hostが1日中relaunchするsubprocessではcostです。

そして今のところ、revision rowが両方を上書きします。 2026-07-28――multi-round-trip requests、resultType、cache hints、server/discover――が必要なら、2つのSDKのうち今日それを持っているのは一方だけで、もう一方は持っていません。

これで、同じserverをどちらの言語でもshipでき、好みではなく表で選択を説明でき、両方のlive transportsで実行でき、拒否するtokenを渡せるようになりました。

あなたが作ったものは、まだfunctionです。schema、endpoint、modelがinvokeするdeterministicなものです。知識の大きな分類の一つは、その形に収まりません。私たちがpostmortemを書く方法、私たちのincident reportsに必要なfields、物事を行う順序とその理由です。それはprocedureであり、proseです。それをtool descriptionに押し込むことが、conversationがincidentsについてかどうかに関係なく毎turn支払われる2,000 tokensのsystem promptsを生みます。

第28章は別の答えです。中にSKILL.mdが入ったfolderを、modelがcallするのではなく読む。3 levelsでloadedされ、必要なturnまでreference materialのcostはほぼゼロです。main languageはありません。そして、それが最初に教えることです。


ここにあるすべては、2026年9月7日に、Node 22.22.3とPython 3.14.4上で、@modelcontextprotocol/sdk 1.30.0とzod 3.25.76、mcp 2.1.1に対して測定しました。それぞれを専用の使い捨てdirectoryにinstallしています。timingsは25 launchesのmedianで、spawnからtools/list responseを含む行までのwall clockです。token countsは各definitionのJSONに対し、tiktoken経由のo200k_baseです。paid APIは呼んでいません。ここにmodelは不要です。

2つのserverは空行を除いて81行と63行です。3つのtoolのうち1つは上で両言語の形で再掲し、残り4つのregistrationは記述した点以外では違いません。Python SDKのerror-disclosure policyは、mcp/server/mcpserver/exceptions.py内のToolErrorUnexpectedToolErrorのdocstringsから引用しています。pretty-printing defaultはmcp/server/mcpserver/resources/types.pyutilities/func_metadata.py内のpydantic_core.to_json(result, fallback=str, indent=2)です。protocol-version constantsはmcp_types/version.py内のLATEST_PROTOCOL_VERSIONとTypeScript SDKのtypes.jsにあり、changelogではなくinstall済みpackagesから読みました。

  1. SDKs, modelcontextprotocol.io/docs/sdk, and Build an MCP server, modelcontextprotocol.io/docs/develop/build-server, いずれも2026年9月7日閲覧。tier table、「Each SDK provides the same functionality but follows the idioms and best practices of its language」という文、tutorialのlanguage-tab order(Python, TypeScript, Java, Kotlin, C#, Ruby, Rust, Go)、およびprint()stdoutについて引用したlogging ruleの出典。 2 3 4

  2. stdio transport, .../basic/transports/stdio. newline framingとstdout purity ruleの出典。第26章ではこのページを全文読んでいます。ここではbroken serverが違反する行のために引用しています。

  3. MCP Inspector, modelcontextprotocol.io/docs/2026-07-28/tools/inspector, 2026年9月7日閲覧。1つのpackage、1つのbinaryの背後に3つのclients――web、--cli--tui――があり、1つのcore、1組のtransports、disk上の1つのOAuth stateを共有します。ここでのcatalogue tracesはCLIが生成しました。

  4. Authorization, modelcontextprotocol.io/specification/2026-07-28/basic/authorization, 2026年9月7日閲覧。resource-server role、全文引用した4つのtoken-handling clauses、serverがRFC 9728をimplementしclientがdiscoveryに使うrequirement、resource parameter rulesとcanonical-URI definition、issuer-validation table、Dynamic Client Registrationのdeprecation、401/403/400 tableとinsufficient_scope challenge、そしてstdio exemption「Implementations using an STDIO transport SHOULD NOT follow this specification, and instead retrieve credentials from the environment.」の出典。 2 3 4

  5. Streamable HTTP, .../basic/transports/streamable-http, and Transports overview, .../basic/transports. single-endpoint POST rule、dual Accept requirement、MCP-Protocol-Version headerとbody一致rule、"REQUIRED for compliance"と記述されたMcp-MethodおよびMcp-Name headers、GET stream、sessions、Last-Event-IDの削除、405 guidance、mandatory Origin validation、SEP-2596下で2024-11-05 HTTP+SSE transportをDeprecatedと分類することの出典。 2 3 4

  6. 仕様が依拠する4つと、そのprofiles draft。The OAuth 2.1 Authorization Framework, draft-ietf-oauth-v2-1-13. Campbell, B., Bradley, J. and Tschofenig, H., Resource Indicators for OAuth 2.0, RFC 8707, February 2020 — resource parameterとそれがbindするaudience。Jones, M.B., Hunt, P. and Parecki, A., OAuth 2.0 Protected Resource Metadata, RFC 9728, April 2025 — 401が指すdocument。Meyer zu Selhausen, K. and Fett, D., OAuth 2.0 Authorization Server Issuer Identification, RFC 9207, March 2022 — iss parameterとexact-string comparison。Richer, J. (ed.) et al., OAuth 2.0 Dynamic Client Registration Protocol, RFC 7591, July 2015, この用途ではdeprecated。そしてJones, M. and Hardt, D., The OAuth 2.0 Authorization Framework: Bearer Token Usage, RFC 6750, October 2012, section 3, 上のWWW-Authenticate challenge shapeについて。

  7. Official MCP registry, registry.modelcontextprotocol.io/v0/servers, 2026年9月7日にversion=latestでcrawl。282 pages、28,170 servers、distinct server namesに対してregistryTypeで集計。Download figures: @modelcontextprotocol/sdkについてはapi.npmjs.org/downloads/point/last-month(2026年8月8日〜9月6日で194,679,333)、mcpfastmcpについてはpypistats.org/api/packages/<name>/recent、いずれも同日閲覧。Package sizesはnpm registry documentとPyPI JSON APIから取得。 2

  8. MCP Course, Hugging Face, huggingface.co/learn/mcp-course, unit 0, 2026年9月7日閲覧。prerequisitesの中に「Experience with at least one programming language (Python or TypeScript examples will be shown)」。

モデル選びは、LIAにおまかせ。

すべてのAIモデルをひとつの場所で。今日から無料で。