Skip to main content

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

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

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

Get Help

Developer Slack