Skip to content

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).

Terminal window
pnpm add @protogrid/sdk @modelcontextprotocol/client

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.

  • Works with the official MCP client v2, @modelcontextprotocol/client 2.2 or later.
  • Speaks the 2026-07-28 protocol with servers that support it when your Client has versionNegotiation: { mode: "auto" }, and the 2025 revisions with the rest.
  • OAuth: before any code is exchanged, the redirect must carry this flow’s state and 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/sdk 1.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: state is checked with loopbackConsent from 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.

  • New code: 0.5 with @modelcontextprotocol/client.
  • Your agent still creates its Client from @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 v1 Client, 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.
  1. Replace @modelcontextprotocol/sdk with @modelcontextprotocol/client and import Client from @modelcontextprotocol/client.
  2. 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).
  3. A custom ConsentHandler implements waitForCallback() returning the redirect’s query parameters instead of waitForCode(); with manualConsent, relay the whole URL the browser was sent to.
  4. Tokens your TokenStore holds 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>:discovery key.
import { createClient } from "@protogrid/sdk";
const registry = createClient({ baseUrl: "https://api.protogrid.dev" }); // apiKey (default: PROTOGRID_API_KEY), fetch, timeoutMs optional
const 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 block
const 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(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.

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 set
substituteSecrets(conn.connection, process.env); // deep copy; throws MissingSecretsError listing what is missing
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: true
if (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.

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 own
const consent = await loopbackConsent({ port: 8765 }); // prints the URL a human must open once
const 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 clientMetadataUrl and the server supports it), PKCE, the consent URL, then the code exchange. Tokens land in the store.
  • Every later run: connectClient finds 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 state this flow sent and, when the authorization server says who it is (iss), that issuer; otherwise connectClient throws 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.
  • TokenStore is 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). FileTokenStore keeps refresh tokens in a plain file (mode 0600); outside a machine you control, use an encrypted store.

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.

The repository ships sdks/typescript/examples/find-and-call.ts: search, pick an autonomous server, connect, list tools, call one, print the result.

Terminal window
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