agentgate-mcp 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/src/cli.js CHANGED
@@ -1,15 +1,12 @@
1
1
  #!/usr/bin/env -S node --disable-warning=ExperimentalWarning
2
2
 
3
3
  import process from 'node:process';
4
+ import { BrowserSession } from './browser-session.js';
4
5
  import { config, ensureDirs, fileExists } from './config.js';
5
6
  import { KeyDatabase } from './db.js';
6
7
  import { enableFileLogging, createLogger } from './logger.js';
7
8
  import { McpServer } from './mcp-server.js';
8
9
  import { Orchestrator } from './orchestrator.js';
9
- import { ServiceRegistry } from './registry.js';
10
- import { runScaffold } from './scaffold.js';
11
- import { SignupEngine } from './signup-engine.js';
12
- import { Vault } from './vault.js';
13
10
 
14
11
  const log = createLogger('cli');
15
12
 
@@ -17,32 +14,21 @@ function bootstrap() {
17
14
  ensureDirs();
18
15
  enableFileLogging(config.LOG_DIR);
19
16
 
20
- const vault = new Vault({
21
- vaultFile: config.VAULT_FILE,
22
- keyringFile: config.KEYRING_FILE
23
- });
24
- vault.initialize();
25
-
26
17
  const db = new KeyDatabase(config.DB_FILE);
27
- const registry = new ServiceRegistry(config.REGISTRY_DIR);
28
- const automationEngine = new SignupEngine({
29
- db,
30
- vault,
31
- browserProfileDir: config.BROWSER_PROFILE_DIR
32
- });
33
- const orchestrator = new Orchestrator({ db, registry, signupEngine: automationEngine });
34
-
35
- return { vault, db, orchestrator, registry, automationEngine };
18
+ const session = new BrowserSession({ browserProfileDir: config.BROWSER_PROFILE_DIR });
19
+ const orchestrator = new Orchestrator({ db, browserSession: session });
20
+
21
+ return { db, session, orchestrator };
36
22
  }
37
23
 
