An MCP SDK is a library that implements the Model Context Protocol for you: the JSON-RPC messages, the transports (stdio and Streamable HTTP), schema generation, and the client side of OAuth. As of October 14, 2026 there are ten official SDKs under the modelcontextprotocol GitHub org. Six are Tier 1: TypeScript, Python, Go, C#, Rust and Ruby. Java is Tier 2. Kotlin, Swift and PHP are Tier 3.

Most people end up choosing between two packages: mcp on PyPI (version 2.3.0) and the TypeScript pair @modelcontextprotocol/server and @modelcontextprotocol/client (2.3.1). The package most TypeScript tutorials tell you to install, @modelcontextprotocol/sdk, is now the v1 maintenance line. It will never speak the 2026-07-28 protocol revision.

This guide covers the whole SDK list, then builds the same small server and client in Python and TypeScript. We ran every snippet on October 14 and crossed them over (Python client to TypeScript server and the reverse). One difference between the two SDKs never shows up in a quickstart: which protocol revision the client speaks by default. If you only want a Python server walkthrough, our Python SDK v2 tutorial goes deeper on that.

The official MCP SDKs, October 2026

Socket wrench set in an open red metal case: one ratchet handle, two extension bars and ten sockets of different sizes
One handle, many sockets. Photo: Saoyagi2 / Wikimedia Commons (CC0).

Versions and dates below come from each package registry and the SDK's GitHub releases on October 14. The last column is about the current spec revision, 2026-07-28, which drops the initialize handshake and session IDs in favor of a stateless protocol with a server/discover call (what changed on the wire).

LanguagePackageLatestTier2026-07-28
TypeScript@modelcontextprotocol/server, @modelcontextprotocol/client2.3.1 (Oct 5)1Yes, opt-in
TypeScript (v1)@modelcontextprotocol/sdk1.32.1 (Oct 5)maintenanceNo, and not planned
Pythonmcp2.3.0 (Oct 2)1Yes, on by default
Gogithub.com/modelcontextprotocol/go-sdkv1.8.0 (Sep 14)1Yes; on for clients, HTTP server needs Stateless: true
C#ModelContextProtocol (NuGet)2.2.0 (Aug 13)1Yes, on by default
Rustrmcp3.5.1 (Oct 5)1Yes; server by default, client opt-in
Rubymcp gem1.7.0 (Oct 5)1Yes; client probes by default
Javaio.modelcontextprotocol.sdk:mcp2.0.1 (Aug 19)2Not yet; planned for 3.x
Kotlinio.modelcontextprotocol:kotlin-sdk0.15.0 (Jul 28)3Not yet
Swiftmodelcontextprotocol/swift-sdk (SwiftPM)0.12.1 (May 7)3Not yet
PHPmcp/sdk (Composer)v0.8.1 (Aug 29)3Yes; still labelled experimental

Several SDKs are co-maintained with an outside organization. Go is maintained with Google, C# with Microsoft, Kotlin with JetBrains, Java with the Spring AI team, and PHP with the PHP Foundation and Symfony. Ruby started at Shopify. GitHub runs its own MCP server on the Go SDK.

Be careful with older copies of the SDK page. Rust was promoted to Tier 1 on August 21 and Ruby on September 28. Cached pages and the frozen 2025-11-25 docs still show them as Tier 2 and Tier 3. When the spec shipped on July 28, the launch post named only four Tier 1 SDKs.

What the tiers actually promise

The tiers come from SEP-1730. The first tier assignments were published on February 23, 2026, and the SDK Working Group moves SDKs between tiers by consensus. The rules are measurable, which makes the label more useful than a star count:

Tier 1Tier 2Tier 3
Conformance tests100% pass80% passNo minimum
New spec featuresBefore the spec releaseWithin 6 monthsNo commitment
Issue triage2 business daysA monthNone
Critical bugs7 daysTwo weeksNone

An SDK can be moved down if its conformance tests fail for four straight weeks. In practice, Tier 1 means an SDK ships the new protocol with the spec, and Tier 2 means it gets there eventually. Java shows the gap. Its roadmap planned 3.0.0 milestones with 2026-07-28 support for September 2026, but as of mid-October, Maven Central still ends at 2.0.1. The six-month Tier 2 window runs to about late January 2027.

