@civic/mcp-client
TypeScript client library for connecting to Civic MCP Hub and exposing MCP (Model Context Protocol) tools to AI SDKs like Vercel AI, OpenAI, Anthropic, and LangChain.js.
Installation
pnpm add @civic/mcp-client
Depending on which AI SDK you're using, also install the peer dependency:
# For Vercel AI SDK
pnpm add ai
# For OpenAI
pnpm add openai
# For Anthropic
pnpm add @anthropic-ai/sdk
# For LangChain.js
pnpm add @langchain/core
Quick Start
With Vercel AI SDK
import { CivicMcpClient } from "@civic/mcp-client";
import { vercelAIAdapter } from "@civic/mcp-client/adapters/vercel-ai";
import { streamText } from "ai";
const client = new CivicMcpClient({
auth: {
token: "your-access-token",
},
});
const tools = await client.getTools(vercelAIAdapter());
const result = streamText({
model: yourModel,
messages: [{ role: "user", content: "Search GitHub for civic repos" }],
tools,
});
// Clean up when done
await client.close();
With OpenAI SDK
import { CivicMcpClient } from "@civic/mcp-client";
import { openAIAdapter } from "@civic/mcp-client/adapters/openai";
import OpenAI from "openai";
const client = new CivicMcpClient({
auth: { token: "your-access-token" },
});
const tools = await client.getTools(openAIAdapter());
const openai = new OpenAI();
const response = await openai.chat.completions.create({
model: "gpt-4",
messages: [{ role: "user", content: "Search GitHub for civic repos" }],
tools,
});
With Anthropic SDK
import { CivicMcpClient } from "@civic/mcp-client";
import { anthropicAdapter } from "@civic/mcp-client/adapters/anthropic";
import Anthropic from "@anthropic-ai/sdk";
const client = new CivicMcpClient({
auth: { token: "your-access-token" },
});
const tools = await client.getTools(anthropicAdapter());
const anthropic = new Anthropic();
const message = await anthropic.messages.create({
model: "claude-3-5-sonnet-20241022",
messages: [{ role: "user", content: "Search GitHub for civic repos" }],
tools,
});
With LangChain.js
Returns an array of DynamicStructuredTool instances ready to drop into any LangChain agent — langchain v1's createAgent, LangGraph's createReactAgent, the legacy AgentExecutor, or n8n's "AI Agent" node.
Tool results are concatenated into a single string by default (MCP text blocks joined; non-text blocks JSON-stringified), which is the most compatible shape for every model and agent variant.
import { CivicMcpClient } from "@civic/mcp-client";
import { langchainAdapter } from "@civic/mcp-client/adapters/langchain";
import { ChatOpenAI } from "@langchain/openai";
import { createReactAgent } from "@langchain/langgraph/prebuilt";
const client = new CivicMcpClient({
auth: { token: "your-access-token" },
});
const tools = await client.getTools(langchainAdapter());
const agent = createReactAgent({
llm: new ChatOpenAI({ model: "gpt-4o-mini" }),
tools,
});
const result = await agent.invoke({
messages: [{ role: "user", content: "Search GitHub for civic repos" }],
});
Features
- Protocol Version Negotiation: Speaks MCP 2026-07-28 to servers that support it and falls back to the 2025-era handshake automatically
- Session Management: Maintains persistent MCP sessions with automatic reconnection on legacy-era servers
- Framework Agnostic: Works with any AI SDK through adapters
- TypeScript First: Full type safety and IntelliSense support
- Flexible Authentication: Support for static tokens, async token providers, or automatic token exchange
- Lightweight: No unnecessary dependencies, minimal bundle size
API Reference
CivicMcpClient
Constructor Options
interface CivicMcpClientConfig {
// Hub URL (defaults to "https://app.civic.com/hub/mcp")
url?: string;
// Authentication — provide either a token directly or use token exchange
auth:
| { token: string | (() => Promise<string>) }
| { tokenExchange: TokenExchangeConfig };
// Scope requests to a specific profile (UUID)
civicProfile?: string; // Sent as x-civic-profile-id header
// Optional custom headers
headers?: Record<string, string>;
// Optional connection options
reconnection?: {
maxRetries?: number;
initialReconnectionDelay?: number;
maxReconnectionDelay?: number;
reconnectionDelayGrowFactor?: number;
};
// Protocol version negotiation (default: "auto")
// "auto": probe with server/discover, fall back to initialize on legacy servers
// "legacy": always run the 2025-era initialize handshake
// { pin: "2026-07-28" }: require exactly that revision, fail otherwise
versionNegotiation?: "auto" | "legacy" | { pin: string };
}
interface TokenExchangeConfig {
clientId: string; // Civic Auth Client ID
clientSecret: string; // Civic Auth Client Secret
subjectToken: string | (() => Promise<string>); // External IdP token
authUrl?: string; // Token endpoint (default: "https://auth.civic.com/oauth/token")
expiresIn?: number; // Requested token lifetime in seconds (server default if omitted)
lockToProfile?: boolean; // Lock exchanged token to civicProfile (default: true)
}
Methods
getTools<T>(adapter?: ToolAdapter<T>): Promise<T>- Get tools, optionally adapted for specific AI SDKgetServerInstructions(): Promise<string>- Get server instructions for system promptcallTool(name: string, args: unknown): Promise<CallToolResult>- Call a tool directlygetAccessToken(): string | undefined- Get the currently cached Civic Auth access token (token exchange only)close(): Promise<void>- Close the connection
Advanced Usage
Protocol Version Negotiation
By default the client probes the server with server/discover and uses protocol revision 2026-07-28 when the server offers it. Servers that only speak the 2025-era protocol get the classic initialize handshake, so one build works against both. After connecting, getProtocolEra() reports which era was negotiated:
await client.connect();
client.getProtocolEra(); // "modern" (2026-07-28+) or "legacy" (2025-era)
Session IDs and onInitialized only apply to legacy-era connections; modern-era servers do not issue sessions.
Caching Client Instances
The library doesn't include caching logic - you manage CivicMcpClient instances yourself:
// Simple in-memory cache
const clientCache = new Map<string, CivicMcpClient>();
function getCivicMcpClient(userId: string): CivicMcpClient {
let client = clientCache.get(userId);
if (!client) {
client = new CivicMcpClient({ /* config */ });
clientCache.set(userId, client);
}
return client;
}
Profile Scoping
const client = new CivicMcpClient({
auth: { token: accessToken },
civicProfile: "7c9e6679-7425-40de-944b-e07fc1f90ae7",
});
Dynamic Token Refresh
const client = new CivicMcpClient({
auth: {
token: async () => {
const session = await getSession();
return session.accessToken;
},
},
});
Token Exchange (RFC 8693)
Server-side only. Token exchange requires a
clientSecret, which must never be exposed in browser/client-side code. Only usetokenExchangein server-side environments (Node.js, serverless functions, etc.).
If your app uses its own identity provider (Google, Auth0, etc.), the client can automatically exchange external tokens for Civic Auth access tokens. Exchanged tokens are cached and re-exchanged on expiry or when the external token changes.
const client = new CivicMcpClient({
auth: {
tokenExchange: {
clientId: process.env.CIVIC_CLIENT_ID,
clientSecret: process.env.CIVIC_CLIENT_SECRET,
subjectToken: () => getGoogleAccessToken(user),
},
},
civicProfile: "550e8400-e29b-41d4-a716-446655440000", // optional: scope to a specific profile
});
// Token exchange happens automatically on first use.
// The exchanged token is locked to the civicProfile by default.
const tools = await client.getTools(vercelAIAdapter());
For setup instructions (creating an organization, linking your Client ID, and configuring token exchange), see the Civic Auth token exchange guide.
Upgrading from 1.x
Version 2 swaps the underlying SDK from @modelcontextprotocol/sdk v1 to @modelcontextprotocol/client v2. For most consumers nothing changes: the CivicMcpClient API and the adapters are the same, and the client negotiates the protocol version with the hub automatically.
Things to check:
- Node.js 22+ is required.
CallToolResultandListToolsResultare now defined by this package instead of being re-exported from the v1 SDK. They match the MCP spec shapes, so plain usage is unaffected. If you passed them to v1 SDK functions, cast at that boundary or import the SDK's types directly.- The v1 SDK is no longer installed transitively. If your code imported
@modelcontextprotocol/sdkonly because this package pulled it in, add it to your own dependencies. - Session recovery only applies to legacy-era connections. On a 2026-07-28 server there is no session, so
onInitializeddoes not fire andgetSessionId()returnsundefined.
See CHANGELOG.md for the full list.
License
MIT