@opentabs-dev/mcp-server 0.0.64 → 0.0.66

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.
Files changed (94) hide show
  1. package/dist/browser-tools/click-element.js +3 -3
  2. package/dist/browser-tools/click-element.js.map +1 -1
  3. package/dist/browser-tools/extension-get-logs.d.ts +1 -1
  4. package/dist/browser-tools/get-console-logs.d.ts +1 -1
  5. package/dist/browser-tools/press-key.js +5 -5
  6. package/dist/browser-tools/press-key.js.map +1 -1
  7. package/dist/extension-handlers.d.ts +4 -1
  8. package/dist/extension-handlers.d.ts.map +1 -1
  9. package/dist/extension-handlers.js +28 -2
  10. package/dist/extension-handlers.js.map +1 -1
  11. package/dist/extension-protocol.d.ts.map +1 -1
  12. package/dist/extension-protocol.js +5 -1
  13. package/dist/extension-protocol.js.map +1 -1
  14. package/dist/loader.d.ts +2 -0
  15. package/dist/loader.d.ts.map +1 -1
  16. package/dist/loader.js +9 -0
  17. package/dist/loader.js.map +1 -1
  18. package/dist/mcp-prompts.d.ts +31 -4
  19. package/dist/mcp-prompts.d.ts.map +1 -1
  20. package/dist/mcp-prompts.js +190 -354
  21. package/dist/mcp-prompts.js.map +1 -1
  22. package/dist/mcp-resources.d.ts +53 -0
  23. package/dist/mcp-resources.d.ts.map +1 -0
  24. package/dist/mcp-resources.js +139 -0
  25. package/dist/mcp-resources.js.map +1 -0
  26. package/dist/mcp-setup.d.ts +12 -1
  27. package/dist/mcp-setup.d.ts.map +1 -1
  28. package/dist/mcp-setup.js +99 -34
  29. package/dist/mcp-setup.js.map +1 -1
  30. package/dist/plugin-management.d.ts +14 -1
  31. package/dist/plugin-management.d.ts.map +1 -1
  32. package/dist/plugin-management.js +55 -1
  33. package/dist/plugin-management.js.map +1 -1
  34. package/dist/prompts/audit-ai-docs.d.ts +6 -0
  35. package/dist/prompts/audit-ai-docs.d.ts.map +1 -0
  36. package/dist/prompts/audit-ai-docs.js +155 -0
  37. package/dist/prompts/audit-ai-docs.js.map +1 -0
  38. package/dist/prompts/build-plugin.d.ts +3 -0
  39. package/dist/prompts/build-plugin.d.ts.map +1 -0
  40. package/dist/prompts/build-plugin.js +455 -0
  41. package/dist/prompts/build-plugin.js.map +1 -0
  42. package/dist/prompts/contribute-learnings.d.ts +12 -0
  43. package/dist/prompts/contribute-learnings.d.ts.map +1 -0
  44. package/dist/prompts/contribute-learnings.js +107 -0
  45. package/dist/prompts/contribute-learnings.js.map +1 -0
  46. package/dist/prompts/plugin-icon.d.ts +5 -0
  47. package/dist/prompts/plugin-icon.d.ts.map +1 -0
  48. package/dist/prompts/plugin-icon.js +147 -0
  49. package/dist/prompts/plugin-icon.js.map +1 -0
  50. package/dist/prompts/setup-plugin.d.ts +3 -0
  51. package/dist/prompts/setup-plugin.d.ts.map +1 -0
  52. package/dist/prompts/setup-plugin.js +197 -0
  53. package/dist/prompts/setup-plugin.js.map +1 -0
  54. package/dist/prompts/troubleshoot.d.ts +3 -0
  55. package/dist/prompts/troubleshoot.d.ts.map +1 -0
  56. package/dist/prompts/troubleshoot.js +191 -0
  57. package/dist/prompts/troubleshoot.js.map +1 -0
  58. package/dist/reload.js +4 -4
  59. package/dist/resources/browser-tools.d.ts +3 -0
  60. package/dist/resources/browser-tools.d.ts.map +1 -0
  61. package/dist/resources/browser-tools.js +100 -0
  62. package/dist/resources/browser-tools.js.map +1 -0
  63. package/dist/resources/cli.d.ts +3 -0
  64. package/dist/resources/cli.d.ts.map +1 -0
  65. package/dist/resources/cli.js +217 -0
  66. package/dist/resources/cli.js.map +1 -0
  67. package/dist/resources/plugin-development.d.ts +3 -0
  68. package/dist/resources/plugin-development.d.ts.map +1 -0
  69. package/dist/resources/plugin-development.js +596 -0
  70. package/dist/resources/plugin-development.js.map +1 -0
  71. package/dist/resources/quick-start.d.ts +3 -0
  72. package/dist/resources/quick-start.d.ts.map +1 -0
  73. package/dist/resources/quick-start.js +210 -0
  74. package/dist/resources/quick-start.js.map +1 -0
  75. package/dist/resources/sdk-api.d.ts +3 -0
  76. package/dist/resources/sdk-api.d.ts.map +1 -0
  77. package/dist/resources/sdk-api.js +199 -0
  78. package/dist/resources/sdk-api.js.map +1 -0
  79. package/dist/resources/self-improvement.d.ts +10 -0
  80. package/dist/resources/self-improvement.d.ts.map +1 -0
  81. package/dist/resources/self-improvement.js +91 -0
  82. package/dist/resources/self-improvement.js.map +1 -0
  83. package/dist/resources/status.d.ts +5 -0
  84. package/dist/resources/status.d.ts.map +1 -0
  85. package/dist/resources/status.js +27 -0
  86. package/dist/resources/status.js.map +1 -0
  87. package/dist/resources/troubleshooting.d.ts +3 -0
  88. package/dist/resources/troubleshooting.d.ts.map +1 -0
  89. package/dist/resources/troubleshooting.js +167 -0
  90. package/dist/resources/troubleshooting.js.map +1 -0
  91. package/dist/state.d.ts +2 -0
  92. package/dist/state.d.ts.map +1 -1
  93. package/dist/state.js.map +1 -1
  94. package/package.json +1 -1