None of this stops you from using a Tier 3 SDK. Kotlin is very active. But if you write a client that has to talk to every server on the internet, Tier 1 is the safer bet.

TypeScript: the package everyone installs is the old one

The TypeScript SDK split into separate packages for v2, released July 27, 2026:

  • @modelcontextprotocol/server for building servers, and @modelcontextprotocol/client for clients
  • thin runtime adapters: @modelcontextprotocol/node, /express, /fastify and /hono
  • @modelcontextprotocol/codemod, which rewrites v1 imports: npx @modelcontextprotocol/codemod@latest v1-to-v2 .

The old single package, @modelcontextprotocol/sdk, still publishes. The day we checked, both lines had shipped a release on October 5. Its README says v1 implements the spec "up to 2025-11-25" and that "support for the 2026-07-28 spec is not planned for v1.x." It gets bug fixes and security updates for "at least six months after the v2 release." That puts the earliest end at late January 2027. Several of the tutorials ranking for "mcp sdk" when we checked still import from @modelcontextprotocol/sdk/server/mcp.js, so check the package name before you copy anything.

Two more v2 changes trip people up. v2 needs Node 20 or newer, and Zod v3 is no longer supported. The SDK depends on zod ^4.2.0 directly, so it's no longer a peer dependency you install yourself. Your tool schemas can use any Standard Schema library: Zod v4, Valibot or ArkType.

Python: one package, renamed classes

Python kept one package. pip install mcp has installed 2.x since July 28, and the latest release, 2.3.0, supports Python 3.10 and up (the main branch is moving to 3.11). The rename that breaks old code is FastMCP becoming MCPServer. from mcp.server.fastmcp import FastMCP now raises ModuleNotFoundError. v2 also adds a high-level Client class that takes a URL, a command or an in-process server.

The name FastMCP now refers to two different projects. The standalone fastmcp package (PrefectHQ, 4.1.0 on October 8) is a separate framework built on top of the official mcp 2.x. Its 1.0 was folded into the official SDK in 2024. Pick it for its extras, such as deployment and auth tooling, but don't mix its docs with the official SDK docs.

The same server in both SDKs

One tool, forecast, that takes a city and returns typed data, served over Streamable HTTP. Python first:

from pydantic import BaseModel
from mcp.server import MCPServer

mcp = MCPServer("weather")

class Forecast(BaseModel):
    city: str
    days: int
    summary: str

@mcp.tool()
def forecast(city: str, days: int = 1) -> Forecast:
    """Return a canned forecast for a city."""
    return Forecast(city=city, days=days, summary="sunny")

if __name__ == "__main__":
    mcp.run(transport="streamable-http")  # http://127.0.0.1:8000/mcp

The return annotation matters more than it looks. With -> Forecast, the SDK publishes an output schema and the client gets structured_content back as a dict. When we first wrote the tool as a bare -> dict, the call still worked, but structured_content came back None and the client had only text. If you want the model or your code to get typed data, annotate a pydantic model, a TypedDict, or a parameterized type such as dict[str, str].

The TypeScript version, on plain node:http:

import { createServer } from 'node:http';
import { createMcpHandler, McpServer } from '@modelcontextprotocol/server';
import { toNodeHandler, localhostHostValidation, localhostOriginValidation } from '@modelcontextprotocol/node';
import * as z from 'zod/v4';

const handler = createMcpHandler(() => {
  const server = new McpServer({ name: 'weather', version: '1.0.0' });
  server.registerTool(
    'forecast',
    {
      description: 'Return a canned forecast for a city.',
      inputSchema: z.object({ city: z.string(), days: z.number().int().default(1) }),
      outputSchema: z.object({ city: z.string(), days: z.number(), summary: z.string() }),
    },
    async ({ city, days }) => {
      const out = { city, days, summary: 'sunny' };
      return { content: [{ type: 'text', text: JSON.stringify(out) }], structuredContent: out };
    },
  );
  return server;
});

const nodeHandler = toNodeHandler(handler);
const validateHost = localhostHostValidation();
const validateOrigin = localhostOriginValidation();
createServer((req, res) => {
  if (!validateHost(req, res) || !validateOrigin(req, res)) return;
  void nodeHandler(req, res);
}).listen(3000, '127.0.0.1');

