zoe-agent 0.3.1
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/CHANGELOG.md +154 -0
- package/LICENSE +96 -0
- package/README.md +568 -0
- package/dist/adapters/cli/agent.d.ts +59 -0
- package/dist/adapters/cli/agent.js +232 -0
- package/dist/adapters/cli/bootstrap.d.ts +25 -0
- package/dist/adapters/cli/bootstrap.js +204 -0
- package/dist/adapters/cli/commands/build-registry.d.ts +14 -0
- package/dist/adapters/cli/commands/build-registry.js +88 -0
- package/dist/adapters/cli/commands/clear.d.ts +7 -0
- package/dist/adapters/cli/commands/clear.js +10 -0
- package/dist/adapters/cli/commands/compact.d.ts +13 -0
- package/dist/adapters/cli/commands/compact.js +96 -0
- package/dist/adapters/cli/commands/exit.d.ts +7 -0
- package/dist/adapters/cli/commands/exit.js +9 -0
- package/dist/adapters/cli/commands/gateway.d.ts +7 -0
- package/dist/adapters/cli/commands/gateway.js +152 -0
- package/dist/adapters/cli/commands/help.d.ts +9 -0
- package/dist/adapters/cli/commands/help.js +12 -0
- package/dist/adapters/cli/commands/models.d.ts +10 -0
- package/dist/adapters/cli/commands/models.js +32 -0
- package/dist/adapters/cli/commands/registry.d.ts +70 -0
- package/dist/adapters/cli/commands/registry.js +111 -0
- package/dist/adapters/cli/commands/settings-utils.d.ts +38 -0
- package/dist/adapters/cli/commands/settings-utils.js +182 -0
- package/dist/adapters/cli/commands/settings.d.ts +9 -0
- package/dist/adapters/cli/commands/settings.js +395 -0
- package/dist/adapters/cli/commands/skills.d.ts +7 -0
- package/dist/adapters/cli/commands/skills.js +21 -0
- package/dist/adapters/cli/config-loader.d.ts +27 -0
- package/dist/adapters/cli/config-loader.js +48 -0
- package/dist/adapters/cli/docker-utils.d.ts +37 -0
- package/dist/adapters/cli/docker-utils.js +90 -0
- package/dist/adapters/cli/index.d.ts +2 -0
- package/dist/adapters/cli/index.js +88 -0
- package/dist/adapters/cli/repl.d.ts +22 -0
- package/dist/adapters/cli/repl.js +256 -0
- package/dist/adapters/cli/setup.d.ts +19 -0
- package/dist/adapters/cli/setup.js +613 -0
- package/dist/adapters/cli/system-prompts.d.ts +56 -0
- package/dist/adapters/cli/system-prompts.js +131 -0
- package/dist/adapters/cli/tui/app.d.ts +58 -0
- package/dist/adapters/cli/tui/app.js +314 -0
- package/dist/adapters/cli/tui/components/assistant-message.d.ts +5 -0
- package/dist/adapters/cli/tui/components/assistant-message.js +9 -0
- package/dist/adapters/cli/tui/components/autocomplete.d.ts +19 -0
- package/dist/adapters/cli/tui/components/autocomplete.js +75 -0
- package/dist/adapters/cli/tui/components/command-palette.d.ts +15 -0
- package/dist/adapters/cli/tui/components/command-palette.js +50 -0
- package/dist/adapters/cli/tui/components/diff-viewer.d.ts +5 -0
- package/dist/adapters/cli/tui/components/diff-viewer.js +109 -0
- package/dist/adapters/cli/tui/components/error-message.d.ts +5 -0
- package/dist/adapters/cli/tui/components/error-message.js +8 -0
- package/dist/adapters/cli/tui/components/footer.d.ts +20 -0
- package/dist/adapters/cli/tui/components/footer.js +19 -0
- package/dist/adapters/cli/tui/components/goal-status.d.ts +12 -0
- package/dist/adapters/cli/tui/components/goal-status.js +22 -0
- package/dist/adapters/cli/tui/components/info-message.d.ts +5 -0
- package/dist/adapters/cli/tui/components/info-message.js +8 -0
- package/dist/adapters/cli/tui/components/logo-banner.d.ts +7 -0
- package/dist/adapters/cli/tui/components/logo-banner.js +33 -0
- package/dist/adapters/cli/tui/components/markdown.d.ts +9 -0
- package/dist/adapters/cli/tui/components/markdown.js +92 -0
- package/dist/adapters/cli/tui/components/message-area.d.ts +19 -0
- package/dist/adapters/cli/tui/components/message-area.js +55 -0
- package/dist/adapters/cli/tui/components/permission-prompt.d.ts +13 -0
- package/dist/adapters/cli/tui/components/permission-prompt.js +32 -0
- package/dist/adapters/cli/tui/components/prompt-area.d.ts +22 -0
- package/dist/adapters/cli/tui/components/prompt-area.js +68 -0
- package/dist/adapters/cli/tui/components/text-input.d.ts +27 -0
- package/dist/adapters/cli/tui/components/text-input.js +142 -0
- package/dist/adapters/cli/tui/components/tool-call-block.d.ts +11 -0
- package/dist/adapters/cli/tui/components/tool-call-block.js +68 -0
- package/dist/adapters/cli/tui/components/user-message.d.ts +5 -0
- package/dist/adapters/cli/tui/components/user-message.js +8 -0
- package/dist/adapters/cli/tui/diff/file-write-meta.d.ts +11 -0
- package/dist/adapters/cli/tui/diff/file-write-meta.js +11 -0
- package/dist/adapters/cli/tui/diff/line-diff.d.ts +17 -0
- package/dist/adapters/cli/tui/diff/line-diff.js +44 -0
- package/dist/adapters/cli/tui/feed-serializer.d.ts +29 -0
- package/dist/adapters/cli/tui/feed-serializer.js +70 -0
- package/dist/adapters/cli/tui/file-index.d.ts +8 -0
- package/dist/adapters/cli/tui/file-index.js +41 -0
- package/dist/adapters/cli/tui/hooks/use-agent.d.ts +54 -0
- package/dist/adapters/cli/tui/hooks/use-agent.js +177 -0
- package/dist/adapters/cli/tui/hooks/use-feed.d.ts +16 -0
- package/dist/adapters/cli/tui/hooks/use-feed.js +25 -0
- package/dist/adapters/cli/tui/hooks/use-file-watcher.d.ts +10 -0
- package/dist/adapters/cli/tui/hooks/use-file-watcher.js +43 -0
- package/dist/adapters/cli/tui/hooks/use-keybindings.d.ts +16 -0
- package/dist/adapters/cli/tui/hooks/use-keybindings.js +25 -0
- package/dist/adapters/cli/tui/hooks/use-theme.d.ts +8 -0
- package/dist/adapters/cli/tui/hooks/use-theme.js +12 -0
- package/dist/adapters/cli/tui/index.d.ts +19 -0
- package/dist/adapters/cli/tui/index.js +206 -0
- package/dist/adapters/cli/tui/ink-reset.d.ts +29 -0
- package/dist/adapters/cli/tui/ink-reset.js +57 -0
- package/dist/adapters/cli/tui/layout.d.ts +15 -0
- package/dist/adapters/cli/tui/layout.js +15 -0
- package/dist/adapters/cli/tui/logo/gradient.d.ts +11 -0
- package/dist/adapters/cli/tui/logo/gradient.js +31 -0
- package/dist/adapters/cli/tui/overlays/help-dialog.d.ts +4 -0
- package/dist/adapters/cli/tui/overlays/help-dialog.js +26 -0
- package/dist/adapters/cli/tui/overlays/model-selector.d.ts +14 -0
- package/dist/adapters/cli/tui/overlays/model-selector.js +43 -0
- package/dist/adapters/cli/tui/overlays/session-selector.d.ts +35 -0
- package/dist/adapters/cli/tui/overlays/session-selector.js +162 -0
- package/dist/adapters/cli/tui/overlays/settings-overlay.d.ts +24 -0
- package/dist/adapters/cli/tui/overlays/settings-overlay.js +126 -0
- package/dist/adapters/cli/tui/session-export.d.ts +21 -0
- package/dist/adapters/cli/tui/session-export.js +63 -0
- package/dist/adapters/cli/tui/theme.d.ts +23 -0
- package/dist/adapters/cli/tui/theme.js +22 -0
- package/dist/adapters/cli/tui/types.d.ts +52 -0
- package/dist/adapters/cli/tui/types.js +12 -0
- package/dist/adapters/sdk/agent.d.ts +20 -0
- package/dist/adapters/sdk/agent.js +356 -0
- package/dist/adapters/sdk/http.d.ts +43 -0
- package/dist/adapters/sdk/http.js +61 -0
- package/dist/adapters/sdk/index.d.ts +58 -0
- package/dist/adapters/sdk/index.js +209 -0
- package/dist/adapters/sdk/settings.d.ts +18 -0
- package/dist/adapters/sdk/settings.js +57 -0
- package/dist/adapters/sdk/tools.d.ts +7 -0
- package/dist/adapters/sdk/tools.js +13 -0
- package/dist/adapters/server/auth.d.ts +53 -0
- package/dist/adapters/server/auth.js +168 -0
- package/dist/adapters/server/index.d.ts +40 -0
- package/dist/adapters/server/index.js +255 -0
- package/dist/adapters/server/rest-gateway.d.ts +13 -0
- package/dist/adapters/server/rest-gateway.js +218 -0
- package/dist/adapters/server/rest.d.ts +37 -0
- package/dist/adapters/server/rest.js +341 -0
- package/dist/adapters/server/server-core.d.ts +55 -0
- package/dist/adapters/server/server-core.js +121 -0
- package/dist/adapters/server/session-store.d.ts +81 -0
- package/dist/adapters/server/session-store.js +272 -0
- package/dist/adapters/server/settings-handlers.d.ts +24 -0
- package/dist/adapters/server/settings-handlers.js +360 -0
- package/dist/adapters/server/standalone.d.ts +19 -0
- package/dist/adapters/server/standalone.js +113 -0
- package/dist/adapters/server/websocket.d.ts +26 -0
- package/dist/adapters/server/websocket.js +68 -0
- package/dist/adapters/server/ws-handlers.d.ts +32 -0
- package/dist/adapters/server/ws-handlers.js +523 -0
- package/dist/adapters/server/ws-types.d.ts +304 -0
- package/dist/adapters/server/ws-types.js +7 -0
- package/dist/core/agent-loop.d.ts +68 -0
- package/dist/core/agent-loop.js +423 -0
- package/dist/core/config.d.ts +115 -0
- package/dist/core/config.js +189 -0
- package/dist/core/errors.d.ts +58 -0
- package/dist/core/errors.js +88 -0
- package/dist/core/hooks.d.ts +35 -0
- package/dist/core/hooks.js +49 -0
- package/dist/core/index.d.ts +23 -0
- package/dist/core/index.js +29 -0
- package/dist/core/message-convert.d.ts +41 -0
- package/dist/core/message-convert.js +94 -0
- package/dist/core/middleware/auth.d.ts +24 -0
- package/dist/core/middleware/auth.js +28 -0
- package/dist/core/middleware/logging.d.ts +23 -0
- package/dist/core/middleware/logging.js +28 -0
- package/dist/core/middleware/rate-limit.d.ts +27 -0
- package/dist/core/middleware/rate-limit.js +38 -0
- package/dist/core/middleware/semantic-tools.d.ts +10 -0
- package/dist/core/middleware/semantic-tools.js +43 -0
- package/dist/core/middleware.d.ts +48 -0
- package/dist/core/middleware.js +38 -0
- package/dist/core/permission.d.ts +25 -0
- package/dist/core/permission.js +50 -0
- package/dist/core/provider-config.d.ts +129 -0
- package/dist/core/provider-config.js +273 -0
- package/dist/core/provider-env.d.ts +39 -0
- package/dist/core/provider-env.js +142 -0
- package/dist/core/provider-resolver.d.ts +12 -0
- package/dist/core/provider-resolver.js +12 -0
- package/dist/core/session-store.d.ts +75 -0
- package/dist/core/session-store.js +245 -0
- package/dist/core/settings-manager.d.ts +57 -0
- package/dist/core/settings-manager.js +359 -0
- package/dist/core/settings-schema.d.ts +38 -0
- package/dist/core/settings-schema.js +171 -0
- package/dist/core/skill-catalog.d.ts +6 -0
- package/dist/core/skill-catalog.js +17 -0
- package/dist/core/skill-invoker.d.ts +127 -0
- package/dist/core/skill-invoker.js +182 -0
- package/dist/core/stream-accumulator.d.ts +21 -0
- package/dist/core/stream-accumulator.js +51 -0
- package/dist/core/stream-manager.d.ts +58 -0
- package/dist/core/stream-manager.js +212 -0
- package/dist/core/tool-executor.d.ts +84 -0
- package/dist/core/tool-executor.js +256 -0
- package/dist/core/types.d.ts +259 -0
- package/dist/core/types.js +11 -0
- package/dist/gateway/gateway.d.ts +52 -0
- package/dist/gateway/gateway.js +537 -0
- package/dist/gateway/index.d.ts +21 -0
- package/dist/gateway/index.js +31 -0
- package/dist/gateway/openapi-importer.d.ts +15 -0
- package/dist/gateway/openapi-importer.js +66 -0
- package/dist/gateway/semantic-scorer.d.ts +7 -0
- package/dist/gateway/semantic-scorer.js +24 -0
- package/dist/gateway/settings-adapter.d.ts +49 -0
- package/dist/gateway/settings-adapter.js +137 -0
- package/dist/gateway/tool-factory.d.ts +9 -0
- package/dist/gateway/tool-factory.js +414 -0
- package/dist/gateway/types.d.ts +68 -0
- package/dist/gateway/types.js +7 -0
- package/dist/models-catalog.js +46 -0
- package/dist/providers/anthropic.d.ts +22 -0
- package/dist/providers/anthropic.js +148 -0
- package/dist/providers/factory.d.ts +10 -0
- package/dist/providers/factory.js +25 -0
- package/dist/providers/openai.d.ts +15 -0
- package/dist/providers/openai.js +71 -0
- package/dist/providers/types.d.ts +48 -0
- package/dist/providers/types.js +1 -0
- package/dist/skills/args.d.ts +37 -0
- package/dist/skills/args.js +99 -0
- package/dist/skills/index.d.ts +11 -0
- package/dist/skills/index.js +23 -0
- package/dist/skills/loader.d.ts +3 -0
- package/dist/skills/loader.js +59 -0
- package/dist/skills/parser.d.ts +7 -0
- package/dist/skills/parser.js +152 -0
- package/dist/skills/registry.d.ts +13 -0
- package/dist/skills/registry.js +74 -0
- package/dist/skills/resolver.d.ts +19 -0
- package/dist/skills/resolver.js +116 -0
- package/dist/skills/types.d.ts +74 -0
- package/dist/skills/types.js +50 -0
- package/dist/tools/browser.d.ts +2 -0
- package/dist/tools/browser.js +68 -0
- package/dist/tools/core.d.ts +20 -0
- package/dist/tools/core.js +244 -0
- package/dist/tools/email.d.ts +2 -0
- package/dist/tools/email.js +61 -0
- package/dist/tools/image.d.ts +2 -0
- package/dist/tools/image.js +257 -0
- package/dist/tools/index.d.ts +2 -0
- package/dist/tools/index.js +88 -0
- package/dist/tools/interface.d.ts +22 -0
- package/dist/tools/interface.js +1 -0
- package/dist/tools/notify.d.ts +2 -0
- package/dist/tools/notify.js +100 -0
- package/dist/tools/prompt-optimizer.d.ts +2 -0
- package/dist/tools/prompt-optimizer.js +65 -0
- package/dist/tools/screenshot.d.ts +2 -0
- package/dist/tools/screenshot.js +184 -0
- package/dist/tools/search.d.ts +2 -0
- package/dist/tools/search.js +78 -0
- package/dist/tools/todos.d.ts +10 -0
- package/dist/tools/todos.js +50 -0
- package/package.json +119 -0
- package/skills/docker-ops/SKILL.md +329 -0
- package/skills/k8s-deploy/SKILL.md +397 -0
- package/skills/log-analyzer/SKILL.md +331 -0
- package/skills/speckit-analyze/SKILL.md +260 -0
- package/skills/speckit-checklist/SKILL.md +374 -0
- package/skills/speckit-clarify/SKILL.md +286 -0
- package/skills/speckit-constitution/SKILL.md +157 -0
- package/skills/speckit-implement/SKILL.md +224 -0
- package/skills/speckit-plan/SKILL.md +171 -0
- package/skills/speckit-specify/SKILL.md +346 -0
- package/skills/speckit-tasks/SKILL.md +215 -0
- package/skills/speckit-taskstoissues/SKILL.md +107 -0
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zoe Gateway — MCPGateway engine
|
|
3
|
+
*
|
|
4
|
+
* Core engine that manages MCP and REST targets, routes requests,
|
|
5
|
+
* and exposes injectable tools for the agent loop.
|
|
6
|
+
*/
|
|
7
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
8
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
9
|
+
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
|
10
|
+
import { CreateMessageRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
11
|
+
import { GatewayError } from '../core/errors.js';
|
|
12
|
+
import { scoreRelevance } from './semantic-scorer.js';
|
|
13
|
+
export class MCPGateway {
|
|
14
|
+
targets = new Map();
|
|
15
|
+
mcpClients = new Map();
|
|
16
|
+
auditLogs = [];
|
|
17
|
+
routes = [];
|
|
18
|
+
adminTargets = new Set();
|
|
19
|
+
injectableToolsCache = null;
|
|
20
|
+
invalidateToolsCache() {
|
|
21
|
+
this.injectableToolsCache = null;
|
|
22
|
+
}
|
|
23
|
+
settings;
|
|
24
|
+
config;
|
|
25
|
+
hooks;
|
|
26
|
+
constructor(settings, config, hooks = {}) {
|
|
27
|
+
this.settings = settings;
|
|
28
|
+
this.config = config;
|
|
29
|
+
this.hooks = hooks;
|
|
30
|
+
}
|
|
31
|
+
// ── Lifecycle ────────────────────────────────────────────────────────
|
|
32
|
+
async initialize() {
|
|
33
|
+
const stored = this.settings.getTargets();
|
|
34
|
+
for (const [name, target] of Object.entries(stored)) {
|
|
35
|
+
this.targets.set(name, target);
|
|
36
|
+
}
|
|
37
|
+
// Restore admin target status from persistence
|
|
38
|
+
const adminNames = this.settings.getAdminTargets();
|
|
39
|
+
let pruned = false;
|
|
40
|
+
for (const name of adminNames) {
|
|
41
|
+
if (this.targets.has(name)) {
|
|
42
|
+
this.adminTargets.add(name);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
await this.settings.removeAdminTarget(name);
|
|
46
|
+
pruned = true;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
this.routes = this.settings.getRoutes();
|
|
50
|
+
for (const [name, target] of this.targets) {
|
|
51
|
+
if (!target.enabled || target.kind !== 'mcp')
|
|
52
|
+
continue;
|
|
53
|
+
try {
|
|
54
|
+
await this.connectMcpClient(name, target);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// Non-fatal: target fails to connect at startup
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
async shutdown() {
|
|
62
|
+
for (const [, client] of this.mcpClients) {
|
|
63
|
+
try {
|
|
64
|
+
await client.close();
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// Best-effort close
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
this.mcpClients.clear();
|
|
71
|
+
}
|
|
72
|
+
// ── Audit ────────────────────────────────────────────────────────────
|
|
73
|
+
audit(agent, target, operation, status, durationMs, success) {
|
|
74
|
+
const record = {
|
|
75
|
+
timestamp: Date.now(),
|
|
76
|
+
agent,
|
|
77
|
+
target,
|
|
78
|
+
operation,
|
|
79
|
+
status,
|
|
80
|
+
durationMs,
|
|
81
|
+
success,
|
|
82
|
+
};
|
|
83
|
+
this.auditLogs.push(record);
|
|
84
|
+
if (this.auditLogs.length > this.config.maxAuditLogsInMemory) {
|
|
85
|
+
this.auditLogs.shift();
|
|
86
|
+
}
|
|
87
|
+
if (this.hooks.onAudit) {
|
|
88
|
+
try {
|
|
89
|
+
const result = this.hooks.onAudit(record);
|
|
90
|
+
if (result instanceof Promise) {
|
|
91
|
+
result.catch(() => { });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
// Non-fatal hook error
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
getAuditLogs(targetFilter, limit) {
|
|
100
|
+
let logs = this.auditLogs;
|
|
101
|
+
if (targetFilter) {
|
|
102
|
+
logs = logs.filter((r) => r.target === targetFilter);
|
|
103
|
+
}
|
|
104
|
+
if (limit !== undefined && limit < logs.length) {
|
|
105
|
+
logs = logs.slice(-limit);
|
|
106
|
+
}
|
|
107
|
+
return logs;
|
|
108
|
+
}
|
|
109
|
+
getUsageSummary() {
|
|
110
|
+
const summary = {};
|
|
111
|
+
for (const [name] of this.targets) {
|
|
112
|
+
summary[name] = { calls: 0, errors: 0 };
|
|
113
|
+
}
|
|
114
|
+
for (const log of this.auditLogs) {
|
|
115
|
+
if (!summary[log.target]) {
|
|
116
|
+
summary[log.target] = { calls: 0, errors: 0 };
|
|
117
|
+
}
|
|
118
|
+
summary[log.target].calls++;
|
|
119
|
+
if (!log.success) {
|
|
120
|
+
summary[log.target].errors++;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return summary;
|
|
124
|
+
}
|
|
125
|
+
// ── Target management ────────────────────────────────────────────────
|
|
126
|
+
async registerTarget(name, target, isAdmin = false) {
|
|
127
|
+
if (!target || !['mcp', 'rest'].includes(target.kind)) {
|
|
128
|
+
throw new GatewayError(`Invalid target: kind must be 'mcp' or 'rest'`, name, false);
|
|
129
|
+
}
|
|
130
|
+
if (isAdmin) {
|
|
131
|
+
this.adminTargets.add(name);
|
|
132
|
+
await this.settings.addAdminTarget(name);
|
|
133
|
+
}
|
|
134
|
+
this.targets.set(name, target);
|
|
135
|
+
await this.settings.saveTarget(name, target);
|
|
136
|
+
this.invalidateToolsCache();
|
|
137
|
+
}
|
|
138
|
+
async unregisterTarget(name) {
|
|
139
|
+
const client = this.mcpClients.get(name);
|
|
140
|
+
if (client) {
|
|
141
|
+
try {
|
|
142
|
+
await client.close();
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
// Best-effort
|
|
146
|
+
}
|
|
147
|
+
this.mcpClients.delete(name);
|
|
148
|
+
}
|
|
149
|
+
this.routes = this.routes.filter((r) => r.target !== name);
|
|
150
|
+
await this.settings.saveRoutes(this.routes);
|
|
151
|
+
const deleted = this.targets.delete(name);
|
|
152
|
+
if (deleted) {
|
|
153
|
+
await this.settings.deleteTarget(name);
|
|
154
|
+
this.invalidateToolsCache();
|
|
155
|
+
}
|
|
156
|
+
this.adminTargets.delete(name);
|
|
157
|
+
await this.settings.removeAdminTarget(name);
|
|
158
|
+
return deleted;
|
|
159
|
+
}
|
|
160
|
+
async toggleTarget(name, enabled) {
|
|
161
|
+
const target = this.targets.get(name);
|
|
162
|
+
if (!target)
|
|
163
|
+
return false;
|
|
164
|
+
target.enabled = enabled;
|
|
165
|
+
await this.settings.saveTarget(name, target);
|
|
166
|
+
this.invalidateToolsCache();
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
getTargets() {
|
|
170
|
+
return Object.fromEntries(this.targets);
|
|
171
|
+
}
|
|
172
|
+
// ── Routes ───────────────────────────────────────────────────────────
|
|
173
|
+
async addRoute(pattern, target, priority) {
|
|
174
|
+
const dupe = this.routes.find((r) => r.pattern === pattern && r.target === target);
|
|
175
|
+
if (dupe)
|
|
176
|
+
return;
|
|
177
|
+
this.routes.push({ pattern, target, priority });
|
|
178
|
+
this.routes.sort((a, b) => b.priority - a.priority);
|
|
179
|
+
await this.settings.saveRoutes(this.routes);
|
|
180
|
+
}
|
|
181
|
+
async removeRoute(pattern, target) {
|
|
182
|
+
this.routes = this.routes.filter((r) => !(r.pattern === pattern && r.target === target));
|
|
183
|
+
await this.settings.saveRoutes(this.routes);
|
|
184
|
+
}
|
|
185
|
+
routeRequest(request) {
|
|
186
|
+
const lower = request.toLowerCase();
|
|
187
|
+
for (const route of this.routes) {
|
|
188
|
+
if (lower.includes(route.pattern.toLowerCase())) {
|
|
189
|
+
return `-> ${route.target} (route: '${route.pattern}')`;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
let bestTarget = null;
|
|
193
|
+
let bestScore = 0;
|
|
194
|
+
for (const [name, target] of this.targets) {
|
|
195
|
+
if (!target.enabled)
|
|
196
|
+
continue;
|
|
197
|
+
const text = [target.description, ...target.tags].join(' ');
|
|
198
|
+
const score = scoreRelevance(request, text);
|
|
199
|
+
if (score > bestScore) {
|
|
200
|
+
bestScore = score;
|
|
201
|
+
bestTarget = name;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (bestTarget && bestScore > 0) {
|
|
205
|
+
return `-> ${bestTarget} (tag match)`;
|
|
206
|
+
}
|
|
207
|
+
const available = Array.from(this.targets.keys());
|
|
208
|
+
return available.length > 0
|
|
209
|
+
? `No route matched. Available: ${available.join(', ')}`
|
|
210
|
+
: 'No targets registered.';
|
|
211
|
+
}
|
|
212
|
+
// ── MCP client connection ────────────────────────────────────────────
|
|
213
|
+
async connectMcpClient(targetName, target) {
|
|
214
|
+
const cached = this.mcpClients.get(targetName);
|
|
215
|
+
if (cached)
|
|
216
|
+
return cached;
|
|
217
|
+
const client = new Client({ name: 'zoe-gateway', version: '0.1.0' }, { capabilities: { sampling: {} } });
|
|
218
|
+
if (this.hooks.onSamplingRequest) {
|
|
219
|
+
client.setRequestHandler(CreateMessageRequestSchema, (request) => this.hooks.onSamplingRequest(request));
|
|
220
|
+
}
|
|
221
|
+
// Resolve env with B3 credential trust guard
|
|
222
|
+
const env = { ...process.env };
|
|
223
|
+
if (target.env) {
|
|
224
|
+
for (const [k, v] of Object.entries(target.env)) {
|
|
225
|
+
if (v.startsWith('credential:') && v.length > 11) {
|
|
226
|
+
// B3: only resolve credential refs for admin targets
|
|
227
|
+
if (this.adminTargets.has(targetName)) {
|
|
228
|
+
const key = v.substring(11);
|
|
229
|
+
const cred = this.settings.getCredential(key);
|
|
230
|
+
if (cred) {
|
|
231
|
+
env[k] = cred;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
env[k] = v;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (target.transport === 'stdio') {
|
|
241
|
+
if (!target.command) {
|
|
242
|
+
throw new GatewayError(`MCP target "${targetName}" missing command for stdio transport`, targetName, false);
|
|
243
|
+
}
|
|
244
|
+
const transport = new StdioClientTransport({ command: target.command, args: target.args, env });
|
|
245
|
+
await client.connect(transport);
|
|
246
|
+
}
|
|
247
|
+
else if (target.transport === 'sse' || target.transport === 'http') {
|
|
248
|
+
if (!target.url) {
|
|
249
|
+
throw new GatewayError(`MCP target "${targetName}" missing url for ${target.transport} transport`, targetName, false);
|
|
250
|
+
}
|
|
251
|
+
// Resolve auth headers for SSE/HTTP transport
|
|
252
|
+
// B3 trust guard: only resolve credentialRef for admin-registered targets
|
|
253
|
+
const headers = {};
|
|
254
|
+
if (target.auth?.credentialRef) {
|
|
255
|
+
const cred = this.adminTargets.has(targetName) ? this.settings.getCredential(target.auth.credentialRef) : undefined;
|
|
256
|
+
if (cred) {
|
|
257
|
+
if (target.auth.type === 'bearer')
|
|
258
|
+
headers['Authorization'] = `Bearer ${cred}`;
|
|
259
|
+
else if (target.auth.type === 'header' && target.auth.name)
|
|
260
|
+
headers[target.auth.name] = cred;
|
|
261
|
+
else if (target.auth.type === 'basic')
|
|
262
|
+
headers['Authorization'] = `Basic ${Buffer.from(cred).toString('base64')}`;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
const transport = new SSEClientTransport(new URL(target.url), { requestInit: { headers } });
|
|
266
|
+
await client.connect(transport);
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
throw new GatewayError(`MCP target "${targetName}" has unsupported transport: ${target.transport}`, targetName, false);
|
|
270
|
+
}
|
|
271
|
+
// Auto-discover capabilities
|
|
272
|
+
const capabilities = target.capabilities ?? {};
|
|
273
|
+
try {
|
|
274
|
+
const toolsResult = await client.listTools();
|
|
275
|
+
capabilities.tools = toolsResult.tools;
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
capabilities.tools = [];
|
|
279
|
+
}
|
|
280
|
+
try {
|
|
281
|
+
const resourcesResult = await client.listResources();
|
|
282
|
+
capabilities.resources = resourcesResult.resources;
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
capabilities.resources = [];
|
|
286
|
+
}
|
|
287
|
+
try {
|
|
288
|
+
const promptsResult = await client.listPrompts();
|
|
289
|
+
capabilities.prompts = promptsResult.prompts;
|
|
290
|
+
}
|
|
291
|
+
catch {
|
|
292
|
+
capabilities.prompts = [];
|
|
293
|
+
}
|
|
294
|
+
target.capabilities = capabilities;
|
|
295
|
+
this.mcpClients.set(targetName, client);
|
|
296
|
+
return client;
|
|
297
|
+
}
|
|
298
|
+
// ── MCP tool call ────────────────────────────────────────────────────
|
|
299
|
+
async callMcpTool(agent, targetName, toolName, args) {
|
|
300
|
+
const target = this.targets.get(targetName);
|
|
301
|
+
if (!target) {
|
|
302
|
+
throw new GatewayError(`Target not found: ${targetName}`, targetName, false);
|
|
303
|
+
}
|
|
304
|
+
if (!target.enabled) {
|
|
305
|
+
throw new GatewayError(`Target is disabled: ${targetName}`, targetName, false);
|
|
306
|
+
}
|
|
307
|
+
if (target.kind !== 'mcp') {
|
|
308
|
+
throw new GatewayError(`Target "${targetName}" is not an MCP target`, targetName, false);
|
|
309
|
+
}
|
|
310
|
+
const start = Date.now();
|
|
311
|
+
try {
|
|
312
|
+
const client = await this.connectMcpClient(targetName, target);
|
|
313
|
+
const result = await client.callTool({ name: toolName, arguments: args });
|
|
314
|
+
this.audit(agent, targetName, `tool:${toolName}`, 'success', Date.now() - start, true);
|
|
315
|
+
return result;
|
|
316
|
+
}
|
|
317
|
+
catch (err) {
|
|
318
|
+
// Evict cached client for lazy reconnect
|
|
319
|
+
this.mcpClients.delete(targetName);
|
|
320
|
+
this.audit(agent, targetName, `tool:${toolName}`, 'error', Date.now() - start, false);
|
|
321
|
+
if (err instanceof GatewayError)
|
|
322
|
+
throw err;
|
|
323
|
+
throw new GatewayError(err instanceof Error ? err.message : String(err), targetName, true);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
// ── REST proxy ───────────────────────────────────────────────────────
|
|
327
|
+
async callRest(agent, targetName, reqPath, method, query, body) {
|
|
328
|
+
const target = this.targets.get(targetName);
|
|
329
|
+
if (!target) {
|
|
330
|
+
throw new GatewayError(`Target not found: ${targetName}`, targetName, false);
|
|
331
|
+
}
|
|
332
|
+
if (!target.enabled) {
|
|
333
|
+
throw new GatewayError(`Target is disabled: ${targetName}`, targetName, false);
|
|
334
|
+
}
|
|
335
|
+
if (target.kind !== 'rest') {
|
|
336
|
+
throw new GatewayError(`Target "${targetName}" is not a REST target`, targetName, false);
|
|
337
|
+
}
|
|
338
|
+
const url = new URL(reqPath, target.baseUrl);
|
|
339
|
+
if (query) {
|
|
340
|
+
for (const [k, v] of Object.entries(query)) {
|
|
341
|
+
url.searchParams.set(k, v);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
const headers = { ...target.defaultHeaders };
|
|
345
|
+
// B3 trust guard: only resolve credentialRef for admin-registered targets
|
|
346
|
+
if (target.auth.type !== 'none' && target.auth.credentialRef) {
|
|
347
|
+
const cred = this.adminTargets.has(targetName) ? this.settings.getCredential(target.auth.credentialRef) : undefined;
|
|
348
|
+
if (cred) {
|
|
349
|
+
switch (target.auth.type) {
|
|
350
|
+
case 'bearer':
|
|
351
|
+
headers['Authorization'] = `Bearer ${cred}`;
|
|
352
|
+
break;
|
|
353
|
+
case 'header':
|
|
354
|
+
headers[target.auth.name ?? 'X-API-Key'] = cred;
|
|
355
|
+
break;
|
|
356
|
+
case 'query':
|
|
357
|
+
url.searchParams.set(target.auth.name ?? 'api_key', cred);
|
|
358
|
+
break;
|
|
359
|
+
case 'basic':
|
|
360
|
+
headers['Authorization'] = `Basic ${Buffer.from(cred).toString('base64')}`;
|
|
361
|
+
break;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
const start = Date.now();
|
|
366
|
+
try {
|
|
367
|
+
const response = await fetch(url.toString(), {
|
|
368
|
+
method,
|
|
369
|
+
headers: { ...(body ? { 'Content-Type': 'application/json' } : {}), ...headers },
|
|
370
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
371
|
+
});
|
|
372
|
+
const text = await response.text();
|
|
373
|
+
let data;
|
|
374
|
+
try {
|
|
375
|
+
data = JSON.parse(text);
|
|
376
|
+
}
|
|
377
|
+
catch {
|
|
378
|
+
data = text;
|
|
379
|
+
}
|
|
380
|
+
this.audit(agent, targetName, `${method} ${reqPath}`, response.ok ? 'success' : `http:${response.status}`, Date.now() - start, response.ok);
|
|
381
|
+
if (!response.ok) {
|
|
382
|
+
throw new GatewayError(`REST call to ${targetName} failed: ${response.status} ${text}`, targetName, response.status >= 500);
|
|
383
|
+
}
|
|
384
|
+
return data;
|
|
385
|
+
}
|
|
386
|
+
catch (err) {
|
|
387
|
+
if (err instanceof GatewayError)
|
|
388
|
+
throw err;
|
|
389
|
+
this.audit(agent, targetName, `${method} ${reqPath}`, 'error', Date.now() - start, false);
|
|
390
|
+
throw new GatewayError(err instanceof Error ? err.message : String(err), targetName, true);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
// ── MCP resource read ────────────────────────────────────────────────
|
|
394
|
+
async readResource(agent, targetName, uri) {
|
|
395
|
+
const target = this.targets.get(targetName);
|
|
396
|
+
if (!target) {
|
|
397
|
+
throw new GatewayError(`Target not found: ${targetName}`, targetName, false);
|
|
398
|
+
}
|
|
399
|
+
if (!target.enabled) {
|
|
400
|
+
throw new GatewayError(`Target is disabled: ${targetName}`, targetName, false);
|
|
401
|
+
}
|
|
402
|
+
if (target.kind !== 'mcp') {
|
|
403
|
+
throw new GatewayError(`Target "${targetName}" is not an MCP target`, targetName, false);
|
|
404
|
+
}
|
|
405
|
+
const start = Date.now();
|
|
406
|
+
try {
|
|
407
|
+
const client = await this.connectMcpClient(targetName, target);
|
|
408
|
+
const result = await client.readResource({ uri });
|
|
409
|
+
this.audit(agent, targetName, `resource:${uri}`, 'success', Date.now() - start, true);
|
|
410
|
+
return result;
|
|
411
|
+
}
|
|
412
|
+
catch (err) {
|
|
413
|
+
this.mcpClients.delete(targetName);
|
|
414
|
+
this.audit(agent, targetName, `resource:${uri}`, 'error', Date.now() - start, false);
|
|
415
|
+
if (err instanceof GatewayError)
|
|
416
|
+
throw err;
|
|
417
|
+
throw new GatewayError(err instanceof Error ? err.message : String(err), targetName, true);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
// ── MCP prompt get ───────────────────────────────────────────────────
|
|
421
|
+
async getPrompt(agent, targetName, name, args) {
|
|
422
|
+
const target = this.targets.get(targetName);
|
|
423
|
+
if (!target) {
|
|
424
|
+
throw new GatewayError(`Target not found: ${targetName}`, targetName, false);
|
|
425
|
+
}
|
|
426
|
+
if (!target.enabled) {
|
|
427
|
+
throw new GatewayError(`Target is disabled: ${targetName}`, targetName, false);
|
|
428
|
+
}
|
|
429
|
+
if (target.kind !== 'mcp') {
|
|
430
|
+
throw new GatewayError(`Target "${targetName}" is not an MCP target`, targetName, false);
|
|
431
|
+
}
|
|
432
|
+
const start = Date.now();
|
|
433
|
+
try {
|
|
434
|
+
const client = await this.connectMcpClient(targetName, target);
|
|
435
|
+
const result = await client.getPrompt({ name, arguments: args });
|
|
436
|
+
this.audit(agent, targetName, `prompt:${name}`, 'success', Date.now() - start, true);
|
|
437
|
+
return result;
|
|
438
|
+
}
|
|
439
|
+
catch (err) {
|
|
440
|
+
this.mcpClients.delete(targetName);
|
|
441
|
+
this.audit(agent, targetName, `prompt:${name}`, 'error', Date.now() - start, false);
|
|
442
|
+
if (err instanceof GatewayError)
|
|
443
|
+
throw err;
|
|
444
|
+
throw new GatewayError(err instanceof Error ? err.message : String(err), targetName, true);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
// ── Injectable tools ─────────────────────────────────────────────────
|
|
448
|
+
getRoutes() {
|
|
449
|
+
return [...this.routes];
|
|
450
|
+
}
|
|
451
|
+
listCredentialKeys() {
|
|
452
|
+
return this.settings.listCredentialKeys();
|
|
453
|
+
}
|
|
454
|
+
async setCredential(key, value) {
|
|
455
|
+
return this.settings.setCredential(key, value);
|
|
456
|
+
}
|
|
457
|
+
async deleteCredential(key) {
|
|
458
|
+
return this.settings.deleteCredential(key);
|
|
459
|
+
}
|
|
460
|
+
getInjectableTools() {
|
|
461
|
+
if (this.injectableToolsCache)
|
|
462
|
+
return this.injectableToolsCache;
|
|
463
|
+
const tools = this.buildInjectableTools();
|
|
464
|
+
this.injectableToolsCache = tools;
|
|
465
|
+
return tools;
|
|
466
|
+
}
|
|
467
|
+
buildInjectableTools() {
|
|
468
|
+
const tools = [];
|
|
469
|
+
for (const [targetName, target] of this.targets) {
|
|
470
|
+
if (!target.enabled)
|
|
471
|
+
continue;
|
|
472
|
+
if (target.kind === 'mcp') {
|
|
473
|
+
const mcpTools = target.capabilities?.tools ?? [];
|
|
474
|
+
for (const tool of mcpTools) {
|
|
475
|
+
const toolDef = tool;
|
|
476
|
+
const prefixedName = `${targetName}__${toolDef.name}`;
|
|
477
|
+
tools.push({
|
|
478
|
+
name: prefixedName,
|
|
479
|
+
risk: 'communications',
|
|
480
|
+
definition: {
|
|
481
|
+
type: 'function',
|
|
482
|
+
function: {
|
|
483
|
+
name: prefixedName,
|
|
484
|
+
description: toolDef.description ?? `MCP tool ${toolDef.name} on ${targetName}`,
|
|
485
|
+
parameters: toolDef.inputSchema ?? {
|
|
486
|
+
type: 'object',
|
|
487
|
+
properties: {},
|
|
488
|
+
required: [],
|
|
489
|
+
},
|
|
490
|
+
},
|
|
491
|
+
},
|
|
492
|
+
handler: async (args, config) => {
|
|
493
|
+
const agent = config?.agentName ?? 'zoe';
|
|
494
|
+
const result = await this.callMcpTool(agent, targetName, toolDef.name, args);
|
|
495
|
+
return typeof result === 'string' ? result : JSON.stringify(result);
|
|
496
|
+
},
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
if (target.kind === 'rest') {
|
|
501
|
+
for (const op of target.operations) {
|
|
502
|
+
const prefixedName = `${targetName}__${op.opId}`;
|
|
503
|
+
tools.push({
|
|
504
|
+
name: prefixedName,
|
|
505
|
+
risk: 'communications',
|
|
506
|
+
definition: {
|
|
507
|
+
type: 'function',
|
|
508
|
+
function: {
|
|
509
|
+
name: prefixedName,
|
|
510
|
+
description: op.summary ?? `${op.method} ${op.path} on ${targetName}`,
|
|
511
|
+
parameters: {
|
|
512
|
+
type: 'object',
|
|
513
|
+
properties: {
|
|
514
|
+
path: { type: 'string', description: 'Request path' },
|
|
515
|
+
query: {
|
|
516
|
+
type: 'object',
|
|
517
|
+
description: 'Query parameters',
|
|
518
|
+
additionalProperties: { type: 'string' },
|
|
519
|
+
},
|
|
520
|
+
body: { type: 'object', description: 'Request body' },
|
|
521
|
+
},
|
|
522
|
+
required: ['path'],
|
|
523
|
+
},
|
|
524
|
+
},
|
|
525
|
+
},
|
|
526
|
+
handler: async (args, config) => {
|
|
527
|
+
const agent = config?.agentName ?? 'zoe';
|
|
528
|
+
const result = await this.callRest(agent, targetName, args.path ?? op.path, op.method, args.query, args.body);
|
|
529
|
+
return typeof result === 'string' ? result : JSON.stringify(result);
|
|
530
|
+
},
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
return tools;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zoe Gateway — Public API
|
|
3
|
+
*
|
|
4
|
+
* Barrel export for the gateway subsystem.
|
|
5
|
+
*/
|
|
6
|
+
export { MCPGateway } from './gateway.js';
|
|
7
|
+
export { createGatewayTools } from './tool-factory.js';
|
|
8
|
+
export { importOpenApiSpec } from './openapi-importer.js';
|
|
9
|
+
export { GatewaySettingsAdapter } from './settings-adapter.js';
|
|
10
|
+
export { scoreRelevance } from './semantic-scorer.js';
|
|
11
|
+
export type { AuthType, McpTransportType, RestTarget, McpTarget, Target, AuditRecord, GatewayHooks, GatewayConfig, } from './types.js';
|
|
12
|
+
import { MCPGateway } from './gateway.js';
|
|
13
|
+
import { GatewaySettingsAdapter } from './settings-adapter.js';
|
|
14
|
+
import type { GatewayConfig, GatewayHooks } from './types.js';
|
|
15
|
+
/**
|
|
16
|
+
* Create and initialize a gateway instance.
|
|
17
|
+
* Returns null if gateway is disabled in settings.
|
|
18
|
+
*
|
|
19
|
+
* The caller creates and owns the GatewaySettingsAdapter.
|
|
20
|
+
*/
|
|
21
|
+
export declare function createGateway(config: GatewayConfig, settingsAdapter: GatewaySettingsAdapter, hooks?: GatewayHooks): Promise<MCPGateway | null>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zoe Gateway — Public API
|
|
3
|
+
*
|
|
4
|
+
* Barrel export for the gateway subsystem.
|
|
5
|
+
*/
|
|
6
|
+
export { MCPGateway } from './gateway.js';
|
|
7
|
+
export { createGatewayTools } from './tool-factory.js';
|
|
8
|
+
export { importOpenApiSpec } from './openapi-importer.js';
|
|
9
|
+
export { GatewaySettingsAdapter } from './settings-adapter.js';
|
|
10
|
+
export { scoreRelevance } from './semantic-scorer.js';
|
|
11
|
+
import { MCPGateway } from './gateway.js';
|
|
12
|
+
import { createGatewayTools } from './tool-factory.js';
|
|
13
|
+
import { registerTool } from '../core/tool-executor.js';
|
|
14
|
+
/**
|
|
15
|
+
* Create and initialize a gateway instance.
|
|
16
|
+
* Returns null if gateway is disabled in settings.
|
|
17
|
+
*
|
|
18
|
+
* The caller creates and owns the GatewaySettingsAdapter.
|
|
19
|
+
*/
|
|
20
|
+
export async function createGateway(config, settingsAdapter, hooks) {
|
|
21
|
+
if (!config.enabled)
|
|
22
|
+
return null;
|
|
23
|
+
const gateway = new MCPGateway(settingsAdapter, config, hooks);
|
|
24
|
+
await gateway.initialize();
|
|
25
|
+
// Register proxy tools in static registry
|
|
26
|
+
const tools = createGatewayTools(gateway);
|
|
27
|
+
for (const tool of tools) {
|
|
28
|
+
registerTool(tool);
|
|
29
|
+
}
|
|
30
|
+
return gateway;
|
|
31
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zoe Gateway — OpenAPI/Swagger spec importer
|
|
3
|
+
*
|
|
4
|
+
* Fetches an OpenAPI spec (JSON or YAML), extracts operations,
|
|
5
|
+
* and registers the result as a REST target on the gateway.
|
|
6
|
+
*/
|
|
7
|
+
import type { MCPGateway } from './gateway.js';
|
|
8
|
+
export declare function importOpenApiSpec(gateway: MCPGateway, name: string, specUrl: string, options?: {
|
|
9
|
+
baseUrl?: string;
|
|
10
|
+
tagFilter?: string[];
|
|
11
|
+
isAdmin?: boolean;
|
|
12
|
+
}): Promise<{
|
|
13
|
+
imported: number;
|
|
14
|
+
operations: string[];
|
|
15
|
+
}>;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zoe Gateway — OpenAPI/Swagger spec importer
|
|
3
|
+
*
|
|
4
|
+
* Fetches an OpenAPI spec (JSON or YAML), extracts operations,
|
|
5
|
+
* and registers the result as a REST target on the gateway.
|
|
6
|
+
*/
|
|
7
|
+
import * as yaml from 'js-yaml';
|
|
8
|
+
export async function importOpenApiSpec(gateway, name, specUrl, options) {
|
|
9
|
+
const response = await fetch(specUrl);
|
|
10
|
+
if (!response.ok)
|
|
11
|
+
throw new Error(`Failed to fetch spec: HTTP ${response.status}`);
|
|
12
|
+
const raw = await response.text();
|
|
13
|
+
let spec;
|
|
14
|
+
try {
|
|
15
|
+
spec = JSON.parse(raw);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
spec = yaml.load(raw);
|
|
19
|
+
}
|
|
20
|
+
const baseUrl = options?.baseUrl ?? spec.servers?.[0]?.url ?? '';
|
|
21
|
+
if (!baseUrl)
|
|
22
|
+
throw new Error('No base URL found in spec and none provided');
|
|
23
|
+
const operations = [];
|
|
24
|
+
const paths = spec.paths ?? {};
|
|
25
|
+
for (const [routePath, methods] of Object.entries(paths)) {
|
|
26
|
+
for (const [method, op] of Object.entries(methods)) {
|
|
27
|
+
if (['get', 'post', 'put', 'patch', 'delete'].includes(method.toLowerCase())) {
|
|
28
|
+
operations.push({
|
|
29
|
+
opId: op.operationId ?? `${method}_${routePath.replace(/[/{}/]/g, '_')}`,
|
|
30
|
+
method: method.toUpperCase(),
|
|
31
|
+
path: routePath,
|
|
32
|
+
summary: op.summary ?? `${method.toUpperCase()} ${routePath}`,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const filtered = options?.tagFilter
|
|
38
|
+
? operations.filter(op => {
|
|
39
|
+
const opData = (spec.paths?.[op.path]?.[op.method.toLowerCase()]);
|
|
40
|
+
const opTags = opData?.tags ?? [];
|
|
41
|
+
return options.tagFilter.some(t => opTags.includes(t));
|
|
42
|
+
})
|
|
43
|
+
: operations;
|
|
44
|
+
const allTags = [];
|
|
45
|
+
for (const [, methods] of Object.entries(paths)) {
|
|
46
|
+
for (const [, op] of Object.entries(methods)) {
|
|
47
|
+
if (op.tags)
|
|
48
|
+
allTags.push(...op.tags);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const tags = [...new Set(allTags)];
|
|
52
|
+
const target = {
|
|
53
|
+
kind: 'rest',
|
|
54
|
+
baseUrl,
|
|
55
|
+
description: spec.info?.title ?? name,
|
|
56
|
+
auth: { type: 'none' },
|
|
57
|
+
defaultHeaders: {},
|
|
58
|
+
operations: filtered,
|
|
59
|
+
tags,
|
|
60
|
+
enabled: true,
|
|
61
|
+
};
|
|
62
|
+
// Register as admin only when called from REST/CLI (isAdmin param).
|
|
63
|
+
// Agent tool calls pass isAdmin=false to prevent trust guard bypass.
|
|
64
|
+
await gateway.registerTarget(name, target, options?.isAdmin ?? true);
|
|
65
|
+
return { imported: filtered.length, operations: filtered.map(o => o.opId) };
|
|
66
|
+
}
|