@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 +10 -7
- package/package.json +2 -2
- package/src/__tests__/backfill.test.ts +19 -5
- package/src/__tests__/mcpHeaders.test.ts +11 -0
- package/src/backfill.mjs +6 -19
- package/src/mcpHeaders.mjs +12 -0
- package/src/proxy.mjs +2 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# @gemus/mcp-proxy (
|
|
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:
|
|
9
|
+
## Status: implemented
|
|
10
10
|
|
|
11
|
-
`src/proxy.mjs` is the
|
|
12
|
-
|
|
13
|
-
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
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('
|
|
99
|
-
expect(msg.result.instructions).toContain('
|
|
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: '
|
|
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({
|
|
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
|
-
//
|
|
38
|
-
//
|
|
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
|
|
40
|
+
'# Gemus companion',
|
|
43
41
|
'',
|
|
44
|
-
'
|
|
45
|
-
'
|
|
46
|
-
'
|
|
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:
|
|
248
|
+
requestInit: { headers: buildProxyUpstreamHeaders(KEY) },
|
|
248
249
|
})
|
|
249
250
|
const stdio = new StdioServerTransport()
|
|
250
251
|
|