Three things here differ from Python and from v1 tutorials:

  • The factory runs once per HTTP request. createMcpHandler builds a fresh McpServer for every request, so register tools inside the factory. Keep database pools and caches at module scope. This is what makes the endpoint stateless and lets it scale horizontally. It replaces v1's StreamableHTTPServerTransport plus connect() wiring.
  • The handler trusts its caller. It checks no Host header, no Origin and no token. On plain Node you add the localhost guards yourself, as above. The Express, Hono and Fastify app helpers turn them on by default. The Python server has a similar guard. When it binds to localhost it accepts only localhost Host headers, but bound to 0.0.0.0 for a deployment it accepted any Host in our test, so set allowed hosts and origins yourself before you expose it.
  • You write the structured result by hand. Python derives it from the return type. In TypeScript you declare outputSchema and return structuredContent next to a text fallback.

On Cloudflare Workers, Deno or Bun, export default handler mounts it. You still put the Host, Origin and token checks in front, because the handler does none of them.

The same client in both SDKs

Python's client is a context manager. Give it a URL and it uses Streamable HTTP:

import asyncio
from mcp import Client

async def main() -> None:
    async with Client("http://localhost:8000/mcp") as client:
        tools = await client.list_tools()
        print([t.name for t in tools.tools])
        result = await client.call_tool("forecast", {"city": "Austin", "days": 2})
        print(result.structured_content)  # {'city': 'Austin', 'days': 2, 'summary': 'sunny'}

asyncio.run(main())

TypeScript separates the client from the transport:

import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';

const client = new Client(
  { name: 'demo-client', version: '1.0.0' },
  { versionNegotiation: { mode: 'auto' } },
);
await client.connect(new StreamableHTTPClientTransport(new URL('http://127.0.0.1:3000/mcp')));
console.log(client.getProtocolEra()); // 'modern'

const result = await client.callTool({ name: 'forecast', arguments: { city: 'Austin', days: 2 } });
console.log(result.structuredContent);
await client.close();

We ran all four pairings: each client against each server. Every one listed forecast and returned the same structured result, so use whatever language fits each side. The wire protocol is the contract, not the SDK.

The default that differs: which protocol your client speaks

Look at the versionNegotiation option in the TypeScript client. Without it, the same client reported 'legacy' against both of our servers. The TypeScript docs say it plainly: "Nothing in v2 puts a 2026-07-28 byte on the wire by default." A v2 client runs the 2025 initialize handshake "byte for byte" like v1 unless you opt in. The Python Client does the opposite. Its mode defaults to "auto", and it reported protocol version 2026-07-28 against the same server. With mode="legacy", it reported 2025-11-25.

SDKClient defaultHow to change it
PythonAuto: probes server/discover, falls backClient(url, mode="legacy") or a pinned revision
TypeScript v2Legacy 2025 handshakeversionNegotiation: { mode: 'auto' }, or { mode: { pin: '2026-07-28' } } to require it
GoNew protocol on for new clientsSee the SDK's protocol version options
C#Probes server/discover first, falls backClient options
Rust (rmcp)Legacy with plain serve()serve_with_lifecycle with a discover or auto mode

For plain tool calls the era rarely matters, because current servers answer both. It matters when you need something only the new revision has, such as subscriptions/listen or cacheable discovery. It also matters if you maintain a gateway or test harness that classifies traffic. The TypeScript team kept the conservative default on purpose. Their docs say spawn-per-invocation CLIs and debugging tools should keep it, for two reasons. Against an old stdio server that ignores unknown requests, the probe stalls connect() until it times out, and the extra round trip changes recorded transcripts.

The servers differ too. The Python MCPServer and the TypeScript createMcpHandler both serve 2025 and 2026 clients from one endpoint, as our tests showed. Go's HTTP server accepts 2026-07-28 only with Stateless turned on. Java can't serve it yet.

Connecting to a real remote server: auth comes first

Point either client at a production server and the first thing you'll hit is OAuth, not tools. Here's what our own endpoint, https://mcp.dialmcp.com/mcp, returns to an unauthenticated request:

HTTP/2 401
www-authenticate: Bearer realm="OAuth", error="invalid_token",
  error_description="Missing or invalid access token",
  resource_metadata="https://mcp.dialmcp.com/.well-known/oauth-protected-resource/mcp"

