Skip to content
2 changes: 1 addition & 1 deletion docs-site/src/content/docs/guides/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -1062,7 +1062,7 @@ OpenCodex provides official adapter support for Qoder through the `qoder` (Globa
([Global](https://qoder.com/account/integrations), [CN](https://qoder.cn/account/integrations)) and paste it as the provider's API key. The stored key reaches the CLI only as `QODER_PERSONAL_ACCESS_TOKEN` (Global) or `QODERCN_PERSONAL_ACCESS_TOKEN` (CN) in a scoped child environment.
- **Region Isolation:** Each preset accepts only its canonical destination (`https://qoder.com` or `https://qoder.cn`) and resolves its own executable. Credentials, model cache, usage, and health are independent; neither region falls back to the other. An older custom provider named `qoder` with a different destination keeps its existing adapter and URL.
- **Model Discovery:** `qoder --list-models` is the authoritative entitlement roster for the current PAT. The cache is bound to an irreversible fingerprint of the token, so switching accounts never reuses another account's roster. If discovery fails, the provider degrades to a stale cache and then the documented static seed.
- **Tool Ownership:** The CLI runs single-turn `stream-json` with `--tools ""`, `--strict-mcp-config`, setting sources disabled, and session persistence disabled, so Codex keeps exclusive tool ownership. v1 is text and reasoning only; image input fails explicitly.
- **Tool Ownership:** The CLI runs single-turn `stream-json` with `--tools ""`, `--strict-mcp-config`, setting sources disabled, and session persistence disabled, so Codex keeps exclusive tool ownership. Client-declared tools are exposed through an isolated request-scoped MCP server; image input fails explicitly.
- **Quota:** No public quota API is used, so totals and reset times are unavailable. Insufficient-credit errors (vendor code 118) surface as HTTP 429 `insufficient_quota`.
- **Operators:** Qoder Global is operated by BRIGHT ZENITH PRIVATE LIMITED under the [product service terms](https://qoder.com/product-service); Qoder CN by 通义云启(杭州)信息技术有限公司 with Alibaba Cloud. Verify `ocx provider test qoder` (or `qoder-cn`) after configuring.

Expand Down
5 changes: 2 additions & 3 deletions scripts/test-layout/layout.json
Original file line number Diff line number Diff line change
Expand Up @@ -1444,8 +1444,7 @@
"proxy-env.test.ts": "server",
"proxy-liveness-package-tree-fence.test.ts": "server",
"proxy-liveness.test.ts": "server",
"qoder-adapter.test.ts": "providers",
"qoder-live-models.test.ts": "providers",
"qoder-adapter.test.ts": "providers", "qoder-live-models.test.ts": "providers", "qoder-mcp-server.test.ts": "providers",
"qoder-scaffold-guard.test.ts": "providers",
"quota-401-recovery-runtime.test.ts": "usage",
"quota-401-recovery.test.ts": "usage",
Expand Down Expand Up @@ -1612,7 +1611,7 @@
"responses-spill-orphan-sweep.test.ts": "responses",
"responses-spill-shutdown-clock.test.ts": "responses",
"responses-state-snapshot-select.test.ts": "responses",
"responses-state-snapshot-stubs.test.ts": "responses",
"responses-state-snapshot-stubs.test.ts": "responses", "responses-state-store-false.test.ts": "responses",
"responses-state-write-amplification.test.ts": "responses",
"responses-state.test.ts": "responses",
"responses-stateless-dangling-call-repair.test.ts": "responses",
Expand Down
27 changes: 2 additions & 25 deletions src/adapters/codebuddy/mcp-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,7 @@
*/

import { open } from "node:fs/promises";
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { serveCodingAgentMcpTools } from "../coding-agent/mcp-server";
import { CODEBUDDY_TOOL_LIMITS } from "./tool-bridge";

interface ToolDefinition {
Expand Down Expand Up @@ -156,25 +151,7 @@ async function loadTools(path: string): Promise<ToolDefinition[]> {

export async function runCodeBuddyMcpServer(catalogPath: string): Promise<void> {
if (!catalogPath) throw new Error("missing tool catalog");
// The pinned SDK does not detect stdin EOF itself. The capture server must exit when
// the parent terminates its CLI, including after a captured message_stop.
const exitOnStdinClose = (): void => process.exit(0);
process.stdin.on("end", exitOnStdinClose);
process.stdin.on("close", exitOnStdinClose);

const tools = await loadTools(catalogPath);
const advertisedNames = new Set(tools.map(tool => tool.name));
const server = new Server(
{ name: "opencodex-codebuddy-capture", version: "1.0.0" },
{ capabilities: { tools: {} } },
);
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
server.setRequestHandler(CallToolRequestSchema, async request => {
if (!advertisedNames.has(request.params.name)) throw new Error("unknown isolated tool");
// The external Codex client retains approval and execution ownership.
return await new Promise<never>(() => {});
});
await server.connect(new StdioServerTransport());
await serveCodingAgentMcpTools(await loadTools(catalogPath), "opencodex-codebuddy-capture");
}

if (import.meta.main) await runCodeBuddyMcpServer(process.argv[2] ?? "");
8 changes: 1 addition & 7 deletions src/adapters/codebuddy/tool-bridge.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createHash } from "node:crypto";
import {
namespacedToolName,
requiresToolCall,
toolChoiceToolPredicate,
type OcxParsedRequest,
type OcxTool,
Expand Down Expand Up @@ -475,13 +476,6 @@ function codeBuddyToolAliases(wireNames: readonly string[]): Map<string, string>
return aliases;
}

function requiresToolCall(choice: OcxToolChoice | undefined): boolean {
return choice === "required"
|| (typeof choice === "object" && choice !== null && (
"name" in choice || ("mode" in choice && choice.mode === "required")
));
}

function validateToolNamePart(value: unknown): value is string {
return typeof value === "string"
&& value.length > 0
Expand Down
33 changes: 33 additions & 0 deletions src/adapters/coding-agent/mcp-server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { readFile } from "node:fs/promises";
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";

export interface CodingAgentMcpTool {
name: string;
description: string;
inputSchema: Record<string, unknown>;
}

/** Advertise client tools without executing them: only Codex/client may execute a call. */
export async function serveCodingAgentMcpTools(tools: CodingAgentMcpTool[], serverName: string): Promise<void> {
const exitOnStdinClose = (): void => process.exit(0);
process.stdin.on("end", exitOnStdinClose);
process.stdin.on("close", exitOnStdinClose);

const names = new Set(tools.map(tool => tool.name));
const server = new Server({ name: serverName, version: "1.0.0" }, { capabilities: { tools: {} } });
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
server.setRequestHandler(CallToolRequestSchema, async request => {
if (!names.has(request.params.name)) throw new Error("unknown isolated tool");
return new Promise<never>(() => {});
});
await server.connect(new StdioServerTransport());
}

export async function runCodingAgentMcpServer(catalogPath: string): Promise<void> {
const tools = JSON.parse(await readFile(catalogPath, "utf8")) as CodingAgentMcpTool[];
await serveCodingAgentMcpTools(tools, "opencodex-coding-agent-tools");
}

if (import.meta.main) await runCodingAgentMcpServer(process.argv[2] ?? "");
Loading
Loading