release: v0.2.3#7
Conversation
…and clipboard utility Introduce three major feature systems that extend OrbCode beyond its native tool set, along with supporting utilities and UI. ### MCP (Model Context Protocol) Server Support - New `src/mcp/` module: manager, client, config, auth, and types for connecting to external tools via the Model Context Protocol. - New `src/commands/mcp.ts`: CLI subcommand (`orbcode mcp add/remove/list`) for managing MCP servers from the command line. - Three configuration scopes: user (~/.orbcode/settings.json), project (.mcp.json), and local (.orbcode/settings.json) with layered precedence. - Supports stdio, HTTP Streamable, and SSE transports. - OAuth 2.0 authentication flow (PKCE, RFC 9728, RFC 8414) with token persistence and automatic refresh. Also supports M2M grants (client_credentials, private_key_jwt). - Interactive MCP management in the TUI: `/mcp` command opens a picker for enabling/disabling/reconnecting/authenticating servers. - Startup approval prompt for project-scope servers (persisted to .orbcode/settings.json). - MCP tools are surfaced as `mcp__<server>__<tool>` in the model tool list and routed through the existing executor pipeline. ### Skills System - New `src/skills/` module: loader and types for reusable instruction sets stored in ~/.orbcode/skills/ and .orbcode/skills/. - New `src/tools/executors/skills.ts`: executor for the use_skill tool. - Skills catalog is injected into the system prompt so the model knows which skills are available and when to use them. - use_skill tool schema updated with richer description linking to the system prompt catalog. ### AGENTS.md Memory - New `src/memory/` module: loader and types for reading AGENTS.md files from user home, project root, and parent directories. - Memory content (project and user instructions) is injected into the system prompt as a "Project & User Instructions (AGENTS.md)" section that the model treats as authoritative. ### Supporting Changes - New `src/utils/clipboard.ts`: clipboard utility for headless modes. - MCP manager lifecycle integrated into Agent (teardown on SessionEnd), headless mode, and the TUI App component. - getActiveTools() now accepts an optional McpManager to include MCP tools alongside native tools. - executeTool() routes mcp__-prefixed tool calls through the McpManager. - describeToolCall() handles MCP tool names for UI logging. - Settings schema extended with mcpServers, enabledMcpServers, disabledMcpServers, and saveMcpApproval() helper. - System prompt refactored to accept optional memory and skills data for section injection.
…AGENTS.md - /cost slash command renamed to /usage; fetches plan usage from /axoncode/profile (plan name + 5hr/weekly/monthly windows with % used and reset time) instead of session cost + account balance. Session cost remains in the status bar and /status. - orbcode mcp add no longer swallows a -- separator immediately after the server name, matching claude mcp add <name> -- <command>. A later -- is still passed through as a literal arg. - Drop the stale .orbital/AGENTS.md reference. - Ignore .orbcode/AGENTS.md so per-developer project memory doesn't get committed.
There was a problem hiding this comment.
🧪 PR Review is completed: Substantial feature PR adding MCP support, skills, and AGENTS.md memory. Found a critical regex bug breaking MCP tool routing for servers with underscores, a race condition in OAuth port allocation, missing error handling on the callback server, and an unsafe type assertion when parsing MCP tool results.
Reviewed src/commands/mcp.ts: no issues found.
Reviewed src/config/settings.ts: no issues found.
Reviewed src/core/agent.ts: no issues found.
Reviewed src/headless.ts: no issues found.
Reviewed src/index.tsx: no issues found.
Reviewed src/mcp/config.ts: no issues found.
Reviewed src/mcp/types.ts: no issues found.
Reviewed src/memory/loader.ts: no issues found.
Reviewed src/prompts/system.ts: no issues found.
Reviewed src/skills/loader.ts: no issues found.
Reviewed src/ui/App.tsx: no issues found.
Reviewed src/ui/components/McpApprovalPrompt.tsx: no issues found.
Reviewed src/ui/components/McpAuthScreen.tsx: no issues found.
Reviewed src/ui/components/McpPicker.tsx: no issues found.
Reviewed src/ui/components/StatusBar.tsx: no issues found.
Reviewed src/utils/clipboard.ts: no issues found.
Skipped files
.orbital/AGENTS.md: Skipped file patternCHANGELOG.md: Skipped file patternREADME.md: Skipped file patternpackage-lock.json: Skipped file pattern
⬇️ Low Priority Suggestions (2)
src/mcp/auth.ts (1 suggestion)
Location:
src/mcp/auth.ts(Lines 217-220)🟠 Error Handling
Issue:
startCallbackServercreates an HTTP server and callslisten()without attaching an'error'event handler. If the callback port is already in use (likely due to theephemeralPortfallback always returning 8765), the unhandled'error'event will crash the Node.js process.Fix: Attach a no-op error handler to prevent the process from crashing on
EADDRINUSEor other listen errors.Impact: Prevents fatal crashes during OAuth authentication
- }) - server.listen(port, "127.0.0.1") - return server - } + }) + server.on("error", () => {}) + server.listen(port, "127.0.0.1") + return server
src/mcp/client.ts (1 suggestion)
Location:
src/mcp/client.ts(Lines 123-139)🟡 Type Safety
Issue:
callMcpTooluses(result.content ?? []) as Array<Record<string, unknown>>to cast the MCP tool result content. If a misbehaving server returns a non-array value, the subsequentfor...ofloop will throw or behave unpredictably.Fix: Validate that
result.contentis an array before iterating, and guard each block with an object type check.Impact: Hardens the client against malformed MCP server responses
- const content = (result.content ?? []) as Array<Record<string, unknown>> - const parts: string[] = [] - for (const block of content) { - const type = block.type as string - if (type === "text" && typeof block.text === "string") { - parts.push(block.text) - } else if (type === "resource") { - const resource = block.resource as Record<string, unknown> | undefined - if (resource && typeof resource.text === "string") parts.push(resource.text) - } else if (type === "image") { - parts.push(`[image: ${block.mimeType}]`) - } else if (type === "audio") { - parts.push(`[audio: ${block.mimeType}]`) - } else if (type === "resource_link" && typeof block.uri === "string") { - parts.push(`[resource: ${block.uri}]`) - } - } + const content = Array.isArray(result.content) ? result.content : [] + const parts: string[] = [] + for (const block of content) { + if (!block || typeof block !== "object") continue + const b = block as Record<string, unknown> + const type = b.type as string + if (type === "text" && typeof b.text === "string") { + parts.push(b.text) + } else if (type === "resource") { + const resource = b.resource as Record<string, unknown> | undefined + if (resource && typeof resource.text === "string") parts.push(resource.text) + } else if (type === "image") { + parts.push(`[image: ${b.mimeType}]`) + } else if (type === "audio") { + parts.push(`[audio: ${b.mimeType}]`) + } else if (type === "resource_link" && typeof b.uri === "string") { + parts.push(`[resource: ${b.uri}]`) + } + }
| hasTool(toolName: string): boolean { | ||
| return /^mcp__[^_]+__/.test(toolName) | ||
| } |
There was a problem hiding this comment.
🔴 Logic Error
Issue: The hasTool regex /^mcp__[^_]+__/ fails to match valid MCP tool names when the server name contains an underscore. Server names are validated with /^[A-Za-z0-9_-]+$/, so underscores are allowed. For a server named my_server, the tool name mcp__my_server__tool will not be recognized as an MCP tool, causing the tool router to fall through to native tools and return "not available".
Fix: Update the character class to explicitly match the allowed server name characters.
Impact: Restores MCP tool routing for all valid server names
| hasTool(toolName: string): boolean { | |
| return /^mcp__[^_]+__/.test(toolName) | |
| } | |
| hasTool(toolName: string): boolean { | |
| return /^mcp__([A-Za-z0-9_-]+)__/.test(toolName) | |
| } |
|
|
||
| /** Route an `mcp__<server>__<tool>` call to the right connection. */ | ||
| async callTool(toolName: string, args: Record<string, unknown>): Promise<{ text: string; isError?: boolean }> { | ||
| const match = /^mcp__([^_]+)__(.+)$/.exec(toolName) |
There was a problem hiding this comment.
🔴 Logic Error
Issue: The callTool regex /^mcp__([^_]+)__(.+)$/ fails for server names containing underscores (which are valid per /^[A-Za-z0-9_-]+$/). For mcp__my_server__tool, the regex match produces my as the server name and server__tool as the original tool name, causing the lookup this.connections.get(server) to miss the connection and return an error.
Fix: Use a character class that matches the full server name, identical to the hasTool fix.
Impact: Fixes broken MCP tool execution for servers with underscores in their names
| const match = /^mcp__([^_]+)__(.+)$/.exec(toolName) | |
| const match = /^mcp__([A-Za-z0-9_-]+)__(.+)$/.exec(toolName) |
| default: | ||
| // MCP tools (mcp__<server>__<tool>) — show the server + tool name. | ||
| if (/^mcp__/.test(toolName)) { | ||
| const match = /^mcp__([^_]+)__(.+)$/.exec(toolName) |
There was a problem hiding this comment.
🟡 Code Quality
Issue: The summarizeToolCall regex /^mcp__([^_]+)__(.+)$/ has the same underscore-matching bug as the manager's callTool and hasTool methods. While this only affects display text (falling back to the raw tool name), it should be consistent with the rest of the codebase.
Fix: Align the regex with the corrected server name character class.
Impact: Consistent MCP tool name formatting in the TUI
| const match = /^mcp__([^_]+)__(.+)$/.exec(toolName) | |
| const match = /^mcp__([A-Za-z0-9_-]+)__(.+)$/.exec(toolName) |
| function ephemeralPort(): number { | ||
| // Bind to port 0 and read back the assigned port, then close immediately. | ||
| const probe = http.createServer().listen(0, "127.0.0.1") | ||
| const addr = probe.address() | ||
| probe.close() | ||
| return typeof addr === "object" && addr ? addr.port : 8765 | ||
| } |
There was a problem hiding this comment.
🟠 Performance / Reliability
Issue: ephemeralPort() calls probe.address() synchronously after listen(0). Because TCP port binding is asynchronous, address() returns null in most executions, forcing a fallback to the hardcoded port 8765. Multiple concurrent OAuth flows (or a stale process) will collide on this fixed port and throw EADDRINUSE.
Fix: Replace the broken synchronous probe with a random port selection in the IANA ephemeral range.
Impact: Eliminates port collisions and makes OAuth callback allocation reliable
| function ephemeralPort(): number { | |
| // Bind to port 0 and read back the assigned port, then close immediately. | |
| const probe = http.createServer().listen(0, "127.0.0.1") | |
| const addr = probe.address() | |
| probe.close() | |
| return typeof addr === "object" && addr ? addr.port : 8765 | |
| } | |
| function ephemeralPort(): number { | |
| // Pick a random port in the IANA ephemeral range to avoid collisions. | |
| return 49152 + Math.floor(Math.random() * 16384) | |
| } |
release: v0.2.3
Fixed
orbcode mcp addno longer swallows a--separator immediately after the server name.orbcode mcp add --scope user context7 -- npx -y @upstash/context7-mcp …previously wrotecommand: "--", so the server failed to spawn. A--directly after the server name is now consumed as the flag/command separator, matching Claude Code'sclaude mcp add <name> -- <command>. A later--is still passed through as a literal argument to the server command.Changed
/costslash command renamed to/usage. It now fetches and prints your plan usage from/axoncode/profile— the plan name (uppercased) and, for tiered accounts, the 5-hour / weekly / monthly windows with percentage used and reset time (or the credits reset date for non-tiered accounts) — instead of showing the session cost and fetching the account balance. Session cost remains in the status bar and/status. The usage block no longer prints the legacyusagePercentageandremainingReviewslines.Chore
.orbital/AGENTS.mdreference; ignore per-developer.orbcode/AGENTS.mdso local project memory isn't accidentally committed.Post-merge
After this PR is merged into
main, push the tag to publish:```bash
git checkout main && git pull
git tag v0.2.3
git push origin v0.2.3
```