@virtue-ai/gateway-connect 0.2.0 → 0.3.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 +68 -20
- package/dist/index.d.ts +4 -3
- package/dist/index.js +209 -103
- package/dist/trajectory-plugin.js +28 -7
- 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,39 +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
|
|
42
|
+
|
|
43
|
+
### Step 4: Start Using
|
|
28
44
|
|
|
29
|
-
|
|
45
|
+
One-shot mode:
|
|
30
46
|
|
|
31
47
|
```bash
|
|
32
|
-
openclaw agent --local --
|
|
48
|
+
openclaw agent --local --message "What tools do you have?"
|
|
33
49
|
```
|
|
34
50
|
|
|
35
|
-
|
|
51
|
+
Interactive TUI mode:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
# Terminal 1: start the OpenClaw gateway
|
|
55
|
+
openclaw gateway
|
|
56
|
+
|
|
57
|
+
# Terminal 2: open the TUI
|
|
58
|
+
openclaw tui
|
|
59
|
+
```
|
|
60
|
+
|
|
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.
|
|
36
78
|
|
|
37
79
|
## What It Does
|
|
38
80
|
|
|
39
|
-
`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
|
|
40
86
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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 |
|
|
46
93
|
|
|
47
94
|
## Options
|
|
48
95
|
|
|
@@ -50,14 +97,15 @@ That's it. OpenClaw now has access to all MCP tools on the gateway (GitHub, Goog
|
|
|
50
97
|
npx @virtue-ai/gateway-connect [options]
|
|
51
98
|
|
|
52
99
|
Options:
|
|
53
|
-
--gateway-url <url> Gateway URL (
|
|
54
|
-
--
|
|
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)
|
|
55
103
|
--help Show help message
|
|
56
104
|
```
|
|
57
105
|
|
|
58
106
|
## Re-authentication
|
|
59
107
|
|
|
60
|
-
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.
|
|
61
109
|
|
|
62
110
|
## License
|
|
63
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,6 +31,8 @@ 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');
|
|
34
|
+
const TOOLS_PLUGIN_ID = 'virtueai-mcp-tools';
|
|
35
|
+
const TOOLS_PLUGIN_DIR = path.join(OPENCLAW_DIR, 'extensions', TOOLS_PLUGIN_ID);
|
|
33
36
|
const DEFAULT_GATEWAY_URL = 'https://virtueai-agent-gtw-l3phon63.ngrok.io';
|
|
34
37
|
// ---------------------------------------------------------------------------
|
|
35
38
|
// Helpers
|
|
@@ -96,7 +99,6 @@ function openBrowser(url) {
|
|
|
96
99
|
// Step 1: OAuth PKCE Authentication
|
|
97
100
|
// ---------------------------------------------------------------------------
|
|
98
101
|
async function authenticate(gatewayUrl) {
|
|
99
|
-
// Discover OAuth metadata
|
|
100
102
|
console.log(' Discovering OAuth endpoints...');
|
|
101
103
|
const { data: metadata } = await fetchJson(`${gatewayUrl}/.well-known/oauth-authorization-server`, { method: 'GET' });
|
|
102
104
|
if (!metadata.authorization_endpoint || !metadata.token_endpoint) {
|
|
@@ -109,7 +111,6 @@ async function authenticate(gatewayUrl) {
|
|
|
109
111
|
const registerEndpoint = metadata.registration_endpoint;
|
|
110
112
|
console.log(` Auth endpoint: ${authEndpoint}`);
|
|
111
113
|
console.log(` Token endpoint: ${tokenEndpoint}`);
|
|
112
|
-
// Register OAuth client
|
|
113
114
|
console.log(' Registering OAuth client...');
|
|
114
115
|
const { status: regStatus, data: clientInfo } = await fetchJson(registerEndpoint, {
|
|
115
116
|
method: 'POST',
|
|
@@ -129,7 +130,6 @@ async function authenticate(gatewayUrl) {
|
|
|
129
130
|
}
|
|
130
131
|
const clientId = clientInfo.client_id;
|
|
131
132
|
console.log(` Client ID: ${clientId}`);
|
|
132
|
-
// Build authorization URL with PKCE
|
|
133
133
|
const codeVerifier = generateCodeVerifier();
|
|
134
134
|
const codeChallenge = generateCodeChallenge(codeVerifier);
|
|
135
135
|
const state = crypto.randomBytes(16).toString('hex');
|
|
@@ -141,7 +141,6 @@ async function authenticate(gatewayUrl) {
|
|
|
141
141
|
authUrl.searchParams.set('state', state);
|
|
142
142
|
authUrl.searchParams.set('code_challenge', codeChallenge);
|
|
143
143
|
authUrl.searchParams.set('code_challenge_method', 'S256');
|
|
144
|
-
// Start callback server & open browser
|
|
145
144
|
const authCode = await new Promise((resolve, reject) => {
|
|
146
145
|
const server = http.createServer((req, res) => {
|
|
147
146
|
if (!req.url?.startsWith(CALLBACK_PATH)) {
|
|
@@ -189,14 +188,12 @@ async function authenticate(gatewayUrl) {
|
|
|
189
188
|
reject(err);
|
|
190
189
|
}
|
|
191
190
|
});
|
|
192
|
-
// Timeout after 5 minutes
|
|
193
191
|
setTimeout(() => {
|
|
194
192
|
server.close();
|
|
195
193
|
reject(new Error('Authentication timed out (5 minutes). Please try again.'));
|
|
196
194
|
}, 5 * 60 * 1000);
|
|
197
195
|
});
|
|
198
196
|
console.log(' Authorization code received. Exchanging for tokens...');
|
|
199
|
-
// Exchange code for tokens
|
|
200
197
|
const tokenBody = new URLSearchParams({
|
|
201
198
|
grant_type: 'authorization_code',
|
|
202
199
|
client_id: clientId,
|
|
@@ -220,33 +217,145 @@ async function authenticate(gatewayUrl) {
|
|
|
220
217
|
clientId,
|
|
221
218
|
};
|
|
222
219
|
}
|
|
220
|
+
async function fetchToolsList(gatewayUrl, accessToken) {
|
|
221
|
+
console.log(' Fetching tools from gateway...');
|
|
222
|
+
const { status, data } = await fetchJson(`${gatewayUrl}/mcp`, {
|
|
223
|
+
method: 'POST',
|
|
224
|
+
headers: {
|
|
225
|
+
'Content-Type': 'application/json',
|
|
226
|
+
Authorization: `Bearer ${accessToken}`,
|
|
227
|
+
},
|
|
228
|
+
body: JSON.stringify({
|
|
229
|
+
jsonrpc: '2.0',
|
|
230
|
+
id: 1,
|
|
231
|
+
method: 'tools/list',
|
|
232
|
+
params: {},
|
|
233
|
+
}),
|
|
234
|
+
});
|
|
235
|
+
if (status !== 200 || !data?.result?.tools) {
|
|
236
|
+
console.warn(` Warning: tools/list returned status ${status}`);
|
|
237
|
+
console.warn(` Response: ${JSON.stringify(data).slice(0, 200)}`);
|
|
238
|
+
return [];
|
|
239
|
+
}
|
|
240
|
+
const tools = data.result.tools;
|
|
241
|
+
const groups = {};
|
|
242
|
+
for (const t of tools) {
|
|
243
|
+
const prefix = t.name.split('_')[0];
|
|
244
|
+
groups[prefix] = (groups[prefix] || 0) + 1;
|
|
245
|
+
}
|
|
246
|
+
console.log(` Found ${tools.length} tools:`);
|
|
247
|
+
for (const [prefix, count] of Object.entries(groups).sort()) {
|
|
248
|
+
console.log(` ${prefix}: ${count} tools`);
|
|
249
|
+
}
|
|
250
|
+
return tools;
|
|
251
|
+
}
|
|
252
|
+
function escapeTs(s) {
|
|
253
|
+
return s
|
|
254
|
+
.replace(/\\/g, '\\\\')
|
|
255
|
+
.replace(/"/g, '\\"')
|
|
256
|
+
.replace(/\n/g, '\\n')
|
|
257
|
+
.replace(/\r/g, '\\r');
|
|
258
|
+
}
|
|
259
|
+
function generateMcpToolsPlugin(gatewayUrl, accessToken, tools) {
|
|
260
|
+
fs.mkdirSync(TOOLS_PLUGIN_DIR, { recursive: true });
|
|
261
|
+
// package.json
|
|
262
|
+
const pkg = {
|
|
263
|
+
name: TOOLS_PLUGIN_ID,
|
|
264
|
+
version: '1.0.0',
|
|
265
|
+
type: 'module',
|
|
266
|
+
openclaw: { extensions: ['./index.ts'] },
|
|
267
|
+
dependencies: {},
|
|
268
|
+
};
|
|
269
|
+
fs.writeFileSync(path.join(TOOLS_PLUGIN_DIR, 'package.json'), JSON.stringify(pkg, null, 2) + '\n');
|
|
270
|
+
// openclaw.plugin.json
|
|
271
|
+
const manifest = {
|
|
272
|
+
id: TOOLS_PLUGIN_ID,
|
|
273
|
+
name: 'VirtueAI MCP Tools',
|
|
274
|
+
description: `${tools.length} MCP tools from VirtueAI gateway`,
|
|
275
|
+
configSchema: { type: 'object', properties: {} },
|
|
276
|
+
};
|
|
277
|
+
fs.writeFileSync(path.join(TOOLS_PLUGIN_DIR, 'openclaw.plugin.json'), JSON.stringify(manifest, null, 2) + '\n');
|
|
278
|
+
// index.ts — register each tool as a native OpenClaw tool
|
|
279
|
+
const mcpUrl = JSON.stringify(gatewayUrl + '/mcp');
|
|
280
|
+
const token = JSON.stringify(accessToken);
|
|
281
|
+
const toolRegistrations = tools.map((tool) => {
|
|
282
|
+
const name = escapeTs(tool.name);
|
|
283
|
+
const description = escapeTs(tool.description || `Tool: ${tool.name}`);
|
|
284
|
+
const schema = JSON.stringify(tool.inputSchema || { type: 'object', properties: {} });
|
|
285
|
+
return `
|
|
286
|
+
api.registerTool({
|
|
287
|
+
name: "${name}",
|
|
288
|
+
description: "${description}",
|
|
289
|
+
parameters: ${schema},
|
|
290
|
+
async execute(_id: string, params: unknown) {
|
|
291
|
+
try {
|
|
292
|
+
const response = await fetch(GATEWAY_MCP_URL, {
|
|
293
|
+
method: "POST",
|
|
294
|
+
headers: { "Content-Type": "application/json", "Authorization": AUTH_HEADER },
|
|
295
|
+
body: JSON.stringify({
|
|
296
|
+
jsonrpc: "2.0",
|
|
297
|
+
id: Date.now(),
|
|
298
|
+
method: "tools/call",
|
|
299
|
+
params: { name: "${name}", arguments: params }
|
|
300
|
+
})
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const result = await response.json();
|
|
304
|
+
|
|
305
|
+
if (result.error) {
|
|
306
|
+
return {
|
|
307
|
+
content: [{ type: "text", text: \`Error: \${result.error.message}\` }],
|
|
308
|
+
isError: true
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const text = result.result?.content
|
|
313
|
+
?.map((c: any) => c.text ?? c.data ?? "")
|
|
314
|
+
.join("\\n") ?? JSON.stringify(result.result);
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
content: [{ type: "text", text }],
|
|
318
|
+
isError: false
|
|
319
|
+
};
|
|
320
|
+
} catch (err) {
|
|
321
|
+
return {
|
|
322
|
+
content: [{ type: "text", text: \`Error calling ${name}: \${err}\` }],
|
|
323
|
+
isError: true
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
});`;
|
|
328
|
+
});
|
|
329
|
+
const pluginSource = `\
|
|
330
|
+
const GATEWAY_MCP_URL = ${mcpUrl};
|
|
331
|
+
const AUTH_HEADER = "Bearer " + ${token};
|
|
332
|
+
|
|
333
|
+
export default function (api: any) {
|
|
334
|
+
${toolRegistrations.join('\n')}
|
|
335
|
+
}
|
|
336
|
+
`;
|
|
337
|
+
fs.writeFileSync(path.join(TOOLS_PLUGIN_DIR, 'index.ts'), pluginSource);
|
|
338
|
+
console.log(` Generated MCP tools plugin: ${TOOLS_PLUGIN_DIR} (${tools.length} tools)`);
|
|
339
|
+
}
|
|
223
340
|
// ---------------------------------------------------------------------------
|
|
224
|
-
// Step
|
|
341
|
+
// Step 3: Write config & patch openclaw.json
|
|
225
342
|
// ---------------------------------------------------------------------------
|
|
226
|
-
function
|
|
343
|
+
function writeGatewayConfig(gatewayUrl, accessToken, guardUuid) {
|
|
227
344
|
fs.mkdirSync(OPENCLAW_DIR, { recursive: true });
|
|
228
345
|
const config = {
|
|
229
|
-
mcpServers: {
|
|
230
|
-
virtueai: {
|
|
231
|
-
type: 'http',
|
|
232
|
-
url: `${gatewayUrl}/mcp`,
|
|
233
|
-
headers: {
|
|
234
|
-
Authorization: `Bearer ${accessToken}`,
|
|
235
|
-
},
|
|
236
|
-
},
|
|
237
|
-
},
|
|
238
346
|
trajectory: {
|
|
239
347
|
gatewayUrl,
|
|
240
348
|
guardUuid: guardUuid || process.env.VIRTUEAI_GUARD_UUID || '',
|
|
241
349
|
},
|
|
350
|
+
_auth: {
|
|
351
|
+
gatewayUrl,
|
|
352
|
+
accessToken,
|
|
353
|
+
},
|
|
242
354
|
};
|
|
243
355
|
fs.writeFileSync(MCP_CONFIG_PATH, JSON.stringify(config, null, 2) + '\n');
|
|
244
356
|
console.log(` Written: ${MCP_CONFIG_PATH}`);
|
|
245
357
|
}
|
|
246
|
-
|
|
247
|
-
// Step 3: Patch openclaw.json
|
|
248
|
-
// ---------------------------------------------------------------------------
|
|
249
|
-
function patchOpenClawConfig() {
|
|
358
|
+
function patchOpenClawConfig(model) {
|
|
250
359
|
let config = {};
|
|
251
360
|
if (fs.existsSync(OPENCLAW_CONFIG_PATH)) {
|
|
252
361
|
try {
|
|
@@ -257,84 +366,64 @@ function patchOpenClawConfig() {
|
|
|
257
366
|
config = {};
|
|
258
367
|
}
|
|
259
368
|
}
|
|
260
|
-
// Ensure nested structure exists
|
|
261
369
|
if (!config.agents)
|
|
262
370
|
config.agents = {};
|
|
263
371
|
if (!config.agents.defaults)
|
|
264
372
|
config.agents.defaults = {};
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
config.agents.defaults.model = { primary: 'claude-cli/sonnet' };
|
|
373
|
+
if (model) {
|
|
374
|
+
config.agents.defaults.model = { primary: model };
|
|
268
375
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
config.agents.defaults.
|
|
272
|
-
if (!config.agents.defaults.cliBackends['claude-cli']) {
|
|
273
|
-
config.agents.defaults.cliBackends['claude-cli'] = {
|
|
274
|
-
command: 'claude',
|
|
275
|
-
args: ['-p', '--output-format', 'json'],
|
|
276
|
-
};
|
|
376
|
+
else if (!config.agents.defaults.model ||
|
|
377
|
+
config.agents.defaults.model?.primary?.startsWith('claude-cli/')) {
|
|
378
|
+
config.agents.defaults.model = { primary: 'anthropic/claude-opus-4-6' };
|
|
277
379
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
380
|
+
// Clear models allowlist so users can switch to any model in OpenClaw
|
|
381
|
+
config.agents.defaults.models = {};
|
|
382
|
+
// Enable virtueai-mcp-tools plugin
|
|
383
|
+
if (!config.plugins)
|
|
384
|
+
config.plugins = {};
|
|
385
|
+
if (!config.plugins.entries)
|
|
386
|
+
config.plugins.entries = {};
|
|
387
|
+
config.plugins.entries[TOOLS_PLUGIN_ID] = { enabled: true };
|
|
388
|
+
if (!config.plugins.allow)
|
|
389
|
+
config.plugins.allow = [];
|
|
390
|
+
if (!config.plugins.allow.includes(TOOLS_PLUGIN_ID)) {
|
|
391
|
+
config.plugins.allow.push(TOOLS_PLUGIN_ID);
|
|
392
|
+
}
|
|
393
|
+
if (!config.plugins.installs)
|
|
394
|
+
config.plugins.installs = {};
|
|
395
|
+
config.plugins.installs[TOOLS_PLUGIN_ID] = {
|
|
396
|
+
source: 'path',
|
|
397
|
+
sourcePath: TOOLS_PLUGIN_DIR,
|
|
398
|
+
installPath: TOOLS_PLUGIN_DIR,
|
|
399
|
+
version: '1.0.0',
|
|
400
|
+
installedAt: new Date().toISOString(),
|
|
401
|
+
};
|
|
402
|
+
// Add plugin tools to agent allowlist (required for OpenClaw to expose them)
|
|
403
|
+
// Follows the pattern from DecodingTrust-Agent:
|
|
404
|
+
// agents.list[{id: "main", tools: {allow: ["group:plugins", pluginId]}}]
|
|
405
|
+
if (!config.agents.list)
|
|
406
|
+
config.agents.list = [];
|
|
407
|
+
let mainAgent = config.agents.list.find((a) => a.id === 'main');
|
|
408
|
+
if (!mainAgent) {
|
|
409
|
+
mainAgent = { id: 'main' };
|
|
410
|
+
config.agents.list.push(mainAgent);
|
|
282
411
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
if (
|
|
286
|
-
|
|
287
|
-
|
|
412
|
+
if (!mainAgent.tools)
|
|
413
|
+
mainAgent.tools = {};
|
|
414
|
+
if (!mainAgent.tools.allow)
|
|
415
|
+
mainAgent.tools.allow = [];
|
|
416
|
+
const allowlist = mainAgent.tools.allow;
|
|
417
|
+
if (!allowlist.includes('group:plugins')) {
|
|
418
|
+
allowlist.push('group:plugins');
|
|
288
419
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
cliBackend.args.push('--mcp-config', MCP_CONFIG_PATH);
|
|
420
|
+
if (!allowlist.includes(TOOLS_PLUGIN_ID)) {
|
|
421
|
+
allowlist.push(TOOLS_PLUGIN_ID);
|
|
292
422
|
}
|
|
293
|
-
// Write back
|
|
294
423
|
fs.writeFileSync(OPENCLAW_CONFIG_PATH, JSON.stringify(config, null, 2) + '\n');
|
|
295
424
|
console.log(` Patched: ${OPENCLAW_CONFIG_PATH}`);
|
|
296
425
|
}
|
|
297
426
|
// ---------------------------------------------------------------------------
|
|
298
|
-
// Step 4: Verify connection
|
|
299
|
-
// ---------------------------------------------------------------------------
|
|
300
|
-
async function verifyConnection(gatewayUrl, accessToken) {
|
|
301
|
-
console.log(' Verifying token with gateway...');
|
|
302
|
-
const { status, data } = await fetchJson(`${gatewayUrl}/mcp`, {
|
|
303
|
-
method: 'POST',
|
|
304
|
-
headers: {
|
|
305
|
-
'Content-Type': 'application/json',
|
|
306
|
-
Authorization: `Bearer ${accessToken}`,
|
|
307
|
-
},
|
|
308
|
-
body: JSON.stringify({
|
|
309
|
-
jsonrpc: '2.0',
|
|
310
|
-
id: 1,
|
|
311
|
-
method: 'tools/list',
|
|
312
|
-
params: {},
|
|
313
|
-
}),
|
|
314
|
-
});
|
|
315
|
-
if (status === 200 && data?.result?.tools) {
|
|
316
|
-
const tools = data.result.tools;
|
|
317
|
-
const toolCount = tools.length;
|
|
318
|
-
// Group by prefix
|
|
319
|
-
const groups = {};
|
|
320
|
-
for (const t of tools) {
|
|
321
|
-
const prefix = t.name.split('_')[0];
|
|
322
|
-
groups[prefix] = (groups[prefix] || 0) + 1;
|
|
323
|
-
}
|
|
324
|
-
console.log(` Verified! ${toolCount} tools available:`);
|
|
325
|
-
for (const [prefix, count] of Object.entries(groups).sort()) {
|
|
326
|
-
console.log(` ${prefix}: ${count} tools`);
|
|
327
|
-
}
|
|
328
|
-
return toolCount;
|
|
329
|
-
}
|
|
330
|
-
else {
|
|
331
|
-
console.warn(` Warning: verification returned status ${status}`);
|
|
332
|
-
console.warn(` Response: ${JSON.stringify(data).slice(0, 200)}`);
|
|
333
|
-
console.warn(' Token was saved but may not work yet.');
|
|
334
|
-
return 0;
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
// ---------------------------------------------------------------------------
|
|
338
427
|
// Main
|
|
339
428
|
// ---------------------------------------------------------------------------
|
|
340
429
|
async function main() {
|
|
@@ -347,50 +436,67 @@ Usage:
|
|
|
347
436
|
|
|
348
437
|
Options:
|
|
349
438
|
--gateway-url <url> Gateway URL (default: ${DEFAULT_GATEWAY_URL})
|
|
439
|
+
--model <model> Model to use (e.g. openai/gpt-4o, anthropic/claude-sonnet-4-5)
|
|
350
440
|
--guard-uuid <uuid> Guard UUID for trajectory recording (or set VIRTUEAI_GUARD_UUID)
|
|
351
441
|
--help Show this help message
|
|
352
442
|
|
|
353
443
|
What it does:
|
|
354
444
|
1. Opens browser for OAuth login
|
|
355
|
-
2.
|
|
356
|
-
3.
|
|
357
|
-
4.
|
|
358
|
-
5.
|
|
445
|
+
2. Fetches all MCP tools from the gateway
|
|
446
|
+
3. Generates a native OpenClaw plugin wrapping each tool
|
|
447
|
+
4. Patches ~/.openclaw/openclaw.json to enable the plugin
|
|
448
|
+
5. Installs trajectory plugin for full session recording
|
|
449
|
+
|
|
450
|
+
Supported models:
|
|
451
|
+
Any embedded model supported by OpenClaw (NOT claude-cli).
|
|
452
|
+
Examples: openai/gpt-4o, anthropic/claude-sonnet-4-5
|
|
359
453
|
`);
|
|
360
454
|
process.exit(0);
|
|
361
455
|
}
|
|
362
456
|
let gatewayUrl = getArg('gateway-url') || DEFAULT_GATEWAY_URL;
|
|
457
|
+
const model = getArg('model');
|
|
363
458
|
const guardUuid = getArg('guard-uuid') || process.env.VIRTUEAI_GUARD_UUID;
|
|
364
|
-
// Strip /mcp suffix and normalize to lowercase
|
|
365
459
|
gatewayUrl = gatewayUrl.replace(/\/mcp\/?$/, '').toLowerCase();
|
|
366
460
|
console.log('\n VirtueAI Gateway Connect\n');
|
|
367
461
|
console.log(` Gateway: ${gatewayUrl}`);
|
|
462
|
+
if (model)
|
|
463
|
+
console.log(` Model: ${model}`);
|
|
368
464
|
// Step 1: Authenticate
|
|
369
465
|
const { accessToken } = await authenticate(gatewayUrl);
|
|
370
|
-
// Step 2:
|
|
466
|
+
// Step 2: Fetch tools & generate native plugin
|
|
371
467
|
console.log('\n Configuring OpenClaw...');
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
468
|
+
const tools = await fetchToolsList(gatewayUrl, accessToken);
|
|
469
|
+
if (tools.length === 0) {
|
|
470
|
+
console.error('\n Error: No tools found on gateway. Token may be invalid.');
|
|
471
|
+
process.exit(1);
|
|
472
|
+
}
|
|
473
|
+
generateMcpToolsPlugin(gatewayUrl, accessToken, tools);
|
|
474
|
+
// Step 3: Write gateway config (for trajectory plugin)
|
|
475
|
+
writeGatewayConfig(gatewayUrl, accessToken, guardUuid);
|
|
476
|
+
// Step 4: Patch openclaw.json (enable tools plugin + set model)
|
|
477
|
+
patchOpenClawConfig(model);
|
|
478
|
+
// Step 5: Install trajectory plugin
|
|
376
479
|
console.log('\n Setting up trajectory recording...');
|
|
377
480
|
generateTrajectoryPlugin(guardUuid);
|
|
378
481
|
enableTrajectoryPlugin();
|
|
379
|
-
// Step 5: Verify
|
|
380
|
-
console.log('');
|
|
381
|
-
const toolCount = await verifyConnection(gatewayUrl, accessToken);
|
|
382
482
|
// Done
|
|
483
|
+
const modelDisplay = model || 'existing (unchanged)';
|
|
383
484
|
console.log(`
|
|
384
485
|
Done! OpenClaw is now connected to VirtueAI MCP gateway.
|
|
385
|
-
${
|
|
486
|
+
${tools.length} tools registered as native OpenClaw tools.
|
|
487
|
+
Model: ${modelDisplay}
|
|
386
488
|
Trajectory recording enabled (via virtueai-trajectory plugin).
|
|
387
489
|
|
|
388
490
|
Config files:
|
|
389
491
|
${MCP_CONFIG_PATH}
|
|
390
492
|
${OPENCLAW_CONFIG_PATH}
|
|
493
|
+
${TOOLS_PLUGIN_DIR}
|
|
391
494
|
|
|
392
495
|
Start using it:
|
|
393
496
|
openclaw agent --local --message "What tools do you have?"
|
|
497
|
+
|
|
498
|
+
To use a different model:
|
|
499
|
+
npx @virtue-ai/gateway-connect --gateway-url ${gatewayUrl} --model openai/gpt-4o
|
|
394
500
|
`);
|
|
395
501
|
process.exit(0);
|
|
396
502
|
}
|
|
@@ -47,12 +47,9 @@ function loadConfig() {
|
|
|
47
47
|
try {
|
|
48
48
|
const raw = readFileSync(MCP_CONFIG_PATH, "utf-8");
|
|
49
49
|
const cfg = JSON.parse(raw);
|
|
50
|
-
const virtueai = cfg.mcpServers?.virtueai;
|
|
51
|
-
if (!virtueai) return null;
|
|
52
50
|
|
|
53
|
-
const
|
|
54
|
-
const token =
|
|
55
|
-
const gatewayUrl = (virtueai.url ?? "").replace(/\\/mcp\\/?$/, "");
|
|
51
|
+
const gatewayUrl = cfg._auth?.gatewayUrl ?? cfg.trajectory?.gatewayUrl ?? "";
|
|
52
|
+
const token = cfg._auth?.accessToken ?? "";
|
|
56
53
|
|
|
57
54
|
const guardUuid =
|
|
58
55
|
cfg.trajectory?.guardUuid ||
|
|
@@ -86,9 +83,14 @@ const plugin = {
|
|
|
86
83
|
}
|
|
87
84
|
|
|
88
85
|
let gatewaySessionId = null;
|
|
86
|
+
let endpointDisabled = false;
|
|
89
87
|
const endpoint = config.gatewayUrl + "/api/prompt-guard/topic_guard";
|
|
90
88
|
|
|
89
|
+
api.logger.info("[virtueai-trajectory] Plugin registered, sending to " + config.gatewayUrl);
|
|
90
|
+
|
|
91
91
|
async function sendStep(role, content) {
|
|
92
|
+
if (endpointDisabled) return;
|
|
93
|
+
|
|
92
94
|
const body = {
|
|
93
95
|
user_prompt: truncate(content),
|
|
94
96
|
guard_uuid: config.guardUuid,
|
|
@@ -107,8 +109,18 @@ const plugin = {
|
|
|
107
109
|
},
|
|
108
110
|
body: JSON.stringify(body),
|
|
109
111
|
});
|
|
110
|
-
const data = await res.json();
|
|
111
112
|
|
|
113
|
+
if (res.status === 404) {
|
|
114
|
+
endpointDisabled = true;
|
|
115
|
+
api.logger.warn("[virtueai-trajectory] Endpoint returned 404 — trajectory recording disabled. Is the prompt-guard API deployed on the gateway?");
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (!res.ok) {
|
|
119
|
+
api.logger.warn("[virtueai-trajectory] HTTP " + res.status + " from gateway");
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const data = await res.json();
|
|
112
124
|
if (data?.session_id && !gatewaySessionId) {
|
|
113
125
|
gatewaySessionId = data.session_id;
|
|
114
126
|
api.logger.info("[virtueai-trajectory] Gateway session: " + gatewaySessionId);
|
|
@@ -162,7 +174,7 @@ export function generateTrajectoryPlugin(guardUuid) {
|
|
|
162
174
|
fs.mkdirSync(PLUGIN_DIR, { recursive: true });
|
|
163
175
|
// package.json
|
|
164
176
|
const pkg = {
|
|
165
|
-
name: '@virtue-ai/trajectory',
|
|
177
|
+
name: '@virtue-ai/virtueai-trajectory',
|
|
166
178
|
version: '1.0.0',
|
|
167
179
|
type: 'module',
|
|
168
180
|
openclaw: {
|
|
@@ -170,6 +182,15 @@ export function generateTrajectoryPlugin(guardUuid) {
|
|
|
170
182
|
},
|
|
171
183
|
};
|
|
172
184
|
fs.writeFileSync(path.join(PLUGIN_DIR, 'package.json'), JSON.stringify(pkg, null, 2) + '\n');
|
|
185
|
+
// openclaw.plugin.json (manifest required by OpenClaw)
|
|
186
|
+
const manifest = {
|
|
187
|
+
id: PLUGIN_ID,
|
|
188
|
+
name: 'VirtueAI Trajectory',
|
|
189
|
+
description: 'Sends agent trajectory steps to VirtueAI gateway for dashboard visibility',
|
|
190
|
+
version: '1.0.0',
|
|
191
|
+
configSchema: {},
|
|
192
|
+
};
|
|
193
|
+
fs.writeFileSync(path.join(PLUGIN_DIR, 'openclaw.plugin.json'), JSON.stringify(manifest, null, 2) + '\n');
|
|
173
194
|
// index.ts (the actual plugin)
|
|
174
195
|
fs.writeFileSync(path.join(PLUGIN_DIR, 'index.ts'), buildPluginSource());
|
|
175
196
|
// Persist guard UUID in mcp-gateway.json trajectory section
|