@@ -0,0 +1,596 @@
1
+ /** Plugin Development Guide resource content. */
2
+ export const PLUGIN_DEVELOPMENT_CONTENT = `# Plugin Development Guide
3
+
4
+ ## Architecture
5
+
6
+ OpenTabs plugins run **in the browser page context**, not on the server. The MCP server discovers plugins, but tool execution happens inside the web page via an adapter IIFE injected by the Chrome extension. This means plugin code has full access to the page's DOM, JavaScript globals, cookies, localStorage, and authenticated fetch requests.
7
+
8
+ **Flow:** AI client → MCP server → Chrome extension (WebSocket) → adapter IIFE (page context) → tool handler → result back through the chain.
9
+
10
+ ## Plugin Structure
11
+
12
+ A plugin is a standalone npm package with this structure:
13
+
14
+ \`\`\`
15
+ my-plugin/
16
+ ├── package.json # Must include "opentabs" field
17
+ ├── src/
18
+ │ ├── plugin.ts # OpenTabsPlugin subclass (entry point)
19
+ │ └── tools/
20
+ │ ├── get-data.ts # One file per tool (convention)
21
+ │ └── send-msg.ts
22
+ ├── dist/ # Built by opentabs-plugin build
23
+ │ ├── adapter.iife.js # Injected into matching browser tabs
24
+ │ └── tools.json # Tool schemas for MCP registration
25
+ └── tsconfig.json
26
+ \`\`\`
27
+
28
+ ### package.json
29
+
30
+ \`\`\`json
31
+ {
32
+ "name": "@scope/opentabs-plugin-myapp",
33
+ "version": "1.0.0",
34
+ "opentabs": {
35
+ "name": "myapp",
36
+ "displayName": "My App",
37
+ "description": "Tools for My App",
38
+ "urlPatterns": ["*://myapp.com/*"]
39
+ },
40
+ "main": "src/plugin.ts",
41
+ "scripts": {
42
+ "build": "opentabs-plugin build"
43
+ },
44
+ "dependencies": {
45
+ "@opentabs-dev/plugin-sdk": "latest"
46
+ },
47
+ "devDependencies": {
48
+ "@opentabs-dev/plugin-tools": "latest"
49
+ }
50
+ }
51
+ \`\`\`
52
+
53
+ The \`opentabs.name\` field is the plugin identifier (lowercase, alphanumeric + hyphens). It becomes the tool name prefix (e.g., \`myapp_get_data\`).
54
+
55
+ ## OpenTabsPlugin Base Class
56
+
57
+ Every plugin extends \`OpenTabsPlugin\` and exports an instance:
58
+
59
+ \`\`\`typescript
60
+ import { OpenTabsPlugin } from '@opentabs-dev/plugin-sdk';
61
+ import type { ToolDefinition } from '@opentabs-dev/plugin-sdk';
62
+ import { getDataTool } from './tools/get-data.js';
63
+ import { sendMsgTool } from './tools/send-msg.js';
64
+
65
+ class MyPlugin extends OpenTabsPlugin {
66
+ readonly name = 'myapp';
67
+ readonly displayName = 'My App';
68
+ readonly description = 'Tools for My App';
69
+ readonly urlPatterns = ['*://myapp.com/*'];
70
+ readonly tools: ToolDefinition[] = [getDataTool, sendMsgTool];
71
+
72
+ async isReady(): Promise<boolean> {
73
+ // Return true when the user is authenticated and the app is loaded
74
+ return document.querySelector('.logged-in-indicator') !== null;
75
+ }
76
+ }
77
+
78
+ export default new MyPlugin();
79
+ \`\`\`
80
+
81
+ ### Required Members
82
+
83
+ | Member | Type | Purpose |
84
+ |--------|------|---------|
85
+ | \`name\` | \`string\` | Unique identifier (lowercase alphanumeric + hyphens) |
86
+ | \`displayName\` | \`string\` | Human-readable name shown in side panel |
87
+ | \`description\` | \`string\` | Brief plugin description |
88
+ | \`urlPatterns\` | \`string[]\` | Chrome match patterns for tab injection |
89
+ | \`tools\` | \`ToolDefinition[]\` | Array of tool definitions |
90
+ | \`isReady()\` | \`() => Promise<boolean>\` | Readiness probe — returns true when tab is ready for tool calls |
91
+
92
+ ### Tab State Machine
93
+
94
+ | State | Condition |
95
+ |-------|-----------|
96
+ | \`closed\` | No browser tab matches the plugin's URL patterns |
97
+ | \`unavailable\` | Tab matches URL patterns but \`isReady()\` returns false |
98
+ | \`ready\` | Tab matches URL patterns and \`isReady()\` returns true |
99
+
100
+ ## defineTool Factory
101
+
102
+ Each tool is defined with \`defineTool\`, which provides type inference:
103
+
104
+ \`\`\`typescript
105
+ import { z } from 'zod';
106
+ import { defineTool, fetchJSON } from '@opentabs-dev/plugin-sdk';
107
+ import type { ToolHandlerContext } from '@opentabs-dev/plugin-sdk';
108
+
109
+ export const getDataTool = defineTool({
110
+ name: 'get_data',
111
+ displayName: 'Get Data',
112
+ description: 'Retrieves data from the app. Returns the matching records.',
113
+ summary: 'Retrieve app data',
114
+ icon: 'database',
115
+ group: 'Data',
116
+ input: z.object({
117
+ query: z.string().describe('Search query string'),
118
+ limit: z.number().int().min(1).max(100).default(25).describe('Max results to return'),
119
+ }),
120
+ output: z.object({
121
+ results: z.array(z.object({
122
+ id: z.string(),
123
+ title: z.string(),
124
+ })),
125
+ total: z.number(),
126
+ }),
127
+ async handle(params, context?: ToolHandlerContext) {
128
+ const data = await fetchJSON<{ items: Array<{ id: string; title: string }>; total: number }>(
129
+ \`/api/data?q=\${encodeURIComponent(params.query)}&limit=\${params.limit}\`
130
+ );
131
+ return { results: data?.items ?? [], total: data?.total ?? 0 };
132
+ },
133
+ });
134
+ \`\`\`
135
+
136
+ ### ToolDefinition Fields
137
+
138
+ | Field | Required | Description |
139
+ |-------|----------|-------------|
140
+ | \`name\` | Yes | Tool name (auto-prefixed with plugin name) |
141
+ | \`displayName\` | No | Human-readable name for side panel (auto-derived from name if omitted) |
142
+ | \`description\` | Yes | Shown to AI agents — be specific and include return value info |
143
+ | \`summary\` | No | Short UI summary (falls back to description) |
144
+ | \`icon\` | No | Lucide icon name in kebab-case (defaults to \`wrench\`) |
145
+ | \`group\` | No | Visual grouping in the side panel |
146
+ | \`input\` | Yes | Zod object schema for parameters |
147
+ | \`output\` | Yes | Zod schema for return value |
148
+ | \`handle\` | Yes | Async function — runs in page context. Second arg is optional \`ToolHandlerContext\` |
149
+
150
+ ### Progress Reporting
151
+
152
+ Long-running tools can report progress via the optional \`context\` parameter:
153
+
154
+ \`\`\`typescript
155
+ async handle(params, context?: ToolHandlerContext) {
156
+ const items = await getItemList();
157
+ for (let i = 0; i < items.length; i++) {
158
+ context?.reportProgress({ progress: i + 1, total: items.length, message: \`Processing \${items[i].name}\` });
159
+ await processItem(items[i]);
160
+ }
161
+ return { processed: items.length };
162
+ }
163
+ \`\`\`
164
+
165
+ ## SDK Utilities Reference
166
+
167
+ All utilities are imported from \`@opentabs-dev/plugin-sdk\`. They run in the page context.
168
+
169
+ ### DOM
170
+
171
+ | Function | Signature | Description |
172
+ |----------|-----------|-------------|
173
+ | \`waitForSelector\` | \`<T extends Element>(selector, opts?) → Promise<T>\` | Waits for element to appear (MutationObserver, default 10s timeout) |
174
+ | \`waitForSelectorRemoval\` | \`(selector, opts?) → Promise<void>\` | Waits for element to be removed (default 10s timeout) |
175
+ | \`querySelectorAll\` | \`<T extends Element>(selector) → T[]\` | Returns real array instead of NodeList |
176
+ | \`getTextContent\` | \`(selector) → string \\| null\` | Trimmed textContent of first match |
177
+ | \`observeDOM\` | \`(selector, callback, opts?) → () => void\` | MutationObserver on element, returns cleanup function |
178
+
179
+ ### Fetch
180
+
181
+ All fetch utilities use \`credentials: 'include'\` to leverage the page's authenticated session.
182
+
183
+ | Function | Signature | Description |
184
+ |----------|-----------|-------------|
185
+ | \`fetchFromPage\` | \`(url, init?) → Promise<Response>\` | Fetch with session cookies, 30s timeout, ToolError on non-ok |
186
+ | \`fetchJSON\` | \`<T>(url, init?, schema?) → Promise<T>\` | Fetch + JSON parse. Optional Zod schema validation |
187
+ | \`postJSON\` | \`<T>(url, body, init?, schema?) → Promise<T>\` | POST with JSON body + parse response |
188
+ | \`putJSON\` | \`<T>(url, body, init?, schema?) → Promise<T>\` | PUT with JSON body + parse response |
189
+ | \`patchJSON\` | \`<T>(url, body, init?, schema?) → Promise<T>\` | PATCH with JSON body + parse response |
190
+ | \`deleteJSON\` | \`<T>(url, init?, schema?) → Promise<T>\` | DELETE + parse response |
191
+ | \`postForm\` | \`<T>(url, body, init?, schema?) → Promise<T>\` | POST URL-encoded form (Record<string,string>) |
192
+ | \`postFormData\` | \`<T>(url, body, init?, schema?) → Promise<T>\` | POST multipart/form-data (FormData) |
193
+
194
+ ### Storage
195
+
196
+ | Function | Signature | Description |
197
+ |----------|-----------|-------------|
198
+ | \`getLocalStorage\` | \`(key) → string \\| null\` | Safe localStorage read (null on SecurityError) |
199
+ | \`setLocalStorage\` | \`(key, value) → void\` | Safe localStorage write |
200
+ | \`removeLocalStorage\` | \`(key) → void\` | Safe localStorage remove |
201
+ | \`getSessionStorage\` | \`(key) → string \\| null\` | Safe sessionStorage read |
202
+ | \`setSessionStorage\` | \`(key, value) → void\` | Safe sessionStorage write |
203
+ | \`removeSessionStorage\` | \`(key) → void\` | Safe sessionStorage remove |
204
+ | \`getCookie\` | \`(name) → string \\| null\` | Parse cookie by name from document.cookie |
205
+
206
+ ### Page State
207
+
208
+ | Function | Signature | Description |
209
+ |----------|-----------|-------------|
210
+ | \`getPageGlobal\` | \`(path) → unknown\` | Safe deep property access on globalThis via dot-notation |
211
+ | \`getCurrentUrl\` | \`() → string\` | Returns window.location.href |
212
+ | \`getPageTitle\` | \`() → string\` | Returns document.title |
213
+
214
+ ### Timing
215
+
216
+ | Function | Signature | Description |
217
+ |----------|-----------|-------------|
218
+ | \`retry\` | \`<T>(fn, opts?) → Promise<T>\` | Retry with configurable attempts (3), delay (1s), backoff, AbortSignal |
219
+ | \`sleep\` | \`(ms, opts?) → Promise<void>\` | Promisified setTimeout with optional AbortSignal |
220
+ | \`waitUntil\` | \`(predicate, opts?) → Promise<void>\` | Poll predicate at interval (200ms) until true, timeout (10s) |
221
+
222
+ ### Logging
223
+
224
+ | Function | Description |
225
+ |----------|-------------|
226
+ | \`log.debug(message, ...args)\` | Debug level |
227
+ | \`log.info(message, ...args)\` | Info level |
228
+ | \`log.warn(message, ...args)\` | Warning level |
229
+ | \`log.error(message, ...args)\` | Error level |
230
+
231
+ Log entries flow from the page context through the extension to the MCP server and connected clients. Falls back to \`console\` methods outside the adapter runtime.
232
+
233
+ ## ToolError Factories
234
+
235
+ Use static factory methods for structured errors. The dispatch chain propagates metadata (category, retryable, retryAfterMs) to AI clients.
236
+
237
+ | Factory | Signature | Category | Retryable |
238
+ |---------|-----------|----------|-----------|
239
+ | \`ToolError.auth\` | \`(message, code?) → ToolError\` | \`auth\` | No |
240
+ | \`ToolError.notFound\` | \`(message, code?) → ToolError\` | \`not_found\` | No |
241
+ | \`ToolError.rateLimited\` | \`(message, retryAfterMs?, code?) → ToolError\` | \`rate_limit\` | Yes |
242
+ | \`ToolError.validation\` | \`(message, code?) → ToolError\` | \`validation\` | No |
243
+ | \`ToolError.timeout\` | \`(message, code?) → ToolError\` | \`timeout\` | Yes |
244
+ | \`ToolError.internal\` | \`(message, code?) → ToolError\` | \`internal\` | No |
245
+
246
+ \`\`\`typescript
247
+ import { ToolError, fetchJSON } from '@opentabs-dev/plugin-sdk';
248
+
249
+ // Auth errors are automatically thrown by fetchJSON on 401/403
250
+ // For manual auth checks:
251
+ const token = getPageGlobal('app.auth.token') as string | undefined;
252
+ if (!token) throw ToolError.auth('User is not logged in');
253
+
254
+ // For domain-specific errors with custom codes:
255
+ throw ToolError.notFound('Channel not found', 'CHANNEL_NOT_FOUND');
256
+ throw ToolError.rateLimited('Slow down', 5000, 'SLACK_RATE_LIMITED');
257
+ \`\`\`
258
+
259
+ ## Zod Schema Rules
260
+
261
+ Schemas are serialized to JSON Schema via \`z.toJSONSchema()\` for MCP registration. Follow these rules:
262
+
263
+ 1. **Never use \`.transform()\`** — transforms cannot be represented in JSON Schema. Normalize input in the handler.
264
+ 2. **Avoid \`.pipe()\`, \`.preprocess()\`, and effects** — these are runtime-only and break serialization.
265
+ 3. **\`.refine()\` callbacks must never throw** — Zod 4 runs refine even on invalid base values. Wrap throwing code in try-catch.
266
+ 4. **Use \`.describe()\` on every field** — descriptions are shown to AI agents in the tool schema.
267
+ 5. **Keep schemas declarative** — primitives, objects, arrays, unions, literals, enums, optional, default.
268
+
269
+ ## Lifecycle Hooks
270
+
271
+ Optional methods on \`OpenTabsPlugin\` — implement only what you need:
272
+
273
+ | Hook | Signature | When Called |
274
+ |------|-----------|------------|
275
+ | \`onActivate\` | \`() → void\` | After adapter registered on \`globalThis.__openTabs.adapters\` |
276
+ | \`onDeactivate\` | \`() → void\` | Before adapter removal (fires before \`teardown\`) |
277
+ | \`onNavigate\` | \`(url: string) → void\` | On in-page URL changes (pushState, replaceState, popstate, hashchange) |
278
+ | \`onToolInvocationStart\` | \`(toolName: string) → void\` | Before each \`tool.handle()\` |
279
+ | \`onToolInvocationEnd\` | \`(toolName: string, success: boolean, durationMs: number) → void\` | After each \`tool.handle()\` |
280
+ | \`teardown\` | \`() → void\` | Before re-injection on plugin update |
281
+
282
+ Errors in hooks are caught and logged — they do not affect tool execution.
283
+
284
+ ## isReady() Polling Pattern
285
+
286
+ The extension polls \`isReady()\` to determine tab state. Common patterns:
287
+
288
+ \`\`\`typescript
289
+ // DOM-based: check for a logged-in indicator
290
+ async isReady(): Promise<boolean> {
291
+ return document.querySelector('[data-testid="user-menu"]') !== null;
292
+ }
293
+
294
+ // Global-based: check for auth token in window globals
295
+ async isReady(): Promise<boolean> {
296
+ return getPageGlobal('app.auth.token') !== undefined;
297
+ }
298
+
299
+ // API-based: verify session with a lightweight request
300
+ async isReady(): Promise<boolean> {
301
+ try {
302
+ await fetchJSON('/api/me');
303
+ return true;
304
+ } catch {
305
+ return false;
306
+ }
307
+ }
308
+ \`\`\`
309
+
310
+ ## Auth Token Extraction
311
+
312
+ Plugins extract auth from the page — never ask users for credentials.
313
+
314
+ \`\`\`typescript
315
+ // From window globals (Slack pattern)
316
+ const token = getPageGlobal('TS.boot_data.api_token') as string | undefined;
317
+ if (!token) throw ToolError.auth('Not logged in');
318
+
319
+ // From localStorage
320
+ const token = getLocalStorage('auth_token');
321
+ if (!token) throw ToolError.auth('No auth token found');
322
+
323
+ // From cookies (session-based auth)
324
+ const session = getCookie('session_id');
325
+ if (!session) throw ToolError.auth('No session cookie');
326
+
327
+ // Cache on globalThis to avoid repeated extraction
328
+ const CACHE_KEY = '__opentabs_myapp_token';
329
+ function getToken(): string {
330
+ const cached = (globalThis as Record<string, unknown>)[CACHE_KEY] as string | undefined;
331
+ if (cached) return cached;
332
+ const token = getPageGlobal('app.token') as string | undefined;
333
+ if (!token) throw ToolError.auth('Not authenticated');
334
+ (globalThis as Record<string, unknown>)[CACHE_KEY] = token;
335
+ return token;
336
+ }
337
+ \`\`\`
338
+
339
+ ## Build and Test Workflow
340
+
341
+ \`\`\`bash
342
+ # Build the plugin (generates dist/adapter.iife.js and dist/tools.json)
343
+ npx opentabs-plugin build
344
+ # Or if installed globally:
345
+ opentabs-plugin build
346
+
347
+ # The build command notifies the running MCP server via POST /reload
348
+ # No server restart needed — plugin changes are picked up automatically
349
+ \`\`\`
350
+
351
+ ### Testing During Development
352
+
353
+ 1. Build the plugin: \`opentabs-plugin build\`
354
+ 2. Open the target web app in Chrome
355
+ 3. Verify plugin loaded: call \`plugin_list_tabs\` from your AI client
356
+ 4. Test a tool: call any plugin tool (e.g., \`myapp_get_data\`)
357
+ 5. Check logs: call \`extension_get_logs\` to see adapter injection and tool execution logs
358
+
359
+ ### Scaffolding a New Plugin
360
+
361
+ \`\`\`bash
362
+ npx @opentabs-dev/create-plugin
363
+ # Or with the CLI installed:
364
+ opentabs plugin create
365
+ \`\`\`
366
+
367
+ ## Publishing to npm
368
+
369
+ \`\`\`json
370
+ {
371
+ "name": "@scope/opentabs-plugin-myapp",
372
+ "opentabs": {
373
+ "name": "myapp",
374
+ "displayName": "My App",
375
+ "description": "Tools for My App",
376
+ "urlPatterns": ["*://myapp.com/*"]
377
+ }
378
+ }
379
+ \`\`\`
380
+
381
+ Package naming convention: \`opentabs-plugin-<name>\` or \`@scope/opentabs-plugin-<name>\`. The MCP server auto-discovers packages matching these patterns in global node_modules.
382
+
383
+ \`\`\`bash
384
+ npm publish
385
+ # Users install with:
386
+ opentabs plugin install myapp
387
+ \`\`\`
388
+
389
+ ## Common Patterns
390
+
391
+ ### API Wrapper
392
+
393
+ \`\`\`typescript
394
+ const API_BASE = '/api/v1';
395
+
396
+ async function apiGet<T>(path: string): Promise<T> {
397
+ const result = await fetchJSON<T>(\`\${API_BASE}\${path}\`);
398
+ if (result === undefined) throw ToolError.internal(\`Unexpected empty response from \${path}\`);
399
+ return result;
400
+ }
401
+
402
+ async function apiPost<T>(path: string, body: unknown): Promise<T> {
403
+ const result = await postJSON<T>(\`\${API_BASE}\${path}\`, body);
404
+ if (result === undefined) throw ToolError.internal(\`Unexpected empty response from \${path}\`);
405
+ return result;
406
+ }
407
+ \`\`\`
408
+
409
+ ### Waiting for App State
410
+
411
+ \`\`\`typescript
412
+ import { waitForSelector, waitUntil, getPageGlobal } from '@opentabs-dev/plugin-sdk';
413
+
414
+ // Wait for the app to finish loading before executing
415
+ await waitForSelector('.app-loaded');
416
+
417
+ // Wait for a specific global to be set
418
+ await waitUntil(() => getPageGlobal('app.initialized') === true);
419
+ \`\`\`
420
+
421
+ ### Retrying Flaky Operations
422
+
423
+ \`\`\`typescript
424
+ import { retry, ToolError } from '@opentabs-dev/plugin-sdk';
425
+
426
+ const result = await retry(
427
+ () => fetchJSON<Data>('/api/flaky-endpoint'),
428
+ { maxAttempts: 3, delay: 1000, backoff: true }
429
+ );
430
+ \`\`\`
431
+
432
+ ## Core Principle: APIs Not DOM
433
+
434
+ Every tool must use the web app's own APIs — the same endpoints the web app calls internally. DOM scraping is never acceptable as a tool implementation strategy: it is fragile (breaks on UI changes), limited (only sees what's rendered), and slow (requires waiting for DOM mutations).
435
+
436
+ When an API is hard to discover, invest time reverse-engineering network traffic rather than falling back to DOM. The only acceptable DOM uses are:
437
+ - **\`isReady()\`** — checking auth indicators (e.g., a logged-in avatar)
438
+ - **URL hash navigation** — changing views via \`window.location.hash\`
439
+ - **Last-resort compose flows** — when no API exists for creating content (extremely rare)
440
+
441
+ ## Token Persistence
442
+
443
+ Module-level variables (\`let cachedAuth = null\`) are reset when the Chrome extension reloads and re-injects the adapter IIFE. If the host app has already deleted the token from localStorage by this point, the plugin becomes unavailable.
444
+
445
+ Persist auth tokens to \`globalThis.__openTabs.tokenCache.<pluginName>\`, which survives adapter re-injection (the page itself is not reloaded — only the IIFE is re-executed).
446
+
447
+ \`\`\`typescript
448
+ const getPersistedToken = (): string | null => {
449
+ try {
450
+ const ns = (globalThis as Record<string, unknown>).__openTabs as
451
+ | Record<string, unknown>
452
+ | undefined;
453
+ const cache = ns?.tokenCache as
454
+ | Record<string, string | undefined>
455
+ | undefined;
456
+ return cache?.myPlugin ?? null;
457
+ } catch {
458
+ return null;
459
+ }
460
+ };
461
+
462
+ const setPersistedToken = (token: string): void => {
463
+ try {
464
+ const g = globalThis as Record<string, unknown>;
465
+ if (!g.__openTabs) g.__openTabs = {};
466
+ const ns = g.__openTabs as Record<string, unknown>;
467
+ if (!ns.tokenCache) ns.tokenCache = {};
468
+ const cache = ns.tokenCache as Record<string, string | undefined>;
469
+ cache.myPlugin = token;
470
+ } catch {}
471
+ };
472
+
473
+ const clearPersistedToken = (): void => {
474
+ try {
475
+ const ns = (globalThis as Record<string, unknown>).__openTabs as
476
+ | Record<string, unknown>
477
+ | undefined;
478
+ const cache = ns?.tokenCache as
479
+ | Record<string, string | undefined>
480
+ | undefined;
481
+ if (cache) cache.myPlugin = undefined;
482
+ } catch {}
483
+ };
484
+
485
+ // In getAuth():
486
+ const getAuth = (): Auth | null => {
487
+ const persisted = getPersistedToken();
488
+ if (persisted) return { token: persisted };
489
+
490
+ const raw = readLocalStorage('token');
491
+ if (!raw) return null;
492
+ setPersistedToken(raw);
493
+ return { token: raw };
494
+ };
495
+ \`\`\`
496
+
497
+ Always clear the persisted token on 401 responses to handle token rotation.
498
+
499
+ ## Adapter Injection Timing
500
+
501
+ Adapters are injected at **two points** during page load:
502
+
503
+ 1. **\`loading\`** — before page JavaScript runs. The adapter IIFE registers on \`globalThis.__openTabs\` and can read localStorage/cookies before the host app modifies them.
504
+ 2. **\`complete\`** — after the page is fully loaded. The adapter is re-injected (idempotent) and \`isReady()\` is probed to determine tab state.
505
+
506
+ This means:
507
+ - \`isReady()\` may be called at both injection points. At \`loading\` time, page globals do not exist yet — return \`false\` gracefully. At \`complete\` time, everything is ready.
508
+ - Auth tokens from localStorage should be cached at \`loading\` time before the host app can delete them.
509
+
510
+ ## Advanced Auth Patterns
511
+
512
+ ### XHR/Fetch Interception
513
+
514
+ Some web apps use internal RPC endpoints or obfuscated API paths that are hard to discover via network capture. Monkey-patch \`XMLHttpRequest\` to intercept all API traffic and capture auth headers at runtime.
515
+
516
+ \`\`\`typescript
517
+ const origOpen = XMLHttpRequest.prototype.open;
518
+ const origSetHeader = XMLHttpRequest.prototype.setRequestHeader;
519
+ const origSend = XMLHttpRequest.prototype.send;
520
+
521
+ XMLHttpRequest.prototype.open = function (method: string, url: string) {
522
+ (this as Record<string, unknown>)._url = url;
523
+ (this as Record<string, unknown>)._method = method;
524
+ return origOpen.apply(this, arguments as unknown as Parameters<typeof origOpen>);
525
+ };
526
+ XMLHttpRequest.prototype.setRequestHeader = function (name: string, value: string) {
527
+ if (name.toLowerCase() === 'authorization') {
528
+ setPersistedToken(value); // Capture auth header
529
+ }
530
+ return origSetHeader.apply(this, arguments as unknown as Parameters<typeof origSetHeader>);
531
+ };
532
+ \`\`\`
533
+
534
+ Install the interceptor at adapter load time to capture auth tokens from early boot requests. Store captured tokens on \`globalThis\` so they survive adapter re-injection.
535
+
536
+ ### Cookie-Based Auth with CSRF
537
+
538
+ Many web apps use HttpOnly session cookies for auth but require a CSRF token for write operations. The CSRF token is typically in a non-HttpOnly cookie (e.g., \`csrftoken\`, \`sentry-sc\`).
539
+
540
+ \`\`\`typescript
541
+ const csrfToken = getCookie('csrftoken');
542
+ const response = await fetch('/api/endpoint', {
543
+ method: 'POST',
544
+ headers: {
545
+ 'Content-Type': 'application/json',
546
+ 'X-CSRFToken': csrfToken ?? '',
547
+ },
548
+ body: JSON.stringify(payload),
549
+ credentials: 'include', // HttpOnly cookies sent automatically
550
+ });
551
+ \`\`\`
552
+
553
+ Check \`window.__initialData.csrfCookieName\` or similar bootstrap globals to discover the cookie name. GET requests work without the CSRF token.
554
+
555
+ ### Opaque Auth Headers
556
+
557
+ Some apps compute cryptographic auth tokens via obfuscated JavaScript. These tokens cannot be generated — only captured and replayed. Use the XHR interceptor pattern above to capture them, then implement a polling wait:
558
+
559
+ \`\`\`typescript
560
+ const waitForToken = async (): Promise<string> => {
561
+ for (let i = 0; i < 50; i++) {
562
+ const token = getPersistedToken();
563
+ if (token) return token;
564
+ await new Promise((r) => setTimeout(r, 200));
565
+ }
566
+ throw ToolError.auth('Auth token not captured — try refreshing the page');
567
+ };
568
+ \`\`\`
569
+
570
+ If a write operation returns 200 but the action does not take effect, the cryptographic token may be missing or stale. Capture and replay the token using the XHR interceptor pattern above.
571
+
572
+ ### Extension/Programmatic APIs
573
+
574
+ When standard API paths are blocked (undocumented crypto tokens, deprecated endpoints), complex web apps often expose higher-level programmatic interfaces:
575
+
576
+ - Internal extension APIs on \`window\` (compose, send, draft management)
577
+ - JavaScript-exposed infrastructure for accessibility or testing
578
+ - \`webpackChunk\`-based module access to internal stores
579
+
580
+ Discovery: use \`browser_execute_script\` with \`Object.keys(window).filter(k => !['location', 'chrome', 'document', 'navigator'].includes(k))\` to find non-standard globals, then explore their methods.
581
+
582
+ ### API Deprecation
583
+
584
+ Internal API endpoints can be deprecated without warning. When multiple API generations exist, test each endpoint independently. If an endpoint returns 404 or 403 unexpectedly, it may be deprecated for that account or region. Remove tools that depend on deprecated endpoints rather than shipping broken tools.
585
+
586
+ ## CSP Considerations
587
+
588
+ The adapter IIFE bypasses the page's Content Security Policy via file-based injection (\`chrome.scripting.executeScript({ files: [...] })\`). Plugin code runs as extension-origin code and is not subject to inline script restrictions.
589
+
590
+ **Trusted Types**: Some pages enforce Trusted Types CSP, which blocks \`innerHTML\`, \`outerHTML\`, and \`insertAdjacentHTML\`. If you need to extract text from HTML strings, use regex instead:
591
+
592
+ \`\`\`typescript
593
+ const text = html.replace(/<[^>]+>/g, '');
594
+ \`\`\`
595
+ `;
596
+ //# sourceMappingURL=plugin-development.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin-development.js","sourceRoot":"","sources":["../../src/resources/plugin-development.ts"],"names":[],"mappings":"AAAA,iDAAiD;AAEjD,MAAM,CAAC,MAAM,0BAA0B,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAilBzC,CAAC"}
@@ -0,0 +1,3 @@
1
+ /** Quick Start Guide resource content. */
2
+ export declare const QUICK_START_CONTENT = "# OpenTabs Quick Start Guide\n\n## What is OpenTabs?\n\nOpenTabs is a platform that gives AI agents access to web applications through the user's authenticated browser session. It consists of:\n\n- **MCP Server** \u2014 runs on localhost, serves tools to AI clients via Streamable HTTP\n- **Chrome Extension** \u2014 injects plugin adapters into matching browser tabs, relays tool calls\n- **Plugin SDK** \u2014 allows anyone to create plugins as standalone npm packages\n\nWhen connected, your AI client gets browser tools (tab management, screenshots, DOM interaction, network capture) and plugin tools (e.g., `slack_send_message`, `github_list_repos`) that operate in the user's authenticated context.\n\n## Installation\n\n```bash\nnpm install -g @opentabs-dev/cli\n```\n\n## Starting the Server\n\n```bash\nopentabs start\n```\n\nOn first run, this:\n1. Creates `~/.opentabs/` (config, logs, extension files)\n2. Generates a WebSocket auth secret at `~/.opentabs/extension/auth.json`\n3. Prints MCP client configuration blocks for Claude Code, Cursor, and Windsurf\n4. Starts the MCP server on `http://127.0.0.1:9515/mcp`\n\nTo re-display the configuration blocks later:\n\n```bash\nopentabs start --show-config\n```\n\n## Loading the Chrome Extension\n\n1. Open `chrome://extensions/` in Chrome\n2. Enable **Developer mode** (top-right toggle)\n3. Click **Load unpacked** and select `~/.opentabs/extension`\n\nThe extension icon appears in the toolbar. Click it to open the side panel showing plugin states and tool permissions.\n\n## Configuring Your MCP Client\n\nGet the auth secret:\n\n```bash\nopentabs config show --json --show-secret | jq -r .secret\n```\n\n### Claude Code\n\nCLI method (recommended):\n\n```bash\nclaude mcp add --transport http opentabs http://127.0.0.1:9515/mcp \\\n --header \"Authorization: Bearer YOUR_SECRET_HERE\"\n```\n\nOr merge into `~/.claude.json`:\n\n```json\n{\n \"mcpServers\": {\n \"opentabs\": {\n \"type\": \"streamable-http\",\n \"url\": \"http://127.0.0.1:9515/mcp\",\n \"headers\": {\n \"Authorization\": \"Bearer YOUR_SECRET_HERE\"\n }\n }\n }\n}\n```\n\n### Cursor\n\nAdd to `.cursor/mcp.json`:\n\n```json\n{\n \"mcpServers\": {\n \"opentabs\": {\n \"type\": \"http\",\n \"url\": \"http://127.0.0.1:9515/mcp\",\n \"headers\": {\n \"Authorization\": \"Bearer YOUR_SECRET_HERE\"\n }\n }\n }\n}\n```\n\n### Windsurf\n\nAdd to `~/.codeium/windsurf/mcp_config.json`:\n\n```json\n{\n \"mcpServers\": {\n \"opentabs\": {\n \"serverUrl\": \"http://127.0.0.1:9515/mcp\",\n \"headers\": {\n \"Authorization\": \"Bearer YOUR_SECRET_HERE\"\n }\n }\n }\n}\n```\n\n### OpenCode\n\nAdd to `opencode.json` in the project root:\n\n```json\n{\n \"mcp\": {\n \"opentabs\": {\n \"type\": \"remote\",\n \"url\": \"http://127.0.0.1:9515/mcp\",\n \"headers\": {\n \"Authorization\": \"Bearer YOUR_SECRET_HERE\"\n }\n }\n }\n}\n```\n\n## Installing a Plugin\n\n```bash\nopentabs plugin search # Browse available plugins\nopentabs plugin install <name> # Install (e.g., opentabs plugin install slack)\n```\n\nAfter installing, open the target web app in Chrome (e.g., `app.slack.com` for Slack). The extension detects the matching tab and loads the plugin adapter.\n\n## Plugin Review Flow\n\nPlugins start with permission `'off'` and must be reviewed before use. When you call a tool on an unreviewed plugin, the error response guides you through the review:\n\n1. Call `plugin_inspect` with the plugin name to retrieve the adapter source code and a review token\n2. Review the code for security (the response includes review guidance)\n3. If the code is safe, call `plugin_mark_reviewed` with the review token and desired permission (`'ask'` or `'auto'`)\n4. The plugin is now active \u2014 its tools are available\n\nWhen a plugin updates to a new version, its permission resets to `'off'` and requires re-review.\n\n## Permission Model\n\nEvery tool has a 3-state permission:\n\n| Permission | Behavior |\n|------------|----------|\n| `'off'` | Disabled \u2014 tool call returns an error |\n| `'ask'` | Requires human approval via the side panel dialog |\n| `'auto'` | Executes immediately without user confirmation |\n\nConfigure permissions via CLI:\n\n```bash\nopentabs config set plugin-permission.<plugin> ask\nopentabs config set tool-permission.<plugin>.<tool> auto\n```\n\nTo bypass all permission checks (development only):\n\n```bash\nOPENTABS_DANGEROUSLY_SKIP_PERMISSIONS=1 opentabs start\n```\n\n## Available Tool Categories\n\n### Plugin Tools (`<plugin>_<tool>`)\nExecute inside the web page context using the user's authenticated browser session. Each plugin exposes domain-specific tools (e.g., `slack_send_message`, `github_create_issue`).\n\n### Browser Tools (`browser_*`) \u2014 40 built-in tools\nGeneral-purpose tools organized by category:\n- **Tab Management** \u2014 open, close, list, switch tabs\n- **Content Retrieval** \u2014 read page content, HTML, take screenshots\n- **DOM Interaction** \u2014 click elements, type text, query selectors\n- **Scroll & Navigation** \u2014 scroll, navigate, go back/forward\n- **Storage & Cookies** \u2014 read/write localStorage, sessionStorage, cookies\n- **Network Capture** \u2014 capture and inspect network requests, WebSocket frames, HAR export\n- **Console** \u2014 read browser console logs\n- **Site Analysis** \u2014 comprehensive analysis of a web page for plugin development\n\n### Extension Tools (`extension_*`)\nDiagnostics: extension state, logs, adapter injection status, WebSocket connectivity.\n\n## Multi-Tab Targeting\n\nWhen multiple tabs match a plugin, use `plugin_list_tabs` to discover available tabs and their IDs. Pass the optional `tabId` parameter to any plugin tool to target a specific tab. Without `tabId`, the platform auto-selects the best-ranked tab.\n\n## Verifying the Setup\n\n```bash\nopentabs status # Check server, extension, and plugin status\nopentabs doctor # Run diagnostics and suggest fixes\n```\n\nFrom your AI client, you can also:\n1. Fetch `opentabs://status` to get a JSON snapshot of the server state\n2. Call `extension_get_state` to verify the Chrome extension is connected\n3. Call `plugin_list_tabs` to see which plugin tabs are ready\n";
3
+ //# sourceMappingURL=quick-start.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quick-start.d.ts","sourceRoot":"","sources":["../../src/resources/quick-start.ts"],"names":[],"mappings":"AAAA,0CAA0C;AAE1C,eAAO,MAAM,mBAAmB,qtMA+M/B,CAAC"}