@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 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
- 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
42
+
43
+ ### Step 4: Start Using
28
44
 
29
- ### Step 3: Start Using
45
+ One-shot mode:
30
46
 
31
47
  ```bash
32
- openclaw agent --local --session-id demo --message "What tools do you have?"
48
+ openclaw agent --local --message "What tools do you have?"
33
49
  ```
34
50
 
35
- That's it. 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).
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` 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
40
86
 
41
- 1. **OAuth 2.0 PKCE authentication** — Registers an OAuth client, opens browser for login, exchanges authorization code for tokens
42
- 2. **MCP config generation** — Writes `~/.openclaw/mcp-gateway.json` with gateway URL and bearer token
43
- 3. **OpenClaw config patching** — Adds `--mcp-config` to the claude-cli backend args in `~/.openclaw/openclaw.json`
44
- 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
45
- 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 |
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 (required)
54
- --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)
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 update the existing config files.
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. 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,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 2: Write MCP gateway config
341
+ // Step 3: Write config & patch openclaw.json
225
342
  // ---------------------------------------------------------------------------
226
- function writeMcpConfig(gatewayUrl, accessToken, guardUuid) {
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
- // Set model to claude-cli if not already set
266
- if (!config.agents.defaults.model) {
267
- config.agents.defaults.model = { primary: 'claude-cli/sonnet' };
373
+ if (model) {
374
+ config.agents.defaults.model = { primary: model };
268
375
  }
269
- // Ensure cliBackends.claude-cli exists
270
- if (!config.agents.defaults.cliBackends)
271
- config.agents.defaults.cliBackends = {};
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
- const cliBackend = config.agents.defaults.cliBackends['claude-cli'];
279
- // Ensure args is an array
280
- if (!Array.isArray(cliBackend.args)) {
281
- cliBackend.args = ['-p', '--output-format', 'json'];
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
- // Add or update --mcp-config
284
- const mcpIdx = cliBackend.args.indexOf('--mcp-config');
285
- if (mcpIdx !== -1) {
286
- // Update existing path
287
- cliBackend.args[mcpIdx + 1] = MCP_CONFIG_PATH;
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
- else {
290
- // Append
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. Saves MCP config to ~/.openclaw/mcp-gateway.json
356
- 3. Patches ~/.openclaw/openclaw.json to use the gateway
357
- 4. Installs trajectory plugin for full session recording
358
- 5. Verifies connection by listing available tools
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: Write MCP config
466
+ // Step 2: Fetch tools & generate native plugin
371
467
  console.log('\n Configuring OpenClaw...');
372
- writeMcpConfig(gatewayUrl, accessToken, guardUuid);
373
- // Step 3: Patch openclaw.json
374
- patchOpenClawConfig();
375
- // Step 4: Install trajectory plugin
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
- ${toolCount} tools available across the gateway.
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 authHeader = virtueai.headers?.Authorization ?? "";
54
- const token = authHeader.replace(/^Bearer\\s+/i, "");
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@virtue-ai/gateway-connect",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "One-command setup to connect OpenClaw to VirtueAI MCP gateway",
5
5
  "type": "module",
6
6
  "files": [