That resource_metadata URL is where a client learns which authorization server to use (the full MCP OAuth flow). The SDKs report the 401 in different ways. TypeScript in auto mode threw a typed SdkHttpError with code CLIENT_HTTP_AUTHENTICATION and the message "Version negotiation failed: the server requires authorization (HTTP 401)". Its docs say an auth status is never treated as a sign of the protocol era, so the client doesn't quietly fall back. Python raised a generic MCPError ("Server returned an error response") inside an exception group. That's harder to catch, so wrap remote connects and check for auth failures explicitly.

To get past the 401:

  • TypeScript: pass authProvider in the transport options. For an API key or a gateway-issued token, { token: async () => process.env.API_KEY } is enough, and an optional onUnauthorized lets the transport refresh and retry once. For interactive OAuth, pass an OAuthClientProvider instead.
  • Python: mcp.client.auth.OAuthClientProvider handles discovery, client registration, PKCE and token storage. You supply a token store, a redirect handler and a callback handler. Client has no auth parameter, so pass the provider as httpx2.AsyncClient(auth=provider) to mcp.client.streamable_http.streamable_http_client(url, http_client=...) and hand that transport to Client. If the server supports client ID metadata documents, it can use an HTTPS URL as the client ID instead of dynamic registration.

The interactive part, opening a browser and catching the redirect, is the host's job. That's why most people use an MCP host such as Claude, ChatGPT or Cursor for remote servers, and only reach for a client SDK when they're building a host or an agent themselves. If that's you, MCP client vs server explains who owns which part of the job.

How to pick an SDK

  • Building a server: use the language of the system you're wrapping. A server is mostly glue around your existing code, and current Tier 1 server libraries still answer 2025-era clients, so you won't lock out older hosts.
  • Building a client or agent that connects to many servers: pick a Tier 1 SDK, decide your protocol default on purpose, and plan for OAuth from day one.
  • On TypeScript: start new code on @modelcontextprotocol/server and /client. Leave @modelcontextprotocol/sdk for code you haven't migrated yet, and migrate before early 2027.
  • On Python: put an upper bound on mcp (mcp>=2.3,<3). The 1.x to 2.x jump broke unpinned servers, and the next major will too.
  • On the JVM: Java and Kotlin are fine for 2025-era servers. If you need 2026-07-28 today, wait for Java 3.x or put the server in another language.

Test whatever you build with MCP Inspector before you connect it to a model. For the server design questions an SDK can't answer for you, such as transport, hosting and side effects, see how to build an MCP server.

Further reading

Need a phone-call tool for the agent you're building? DialMCP is a hosted remote MCP server. Any MCP client, from Claude and ChatGPT to your own SDK client, can use it to place real phone calls from your own verified number and get back a transcript and a structured outcome. Connect to https://mcp.dialmcp.com/mcp and sign in with OAuth. US and Canada, free during launch.

Connect DialMCP to your client

FAQ

What is the MCP SDK?

There isn't one MCP SDK. The Model Context Protocol project publishes ten official SDKs: TypeScript, Python, Go, C#, Rust and Ruby (Tier 1), Java (Tier 2), and Kotlin, Swift and PHP (Tier 3). Each one implements the protocol messages, transports and auth helpers so you can build MCP servers or clients without handling JSON-RPC yourself.

Is @modelcontextprotocol/sdk deprecated?

It isn't deprecated, but it is the v1 maintenance line. It implements the spec up to 2025-11-25 and will not get 2026-07-28 support. It receives bug fixes and security updates for at least six months after v2's July 27, 2026 release. New TypeScript code should use @modelcontextprotocol/server and @modelcontextprotocol/client.

Should I use the official mcp package or FastMCP in Python?

The official package is mcp on PyPI, and its server class is now called MCPServer (it was FastMCP in v1). The standalone fastmcp package from PrefectHQ is a separate framework built on top of mcp 2.x. Start with the official SDK. Add fastmcp only if you need its extra tooling.

Which MCP SDKs support the 2026-07-28 spec?

As of October 14, 2026: TypeScript v2, Python, Go, C#, Rust, Ruby and PHP. In TypeScript it is opt-in on both the client and the server, and Go's HTTP server needs stateless mode. Java, Kotlin and Swift have no release with it yet; Java plans it for 3.x.

Do I need an SDK to use an MCP server?

No. To use an existing server you only need an MCP host such as Claude, ChatGPT, Cursor or VS Code: add the server's URL as a connector. You need an SDK only to build a server, or to build your own client or agent.