@openviking/opencode-plugin 2026.9.9 → 2026.9.21
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/INSTALL-ZH.md +42 -28
- package/INSTALL.md +41 -27
- package/README.md +47 -33
- package/index.mjs +5 -9
- package/lib/config.mjs +30 -307
- package/lib/memory-recall.mjs +2 -2
- package/lib/memory-session.mjs +6 -5
- package/lib/repo-context.mjs +2 -3
- package/lib/runtime.mjs +12 -19
- package/lib/shared/capture-utils.mjs +145 -6
- package/lib/shared/config-schema.mjs +359 -0
- package/lib/shared/credentials.mjs +238 -102
- package/lib/shared/input-filters.mjs +219 -0
- package/lib/shared/mcp-proxy-config.mjs +154 -2
- package/lib/shared/mcp-proxy-core.mjs +23 -15
- package/lib/shared/ov-http.mjs +112 -0
- package/lib/shared/pending-queue.mjs +41 -5
- package/lib/shared/plugin-config.mjs +300 -0
- package/lib/shared/recall-core.mjs +66 -13
- package/lib/shared/uri-guard.mjs +173 -1
- package/lib/shared/workspace-config.mjs +433 -0
- package/lib/shared/workspace-peer.mjs +63 -0
- package/lib/shared/workspace-registry.mjs +87 -0
- package/lib/utils.mjs +34 -177
- package/lib/viking-uri-guard.mjs +23 -8
- package/package.json +4 -3
- package/servers/mcp-proxy.mjs +7 -27
- package/servers/mcp-proxy.test.mjs +0 -102
package/lib/utils.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import fs from "fs"
|
|
2
2
|
import path from "path"
|
|
3
3
|
|
|
4
|
+
import { createOvHttp } from "./shared/ov-http.mjs"
|
|
5
|
+
|
|
4
6
|
let logFilePath = null
|
|
5
7
|
|
|
6
8
|
export function initLogger(dataDir) {
|
|
@@ -68,167 +70,49 @@ export function effectivePeerId(config) {
|
|
|
68
70
|
return String(config.effectivePeer?.peerId || config.peerId || "").trim() || null
|
|
69
71
|
}
|
|
70
72
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
+
// The config travels with every call here, so the client is built per call —
|
|
74
|
+
// a closure next to a network round-trip costs nothing.
|
|
75
|
+
function ovHttp(config) {
|
|
76
|
+
return createOvHttp(
|
|
77
|
+
{ ...config, baseUrl: normalizeEndpoint(config.endpoint) },
|
|
78
|
+
{ defaultTimeoutMs: config.timeoutMs },
|
|
79
|
+
)
|
|
73
80
|
}
|
|
74
81
|
|
|
75
82
|
export async function fetchJSON(config, endpoint, init = {}, options = {}) {
|
|
76
|
-
|
|
77
|
-
const headers = makeAuthHeaders(
|
|
78
|
-
config,
|
|
79
|
-
{ "Content-Type": "application/json", ...(init.headers ?? {}) },
|
|
80
|
-
options.actorPeerId,
|
|
81
|
-
)
|
|
82
|
-
const controller = new AbortController()
|
|
83
|
-
const timeout = setTimeout(() => controller.abort(), options.timeoutMs ?? config.timeoutMs)
|
|
84
|
-
try {
|
|
85
|
-
const response = await fetch(url, {
|
|
86
|
-
...init,
|
|
87
|
-
headers,
|
|
88
|
-
signal: controller.signal,
|
|
89
|
-
})
|
|
90
|
-
const text = await response.text()
|
|
91
|
-
const payload = text ? parseJsonOrText(text) : {}
|
|
92
|
-
const traceId = responseTraceId(payload)
|
|
93
|
-
if (!response.ok || payload?.status === "error") {
|
|
94
|
-
return {
|
|
95
|
-
ok: false,
|
|
96
|
-
status: response.status,
|
|
97
|
-
error: payload?.error || payload?.message || { message: `HTTP ${response.status}` },
|
|
98
|
-
traceId,
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
return { ok: true, status: response.status, result: payload?.result ?? payload, traceId }
|
|
102
|
-
} catch (error) {
|
|
103
|
-
return { ok: false, status: 0, error: { message: error?.message ?? String(error) } }
|
|
104
|
-
} finally {
|
|
105
|
-
clearTimeout(timeout)
|
|
106
|
-
}
|
|
83
|
+
return ovHttp(config)(endpoint, init, options)
|
|
107
84
|
}
|
|
108
85
|
|
|
86
|
+
/**
|
|
87
|
+
* `fetchJSON` for the callers that would rather catch than branch: it resolves
|
|
88
|
+
* to the unwrapped result and turns every failure into an Error whose message
|
|
89
|
+
* is what the user can act on.
|
|
90
|
+
*/
|
|
109
91
|
export async function makeRequest(config, options) {
|
|
110
|
-
const
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
options.
|
|
115
|
-
)
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
if (
|
|
122
|
-
if (
|
|
123
|
-
|
|
124
|
-
options.abortSignal.addEventListener("abort", onAbort, { once: true })
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
try {
|
|
128
|
-
const response = await fetch(url, {
|
|
129
|
-
method: options.method,
|
|
130
|
-
headers,
|
|
131
|
-
body: options.body === undefined ? undefined : JSON.stringify(options.body),
|
|
132
|
-
signal: controller.signal,
|
|
133
|
-
})
|
|
134
|
-
|
|
135
|
-
const text = await response.text()
|
|
136
|
-
const payload = text ? parseJsonOrText(text) : {}
|
|
137
|
-
|
|
138
|
-
if (!response.ok) {
|
|
139
|
-
const rawError = typeof payload === "object" ? payload.error ?? payload.message : payload
|
|
140
|
-
const errorMessage = typeof rawError === "string" ? rawError : JSON.stringify(rawError)
|
|
141
|
-
if (response.status === 401 || response.status === 403) {
|
|
142
|
-
throw new Error("Authentication failed. Please check apiKey/account/user in openviking-config.json or OPENVIKING_* environment variables.")
|
|
143
|
-
}
|
|
144
|
-
throw new Error(`Request failed (${response.status}): ${errorMessage}`)
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
return payload
|
|
148
|
-
} catch (error) {
|
|
149
|
-
if (error?.name === "AbortError") {
|
|
150
|
-
throw new Error(`Request timeout after ${options.timeoutMs ?? config.timeoutMs}ms`)
|
|
151
|
-
}
|
|
152
|
-
if (error?.message?.includes("fetch failed") || error?.code === "ECONNREFUSED") {
|
|
153
|
-
throw new Error(`OpenViking service unavailable at ${config.endpoint}. Start it with: openviking-server --config ~/.openviking/ov.conf`)
|
|
154
|
-
}
|
|
155
|
-
throw error
|
|
156
|
-
} finally {
|
|
157
|
-
clearTimeout(timeout)
|
|
158
|
-
if (options.abortSignal && onAbort) {
|
|
159
|
-
options.abortSignal.removeEventListener("abort", onAbort)
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
export async function makeMultipartRequest(config, options) {
|
|
165
|
-
const url = `${normalizeEndpoint(config.endpoint)}${options.endpoint}`
|
|
166
|
-
const headers = makeAuthHeaders(config, options.headers ?? {}, options.actorPeerId)
|
|
167
|
-
|
|
168
|
-
const controller = new AbortController()
|
|
169
|
-
const timeout = setTimeout(() => controller.abort(), options.timeoutMs ?? config.timeoutMs)
|
|
170
|
-
let onAbort = null
|
|
171
|
-
|
|
172
|
-
if (options.abortSignal) {
|
|
173
|
-
if (options.abortSignal.aborted) controller.abort()
|
|
174
|
-
onAbort = () => controller.abort()
|
|
175
|
-
options.abortSignal.addEventListener("abort", onAbort, { once: true })
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
try {
|
|
179
|
-
const response = await fetch(url, {
|
|
180
|
-
method: options.method,
|
|
181
|
-
headers,
|
|
182
|
-
body: options.body,
|
|
183
|
-
signal: controller.signal,
|
|
184
|
-
})
|
|
185
|
-
|
|
186
|
-
const text = await response.text()
|
|
187
|
-
const payload = text ? parseJsonOrText(text) : {}
|
|
188
|
-
|
|
189
|
-
if (!response.ok) {
|
|
190
|
-
const rawError = typeof payload === "object" ? payload.error ?? payload.message : payload
|
|
191
|
-
const errorMessage = typeof rawError === "string" ? rawError : JSON.stringify(rawError)
|
|
192
|
-
if (response.status === 401 || response.status === 403) {
|
|
193
|
-
throw new Error("Authentication failed. Please check apiKey/account/user in openviking-config.json or OPENVIKING_* environment variables.")
|
|
194
|
-
}
|
|
195
|
-
throw new Error(`Request failed (${response.status}): ${errorMessage}`)
|
|
92
|
+
const timeoutMs = options.timeoutMs ?? config.timeoutMs
|
|
93
|
+
const response = await fetchJSON(config, options.endpoint, {
|
|
94
|
+
method: options.method,
|
|
95
|
+
headers: options.headers,
|
|
96
|
+
body: options.body === undefined ? undefined : JSON.stringify(options.body),
|
|
97
|
+
}, { timeoutMs, actorPeerId: options.actorPeerId })
|
|
98
|
+
if (response.ok) return response.result
|
|
99
|
+
|
|
100
|
+
const message = getResponseErrorMessage(response.error)
|
|
101
|
+
// Status 0 is the transport: nothing answered, so the message is ours, not
|
|
102
|
+
// the server's, and it says what the user has to go fix.
|
|
103
|
+
if (response.status === 0) {
|
|
104
|
+
if (response.error?.aborted) {
|
|
105
|
+
throw new Error(`Request timeout after ${timeoutMs}ms`)
|
|
196
106
|
}
|
|
197
|
-
|
|
198
|
-
return payload
|
|
199
|
-
} catch (error) {
|
|
200
|
-
if (error?.name === "AbortError") {
|
|
201
|
-
throw new Error(`Request timeout after ${options.timeoutMs ?? config.timeoutMs}ms`)
|
|
202
|
-
}
|
|
203
|
-
if (error?.message?.includes("fetch failed") || error?.code === "ECONNREFUSED") {
|
|
107
|
+
if (message.includes("fetch failed") || message.includes("ECONNREFUSED")) {
|
|
204
108
|
throw new Error(`OpenViking service unavailable at ${config.endpoint}. Start it with: openviking-server --config ~/.openviking/ov.conf`)
|
|
205
109
|
}
|
|
206
|
-
throw
|
|
207
|
-
} finally {
|
|
208
|
-
clearTimeout(timeout)
|
|
209
|
-
if (options.abortSignal && onAbort) {
|
|
210
|
-
options.abortSignal.removeEventListener("abort", onAbort)
|
|
211
|
-
}
|
|
110
|
+
throw new Error(message)
|
|
212
111
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
function makeAuthHeaders(config, headers = {}, actorPeerId = "") {
|
|
216
|
-
const result = { ...headers }
|
|
217
|
-
if (config.apiKey) result["Authorization"] = `Bearer ${config.apiKey}`
|
|
218
|
-
if (config.account) result["X-OpenViking-Account"] = config.account
|
|
219
|
-
if (config.user) result["X-OpenViking-User"] = config.user
|
|
220
|
-
const peerId = String(actorPeerId || "").trim()
|
|
221
|
-
if (peerId) result["X-OpenViking-Actor-Peer"] = peerId
|
|
222
|
-
if (config.userAgent) result["User-Agent"] = config.userAgent
|
|
223
|
-
return result
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
function parseJsonOrText(text) {
|
|
227
|
-
try {
|
|
228
|
-
return JSON.parse(text)
|
|
229
|
-
} catch {
|
|
230
|
-
return text
|
|
112
|
+
if (response.status === 401 || response.status === 403) {
|
|
113
|
+
throw new Error("Authentication failed. Check api_key/account/user in ~/.openviking/ovcli.conf, or the OPENVIKING_* environment variables.")
|
|
231
114
|
}
|
|
115
|
+
throw new Error(`Request failed (${response.status}): ${message}`)
|
|
232
116
|
}
|
|
233
117
|
|
|
234
118
|
export function getResponseErrorMessage(error) {
|
|
@@ -236,30 +120,3 @@ export function getResponseErrorMessage(error) {
|
|
|
236
120
|
if (typeof error === "string") return error
|
|
237
121
|
return error.message || error.code || "Unknown OpenViking error"
|
|
238
122
|
}
|
|
239
|
-
|
|
240
|
-
export function unwrapResponse(response) {
|
|
241
|
-
if (!response || typeof response !== "object") {
|
|
242
|
-
throw new Error("OpenViking returned an invalid response")
|
|
243
|
-
}
|
|
244
|
-
if (response.status && response.status !== "ok") {
|
|
245
|
-
throw new Error(getResponseErrorMessage(response.error))
|
|
246
|
-
}
|
|
247
|
-
return response.result
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
export function validateVikingUri(uri, toolName = "tool") {
|
|
251
|
-
if (typeof uri !== "string" || !uri.startsWith("viking://")) {
|
|
252
|
-
log("ERROR", toolName, "Invalid Viking URI", { uri })
|
|
253
|
-
return 'Error: Invalid URI format. Must start with "viking://".'
|
|
254
|
-
}
|
|
255
|
-
return null
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
export function ensureRemoteUrl(value) {
|
|
259
|
-
try {
|
|
260
|
-
const url = new URL(value)
|
|
261
|
-
return url.protocol === "http:" || url.protocol === "https:"
|
|
262
|
-
} catch {
|
|
263
|
-
return false
|
|
264
|
-
}
|
|
265
|
-
}
|
package/lib/viking-uri-guard.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { log } from "./utils.mjs"
|
|
2
|
-
import {
|
|
2
|
+
import { evaluateUriGuard, evaluateUriNotice, findVikingUri, normalizeToolName } from "./shared/uri-guard.mjs"
|
|
3
3
|
|
|
4
4
|
const FILESYSTEM_TOOL_HINTS = {
|
|
5
5
|
read: {
|
|
@@ -14,23 +14,38 @@ const FILESYSTEM_TOOL_HINTS = {
|
|
|
14
14
|
tool: "openviking_search",
|
|
15
15
|
example: (uri, args = {}) => `openviking_search(query="${String(args.pattern ?? "").replaceAll('"', '\\"')}", target_uri="${uri}")`,
|
|
16
16
|
},
|
|
17
|
+
bash: {
|
|
18
|
+
tool: "openviking_read or openviking_search",
|
|
19
|
+
example: (uri) => `openviking_read(uris=["${uri}"])`,
|
|
20
|
+
},
|
|
17
21
|
}
|
|
18
22
|
|
|
19
23
|
export function createVikingUriGuard() {
|
|
20
24
|
return async (input, output) => {
|
|
21
25
|
const toolName = normalizeToolName(input?.tool ?? input?.name)
|
|
22
|
-
const hint = FILESYSTEM_TOOL_HINTS[toolName]
|
|
23
|
-
if (!hint) return
|
|
24
|
-
|
|
25
26
|
const args = output?.args ?? input?.args ?? {}
|
|
26
|
-
const
|
|
27
|
-
if (!
|
|
27
|
+
const decision = evaluateUriGuard(toolName, args, { hints: FILESYSTEM_TOOL_HINTS })
|
|
28
|
+
if (!decision) return
|
|
28
29
|
|
|
29
30
|
log("INFO", "viking-uri-guard", "Blocked filesystem tool for viking URI", {
|
|
30
31
|
tool: toolName,
|
|
31
|
-
uri,
|
|
32
|
+
uri: decision.uri,
|
|
33
|
+
})
|
|
34
|
+
throw new Error(decision.reason)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function createVikingUriNotice() {
|
|
39
|
+
return async (input, output) => {
|
|
40
|
+
const toolName = normalizeToolName(input?.tool ?? input?.name)
|
|
41
|
+
const notice = evaluateUriNotice(toolName, input?.args ?? {}, { hints: FILESYSTEM_TOOL_HINTS })
|
|
42
|
+
if (!notice || !output) return
|
|
43
|
+
|
|
44
|
+
log("INFO", "viking-uri-guard", "Attached viking URI notice to shell tool output", {
|
|
45
|
+
tool: toolName,
|
|
46
|
+
uri: notice.uri,
|
|
32
47
|
})
|
|
33
|
-
|
|
48
|
+
output.output = output.output ? `${output.output}\n\n${notice.reason}` : notice.reason
|
|
34
49
|
}
|
|
35
50
|
}
|
|
36
51
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openviking/opencode-plugin",
|
|
3
|
-
"version": "2026.9.
|
|
3
|
+
"version": "2026.9.21",
|
|
4
4
|
"author": "@openviking",
|
|
5
5
|
"description": "Unified OpenCode plugin for OpenViking repository retrieval and long-term memory",
|
|
6
6
|
"type": "module",
|
|
@@ -23,8 +23,9 @@
|
|
|
23
23
|
"INSTALL-ZH.md"
|
|
24
24
|
],
|
|
25
25
|
"scripts": {
|
|
26
|
+
"prepack": "node ../memory-plugin-shared/sync.mjs",
|
|
26
27
|
"check": "node --check index.mjs && node --check lib/config.mjs && node --check lib/mcp-config.mjs && node --check lib/runtime.mjs && node --check lib/repo-context.mjs && node --check lib/memory-session.mjs && node --check lib/memory-recall.mjs && node --check lib/session-inject.mjs && node --check lib/viking-uri-guard.mjs && node --check lib/utils.mjs && node --check lib/shared/*.mjs && node --check servers/mcp-proxy.mjs && node --check scripts/setup.mjs && node --check tests/*.test.mjs",
|
|
27
|
-
"test": "node --test tests/*.test.mjs
|
|
28
|
+
"test": "node --test tests/*.test.mjs"
|
|
28
29
|
},
|
|
29
30
|
"dependencies": {},
|
|
30
31
|
"publishConfig": {
|
|
@@ -44,5 +45,5 @@
|
|
|
44
45
|
"code-search"
|
|
45
46
|
],
|
|
46
47
|
"license": "Apache-2.0",
|
|
47
|
-
"openvikingSourceCommit": "
|
|
48
|
+
"openvikingSourceCommit": "aa77061c14447452d0ae48a523271a2676150248"
|
|
48
49
|
}
|
package/servers/mcp-proxy.mjs
CHANGED
|
@@ -3,42 +3,22 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* stdio -> streamable-HTTP MCP proxy for the OpenViking OpenCode plugin.
|
|
5
5
|
*
|
|
6
|
-
* OpenCode starts this process as a local MCP server. The proxy
|
|
7
|
-
*
|
|
6
|
+
* OpenCode starts this process as a local MCP server. The proxy resolves its
|
|
7
|
+
* connection through the plugin's own `loadConfig()`, forwards JSON-RPC
|
|
8
8
|
* requests to the server's /mcp endpoint, and keeps stdout protocol-clean.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import {
|
|
12
|
-
import { join, resolve as resolvePath } from "node:path"
|
|
11
|
+
import { resolve as resolvePath } from "node:path"
|
|
13
12
|
import { fileURLToPath } from "node:url"
|
|
14
13
|
import { loadConfig } from "../lib/config.mjs"
|
|
15
14
|
import { createLogger } from "../lib/shared/debug-log.mjs"
|
|
16
|
-
import {
|
|
15
|
+
import { toMcpProxyConfig } from "../lib/shared/mcp-proxy-config.mjs"
|
|
17
16
|
import { createOpenVikingMcpProxy } from "../lib/shared/mcp-proxy-core.mjs"
|
|
18
17
|
|
|
19
|
-
|
|
18
|
+
const PLUGIN_ROOT = resolvePath(fileURLToPath(import.meta.url), "..", "..")
|
|
20
19
|
|
|
21
|
-
function readProxyConfig() {
|
|
22
|
-
|
|
23
|
-
return buildMcpProxyConfig({
|
|
24
|
-
baseUrl: cfg.endpoint,
|
|
25
|
-
mcpUrl: cfg.mcpUrl,
|
|
26
|
-
apiKey: cfg.apiKey,
|
|
27
|
-
account: cfg.account,
|
|
28
|
-
user: cfg.user,
|
|
29
|
-
peerId: resolveMcpActorPeerId(cfg),
|
|
30
|
-
userAgent: cfg.userAgent,
|
|
31
|
-
timeoutMs: cfg.timeoutMs,
|
|
32
|
-
debug: cfg.debug,
|
|
33
|
-
debugLogPath: cfg.debugLogPath,
|
|
34
|
-
credentialSource: cfg.credentialSource,
|
|
35
|
-
credentialPath: cfg.credentialPath || cfg.configPath,
|
|
36
|
-
watchedPaths: [
|
|
37
|
-
cfg.credentialPath,
|
|
38
|
-
cfg.configPath,
|
|
39
|
-
join(homedir(), ".config", "opencode", "openviking-config.json"),
|
|
40
|
-
],
|
|
41
|
-
})
|
|
20
|
+
export function readProxyConfig(env = process.env) {
|
|
21
|
+
return toMcpProxyConfig(loadConfig(PLUGIN_ROOT, undefined, { env }), { env })
|
|
42
22
|
}
|
|
43
23
|
|
|
44
24
|
if (process.argv[1] && fileURLToPath(import.meta.url) === resolvePath(process.argv[1])) {
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
import test from "node:test"
|
|
2
|
-
import assert from "node:assert/strict"
|
|
3
|
-
import { createServer } from "node:http"
|
|
4
|
-
import { Writable } from "node:stream"
|
|
5
|
-
import { createOpenVikingMcpProxy } from "./mcp-proxy.mjs"
|
|
6
|
-
|
|
7
|
-
function jsonRpc(id, result = {}) {
|
|
8
|
-
return { jsonrpc: "2.0", id, result }
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function sseMessage(obj) {
|
|
12
|
-
return `event: message\r\ndata: ${JSON.stringify(obj)}\r\n\r\n`
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
async function readBody(req) {
|
|
16
|
-
const chunks = []
|
|
17
|
-
for await (const chunk of req) chunks.push(chunk)
|
|
18
|
-
return Buffer.concat(chunks).toString("utf-8")
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
async function withServer(handler, fn) {
|
|
22
|
-
const requests = []
|
|
23
|
-
const server = createServer(async (req, res) => {
|
|
24
|
-
const body = await readBody(req)
|
|
25
|
-
const entry = {
|
|
26
|
-
method: req.method,
|
|
27
|
-
url: req.url,
|
|
28
|
-
headers: req.headers,
|
|
29
|
-
body: body ? JSON.parse(body) : null,
|
|
30
|
-
}
|
|
31
|
-
requests.push(entry)
|
|
32
|
-
await handler(req, res, entry)
|
|
33
|
-
})
|
|
34
|
-
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve))
|
|
35
|
-
try {
|
|
36
|
-
const { port } = server.address()
|
|
37
|
-
return await fn({ url: `http://127.0.0.1:${port}/mcp`, requests })
|
|
38
|
-
} finally {
|
|
39
|
-
await new Promise((resolve) => server.close(resolve))
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function makeProxy(url) {
|
|
44
|
-
const out = []
|
|
45
|
-
const stdout = new Writable({
|
|
46
|
-
write(chunk, _encoding, callback) {
|
|
47
|
-
out.push(chunk.toString("utf-8"))
|
|
48
|
-
callback()
|
|
49
|
-
},
|
|
50
|
-
})
|
|
51
|
-
const proxy = createOpenVikingMcpProxy({
|
|
52
|
-
stdout,
|
|
53
|
-
readConfig: () => ({
|
|
54
|
-
mcpUrl: url,
|
|
55
|
-
apiKey: "test-key",
|
|
56
|
-
account: "acct",
|
|
57
|
-
user: "user",
|
|
58
|
-
peerId: "peer",
|
|
59
|
-
userAgent: "openviking-memory-opencode/9.9.9",
|
|
60
|
-
timeoutMs: 5000,
|
|
61
|
-
debug: false,
|
|
62
|
-
debugLogPath: "",
|
|
63
|
-
credentialSource: "test",
|
|
64
|
-
credentialPath: "",
|
|
65
|
-
watchedPaths: [],
|
|
66
|
-
}),
|
|
67
|
-
loggerFactory: () => ({ log() {}, logError() {} }),
|
|
68
|
-
})
|
|
69
|
-
return {
|
|
70
|
-
proxy,
|
|
71
|
-
async messages() {
|
|
72
|
-
await new Promise((resolve) => setImmediate(resolve))
|
|
73
|
-
return out.join("").trim().split("\n").filter(Boolean).map((line) => JSON.parse(line))
|
|
74
|
-
},
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
test("OpenCode MCP proxy forwards initialize with auth and identity headers", async () => {
|
|
79
|
-
await withServer((_req, res, entry) => {
|
|
80
|
-
assert.equal(entry.method, "POST")
|
|
81
|
-
assert.equal(entry.headers.authorization, "Bearer test-key")
|
|
82
|
-
assert.equal(entry.headers["x-openviking-account"], "acct")
|
|
83
|
-
assert.equal(entry.headers["x-openviking-user"], "user")
|
|
84
|
-
assert.equal(entry.headers["x-openviking-actor-peer"], "peer")
|
|
85
|
-
assert.equal(entry.headers["user-agent"], "openviking-memory-opencode/9.9.9")
|
|
86
|
-
res.writeHead(200, {
|
|
87
|
-
"content-type": "text/event-stream",
|
|
88
|
-
"mcp-session-id": "sid-oc",
|
|
89
|
-
})
|
|
90
|
-
res.end(sseMessage(jsonRpc(1, { protocolVersion: "2025-06-18" })))
|
|
91
|
-
}, async ({ url, requests }) => {
|
|
92
|
-
const { proxy, messages } = makeProxy(url)
|
|
93
|
-
await proxy.handleMessage({
|
|
94
|
-
jsonrpc: "2.0",
|
|
95
|
-
id: 1,
|
|
96
|
-
method: "initialize",
|
|
97
|
-
params: { protocolVersion: "2025-06-18" },
|
|
98
|
-
})
|
|
99
|
-
assert.deepEqual(await messages(), [jsonRpc(1, { protocolVersion: "2025-06-18" })])
|
|
100
|
-
assert.equal(requests.length, 1)
|
|
101
|
-
})
|
|
102
|
-
})
|