@gemus/mcp-proxy 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # @gemus/mcp-proxy (Issue #1751, P1 — WIP)
1
+ # @gemus/mcp-proxy (Issues #1751, #1756, #1917)
2
2
 
3
3
  Local stdio↔HTTP MCP proxy that gives **Codex desktop (direct-connect / Mode B)** users canvas
4
4
  image write-back. Bundled into the Codex plugin (`.codex-plugin/`), declared as a **command-based**
@@ -6,11 +6,13 @@ MCP server so codex spawns it locally; the proxy owns upstream auth to the remot
6
6
  (Codex does no OAuth for stdio servers). Full design: `docs/plans/1751-codex-desktop-companion.md`
7
7
  and `docs/architecture/agent architecture/codex-desktop-companion.md`.
8
8
 
9
- ## Status: minimal skeleton, behavioral checkpoint PASSED
9
+ ## Status: implemented
10
10
 
11
- `src/proxy.mjs` is the validated passthrough+tap skeleton. The backfill delivery layer (Stop-hook
12
- endpoint read rollout via `transcript_path` `ExecuteClaimTracker` → key-only `deliverImageToNode`)
13
- is the next build increment (plan Steps 2–4).
11
+ `src/proxy.mjs` is the production passthrough, execute-tap, and deterministic image-backfill
12
+ companion. A trusted Stop hook triggers `generated_images`/rollout discovery; `ExecuteClaimTracker`
13
+ then delivers an unambiguous image to its planned node or conservatively creates an orphan
14
+ `image-upload`. MCP initialization advertises the shared Codex capability contract used by the
15
+ server blueprint policy.
14
16
 
15
17
  ## What was validated end-to-end (2026-07-06, codex-cli 0.142.2 → local dev `/api/mcp`)
16
18
 
@@ -23,7 +25,8 @@ is the next build increment (plan Steps 2–4).
23
25
  `workflowId` from `arguments`, and **turn/thread identity from `_meta.x-codex-turn-metadata`
24
26
  (`thread_id`/`turn_id`)** — so per-turn scoping needs no Stop hook. `execute` is the same path.
25
27
  - **(c) turn-end + retrieval** — `Stop` hook fires post-flush carrying `transcript_path` → exact
26
- rollout; imagegen writes `generated_images/<session>/ig_<call_id>.png` + rollout `saved_path`+base64.
28
+ rollout; imagegen writes `generated_images/<session>/<sanitized-call-id>.png` (historically `ig_*`,
29
+ currently `call_*`) + rollout `saved_path`+base64.
27
30
  - **Session cap** — `KEY_ONLY_SESSION_CAP=3`/user is real; the proxy MUST `DELETE` its upstream
28
31
  session on close (implemented in `shutdown()`) to avoid leaking slots.
29
32
 
@@ -53,4 +56,4 @@ GEMUS_KEY=mak_... GEMUS_URL=http://localhost:3000/api/mcp PROXY_LOG=/tmp/proxy.l
53
56
  ```
54
57
 
55
58
  Env: `GEMUS_KEY` (required), `GEMUS_URL` (default `https://gemus.ai/api/mcp`), `PROXY_LOG` (optional
56
- debug log), `PROXY_BACKFILL_IDLE_MS` (default 30000 — turn-idle fallback before salvage-orphan).
59
+ debug log), `PROXY_BACKFILL_IDLE_MS` (default 180000 — turn-idle fallback before salvage-orphan).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gemus/mcp-proxy",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Local stdio<->HTTP MCP proxy for Codex desktop: transparent passthrough + execute tap + imagegen backfill (Issue #1751, P1).",
5
5
  "type": "module",
6
6
  "bin": {
@@ -25,7 +25,7 @@
25
25
  },
26
26
  "dependencies": {
27
27
  "@modelcontextprotocol/sdk": "^1.29.0",
28
- "@gemus/codex-backfill-core": "0.1.1"
28
+ "@gemus/codex-backfill-core": "0.1.2"
29
29
  },
