opencode-raven 2.0.1 → 2.1.0
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 +161 -171
- package/Raven.md +14 -10
- package/example.gif +0 -0
- package/index.ts +858 -94
- package/mcp-guidance.md +24 -21
- package/package.json +6 -2
package/index.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { Plugin, PluginInput } from "@opencode-ai/plugin"
|
|
2
2
|
import { tool } from "@opencode-ai/plugin"
|
|
3
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js"
|
|
4
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
|
|
5
|
+
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"
|
|
6
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
|
|
3
7
|
import { readFileSync, writeFileSync, existsSync, mkdirSync, appendFileSync, readdirSync, rmSync } from "node:fs"
|
|
4
8
|
import { join } from "node:path"
|
|
5
9
|
import { homedir, tmpdir } from "node:os"
|
|
@@ -19,29 +23,43 @@ const DEFAULT_ROUTE_TOOLS = [
|
|
|
19
23
|
"glob",
|
|
20
24
|
"webfetch",
|
|
21
25
|
"fetch",
|
|
26
|
+
"websearch",
|
|
22
27
|
"bash",
|
|
23
28
|
]
|
|
24
29
|
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
30
|
+
const DEFAULT_ROUTE_TOOL_KEYWORDS: string[] = []
|
|
31
|
+
|
|
32
|
+
const DEFAULT_ON_DEMAND_MCP_SERVERS: Record<string, OnDemandMcpServer> = {
|
|
33
|
+
context7: {
|
|
34
|
+
type: "remote",
|
|
35
|
+
url: "https://mcp.context7.com/mcp",
|
|
36
|
+
description: "Library/framework documentation lookup.",
|
|
37
|
+
},
|
|
38
|
+
exa: {
|
|
39
|
+
type: "remote",
|
|
40
|
+
url: "https://mcp.exa.ai/mcp",
|
|
41
|
+
description: "Live web search and webpage fetching.",
|
|
42
|
+
},
|
|
43
|
+
grep_app: {
|
|
44
|
+
type: "remote",
|
|
45
|
+
url: "https://mcp.grep.app",
|
|
46
|
+
description: "Public GitHub code search and real-world examples.",
|
|
47
|
+
},
|
|
42
48
|
}
|
|
43
49
|
|
|
44
|
-
const
|
|
50
|
+
const DEFAULT_ON_DEMAND_MCP_METADATA: Record<string, OnDemandMcpMetadata> = {
|
|
51
|
+
context7: {
|
|
52
|
+
autoGeneratedDescription: "Finds current library, framework, SDK, package, and API documentation; useful for implementation, configuration, and debugging with version-specific docs.",
|
|
53
|
+
},
|
|
54
|
+
exa: {
|
|
55
|
+
autoGeneratedDescription: "Searches the live web, fetches webpages, researches companies/products, and gathers current information from online sources.",
|
|
56
|
+
},
|
|
57
|
+
grep_app: {
|
|
58
|
+
autoGeneratedDescription: "Searches public GitHub repositories for real-world code examples, usage patterns, configuration snippets, and implementation references.",
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const NEVER_ROUTE_TOOLS = new Set(["raven_seek", "raven_mcp", "task", "subtask"])
|
|
45
63
|
|
|
46
64
|
// ── Bash commands that look like search workarounds ──
|
|
47
65
|
const SEARCH_BASH_RE = /\b(?:rg|ripgrep|grep|egrep|fgrep|git\s+grep|ack|ag\b|findstr|Select-String)\b|\b(?:Get-ChildItem|gci)\b(?=[^|;&\n]*(?:-Recurse|-Filter|-Include|\s-[A-Za-z]*r[A-Za-z]*\b))|\bdir\b(?=[^|;&\n]*(?:[/-][sS]\b|-Recurse|-Filter|-Include))|\bls\b(?=[^|;&\n]*(?:\s-[A-Za-z]*R[A-Za-z]*\b|--recursive\b))|\bfind\b\s+.*(?:-name|-type)\b/i
|
|
@@ -114,15 +132,51 @@ function isSearchBash(tool: string, args: any): boolean {
|
|
|
114
132
|
}
|
|
115
133
|
|
|
116
134
|
// ── Config file shape ──
|
|
135
|
+
interface OnDemandMcpToolInfo {
|
|
136
|
+
name: string
|
|
137
|
+
description?: string
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
interface BaseOnDemandMcpServer {
|
|
141
|
+
description?: string
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
interface OnDemandMcpMetadata {
|
|
145
|
+
autoGeneratedDescription?: string
|
|
146
|
+
autoGeneratedTools?: OnDemandMcpToolInfo[]
|
|
147
|
+
avoidedSchemaBytes?: number
|
|
148
|
+
status?: "pending" | "ready" | "failed"
|
|
149
|
+
lastError?: string
|
|
150
|
+
lastCheckedAt?: string
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
interface RemoteOnDemandMcpServer extends BaseOnDemandMcpServer {
|
|
154
|
+
type: "remote"
|
|
155
|
+
url: string
|
|
156
|
+
headers?: Record<string, string>
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
interface StdioOnDemandMcpServer extends BaseOnDemandMcpServer {
|
|
160
|
+
type: "stdio"
|
|
161
|
+
command: string
|
|
162
|
+
args?: string[]
|
|
163
|
+
env?: Record<string, string>
|
|
164
|
+
cwd?: string
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
type OnDemandMcpServer = RemoteOnDemandMcpServer | StdioOnDemandMcpServer
|
|
168
|
+
type OnDemandMcpDescriptionDetail = "full" | "minimized"
|
|
169
|
+
type RavenMcpOperation = "list_tools" | "call_tool" | "list_resources" | "read_resource" | "list_prompts" | "get_prompt"
|
|
170
|
+
|
|
117
171
|
interface RavenConfig {
|
|
118
172
|
enabled: boolean
|
|
119
173
|
model?: string
|
|
120
174
|
reasoning_effort?: string
|
|
121
|
-
ravenInstructions?: string
|
|
122
|
-
routeTools?: string[]
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
175
|
+
ravenInstructions?: string
|
|
176
|
+
routeTools?: string[]
|
|
177
|
+
routeToolKeywords?: string[]
|
|
178
|
+
onDemandMcpDescriptionDetail?: OnDemandMcpDescriptionDetail
|
|
179
|
+
onDemandMcpServers?: Record<string, OnDemandMcpServer>
|
|
126
180
|
excludeAgents?: string[]
|
|
127
181
|
excludeTools?: string[]
|
|
128
182
|
timeout?: number
|
|
@@ -137,11 +191,11 @@ const DEFAULT_CONFIG: RavenConfig = {
|
|
|
137
191
|
enabled: true,
|
|
138
192
|
model: fm.model,
|
|
139
193
|
reasoning_effort: fm.reasoning_effort,
|
|
140
|
-
ravenInstructions: "",
|
|
141
|
-
routeTools: DEFAULT_ROUTE_TOOLS,
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
194
|
+
ravenInstructions: "",
|
|
195
|
+
routeTools: DEFAULT_ROUTE_TOOLS,
|
|
196
|
+
routeToolKeywords: DEFAULT_ROUTE_TOOL_KEYWORDS,
|
|
197
|
+
onDemandMcpDescriptionDetail: "full",
|
|
198
|
+
onDemandMcpServers: DEFAULT_ON_DEMAND_MCP_SERVERS,
|
|
145
199
|
excludeAgents: [],
|
|
146
200
|
excludeTools: [],
|
|
147
201
|
timeout: 180,
|
|
@@ -230,25 +284,123 @@ function uniqueStrings(value: unknown, fallback: string[] = []): string[] {
|
|
|
230
284
|
return [...new Set(source.filter((item) => typeof item === "string" && item.trim()).map((item) => item.trim()))]
|
|
231
285
|
}
|
|
232
286
|
|
|
287
|
+
function cloneOnDemandMcpServers(value: Record<string, OnDemandMcpServer>): Record<string, OnDemandMcpServer> {
|
|
288
|
+
return JSON.parse(JSON.stringify(value))
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function cloneOnDemandMcpMetadata(value: Record<string, OnDemandMcpMetadata>): Record<string, OnDemandMcpMetadata> {
|
|
292
|
+
return JSON.parse(JSON.stringify(value))
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function normalizeToolInfo(value: unknown): OnDemandMcpToolInfo[] | undefined {
|
|
296
|
+
if (!Array.isArray(value)) return undefined
|
|
297
|
+
const tools = value
|
|
298
|
+
.filter((item) => item && typeof item === "object" && typeof item.name === "string" && item.name.trim())
|
|
299
|
+
.map((item) => ({
|
|
300
|
+
name: item.name.trim(),
|
|
301
|
+
...(typeof item.description === "string" && item.description.trim() ? { description: item.description.trim() } : {}),
|
|
302
|
+
}))
|
|
303
|
+
return tools.length ? tools : undefined
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function normalizeOnDemandMcpServer(value: unknown): OnDemandMcpServer | undefined {
|
|
307
|
+
if (!value || typeof value !== "object") return undefined
|
|
308
|
+
const source = value as Record<string, any>
|
|
309
|
+
const base = {
|
|
310
|
+
...(typeof source.description === "string" && source.description.trim() ? { description: source.description.trim() } : {}),
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (source.type === "stdio") {
|
|
314
|
+
if (typeof source.command !== "string" || !source.command.trim()) return undefined
|
|
315
|
+
return {
|
|
316
|
+
...base,
|
|
317
|
+
type: "stdio",
|
|
318
|
+
command: source.command.trim(),
|
|
319
|
+
...(Array.isArray(source.args) ? { args: source.args.filter((arg: unknown) => typeof arg === "string") } : {}),
|
|
320
|
+
...(source.env && typeof source.env === "object" ? { env: Object.fromEntries(Object.entries(source.env).filter((entry): entry is [string, string] => typeof entry[1] === "string")) } : {}),
|
|
321
|
+
...(typeof source.cwd === "string" && source.cwd.trim() ? { cwd: source.cwd.trim() } : {}),
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (source.type === "remote" || typeof source.url === "string") {
|
|
326
|
+
if (typeof source.url !== "string" || !source.url.trim()) return undefined
|
|
327
|
+
return {
|
|
328
|
+
...base,
|
|
329
|
+
type: "remote",
|
|
330
|
+
url: source.url.trim(),
|
|
331
|
+
...(source.headers && typeof source.headers === "object" ? { headers: Object.fromEntries(Object.entries(source.headers).filter((entry): entry is [string, string] => typeof entry[1] === "string")) } : {}),
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return undefined
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function normalizeOnDemandMcpMetadata(value: unknown, fallback: Record<string, OnDemandMcpMetadata> = {}): Record<string, OnDemandMcpMetadata> {
|
|
339
|
+
const source = value && typeof value === "object" ? value as Record<string, any> : fallback
|
|
340
|
+
const result: Record<string, OnDemandMcpMetadata> = {}
|
|
341
|
+
for (const [key, raw] of Object.entries(source)) {
|
|
342
|
+
if (!key.trim() || !raw || typeof raw !== "object") continue
|
|
343
|
+
const item = raw as Record<string, unknown>
|
|
344
|
+
const metadata: OnDemandMcpMetadata = {}
|
|
345
|
+
if (typeof item.autoGeneratedDescription === "string" && item.autoGeneratedDescription.trim()) {
|
|
346
|
+
metadata.autoGeneratedDescription = item.autoGeneratedDescription.trim()
|
|
347
|
+
}
|
|
348
|
+
const tools = normalizeToolInfo(item.autoGeneratedTools)
|
|
349
|
+
if (tools) metadata.autoGeneratedTools = tools
|
|
350
|
+
if (typeof item.avoidedSchemaBytes === "number" && Number.isFinite(item.avoidedSchemaBytes) && item.avoidedSchemaBytes >= 0) {
|
|
351
|
+
metadata.avoidedSchemaBytes = Math.round(item.avoidedSchemaBytes)
|
|
352
|
+
}
|
|
353
|
+
if (item.status === "pending" || item.status === "ready" || item.status === "failed") metadata.status = item.status
|
|
354
|
+
if (typeof item.lastError === "string" && item.lastError.trim()) metadata.lastError = item.lastError.trim()
|
|
355
|
+
if (typeof item.lastCheckedAt === "string" && item.lastCheckedAt.trim()) metadata.lastCheckedAt = item.lastCheckedAt.trim()
|
|
356
|
+
if (metadata.autoGeneratedDescription || metadata.autoGeneratedTools?.length || metadata.avoidedSchemaBytes || metadata.status || metadata.lastError || metadata.lastCheckedAt) result[key.trim()] = metadata
|
|
357
|
+
}
|
|
358
|
+
return result
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function extractOnDemandMcpMetadata(value: unknown): Record<string, OnDemandMcpMetadata> {
|
|
362
|
+
return normalizeOnDemandMcpMetadata(value)
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function normalizeOnDemandMcpServers(value: unknown, fallback: Record<string, OnDemandMcpServer> = {}): Record<string, OnDemandMcpServer> {
|
|
366
|
+
const source = value && typeof value === "object" ? value as Record<string, unknown> : fallback
|
|
367
|
+
const result: Record<string, OnDemandMcpServer> = {}
|
|
368
|
+
for (const [key, raw] of Object.entries(source)) {
|
|
369
|
+
if (!key.trim()) continue
|
|
370
|
+
const server = normalizeOnDemandMcpServer(raw)
|
|
371
|
+
if (server) result[key.trim()] = server
|
|
372
|
+
}
|
|
373
|
+
return result
|
|
374
|
+
}
|
|
375
|
+
|
|
233
376
|
// ── Plugin ──
|
|
234
377
|
export default ((input: PluginInput) => {
|
|
235
378
|
const client = input.client
|
|
236
379
|
|
|
237
|
-
//
|
|
238
|
-
const
|
|
380
|
+
// Raven state lives under the global opencode config directory.
|
|
381
|
+
const opencodeConfigDir = join(homedir(), ".config", "opencode")
|
|
382
|
+
const ravenConfigDir = join(opencodeConfigDir, "opencode-raven")
|
|
383
|
+
const oldConfigFile = join(opencodeConfigDir, "raven-config.json")
|
|
384
|
+
const configFile = join(ravenConfigDir, "raven-config.json")
|
|
385
|
+
const mcpMetadataFile = join(ravenConfigDir, "autogenerated-on-demand-mcp-metadata.json")
|
|
386
|
+
const dynamicGuidanceFile = join(ravenConfigDir, "autogenerated-on-demand-guidance.md")
|
|
387
|
+
let migratedFromOldConfig = false
|
|
388
|
+
let initialMcpMetadata: Record<string, OnDemandMcpMetadata> = {}
|
|
239
389
|
|
|
240
390
|
function normalizeConfig(raw: any): RavenConfig {
|
|
241
391
|
const source = raw && typeof raw === "object" ? raw : {}
|
|
242
|
-
const normalized: RavenConfig = { ...DEFAULT_CONFIG
|
|
392
|
+
const normalized: RavenConfig = { ...DEFAULT_CONFIG }
|
|
243
393
|
|
|
244
394
|
normalized.enabled = source.enabled !== false
|
|
245
395
|
normalized.model = typeof source.model === "string" ? source.model : DEFAULT_CONFIG.model
|
|
246
396
|
normalized.reasoning_effort = typeof source.reasoning_effort === "string" ? source.reasoning_effort : DEFAULT_CONFIG.reasoning_effort
|
|
247
|
-
normalized.ravenInstructions = typeof source.ravenInstructions === "string" ? source.ravenInstructions : DEFAULT_CONFIG.ravenInstructions
|
|
248
|
-
normalized.routeTools = uniqueStrings(source.routeTools, DEFAULT_ROUTE_TOOLS)
|
|
249
|
-
normalized.
|
|
250
|
-
normalized.
|
|
251
|
-
normalized.
|
|
397
|
+
normalized.ravenInstructions = typeof source.ravenInstructions === "string" ? source.ravenInstructions : DEFAULT_CONFIG.ravenInstructions
|
|
398
|
+
normalized.routeTools = uniqueStrings(source.routeTools, DEFAULT_ROUTE_TOOLS)
|
|
399
|
+
normalized.routeToolKeywords = uniqueStrings(source.routeToolKeywords, DEFAULT_ROUTE_TOOL_KEYWORDS)
|
|
400
|
+
normalized.onDemandMcpDescriptionDetail = source.onDemandMcpDescriptionDetail === "minimized" ? "minimized" : "full"
|
|
401
|
+
normalized.onDemandMcpServers = source.onDemandMcpServers === undefined
|
|
402
|
+
? cloneOnDemandMcpServers(DEFAULT_ON_DEMAND_MCP_SERVERS)
|
|
403
|
+
: normalizeOnDemandMcpServers(source.onDemandMcpServers)
|
|
252
404
|
normalized.excludeAgents = uniqueStrings(source.excludeAgents)
|
|
253
405
|
normalized.excludeTools = uniqueStrings(source.excludeTools)
|
|
254
406
|
normalized.timeout = typeof source.timeout === "number" ? source.timeout : DEFAULT_CONFIG.timeout
|
|
@@ -259,12 +411,18 @@ export default ((input: PluginInput) => {
|
|
|
259
411
|
|
|
260
412
|
function loadConfig(): RavenConfig {
|
|
261
413
|
try {
|
|
262
|
-
|
|
263
|
-
|
|
414
|
+
const sourceFile = existsSync(configFile) ? configFile : existsSync(oldConfigFile) ? oldConfigFile : undefined
|
|
415
|
+
if (sourceFile) {
|
|
416
|
+
const raw = JSON.parse(readFileSync(sourceFile, "utf-8"))
|
|
417
|
+
migratedFromOldConfig = sourceFile === oldConfigFile
|
|
418
|
+
initialMcpMetadata = extractOnDemandMcpMetadata(raw.onDemandMcpServers)
|
|
264
419
|
const normalized = normalizeConfig(raw)
|
|
265
|
-
if (JSON.stringify(raw) !== JSON.stringify(normalized)) {
|
|
420
|
+
if (migratedFromOldConfig || JSON.stringify(raw) !== JSON.stringify(normalized)) {
|
|
266
421
|
saveConfig(normalized)
|
|
267
422
|
}
|
|
423
|
+
if (migratedFromOldConfig) {
|
|
424
|
+
try { rmSync(oldConfigFile, { force: true }) } catch { /* best-effort */ }
|
|
425
|
+
}
|
|
268
426
|
return normalized
|
|
269
427
|
}
|
|
270
428
|
} catch { /* ignore corruption, use defaults */ }
|
|
@@ -275,19 +433,75 @@ export default ((input: PluginInput) => {
|
|
|
275
433
|
|
|
276
434
|
function saveConfig(config: RavenConfig) {
|
|
277
435
|
try {
|
|
278
|
-
mkdirSync(
|
|
436
|
+
mkdirSync(ravenConfigDir, { recursive: true })
|
|
279
437
|
writeFileSync(configFile, JSON.stringify(config, null, 2) + "\n")
|
|
280
438
|
} catch { /* non-fatal: config won't persist but toggle still works in-session */ }
|
|
281
439
|
}
|
|
282
440
|
|
|
441
|
+
function loadMcpMetadata(seed: Record<string, OnDemandMcpMetadata>): Record<string, OnDemandMcpMetadata> {
|
|
442
|
+
const defaults = cloneOnDemandMcpMetadata(DEFAULT_ON_DEMAND_MCP_METADATA)
|
|
443
|
+
try {
|
|
444
|
+
if (existsSync(mcpMetadataFile)) {
|
|
445
|
+
return { ...defaults, ...normalizeOnDemandMcpMetadata(JSON.parse(readFileSync(mcpMetadataFile, "utf-8"))) }
|
|
446
|
+
}
|
|
447
|
+
} catch { /* ignore corruption, use defaults + seed */ }
|
|
448
|
+
const metadata = { ...defaults, ...seed }
|
|
449
|
+
saveMcpMetadata(metadata)
|
|
450
|
+
return metadata
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function saveMcpMetadata(metadata: Record<string, OnDemandMcpMetadata>) {
|
|
454
|
+
try {
|
|
455
|
+
mkdirSync(ravenConfigDir, { recursive: true })
|
|
456
|
+
writeFileSync(mcpMetadataFile, JSON.stringify(metadata, null, 2) + "\n")
|
|
457
|
+
} catch { /* non-fatal: generated metadata can be refreshed */ }
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function saveDynamicGuidance() {
|
|
461
|
+
try {
|
|
462
|
+
mkdirSync(ravenConfigDir, { recursive: true })
|
|
463
|
+
writeFileSync(dynamicGuidanceFile, `## Raven on-demand MCP capabilities\n\n${onDemandMcpGuidance("delegate")}\n`, "utf-8")
|
|
464
|
+
} catch { /* non-fatal: guidance still exists in tool/subagent paths */ }
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function cleanupOldRootFiles() {
|
|
468
|
+
try {
|
|
469
|
+
if (existsSync(configFile)) rmSync(oldConfigFile, { force: true })
|
|
470
|
+
} catch { /* best-effort migration cleanup */ }
|
|
471
|
+
}
|
|
472
|
+
|
|
283
473
|
let config = loadConfig()
|
|
474
|
+
let mcpMetadata = loadMcpMetadata(initialMcpMetadata)
|
|
284
475
|
const ravenSessions = new Set<string>()
|
|
285
476
|
const ravenTaskCalls = new Set<string>()
|
|
286
477
|
const sessionAgents = new Map<string, string>()
|
|
287
478
|
const ravenSessionParents = new Map<string, string>()
|
|
479
|
+
let globalMcpServers: string[] = []
|
|
480
|
+
let duplicateMcpServers: string[] = []
|
|
288
481
|
let updateInfo: { current: string; latest?: string; available: boolean } | undefined
|
|
289
482
|
let updateCheckPromise: Promise<{ current: string; latest?: string; available: boolean }> | undefined
|
|
290
483
|
let updateToastPending = false
|
|
484
|
+
let mcpFailureToastPending = false
|
|
485
|
+
let duplicateMcpToastPending = false
|
|
486
|
+
let mcpRefreshScheduled = false
|
|
487
|
+
let mcpRefreshPromise: Promise<{ refreshed: string[]; failed: string[] }> | undefined
|
|
488
|
+
let lastNotifiedMcpFailureKey = ""
|
|
489
|
+
|
|
490
|
+
resetOnDemandMcpRuntimeStatuses()
|
|
491
|
+
|
|
492
|
+
function resetOnDemandMcpRuntimeStatuses() {
|
|
493
|
+
let changed = false
|
|
494
|
+
for (const name of Object.keys(config.onDemandMcpServers ?? {})) {
|
|
495
|
+
const metadata = mcpMetadata[name] ?? {}
|
|
496
|
+
if (metadata.status !== "pending" || metadata.lastError || metadata.lastCheckedAt) {
|
|
497
|
+
mcpMetadata[name] = { ...metadata, status: "pending" }
|
|
498
|
+
delete mcpMetadata[name].lastError
|
|
499
|
+
delete mcpMetadata[name].lastCheckedAt
|
|
500
|
+
changed = true
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
if (changed) saveMcpMetadata(mcpMetadata)
|
|
504
|
+
}
|
|
291
505
|
|
|
292
506
|
// ── Check if an agent is excluded from Raven enforcement (case-insensitive) ──
|
|
293
507
|
function isExcluded(agent: string | undefined): boolean {
|
|
@@ -297,19 +511,19 @@ export default ((input: PluginInput) => {
|
|
|
297
511
|
}
|
|
298
512
|
|
|
299
513
|
function ravenGuidance(): string {
|
|
300
|
-
const tools = config.routeTools?.length ? config.routeTools.join(", ") : "none"
|
|
301
|
-
const mcps =
|
|
302
|
-
const keywords = config.routeToolKeywords?.length ? config.routeToolKeywords.join(", ") : "none"
|
|
303
|
-
return `Some tools/MCPs are routed through Raven to save context. Routed tools: ${tools}.
|
|
304
|
-
}
|
|
514
|
+
const tools = config.routeTools?.length ? config.routeTools.join(", ") : "none"
|
|
515
|
+
const mcps = globalMcpServers.length ? globalMcpServers.map((server) => `${server}_*`).join(", ") : "none"
|
|
516
|
+
const keywords = config.routeToolKeywords?.length ? config.routeToolKeywords.join(", ") : "none"
|
|
517
|
+
return `Some tools/MCPs are routed through Raven to save context. Routed tools: ${tools}. Auto-routed global MCP prefixes: ${mcps}. Routed tool-name keywords: ${keywords}. ${onDemandMcpGuidance("delegate")} If one is blocked, your next tool call should be raven_seek(query="<same request>"). Include the original tool/MCP name and relevant arguments.`
|
|
518
|
+
}
|
|
305
519
|
|
|
306
520
|
function isRouteConfigured(toolName: string): boolean {
|
|
307
521
|
const tool = toolName.toLowerCase()
|
|
308
|
-
if (NEVER_ROUTE_TOOLS.has(tool)) return false
|
|
309
|
-
if (config.routeTools?.some((name) => name.toLowerCase() === tool)) return true
|
|
310
|
-
if (config.routeToolKeywords?.some((keyword) => tool.includes(keyword.toLowerCase()))) return true
|
|
311
|
-
return
|
|
312
|
-
}
|
|
522
|
+
if (NEVER_ROUTE_TOOLS.has(tool)) return false
|
|
523
|
+
if (config.routeTools?.some((name) => name.toLowerCase() === tool)) return true
|
|
524
|
+
if (config.routeToolKeywords?.some((keyword) => tool.includes(keyword.toLowerCase()))) return true
|
|
525
|
+
return globalMcpServers.some((server) => tool.startsWith(`${server.toLowerCase()}_`))
|
|
526
|
+
}
|
|
313
527
|
|
|
314
528
|
function compactArgs(value: any): any {
|
|
315
529
|
if (Array.isArray(value)) {
|
|
@@ -342,19 +556,423 @@ export default ((input: PluginInput) => {
|
|
|
342
556
|
}
|
|
343
557
|
|
|
344
558
|
function routeSummary(): string {
|
|
345
|
-
const tools = config.routeTools?.length ? config.routeTools.join(", ") : "(none)"
|
|
346
|
-
const mcps =
|
|
347
|
-
const keywords = config.routeToolKeywords?.length ? config.routeToolKeywords.join(", ") : "(none)"
|
|
348
|
-
|
|
349
|
-
}
|
|
559
|
+
const tools = config.routeTools?.length ? config.routeTools.join(", ") : "(none)"
|
|
560
|
+
const mcps = globalMcpServers.length ? `${globalMcpServers.join(", ")} (auto-detected)` : "none detected"
|
|
561
|
+
const keywords = config.routeToolKeywords?.length ? config.routeToolKeywords.join(", ") : "(none)"
|
|
562
|
+
const duplicates = duplicateMcpServers.length ? `\n Duplicate MCP config: ${duplicateMcpServers.join(", ")} (configured globally and on-demand)` : ""
|
|
563
|
+
return `Raven routing:\n Routed tools: ${tools}\n Routed global MCPs: ${mcps}\n Routed tool keywords: ${keywords}${duplicates}`
|
|
564
|
+
}
|
|
350
565
|
|
|
351
566
|
function mcpSummary(): string {
|
|
352
|
-
|
|
567
|
+
const buckets = onDemandMcpStatusBuckets()
|
|
568
|
+
const total = buckets.loaded.length + buckets.failed.length + buckets.pending.length
|
|
569
|
+
if (!total) return `Raven on-demand MCPs (always behind Raven): none configured.\n Guidance detail: ${config.onDemandMcpDescriptionDetail ?? "full"}`
|
|
570
|
+
const lines = [
|
|
571
|
+
"Raven on-demand MCPs (always behind Raven):",
|
|
572
|
+
` Loaded: ${buckets.loaded.length ? buckets.loaded.join(", ") : "none"}`,
|
|
573
|
+
` Failed: ${buckets.failed.length ? buckets.failed.join(", ") : "none"}`,
|
|
574
|
+
` Pending: ${buckets.pending.length ? buckets.pending.join(", ") : "none"}`,
|
|
575
|
+
` Guidance detail: ${config.onDemandMcpDescriptionDetail ?? "full"}`,
|
|
576
|
+
]
|
|
577
|
+
return lines.join("\n")
|
|
353
578
|
}
|
|
354
579
|
|
|
355
580
|
function ravenAgentPrompt(): string {
|
|
356
581
|
const extra = config.ravenInstructions?.trim()
|
|
357
|
-
|
|
582
|
+
const onDemand = onDemandMcpGuidance("raven")
|
|
583
|
+
const prompt = `${ravenPrompt}\n\nOn-demand MCP guidance:\n${onDemand}\nUse direct MCP tools when they are available globally. Use raven_mcp for on-demand MCPs listed above. If you do not know the exact tool name for an on-demand MCP, call raven_mcp with operation list_tools first, then call the selected tool with operation call_tool. For call_tool and get_prompt, pass arguments as a JSON object string in argumentsJson.`
|
|
584
|
+
return extra ? `${prompt}\n\nAdditional user instructions:\n${extra}` : prompt
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function truncateText(value: unknown, max = 240): string {
|
|
588
|
+
const text = String(value ?? "").replace(/\s+/g, " ").trim()
|
|
589
|
+
return text.length > max ? `${text.slice(0, Math.max(0, max - 3))}...` : text
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function onDemandMcpGuidance(target: "delegate" | "raven" = "delegate"): string {
|
|
593
|
+
const servers = config.onDemandMcpServers ?? {}
|
|
594
|
+
const entries = Object.entries(servers)
|
|
595
|
+
if (!entries.length) return "No on-demand MCPs are configured."
|
|
596
|
+
|
|
597
|
+
const detail = config.onDemandMcpDescriptionDetail ?? "full"
|
|
598
|
+
const lines = entries.map(([name, server]) => {
|
|
599
|
+
const metadata = mcpMetadata[name] ?? {}
|
|
600
|
+
const userDescription = truncateText(server.description, 220)
|
|
601
|
+
const generated = truncateText(metadata.autoGeneratedDescription, 260)
|
|
602
|
+
const parts = [userDescription, generated ? `Discovered capabilities: ${generated}` : ""].filter(Boolean)
|
|
603
|
+
const summary = parts.length ? parts.join(" ") : "Description discovery pending."
|
|
604
|
+
if (detail === "minimized" || !metadata.autoGeneratedTools?.length) return `- ${name}: ${summary}`
|
|
605
|
+
const tools = metadata.autoGeneratedTools
|
|
606
|
+
.map((item) => `${item.name}${item.description ? ` - ${truncateText(item.description, 140)}` : ""}`)
|
|
607
|
+
.join("; ")
|
|
608
|
+
return `- ${name}: ${summary} Tools: ${tools}`
|
|
609
|
+
})
|
|
610
|
+
if (target === "raven") {
|
|
611
|
+
return `Raven can handle these on-demand MCP capabilities through raven_mcp:\n${lines.join("\n")}`
|
|
612
|
+
}
|
|
613
|
+
return `Raven can handle these on-demand MCP capabilities. Delegate matching requests with raven_seek; include the target MCP server and likely tool name when obvious, but do not call raven_mcp directly:\n${lines.join("\n")}`
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function resolveEnvPlaceholders(value: string): string {
|
|
617
|
+
return value.replace(/\{env:([A-Za-z_][A-Za-z0-9_]*)\}/g, (_match, name) => {
|
|
618
|
+
const envValue = process.env[name]
|
|
619
|
+
if (envValue === undefined) throw new Error(`Missing environment variable ${name}`)
|
|
620
|
+
return envValue
|
|
621
|
+
})
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function resolveStringRecord(value: Record<string, string> | undefined): Record<string, string> | undefined {
|
|
625
|
+
if (!value) return undefined
|
|
626
|
+
const result: Record<string, string> = {}
|
|
627
|
+
for (const [key, raw] of Object.entries(value)) {
|
|
628
|
+
result[key] = resolveEnvPlaceholders(raw)
|
|
629
|
+
}
|
|
630
|
+
return result
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function cleanProcessEnv(): Record<string, string> {
|
|
634
|
+
const result: Record<string, string> = {}
|
|
635
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
636
|
+
if (typeof value === "string") result[key] = value
|
|
637
|
+
}
|
|
638
|
+
return result
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
async function withTimeout<T>(promise: Promise<T>, ms: number, label: string): Promise<T> {
|
|
642
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
643
|
+
try {
|
|
644
|
+
return await Promise.race([
|
|
645
|
+
promise,
|
|
646
|
+
new Promise<never>((_, reject) => {
|
|
647
|
+
timer = setTimeout(() => reject(new Error(`${label} timed out after ${Math.round(ms / 1000)}s`)), ms)
|
|
648
|
+
}),
|
|
649
|
+
])
|
|
650
|
+
} finally {
|
|
651
|
+
if (timer) clearTimeout(timer)
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
type McpConnection = {
|
|
656
|
+
client: Client
|
|
657
|
+
transport: any
|
|
658
|
+
serverName: string
|
|
659
|
+
lastUsed: number
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
const mcpConnections = new Map<string, McpConnection>()
|
|
663
|
+
|
|
664
|
+
async function closeMcpConnection(connection: McpConnection) {
|
|
665
|
+
try {
|
|
666
|
+
if (typeof connection.transport?.terminateSession === "function") {
|
|
667
|
+
await connection.transport.terminateSession()
|
|
668
|
+
}
|
|
669
|
+
} catch { /* best-effort */ }
|
|
670
|
+
try {
|
|
671
|
+
await connection.client.close()
|
|
672
|
+
} catch { /* best-effort */ }
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
async function closeMcpConnectionKey(key: string) {
|
|
676
|
+
const existing = mcpConnections.get(key)
|
|
677
|
+
if (!existing) return
|
|
678
|
+
mcpConnections.delete(key)
|
|
679
|
+
await closeMcpConnection(existing)
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function closeIdleMcpConnections(maxAgeMs = 10 * 60 * 1000) {
|
|
683
|
+
const now = Date.now()
|
|
684
|
+
for (const [key, connection] of mcpConnections) {
|
|
685
|
+
if (now - connection.lastUsed > maxAgeMs) {
|
|
686
|
+
void closeMcpConnectionKey(key)
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function mcpConnectionKey(sessionId: string, serverName: string): string {
|
|
692
|
+
return `${sessionId}:${serverName.toLowerCase()}`
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function getOnDemandMcpServer(serverName: string): [string, OnDemandMcpServer] | undefined {
|
|
696
|
+
const entries = Object.entries(config.onDemandMcpServers ?? {})
|
|
697
|
+
return entries.find(([name]) => name.toLowerCase() === serverName.toLowerCase())
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
async function connectOnDemandMcp(serverName: string, server: OnDemandMcpServer): Promise<McpConnection> {
|
|
701
|
+
const timeout = (config.timeout ?? 180) * 1000
|
|
702
|
+
const client = new Client({ name: PACKAGE_NAME, version: PACKAGE_VERSION })
|
|
703
|
+
|
|
704
|
+
if (server.type === "stdio") {
|
|
705
|
+
const env = { ...cleanProcessEnv(), ...resolveStringRecord(server.env) }
|
|
706
|
+
const transport = new StdioClientTransport({
|
|
707
|
+
command: resolveEnvPlaceholders(server.command),
|
|
708
|
+
args: server.args?.map((arg) => resolveEnvPlaceholders(arg)),
|
|
709
|
+
env,
|
|
710
|
+
cwd: server.cwd ? resolveEnvPlaceholders(server.cwd) : undefined,
|
|
711
|
+
})
|
|
712
|
+
await withTimeout(client.connect(transport), timeout, `Connecting to ${serverName}`)
|
|
713
|
+
return { client, transport, serverName, lastUsed: Date.now() }
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
const url = new URL(resolveEnvPlaceholders(server.url))
|
|
717
|
+
const headers = resolveStringRecord(server.headers)
|
|
718
|
+
try {
|
|
719
|
+
const transport = new StreamableHTTPClientTransport(url, headers ? { requestInit: { headers } } : undefined)
|
|
720
|
+
await withTimeout(client.connect(transport), timeout, `Connecting to ${serverName}`)
|
|
721
|
+
return { client, transport, serverName, lastUsed: Date.now() }
|
|
722
|
+
} catch (err) {
|
|
723
|
+
try { await client.close() } catch { /* best-effort */ }
|
|
724
|
+
const fallbackClient = new Client({ name: PACKAGE_NAME, version: PACKAGE_VERSION })
|
|
725
|
+
const fallback = new SSEClientTransport(url, headers ? { requestInit: { headers } } : undefined)
|
|
726
|
+
await withTimeout(fallbackClient.connect(fallback), timeout, `Connecting to ${serverName} via SSE`)
|
|
727
|
+
return { client: fallbackClient, transport: fallback, serverName, lastUsed: Date.now() }
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
async function getMcpConnection(sessionId: string, serverName: string): Promise<McpConnection> {
|
|
732
|
+
closeIdleMcpConnections()
|
|
733
|
+
const match = getOnDemandMcpServer(serverName)
|
|
734
|
+
if (!match) throw new Error(`Unknown on-demand MCP server: ${serverName}`)
|
|
735
|
+
const [canonicalName, server] = match
|
|
736
|
+
const key = mcpConnectionKey(sessionId, canonicalName)
|
|
737
|
+
const existing = mcpConnections.get(key)
|
|
738
|
+
if (existing) {
|
|
739
|
+
existing.lastUsed = Date.now()
|
|
740
|
+
return existing
|
|
741
|
+
}
|
|
742
|
+
const connection = await connectOnDemandMcp(canonicalName, server)
|
|
743
|
+
mcpConnections.set(key, connection)
|
|
744
|
+
return connection
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
async function listAllMcpTools(client: Client): Promise<any[]> {
|
|
748
|
+
const tools: any[] = []
|
|
749
|
+
let cursor: string | undefined
|
|
750
|
+
do {
|
|
751
|
+
const result: any = await client.listTools(cursor ? { cursor } : undefined as any)
|
|
752
|
+
tools.push(...(result.tools ?? []))
|
|
753
|
+
cursor = result.nextCursor
|
|
754
|
+
} while (cursor)
|
|
755
|
+
return tools
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
async function listAllMcpResources(client: Client): Promise<any[]> {
|
|
759
|
+
const resources: any[] = []
|
|
760
|
+
let cursor: string | undefined
|
|
761
|
+
do {
|
|
762
|
+
const result: any = await client.listResources(cursor ? { cursor } : undefined as any)
|
|
763
|
+
resources.push(...(result.resources ?? []))
|
|
764
|
+
cursor = result.nextCursor
|
|
765
|
+
} while (cursor)
|
|
766
|
+
return resources
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
async function listAllMcpPrompts(client: Client): Promise<any[]> {
|
|
770
|
+
const prompts: any[] = []
|
|
771
|
+
let cursor: string | undefined
|
|
772
|
+
do {
|
|
773
|
+
const result: any = await client.listPrompts(cursor ? { cursor } : undefined as any)
|
|
774
|
+
prompts.push(...(result.prompts ?? []))
|
|
775
|
+
cursor = result.nextCursor
|
|
776
|
+
} while (cursor)
|
|
777
|
+
return prompts
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function generatedToolsFromMcpTools(tools: any[]): OnDemandMcpToolInfo[] {
|
|
781
|
+
return tools
|
|
782
|
+
.filter((item) => item?.name)
|
|
783
|
+
.map((item) => ({
|
|
784
|
+
name: String(item.name),
|
|
785
|
+
...(item.description ? { description: truncateText(item.description, 500) } : {}),
|
|
786
|
+
}))
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function estimateAvoidedMcpSchemaBytes(tools: any[]): number {
|
|
790
|
+
if (!tools.length) return 0
|
|
791
|
+
return JSON.stringify(tools).length
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function synthesizeMcpDescription(serverName: string, tools: OnDemandMcpToolInfo[]): string {
|
|
795
|
+
if (!tools.length) return `${serverName} exposes on-demand MCP capabilities. Use list_tools for available operations.`
|
|
796
|
+
const descriptions = tools.map((item) => item.description || item.name).filter(Boolean)
|
|
797
|
+
const joined = descriptions.slice(0, 6).join("; ")
|
|
798
|
+
return truncateText(`${serverName} provides on-demand MCP tools for ${joined}.`, 300)
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
async function refreshOnDemandMcpDescriptions(force = false, serverName?: string): Promise<{ refreshed: string[]; failed: string[] }> {
|
|
802
|
+
const entries = Object.entries(config.onDemandMcpServers ?? {})
|
|
803
|
+
.filter(([name]) => !serverName || name.toLowerCase() === serverName.toLowerCase())
|
|
804
|
+
.filter(([name]) => {
|
|
805
|
+
const metadata = mcpMetadata[name]
|
|
806
|
+
return force || metadata?.status === "pending" || metadata?.status === "failed" || !metadata?.autoGeneratedDescription || !metadata.autoGeneratedTools?.length || metadata.avoidedSchemaBytes === undefined
|
|
807
|
+
})
|
|
808
|
+
const refreshed: string[] = []
|
|
809
|
+
const failed: string[] = []
|
|
810
|
+
if (!entries.length) return { refreshed, failed }
|
|
811
|
+
let changed = false
|
|
812
|
+
for (const [name, server] of entries) {
|
|
813
|
+
let connection: McpConnection | undefined
|
|
814
|
+
const checkedAt = new Date().toISOString()
|
|
815
|
+
try {
|
|
816
|
+
connection = await connectOnDemandMcp(name, server)
|
|
817
|
+
const rawTools = await listAllMcpTools(connection.client)
|
|
818
|
+
const tools = generatedToolsFromMcpTools(rawTools)
|
|
819
|
+
mcpMetadata[name] = {
|
|
820
|
+
autoGeneratedTools: tools,
|
|
821
|
+
autoGeneratedDescription: synthesizeMcpDescription(name, tools),
|
|
822
|
+
avoidedSchemaBytes: estimateAvoidedMcpSchemaBytes(rawTools),
|
|
823
|
+
status: "ready",
|
|
824
|
+
lastCheckedAt: checkedAt,
|
|
825
|
+
}
|
|
826
|
+
changed = true
|
|
827
|
+
refreshed.push(name)
|
|
828
|
+
} catch (err: any) {
|
|
829
|
+
mcpMetadata[name] = {
|
|
830
|
+
...(mcpMetadata[name] ?? {}),
|
|
831
|
+
status: "failed",
|
|
832
|
+
lastError: truncateText(err?.message ?? err, 500),
|
|
833
|
+
lastCheckedAt: checkedAt,
|
|
834
|
+
}
|
|
835
|
+
changed = true
|
|
836
|
+
failed.push(name)
|
|
837
|
+
// Keep startup/discovery best-effort; /raven mcp exposes failures.
|
|
838
|
+
} finally {
|
|
839
|
+
if (connection) await closeMcpConnection(connection)
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
if (changed) {
|
|
843
|
+
saveMcpMetadata(mcpMetadata)
|
|
844
|
+
saveDynamicGuidance()
|
|
845
|
+
}
|
|
846
|
+
return { refreshed, failed }
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function scheduleOnDemandMcpRefresh(): Promise<{ refreshed: string[]; failed: string[] }> {
|
|
850
|
+
if (mcpRefreshScheduled && mcpRefreshPromise) return mcpRefreshPromise
|
|
851
|
+
mcpRefreshScheduled = true
|
|
852
|
+
mcpRefreshPromise = new Promise((resolve) => {
|
|
853
|
+
setTimeout(() => {
|
|
854
|
+
void refreshOnDemandMcpDescriptions().then(resolve)
|
|
855
|
+
}, 1000)
|
|
856
|
+
})
|
|
857
|
+
return mcpRefreshPromise
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
function onDemandMcpRuntimeStatus(name: string): "pending" | "ready" | "failed" {
|
|
861
|
+
const metadata = mcpMetadata[name]
|
|
862
|
+
if (metadata?.status) return metadata.status
|
|
863
|
+
return "pending"
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
function displayMcpStatus(status: "pending" | "ready" | "failed"): "pending" | "loaded" | "failed" {
|
|
867
|
+
return status === "ready" ? "loaded" : status
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
function onDemandMcpStatusBuckets(): { loaded: string[]; failed: string[]; pending: string[] } {
|
|
871
|
+
const buckets = { loaded: [] as string[], failed: [] as string[], pending: [] as string[] }
|
|
872
|
+
for (const name of Object.keys(config.onDemandMcpServers ?? {})) {
|
|
873
|
+
const status = onDemandMcpRuntimeStatus(name)
|
|
874
|
+
if (status === "ready") buckets.loaded.push(name)
|
|
875
|
+
else if (status === "failed") buckets.failed.push(name)
|
|
876
|
+
else buckets.pending.push(name)
|
|
877
|
+
}
|
|
878
|
+
return buckets
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
function failedOnDemandMcpNames(names?: string[]): string[] {
|
|
882
|
+
const configured = Object.keys(config.onDemandMcpServers ?? {})
|
|
883
|
+
const wanted = names?.length ? names : configured
|
|
884
|
+
return wanted.filter((name) => configured.includes(name) && onDemandMcpRuntimeStatus(name) === "failed")
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
function setOnDemandMcpHealth(serverName: string, status: "ready" | "failed", error?: unknown) {
|
|
888
|
+
const match = getOnDemandMcpServer(serverName)
|
|
889
|
+
if (!match) return undefined
|
|
890
|
+
const [name] = match
|
|
891
|
+
const next: OnDemandMcpMetadata = {
|
|
892
|
+
...(mcpMetadata[name] ?? {}),
|
|
893
|
+
status,
|
|
894
|
+
lastCheckedAt: new Date().toISOString(),
|
|
895
|
+
}
|
|
896
|
+
if (status === "failed") next.lastError = truncateText(error, 500)
|
|
897
|
+
else delete next.lastError
|
|
898
|
+
mcpMetadata[name] = next
|
|
899
|
+
saveMcpMetadata(mcpMetadata)
|
|
900
|
+
return name
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
function onDemandMcpStatus(): string {
|
|
904
|
+
const entries = Object.entries(config.onDemandMcpServers ?? {})
|
|
905
|
+
if (!entries.length) return "Raven on-demand MCPs: none configured."
|
|
906
|
+
const buckets = onDemandMcpStatusBuckets()
|
|
907
|
+
const lines = entries.map(([name, server]) => {
|
|
908
|
+
const metadata = mcpMetadata[name]
|
|
909
|
+
const source = server.description ? "user description" : "no user description"
|
|
910
|
+
const generated = metadata?.autoGeneratedDescription ? "generated" : "missing generated description"
|
|
911
|
+
const tools = metadata?.autoGeneratedTools?.length ? `${metadata.autoGeneratedTools.length} tools` : "tools pending"
|
|
912
|
+
const status = onDemandMcpRuntimeStatus(name)
|
|
913
|
+
const avoided = status === "ready" && metadata?.avoidedSchemaBytes ? `, ${formatBytes(metadata.avoidedSchemaBytes)} avoided schema` : ""
|
|
914
|
+
const error = status === "failed" && metadata?.lastError ? `, error: ${metadata.lastError}` : ""
|
|
915
|
+
return `- ${name} (${server.type}): ${displayMcpStatus(status)}, ${source}, ${generated}, ${tools}${avoided}${error}`
|
|
916
|
+
})
|
|
917
|
+
return `Raven on-demand MCPs (${config.onDemandMcpDescriptionDetail ?? "full"} guidance):\n Loaded: ${buckets.loaded.length ? buckets.loaded.join(", ") : "none"}\n Failed: ${buckets.failed.length ? buckets.failed.join(", ") : "none"}\n Pending: ${buckets.pending.length ? buckets.pending.join(", ") : "none"}\n${lines.join("\n")}`
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
function avoidedMcpSchemaBytes(): number {
|
|
921
|
+
return Object.keys(config.onDemandMcpServers ?? {}).reduce((sum, name) => {
|
|
922
|
+
const metadata = mcpMetadata[name]
|
|
923
|
+
if (onDemandMcpRuntimeStatus(name) !== "ready") return sum
|
|
924
|
+
return sum + Math.max(0, metadata?.avoidedSchemaBytes ?? 0)
|
|
925
|
+
}, 0)
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
function statsText(): string {
|
|
929
|
+
const avoided = avoidedMcpSchemaBytes()
|
|
930
|
+
return `Raven context saved:\n Avoided MCP schema load: ${formatBytes(avoided)} (~${formatTokens(avoided)} context)\n This session: ${formatBytes(sessionBytes)} (~${formatTokens(sessionBytes)} context)\n All time: ${formatBytes(totalBytes)} (~${formatTokens(totalBytes)} context)`
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
function helpText(): string {
|
|
934
|
+
return `Raven commands:\n /raven help — show this help\n /raven on — enable tool/MCP routing\n /raven off — disable tool/MCP routing\n /raven route — show or edit routed tools/keywords\n /raven mcp — show or refresh on-demand MCP metadata\n /raven update — check npm, clear plugin cache if newer, then restart opencode\n /raven model <name> — change Raven's model (requires restart)\n /raven effort <value> — change Raven's reasoning effort (requires restart)\n /raven timeout <seconds> — change raven_seek timeout\n /raven stats — show context saved`
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
function formatMcpList(title: string, items: any[], fields: string[] = ["name", "description"]): string {
|
|
938
|
+
if (!items.length) return `${title}: none found.`
|
|
939
|
+
const lines = items.map((item) => {
|
|
940
|
+
const primary = fields.map((field) => item?.[field]).find(Boolean) ?? JSON.stringify(item)
|
|
941
|
+
const description = item?.description && primary !== item.description ? ` - ${truncateText(item.description, 300)}` : ""
|
|
942
|
+
return `- ${truncateText(primary, 180)}${description}`
|
|
943
|
+
})
|
|
944
|
+
return `${title}:\n${lines.join("\n")}`
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
function formatMcpToolsForRaven(serverName: string, tools: any[]): string {
|
|
948
|
+
if (!tools.length) return `Tools for ${serverName}: none found.`
|
|
949
|
+
const lines = tools.map((item) => {
|
|
950
|
+
const description = item?.description ? `\n Description: ${truncateText(item.description, 600)}` : ""
|
|
951
|
+
const schema = item?.inputSchema ? `\n Input schema: ${truncateText(JSON.stringify(item.inputSchema), 3000)}` : ""
|
|
952
|
+
return `- ${item?.name ?? "(unnamed)"}${description}${schema}`
|
|
953
|
+
})
|
|
954
|
+
return `Tools for ${serverName}:\n${lines.join("\n")}`
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
function formatMcpResult(result: any): string {
|
|
958
|
+
if (!result) return "No result."
|
|
959
|
+
if (Array.isArray(result.content)) {
|
|
960
|
+
const parts = result.content.map((part: any) => {
|
|
961
|
+
if (part?.type === "text") return part.text
|
|
962
|
+
return JSON.stringify(part)
|
|
963
|
+
})
|
|
964
|
+
return parts.join("\n") || JSON.stringify(result)
|
|
965
|
+
}
|
|
966
|
+
return typeof result === "string" ? result : JSON.stringify(result, null, 2)
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
function parseMcpArguments(raw: string | undefined): Record<string, any> {
|
|
970
|
+
if (!raw?.trim()) return {}
|
|
971
|
+
const parsed = JSON.parse(raw)
|
|
972
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
973
|
+
throw new Error("argumentsJson must be a JSON object")
|
|
974
|
+
}
|
|
975
|
+
return parsed
|
|
358
976
|
}
|
|
359
977
|
|
|
360
978
|
// ── Context saved by Raven delegation ──
|
|
@@ -496,34 +1114,58 @@ export default ((input: PluginInput) => {
|
|
|
496
1114
|
} catch { /* update checks are best-effort */ }
|
|
497
1115
|
}
|
|
498
1116
|
|
|
499
|
-
function
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
1117
|
+
async function notifyIfMcpFailures(names?: string[]) {
|
|
1118
|
+
try {
|
|
1119
|
+
const failed = failedOnDemandMcpNames(names)
|
|
1120
|
+
if (!failed.length) return
|
|
1121
|
+
const key = failed.sort().join("|")
|
|
1122
|
+
if (key === lastNotifiedMcpFailureKey) return
|
|
1123
|
+
lastNotifiedMcpFailureKey = key
|
|
1124
|
+
const label = failed.length === 1 ? failed[0] : `${failed.length} on-demand MCPs`
|
|
1125
|
+
await (client as any).tui?.showToast?.({
|
|
1126
|
+
body: {
|
|
1127
|
+
title: "Raven MCP warning",
|
|
1128
|
+
message: `${label} failed to load. Run /raven mcp for details.`,
|
|
1129
|
+
variant: "warning",
|
|
1130
|
+
duration: 12000,
|
|
1131
|
+
},
|
|
1132
|
+
})
|
|
1133
|
+
} catch { /* MCP failure notifications are best-effort */ }
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
async function notifyIfDuplicateMcps() {
|
|
1137
|
+
try {
|
|
1138
|
+
if (!duplicateMcpServers.length) return
|
|
1139
|
+
const label = duplicateMcpServers.length === 1 ? duplicateMcpServers[0] : `${duplicateMcpServers.length} MCPs`
|
|
1140
|
+
await (client as any).tui?.showToast?.({
|
|
1141
|
+
body: {
|
|
1142
|
+
title: "Raven MCP duplicate config",
|
|
1143
|
+
message: `${label} configured both globally and on-demand. Prefer one location to avoid duplicate schema/connection behavior.`,
|
|
1144
|
+
variant: "warning",
|
|
1145
|
+
duration: 12000,
|
|
1146
|
+
},
|
|
1147
|
+
})
|
|
1148
|
+
} catch { /* duplicate MCP notifications are best-effort */ }
|
|
510
1149
|
}
|
|
511
1150
|
|
|
512
1151
|
return {
|
|
513
1152
|
config(configInput: any) {
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
}
|
|
520
|
-
}
|
|
1153
|
+
globalMcpServers = uniqueStrings(Object.entries(configInput.mcp ?? {})
|
|
1154
|
+
.filter(([, value]) => !value || typeof value !== "object" || (value as any).enabled !== false)
|
|
1155
|
+
.map(([name]) => name))
|
|
1156
|
+
const onDemandNames = new Set(Object.keys(config.onDemandMcpServers ?? {}).map((name) => name.toLowerCase()))
|
|
1157
|
+
duplicateMcpServers = globalMcpServers.filter((name) => onDemandNames.has(name.toLowerCase()))
|
|
521
1158
|
|
|
522
1159
|
// Inject MCP guidance as a startup instruction file (absolute path for npm compat)
|
|
1160
|
+
saveDynamicGuidance()
|
|
1161
|
+
cleanupOldRootFiles()
|
|
523
1162
|
configInput.instructions = configInput.instructions || []
|
|
524
1163
|
if (!configInput.instructions.includes(MCP_GUIDANCE_MD)) {
|
|
525
1164
|
configInput.instructions.push(MCP_GUIDANCE_MD)
|
|
526
1165
|
}
|
|
1166
|
+
if (!configInput.instructions.includes(dynamicGuidanceFile)) {
|
|
1167
|
+
configInput.instructions.push(dynamicGuidanceFile)
|
|
1168
|
+
}
|
|
527
1169
|
|
|
528
1170
|
// Register Raven from Raven.md, with config file overrides
|
|
529
1171
|
configInput.agent = configInput.agent || {}
|
|
@@ -544,18 +1186,21 @@ export default ((input: PluginInput) => {
|
|
|
544
1186
|
configInput.command = configInput.command || {}
|
|
545
1187
|
if (!configInput.command.raven) {
|
|
546
1188
|
configInput.command.raven = {
|
|
547
|
-
template: "Manage Raven: /raven on|off|route|update|model <name>|status",
|
|
548
|
-
description: "Toggle Raven routing, manage routed
|
|
1189
|
+
template: "Manage Raven: /raven on|off|route|mcp|update|model <name>|status",
|
|
1190
|
+
description: "Toggle Raven routing, manage routed/on-demand MCPs, or change Raven's model",
|
|
549
1191
|
}
|
|
550
1192
|
}
|
|
551
1193
|
|
|
1194
|
+
scheduleOnDemandMcpRefresh()
|
|
552
1195
|
updateToastPending = true
|
|
1196
|
+
mcpFailureToastPending = true
|
|
1197
|
+
duplicateMcpToastPending = duplicateMcpServers.length > 0
|
|
553
1198
|
},
|
|
554
1199
|
|
|
555
1200
|
// Register raven_seek tool — lets agents with task:false still delegate through Raven
|
|
556
1201
|
tool: {
|
|
557
1202
|
"raven_seek": tool({
|
|
558
|
-
description: "Unified Raven delegation tool. Use this whenever a tool/MCP is blocked by Raven, or when grep, glob, WebFetch/fetch, websearch, docs lookup, GitHub search, or search-like bash would be used. Handles routed MCP requests, local codebase search, filesystem discovery, specific URL/page reads, web/docs research, GitHub examples, and command-output/system inspection via Raven.",
|
|
1203
|
+
description: "Unified Raven delegation tool. Use this whenever a tool/MCP is blocked by Raven, or when grep, glob, WebFetch/fetch, websearch, docs lookup, GitHub search, on-demand MCP capabilities, or search-like bash would be used. Handles routed MCP requests, on-demand MCPs, local codebase search, filesystem discovery, specific URL/page reads, web/docs research, GitHub examples, and command-output/system inspection via Raven.",
|
|
559
1204
|
args: {
|
|
560
1205
|
query: tool.schema.string().describe("What Raven should do. Include the original blocked tool/MCP request and relevant args, exact URLs when replacing WebFetch, and commands/output checks when replacing grep/rg/head over command output."),
|
|
561
1206
|
},
|
|
@@ -641,6 +1286,85 @@ export default ((input: PluginInput) => {
|
|
|
641
1286
|
}
|
|
642
1287
|
},
|
|
643
1288
|
}),
|
|
1289
|
+
"raven_mcp": tool({
|
|
1290
|
+
description: "Raven-only bridge for on-demand MCP servers. Non-Raven agents must not call this directly; use raven_seek instead. Raven uses this to list and call tools/resources/prompts from MCPs configured in Raven's onDemandMcpServers without exposing their full schemas to the main model.",
|
|
1291
|
+
args: {
|
|
1292
|
+
server: tool.schema.string().describe("On-demand MCP server name from Raven config, e.g. context7, exa, grep_app, unityMCP."),
|
|
1293
|
+
operation: tool.schema.enum(["list_tools", "call_tool", "list_resources", "read_resource", "list_prompts", "get_prompt"]).describe("MCP operation to perform."),
|
|
1294
|
+
name: tool.schema.string().optional().describe("Tool name for call_tool, resource URI for read_resource, or prompt name for get_prompt."),
|
|
1295
|
+
argumentsJson: tool.schema.string().optional().describe("JSON object string with arguments for call_tool or get_prompt, e.g. {\"query\":\"latest AI news\"}. Omit for no arguments."),
|
|
1296
|
+
},
|
|
1297
|
+
async execute(args, context) {
|
|
1298
|
+
if (!ravenSessions.has(context.sessionID) && sessionAgents.get(context.sessionID) !== "raven") {
|
|
1299
|
+
return { title: "Raven MCP", output: "raven_mcp is only available inside Raven. Use raven_seek for MCP requests." }
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
const operation = args.operation as RavenMcpOperation
|
|
1303
|
+
const timeout = (config.timeout ?? 180) * 1000
|
|
1304
|
+
let connected = false
|
|
1305
|
+
try {
|
|
1306
|
+
const connection = await getMcpConnection(context.sessionID, args.server)
|
|
1307
|
+
connected = true
|
|
1308
|
+
setOnDemandMcpHealth(args.server, "ready")
|
|
1309
|
+
connection.lastUsed = Date.now()
|
|
1310
|
+
const client = connection.client
|
|
1311
|
+
|
|
1312
|
+
if (operation === "list_tools") {
|
|
1313
|
+
const tools = await withTimeout(listAllMcpTools(client), timeout, `Listing ${args.server} tools`)
|
|
1314
|
+
const match = getOnDemandMcpServer(args.server)
|
|
1315
|
+
if (match) {
|
|
1316
|
+
const [name] = match
|
|
1317
|
+
const generatedTools = generatedToolsFromMcpTools(tools)
|
|
1318
|
+
mcpMetadata[name] = {
|
|
1319
|
+
autoGeneratedTools: generatedTools,
|
|
1320
|
+
autoGeneratedDescription: mcpMetadata[name]?.autoGeneratedDescription || synthesizeMcpDescription(name, generatedTools),
|
|
1321
|
+
avoidedSchemaBytes: estimateAvoidedMcpSchemaBytes(tools),
|
|
1322
|
+
status: "ready",
|
|
1323
|
+
lastCheckedAt: new Date().toISOString(),
|
|
1324
|
+
}
|
|
1325
|
+
saveMcpMetadata(mcpMetadata)
|
|
1326
|
+
}
|
|
1327
|
+
return { title: `Raven MCP: ${args.server}`, output: formatMcpToolsForRaven(args.server, tools) }
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
if (operation === "call_tool") {
|
|
1331
|
+
if (!args.name) return { title: `Raven MCP: ${args.server}`, output: "call_tool requires name." }
|
|
1332
|
+
const result = await withTimeout(client.callTool({ name: args.name, arguments: parseMcpArguments(args.argumentsJson) }), timeout, `Calling ${args.server}.${args.name}`)
|
|
1333
|
+
return { title: `Raven MCP: ${args.server}.${args.name}`, output: formatMcpResult(result) }
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
if (operation === "list_resources") {
|
|
1337
|
+
const resources = await withTimeout(listAllMcpResources(client), timeout, `Listing ${args.server} resources`)
|
|
1338
|
+
return { title: `Raven MCP: ${args.server}`, output: formatMcpList(`Resources for ${args.server}`, resources, ["uri", "name", "description"]) }
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
if (operation === "read_resource") {
|
|
1342
|
+
if (!args.name) return { title: `Raven MCP: ${args.server}`, output: "read_resource requires name as the resource URI." }
|
|
1343
|
+
const result = await withTimeout(client.readResource({ uri: args.name }), timeout, `Reading ${args.server} resource`)
|
|
1344
|
+
return { title: `Raven MCP: ${args.server}`, output: formatMcpResult(result) }
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
if (operation === "list_prompts") {
|
|
1348
|
+
const prompts = await withTimeout(listAllMcpPrompts(client), timeout, `Listing ${args.server} prompts`)
|
|
1349
|
+
return { title: `Raven MCP: ${args.server}`, output: formatMcpList(`Prompts for ${args.server}`, prompts) }
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
if (operation === "get_prompt") {
|
|
1353
|
+
if (!args.name) return { title: `Raven MCP: ${args.server}`, output: "get_prompt requires name." }
|
|
1354
|
+
const result = await withTimeout(client.getPrompt({ name: args.name, arguments: parseMcpArguments(args.argumentsJson) }), timeout, `Getting ${args.server} prompt`)
|
|
1355
|
+
return { title: `Raven MCP: ${args.server}.${args.name}`, output: formatMcpResult(result) }
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
return { title: `Raven MCP: ${args.server}`, output: `Unsupported operation: ${operation}` }
|
|
1359
|
+
} catch (err: any) {
|
|
1360
|
+
if (!connected) {
|
|
1361
|
+
const name = setOnDemandMcpHealth(args.server, "failed", err?.message ?? err)
|
|
1362
|
+
if (name) await notifyIfMcpFailures([name])
|
|
1363
|
+
}
|
|
1364
|
+
return { title: `Raven MCP: ${args.server}`, output: `MCP call failed: ${err?.message ?? err}` }
|
|
1365
|
+
}
|
|
1366
|
+
},
|
|
1367
|
+
}),
|
|
644
1368
|
},
|
|
645
1369
|
|
|
646
1370
|
// Track agent ↔ session mapping for allowlist + Raven exclusion
|
|
@@ -660,12 +1384,21 @@ export default ((input: PluginInput) => {
|
|
|
660
1384
|
ravenSessionParents.set(evt.properties.parentID, evt.properties.id)
|
|
661
1385
|
}
|
|
662
1386
|
|
|
663
|
-
if (
|
|
664
|
-
|
|
665
|
-
|
|
1387
|
+
if (updateToastPending) {
|
|
1388
|
+
updateToastPending = false
|
|
1389
|
+
setTimeout(() => void notifyIfUpdateAvailable(), 500)
|
|
1390
|
+
}
|
|
1391
|
+
if (mcpFailureToastPending) {
|
|
1392
|
+
mcpFailureToastPending = false
|
|
1393
|
+
void scheduleOnDemandMcpRefresh().then((result) => notifyIfMcpFailures(result.failed))
|
|
1394
|
+
}
|
|
1395
|
+
if (duplicateMcpToastPending) {
|
|
1396
|
+
duplicateMcpToastPending = false
|
|
1397
|
+
setTimeout(() => void notifyIfDuplicateMcps(), 700)
|
|
1398
|
+
}
|
|
666
1399
|
},
|
|
667
1400
|
|
|
668
|
-
// /raven on|off|route|model <name>|effort <value>|timeout <seconds>|stats|status
|
|
1401
|
+
// /raven help|on|off|route|mcp|model <name>|effort <value>|timeout <seconds>|stats|status
|
|
669
1402
|
async "command.execute.before"(input: any, output: any) {
|
|
670
1403
|
if (input.command !== "raven") return
|
|
671
1404
|
output.parts.length = 0
|
|
@@ -676,23 +1409,25 @@ export default ((input: PluginInput) => {
|
|
|
676
1409
|
config.enabled = true
|
|
677
1410
|
saveConfig(config)
|
|
678
1411
|
output.parts.push({ type: "text", text: "Raven tool/MCP routing enabled. Non-Raven agents will be redirected to raven_seek for configured tools." })
|
|
1412
|
+
} else if (arg === "help") {
|
|
1413
|
+
output.parts.push({ type: "text", text: helpText() })
|
|
679
1414
|
} else if (arg === "off") {
|
|
680
1415
|
config.enabled = false
|
|
681
1416
|
saveConfig(config)
|
|
682
1417
|
output.parts.push({ type: "text", text: "Raven tool/MCP routing disabled. All agents can use tools directly." })
|
|
683
1418
|
} else if (arg === "stats") {
|
|
684
|
-
output.parts.push({ type: "text", text:
|
|
1419
|
+
output.parts.push({ type: "text", text: statsText() })
|
|
685
1420
|
} else if (arg === "route") {
|
|
686
|
-
output.parts.push({ type: "text", text: `${routeSummary()}\n\nUsage:\n /raven route tool add <tool_name>\n /raven route tool remove <tool_name>\n /raven route
|
|
687
|
-
} else if (arg.startsWith("route ")) {
|
|
688
|
-
const parts = raw.split(/\s+/)
|
|
689
|
-
const kind = parts[1]?.toLowerCase()
|
|
690
|
-
const action = parts[2]?.toLowerCase()
|
|
691
|
-
const name = parts.slice(3).join(" ").trim()
|
|
692
|
-
const key = kind === "tool" ? "routeTools" : kind === "
|
|
693
|
-
|
|
694
|
-
if (!key || !["add", "remove", "rm"].includes(action) || !name) {
|
|
695
|
-
output.parts.push({ type: "text", text: "Usage:\n /raven route tool add <tool_name>\n /raven route tool remove <tool_name>\n /raven route
|
|
1421
|
+
output.parts.push({ type: "text", text: `${routeSummary()}\n\nUsage:\n /raven route tool add <tool_name>\n /raven route tool remove <tool_name>\n /raven route keyword add <keyword>\n /raven route keyword remove <keyword>\n\nGlobal MCPs are auto-routed by detected OpenCode MCP server name.` })
|
|
1422
|
+
} else if (arg.startsWith("route ")) {
|
|
1423
|
+
const parts = raw.split(/\s+/)
|
|
1424
|
+
const kind = parts[1]?.toLowerCase()
|
|
1425
|
+
const action = parts[2]?.toLowerCase()
|
|
1426
|
+
const name = parts.slice(3).join(" ").trim()
|
|
1427
|
+
const key = kind === "tool" ? "routeTools" : kind === "keyword" ? "routeToolKeywords" : undefined
|
|
1428
|
+
|
|
1429
|
+
if (!key || !["add", "remove", "rm"].includes(action) || !name) {
|
|
1430
|
+
output.parts.push({ type: "text", text: "Usage:\n /raven route tool add <tool_name>\n /raven route tool remove <tool_name>\n /raven route keyword add <keyword>\n /raven route keyword remove <keyword>\n\nGlobal MCPs are auto-routed by detected OpenCode MCP server name." })
|
|
696
1431
|
} else {
|
|
697
1432
|
const values = uniqueStrings(config[key])
|
|
698
1433
|
const exists = values.some((value) => value.toLowerCase() === name.toLowerCase())
|
|
@@ -702,6 +1437,31 @@ export default ((input: PluginInput) => {
|
|
|
702
1437
|
saveConfig(config)
|
|
703
1438
|
output.parts.push({ type: "text", text: routeSummary() })
|
|
704
1439
|
}
|
|
1440
|
+
} else if (arg === "mcp") {
|
|
1441
|
+
output.parts.push({ type: "text", text: `${onDemandMcpStatus()}\n\nUsage:\n /raven mcp refresh [server_name]\n /raven mcp detail full\n /raven mcp detail minimized` })
|
|
1442
|
+
} else if (arg.startsWith("mcp ")) {
|
|
1443
|
+
const parts = raw.split(/\s+/)
|
|
1444
|
+
const action = parts[1]?.toLowerCase()
|
|
1445
|
+
if (action === "refresh") {
|
|
1446
|
+
const serverName = parts.slice(2).join(" ").trim() || undefined
|
|
1447
|
+
const result = await refreshOnDemandMcpDescriptions(true, serverName)
|
|
1448
|
+
await notifyIfMcpFailures(result.failed)
|
|
1449
|
+
output.parts.push({
|
|
1450
|
+
type: "text",
|
|
1451
|
+
text: `On-demand MCP refresh complete.\n Refreshed: ${result.refreshed.length ? result.refreshed.join(", ") : "none"}\n Failed: ${result.failed.length ? result.failed.join(", ") : "none"}`,
|
|
1452
|
+
})
|
|
1453
|
+
} else if (action === "detail") {
|
|
1454
|
+
const detail = parts[2]?.toLowerCase()
|
|
1455
|
+
if (detail !== "full" && detail !== "minimized") {
|
|
1456
|
+
output.parts.push({ type: "text", text: `Usage: /raven mcp detail full|minimized\nCurrent: ${config.onDemandMcpDescriptionDetail ?? "full"}` })
|
|
1457
|
+
} else {
|
|
1458
|
+
config.onDemandMcpDescriptionDetail = detail
|
|
1459
|
+
saveConfig(config)
|
|
1460
|
+
output.parts.push({ type: "text", text: `On-demand MCP guidance detail set to: ${detail}\nRestart opencode for startup instructions/tool descriptions to fully refresh.` })
|
|
1461
|
+
}
|
|
1462
|
+
} else {
|
|
1463
|
+
output.parts.push({ type: "text", text: "Usage:\n /raven mcp\n /raven mcp refresh [server_name]\n /raven mcp detail full\n /raven mcp detail minimized" })
|
|
1464
|
+
}
|
|
705
1465
|
} else if (arg === "update") {
|
|
706
1466
|
try {
|
|
707
1467
|
const info = await refreshUpdateInfo()
|
|
@@ -755,7 +1515,7 @@ export default ((input: PluginInput) => {
|
|
|
755
1515
|
? `Update: ${info.latest} available. Run /raven update, then restart opencode.`
|
|
756
1516
|
: `Update: up to date${info.latest ? ` (latest ${info.latest})` : ""}.`
|
|
757
1517
|
} catch { /* keep fallback */ }
|
|
758
|
-
output.parts.push({ type: "text", text: `Raven is ${enabled}. Version: ${PACKAGE_VERSION}. Model: ${model}. Reasoning: ${effort}. Timeout: ${timeout}s\n${update}\n\n${routeSummary()}\n\n${mcpSummary()}\n\
|
|
1518
|
+
output.parts.push({ type: "text", text: `Raven is ${enabled}. Version: ${PACKAGE_VERSION}. Model: ${model}. Reasoning: ${effort}. Timeout: ${timeout}s\n${update}\n\n${routeSummary()}\n\n${mcpSummary()}\n\n${statsText()}\n\nRun /raven help for commands.` })
|
|
759
1519
|
}
|
|
760
1520
|
},
|
|
761
1521
|
|
|
@@ -764,6 +1524,10 @@ export default ((input: PluginInput) => {
|
|
|
764
1524
|
throw new Error("raven_seek is disabled inside Raven. Use Raven's direct search/fetch tools instead.")
|
|
765
1525
|
}
|
|
766
1526
|
|
|
1527
|
+
if (input.tool === "raven_mcp" && !ravenSessions.has(input.sessionID) && sessionAgents.get(input.sessionID) !== "raven") {
|
|
1528
|
+
throw new Error("raven_mcp is only available inside Raven. Use raven_seek(query=\"Use the relevant on-demand MCP to handle this request\") instead.")
|
|
1529
|
+
}
|
|
1530
|
+
|
|
767
1531
|
if (!config.enabled) return
|
|
768
1532
|
if (ravenSessions.has(input.sessionID)) return
|
|
769
1533
|
if (isExcluded(sessionAgents.get(input.sessionID))) return
|