Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.internal.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ This changelog documents internal development changes, refactors, tooling update

### Added
- Added Cursor harness `[agent=cursor]`, including offline F1 drives for stop/tool activity, resume continuation, and permission synchronization behavior. Also added project-level Cursor CLI permissions mapping from Cyrus tool permissions (including subroutine-time updates), pre-run MCP server enablement (`agent mcp list` + `agent mcp enable <server>`), switched the default Codex runner model to `gpt-5.3-codex`, and aligned edge-worker Vitest module resolution to use local `cyrus-claude-runner` sources during tests. ([CYPACK-804](https://linear.app/ceedar/issue/CYPACK-804), [#858](https://github.com/ceedaragents/cyrus/pull/858))
- Added Fastify MCP transport for `cyrus-tools` on the shared application server endpoint, replacing inline SDK-only wiring with HTTP MCP configuration and per-session context headers, and now enforcing `Authorization: Bearer <CYRUS_API_KEY>` on `/mcp/cyrus-tools` requests. Also fixed Codex MCP server config mapping so `headers` are translated to Codex `http_headers` (while preserving `http_headers`, `env_http_headers`, and `bearer_token_env_var`) for authenticated HTTP MCP initialization. Includes F1 validation covering `initialize` and `tools/list` on `/mcp/cyrus-tools`. ([CYPACK-817](https://linear.app/ceedar/issue/CYPACK-817), [#870](https://github.com/ceedaragents/cyrus/pull/870))

### Fixed
- Updated orchestrator system prompts to explicitly require `state: "To Do"` when creating issues via `mcp__linear__create_issue`, preventing issues from being created in "Triage" status. ([CYPACK-761](https://linear.app/ceedar/issue/CYPACK-761), [#815](https://github.com/ceedaragents/cyrus/pull/815))
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ All notable changes to this project will be documented in this file.
- **Codex tool activity is now visible in Linear sessions** - Codex runs now emit tool lifecycle activity (including command execution, file edits, web fetch/search, MCP tool calls, and todo updates) so activity streams show execution details instead of only final output. ([#850](https://github.com/ceedaragents/cyrus/pull/850))
- **Codex todo output now renders as proper checklists** - Todo items are now formatted as markdown task lists (`- [ ]` and `- [x]`) for correct checklist rendering in Linear. ([#850](https://github.com/ceedaragents/cyrus/pull/850))
- **Major new feature: Cursor agent harness support** - Cyrus now supports Cursor as a first-class agent option. To use it, set `[agent=cursor]` in the issue description or apply a `cursor` issue label; either selector runs end-to-end with the Cursor runner and posts the final response back to the issue thread. Cursor runs now map Cyrus tool permissions into project-level Cursor CLI permissions, pre-enable configured MCP servers before run, and refresh permissions between subroutines so permission changes take effect without restarting the issue flow. Cursor sandbox is enabled by default for tool execution isolation; set `CYRUS_SANDBOX=disabled` to disable. Before each run, Cyrus validates that the installed `cursor-agent` version matches the tested version; a mismatch posts an error to Linear. Set `CYRUS_CURSOR_AGENT_VERSION` to your installed version to override. Assembled cursor-agent CLI args are now logged to console and session log files for debugging. Codex default runner model is now `gpt-5.3-codex` (configurable via `codexDefaultModel`). ([CYPACK-804](https://linear.app/ceedar/issue/CYPACK-804), [#858](https://github.com/ceedaragents/cyrus/pull/858))
- **Cyrus MCP tools now run on the built-in server endpoint with authenticated Codex access** - Cyrus tools are now served via Fastify MCP on the same configured server port, cyrus-tools MCP requests require `Authorization: Bearer <CYRUS_API_KEY>`, and Codex now forwards configured MCP HTTP auth headers correctly so authenticated MCP servers initialize successfully. ([CYPACK-817](https://linear.app/ceedar/issue/CYPACK-817), [#870](https://github.com/ceedaragents/cyrus/pull/870))

## [0.2.21] - 2026-02-09

Expand Down
103 changes: 103 additions & 0 deletions apps/f1/test-drives/2026-02-18-cypack-817-fastify-mcp-validation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# Test Drive CYPACK-817: Fastify-MCP Cyrus Tools Validation

**Date**: 2026-02-18
**Goal**: Validate that cyrus-tools are served from the existing Fastify server (CYRUS_PORT/CYRUS_SERVER_PORT equivalent) via `fastify-mcp`.
**Test Repo**: `/tmp/f1-test-drive-cypack-817-20260217-185650`
**Server Port**: `3617`

## Verification Results

### Issue-Tracker
- [x] Issue created
- [x] Issue ID returned
- [x] Issue metadata accessible

### EdgeWorker
- [x] Session started
- [x] Worktree created (fallback path used after existing checked-out branch warning)
- [x] Activities tracked
- [x] Agent processed issue

### Renderer
- [x] Activity format correct (`thought`, `action`, `response` visible)
- [x] Pagination works (`--limit`, `--offset`)
- [x] Search works (`--search Bash`)

### MCP Endpoint (CYPACK-817 target)
- [x] `cyrus-tools` endpoint registered on same Fastify server: `/mcp/cyrus-tools`
- [x] MCP `initialize` succeeds with `mcp-session-id` returned
- [x] MCP `tools/list` succeeds and returns cyrus tool set

## Session Log

1. Create fresh F1 repo
```bash
cd apps/f1
./f1 init-test-repo --path /tmp/f1-test-drive-cypack-817-20260217-185650
```
Result: repo created with git init + initial commit.

2. Start F1 server
```bash
cd apps/f1
HOME=/tmp CYRUS_PORT=3617 CYRUS_REPO_PATH=/tmp/f1-test-drive-cypack-817-20260217-185650 node dist/server.js
```
Key output included:
- `✅ Cyrus tools MCP endpoint registered at /mcp/cyrus-tools`
- `RPC endpoint: /cli/rpc`
- `Shared application server listening on http://localhost:3617`

3. Health checks
```bash
CYRUS_PORT=3617 ./f1 ping
CYRUS_PORT=3617 ./f1 status
```
Result: healthy, `status: ready`.

4. Issue + session flow
```bash
CYRUS_PORT=3617 ./f1 create-issue --title "CYPACK-817 MCP fastify validation" --description "Validate cyrus-tools served from fastify-mcp endpoint on CYRUS_SERVER_PORT"
CYRUS_PORT=3617 ./f1 start-session --issue-id issue-1
CYRUS_PORT=3617 ./f1 view-session --session-id session-1 --limit 10 --offset 0
CYRUS_PORT=3617 ./f1 view-session --session-id session-1 --limit 5 --offset 0 --search Bash
```
Result:
- session created (`session-1`)
- activities present with expected types
- pagination/search behavior verified

5. Direct MCP validation on same Fastify server
```bash
curl -X POST http://127.0.0.1:3617/mcp/cyrus-tools \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'x-cyrus-mcp-context-id: f1-test-repo:session-1' \
--data '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"cypack817-test","version":"1.0.0"}}}'
```
Initialize response included:
- `mcp-session-id: 959cb3f1-b2b2-4597-88f9-e37bf20db049`
- `serverInfo.name: "cyrus-tools"`

Then:
```bash
curl -X POST http://127.0.0.1:3617/mcp/cyrus-tools \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'x-cyrus-mcp-context-id: f1-test-repo:session-1' \
-H 'mcp-session-id: 959cb3f1-b2b2-4597-88f9-e37bf20db049' \
--data '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'
```
Result: tool list returned (`linear_upload_file`, `linear_agent_session_create`, `linear_agent_give_feedback`, etc.).

6. Cleanup
```bash
CYRUS_PORT=3617 ./f1 stop-session --session-id session-1
# Ctrl+C on server process
```
Result: clean session stop + graceful server shutdown.

## Final Retrospective

- Fastify MCP wiring is working end-to-end on the same server port as RPC/status/version routes.
- `buildMcpConfig` now routes `cyrus-tools` through local HTTP MCP with context headers, and Claude connected successfully (`cyrus-tools MCP session connected` observed).
- One environment-specific issue occurred on an initial run (`EPERM` writing under `/Users/agentops/.claude/debug`); rerunning server with `HOME=/tmp` resolved this in the sandbox and did not affect MCP functionality.
4 changes: 0 additions & 4 deletions packages/claude-runner/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,6 @@ export {
ClaudeMessageFormatter,
type IMessageFormatter,
} from "./formatter.js";
export {
type CyrusToolsOptions,
createCyrusToolsServer,
} from "./tools/cyrus-tools/index.js";
export {
createImageToolsServer,
type ImageToolsOptions,
Expand Down
193 changes: 192 additions & 1 deletion packages/codex-runner/src/CodexRunner.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import crypto from "node:crypto";
import { EventEmitter } from "node:events";
import { mkdirSync } from "node:fs";
import { existsSync, mkdirSync, readFileSync } from "node:fs";
import { homedir } from "node:os";
import { join, relative as pathRelative } from "node:path";
import { cwd } from "node:process";
Expand All @@ -19,6 +19,7 @@ import { Codex } from "@openai/codex-sdk";
import type {
IAgentRunner,
IMessageFormatter,
McpServerConfig,
SDKAssistantMessage,
SDKMessage,
SDKResultMessage,
Expand All @@ -27,6 +28,7 @@ import type {
import { CodexMessageFormatter } from "./formatter.js";
import type {
CodexConfigOverrides,
CodexConfigValue,
CodexJsonEvent,
CodexRunnerConfig,
CodexRunnerEvents,
Expand Down Expand Up @@ -55,6 +57,7 @@ interface ToolProjection {
}

const DEFAULT_CODEX_MODEL = "gpt-5.3-codex";
const CODEX_MCP_DOCS_URL = "https://platform.openai.com/docs/docs-mcp";

function toFiniteNumber(value: number | undefined): number {
return typeof value === "number" && Number.isFinite(value) ? value : 0;
Expand Down Expand Up @@ -304,6 +307,66 @@ function normalizeMcpIdentifier(value: string): string {
return normalized || "unknown";
}

function autoDetectMcpConfigPath(
workingDirectory?: string,
): string | undefined {
if (!workingDirectory) {
return undefined;
}

const mcpPath = join(workingDirectory, ".mcp.json");
if (!existsSync(mcpPath)) {
return undefined;
}

try {
JSON.parse(readFileSync(mcpPath, "utf8"));
return mcpPath;
} catch {
console.warn(
`[CodexRunner] Found .mcp.json at ${mcpPath} but it is invalid JSON, skipping`,
);
return undefined;
}
}

function loadMcpConfigFromPaths(
configPaths: string | string[] | undefined,
): Record<string, McpServerConfig> {
if (!configPaths) {
return {};
}

const paths = Array.isArray(configPaths) ? configPaths : [configPaths];
let mcpServers: Record<string, McpServerConfig> = {};

for (const configPath of paths) {
try {
const mcpConfigContent = readFileSync(configPath, "utf8");
const mcpConfig = JSON.parse(mcpConfigContent);
const servers =
mcpConfig &&
typeof mcpConfig === "object" &&
!Array.isArray(mcpConfig) &&
mcpConfig.mcpServers &&
typeof mcpConfig.mcpServers === "object" &&
!Array.isArray(mcpConfig.mcpServers)
? (mcpConfig.mcpServers as Record<string, McpServerConfig>)
: {};
mcpServers = { ...mcpServers, ...servers };
console.log(
`[CodexRunner] Loaded MCP config from ${configPath}: ${Object.keys(servers).join(", ")}`,
);
} catch (error) {
console.warn(
`[CodexRunner] Failed to load MCP config from ${configPath}: ${error instanceof Error ? error.message : String(error)}`,
);
}
}

return mcpServers;
}

export declare interface CodexRunner {
on<K extends keyof CodexRunnerEvents>(
event: K,
Expand Down Expand Up @@ -492,11 +555,139 @@ export class CodexRunner extends EventEmitter implements IAgentRunner {
return env;
}

private buildCodexMcpServersConfig():
| Record<string, CodexConfigOverrides>
| undefined {
const autoDetectedPath = autoDetectMcpConfigPath(
this.config.workingDirectory,
);
const configPaths = autoDetectedPath
? [autoDetectedPath]
: ([] as string[]);
if (this.config.mcpConfigPath) {
const explicitPaths = Array.isArray(this.config.mcpConfigPath)
? this.config.mcpConfigPath
: [this.config.mcpConfigPath];
configPaths.push(...explicitPaths);
}

const fileBasedServers = loadMcpConfigFromPaths(configPaths);
const mergedServers = this.config.mcpConfig
? { ...fileBasedServers, ...this.config.mcpConfig }
: fileBasedServers;
if (Object.keys(mergedServers).length === 0) {
return undefined;
}

// Codex MCP configuration reference:
// https://platform.openai.com/docs/docs-mcp
const codexServers: Record<string, CodexConfigOverrides> = {};
for (const [serverName, rawConfig] of Object.entries(mergedServers)) {
const configAny = rawConfig as Record<string, unknown>;
if (
typeof configAny.listTools === "function" ||
typeof configAny.callTool === "function"
) {
console.warn(
`[CodexRunner] Skipping MCP server '${serverName}' because in-process SDK server instances cannot be mapped to codex config`,
);
continue;
}

const mapped: CodexConfigOverrides = {};
if (typeof configAny.command === "string") {
mapped.command = configAny.command;
}
if (Array.isArray(configAny.args)) {
mapped.args =
configAny.args as unknown as CodexConfigOverrides[keyof CodexConfigOverrides];
}
if (
configAny.env &&
typeof configAny.env === "object" &&
!Array.isArray(configAny.env)
) {
mapped.env =
configAny.env as unknown as CodexConfigOverrides[keyof CodexConfigOverrides];
}
if (typeof configAny.cwd === "string") {
mapped.cwd = configAny.cwd;
}
if (typeof configAny.url === "string") {
mapped.url = configAny.url;
}
if (
configAny.http_headers &&
typeof configAny.http_headers === "object" &&
!Array.isArray(configAny.http_headers)
) {
mapped.http_headers =
configAny.http_headers as unknown as CodexConfigOverrides[keyof CodexConfigOverrides];
}
if (
configAny.headers &&
typeof configAny.headers === "object" &&
!Array.isArray(configAny.headers)
) {
mapped.http_headers =
configAny.headers as unknown as CodexConfigOverrides[keyof CodexConfigOverrides];
Comment on lines +627 to +633
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve explicit http_headers when generic headers exist

If a server config contains both Codex-native http_headers and generic headers, the second assignment overwrites http_headers with headers, so explicitly configured Codex auth headers are silently dropped. This breaks authenticated MCP setups that intentionally provide Codex-specific headers (for example via http_headers/env_http_headers) alongside generic metadata, and contradicts the intended pass-through behavior.

Useful? React with 👍 / 👎.

}
if (
configAny.env_http_headers &&
typeof configAny.env_http_headers === "object" &&
!Array.isArray(configAny.env_http_headers)
) {
mapped.env_http_headers =
configAny.env_http_headers as unknown as CodexConfigOverrides[keyof CodexConfigOverrides];
}
if (typeof configAny.bearer_token_env_var === "string") {
mapped.bearer_token_env_var = configAny.bearer_token_env_var;
}
if (typeof configAny.timeout === "number") {
mapped.timeout = configAny.timeout;
}

if (!mapped.command && !mapped.url) {
console.warn(
`[CodexRunner] Skipping MCP server '${serverName}' because it has no command/url transport`,
);
continue;
}

codexServers[serverName] = mapped;
}

if (Object.keys(codexServers).length === 0) {
return undefined;
}

console.log(
`[CodexRunner] Configured ${Object.keys(codexServers).length} MCP server(s) for codex config (docs: ${CODEX_MCP_DOCS_URL})`,
);
return codexServers;
}

private buildConfigOverrides(): CodexConfigOverrides | undefined {
const appendSystemPrompt = (this.config.appendSystemPrompt ?? "").trim();
const configOverrides = this.config.configOverrides
? { ...this.config.configOverrides }
: {};
const mcpServers = this.buildCodexMcpServersConfig();
if (mcpServers) {
const existingMcpServers = configOverrides.mcp_servers;
if (
existingMcpServers &&
typeof existingMcpServers === "object" &&
!Array.isArray(existingMcpServers)
) {
configOverrides.mcp_servers = {
...(existingMcpServers as Record<string, CodexConfigValue>),
...mcpServers,
};
} else {
configOverrides.mcp_servers = mcpServers;
}
}

const sandboxWorkspaceWrite = configOverrides.sandbox_workspace_write;
// Keep workspace-write as the default sandbox, but enable outbound network so
Expand Down
Loading
Loading