@virtue-ai/gateway-connect 0.2.1 → 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/README.md +59 -23
- package/dist/index.d.ts +4 -3
- package/dist/index.js +207 -155
- package/dist/trajectory-plugin.js +6 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @virtue-ai/gateway-connect
|
|
2
2
|
|
|
3
|
-
One-command setup to connect [OpenClaw](https://github.com/openclaw/openclaw) to the VirtueAI MCP gateway.
|
|
3
|
+
One-command setup to connect [OpenClaw](https://github.com/openclaw/openclaw) to the VirtueAI MCP gateway. Works with **any model** — Anthropic, OpenAI, Google, LiteLLM, etc.
|
|
4
4
|
|
|
5
5
|
## Quick Start
|
|
6
6
|
|
|
@@ -10,51 +10,86 @@ One-command setup to connect [OpenClaw](https://github.com/openclaw/openclaw) to
|
|
|
10
10
|
npm install -g openclaw@latest
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
### Step 2: Configure Model Auth
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
Set up API key for your preferred model provider:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
openclaw models auth paste-token --provider anthropic
|
|
19
|
+
# or
|
|
20
|
+
openclaw models auth paste-token --provider openai
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Step 3: Connect to VirtueAI MCP Gateway
|
|
16
24
|
|
|
17
25
|
```bash
|
|
18
26
|
npx @virtue-ai/gateway-connect --gateway-url https://virtueai-agent-gtw-xxxx.ngrok.io
|
|
19
27
|
```
|
|
20
28
|
|
|
29
|
+
To use a specific model:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npx @virtue-ai/gateway-connect --gateway-url https://virtueai-agent-gtw-xxxx.ngrok.io --model openai/gpt-4o
|
|
33
|
+
```
|
|
34
|
+
|
|
21
35
|
This will:
|
|
22
36
|
|
|
23
|
-
1. Open your browser for OAuth login
|
|
24
|
-
2.
|
|
25
|
-
3.
|
|
26
|
-
4.
|
|
27
|
-
5.
|
|
37
|
+
1. Open your browser for OAuth login
|
|
38
|
+
2. Fetch all MCP tools from the gateway
|
|
39
|
+
3. Generate a native OpenClaw plugin wrapping each tool (`~/.openclaw/extensions/virtueai-mcp-tools/`)
|
|
40
|
+
4. Patch `~/.openclaw/openclaw.json` to enable the plugin
|
|
41
|
+
5. Install the trajectory recording plugin
|
|
28
42
|
|
|
29
|
-
### Step
|
|
43
|
+
### Step 4: Start Using
|
|
30
44
|
|
|
31
45
|
One-shot mode:
|
|
32
46
|
|
|
33
47
|
```bash
|
|
34
|
-
openclaw agent --local --
|
|
48
|
+
openclaw agent --local --message "What tools do you have?"
|
|
35
49
|
```
|
|
36
50
|
|
|
37
|
-
Interactive TUI
|
|
51
|
+
Interactive TUI mode:
|
|
38
52
|
|
|
39
53
|
```bash
|
|
40
|
-
# Terminal 1: start the gateway
|
|
54
|
+
# Terminal 1: start the OpenClaw gateway
|
|
41
55
|
openclaw gateway
|
|
42
56
|
|
|
43
|
-
# Terminal 2: open TUI
|
|
57
|
+
# Terminal 2: open the TUI
|
|
44
58
|
openclaw tui
|
|
45
59
|
```
|
|
46
60
|
|
|
47
|
-
|
|
61
|
+
To stop the gateway, press `Ctrl+C` in Terminal 1, or run:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
openclaw gateway stop
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Switching Models
|
|
68
|
+
|
|
69
|
+
In TUI mode, use slash commands to switch models on the fly:
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
/model openai/gpt-4o
|
|
73
|
+
/model anthropic/claude-opus-4-6
|
|
74
|
+
/models # opens model picker
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
All gateway tools remain available regardless of which model you use. Any model with a configured API key can be selected.
|
|
48
78
|
|
|
49
79
|
## What It Does
|
|
50
80
|
|
|
51
|
-
`gateway-connect`
|
|
81
|
+
`gateway-connect` generates a **native OpenClaw plugin** that registers every MCP gateway tool via `api.registerTool()`. Each tool call is proxied to the gateway as a JSON-RPC `tools/call` request with Bearer auth.
|
|
82
|
+
|
|
83
|
+
This approach works with any embedded model provider (not just Claude CLI), because the tools are part of OpenClaw's plugin system rather than being passed via `--mcp-config`.
|
|
84
|
+
|
|
85
|
+
### Generated Files
|
|
52
86
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
87
|
+
| Path | Purpose |
|
|
88
|
+
|------|---------|
|
|
89
|
+
| `~/.openclaw/extensions/virtueai-mcp-tools/` | Native plugin with all gateway tools |
|
|
90
|
+
| `~/.openclaw/extensions/virtueai-trajectory/` | Trajectory recording plugin |
|
|
91
|
+
| `~/.openclaw/mcp-gateway.json` | Auth & trajectory config |
|
|
92
|
+
| `~/.openclaw/openclaw.json` | Patched with plugin entries |
|
|
58
93
|
|
|
59
94
|
## Options
|
|
60
95
|
|
|
@@ -62,14 +97,15 @@ OpenClaw now has access to all MCP tools on the gateway (GitHub, Google Workspac
|
|
|
62
97
|
npx @virtue-ai/gateway-connect [options]
|
|
63
98
|
|
|
64
99
|
Options:
|
|
65
|
-
--gateway-url <url> Gateway URL (
|
|
66
|
-
--
|
|
100
|
+
--gateway-url <url> Gateway URL (default: https://virtueai-agent-gtw-l3phon63.ngrok.io)
|
|
101
|
+
--model <model> Model to use (e.g. openai/gpt-4o, anthropic/claude-sonnet-4-5)
|
|
102
|
+
--guard-uuid <uuid> Guard UUID for trajectory recording (or set VIRTUEAI_GUARD_UUID)
|
|
67
103
|
--help Show help message
|
|
68
104
|
```
|
|
69
105
|
|
|
70
106
|
## Re-authentication
|
|
71
107
|
|
|
72
|
-
If your token expires, just run the command again. It will
|
|
108
|
+
If your token expires, just run the command again. It will regenerate the plugin with a fresh token.
|
|
73
109
|
|
|
74
110
|
## License
|
|
75
111
|
|
package/dist/index.d.ts
CHANGED
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
* One-command setup to connect OpenClaw to VirtueAI MCP gateway.
|
|
6
6
|
*
|
|
7
7
|
* 1. OAuth 2.0 PKCE login → obtain access token
|
|
8
|
-
* 2.
|
|
9
|
-
* 3.
|
|
10
|
-
* 4.
|
|
8
|
+
* 2. Fetch all MCP tools from the gateway
|
|
9
|
+
* 3. Generate a native OpenClaw plugin that wraps each tool (works with any model)
|
|
10
|
+
* 4. Patch ~/.openclaw/openclaw.json to enable the plugin
|
|
11
|
+
* 5. Install trajectory recording plugin
|
|
11
12
|
*
|
|
12
13
|
* Usage: npx @virtue-ai/gateway-connect [--gateway-url https://...]
|
|
13
14
|
*/
|
package/dist/index.js
CHANGED
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
* One-command setup to connect OpenClaw to VirtueAI MCP gateway.
|
|
6
6
|
*
|
|
7
7
|
* 1. OAuth 2.0 PKCE login → obtain access token
|
|
8
|
-
* 2.
|
|
9
|
-
* 3.
|
|
10
|
-
* 4.
|
|
8
|
+
* 2. Fetch all MCP tools from the gateway
|
|
9
|
+
* 3. Generate a native OpenClaw plugin that wraps each tool (works with any model)
|
|
10
|
+
* 4. Patch ~/.openclaw/openclaw.json to enable the plugin
|
|
11
|
+
* 5. Install trajectory recording plugin
|
|
11
12
|
*
|
|
12
13
|
* Usage: npx @virtue-ai/gateway-connect [--gateway-url https://...]
|
|
13
14
|
*/
|
|
@@ -30,8 +31,11 @@ const SCOPES = 'claudeai copilot mcp:read mcp:execute mcp:access';
|
|
|
30
31
|
const OPENCLAW_DIR = path.join(os.homedir(), '.openclaw');
|
|
31
32
|
const MCP_CONFIG_PATH = path.join(OPENCLAW_DIR, 'mcp-gateway.json');
|
|
32
33
|
const OPENCLAW_CONFIG_PATH = path.join(OPENCLAW_DIR, 'openclaw.json');
|
|
33
|
-
const
|
|
34
|
+
const TOOLS_PLUGIN_ID = 'virtueai-mcp-tools';
|
|
35
|
+
const TOOLS_PLUGIN_DIR = path.join(OPENCLAW_DIR, 'extensions', TOOLS_PLUGIN_ID);
|
|
34
36
|
const DEFAULT_GATEWAY_URL = 'https://virtueai-agent-gtw-l3phon63.ngrok.io';
|
|
37
|
+
const DEFAULT_API_URL = 'https://agentgateway1.virtueai.io';
|
|
38
|
+
const DEFAULT_GATEWAY_ID = 'gtw_L3pHOn63';
|
|
35
39
|
// ---------------------------------------------------------------------------
|
|
36
40
|
// Helpers
|
|
37
41
|
// ---------------------------------------------------------------------------
|
|
@@ -97,7 +101,6 @@ function openBrowser(url) {
|
|
|
97
101
|
// Step 1: OAuth PKCE Authentication
|
|
98
102
|
// ---------------------------------------------------------------------------
|
|
99
103
|
async function authenticate(gatewayUrl) {
|
|
100
|
-
// Discover OAuth metadata
|
|
101
104
|
console.log(' Discovering OAuth endpoints...');
|
|
102
105
|
const { data: metadata } = await fetchJson(`${gatewayUrl}/.well-known/oauth-authorization-server`, { method: 'GET' });
|
|
103
106
|
if (!metadata.authorization_endpoint || !metadata.token_endpoint) {
|
|
@@ -110,7 +113,6 @@ async function authenticate(gatewayUrl) {
|
|
|
110
113
|
const registerEndpoint = metadata.registration_endpoint;
|
|
111
114
|
console.log(` Auth endpoint: ${authEndpoint}`);
|
|
112
115
|
console.log(` Token endpoint: ${tokenEndpoint}`);
|
|
113
|
-
// Register OAuth client
|
|
114
116
|
console.log(' Registering OAuth client...');
|
|
115
117
|
const { status: regStatus, data: clientInfo } = await fetchJson(registerEndpoint, {
|
|
116
118
|
method: 'POST',
|
|
@@ -130,7 +132,6 @@ async function authenticate(gatewayUrl) {
|
|
|
130
132
|
}
|
|
131
133
|
const clientId = clientInfo.client_id;
|
|
132
134
|
console.log(` Client ID: ${clientId}`);
|
|
133
|
-
// Build authorization URL with PKCE
|
|
134
135
|
const codeVerifier = generateCodeVerifier();
|
|
135
136
|
const codeChallenge = generateCodeChallenge(codeVerifier);
|
|
136
137
|
const state = crypto.randomBytes(16).toString('hex');
|
|
@@ -142,7 +143,6 @@ async function authenticate(gatewayUrl) {
|
|
|
142
143
|
authUrl.searchParams.set('state', state);
|
|
143
144
|
authUrl.searchParams.set('code_challenge', codeChallenge);
|
|
144
145
|
authUrl.searchParams.set('code_challenge_method', 'S256');
|
|
145
|
-
// Start callback server & open browser
|
|
146
146
|
const authCode = await new Promise((resolve, reject) => {
|
|
147
147
|
const server = http.createServer((req, res) => {
|
|
148
148
|
if (!req.url?.startsWith(CALLBACK_PATH)) {
|
|
@@ -190,14 +190,12 @@ async function authenticate(gatewayUrl) {
|
|
|
190
190
|
reject(err);
|
|
191
191
|
}
|
|
192
192
|
});
|
|
193
|
-
// Timeout after 5 minutes
|
|
194
193
|
setTimeout(() => {
|
|
195
194
|
server.close();
|
|
196
195
|
reject(new Error('Authentication timed out (5 minutes). Please try again.'));
|
|
197
196
|
}, 5 * 60 * 1000);
|
|
198
197
|
});
|
|
199
198
|
console.log(' Authorization code received. Exchanging for tokens...');
|
|
200
|
-
// Exchange code for tokens
|
|
201
199
|
const tokenBody = new URLSearchParams({
|
|
202
200
|
grant_type: 'authorization_code',
|
|
203
201
|
client_id: clientId,
|
|
@@ -221,84 +219,136 @@ async function authenticate(gatewayUrl) {
|
|
|
221
219
|
clientId,
|
|
222
220
|
};
|
|
223
221
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
222
|
+
async function fetchToolsList(gatewayUrl, accessToken) {
|
|
223
|
+
console.log(' Fetching tools from gateway...');
|
|
224
|
+
const { status, data } = await fetchJson(`${gatewayUrl}/mcp`, {
|
|
225
|
+
method: 'POST',
|
|
226
|
+
headers: {
|
|
227
|
+
'Content-Type': 'application/json',
|
|
228
|
+
Authorization: `Bearer ${accessToken}`,
|
|
229
|
+
},
|
|
230
|
+
body: JSON.stringify({
|
|
231
|
+
jsonrpc: '2.0',
|
|
232
|
+
id: 1,
|
|
233
|
+
method: 'tools/list',
|
|
234
|
+
params: {},
|
|
235
|
+
}),
|
|
236
|
+
});
|
|
237
|
+
if (status !== 200 || !data?.result?.tools) {
|
|
238
|
+
console.warn(` Warning: tools/list returned status ${status}`);
|
|
239
|
+
console.warn(` Response: ${JSON.stringify(data).slice(0, 200)}`);
|
|
240
|
+
return [];
|
|
241
|
+
}
|
|
242
|
+
const tools = data.result.tools;
|
|
243
|
+
const groups = {};
|
|
244
|
+
for (const t of tools) {
|
|
245
|
+
const prefix = t.name.split('_')[0];
|
|
246
|
+
groups[prefix] = (groups[prefix] || 0) + 1;
|
|
247
|
+
}
|
|
248
|
+
console.log(` Found ${tools.length} tools:`);
|
|
249
|
+
for (const [prefix, count] of Object.entries(groups).sort()) {
|
|
250
|
+
console.log(` ${prefix}: ${count} tools`);
|
|
251
|
+
}
|
|
252
|
+
return tools;
|
|
253
|
+
}
|
|
254
|
+
function escapeTs(s) {
|
|
255
|
+
return s
|
|
256
|
+
.replace(/\\/g, '\\\\')
|
|
257
|
+
.replace(/"/g, '\\"')
|
|
258
|
+
.replace(/\n/g, '\\n')
|
|
259
|
+
.replace(/\r/g, '\\r');
|
|
260
|
+
}
|
|
261
|
+
function generateMcpToolsPlugin(gatewayUrl, accessToken, tools) {
|
|
262
|
+
fs.mkdirSync(TOOLS_PLUGIN_DIR, { recursive: true });
|
|
263
|
+
// package.json
|
|
264
|
+
const pkg = {
|
|
265
|
+
name: TOOLS_PLUGIN_ID,
|
|
266
|
+
version: '1.0.0',
|
|
267
|
+
type: 'module',
|
|
268
|
+
openclaw: { extensions: ['./index.ts'] },
|
|
269
|
+
dependencies: {},
|
|
270
|
+
};
|
|
271
|
+
fs.writeFileSync(path.join(TOOLS_PLUGIN_DIR, 'package.json'), JSON.stringify(pkg, null, 2) + '\n');
|
|
272
|
+
// openclaw.plugin.json
|
|
273
|
+
const manifest = {
|
|
274
|
+
id: TOOLS_PLUGIN_ID,
|
|
275
|
+
name: 'VirtueAI MCP Tools',
|
|
276
|
+
description: `${tools.length} MCP tools from VirtueAI gateway`,
|
|
277
|
+
configSchema: { type: 'object', properties: {} },
|
|
278
|
+
};
|
|
279
|
+
fs.writeFileSync(path.join(TOOLS_PLUGIN_DIR, 'openclaw.plugin.json'), JSON.stringify(manifest, null, 2) + '\n');
|
|
280
|
+
// index.ts — register each tool as a native OpenClaw tool
|
|
281
|
+
const mcpUrl = JSON.stringify(gatewayUrl + '/mcp');
|
|
282
|
+
const token = JSON.stringify(accessToken);
|
|
283
|
+
const toolRegistrations = tools.map((tool) => {
|
|
284
|
+
const name = escapeTs(tool.name);
|
|
285
|
+
const description = escapeTs(tool.description || `Tool: ${tool.name}`);
|
|
286
|
+
const schema = JSON.stringify(tool.inputSchema || { type: 'object', properties: {} });
|
|
287
|
+
return `
|
|
288
|
+
api.registerTool({
|
|
289
|
+
name: "${name}",
|
|
290
|
+
description: "${description}",
|
|
291
|
+
parameters: ${schema},
|
|
292
|
+
async execute(_id: string, params: unknown) {
|
|
293
|
+
try {
|
|
294
|
+
const response = await fetch(GATEWAY_MCP_URL, {
|
|
295
|
+
method: "POST",
|
|
296
|
+
headers: { "Content-Type": "application/json", "Authorization": AUTH_HEADER },
|
|
297
|
+
body: JSON.stringify({
|
|
298
|
+
jsonrpc: "2.0",
|
|
299
|
+
id: Date.now(),
|
|
300
|
+
method: "tools/call",
|
|
301
|
+
params: { name: "${name}", arguments: params }
|
|
302
|
+
})
|
|
303
|
+
});
|
|
238
304
|
|
|
239
|
-
const
|
|
240
|
-
const TOKEN = ${JSON.stringify(accessToken)};
|
|
305
|
+
const result = await response.json();
|
|
241
306
|
|
|
242
|
-
|
|
243
|
-
|
|
307
|
+
if (result.error) {
|
|
308
|
+
return {
|
|
309
|
+
content: [{ type: "text", text: \`Error: \${result.error.message}\` }],
|
|
310
|
+
isError: true
|
|
311
|
+
};
|
|
312
|
+
}
|
|
244
313
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
try { msg = JSON.parse(buf); } catch { return; }
|
|
249
|
-
buf = "";
|
|
250
|
-
forward(msg);
|
|
251
|
-
});
|
|
314
|
+
const text = result.result?.content
|
|
315
|
+
?.map((c: any) => c.text ?? c.data ?? "")
|
|
316
|
+
.join("\\n") ?? JSON.stringify(result.result);
|
|
252
317
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
"Accept": "application/json, text/event-stream",
|
|
263
|
-
"Content-Length": Buffer.byteLength(body),
|
|
264
|
-
},
|
|
265
|
-
}, (res) => {
|
|
266
|
-
let data = "";
|
|
267
|
-
res.on("data", (c) => data += c);
|
|
268
|
-
res.on("end", () => {
|
|
269
|
-
if (data.trim()) {
|
|
270
|
-
process.stdout.write(data.trim() + "\\n");
|
|
318
|
+
return {
|
|
319
|
+
content: [{ type: "text", text }],
|
|
320
|
+
isError: false
|
|
321
|
+
};
|
|
322
|
+
} catch (err) {
|
|
323
|
+
return {
|
|
324
|
+
content: [{ type: "text", text: \`Error calling ${name}: \${err}\` }],
|
|
325
|
+
isError: true
|
|
326
|
+
};
|
|
271
327
|
}
|
|
272
|
-
});
|
|
273
|
-
});
|
|
274
|
-
req.on("error", (e) => {
|
|
275
|
-
if (msg.id != null) {
|
|
276
|
-
process.stdout.write(JSON.stringify({
|
|
277
|
-
jsonrpc: "2.0",
|
|
278
|
-
id: msg.id,
|
|
279
|
-
error: { code: -32000, message: "Proxy error: " + e.message },
|
|
280
|
-
}) + "\\n");
|
|
281
328
|
}
|
|
282
|
-
})
|
|
283
|
-
|
|
329
|
+
});`;
|
|
330
|
+
});
|
|
331
|
+
const pluginSource = `\
|
|
332
|
+
const GATEWAY_MCP_URL = ${mcpUrl};
|
|
333
|
+
const AUTH_HEADER = "Bearer " + ${token};
|
|
334
|
+
|
|
335
|
+
export default function (api: any) {
|
|
336
|
+
${toolRegistrations.join('\n')}
|
|
284
337
|
}
|
|
285
338
|
`;
|
|
286
|
-
fs.writeFileSync(
|
|
287
|
-
console.log(` Generated
|
|
339
|
+
fs.writeFileSync(path.join(TOOLS_PLUGIN_DIR, 'index.ts'), pluginSource);
|
|
340
|
+
console.log(` Generated MCP tools plugin: ${TOOLS_PLUGIN_DIR} (${tools.length} tools)`);
|
|
288
341
|
}
|
|
289
|
-
|
|
342
|
+
// ---------------------------------------------------------------------------
|
|
343
|
+
// Step 3: Write config & patch openclaw.json
|
|
344
|
+
// ---------------------------------------------------------------------------
|
|
345
|
+
function writeGatewayConfig(gatewayUrl, accessToken, guardUuid, apiUrl, gatewayId) {
|
|
290
346
|
fs.mkdirSync(OPENCLAW_DIR, { recursive: true });
|
|
291
|
-
generateMcpProxy(gatewayUrl, accessToken);
|
|
292
347
|
const config = {
|
|
293
|
-
mcpServers: {
|
|
294
|
-
virtueai: {
|
|
295
|
-
type: 'stdio',
|
|
296
|
-
command: 'node',
|
|
297
|
-
args: [MCP_PROXY_PATH],
|
|
298
|
-
},
|
|
299
|
-
},
|
|
300
348
|
trajectory: {
|
|
301
349
|
gatewayUrl,
|
|
350
|
+
apiUrl: apiUrl || DEFAULT_API_URL,
|
|
351
|
+
gatewayId: gatewayId || DEFAULT_GATEWAY_ID,
|
|
302
352
|
guardUuid: guardUuid || process.env.VIRTUEAI_GUARD_UUID || '',
|
|
303
353
|
},
|
|
304
354
|
_auth: {
|
|
@@ -309,10 +359,7 @@ function writeMcpConfig(gatewayUrl, accessToken, guardUuid) {
|
|
|
309
359
|
fs.writeFileSync(MCP_CONFIG_PATH, JSON.stringify(config, null, 2) + '\n');
|
|
310
360
|
console.log(` Written: ${MCP_CONFIG_PATH}`);
|
|
311
361
|
}
|
|
312
|
-
|
|
313
|
-
// Step 3: Patch openclaw.json
|
|
314
|
-
// ---------------------------------------------------------------------------
|
|
315
|
-
function patchOpenClawConfig() {
|
|
362
|
+
function patchOpenClawConfig(model) {
|
|
316
363
|
let config = {};
|
|
317
364
|
if (fs.existsSync(OPENCLAW_CONFIG_PATH)) {
|
|
318
365
|
try {
|
|
@@ -323,80 +370,64 @@ function patchOpenClawConfig() {
|
|
|
323
370
|
config = {};
|
|
324
371
|
}
|
|
325
372
|
}
|
|
326
|
-
// Ensure nested structure exists
|
|
327
373
|
if (!config.agents)
|
|
328
374
|
config.agents = {};
|
|
329
375
|
if (!config.agents.defaults)
|
|
330
376
|
config.agents.defaults = {};
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
config.agents.defaults.model = { primary: 'claude-cli/sonnet' };
|
|
377
|
+
if (model) {
|
|
378
|
+
config.agents.defaults.model = { primary: model };
|
|
334
379
|
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
config.agents.defaults.
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
380
|
+
else if (!config.agents.defaults.model ||
|
|
381
|
+
config.agents.defaults.model?.primary?.startsWith('claude-cli/')) {
|
|
382
|
+
config.agents.defaults.model = { primary: 'anthropic/claude-opus-4-6' };
|
|
383
|
+
}
|
|
384
|
+
// Clear models allowlist so users can switch to any model in OpenClaw
|
|
385
|
+
config.agents.defaults.models = {};
|
|
386
|
+
// Enable virtueai-mcp-tools plugin
|
|
387
|
+
if (!config.plugins)
|
|
388
|
+
config.plugins = {};
|
|
389
|
+
if (!config.plugins.entries)
|
|
390
|
+
config.plugins.entries = {};
|
|
391
|
+
config.plugins.entries[TOOLS_PLUGIN_ID] = { enabled: true };
|
|
392
|
+
if (!config.plugins.allow)
|
|
393
|
+
config.plugins.allow = [];
|
|
394
|
+
if (!config.plugins.allow.includes(TOOLS_PLUGIN_ID)) {
|
|
395
|
+
config.plugins.allow.push(TOOLS_PLUGIN_ID);
|
|
342
396
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
397
|
+
if (!config.plugins.installs)
|
|
398
|
+
config.plugins.installs = {};
|
|
399
|
+
config.plugins.installs[TOOLS_PLUGIN_ID] = {
|
|
400
|
+
source: 'path',
|
|
401
|
+
sourcePath: TOOLS_PLUGIN_DIR,
|
|
402
|
+
installPath: TOOLS_PLUGIN_DIR,
|
|
403
|
+
version: '1.0.0',
|
|
404
|
+
installedAt: new Date().toISOString(),
|
|
405
|
+
};
|
|
406
|
+
// Add plugin tools to agent allowlist (required for OpenClaw to expose them)
|
|
407
|
+
// Follows the pattern from DecodingTrust-Agent:
|
|
408
|
+
// agents.list[{id: "main", tools: {allow: ["group:plugins", pluginId]}}]
|
|
409
|
+
if (!config.agents.list)
|
|
410
|
+
config.agents.list = [];
|
|
411
|
+
let mainAgent = config.agents.list.find((a) => a.id === 'main');
|
|
412
|
+
if (!mainAgent) {
|
|
413
|
+
mainAgent = { id: 'main' };
|
|
414
|
+
config.agents.list.push(mainAgent);
|
|
347
415
|
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
416
|
+
if (!mainAgent.tools)
|
|
417
|
+
mainAgent.tools = {};
|
|
418
|
+
if (!mainAgent.tools.allow)
|
|
419
|
+
mainAgent.tools.allow = [];
|
|
420
|
+
const allowlist = mainAgent.tools.allow;
|
|
421
|
+
if (!allowlist.includes('group:plugins')) {
|
|
422
|
+
allowlist.push('group:plugins');
|
|
351
423
|
}
|
|
352
|
-
|
|
353
|
-
|
|
424
|
+
if (!allowlist.includes(TOOLS_PLUGIN_ID)) {
|
|
425
|
+
allowlist.push(TOOLS_PLUGIN_ID);
|
|
354
426
|
}
|
|
355
|
-
// Write back
|
|
356
427
|
fs.writeFileSync(OPENCLAW_CONFIG_PATH, JSON.stringify(config, null, 2) + '\n');
|
|
357
428
|
console.log(` Patched: ${OPENCLAW_CONFIG_PATH}`);
|
|
358
429
|
}
|
|
359
430
|
// ---------------------------------------------------------------------------
|
|
360
|
-
// Step 4: Verify connection
|
|
361
|
-
// ---------------------------------------------------------------------------
|
|
362
|
-
async function verifyConnection(gatewayUrl, accessToken) {
|
|
363
|
-
console.log(' Verifying token with gateway...');
|
|
364
|
-
const { status, data } = await fetchJson(`${gatewayUrl}/mcp`, {
|
|
365
|
-
method: 'POST',
|
|
366
|
-
headers: {
|
|
367
|
-
'Content-Type': 'application/json',
|
|
368
|
-
Authorization: `Bearer ${accessToken}`,
|
|
369
|
-
},
|
|
370
|
-
body: JSON.stringify({
|
|
371
|
-
jsonrpc: '2.0',
|
|
372
|
-
id: 1,
|
|
373
|
-
method: 'tools/list',
|
|
374
|
-
params: {},
|
|
375
|
-
}),
|
|
376
|
-
});
|
|
377
|
-
if (status === 200 && data?.result?.tools) {
|
|
378
|
-
const tools = data.result.tools;
|
|
379
|
-
const toolCount = tools.length;
|
|
380
|
-
// Group by prefix
|
|
381
|
-
const groups = {};
|
|
382
|
-
for (const t of tools) {
|
|
383
|
-
const prefix = t.name.split('_')[0];
|
|
384
|
-
groups[prefix] = (groups[prefix] || 0) + 1;
|
|
385
|
-
}
|
|
386
|
-
console.log(` Verified! ${toolCount} tools available:`);
|
|
387
|
-
for (const [prefix, count] of Object.entries(groups).sort()) {
|
|
388
|
-
console.log(` ${prefix}: ${count} tools`);
|
|
389
|
-
}
|
|
390
|
-
return toolCount;
|
|
391
|
-
}
|
|
392
|
-
else {
|
|
393
|
-
console.warn(` Warning: verification returned status ${status}`);
|
|
394
|
-
console.warn(` Response: ${JSON.stringify(data).slice(0, 200)}`);
|
|
395
|
-
console.warn(' Token was saved but may not work yet.');
|
|
396
|
-
return 0;
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
// ---------------------------------------------------------------------------
|
|
400
431
|
// Main
|
|
401
432
|
// ---------------------------------------------------------------------------
|
|
402
433
|
async function main() {
|
|
@@ -408,51 +439,72 @@ Usage:
|
|
|
408
439
|
npx @virtue-ai/gateway-connect [options]
|
|
409
440
|
|
|
410
441
|
Options:
|
|
411
|
-
--gateway-url <url>
|
|
442
|
+
--gateway-url <url> MCP gateway URL (default: ${DEFAULT_GATEWAY_URL})
|
|
443
|
+
--api-url <url> Prompt-guard API URL (default: ${DEFAULT_API_URL})
|
|
444
|
+
--gateway-id <id> Gateway ID for trajectory (default: ${DEFAULT_GATEWAY_ID})
|
|
445
|
+
--model <model> Model to use (e.g. openai/gpt-4o, anthropic/claude-sonnet-4-5)
|
|
412
446
|
--guard-uuid <uuid> Guard UUID for trajectory recording (or set VIRTUEAI_GUARD_UUID)
|
|
413
447
|
--help Show this help message
|
|
414
448
|
|
|
415
449
|
What it does:
|
|
416
450
|
1. Opens browser for OAuth login
|
|
417
|
-
2.
|
|
418
|
-
3.
|
|
419
|
-
4.
|
|
420
|
-
5.
|
|
451
|
+
2. Fetches all MCP tools from the gateway
|
|
452
|
+
3. Generates a native OpenClaw plugin wrapping each tool
|
|
453
|
+
4. Patches ~/.openclaw/openclaw.json to enable the plugin
|
|
454
|
+
5. Installs trajectory plugin for full session recording
|
|
455
|
+
|
|
456
|
+
Supported models:
|
|
457
|
+
Any embedded model supported by OpenClaw (NOT claude-cli).
|
|
458
|
+
Examples: openai/gpt-4o, anthropic/claude-sonnet-4-5
|
|
421
459
|
`);
|
|
422
460
|
process.exit(0);
|
|
423
461
|
}
|
|
424
462
|
let gatewayUrl = getArg('gateway-url') || DEFAULT_GATEWAY_URL;
|
|
463
|
+
const model = getArg('model');
|
|
425
464
|
const guardUuid = getArg('guard-uuid') || process.env.VIRTUEAI_GUARD_UUID;
|
|
426
|
-
|
|
465
|
+
const apiUrl = getArg('api-url') || DEFAULT_API_URL;
|
|
466
|
+
const gatewayId = getArg('gateway-id') || DEFAULT_GATEWAY_ID;
|
|
427
467
|
gatewayUrl = gatewayUrl.replace(/\/mcp\/?$/, '').toLowerCase();
|
|
428
468
|
console.log('\n VirtueAI Gateway Connect\n');
|
|
429
469
|
console.log(` Gateway: ${gatewayUrl}`);
|
|
470
|
+
if (model)
|
|
471
|
+
console.log(` Model: ${model}`);
|
|
430
472
|
// Step 1: Authenticate
|
|
431
473
|
const { accessToken } = await authenticate(gatewayUrl);
|
|
432
|
-
// Step 2:
|
|
474
|
+
// Step 2: Fetch tools & generate native plugin
|
|
433
475
|
console.log('\n Configuring OpenClaw...');
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
476
|
+
const tools = await fetchToolsList(gatewayUrl, accessToken);
|
|
477
|
+
if (tools.length === 0) {
|
|
478
|
+
console.error('\n Error: No tools found on gateway. Token may be invalid.');
|
|
479
|
+
process.exit(1);
|
|
480
|
+
}
|
|
481
|
+
generateMcpToolsPlugin(gatewayUrl, accessToken, tools);
|
|
482
|
+
// Step 3: Write gateway config (for trajectory plugin)
|
|
483
|
+
writeGatewayConfig(gatewayUrl, accessToken, guardUuid, apiUrl, gatewayId);
|
|
484
|
+
// Step 4: Patch openclaw.json (enable tools plugin + set model)
|
|
485
|
+
patchOpenClawConfig(model);
|
|
486
|
+
// Step 5: Install trajectory plugin
|
|
438
487
|
console.log('\n Setting up trajectory recording...');
|
|
439
488
|
generateTrajectoryPlugin(guardUuid);
|
|
440
489
|
enableTrajectoryPlugin();
|
|
441
|
-
// Step 5: Verify
|
|
442
|
-
console.log('');
|
|
443
|
-
const toolCount = await verifyConnection(gatewayUrl, accessToken);
|
|
444
490
|
// Done
|
|
491
|
+
const modelDisplay = model || 'existing (unchanged)';
|
|
445
492
|
console.log(`
|
|
446
493
|
Done! OpenClaw is now connected to VirtueAI MCP gateway.
|
|
447
|
-
${
|
|
494
|
+
${tools.length} tools registered as native OpenClaw tools.
|
|
495
|
+
Model: ${modelDisplay}
|
|
448
496
|
Trajectory recording enabled (via virtueai-trajectory plugin).
|
|
449
497
|
|
|
450
498
|
Config files:
|
|
451
499
|
${MCP_CONFIG_PATH}
|
|
452
500
|
${OPENCLAW_CONFIG_PATH}
|
|
501
|
+
${TOOLS_PLUGIN_DIR}
|
|
453
502
|
|
|
454
503
|
Start using it:
|
|
455
504
|
openclaw agent --local --message "What tools do you have?"
|
|
505
|
+
|
|
506
|
+
To use a different model:
|
|
507
|
+
npx @virtue-ai/gateway-connect --gateway-url ${gatewayUrl} --model openai/gpt-4o
|
|
456
508
|
`);
|
|
457
509
|
process.exit(0);
|
|
458
510
|
}
|
|
@@ -49,6 +49,8 @@ function loadConfig() {
|
|
|
49
49
|
const cfg = JSON.parse(raw);
|
|
50
50
|
|
|
51
51
|
const gatewayUrl = cfg._auth?.gatewayUrl ?? cfg.trajectory?.gatewayUrl ?? "";
|
|
52
|
+
const apiUrl = cfg.trajectory?.apiUrl ?? gatewayUrl;
|
|
53
|
+
const gatewayId = cfg.trajectory?.gatewayId ?? "";
|
|
52
54
|
const token = cfg._auth?.accessToken ?? "";
|
|
53
55
|
|
|
54
56
|
const guardUuid =
|
|
@@ -56,8 +58,8 @@ function loadConfig() {
|
|
|
56
58
|
process.env.VIRTUEAI_GUARD_UUID ||
|
|
57
59
|
DEFAULT_GUARD_UUID;
|
|
58
60
|
|
|
59
|
-
if (!
|
|
60
|
-
return { gatewayUrl, token, guardUuid };
|
|
61
|
+
if (!apiUrl || !token) return null;
|
|
62
|
+
return { gatewayUrl, apiUrl, gatewayId, token, guardUuid };
|
|
61
63
|
} catch {
|
|
62
64
|
return null;
|
|
63
65
|
}
|
|
@@ -84,7 +86,7 @@ const plugin = {
|
|
|
84
86
|
|
|
85
87
|
let gatewaySessionId = null;
|
|
86
88
|
let endpointDisabled = false;
|
|
87
|
-
const endpoint = config.
|
|
89
|
+
const endpoint = config.apiUrl + "/api/prompt-guard/topic_guard";
|
|
88
90
|
|
|
89
91
|
api.logger.info("[virtueai-trajectory] Plugin registered, sending to " + config.gatewayUrl);
|
|
90
92
|
|
|
@@ -94,6 +96,7 @@ const plugin = {
|
|
|
94
96
|
const body = {
|
|
95
97
|
user_prompt: truncate(content),
|
|
96
98
|
guard_uuid: config.guardUuid,
|
|
99
|
+
gateway_id: config.gatewayId,
|
|
97
100
|
role,
|
|
98
101
|
};
|
|
99
102
|
if (gatewaySessionId) {
|