@gjsify/devtools-mcp 0.8.0 → 0.11.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 ADDED
@@ -0,0 +1,66 @@
1
+ # @gjsify/devtools-mcp
2
+
3
+ The **MCP bridge** for the gjsify devtools control plane. It speaks JSON-RPC on stdio to an MCP client (Claude Code, the MCP Inspector, …) and translates each tool call to the app's `org.gjsify.Devtools` DBus interface. The result: an AI agent can inspect, screenshot, and drive any [`@gjsify/devtools`](../devtools)-enabled GJS app.
4
+
5
+ You usually don't import this directly — `gjsify debug` generates a bridge entry that calls `runDevtoolsMcp(profile)` and builds/launches it. Reach for the library API only when wiring a custom bridge.
6
+
7
+ ## Quick start — `gjsify debug`
8
+
9
+ 1. Enable devtools in your app (see [`@gjsify/devtools`](../devtools)) and run it with `GJSIFY_DEVTOOLS=1`.
10
+ 2. Point your MCP client at the bridge. The CLI builds it on demand:
11
+
12
+ ```jsonc
13
+ // .mcp.json
14
+ {
15
+ "mcpServers": {
16
+ "my-app": { "command": "gjsify", "args": ["debug", "--bus-name", "org.example.App"] }
17
+ }
18
+ }
19
+ ```
20
+
21
+ The bridge auto-selects a **tool profile** from your `package.json` deps: `storybook` if `@gjsify/storybook` is present, `browser` if `@gjsify/devtools-browser` is, else the `generic` profile. Override with `--profile`.
22
+
23
+ For a fixed binary path (no rebuild per launch), build once and point at the bundle:
24
+
25
+ ```bash
26
+ gjsify debug --build-only --out dist/bridge.gjs.mjs
27
+ # .mcp.json → { "command": "dist/bridge.gjs.mjs" }
28
+ ```
29
+
30
+ ## Profiles
31
+
32
+ A `DevtoolsToolProfile` maps the generic + app-specific DBus methods to MCP tools:
33
+
34
+ - **generic** — `get_status`, `screenshot`, `list_actions`, `activate_action`, `resize_window`, `present_window`, and the introspection tools.
35
+ - **storybook** — `list_stories`, `get_current_story`, `open_story`, `set_story_arg` (+ generics; screenshot via the generic `screenshot`).
36
+ - **browser** — the [`@gjsify/devtools-browser`](../devtools-browser) web-debugging tools: `navigate`, `page_screenshot`, `eval_js`, `inspect_element`, `dom_tree`, `get_network`, … (+ generics).
37
+
38
+ ## Library API
39
+
40
+ ```ts
41
+ import { runDevtoolsMcp, storybookProfile } from '@gjsify/devtools-mcp';
42
+
43
+ // A built-in profile:
44
+ await runDevtoolsMcp(storybookProfile('org.example.Storybook'));
45
+
46
+ // Or the generic profile for any devtools-enabled app:
47
+ await runDevtoolsMcp({ name: 'my-app-devtools', version: '0.10.0', busNameBase: 'org.example.App' });
48
+ ```
49
+
50
+ ## Exports
51
+
52
+ - `runDevtoolsMcp(profile)` — build the MCP server, register the profile's tools, serve over `GjsStdioTransport`.
53
+ - `DbusDevtoolsClient` — the DBus client (`control()` for raw GVariant replies, `jsonCall()` for `…->s` JSON methods).
54
+ - `registerGenericTools`, `storybookProfile` / `registerStorybookTools`.
55
+ - `GjsStdioTransport` — stdio transport (Node's `StdioServerTransport` does not work under the GJS bundle).
56
+ - `ok` / `fail` / `image` (`ToolResult` helpers; `image` takes base64), `dbusError` (error mapping).
57
+ - `DevtoolsToolProfile`, `McpToolContext`, `GenericToolName`, and the re-exported `@gjsify/devtools-protocol` contract.
58
+
59
+ > **MCP gotcha:** stdout is the JSON-RPC channel — the bridge and `gjsify debug` log to **stderr only**. Keep any custom logging off stdout.
60
+
61
+ ## Build / test
62
+
63
+ ```bash
64
+ gjsify workspace @gjsify/devtools-mcp build
65
+ gjsify workspace @gjsify/devtools-mcp test
66
+ ```
@@ -1 +1 @@
1
- import"./_virtual/_rolldown/runtime.js";import e from"@girs/glib-2.0";import t from"@girs/gio-2.0";import{DEVTOOLS_INTERFACE as n,resolveBusAddress as r}from"@gjsify/devtools-protocol";const i=`org.freedesktop.DBus`;var DbusDevtoolsClient=class{constructor(e){this.busNameBase=e,this._bus=t.bus_get_sync(t.BusType.SESSION,null)}resolve(e){return r(this.busNameBase,e)}control(r,i,a,o){let{busName:s,objectPath:c}=this.resolve(r);return new Promise((r,l)=>{this._bus.call(s,c,n,i,a,o?e.VariantType.new(o):null,t.DBusCallFlags.NONE,-1,null,(e,t)=>{try{r(e.call_finish(t))}catch(e){l(e)}})})}async jsonCall(e,t,n=null){let[r]=(await this.control(e,t,n,`(s)`)).recursiveUnpack();return JSON.stringify(JSON.parse(r),null,2)}nameHasOwner(n){return new Promise(r=>{this._bus.call(i,`/org/freedesktop/DBus`,i,`NameHasOwner`,e.Variant.new_tuple([e.Variant.new_string(n)]),e.VariantType.new(`(b)`),t.DBusCallFlags.NONE,-1,null,(e,t)=>{try{r(e.call_finish(t).recursiveUnpack()[0])}catch{r(!1)}})})}listInstances(){return new Promise(n=>{this._bus.call(i,`/org/freedesktop/DBus`,i,`ListNames`,null,e.VariantType.new(`(as)`),t.DBusCallFlags.NONE,-1,null,(e,t)=>{try{let r=e.call_finish(t).recursiveUnpack()[0],i=this.busNameBase;n(r.filter(e=>e===i||e.startsWith(`${i}.`)).map(e=>({instance:e===i?`default`:e.slice(i.length+1),busName:e})))}catch{n([])}})})}};export{DbusDevtoolsClient};
1
+ import"./_virtual/_rolldown/runtime.js";import e from"@girs/gio-2.0";import t from"@girs/glib-2.0";import{DEVTOOLS_INTERFACE as n,resolveBusAddress as r}from"@gjsify/devtools-protocol";const i=`org.freedesktop.DBus`;var DbusDevtoolsClient=class{constructor(t){this.busNameBase=t,this._bus=e.bus_get_sync(e.BusType.SESSION,null)}resolve(e){return r(this.busNameBase,e)}control(r,i,a,o){let{busName:s,objectPath:c}=this.resolve(r);return new Promise((r,l)=>{this._bus.call(s,c,n,i,a,o?t.VariantType.new(o):null,e.DBusCallFlags.NONE,-1,null,(e,t)=>{try{r(e.call_finish(t))}catch(e){l(e)}})})}async jsonCall(e,t,n=null){let[r]=(await this.control(e,t,n,`(s)`)).recursiveUnpack();return JSON.stringify(JSON.parse(r),null,2)}nameHasOwner(n){return new Promise(r=>{this._bus.call(i,`/org/freedesktop/DBus`,i,`NameHasOwner`,t.Variant.new_tuple([t.Variant.new_string(n)]),t.VariantType.new(`(b)`),e.DBusCallFlags.NONE,-1,null,(e,t)=>{try{r(e.call_finish(t).recursiveUnpack()[0])}catch{r(!1)}})})}listInstances(){return new Promise(n=>{this._bus.call(i,`/org/freedesktop/DBus`,i,`ListNames`,null,t.VariantType.new(`(as)`),e.DBusCallFlags.NONE,-1,null,(e,t)=>{try{let r=e.call_finish(t).recursiveUnpack()[0],i=this.busNameBase;n(r.filter(e=>e===i||e.startsWith(`${i}.`)).map(e=>({instance:e===i?`default`:e.slice(i.length+1),busName:e})))}catch{n([])}})})}};export{DbusDevtoolsClient};
package/lib/esm/index.js CHANGED
@@ -1 +1 @@
1
- import{DbusDevtoolsClient as e}from"./dbus-client.js";import{fail as t,image as n,ok as r}from"./tool-result.js";import{dbusError as i}from"./error-map.js";import{registerGenericTools as a}from"./generic-tools.js";import{GjsStdioTransport as o}from"./stdio-transport.js";import{runDevtoolsMcp as s}from"./run-server.js";import{registerStorybookTools as c,storybookProfile as l}from"./profiles/storybook.js";export*from"@gjsify/devtools-protocol";export{e as DbusDevtoolsClient,o as GjsStdioTransport,i as dbusError,t as fail,n as image,r as ok,a as registerGenericTools,c as registerStorybookTools,s as runDevtoolsMcp,l as storybookProfile};
1
+ import{DbusDevtoolsClient as e}from"./dbus-client.js";import{fail as t,image as n,ok as r}from"./tool-result.js";import{dbusError as i}from"./error-map.js";import{registerGenericTools as a}from"./generic-tools.js";import{GjsStdioTransport as o}from"./stdio-transport.js";import{runDevtoolsMcp as s}from"./run-server.js";import{registerStorybookTools as c,storybookProfile as l}from"./profiles/storybook.js";import{browserProfile as u,registerBrowserTools as d}from"./profiles/browser.js";export*from"@gjsify/devtools-protocol";export{e as DbusDevtoolsClient,o as GjsStdioTransport,u as browserProfile,i as dbusError,t as fail,n as image,r as ok,d as registerBrowserTools,a as registerGenericTools,c as registerStorybookTools,s as runDevtoolsMcp,l as storybookProfile};
@@ -0,0 +1 @@
1
+ import"../_virtual/_rolldown/runtime.js";import{image as e}from"../tool-result.js";import t from"@girs/glib-2.0";import{z as n}from"zod";const strv=e=>t.Variant.new_string(e),intv=e=>t.Variant.new_int32(e),r={instance:n.string().optional().describe(`Browser instance label; omit for the default app`)};function registerBrowserTools(i){let{server:a,client:o,ok:s,dbusError:c}=i;a.registerTool(`navigate`,{description:"Navigate to a URL (a `page:*` built-in page or `https://…`). Updates the history stack + URL bar.",inputSchema:n.object({url:n.string(),...r})},async({url:e,instance:n})=>{try{return await o.control(n,`BrowserNavigate`,t.Variant.new_tuple([strv(e)]),null),s(`Navigated to ${e}`)}catch(e){return c(e,n)}});for(let[e,t,i]of[[`back`,`BrowserBack`,`Go back one entry in browser history.`],[`forward`,`BrowserForward`,`Go forward one entry in browser history.`],[`reload`,`BrowserReload`,`Reload the current page.`]])a.registerTool(e,{description:i,inputSchema:n.object({...r})},async({instance:e})=>{try{return await o.control(e,t,null,null),s(i)}catch(t){return c(t,e)}});a.registerTool(`get_page_info`,{description:`Current page state: uri, appUrl, title, canGoBack, canGoForward, lastLoadError.`,inputSchema:n.object({...r})},async({instance:e})=>{try{return s(await o.jsonCall(e,`BrowserGetPageInfo`))}catch(t){return c(t,e)}}),a.registerTool(`page_screenshot`,{description:"PNG of the rendered WEB CONTENT via the WebKit compositor (use this, not the generic `screenshot`, for the page — WebKit composites out-of-process so the GSK shot is blank). region=full (whole scrollable document) | visible (viewport).",inputSchema:n.object({region:n.enum([`full`,`visible`]).optional().default(`full`),...r})},async({region:n,instance:r})=>{try{let a=(await o.control(r,`BrowserScreenshot`,t.Variant.new_tuple([strv(n)]),`(ay)`)).get_child_value(0).deepUnpack();return!a||a.length===0?i.fail(`page_screenshot returned no data — is a page loaded and the window realized?`):e(t.base64_encode(a))}catch(e){return c(e,r)}}),a.registerTool(`set_viewport`,{description:`Set the WebView content-region size in pixels (responsive testing); returns the applied size.`,inputSchema:n.object({width:n.number().int(),height:n.number().int(),...r})},async({width:e,height:n,instance:r})=>{try{let[i,a]=(await o.control(r,`BrowserSetViewport`,t.Variant.new_tuple([intv(e),intv(n)]),`(ii)`)).recursiveUnpack();return s(`Viewport set to ${i}x${a}`)}catch(e){return c(e,r)}}),a.registerTool(`eval_js`,{description:"Evaluate a JavaScript EXPRESSION in the page and return its value (JSON round-trip). Wrap multi-statement code in an IIFE, e.g. `(() => { const a = 1; return a + 1; })()`. Page errors surface as an error.",inputSchema:n.object({script:n.string(),...r})},async({script:e,instance:n})=>{try{let[r]=(await o.control(n,`BrowserEvalJs`,t.Variant.new_tuple([strv(e)]),`(s)`)).recursiveUnpack();return s(r)}catch(e){return c(e,n)}}),a.registerTool(`get_links`,{description:"Every `<a href>` on the page as {href, text, title}.",inputSchema:n.object({...r})},async({instance:e})=>{try{return s(await o.jsonCall(e,`BrowserGetLinks`))}catch(t){return c(t,e)}}),a.registerTool(`follow_link`,{description:"Click the first element matching a CSS selector (or an `<a>` by exact visible text), then wait for the resulting navigation. Returns {clicked, navigated, uri}.",inputSchema:n.object({selectorOrText:n.string(),...r})},async({selectorOrText:e,instance:n})=>{try{let[r]=(await o.control(n,`BrowserFollowLink`,t.Variant.new_tuple([strv(e)]),`(s)`)).recursiveUnpack();return s(r)}catch(e){return c(e,n)}}),a.registerTool(`query_dom`,{description:`Metadata for elements matching a CSS selector ({tagName, id, className, text, href?, visible}).`,inputSchema:n.object({selector:n.string(),...r})},async({selector:e,instance:n})=>{try{let[r]=(await o.control(n,`BrowserQueryDom`,t.Variant.new_tuple([strv(e)]),`(s)`)).recursiveUnpack();return s(r)}catch(e){return c(e,n)}}),a.registerTool(`wait_for_load`,{description:`Wait for the next page load to finish (ms; default 30000). Returns whether it loaded in time.`,inputSchema:n.object({timeout:n.number().int().optional().default(3e4),...r})},async({timeout:e,instance:n})=>{try{let[r]=(await o.control(n,`BrowserWaitForLoad`,t.Variant.new_tuple([intv(e)]),`(b)`)).recursiveUnpack();return s(r?`Page loaded.`:`Timed out waiting for the next load.`)}catch(e){return c(e,n)}}),a.registerTool(`get_console`,{description:`Buffered console.* output captured from the page ({level, args}).`,inputSchema:n.object({...r})},async({instance:e})=>{try{return s(await o.jsonCall(e,`BrowserGetConsole`))}catch(t){return c(t,e)}}),a.registerTool(`inspect_element`,{description:`Inspect the first element matching a CSS selector — tag/id/class + attributes + bounding rect + box model (margin/border/padding/content) + a curated set of computed styles. The Elements + Computed + Box Model panels in one call.`,inputSchema:n.object({selector:n.string(),...r})},async({selector:e,instance:n})=>{try{let[r]=(await o.control(n,`BrowserInspectElement`,t.Variant.new_tuple([strv(e)]),`(s)`)).recursiveUnpack();return s(r)}catch(e){return c(e,n)}}),a.registerTool(`dom_tree`,{description:`The DOM/Elements tree from a selector (default: document root) down to max_depth, capped per node.`,inputSchema:n.object({selector:n.string().optional().default(``),maxDepth:n.number().int().optional().default(3),...r})},async({selector:e,maxDepth:n,instance:r})=>{try{let[i]=(await o.control(r,`BrowserDomTree`,t.Variant.new_tuple([strv(e),intv(n)]),`(s)`)).recursiveUnpack();return s(i)}catch(e){return c(e,r)}}),a.registerTool(`get_network`,{description:`Page network activity via the Resource Timing API — the navigation entry + each resource.`,inputSchema:n.object({...r})},async({instance:e})=>{try{return s(await o.jsonCall(e,`BrowserGetNetwork`))}catch(t){return c(t,e)}}),a.registerTool(`get_accessibility`,{description:`Approximate accessibility tree from a selector (default: body) — role + accessible name + aria-* per node, down to max_depth.`,inputSchema:n.object({selector:n.string().optional().default(``),maxDepth:n.number().int().optional().default(4),...r})},async({selector:e,maxDepth:n,instance:r})=>{try{let[i]=(await o.control(r,`BrowserGetAccessibility`,t.Variant.new_tuple([strv(e),intv(n)]),`(s)`)).recursiveUnpack();return s(i)}catch(e){return c(e,r)}});for(let[e,t,i]of[[`open_inspector`,`BrowserOpenInspector`,`Open the WebKit Web Inspector panel on the browser window.`],[`close_inspector`,`BrowserCloseInspector`,`Close the WebKit Web Inspector panel.`]])a.registerTool(e,{description:i,inputSchema:n.object({...r})},async({instance:e})=>{try{return await o.control(e,t,null,null),s(i)}catch(t){return c(t,e)}})}function browserProfile(e){return{name:`gjsify-browser-devtools`,version:`0.10.0`,busNameBase:e,registerTools:registerBrowserTools}}export{browserProfile,registerBrowserTools};
@@ -1 +1 @@
1
- import"./_virtual/_rolldown/runtime.js";import e from"@girs/glib-2.0";import t from"@girs/gio-2.0";import n from"@girs/giounix-2.0";t._promisify(t.DataInputStream.prototype,`read_line_async`,`read_line_finish`);var GjsStdioTransport=class{constructor(e){this.onExit=e,this._in=null,this._out=null,this._cancellable=new t.Cancellable,this._closed=!1,this._encoder=new TextEncoder,this._decoder=new TextDecoder}async start(){if(this._in)throw Error(`GjsStdioTransport already started`);let e=n.InputStream.new(0,!1);this._in=new t.DataInputStream({base_stream:e}),this._out=n.OutputStream.new(1,!1),this._readLoop()}async _readLoop(){for(;!this._closed&&this._in;){let t;try{[t]=await this._in.read_line_async(e.PRIORITY_DEFAULT,this._cancellable)}catch(e){this._closed||this.onerror?.(e instanceof Error?e:Error(String(e)));break}if(t===null)break;let n=this._decoder.decode(t).trim();if(n)try{this.onmessage?.(JSON.parse(n))}catch(e){this.onerror?.(e instanceof Error?e:Error(String(e)))}}this._closed||(this._closed=!0,this.onclose?.()),this.onExit?.()}async send(e){if(!this._out)throw Error(`GjsStdioTransport not started`);this._out.write_all(this._encoder.encode(`${JSON.stringify(e)}\n`),null)}async close(){this._closed||(this._closed=!0,this._cancellable.cancel(),this.onclose?.(),this.onExit?.())}};export{GjsStdioTransport};
1
+ import"./_virtual/_rolldown/runtime.js";import e from"@girs/gio-2.0";import t from"@girs/glib-2.0";import n from"@girs/giounix-2.0";e._promisify(e.DataInputStream.prototype,`read_line_async`,`read_line_finish`);var GjsStdioTransport=class{constructor(t){this.onExit=t,this._in=null,this._out=null,this._cancellable=new e.Cancellable,this._closed=!1,this._encoder=new TextEncoder,this._decoder=new TextDecoder}async start(){if(this._in)throw Error(`GjsStdioTransport already started`);let t=n.InputStream.new(0,!1);this._in=new e.DataInputStream({base_stream:t}),this._out=n.OutputStream.new(1,!1),this._readLoop()}async _readLoop(){for(;!this._closed&&this._in;){let e;try{[e]=await this._in.read_line_async(t.PRIORITY_DEFAULT,this._cancellable)}catch(e){this._closed||this.onerror?.(e instanceof Error?e:Error(String(e)));break}if(e===null)break;let n=this._decoder.decode(e).trim();if(n)try{this.onmessage?.(JSON.parse(n))}catch(e){this.onerror?.(e instanceof Error?e:Error(String(e)))}}this._closed||(this._closed=!0,this.onclose?.()),this.onExit?.()}async send(e){if(!this._out)throw Error(`GjsStdioTransport not started`);this._out.write_all(this._encoder.encode(`${JSON.stringify(e)}\n`),null)}async close(){this._closed||(this._closed=!0,this._cancellable.cancel(),this.onclose?.(),this.onExit?.())}};export{GjsStdioTransport};
@@ -7,4 +7,5 @@ export type { ToolResult } from './tool-result.js';
7
7
  export { GjsStdioTransport } from './stdio-transport.js';
8
8
  export type { DevtoolsToolProfile, GenericToolName, McpToolContext } from './profile.js';
9
9
  export { registerStorybookTools, storybookProfile } from './profiles/storybook.js';
10
+ export { registerBrowserTools, browserProfile } from './profiles/browser.js';
10
11
  export * from '@gjsify/devtools-protocol';
@@ -0,0 +1,4 @@
1
+ import type { DevtoolsToolProfile, McpToolContext } from '../profile.js';
2
+ export declare function registerBrowserTools(ctx: McpToolContext): void;
3
+ /** Tool profile for a @gjsify/devtools-browser app. */
4
+ export declare function browserProfile(busNameBase: string): DevtoolsToolProfile;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/devtools-mcp",
3
- "version": "0.8.0",
3
+ "version": "0.11.0",
4
4
  "description": "MCP↔devtools bridge for GJS apps — exposes a running app's org.gjsify.Devtools control plane (DBus) to an AI agent as MCP tools",
5
5
  "type": "module",
6
6
  "module": "lib/esm/index.js",
@@ -38,13 +38,13 @@
38
38
  "@girs/giounix-2.0": "2.0.0-4.0.4",
39
39
  "@girs/gjs": "4.0.4",
40
40
  "@girs/glib-2.0": "2.88.0-4.0.4",
41
- "@gjsify/devtools-protocol": "^0.8.0",
41
+ "@gjsify/devtools-protocol": "^0.11.0",
42
42
  "@modelcontextprotocol/sdk": "^1.29.0",
43
- "zod": "4.3.6"
43
+ "zod": "^4.4.3"
44
44
  },
45
45
  "devDependencies": {
46
- "@gjsify/cli": "^0.8.0",
47
- "@gjsify/unit": "^0.8.0",
46
+ "@gjsify/cli": "^0.11.0",
47
+ "@gjsify/unit": "^0.11.0",
48
48
  "@types/node": "^25.9.2",
49
49
  "typescript": "^6.0.3"
50
50
  },