38
24
  async function main() {
39
25
  const command = process.argv[2] ?? 'serve';
40
- const { vault, db, orchestrator, registry, automationEngine } = bootstrap();
26
+ const { db, session, orchestrator } = bootstrap();
41
27
 
42
28
  switch (command) {
43
29
  case 'login':
44
- await automationEngine.login();
45
- process.stdout.write('\nGoogle session saved. AgentGate can now create API keys for any service.\n');
30
+ await session.login();
31
+ process.stdout.write('\nGoogle session saved. AgentGate can now open browsers with your Google account.\n');
46
32
  process.stdout.write('Start the MCP server with: agentgate serve\n\n');
47
33
  break;
48
34
 
@@ -52,42 +38,25 @@ async function main() {
52
38
  break;
53
39
 
54
40
  case 'doctor':
55
- await runDoctor({ orchestrator, registry, db });
56
- break;
57
-
58
- case 'scaffold': {
59
- const code = runScaffold({
60
- registryDir: config.REGISTRY_DIR,
61
- argv: process.argv.slice(3),
62
- stdout: process.stdout,
63
- stderr: process.stderr
64
- });
65
- process.exitCode = code;
41
+ await runDoctor({ db });
66
42
  break;
67
- }
68
43
 
69
44
  default:
70
- process.stderr.write(`Unknown command: ${command}\n`);
71
- process.stderr.write('\nUsage:\n');
45
+ process.stderr.write(`Unknown command: ${command}\n\n`);
46
+ process.stderr.write('Usage:\n');
72
47
  process.stderr.write(' agentgate login Sign in with Google (opens browser)\n');
73
48
  process.stderr.write(' agentgate serve Start MCP server\n');
74
- process.stderr.write(' agentgate doctor Health check\n');
75
- process.stderr.write(' agentgate scaffold Generate a service recipe\n\n');
49
+ process.stderr.write(' agentgate doctor Health check\n\n');
76
50
  process.exitCode = 1;
77
51
  }
78
52
  }
79
53
 
80
- async function runDoctor({ orchestrator, registry, db }) {
54
+ async function runDoctor({ db }) {
81
55
  const checks = {};
82
56
 
83
57
  checks.googleLoggedIn = fileExists(config.BROWSER_PROFILE_DIR);
84
- checks.vaultExists = fileExists(config.VAULT_FILE);
85
- checks.keyringExists = fileExists(config.KEYRING_FILE);
86
-
87
- const recipes = registry.listServices();
88
- checks.serviceRecipes = recipes.length;
89
-
90
58
  checks.dbFile = config.DB_FILE;
59
+
91
60
  checks.dbAccessible = true;
92
61
  try { db.getAllKeys(); } catch { checks.dbAccessible = false; }
93
62
 
package/src/config.js CHANGED
@@ -20,22 +20,14 @@ const APP_DIR = resolveAppDir();
20
20
  const DATA_DIR = path.join(APP_DIR, 'data');
21
21
  const LOG_DIR = path.join(APP_DIR, 'logs');
22
22
  const BROWSER_PROFILE_DIR = path.join(APP_DIR, 'browser-profile');
23
- const REGISTRY_DIR = path.resolve(process.cwd(), 'services');
24
- const VAULT_FILE = path.join(APP_DIR, 'vault.enc');
25
- const KEYRING_FILE = path.join(APP_DIR, 'keyring.json');
26
23
  const DB_FILE = path.join(DATA_DIR, 'agentgate.sqlite');
27
- const SETTINGS_FILE = path.join(APP_DIR, 'settings.json');
28
24
 
29
25
  export const config = {
30
26
  APP_DIR,
31
27
  DATA_DIR,
32
28
  LOG_DIR,
33
29
  BROWSER_PROFILE_DIR,
34
- REGISTRY_DIR,
35
- VAULT_FILE,
36
- KEYRING_FILE,
37
- DB_FILE,
38
- SETTINGS_FILE
30
+ DB_FILE
39
31
  };
40
32
 
41
33
  export function ensureDirs() {
package/src/mcp-server.js CHANGED
@@ -8,8 +8,7 @@ const log = createLogger('mcp-server');
8
8
  function loadVersion() {
9
9
  try {
10
10
  const pkgPath = path.resolve(import.meta.dirname, '..', 'package.json');
11
- const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
12
- return pkg.version || '0.0.0';
11
+ return JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version || '0.0.0';
13
12
  } catch {
14
13
  return '0.0.0';
15
14
  }
@@ -21,60 +20,103 @@ const TOOL_DEFS = [
21
20
  {
22
21
  name: 'get_or_create_key',
23
22
  description:
24
- 'Get an API key for any service. Returns cached key if one exists, otherwise opens a browser ' +
25
- 'with the saved Google session to sign up and extract a new key. Works for ANY service ' +
26
- 'just provide the service name and signup URL.',
23
+ 'Check if AgentGate has a cached API key for a service. ' +
24
+ 'Returns the key if cached, or tells you no key exists so you can use open_browser to go get one.',
27
25
  inputSchema: {
28
26
  type: 'object',
29
27
  properties: {
30
- service: {
28
+ service: { type: 'string', description: 'Service name, e.g. "openai", "twelvelabs"' }
29
+ },
30
+ required: ['service']
31
+ }
32
+ },
33
+ {
34
+ name: 'open_browser',
35
+ description:
36
+ 'Open a browser with the saved Google session and navigate to a URL. ' +
37
+ 'Returns a screenshot of the page so you can see what is on screen and decide what to do next. ' +
38
+ 'The user must have run `agentgate login` first.',
39
+ inputSchema: {
40
+ type: 'object',
41
+ properties: {
42
+ url: { type: 'string', description: 'URL to navigate to' }
43
+ },
44
+ required: ['url']
45
+ }
46
+ },
47
+ {
48
+ name: 'browser_action',
49
+ description:
50
+ 'Perform an action in the open browser and get a screenshot back. ' +
51
+ 'Actions: click, fill, select, press, scroll, goto, wait, screenshot, extract_text, extract_all_text. ' +
52
+ 'Use the screenshot to see the result and decide your next step.',
53
+ inputSchema: {
54
+ type: 'object',
55
+ properties: {
56
+ action: {
31
57
  type: 'string',
32
- description: 'Service name (e.g. "openai", "anthropic", "twelvelabs")'
58
+ description: 'Action to perform',
59
+ enum: ['click', 'fill', 'select', 'press', 'scroll', 'goto', 'wait', 'screenshot', 'extract_text', 'extract_all_text']
33
60
  },
34
- signup_url: {
61
+ selector: {
35
62
  type: 'string',
36
- description: 'URL to the service signup or login page (e.g. "https://platform.openai.com/signup")'
63
+ description: 'CSS selector or Playwright selector (e.g. "text=Sign in with Google", "#submit-btn", "input[name=email]")'
37
64
  },
38
- api_key_url: {
65
+ value: {
39
66
  type: 'string',
40
- description: 'Direct URL to the API keys dashboard (e.g. "https://platform.openai.com/api-keys"). If provided, navigates here after sign-in to find/create the key.'
67
+ description: 'Value for fill/select actions, or scroll distance in pixels'
68
+ },
69
+ key: {
70
+ type: 'string',
71
+ description: 'Key to press (e.g. "Enter", "Tab")'
72
+ },
73
+ url: {
74
+ type: 'string',
75
+ description: 'URL for goto action'
76
+ },
77
+ ms: {
78
+ type: 'number',
79
+ description: 'Milliseconds for wait action (default 5000)'
41
80
  }
42
81
  },
43
- required: ['service', 'signup_url']
82
+ required: ['action']
44
83
  }
45
84
  },
46
85
  {
47
- name: 'list_my_keys',
48
- description: 'List all API keys stored by AgentGate, including active and revoked keys.',
86
+ name: 'save_key',
87
+ description: 'Save an API key you found on the page to AgentGate\'s local database for future use.',
88
+ inputSchema: {
89
+ type: 'object',
90
+ properties: {
91
+ service: { type: 'string', description: 'Service name to store the key under' },
92
+ api_key: { type: 'string', description: 'The API key value to store' }
93
+ },
94
+ required: ['service', 'api_key']
95
+ }
96
+ },
97
+ {
98
+ name: 'close_browser',
99
+ description: 'Close the browser session when you are done.',
49
100
  inputSchema: {
50
101
  type: 'object',
51
102
  properties: {}
52
103
  }
53
104
  },
54
105
  {
55
- name: 'revoke_key',
56
- description: 'Revoke the stored API key for a service. This only removes it from AgentGate — it does not revoke it on the provider side.',
106
+ name: 'list_my_keys',
107
+ description: 'List all API keys stored by AgentGate.',
57
108
  inputSchema: {
58
109
  type: 'object',
59
- properties: {
60
- service: {
61
- type: 'string',
62
- description: 'Service name to revoke the key for'
63
- }
64
- },
65
- required: ['service']
110
+ properties: {}
66
111
  }
67
112
  },
68
113
  {
69
- name: 'check_key_status',
70
- description: 'Check whether AgentGate has an active API key stored for a service.',
114
+ name: 'revoke_key',
115
+ description: 'Delete a stored API key from AgentGate.',
71
116
  inputSchema: {
72
117
  type: 'object',
73
118
  properties: {
74
- service: {
75
- type: 'string',
76
- description: 'Service name to check'
77
- }
119
+ service: { type: 'string', description: 'Service name to revoke' }
78
120
  },
79
121
  required: ['service']
80
122
  }
@@ -85,14 +127,10 @@ export class McpServer {
85
127
  constructor({ orchestrator }) {
86
128
  this.orchestrator = orchestrator;
87
129
  this.queue = Promise.resolve();
88
- this.initialized = false;
89
130
  }
90
131
 
91
132
  start() {
92
- const rl = readline.createInterface({
93
- input: process.stdin,
94
- crlfDelay: Infinity
95
- });
133
+ const rl = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
96
134
 
97
135
  rl.on('line', (line) => {
98
136
  this.queue = this.queue.then(() => this.processLine(line));
@@ -100,6 +138,7 @@ export class McpServer {
100
138
 
101
139
  rl.on('close', () => {
102
140
  log.info('stdin closed, shutting down');
141
+ this.orchestrator.closeBrowser().catch(() => {});
103
142
  });
104
143
 
105
144
  log.info('MCP server started', { version: VERSION });
@@ -113,11 +152,7 @@ export class McpServer {
113
152
  try {
114
153
  request = JSON.parse(trimmed);
115
154
  } catch {
116
- this.write({
117
- jsonrpc: '2.0',
118
- error: { code: -32700, message: 'Parse error' },
119
- id: null
120
- });
155
+ this.write({ jsonrpc: '2.0', error: { code: -32700, message: 'Parse error' }, id: null });
121
156
  return;
122
157
  }
123
158
 
@@ -132,23 +167,13 @@ export class McpServer {
132
167
  } catch (error) {
133
168
  const message = error instanceof Error ? error.message : 'Unknown error';
134
169
  log.error(`Request failed: ${request.method}`, { error: message });
135
- this.write({
136
- jsonrpc: '2.0',
137
- id: request.id,
138
- error: { code: -32000, message }
139
- });
170
+ this.write({ jsonrpc: '2.0', id: request.id, error: { code: -32000, message } });
140
171
  }
141
172
  }
142
173
 
143
174
  handleNotification(request) {
144
- const { method } = request;
145
- if (method === 'notifications/initialized') {
146
- this.initialized = true;
175
+ if (request.method === 'notifications/initialized') {
147
176
  log.info('Client initialized');
148
- } else if (method === 'notifications/cancelled') {
149
- log.info('Client cancelled request', { params: request.params });
150
- } else {
151
- log.debug(`Unhandled notification: ${method}`);
152
177
  }
153
178
  }
154
179
 
@@ -163,21 +188,14 @@ export class McpServer {
163
188
  };
164
189
  }
165
190
 
166
- if (method === 'ping') {
167
- return {};
168
- }
191
+ if (method === 'ping') return {};
169
192
 
170
- if (method === 'tools/list') {
171
- return { tools: TOOL_DEFS };
172
- }
193
+ if (method === 'tools/list') return { tools: TOOL_DEFS };
173
194
 
174
195
  if (method === 'tools/call') {
175
196
  const { name, arguments: args } = params || {};
176
- log.info(`Calling tool: ${name}`, { args });
177
- const output = await this.callTool(name, args || {});
178
- return {
179
- content: [{ type: 'text', text: JSON.stringify(output) }]
180
- };
197
+ log.info(`Tool call: ${name}`);
198
+ return this.callTool(name, args || {});
181
199
  }
182
200
 
183
201
  throw new Error(`Unsupported method: ${method}`);
@@ -186,18 +204,69 @@ export class McpServer {
186
204
  async callTool(name, args) {
187
205
  switch (name) {
188
206
  case 'get_or_create_key':
189
- return this.orchestrator.getOrCreateKey(args);
207
+ return this.formatText(this.orchestrator.getKey(args.service));
208
+
209
+ case 'open_browser': {
210
+ const result = await this.orchestrator.openBrowser(args.url);
211
+ return this.formatBrowserResult(result);
212
+ }
213
+
214
+ case 'browser_action': {
215
+ const result = await this.orchestrator.browserAction(args);
216
+ return this.formatBrowserResult(result);
217
+ }
218
+
219
+ case 'save_key':
220
+ return this.formatText(this.orchestrator.saveKey(args.service, args.api_key));
221
+
222
+ case 'close_browser':
223
+ await this.orchestrator.closeBrowser();
224
+ return this.formatText({ status: 'browser closed' });
225
+
190
226
  case 'list_my_keys':
191
- return this.orchestrator.getAllMyKeys();
227
+ return this.formatText(this.orchestrator.getAllMyKeys());
228
+
192
229
  case 'revoke_key':
193
- return this.orchestrator.revokeKey(args.service);
194
- case 'check_key_status':
195
- return this.orchestrator.checkKeyStatus(args.service);
230
+ return this.formatText(this.orchestrator.revokeKey(args.service));
231
+
196
232
  default:
197
233
  throw new Error(`Unknown tool: ${name}`);
198
234
  }
199
235
  }
200
236
 
237
+ /**
238
+ * Format a browser result with screenshot as image content.
239
+ */
240
+ formatBrowserResult(result) {
241
+ const content = [];
242
+
243
+ if (result.screenshot) {
244
+ content.push({
245
+ type: 'image',
246
+ data: result.screenshot,
247
+ mimeType: 'image/png'
248
+ });
249
+ }
250
+
251
+ // Include text metadata
252
+ const meta = { url: result.url, title: result.title };
253
+ if (result.extracted_text) {
254
+ meta.extracted_text = result.extracted_text;
255
+ }
256
+ content.push({ type: 'text', text: JSON.stringify(meta) });
257
+
258
+ return { content };
259
+ }
260
+
261
+ /**
262
+ * Format a plain JSON result as text content.
263
+ */
264
+ formatText(data) {
265
+ return {
266
+ content: [{ type: 'text', text: JSON.stringify(data) }]
267
+ };
268
+ }
269
+
201
270
  write(payload) {
202
271
  process.stdout.write(`${JSON.stringify(payload)}\n`);
203
272
  }
@@ -3,52 +3,20 @@ import { createLogger } from './logger.js';
3
3
  const log = createLogger('orchestrator');
4
4
 
5
5
  export class Orchestrator {
6
- constructor({ db, registry, signupEngine }) {
6
+ constructor({ db, browserSession }) {
7
7
  this.db = db;
8
- this.registry = registry;
9
- this.signupEngine = signupEngine;
10
- }
11
-
12
- getAllMyKeys() {
13
- return this.db.getAllKeys().map((row) => ({
14
- service: row.service,
15
- api_key: row.api_key,
16
- status: row.status,
17
- created_at: row.created_at,
18
- updated_at: row.updated_at
19
- }));
20
- }
21
-
22
- checkKeyStatus(service) {
23
- const existing = this.db.getActiveKey(service);
24
- if (!existing) {
25
- return { service, exists: false, status: 'missing' };
26
- }
27
- return {
28
- service,
29
- exists: true,
30
- status: existing.status,
31
- created_at: existing.created_at,
32
- updated_at: existing.updated_at
33
- };
8
+ this.session = browserSession;
34
9
  }
35
10
 
36
11
  /**
37
- * Get or create an API key for any service.
38
- *
39
- * @param {object} opts
40
- * @param {string} opts.service - Service name (e.g. "openai")
41
- * @param {string} opts.signup_url - Signup or login URL
42
- * @param {string} [opts.api_key_url] - Direct URL to API keys dashboard
12
+ * Check DB for a cached key. Does NOT create one.
43
13
  */
44
- async getOrCreateKey({ service, signup_url, api_key_url }) {
14
+ getKey(service) {
45
15
  if (!service) throw new Error('service is required');
46
- if (!signup_url) throw new Error('signup_url is required');
47
16
 
48
- // Check cache first
49
17
  const existing = this.db.getActiveKey(service);
50
18
  if (existing) {
51
- log.info(`Returning cached key for ${service}`);
19
+ log.info(`Cache hit for ${service}`);
52
20
  return {
53
21
  service,
54
22
  api_key: existing.api_key,
@@ -57,51 +25,70 @@ export class Orchestrator {
57
25
  };
58
26
  }
59
27
 
60
- // Build service definition — check registry for a recipe, else use smart mode
61
- let serviceDef = this.registry.getService(service);
62
-
63
- if (!serviceDef) {
64
- // No recipe — create a dynamic service definition
65
- serviceDef = {
66
- name: service,
67
- signup_url,
68
- api_key_url: api_key_url || null,
69
- workflow: [] // empty = smart navigation mode
70
- };
71
- } else {
72
- // Recipe exists — override URLs if caller provided them
73
- if (signup_url) serviceDef.signup_url = signup_url;
74
- if (api_key_url) serviceDef.api_key_url = api_key_url;
75
- }
28
+ return {
29
+ service,
30
+ exists: false,
31
+ message: `No key cached for "${service}". Use open_browser to navigate to the service and get one.`
32
+ };
33
+ }
76
34
 
77
- log.info(`Creating key for ${service}`, {
78
- mode: serviceDef.workflow?.length > 0 ? 'recipe' : 'smart',
79
- signup_url: serviceDef.signup_url,
80
- api_key_url: serviceDef.api_key_url
81
- });
35
+ /**
36
+ * Save a key the agent found on a page.
37
+ */
38
+ saveKey(service, apiKey) {
39
+ if (!service) throw new Error('service is required');
40
+ if (!apiKey) throw new Error('api_key is required');
82
41
 
83
- const fresh = await this.signupEngine.createKey(serviceDef);
84
42
  const stored = this.db.upsertActiveKey({
85
43
  service,
86
- apiKey: fresh.apiKey,
87
- metadata: fresh.metadata
44
+ apiKey,
45
+ metadata: { savedAt: new Date().toISOString() }
88
46
  });
89
47
 
90
- log.info(`Key created for ${service}`, { source: fresh.metadata?.flow });
91
-
48
+ log.info(`Key saved for ${service}`);
92
49
  return {
93
50
  service,
94
51
  api_key: stored.api_key,
95
- source: fresh.metadata?.flow || 'signup',
52
+ status: 'saved',
96
53
  created_at: stored.created_at
97
54
  };
98
55
  }
99
56
 
57
+ /**
58
+ * Open browser with Google session.
59
+ */
60
+ async openBrowser(url) {
61
+ if (!url) throw new Error('url is required');
62
+ return this.session.open(url);
63
+ }
64
+
65
+ /**
66
+ * Perform a browser action.
67
+ */
68
+ async browserAction(params) {
69
+ return this.session.action(params);
70
+ }
71
+
72
+ /**
73
+ * Close browser.
74
+ */
75
+ async closeBrowser() {
76
+ await this.session.close();
77
+ }
78
+
79
+ getAllMyKeys() {
80
+ return this.db.getAllKeys().map((row) => ({
81
+ service: row.service,
82
+ api_key: row.api_key,
83
+ status: row.status,
84
+ created_at: row.created_at,
85
+ updated_at: row.updated_at
86
+ }));
87
+ }
88
+
100
89
  revokeKey(service) {
101
90
  const changed = this.db.revokeKey(service);
102
- if (changed) {
103
- log.info(`Revoked key for ${service}`);
104
- }
91
+ if (changed) log.info(`Revoked key for ${service}`);
105
92
  return { service, revoked: changed };
106
93
  }
107
94
  }
@@ -1,34 +0,0 @@
1
- {
2
- "name": "replace_me",
3
- "signup_url": "https://example.com/signup",
4
- "requires": {
5
- "google": false,
6
- "card": false
7
- },
8
- "runtime": {
9
- "headless": true
10
- },
11
- "workflow": [
12
- { "type": "goto", "url": "{{service.signup_url}}" },
13
- { "type": "wait_for", "selector": "body" },
14
-
15
- { "type": "fill", "selector": "input[name='email']", "value": "{{generated.emailAlias}}" },
16
- { "type": "fill", "selector": "input[name='password']", "value": "{{generated.password}}" },
17
- { "type": "click", "selector": "button[type='submit']" },
18
- { "type": "store_alias", "emailPath": "generated.emailAlias", "passwordPath": "generated.password" },
19
-
20
- { "type": "wait_for_email_code", "target": "scratch.emailCode", "timeoutMs": 120000 },
21
- { "type": "fill", "selector": "input[name='verification_code']", "value": "{{scratch.emailCode}}" },
22
- { "type": "click", "selector": "button[type='submit']" },
23
-
24
- { "type": "solve_captcha", "target": "scratch.captchaToken", "provider": "capsolver" },
25
- { "type": "fill_card" },
26
-
27
- { "type": "goto", "url": "https://example.com/dashboard/api-keys" },
28
- { "type": "click", "selector": "text=Create API Key" },
29
-
30
- { "type": "extract_text", "selector": "body", "target": "scratch.pageText" },
31
- { "type": "regex_extract", "source": "scratch.pageText", "pattern": "api[_-]?key[:\\s]+([A-Za-z0-9_\\-]{20,})", "target": "result.apiKey" },
32
- { "type": "assert_present", "path": "result.apiKey", "message": "API key not found in page output" }
33
- ]
34
- }