Overview
This recipe shows how to wire Civic MCP tools to the OpenAI Node SDK using manual function calling.
Want a simpler approach? The OpenAI Agents SDK recipe uses hostedMcpTool() — no manual tool looping required. Use this page if you need full control over the tool loop.
Prerequisites
Installation
npm install openai @modelcontextprotocol/sdk
Authentication
Generate a Civic Token
Log in to app.civic.com
Click your account name in the bottom left
Go to Install → MCP URL
Click Generate Token and copy it immediately — it won’t be shown again
Never commit your token to source control. Store it in environment variables or a secrets manager. Tokens expire after 30 days.
Set Environment Variables CIVIC_TOKEN = your-civic-token-here
CIVIC_URL = https://app.civic.com/hub/mcp
For production agents, lock to a specific toolkit by appending a profile parameter: CIVIC_URL = https://app.civic.com/hub/mcp? profile = your-toolkit-alias
Use the Token Pass the token as a Bearer token in the Authorization header: headers = { "Authorization" : f "Bearer { os.environ[ 'CIVIC_TOKEN' ] } " }
headers : { Authorization : `Bearer ${ process . env . CIVIC_TOKEN } ` }
Full credentials guide Token generation, URL parameters, OAuth vs token comparison
# .env
OPENAI_API_KEY = your_openai_api_key
CIVIC_TOKEN = your-civic-token-here
CIVIC_URL = https://app.civic.com/hub/mcp
For web apps where each user has their own Civic session. Install the additional auth package: Why Civic Auth? Civic needs to identify which user is accessing tools and authorize their permissions. Civic Auth provides the secure access token. (Support for additional identity providers coming soon.) 1. next.config.ts
2. API Route
3. Middleware
4. Get Token
import { createCivicAuthPlugin } from "@civic/auth/nextjs"
import type { NextConfig } from "next" ;
const nextConfig : NextConfig = {};
const withCivicAuth = createCivicAuthPlugin ({ clientId: "YOUR_CLIENT_ID" });
export default withCivicAuth ( nextConfig )
File: src/app/api/auth/[...civicauth]/route.tsimport { handler } from "@civic/auth/nextjs"
export const GET = handler ()
export const POST = handler ()
File: src/middleware.tsimport { authMiddleware } from "@civic/auth/nextjs/middleware"
export default authMiddleware () ;
export const config = { matcher: [ '/((?!_next|favicon.ico|.* \\ .png).*)' ,] };
import { getTokens } from "@civic/auth/nextjs" ;
const { accessToken } = await getTokens ();
// Use in headers:
headers : { Authorization : `Bearer ${ accessToken } ` }
Full Integration Guide Complete Next.js setup with frontend components, configuration options, and deployment details
AI Prompt for Next.js Use Claude, ChatGPT, or other AI assistants to automatically set up Civic Auth
# .env.local
OPENAI_API_KEY = your_openai_api_key
CIVIC_AUTH_CLIENT_ID = your_client_id # from auth.civic.com
Create an MCP Client
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js' ;
import { Client } from '@modelcontextprotocol/sdk/client/index.js' ;
async function createMCP ( token : string ) {
const transport = new StreamableHTTPClientTransport (
new URL ( process . env . CIVIC_URL ! ),
{
requestInit: {
headers: {
Authorization: `Bearer ${ token } ` ,
'Content-Type' : 'application/json' ,
},
},
}
);
const client = new Client (
{ name: 'my-app' , version: '1.0.0' },
{ capabilities: {} }
);
await client . connect ( transport );
return client ;
}
This example handles a single round of tool calling. Real agent loops need to iterate until the model stops requesting tools — see the multi-turn loop below.
import OpenAI from 'openai' ;
export async function chatWithTools ( messages : any [], civicToken : string ) {
const openai = new OpenAI ({ apiKey: process . env . OPENAI_API_KEY ! });
const mcp = await createMCP ( civicToken );
const { tools } = await mcp . listTools ();
const toolDefs = tools . map (( t ) => ({
type: 'function' as const ,
function: {
name: t . name ,
description: t . description ,
parameters: t . inputSchema ,
},
}));
// Multi-turn tool loop — runs until the model stops requesting tools
let response = await openai . chat . completions . create ({
model: 'gpt-4o-mini' ,
messages ,
tools: toolDefs ,
tool_choice: 'auto' ,
});
while ( response . choices [ 0 ]?. finish_reason === 'tool_calls' ) {
const toolCalls = response . choices [ 0 ]. message . tool_calls ?? [];
const toolResults = await Promise . all (
toolCalls . map ( async ( call ) => {
const args = JSON . parse ( call . function . arguments || '{}' );
const result = await mcp . callTool ({ name: call . function . name , arguments: args });
return {
role: 'tool' as const ,
tool_call_id: call . id ,
content: JSON . stringify ( result . content ),
};
})
);
messages = [
... messages ,
response . choices [ 0 ]. message ,
... toolResults ,
];
response = await openai . chat . completions . create ({
model: 'gpt-4o-mini' ,
messages ,
tools: toolDefs ,
});
}
await mcp . close ();
return response ;
}
Usage
// Backend / Script
const result = await chatWithTools (
[{ role: 'user' , content: 'List my GitHub repositories' }],
process . env . CIVIC_TOKEN !
);
console . log ( result . choices [ 0 ]. message . content );
OpenAI Agents SDK Simpler approach — hostedMcpTool() handles the loop for you