TypeScript SDK
MIT licensed, zero runtime dependencies. @modelcontextprotocol/sdk is an optional peer, needed only for createTransport.
pnpm add @protogrid/sdk @modelcontextprotocol/sdkClient
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 });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");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);}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/sdk/client/index.js";
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" }); 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.
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/sdk/client/index.js";
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" });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. - Headless agents use
manualConsent({ redirectUrl, onAuthorizationUrl, waitForCode })to relay the URL and the code through whatever channel they have. TokenStoreis three async methods (get,set,delete); keys are<server name>:<tokens|client|verifier|state>.
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.
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