@gemus/mcp-proxy 0.1.8 → 0.1.10

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.
@@ -1,136 +0,0 @@
1
- function moduleUrl(source) {
2
- return `data:text/javascript,${encodeURIComponent(source)}`
3
- }
4
-
5
- const markerPrelude = `
6
- import fs from 'node:fs'
7
- function mark(name) {
8
- const file = process.env.PROXY_TEST_CONSTRUCTION_LOG
9
- if (file) fs.appendFileSync(file, name + '\\n')
10
- }
11
- `
12
-
13
- const modules = new Map([
14
- ['@gemus/codex-backfill-core', `
15
- export const CLIENT_CAPABILITIES = []
16
- export const CLIENT_CAPABILITIES_HEADER = 'X-Gemus-Client-Capabilities'
17
- export function serializeClientCapabilities(value) { return JSON.stringify(value) }
18
- export function unwrapMcpContent(value) { return value }
19
- `],
20
- ['@modelcontextprotocol/sdk/server/stdio.js', `
21
- ${markerPrelude}
22
- export class StdioServerTransport {
23
- constructor() { mark('stdio-transport') }
24
- async start() {
25
- if (process.env.PROXY_TEST_STDIO_START_FAILURE === '1') {
26
- this.onclose?.()
27
- throw new Error(
28
- 'stdio failed Authorization: Bearer '
29
- + process.env.GEMUS_KEY
30
- + ' at '
31
- + process.env.GEMUS_URL
32
- )
33
- }
34
- if (process.env.PROXY_TEST_FORWARD_STDIN === '1') {
35
- let input = ''
36
- process.stdin.setEncoding('utf8')
37
- process.stdin.on('data', (chunk) => {
38
- input += chunk
39
- for (;;) {
40
- const newline = input.indexOf('\\n')
41
- if (newline < 0) break
42
- const line = input.slice(0, newline).trim()
43
- input = input.slice(newline + 1)
44
- if (line) this.onmessage?.(JSON.parse(line))
45
- }
46
- })
47
- }
48
- process.stdin.resume()
49
- process.stdin.once('end', () => this.onclose?.())
50
- }
51
- async send() {}
52
- }
53
- `],
54
- ['@modelcontextprotocol/sdk/client/streamableHttp.js', `
55
- ${markerPrelude}
56
- export class StreamableHTTPClientTransport {
57
- constructor() { mark('upstream-transport') }
58
- async start() {}
59
- async send(message) {
60
- if (
61
- process.env.PROXY_TEST_UPSTREAM_INITIALIZE_FAILURE === '1'
62
- && message?.method === 'initialize'
63
- ) {
64
- throw new Error(
65
- 'initialize fetch failed Authorization: Bearer '
66
- + process.env.GEMUS_KEY
67
- + ' at '
68
- + process.env.GEMUS_URL
69
- )
70
- }
71
- }
72
- async terminateSession() {}
73
- async close() {}
74
- }
75
- `],
76
- ['node:http', `
77
- ${markerPrelude}
78
- function hookError() {
79
- return new Error(
80
- 'hook listen failed Authorization: Bearer '
81
- + process.env.GEMUS_KEY
82
- + ' at '
83
- + process.env.GEMUS_URL
84
- )
85
- }
86
- const http = {
87
- createServer() {
88
- mark('hook-server')
89
- const listeners = new Map()
90
- return {
91
- on(event, handler) { listeners.set(event, handler); return this },
92
- listen(_port, _host, callback) {
93
- if (process.env.PROXY_TEST_HOOK_LISTEN_FAILURE === '1') {
94
- queueMicrotask(() => listeners.get('error')?.(hookError()))
95
- } else {
96
- queueMicrotask(callback)
97
- }
98
- return this
99
- },
100
- address() { return { port: 43114 } },
101
- unref() {},
102
- close() {
103
- mark('hook-server-close')
104
- if (process.env.PROXY_TEST_HOOK_ERROR_AFTER_CLOSE === '1') {
105
- queueMicrotask(() => listeners.get('error')?.(hookError()))
106
- }
107
- },
108
- }
109
- },
110
- }
111
- export default http
112
- `],
113
- ['./backfill.mjs', `
114
- ${markerPrelude}
115
- export function createBackfiller() {
116
- mark('backfiller')
117
- return {
118
- observeCall() {},
119
- observeResult() {},
120
- async backfillTurn() {},
121
- }
122
- }
123
- export function injectSteering() { return false }
124
- `],
125
- ])
126
-
127
- export async function resolve(specifier, context, nextResolve) {
128
- const source = modules.get(specifier)
129
- if (
130
- source !== undefined
131
- && (specifier !== './backfill.mjs' || context.parentURL?.endsWith('/proxy.mjs'))
132
- ) {
133
- return { url: moduleUrl(source), shortCircuit: true }
134
- }
135
- return nextResolve(specifier, context)
136
- }
@@ -1,32 +0,0 @@
1
- import { describe, expect, it } from 'vitest'
2
- import { CLIENT_CAPABILITIES } from '@gemus/codex-backfill-core'
3
- import { buildProxyUpstreamHeaders } from '../mcpHeaders.mjs'
4
-
5
- describe('buildProxyUpstreamHeaders', () => {
6
- it('advertises only the capabilities the proxy can actually fulfil', () => {
7
- expect(buildProxyUpstreamHeaders('mak_test123')).toEqual({
8
- Authorization: 'Bearer mak_test123',
9
- 'X-Gemus-Client-Capabilities': 'agent-authored-prompts,codex-imagen',
10
- })
11
- })
12
-
13
- it('never advertises codex-instant-delivery — the proxy has no ImageScheduler to renew the lease (#2049)', () => {
14
- // 该能力门控服务端的 AWAITING_CLIENT execute 分支,要求客户端有 scheduler 每 60s renewLease。
15
- // 只有 bridge 有(且 probe 门控,见 bridge/src/cli.ts)。proxy 没有 —— 发了它就等于承诺一个
16
- // 永不到来的续租 → 5min 后 CLIENT_LEASE_EXPIRED → 图 orphan 成孤立 image-upload 节点。
17
- // absent → 服务端回退经典两阶段 client-delivered 协议,proxy 完全支持(backfill.mjs)。
18
- const header = buildProxyUpstreamHeaders('mak_test123')['X-Gemus-Client-Capabilities']
19
- expect(header).not.toContain('codex-instant-delivery')
20
- })
21
-
22
- it('stays a strict subset of the shared allowlist — a new capability must not auto-inherit (#2049)', () => {
23
- // 回归门:本 bug 的成因正是 proxy 发「整个 CLIENT_CAPABILITIES 数组」—— #1969 往数组里加了
24
- // codex-instant-delivery,proxy 就自动继承了一个它做不到的承诺,而 #1990 把红掉的断言改成
25
- // 迎合它。此断言让「加能力」与「proxy 声称支持它」变成两个独立决策,不再自动传染。
26
- const advertised = buildProxyUpstreamHeaders('mak_test123')['X-Gemus-Client-Capabilities'].split(',')
27
- for (const capability of advertised) {
28
- expect(CLIENT_CAPABILITIES).toContain(capability)
29
- }
30
- expect(advertised.length).toBeLessThan(CLIENT_CAPABILITIES.length)
31
- })
32
- })
@@ -1,68 +0,0 @@
1
- import { describe, it, expect } from 'vitest'
2
- import { Client } from '@modelcontextprotocol/sdk/client/index.js'
3
- import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
4
-
5
- /**
6
- * Opt-in integration test against a REAL running gemus `/api/mcp` route (#1751). Validates the Mode-B
7
- * **key-only** contract the proxy + core delivery rely on: Bearer `mak_` auth with **no**
8
- * `X-Gemus-Session-Id` header, and addressing by an explicit real `workflowId`.
9
- *
10
- * Excluded from the default runner (`*.integration.test.ts`) and gated on env so it never runs in CI or
11
- * by accident. To run against local dev:
12
- * GEMUS_IT_KEY=mak_... GEMUS_IT_WORKFLOW=<realWorkflowId> \
13
- * pnpm exec vitest run packages/gemus-mcp-proxy/src/__tests__/route.integration.test.ts
14
- * (GEMUS_IT_URL defaults to http://localhost:3000/api/mcp.) All calls here are read-only (no mutation).
15
- *
16
- * connect-per-call invariant: every case opens + closes its own client (fresh connectionNonce, #1744).
17
- */
18
- const KEY = process.env.GEMUS_IT_KEY
19
- const URL_ = process.env.GEMUS_IT_URL || 'http://localhost:3000/api/mcp'
20
- const WORKFLOW = process.env.GEMUS_IT_WORKFLOW
21
-
22
- /** Key-only client: Bearer only, deliberately NO X-Gemus-Session-Id (the Mode-B contract). */
23
- async function connectKeyOnly() {
24
- const client = new Client({ name: 'gemus-proxy-it', version: '0.1.0' })
25
- const transport = new StreamableHTTPClientTransport(new URL(URL_), {
26
- requestInit: { headers: { Authorization: `Bearer ${KEY}` } },
27
- })
28
- await client.connect(transport)
29
- return client
30
- }
31
-
32
- describe.skipIf(!KEY)('key-only /api/mcp route (real, opt-in)', () => {
33
- it('relays the full tool surface incl. execute/batch_execute (no session header)', async () => {
34
- const client = await connectKeyOnly()
35
- try {
36
- const { tools } = await client.listTools()
37
- const names = tools.map((t) => t.name)
38
- expect(names).toEqual(expect.arrayContaining(['execute', 'batch_execute', 'canvas_read', 'canvas_edit']))
39
- } finally {
40
- await client.close()
41
- }
42
- })
43
-
44
- it('serves resources (skills / knowledge) through the key-only connection', async () => {
45
- const client = await connectKeyOnly()
46
- try {
47
- const { resources } = await client.listResources()
48
- // The route exposes skill:// / knowledge:// resources; a tool-only passthrough would drop these.
49
- expect(Array.isArray(resources)).toBe(true)
50
- } finally {
51
- await client.close()
52
- }
53
- })
54
-
55
- it.skipIf(!WORKFLOW)('addresses a real workflow by explicit workflowId (key-only, no activeContext)', async () => {
56
- const client = await connectKeyOnly()
57
- try {
58
- // canvas_read is read-only; success proves key-only addressing by explicit workflowId works
59
- // without a session header (what deliverImageToNode relies on in Mode B).
60
- const res = (await client.callTool({ name: 'canvas_read', arguments: { workflowId: WORKFLOW } })) as {
61
- isError?: boolean
62
- }
63
- expect(res.isError).not.toBe(true)
64
- } finally {
65
- await client.close()
66
- }
67
- })
68
- })