TypeScript SDK
MIT licensed, zero runtime dependencies. The official MCP client, @modelcontextprotocol/client 2.x, is an optional peer, needed only to connect (createTransport, connectClient, OAuth).
pnpm add @protogrid/sdk @modelcontextprotocol/clientVersions and compatibility
Section titled “Versions and compatibility”Two release lines exist because the official MCP TypeScript SDK changed its package between v1 and v2, and a transport must come from the same package as the Client it is handed to.
Use 0.5.1 or 0.4.2 or later for OAuth. Earlier MCP client packages could send stored OAuth credentials to an authorization server chosen by the MCP server (GHSA-6qxp-vccf-f47h), and both releases ignore tokens saved without the authorization server that issued them, so each OAuth server asks for consent once more after the upgrade.
0.5 (current)
Section titled “0.5 (current)”- Works with the official MCP client v2,
@modelcontextprotocol/client2.2 or later. - Speaks the 2026-07-28 protocol with servers that support it when your
ClienthasversionNegotiation: { mode: "auto" }, and the 2025 revisions with the rest. - OAuth: before any code is exchanged, the redirect must carry this flow’s
stateand the issuer of the authorization server (RFC 9207). - Consent handlers implement
waitForCallback();manualConsent({ waitForRedirect })takes the full URL the browser was redirected to.
- Works with the official MCP SDK v1,
@modelcontextprotocol/sdk1.31 or later (from 0.4.2; 0.4.1 accepted 1.20). - Install with
pnpm add @protogrid/sdk@mcp-client-v1 @modelcontextprotocol/sdk: npm’s default tag points to 0.5. - Speaks the 2025 revisions only (up to 2025-11-25); every server that supports 2026-07-28 still answers 2025 clients.
- OAuth:
stateis checked withloopbackConsentfrom 0.4.1 on; no RFC 9207 check of the redirect’s issuer. - Consent handlers implement
waitForCode();manualConsent({ waitForCode })takes the code.
Both lines have the same registry client (search and its filters, descriptors, connection blocks, quality, check; gate from 0.5.4 only), the same secret helpers and formatters, and need Node.js 20 or later.
Which one to choose
Section titled “Which one to choose”- New code: 0.5 with
@modelcontextprotocol/client. - Your agent still creates its
Clientfrom@modelcontextprotocol/sdk(v1): stay on 0.4 (pnpm add @protogrid/sdk@mcp-client-v1) until you move to v2. Mixing a 0.5 transport with a v1Client, or the reverse, is not supported. 0.4 stays on npm and gets no new features. - You only use the registry (search, descriptors, connection blocks, checks) and connect with something else: either line works, and you need no MCP package at all.
Moving from 0.4 to 0.5
Section titled “Moving from 0.4 to 0.5”- Replace
@modelcontextprotocol/sdkwith@modelcontextprotocol/clientand importClientfrom@modelcontextprotocol/client. - Create the client with
new Client(info, { versionNegotiation: { mode: "auto" } })to speak 2026-07-28 with servers that support it (without it, v2 clients use the 2025 handshake). - A custom
ConsentHandlerimplementswaitForCallback()returning the redirect’s query parameters instead ofwaitForCode(); withmanualConsent, relay the whole URL the browser was sent to. - Tokens your
TokenStoreholds from 0.4.1 or earlier carry no issuer and are ignored, so each OAuth server asks for consent once; the store gains a<server name>:discoverykey.
Client
Section titled “Client”import { createClient } from "@protogrid/sdk";
const registry = createClient({ baseUrl: "https://api.protogrid.dev" }); // apiKey (default: PROTOGRID_API_KEY), fetch, timeoutMs optionalconst hits = await registry.search({ q: "send an email", class: ["R0", "R1"], min_trust: 60 }); // or registry.search("send an email")const descriptor = await registry.getServer(hits.results[0].name);const tools = await registry.listAllTools(hits.results[0].name);const conn = await registry.getConnection(hits.results[0].name); // mcpServers blockconst cli = await registry.getConnection(hits.results[0].name, "claude-code-cli");search("text") is shorthand for search({ q: "text" }) from 0.5.2; a search without a query rejects with a TypeError instead of sending a request.
From 0.5.3 every other method does the same when a server name, check id, URL or connection target is missing, empty or not a string (for example getServer({ name }) instead of getServer(name)).
Search takes the same filters as the REST API: quality and owner too, so an agent can skip servers with known vulnerabilities in their dependencies:
const safer = await registry.search({ q: "query postgres", min_quality: 75, exclude_quality_flags: ["known-vulns"], owner_verified: true });getQuality(name, { days }), getChanges(name, { limit, before }) and getDependencies(name) read a server’s quality report, its tool-definition history and its dependency advisories.
Pin quality.tools_hash from the descriptor and compare it before trusting new tool definitions.
From 0.5.4, gate(name, rules) answers pass or fail for one server against your rules, with every rule it broke (see the gate); call it before connecting:
const g = await registry.gate(name, { min_quality: 75, no_fail: ["tools.hidden_text"], exclude_quality_flags: ["known-vulns", "hidden-text"] });if (!g.pass) throw new Error(`${name}: ${g.failed.map((f) => f.detail).join("; ")}`);Errors are ProtogridError with status, code (not_found, blocked, deleted, no_connection, bad_request, http_429 …), body and retryAfterMs.
Check a server
Section titled “Check a server”check(url) probes any remote MCP server URL once, listed or not, and returns its quality checks and its readiness for the Claude and OpenAI directories (see Check your server).
It waits up to 90 seconds by default (waitMs); a check still running then can be read later with getCheck(id).
const c = await registry.check("https://mcp.example.com/mcp");if (c.status === "done") { for (const d of c.result!.readiness) console.log(d.name, d.summary.blockers, "blockers"); console.log("quality", c.result!.quality.score, c.page);}The same URL within five minutes returns the recent check; with an API key, check(url, { fresh: true }) runs a new one (for CI right after a deploy; it counts against your hourly allowance).
A refused URL throws invalid_url; an exhausted hourly allowance throws check_quota_exceeded with retryAfterMs.
Secrets
Section titled “Secrets”Connection blocks carry ${NAME} placeholders. The SDK fills them at the last moment from a map you provide; the registry never sees values.
import { placeholdersIn, substituteSecrets, secretsSatisfied } from "@protogrid/sdk";
placeholdersIn(conn.connection); // ["AGENTDM_TOKEN"]secretsSatisfied(conn, process.env); // false until AGENTDM_TOKEN is setsubstituteSecrets(conn.connection, process.env); // deep copy; throws MissingSecretsError listing what is missingConnect
Section titled “Connect”import { createTransport, findConnectable } from "@protogrid/sdk";import { Client } from "@modelcontextprotocol/client";
const found = await findConnectable(registry, { q: "get the weather for a city" }, { secrets: process.env });// first hit that is R0, or R1 with every secret present; L0 only with allowLocal: trueif (found) { const mcp = new Client({ name: "my-agent", version: "1.0.0" }, { versionNegotiation: { mode: "auto" } }); await mcp.connect(await createTransport(found.connection, process.env)); const { tools } = await mcp.listTools(); const out = await mcp.callTool({ name: tools[0].name, arguments: { city: "Madrid" } });}createTransport builds a streamable-HTTP, SSE or stdio transport from the mcpServers entry. Bundles (.mcpb) have no transport.
With versionNegotiation: { mode: "auto" } the client asks the server for the 2026-07-28 protocol first and falls back to the 2025 handshake, so one client reaches both kinds of server.
Without @modelcontextprotocol/client installed, createTransport throws an error that says what to install.
OAuth (R2 servers)
Section titled “OAuth (R2 servers)”R2 servers sit behind OAuth 2. The agent holds the tokens; the registry only exposes the metadata. The SDK supplies the official MCP SDK’s OAuthClientProvider from a pluggable TokenStore and runs the single human step, the one-time consent:
import { connectClient, createClient, FileTokenStore, loopbackConsent } from "@protogrid/sdk";import { Client } from "@modelcontextprotocol/client";
const store = new FileTokenStore(".mcp-tokens.json"); // or MemoryTokenStore, or your ownconst consent = await loopbackConsent({ port: 8765 }); // prints the URL a human must open onceconst conn = await registry.getConnection("ac.snag/snag");const mcp = new Client({ name: "my-agent", version: "1.0.0" }, { versionNegotiation: { mode: "auto" } });await connectClient(mcp, conn, process.env, { oauth: { store, consent, clientName: "my agent" } });- First run: protected-resource and authorization-server discovery, dynamic client registration (or a Client ID Metadata Document when you pass
clientMetadataUrland the server supports it), PKCE, the consent URL, then the code exchange. Tokens land in the store. - Every later run:
connectClientfinds the tokens, refreshes when needed, and connects with no human step.findConnectable(..., { tokenStore })then counts that R2 server as connectable. - Before any code is exchanged, the redirect must carry the
statethis flow sent and, when the authorization server says who it is (iss), that issuer; otherwiseconnectClientthrows and nothing is stored. A denied consent throws with the error code only. - Headless agents use
manualConsent({ redirectUrl, onAuthorizationUrl, waitForRedirect })to relay the authorization URL through whatever channel they have and hand back the full URL the browser was redirected to. TokenStoreis three async methods (get,set,delete); keys are<server name>:<tokens|client|verifier|state|discovery>. Return values exactly as they were stored: tokens and client information carry the issuer that granted them, and values without one are ignored (0.5.1).FileTokenStorekeeps refresh tokens in a plain file (mode 0600); outside a machine you control, use an encrypted store.
Other formatters
Section titled “Other formatters”toClaudeAgentSdk(conn, secrets) returns the mcpServers map the Claude Agent SDK’s query() options take (http, sse or stdio entries). toRawTransport(conn, secrets) returns the parameters for hand-built official-SDK transports.
Every wire shape is exported: SearchResponse, Descriptor, Connectability, DescriptorRemote, DescriptorPackage, Trust, ListToolsResponse, ConnectionResponse, McpServersConnection, ConnectionClass, ConnectionTarget, TrustFlag, Quality, QualityCheck, QualityFlag, QualityResponse, ChangesResponse, DependenciesResponse, CheckResponse.
Example
Section titled “Example”The repository ships sdks/typescript/examples/find-and-call.ts: search, pick an autonomous server, connect, list tools, call one, print the result.
pnpm --filter @protogrid/sdk example "get the current weather for a city" "Madrid"npx tsx sdks/typescript/examples/oauth-consent.ts ac.snag/snag # R2: consent once# both use the public registry; prefix with REGISTRY_URL=http://localhost:8080 for a local stack