@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 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
- Make sure you have [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code) installed and logged in (`claude` command should work).
13
+ ### Step 2: Configure Model Auth
14
14
 
15
- ### Step 2: Connect to VirtueAI MCP Gateway
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 (select "Authorize the platform")
24
- 2. Save gateway credentials to `~/.openclaw/mcp-gateway.json`
25
- 3. Patch `~/.openclaw/openclaw.json` to connect claude-cli to the gateway
26
- 4. Install the trajectory recording plugin (sends full session trace to VirtueAI dashboard)
27
- 5. Verify connection and list available tools
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 3: Start Using
43
+ ### Step 4: Start Using
30
44
 
31
45
  One-shot mode:
32
46
 
33
47
  ```bash
34
- openclaw agent --local --session-id demo --message "What tools do you have?"
48
+ openclaw agent --local --message "What tools do you have?"
35
49
  ```
36
50
 
37
- Interactive TUI (two terminals):
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
- OpenClaw now has access to all MCP tools on the gateway (GitHub, Google Workspace, Gmail, Calendar, Slack, PayPal, HR, Firebase, BigQuery, Brave Search, Chrome DevTools, and more).
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` automates the following:
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
- 1. **OAuth 2.0 PKCE authentication** — Registers an OAuth client, opens browser for login, exchanges authorization code for tokens
54
- 2. **MCP config generation** — Writes `~/.openclaw/mcp-gateway.json` with gateway URL and bearer token
55
- 3. **OpenClaw config patching** — Adds `--mcp-config` to the claude-cli backend args in `~/.openclaw/openclaw.json`
56
- 4. **Trajectory recording** — Installs an OpenClaw plugin (`virtueai-trajectory`) that automatically sends every agent step (user prompts, agent responses, tool calls) to the VirtueAI prompt-guard API for dashboard visibility
57
- 5. **Connection verification** — Calls `tools/list` on the gateway and reports available tools
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 (required)
66
- --guard-uuid <uuid> Guard UUID for trajectory recording (or set VIRTUEAI_GUARD_UUID env var)
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 update the existing config files.
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. Write MCP gateway config to ~/.openclaw/mcp-gateway.json
9
- * 3. Patch ~/.openclaw/openclaw.json to use claude-cli with --mcp-config
10
- * 4. Verify connection by listing tools
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. Write MCP gateway config to ~/.openclaw/mcp-gateway.json
9
- * 3. Patch ~/.openclaw/openclaw.json to use claude-cli with --mcp-config
10
- * 4. Verify connection by listing tools
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 MCP_PROXY_PATH = path.join(OPENCLAW_DIR, 'virtueai-mcp-proxy.mjs');
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
- // Step 2: Write MCP gateway config
226
- // ---------------------------------------------------------------------------
227
- function generateMcpProxy(gatewayUrl, accessToken) {
228
- const source = `#!/usr/bin/env node
229
- // Stdio-to-HTTP MCP proxy for VirtueAI gateway.
230
- // Claude Code checks OAuth discovery on remote HTTP MCP servers, which
231
- // conflicts with the gateway's OAuth endpoint. This proxy runs as a local
232
- // stdio server so Claude Code skips OAuth and uses the pre-obtained token.
233
-
234
- import { createInterface } from "readline";
235
- import https from "https";
236
- import http from "http";
237
- import { URL } from "url";
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 GATEWAY_MCP_URL = ${JSON.stringify(gatewayUrl + '/mcp')};
240
- const TOKEN = ${JSON.stringify(accessToken)};
305
+ const result = await response.json();
241
306
 
242
- const rl = createInterface({ input: process.stdin, terminal: false });
243
- let buf = "";
307
+ if (result.error) {
308
+ return {
309
+ content: [{ type: "text", text: \`Error: \${result.error.message}\` }],
310
+ isError: true
311
+ };
312
+ }
244
313
 
245
- rl.on("line", (line) => {
246
- buf += line;
247
- let msg;
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
- function forward(msg) {
254
- const parsed = new URL(GATEWAY_MCP_URL);
255
- const mod = parsed.protocol === "https:" ? https : http;
256
- const body = JSON.stringify(msg);
257
- const req = mod.request(parsed, {
258
- method: "POST",
259
- headers: {
260
- "Content-Type": "application/json",
261
- "Authorization": "Bearer " + TOKEN,
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
- req.end(body);
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(MCP_PROXY_PATH, source, { mode: 0o755 });
287
- console.log(` Generated proxy: ${MCP_PROXY_PATH}`);
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
- function writeMcpConfig(gatewayUrl, accessToken, guardUuid) {
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
- // Set model to claude-cli if not already set
332
- if (!config.agents.defaults.model) {
333
- config.agents.defaults.model = { primary: 'claude-cli/sonnet' };
377
+ if (model) {
378
+ config.agents.defaults.model = { primary: model };
334
379
  }
335
- // Ensure cliBackends.claude-cli exists (don't override user's existing config)
336
- if (!config.agents.defaults.cliBackends)
337
- config.agents.defaults.cliBackends = {};
338
- if (!config.agents.defaults.cliBackends['claude-cli']) {
339
- config.agents.defaults.cliBackends['claude-cli'] = {
340
- command: 'claude',
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
- const cliBackend = config.agents.defaults.cliBackends['claude-cli'];
344
- // OpenClaw mergeBackendConfig: override.args ?? base.args (full replacement, not merge)
345
- if (!Array.isArray(cliBackend.args)) {
346
- cliBackend.args = ['-p', '--output-format', 'json', '--permission-mode', 'bypassPermissions'];
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
- const mcpIdx = cliBackend.args.indexOf('--mcp-config');
349
- if (mcpIdx !== -1) {
350
- cliBackend.args[mcpIdx + 1] = MCP_CONFIG_PATH;
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
- else {
353
- cliBackend.args.push('--mcp-config', MCP_CONFIG_PATH);
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> Gateway URL (default: ${DEFAULT_GATEWAY_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. Saves MCP config to ~/.openclaw/mcp-gateway.json
418
- 3. Patches ~/.openclaw/openclaw.json to use the gateway
419
- 4. Installs trajectory plugin for full session recording
420
- 5. Verifies connection by listing available tools
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
- // Strip /mcp suffix and normalize to lowercase
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: Write MCP config
474
+ // Step 2: Fetch tools & generate native plugin
433
475
  console.log('\n Configuring OpenClaw...');
434
- writeMcpConfig(gatewayUrl, accessToken, guardUuid);
435
- // Step 3: Patch openclaw.json
436
- patchOpenClawConfig();
437
- // Step 4: Install trajectory plugin
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
- ${toolCount} tools available across the gateway.
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 (!gatewayUrl || !token) return null;
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.gatewayUrl + "/api/prompt-guard/topic_guard";
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@virtue-ai/gateway-connect",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
4
4
  "description": "One-command setup to connect OpenClaw to VirtueAI MCP gateway",
5
5
  "type": "module",
6
6
  "files": [