@virtue-ai/gateway-connect 0.2.1 → 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 +59 -23
- package/dist/index.d.ts +4 -3
- package/dist/index.js +198 -154
- 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,7 +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');
|
|
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';
|
|
35
37
|
// ---------------------------------------------------------------------------
|
|
36
38
|
// Helpers
|
|
@@ -97,7 +99,6 @@ function openBrowser(url) {
|
|
|
97
99
|
// Step 1: OAuth PKCE Authentication
|
|
98
100
|
// ---------------------------------------------------------------------------
|
|
99
101
|
async function authenticate(gatewayUrl) {
|
|
100
|
-
// Discover OAuth metadata
|
|
101
102
|
console.log(' Discovering OAuth endpoints...');
|
|
102
103
|
const { data: metadata } = await fetchJson(`${gatewayUrl}/.well-known/oauth-authorization-server`, { method: 'GET' });
|
|
103
104
|
if (!metadata.authorization_endpoint || !metadata.token_endpoint) {
|
|
@@ -110,7 +111,6 @@ async function authenticate(gatewayUrl) {
|
|
|
110
111
|
const registerEndpoint = metadata.registration_endpoint;
|
|
111
112
|
console.log(` Auth endpoint: ${authEndpoint}`);
|
|
112
113
|
console.log(` Token endpoint: ${tokenEndpoint}`);
|
|
113
|
-
// Register OAuth client
|
|
114
114
|
console.log(' Registering OAuth client...');
|
|
115
115
|
const { status: regStatus, data: clientInfo } = await fetchJson(registerEndpoint, {
|
|
116
116
|
method: 'POST',
|
|
@@ -130,7 +130,6 @@ async function authenticate(gatewayUrl) {
|
|
|
130
130
|
}
|
|
131
131
|
const clientId = clientInfo.client_id;
|
|
132
132
|
console.log(` Client ID: ${clientId}`);
|
|
133
|
-
// Build authorization URL with PKCE
|
|
134
133
|
const codeVerifier = generateCodeVerifier();
|
|
135
134
|
const codeChallenge = generateCodeChallenge(codeVerifier);
|
|
136
135
|
const state = crypto.randomBytes(16).toString('hex');
|
|
@@ -142,7 +141,6 @@ async function authenticate(gatewayUrl) {
|
|
|
142
141
|
authUrl.searchParams.set('state', state);
|
|
143
142
|
authUrl.searchParams.set('code_challenge', codeChallenge);
|
|
144
143
|
authUrl.searchParams.set('code_challenge_method', 'S256');
|
|
145
|
-
// Start callback server & open browser
|
|
146
144
|
const authCode = await new Promise((resolve, reject) => {
|
|
147
145
|
const server = http.createServer((req, res) => {
|
|
148
146
|
if (!req.url?.startsWith(CALLBACK_PATH)) {
|
|
@@ -190,14 +188,12 @@ async function authenticate(gatewayUrl) {
|
|
|
190
188
|
reject(err);
|
|
191
189
|
}
|
|
192
190
|
});
|
|
193
|
-
// Timeout after 5 minutes
|
|
194
191
|
setTimeout(() => {
|
|
195
192
|
server.close();
|
|
196
193
|
reject(new Error('Authentication timed out (5 minutes). Please try again.'));
|
|
197
194
|
}, 5 * 60 * 1000);
|
|
198
195
|
});
|
|
199
196
|
console.log(' Authorization code received. Exchanging for tokens...');
|
|
200
|
-
// Exchange code for tokens
|
|
201
197
|
const tokenBody = new URLSearchParams({
|
|
202
198
|
grant_type: 'authorization_code',
|
|
203
199
|
client_id: clientId,
|
|
@@ -221,82 +217,132 @@ async function authenticate(gatewayUrl) {
|
|
|
221
217
|
clientId,
|
|
222
218
|
};
|
|
223
219
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
+
});
|
|
238
302
|
|
|
239
|
-
const
|
|
240
|
-
const TOKEN = ${JSON.stringify(accessToken)};
|
|
303
|
+
const result = await response.json();
|
|
241
304
|
|
|
242
|
-
|
|
243
|
-
|
|
305
|
+
if (result.error) {
|
|
306
|
+
return {
|
|
307
|
+
content: [{ type: "text", text: \`Error: \${result.error.message}\` }],
|
|
308
|
+
isError: true
|
|
309
|
+
};
|
|
310
|
+
}
|
|
244
311
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
try { msg = JSON.parse(buf); } catch { return; }
|
|
249
|
-
buf = "";
|
|
250
|
-
forward(msg);
|
|
251
|
-
});
|
|
312
|
+
const text = result.result?.content
|
|
313
|
+
?.map((c: any) => c.text ?? c.data ?? "")
|
|
314
|
+
.join("\\n") ?? JSON.stringify(result.result);
|
|
252
315
|
|
|
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");
|
|
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
|
+
};
|
|
271
325
|
}
|
|
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
326
|
}
|
|
282
|
-
})
|
|
283
|
-
|
|
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')}
|
|
284
335
|
}
|
|
285
336
|
`;
|
|
286
|
-
fs.writeFileSync(
|
|
287
|
-
console.log(` Generated
|
|
337
|
+
fs.writeFileSync(path.join(TOOLS_PLUGIN_DIR, 'index.ts'), pluginSource);
|
|
338
|
+
console.log(` Generated MCP tools plugin: ${TOOLS_PLUGIN_DIR} (${tools.length} tools)`);
|
|
288
339
|
}
|
|
289
|
-
|
|
340
|
+
// ---------------------------------------------------------------------------
|
|
341
|
+
// Step 3: Write config & patch openclaw.json
|
|
342
|
+
// ---------------------------------------------------------------------------
|
|
343
|
+
function writeGatewayConfig(gatewayUrl, accessToken, guardUuid) {
|
|
290
344
|
fs.mkdirSync(OPENCLAW_DIR, { recursive: true });
|
|
291
|
-
generateMcpProxy(gatewayUrl, accessToken);
|
|
292
345
|
const config = {
|
|
293
|
-
mcpServers: {
|
|
294
|
-
virtueai: {
|
|
295
|
-
type: 'stdio',
|
|
296
|
-
command: 'node',
|
|
297
|
-
args: [MCP_PROXY_PATH],
|
|
298
|
-
},
|
|
299
|
-
},
|
|
300
346
|
trajectory: {
|
|
301
347
|
gatewayUrl,
|
|
302
348
|
guardUuid: guardUuid || process.env.VIRTUEAI_GUARD_UUID || '',
|
|
@@ -309,10 +355,7 @@ function writeMcpConfig(gatewayUrl, accessToken, guardUuid) {
|
|
|
309
355
|
fs.writeFileSync(MCP_CONFIG_PATH, JSON.stringify(config, null, 2) + '\n');
|
|
310
356
|
console.log(` Written: ${MCP_CONFIG_PATH}`);
|
|
311
357
|
}
|
|
312
|
-
|
|
313
|
-
// Step 3: Patch openclaw.json
|
|
314
|
-
// ---------------------------------------------------------------------------
|
|
315
|
-
function patchOpenClawConfig() {
|
|
358
|
+
function patchOpenClawConfig(model) {
|
|
316
359
|
let config = {};
|
|
317
360
|
if (fs.existsSync(OPENCLAW_CONFIG_PATH)) {
|
|
318
361
|
try {
|
|
@@ -323,80 +366,64 @@ function patchOpenClawConfig() {
|
|
|
323
366
|
config = {};
|
|
324
367
|
}
|
|
325
368
|
}
|
|
326
|
-
// Ensure nested structure exists
|
|
327
369
|
if (!config.agents)
|
|
328
370
|
config.agents = {};
|
|
329
371
|
if (!config.agents.defaults)
|
|
330
372
|
config.agents.defaults = {};
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
config.agents.defaults.model = { primary: 'claude-cli/sonnet' };
|
|
373
|
+
if (model) {
|
|
374
|
+
config.agents.defaults.model = { primary: model };
|
|
334
375
|
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
config.agents.defaults.
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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' };
|
|
379
|
+
}
|
|
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);
|
|
342
392
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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);
|
|
347
411
|
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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');
|
|
351
419
|
}
|
|
352
|
-
|
|
353
|
-
|
|
420
|
+
if (!allowlist.includes(TOOLS_PLUGIN_ID)) {
|
|
421
|
+
allowlist.push(TOOLS_PLUGIN_ID);
|
|
354
422
|
}
|
|
355
|
-
// Write back
|
|
356
423
|
fs.writeFileSync(OPENCLAW_CONFIG_PATH, JSON.stringify(config, null, 2) + '\n');
|
|
357
424
|
console.log(` Patched: ${OPENCLAW_CONFIG_PATH}`);
|
|
358
425
|
}
|
|
359
426
|
// ---------------------------------------------------------------------------
|
|
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
427
|
// Main
|
|
401
428
|
// ---------------------------------------------------------------------------
|
|
402
429
|
async function main() {
|
|
@@ -409,50 +436,67 @@ Usage:
|
|
|
409
436
|
|
|
410
437
|
Options:
|
|
411
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)
|
|
412
440
|
--guard-uuid <uuid> Guard UUID for trajectory recording (or set VIRTUEAI_GUARD_UUID)
|
|
413
441
|
--help Show this help message
|
|
414
442
|
|
|
415
443
|
What it does:
|
|
416
444
|
1. Opens browser for OAuth login
|
|
417
|
-
2.
|
|
418
|
-
3.
|
|
419
|
-
4.
|
|
420
|
-
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
|
|
421
453
|
`);
|
|
422
454
|
process.exit(0);
|
|
423
455
|
}
|
|
424
456
|
let gatewayUrl = getArg('gateway-url') || DEFAULT_GATEWAY_URL;
|
|
457
|
+
const model = getArg('model');
|
|
425
458
|
const guardUuid = getArg('guard-uuid') || process.env.VIRTUEAI_GUARD_UUID;
|
|
426
|
-
// Strip /mcp suffix and normalize to lowercase
|
|
427
459
|
gatewayUrl = gatewayUrl.replace(/\/mcp\/?$/, '').toLowerCase();
|
|
428
460
|
console.log('\n VirtueAI Gateway Connect\n');
|
|
429
461
|
console.log(` Gateway: ${gatewayUrl}`);
|
|
462
|
+
if (model)
|
|
463
|
+
console.log(` Model: ${model}`);
|
|
430
464
|
// Step 1: Authenticate
|
|
431
465
|
const { accessToken } = await authenticate(gatewayUrl);
|
|
432
|
-
// Step 2:
|
|
466
|
+
// Step 2: Fetch tools & generate native plugin
|
|
433
467
|
console.log('\n Configuring OpenClaw...');
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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
|
|
438
479
|
console.log('\n Setting up trajectory recording...');
|
|
439
480
|
generateTrajectoryPlugin(guardUuid);
|
|
440
481
|
enableTrajectoryPlugin();
|
|
441
|
-
// Step 5: Verify
|
|
442
|
-
console.log('');
|
|
443
|
-
const toolCount = await verifyConnection(gatewayUrl, accessToken);
|
|
444
482
|
// Done
|
|
483
|
+
const modelDisplay = model || 'existing (unchanged)';
|
|
445
484
|
console.log(`
|
|
446
485
|
Done! OpenClaw is now connected to VirtueAI MCP gateway.
|
|
447
|
-
${
|
|
486
|
+
${tools.length} tools registered as native OpenClaw tools.
|
|
487
|
+
Model: ${modelDisplay}
|
|
448
488
|
Trajectory recording enabled (via virtueai-trajectory plugin).
|
|
449
489
|
|
|
450
490
|
Config files:
|
|
451
491
|
${MCP_CONFIG_PATH}
|
|
452
492
|
${OPENCLAW_CONFIG_PATH}
|
|
493
|
+
${TOOLS_PLUGIN_DIR}
|
|
453
494
|
|
|
454
495
|
Start using it:
|
|
455
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
|
|
456
500
|
`);
|
|
457
501
|
process.exit(0);
|
|
458
502
|
}
|