# Connect Locus Pro to model SDKs and agent frameworks

One minimal example per model SDK and agent framework for giving an
application's agent Locus Pro tools over MCP, plus the OpenAI-compatible
inference endpoint. Read it when you are writing application code that runs an
agent with Locus tools. To connect a desktop or coding client (Claude, Cursor,
Codex, OpenClaw, Hermes) use OAuth through the
[MCP guide](https://paywithlocus.com/agent/mcp.md) instead.

Part of the Locus agent docs: [index](https://paywithlocus.com/agent/index.md).

## Connection values

```bash
export LOCUS_AGENT_CONNECTION="lcac_..."   # agent key, from your secret store
export LOCUS_MCP_URL="https://api.paywithlocus.com/api/credits/mcp"
```

- Transport: MCP Streamable HTTP, header `Authorization: Bearer lcac_...`.
- Create the agent key on your trusted backend (dashboard **Connect → API
  keys → An agent**, `POST https://api.paywithlocus.com/api/credits/agent-connections`,
  or `locus.agentConnections.create()` in the
  [SDK](https://paywithlocus.com/agent/sdk.md)) with a tool list, a
  per-call limit and an expiry. Give the runtime only the agent key, never
  the `lcr_` secret key.
- Store it in the runtime's secret manager. These examples are application
  code you deploy; they are not a reason to paste a key into an agent host's
  MCP configuration file (see
  [credentials](https://paywithlocus.com/agent/credentials.md)).
- Give each deployment its own key so analytics and revocation stay per agent.
- If the key has `maxCreditsPerLoop`, every paid call needs a loop ID: MCP
  argument `loop_id`, REST header `X-Locus-Agent-Loop-Id`.

The `@withlocus/credits` presets take a revealed connection: the object
returned by `agentConnections.create()` or `rotate()`. In a runtime that only
has the stored key, build the same shape:

```typescript
import type { RevealedAgentConnection } from "@withlocus/credits";

const credential = process.env.LOCUS_AGENT_CONNECTION!; // must start with lcac_
export const connection: RevealedAgentConnection = {
  credential,
  mcp: {
    url: "https://api.paywithlocus.com/api/credits/mcp",
    headers: { Authorization: `Bearer ${credential}` },
  },
};
```

Presets throw a `TypeError` unless `credential` starts with `lcac_`, the URL
is HTTPS, and `Authorization` equals `Bearer <credential>`. The server name
defaults to `locus_pro`.

| Preset | Output |
|--------|--------|
| `mcpHttpConnection(c)` | `{ url, headers }` for any client |
| `openAIResponsesMcpTool(c, { requireApproval? })` | Responses API `tools[]` entry (`authorization` = raw key, `require_approval` default `"never"`) |
| `openAIAgentsHostedMcpTool(c)` | Options for `hostedMcpTool()` |
| `anthropicMessagesMcp(c)` | `{ server, toolset }` for the Messages MCP connector |
| `claudeAgentSdkMcpServer(c)` | `{ type: "http", url, headers }` |
| `vercelAIMcpTransport(c)` | `{ type: "http", url, headers }` |
| `langChainMcpServer(c)` | `{ transport: "http", url, headers }` |
| `mastraMcpServer(c)` | `{ url: URL, requestInit: { headers } }` |
| `googleGenAIMcpServer(c)` | `{ type: "mcp_server", name, url, headers }` |
| `llamaIndexTSMcpServer(c)` | `{ url, requestInit: { headers } }` |
| `googleAdkMcpServer`, `autoGenMcpServer`, `crewAIMcpServer`, `llamaIndexMcpClient` | Keyword values for the Python constructors below |

## OpenAI

Responses API (hosted MCP tool; OpenAI calls Locus):

```typescript
import OpenAI from "openai";
import { openAIResponsesMcpTool } from "@withlocus/credits";

const response = await new OpenAI().responses.create({
  model: process.env.OPENAI_MODEL!,
  input: "Research the topic and cite sources.",
  tools: [openAIResponsesMcpTool(connection)],
});
console.log(response.output_text);
```

Python uses the same fields: `{"type":"mcp","server_label":"locus_pro","server_url":LOCUS_MCP_URL,"authorization":LOCUS_AGENT_CONNECTION,"require_approval":"never"}`.
Agents SDK (TypeScript): `tools: [hostedMcpTool(openAIAgentsHostedMcpTool(connection))]`
from `@openai/agents`. The managed Agents API expects the full
`Bearer lcac_...` value in its MCP transport `authorization`. Use
`requireApproval: "always"` if your app should confirm each call; Locus limits
apply either way.

## Anthropic

Messages API MCP connector (Anthropic calls Locus; raw key in
`authorization_token`):

```typescript
import Anthropic from "@anthropic-ai/sdk";
import { anthropicMessagesMcp } from "@withlocus/credits";

const { server, toolset } = anthropicMessagesMcp(connection);
const msg = await new Anthropic().beta.messages.create({
  model: process.env.ANTHROPIC_MODEL!,
  max_tokens: 2048,
  betas: ["mcp-client-2025-11-20"],
  mcp_servers: [server],
  tools: [toolset],
  messages: [{ role: "user", content: "Find evidence and summarize it." }],
});
```

Claude Agent SDK (MCP runs in your process; use 0.2.70 or later):

```typescript
import { query } from "@anthropic-ai/claude-agent-sdk";
import { claudeAgentSdkMcpServer } from "@withlocus/credits";

for await (const m of query({
  prompt: "Research the request with the approved Locus tools.",
  options: { mcpServers: { locus_pro: claudeAgentSdkMcpServer(connection) }, strictMcpConfig: true },
})) console.log(m);
```

## Vercel AI SDK

```typescript
import { createMCPClient } from "@ai-sdk/mcp";
import { vercelAIMcpTransport } from "@withlocus/credits";

const mcp = await createMCPClient({ transport: vercelAIMcpTransport(connection) });
try {
  const tools = await mcp.tools(); // pass to generateText / streamText / an Agent
} finally {
  await mcp.close();
}
```

Keep one client open for the agent's lifetime.

## Google

Gen AI Interactions API: `tools: [googleGenAIMcpServer(connection)]` in
`new GoogleGenAI({}).interactions.create({ model, input, tools })`. Server
names must not contain hyphens.

Google ADK (Python, `pip install "google-adk[mcp]"`):

```python
import os
from google.adk.agents import LlmAgent
from google.adk.tools.mcp_tool.mcp_toolset import McpToolset
from google.adk.tools.mcp_tool.mcp_session_manager import StreamableHTTPConnectionParams

agent = LlmAgent(
    name="locus_research_agent",
    model=os.environ["GEMINI_MODEL"],
    instruction="Use only the approved Locus tools.",
    tools=[McpToolset(connection_params=StreamableHTTPConnectionParams(
        url=os.environ["LOCUS_MCP_URL"],
        headers={"Authorization": f"Bearer {os.environ['LOCUS_AGENT_CONNECTION']}"},
    ))],
)
```

## LangChain and LangGraph

`createAgent` builds a LangGraph agent, so the same tools work in your own
graphs.

```typescript
import { createAgent } from "langchain";
import { MultiServerMCPClient } from "@langchain/mcp-adapters";
import { langChainMcpServer } from "@withlocus/credits";

const mcp = new MultiServerMCPClient({ locus_pro: langChainMcpServer(connection) });
const agent = createAgent({ model: process.env.LANGCHAIN_MODEL!, tools: await mcp.getTools() });
const result = await agent.invoke({ messages: [{ role: "user", content: "Research the topic." }] });
```

Python: `MultiServerMCPClient({"locus_pro": {"transport": "http", "url": ..., "headers": {"Authorization": "Bearer ..."}}})`
from `langchain-mcp-adapters`, then `await client.get_tools()`.

## Mastra

```typescript
import { MCPClient } from "@mastra/mcp";
import { mastraMcpServer } from "@withlocus/credits";

const mcp = new MCPClient({ id: "locus-research-prod", servers: { locus_pro: mastraMcpServer(connection) } });
try {
  const tools = await mcp.listTools(); // pass to a Mastra Agent
} finally {
  await mcp.disconnect();
}
```

## LlamaIndex

TypeScript: `const server = mcp(llamaIndexTSMcpServer(connection)); const tools = await server.tools();`
(`@llamaindex/tools`), then `await server.cleanup()`.
Python (`pip install "llama-index-tools-mcp<0.5" "mcp>=1.24,<2"`):

```python
from llama_index.tools.mcp import BasicMCPClient, McpToolSpec
client = BasicMCPClient(os.environ["LOCUS_MCP_URL"],
    headers={"Authorization": f"Bearer {os.environ['LOCUS_AGENT_CONNECTION']}"})
tools = await McpToolSpec(client=client).to_tool_list_async()
```

## CrewAI, AutoGen, Strands

```python
# CrewAI: pip install crewai
from crewai.mcp import MCPServerHTTP
mcps = [MCPServerHTTP(url=URL, headers=HEADERS, streamable=True, cache_tools_list=True)]

# AutoGen: pip install "autogen-agentchat" "autogen-ext[openai,mcp]" "mcp>=1.24,<2"
from autogen_ext.tools.mcp import McpWorkbench, StreamableHttpServerParams
async with McpWorkbench(server_params=StreamableHttpServerParams(url=URL, headers=HEADERS)) as wb:
    ...  # AssistantAgent(..., workbench=wb)

# Strands: pip install "strands-agents>=1.53"
from strands import Agent
from strands.tools.mcp import MCPClient
agent = Agent(tools=[MCPClient(url=URL, headers=HEADERS)])
```

with `URL = os.environ["LOCUS_MCP_URL"]` and
`HEADERS = {"Authorization": f"Bearer {os.environ['LOCUS_AGENT_CONNECTION']}"}`.
Refresh a cached tool list after you change the workspace catalog.

## Custom MCP runtime

```typescript
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import { mcpHttpConnection, parseMcpToolResult } from "@withlocus/credits";

const { url, headers } = mcpHttpConnection(connection);
const client = new Client({ name: "my-agent", version: "1.0.0" });
await client.connect(new StreamableHTTPClientTransport(new URL(url), { requestInit: { headers } }));
const { tools } = await client.listTools();
const result = parseMcpToolResult(await client.callTool({ name: "search_apis", arguments: { query: "web search" } }));
if (!result.ok) throw new Error(`${result.code ?? result.kind}: ${result.message}`);
```

An adapter needs four things: connect with URL and header; map each
`tools/list` JSON Schema to the framework's tool type; forward `tools/call`
arguments unchanged; treat a resolved result with `isError: true` as a failed
call. `parseMcpToolResult` keeps billing metadata (`creditsCharged`,
`creditsBalance`), artifacts, `retryAfter`, `safeToRetry` and `refunded`.
Retry an uncertain paid call with the same `idempotency_key`. The tool
surface itself is described in the [MCP guide](https://paywithlocus.com/agent/mcp.md)
and [tools](https://paywithlocus.com/agent/tools.md).

## OpenAI-compatible inference

Model calls are paid from the same credits. Enable the model endpoint (for
example `openai/chat`) in the workspace first.

```text
Base URL: https://api.paywithlocus.com/api/wrapped/credits/v1
Route:    POST /chat/completions   (OpenAI request body; JSON or stream: true)
API key:  your Locus credential (lcac_ agent key, or lcr_ secret key on a server)
Required: Idempotency-Key header, unique per generation, reused on retries
```

```typescript
import OpenAI from "openai";

const client = new OpenAI({
  baseURL: "https://api.paywithlocus.com/api/wrapped/credits/v1",
  apiKey: process.env.LOCUS_AGENT_CONNECTION!,
});
const completion = await client.chat.completions.create(
  { model: "gpt-4o-mini", messages: [{ role: "user", content: "Summarize this." }], max_tokens: 120 },
  { headers: { "Idempotency-Key": `${turnId}:synthesis:1` } },
);
```

Python: `client.chat.completions.create(..., extra_headers={"Idempotency-Key": key})`.
Vercel AI SDK: `createOpenAI({ baseURL, apiKey }).chat("gpt-4o-mini")`
(Chat Completions, not the Responses API) and pass `headers` per
`generateText` call. Only `/chat/completions` exists under `/v1`. Provider
bodies also work directly at
`POST https://api.paywithlocus.com/api/wrapped/credits/openai/chat` and
`.../anthropic/chat`.

| Header | Meaning |
|--------|---------|
| `X-Locus-Credits-Charged` / `X-Locus-Credits-Balance` | Settled charge and balance (non-streaming) |
| `X-Locus-Api-Call-Id` | Call ID for status and support |
| `X-Locus-Max-Charge-Credits` (request) | Refuse before dispatch if the hold exceeds it: `409 charge_ceiling_exceeded` |
| `X-Locus-Finish-Reason`, `X-Locus-Input-Tokens`, `X-Locus-Visible-Output-Tokens`, `X-Locus-Reasoning-Tokens` | Diagnostics when the provider reports them |
| `X-Locus-Credits-Reserved`, `X-Locus-Stream-Billing: settled-after-delivery`, `X-Locus-Stream-Status-Url` | Streaming: the hold, and where to read the final charge |

Charging: a hold is sized from the prompt and `max_tokens`; the call settles
from reported usage and never exceeds the hold (missing usage charges the
hold). A stream ends with one SSE event `event: locus.billing` whose
`locus_billing` holds `apiCallId`, `chargedCredits`, `balanceCredits`
(omitted when `settlement` is `deferred`) and `statusUrl`; OpenAI clients stop
at `data: [DONE]` and Anthropic clients skip it as a `ping`. A disconnect after
the provider accepts the stream charges the full hold. Reusing a key after a
stream started returns `409 stream_not_replayable`. MCP tool calls are
non-streaming; use REST to stream.

## Next steps

- [MCP guide](https://paywithlocus.com/agent/mcp.md): OAuth setup for coding clients and the full MCP tool surface.
- [SDK](https://paywithlocus.com/agent/sdk.md): create agent keys and use the presets and result helpers.
- [Resilient paid runs](https://paywithlocus.com/agent/runtime.md): durable turns, grounded synthesis and exact accounting around these calls.
- [Tools](https://paywithlocus.com/agent/tools.md): search, describe and execute catalog tools with charge caps.
- [Agent docs index](https://paywithlocus.com/agent/index.md): every Locus agent page.