30
30
  "engines": {
31
31
  "node": ">=18"
@@ -92,14 +92,25 @@ describe('normalizeToolName (tap → tracker)', () => {
92
92
  })
93
93
 
94
94
  describe('injectSteering (Mode-B initialize instructions)', () => {
95
- it('appends companion steering to the initialize response instructions', () => {
95
+ it('appends a search-neutral skill bootstrap to initialize instructions', () => {
96
96
  const msg = { jsonrpc: '2.0', id: 1, result: { protocolVersion: '2024-11-05', serverInfo: { name: 'gemus' } } }
97
97
  expect(injectSteering(msg)).toBe(true)
98
- expect(msg.result.instructions).toContain('auto-backfilled')
99
- expect(msg.result.instructions).toContain('NEVER') // don't self-forward
98
+ expect(msg.result.instructions).toContain('gemus-getting-started')
99
+ expect(msg.result.instructions).toContain('gemus-codex-imagen')
100
100
  expect(msg.result.instructions).toBe(COMPANION_STEERING)
101
101
  })
102
102
 
103
+ it('does not pollute deferred search documents with registered tool names', () => {
104
+ const toolNames = [
105
+ 'canvas_read', 'view_image', 'node_list', 'workflow_list', 'canvas_edit', 'ppt_edit',
106
+ 'open_canvas', 'blueprint', 'execute', 'batch_execute', 'recall_history',
107
+ ]
108
+ for (const toolName of toolNames) {
109
+ const exactName = new RegExp(`(?<![A-Za-z0-9_])${toolName}(?![A-Za-z0-9_])`, 'i')
110
+ expect(COMPANION_STEERING, `companion steering references ${toolName}`).not.toMatch(exactName)
111
+ }
112
+ })
113
+
103
114
  it('preserves any existing server instructions (appends, not replaces)', () => {
104
115
  const msg = { id: 1, result: { protocolVersion: '1', instructions: 'server says hi' } }
105
116
  injectSteering(msg)
@@ -304,7 +315,7 @@ describe('backfillTurn — Layer 2 file byte source (#1756)', () => {
304
315
  // generated_images available with one fresh image → file source wins, rollout untouched.
305
316
  mocks.readGeneratedImageFiles.mockReturnValue({
306
317
  available: true,
307
- images: [{ callId: 'ig_a', base64: 'iVBORw0KGgoAAAA1', mimeType: 'image/png' }],
318
+ images: [{ callId: 'call_current', base64: 'iVBORw0KGgoAAAA1', mimeType: 'image/png' }],
308
319
  })
309
320
 
310
321
  const r = await bf.backfillTurn({ turnKey: 't1', sessionId: 's1', rolloutPath: '/x', salvageOnly: false })
@@ -312,7 +323,10 @@ describe('backfillTurn — Layer 2 file byte source (#1756)', () => {
312
323
  expect(mocks.readGeneratedImageFiles).toHaveBeenCalled()
313
324
  expect(mocks.readRollout).not.toHaveBeenCalled() // file source is authoritative when available
314
325
  expect(r.delivered).toBe(1)
315
- expect(mocks.deliverImageToNode.mock.calls[0][0]).toMatchObject({ nodeId: 'nodeA', image: { callId: 'ig_a' } })
326
+ expect(mocks.deliverImageToNode.mock.calls[0][0]).toMatchObject({
327
+ nodeId: 'nodeA',
328
+ image: { callId: 'call_current' },
329
+ })
316
330
  })
317
331
 
318
332
  it('available but empty → no delivery, and does NOT fall back to rollout (mtime guard preserved)', async () => {
@@ -0,0 +1,11 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { buildProxyUpstreamHeaders } from '../mcpHeaders.mjs'
3
+
4
+ describe('buildProxyUpstreamHeaders', () => {
5
+ it('advertises the canonical Codex MCP capabilities alongside bearer authentication', () => {
6
+ expect(buildProxyUpstreamHeaders('mak_test123')).toEqual({
7
+ Authorization: 'Bearer mak_test123',
8
+ 'X-Gemus-Client-Capabilities': 'agent-authored-prompts,codex-imagen',
9
+ })
10
+ })
11
+ })
package/src/backfill.mjs CHANGED
@@ -34,27 +34,14 @@ export function normalizeToolName(name) {
34
34
  return WIRE_TO_NORMALIZED[name] ?? name
35
35
  }
36
36
 
37
- // Companion steering (Mode B has no bridge to inject CLIENT_DELIVERED_GUIDANCE into the system prompt).
38
- // This proxy IS the backfiller, so it tells codex via the MCP initialize `instructions` field — not to
39
- // self-forward imagegen output. Mirrors src/lib/agent/core/prompt/workflowBehavior.ts (condensed). Without
40
- // it codex hunts the file / uploads it to a public host to self-deliver (#1751 real-machine observation).
37
+ // Keep initialize instructions search-neutral. Codex appends this namespace text to every deferred
38
+ // tool's BM25 document, so operational rules and exact tool names live in installed plugin skills.
41
39
  export const COMPANION_STEERING = [
42
- '# Gemus companion — codex-imagen is auto-backfilled',
40
+ '# Gemus companion',
43
41
  '',
44
- 'A local Gemus companion backfills your own image generations onto the planned canvas nodes at turn',
45
- 'end. For a `codex-imagen` (gen-image) node: (1) create the node with config.model="codex-imagen",',
46
- '(2) call `execute` with dryRun:true and read the returned resolvedPrompt, (3) generate the image',
47
- 'LOCALLY with your built-in imagegen, then (4) STOP.',
48
- '',
49
- 'NEVER locate, read, host, upload, or submit the imagegen file yourself — you cannot access it and it',
50
- 'is delivered for you. NEVER pass `content` for a codex-imagen node, and NEVER spin up a local HTTP',
51
- 'server or upload the image to a public host to "deliver" it. A codex-imagen node showing no output /',
52
- 'clientDeliveredStatus:"awaiting_backfill" DURING your turn is ON TRACK, not a failure — do not re-read',
53
- 'it in a loop, do not switch its model to a platform provider (gemini/etc), do not self-forward. Just',
54
- 'end your turn; the image arrives on the node after it.',
55
- '',
56
- '(Text is the exception: codex-text has no backfill — self-forward with execute dryRun:false +',
57
- 'content:{text:"..."}.)',
42
+ 'Before using Gemus, load and follow the installed `gemus-getting-started` skill.',
43
+ 'For local image generation and turn-end backfill, also load `gemus-codex-imagen`.',
44
+ 'These skills are the authoritative operational guidance for this direct MCP connection.',
58
45
  ].join('\n')
59
46
 
60
47
  /**
@@ -0,0 +1,12 @@
1
+ import {
2
+ CLIENT_CAPABILITIES,
3
+ CLIENT_CAPABILITIES_HEADER,
4
+ serializeClientCapabilities,
5
+ } from '@gemus/codex-backfill-core'
6
+
7
+ export function buildProxyUpstreamHeaders(apiKey) {
8
+ return {
9
+ Authorization: `Bearer ${apiKey}`,
10
+ [CLIENT_CAPABILITIES_HEADER]: serializeClientCapabilities(CLIENT_CAPABILITIES),
11
+ }
12
+ }
package/src/proxy.mjs CHANGED
@@ -25,6 +25,7 @@ import fs from 'node:fs'
25
25
  import { randomBytes } from 'node:crypto'
26
26
  import { createBackfiller, injectSteering } from './backfill.mjs'
27
27
  import { unwrapMcpContent } from '@gemus/codex-backfill-core'
28
+ import { buildProxyUpstreamHeaders } from './mcpHeaders.mjs'
28
29
 
29
30
  const KEY = process.env.GEMUS_KEY || ''
30
31
  const GEMUS_URL = process.env.GEMUS_URL || 'https://gemus.ai/api/mcp'
@@ -244,7 +245,7 @@ if (typeof hookServer.unref === 'function') hookServer.unref()
244
245
 
245
246
  // ── transparent JSON-RPC passthrough (raw send/onmessage) ──────────────────────────────────────
246
247
  const upstream = new StreamableHTTPClientTransport(new URL(GEMUS_URL), {
247
- requestInit: { headers: { Authorization: `Bearer ${KEY}` } },
248
+ requestInit: { headers: buildProxyUpstreamHeaders(KEY) },
248
249
  })
249
250
  const stdio = new StdioServerTransport()
250
251