Skip to main content

OpenAI SDK

Overview

This recipe shows how to wire Civic MCP tools to the OpenAI Node SDK using manual function calling.

note

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

  1. Log in to app.civic.com
  2. Click your account name in the bottom left
  3. Go to Install → MCP URL
  4. Click Generate Token and copy it immediately — it won't be shown again
warning

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

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;
}

Call with Tool Functions

note

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