@gjsify/devtools-mcp 0.27.1 → 0.29.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 +20 -1
- package/lib/esm/dbus-client.js +1 -1
- package/lib/esm/error-map.js +1 -1
- package/lib/esm/generic-tools.js +1 -1
- package/lib/esm/index.js +1 -1
- package/lib/esm/profiles/browser.js +1 -1
- package/lib/esm/profiles/cdp.js +1 -1
- package/lib/esm/profiles/storybook.js +1 -1
- package/lib/esm/run-server.js +1 -1
- package/lib/esm/transport-choice.js +1 -0
- package/lib/types/dbus-client.d.ts +92 -10
- package/lib/types/dbus-client.spec.d.ts +2 -0
- package/lib/types/error-map.d.ts +6 -2
- package/lib/types/index.d.ts +4 -1
- package/lib/types/profile.d.ts +12 -0
- package/lib/types/profiles/browser.d.ts +2 -2
- package/lib/types/profiles/cdp.d.ts +2 -2
- package/lib/types/profiles/storybook.d.ts +2 -2
- package/lib/types/transport-choice.d.ts +44 -0
- package/lib/types/transport-choice.spec.d.ts +2 -0
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -27,6 +27,24 @@ gjsify debug --build-only --out dist/bridge.gjs.mjs
|
|
|
27
27
|
# .mcp.json → { "command": "dist/bridge.gjs.mjs" }
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
+
## No session bus? (macOS, Windows)
|
|
31
|
+
|
|
32
|
+
The bridge dials a **peer address** when there is no session bus — see [`@gjsify/devtools`](../devtools#no-session-bus-macos-windows) for the app side and the `dbus-run-session` trap. Precedence, mirroring the app's:
|
|
33
|
+
|
|
34
|
+
1. `--address <addr>` (equivalently `gjsify.devtools.address` in `package.json`, `profile.address`, or `new DbusDevtoolsClient(base, { address })`)
|
|
35
|
+
2. `GJSIFY_DEVTOOLS_ADDRESS`
|
|
36
|
+
3. the address file the app publishes under `<runtime-dir>/gjsify-devtools/` — it outranks the bus because it is meant to be evidence that an app of exactly this id is listening right now, so the bridge **checks** it: the address is dialled, and if nothing answers the file is deleted and resolution continues at step 4. Ctrl-C, `SIGKILL` and a crash all leave the file behind (and on macOS/Windows `GLib.get_user_runtime_dir()` degrades to the user cache dir, where it survives reboots), so a claim that is merely *present* proves nothing
|
|
37
|
+
4. the session bus (Linux default, unchanged: with nothing in peer mode no address file exists, so step 3 is a `stat` that misses — and a stale one degrades to exactly this row)
|
|
38
|
+
5. otherwise a hard error naming all three ways in, rather than a client that cannot call anything
|
|
39
|
+
|
|
40
|
+
An **explicit** address (steps 1–2) deliberately does *not* fall back: it is pinned on both sides, so a dead one fails with a message naming that address instead of quietly talking to something else.
|
|
41
|
+
|
|
42
|
+
In peer mode a call carries **no destination bus name** (a peer connection has no name registry), `list_instances` reports the single connected app instead of enumerating the bus, and a failure names the address rather than a bus name.
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
gjsify debug --bus-name org.example.App --address unix:path=/tmp/myapp-devtools.sock
|
|
46
|
+
```
|
|
47
|
+
|
|
30
48
|
## Profiles
|
|
31
49
|
|
|
32
50
|
A `DevtoolsToolProfile` maps the generic + app-specific DBus methods to MCP tools:
|
|
@@ -51,7 +69,8 @@ await runDevtoolsMcp({ name: 'my-app-devtools', version: '0.10.0', busNameBase:
|
|
|
51
69
|
## Exports
|
|
52
70
|
|
|
53
71
|
- `runDevtoolsMcp(profile)` — build the MCP server, register the profile's tools, serve over `GjsStdioTransport`.
|
|
54
|
-
- `DbusDevtoolsClient` — the DBus client (`control()` for raw GVariant replies, `jsonCall()` for `…->s` JSON methods).
|
|
72
|
+
- `DbusDevtoolsClient` — the DBus client (`control()` for raw GVariant replies, `jsonCall()` for `…->s` JSON methods, `transport` for the resolved transport, `describeTarget()` for diagnostics).
|
|
73
|
+
- `chooseClientTransport(input)` — the precedence above as a pure function; `connectToDevtools(ctx, deps?)` — that precedence turned into a live connection (dial-then-fallback; `deps` exists so every row is testable on a host with no bus daemon); `ClientTransportChoice`, `DbusDevtoolsClientOptions`, `DevtoolsInstanceRef`.
|
|
55
74
|
- `registerGenericTools`, `storybookProfile` / `registerStorybookTools`.
|
|
56
75
|
- `GjsStdioTransport` — stdio transport (Node's `StdioServerTransport` does not work under the GJS bundle).
|
|
57
76
|
- `ok` / `fail` / `image` (`ToolResult` helpers; `image` takes base64), `dbusError` (error mapping).
|
package/lib/esm/dbus-client.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import"./_virtual/_rolldown/runtime.js";import e from"@girs/gio-2.0";import
|
|
1
|
+
import"./_virtual/_rolldown/runtime.js";import{chooseClientTransport as e}from"./transport-choice.js";import t from"@girs/gio-2.0";import n from"@girs/glib-2.0";import{DEVTOOLS_ADDRESS_ENV as r,DEVTOOLS_INSTANCE_ENV as i,DEVTOOLS_INTERFACE as a,devtoolsAddressFilePath as o,resolveBusAddress as s}from"@gjsify/devtools-protocol";const c=`org.freedesktop.DBus`;function envValue(e){let t=n.getenv(e);return t&&t!==``?t:null}function readAddressFile(e){if(!n.file_test(e,n.FileTest.EXISTS))return null;try{let[t,r]=n.file_get_contents(e);if(!t)return null;let i=new TextDecoder().decode(r).trim();return i===``?null:i}catch{return null}}function deleteAddressFile(e){try{t.File.new_for_path(e).delete(null)}catch{}}function dialPeer(e){return t.DBusConnection.new_for_address_sync(e,t.DBusConnectionFlags.AUTHENTICATION_CLIENT,null,null)}function threeWaysIn(e,t){return`Launch the app with GJSIFY_DEVTOOLS=1 (it then publishes ${o(`<runtime-dir>`,e,t)}), or set ${r} on both sides, or pass --address to \`gjsify debug\`.`}function connectToDevtools(n,i={}){let a=i.dial??dialPeer,o=i.sessionBus??(()=>t.bus_get_sync(t.BusType.SESSION,null)),s=n.addressFileValue,c=null;for(;;){let t=e({optionAddress:n.optionAddress,envAddress:n.envAddress,addressFileValue:s,sessionBusAvailable:n.sessionBusAvailable});if(t.kind===`session-bus`)try{return{bus:o(),transport:t}}catch(e){throw Error(`[gjsify-devtools-mcp] the session bus advertised in DBUS_SESSION_BUS_ADDRESS stopped answering while connecting to ${n.busNameBase}: ${e}. ${threeWaysIn(n.busNameBase,n.instance)}`)}if(t.kind===`unavailable`){let e=c?` (the address this app had published, ${c}, is dead — that stale claim was just removed)`:``;throw Error(`[gjsify-devtools-mcp] no way to reach ${n.busNameBase}: this machine has no session bus, and no peer address was given${e}. ${threeWaysIn(n.busNameBase,n.instance)}`)}try{return{bus:a(t.address),transport:t}}catch(e){if(t.source!==`address-file`)throw Error(`[gjsify-devtools-mcp] cannot reach ${n.busNameBase} at ${t.address} (from ${t.source===`option`?`--address`:r}): ${e}. Start the app with GJSIFY_DEVTOOLS=1 ${r}=${t.address} so it listens there, or omit the address and let the published address file / the session bus decide.`);console.error(`[gjsify-devtools-mcp] the address published for ${n.busNameBase} (${t.address} in ${n.addressFilePath}) does not answer: ${e}. Removing that stale claim — an app killed with Ctrl-C or SIGKILL never gets to retract it — and continuing down the precedence.`),deleteAddressFile(n.addressFilePath),c=t.address,s=null}}}function sessionBusAvailable(){if(!envValue(`DBUS_SESSION_BUS_ADDRESS`))return!1;try{return t.bus_get_sync(t.BusType.SESSION,null),!0}catch{return!1}}var DbusDevtoolsClient=class{constructor(e,t={}){this.busNameBase=e;let a=t.instance??envValue(i)??void 0,s=o(n.get_user_runtime_dir(),e,a),{bus:c,transport:l}=connectToDevtools({busNameBase:e,instance:a,addressFilePath:s,optionAddress:t.address,envAddress:envValue(r),addressFileValue:readAddressFile(s),sessionBusAvailable:sessionBusAvailable()});this._bus=c,this.transport=l}describeTarget(e){return this.transport.kind===`peer`?`peer address ${this.transport.address} (from ${this.transport.source})`:`session bus name ${this.resolve(e).busName}`}resolve(e){return s(this.busNameBase,e)}_destination(e){return this.transport.kind===`peer`?null:this.resolve(e).busName}control(e,r,i,o){let{objectPath:s}=this.resolve(e),c=this._destination(e);return new Promise((e,l)=>{this._bus.call(c,s,a,r,i,o?n.VariantType.new(o):null,t.DBusCallFlags.NONE,-1,null,(t,n)=>{try{e(t.call_finish(n))}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(e){return this.transport.kind===`peer`?Promise.resolve(!this._bus.is_closed()):new Promise(r=>{this._bus.call(c,`/org/freedesktop/DBus`,c,`NameHasOwner`,n.Variant.new_tuple([n.Variant.new_string(e)]),n.VariantType.new(`(b)`),t.DBusCallFlags.NONE,-1,null,(e,t)=>{try{r(e.call_finish(t).recursiveUnpack()[0])}catch{r(!1)}})})}listInstances(){if(this.transport.kind===`peer`){let e=this.transport.address;return Promise.resolve(this._bus.is_closed()?[]:[{instance:this.resolve().instance,address:e}])}return new Promise(e=>{this._bus.call(c,`/org/freedesktop/DBus`,c,`ListNames`,null,n.VariantType.new(`(as)`),t.DBusCallFlags.NONE,-1,null,(t,n)=>{try{let r=t.call_finish(n).recursiveUnpack()[0],i=this.busNameBase;e(r.filter(e=>e===i||e.startsWith(`${i}.`)).map(e=>({instance:e===i?`default`:e.slice(i.length+1),busName:e})))}catch{e([])}})})}};export{DbusDevtoolsClient,connectToDevtools};
|
package/lib/esm/error-map.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import"./_virtual/_rolldown/runtime.js";import{fail as e}from"./tool-result.js";import{parseDbusErrorMessage as t}from"@gjsify/devtools-protocol";const n=/ServiceUnknown|NameHasNoOwner|was not provided by any|StartServiceByName/;function dbusError(r,i){let
|
|
1
|
+
import"./_virtual/_rolldown/runtime.js";import{fail as e}from"./tool-result.js";import{parseDbusErrorMessage as t}from"@gjsify/devtools-protocol";const n=/ServiceUnknown|NameHasNoOwner|was not provided by any|StartServiceByName/;function dbusError(r,i,a){let o=r instanceof Error?r.message:String(r);if(n.test(o))return e(`No devtools-enabled app at ${a??`session bus name ${i.busName}`} (instance "${i.instance}"). Launch the app with GJSIFY_DEVTOOLS=1. (D-Bus error: ${o})`);let s=o.match(/GDBus\.Error:[^\s:]*:\s*([\s\S]+)/)?.[1]?.trim();if(s){let n=t(s);return e(n.code===`internal`?`D-Bus call failed: ${s}`:`${n.code}: ${n.message}`)}return e(`D-Bus call failed: ${o}`)}export{dbusError};
|
package/lib/esm/generic-tools.js
CHANGED
|
@@ -1 +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),r={instance:n.string().optional().describe(`App instance label; omit for the default app`)};function delay(e){return new Promise(n=>{t.timeout_add(t.PRIORITY_DEFAULT,e,()=>(n(),t.SOURCE_REMOVE))})}function registerGenericTools(i,a=`all`){let{server:o,client:s,ok:c,fail:l,dbusError:u}=i,want=e=>a===`all`||a.includes(e);want(`get_status`)&&o.registerTool(`get_status`,{description:`JSON snapshot of the running app: app id, active window, toplevel count, focused widget, pause state, plus any app-specific status contributed by extensions.`,inputSchema:n.object({...r})},async({instance:e})=>{try{return c(await s.jsonCall(e,`GetStatus`))}catch(t){return u(t,e)}}),want(`screenshot`)&&o.registerTool(`screenshot`,{description:`Capture a PNG screenshot of the active window via the GTK GSK pipeline. Needs a visible window (auto-raises + retries once when the capture comes back empty).`,inputSchema:n.object({scope:n.string().optional(),...r})},async({scope:n,instance:r})=>{try{let grab=async()=>(await s.control(r,`Screenshot`,t.Variant.new_tuple([strv(n??`window`)]),`(ay)`)).get_child_value(0).deepUnpack(),i=await grab();return(!i||i.length===0)&&(await s.control(r,`PresentWindow`,null,null),await delay(350),i=await grab()),!i||i.length===0?l(`Screenshot returned no data — the window is likely occluded or minimized. Bring it to the foreground (present_window) and retry.`):e(t.base64_encode(i))}catch(e){return u(e,r)}}),want(`list_actions`)&&o.registerTool(`list_actions`,{description:`List the app.* and win.* GActions (name, enabled, parameter/state types) that activate_action / change_action_state can drive.`,inputSchema:n.object({...r})},async({instance:e})=>{try{return c(await s.jsonCall(e,`ListActions`))}catch(t){return u(t,e)}}),want(`activate_action`)&&o.registerTool(`activate_action`,{description:`Activate a GAction by scope + name, with an optional parameter (a plain JSON value coerced to the action's declared type).`,inputSchema:n.object({scope:n.enum([`app`,`win`]),name:n.string(),value:n.union([n.string(),n.number(),n.boolean()]).optional(),...r})},async({scope:e,name:n,value:r,instance:i})=>{try{let a=r===void 0?``:JSON.stringify(r);return await s.control(i,`ActivateAction`,t.Variant.new_tuple([strv(e),strv(n),strv(a)]),null),c(`Activated ${e}.${n}`)}catch(e){return u(e,i)}}),want(`change_action_state`)&&o.registerTool(`change_action_state`,{description:`Set a stateful GAction’s state — for idempotent toggles like win.toggle-grid.`,inputSchema:n.object({scope:n.enum([`app`,`win`]),name:n.string(),value:n.union([n.string(),n.number(),n.boolean()]),...r})},async({scope:e,name:n,value:r,instance:i})=>{try{return await s.control(i,`ChangeActionState`,t.Variant.new_tuple([strv(e),strv(n),strv(JSON.stringify(r))]),null),c(`Set state ${e}.${n} = ${JSON.stringify(r)}`)}catch(e){return u(e,i)}}),want(`present_window`)&&o.registerTool(`present_window`,{description:`Raise + focus the app’s active window.`,inputSchema:n.object({...r})},async({instance:e})=>{try{return await s.control(e,`PresentWindow`,null,null),c(`Presented the window.`)}catch(t){return u(t,e)}}),want(`resize_window`)&&o.registerTool(`resize_window`,{description:`Resize the active window to an absolute pixel size (exercises responsive breakpoints). Returns the requested size.`,inputSchema:n.object({width:n.number(),height:n.number(),...r})},async({width:e,height:n,instance:r})=>{try{let[i,a]=(await s.control(r,`ResizeWindow`,t.Variant.new_tuple([t.Variant.new_int32(e),t.Variant.new_int32(n)]),`(ii)`)).recursiveUnpack();return c(`Resized to ${i}×${a}.`)}catch(e){return u(e,r)}}),want(`list_toplevels`)&&o.registerTool(`list_toplevels`,{description:`List the app’s live toplevel windows (path, type, title, mapped, focused).`,inputSchema:n.object({...r})},async({instance:e})=>{try{return c(await s.jsonCall(e,`ListToplevels`))}catch(t){return u(t,e)}}),want(`dump_tree`)&&o.registerTool(`dump_tree`,{description:'Dump the widget tree as JSON from `root` (a widget path like "toplevel:0/child:2", or omit for the active window), bounded by `depth`.',inputSchema:n.object({root:n.string().optional(),depth:n.number().optional(),...r})},async({root:e,depth:n,instance:r})=>{try{let i=t.Variant.new_tuple([strv(e??``),t.Variant.new_int32(n??8)]);return c(await s.jsonCall(r,`DumpTree`,i))}catch(e){return u(e,r)}}),want(`get_property`)&&o.registerTool(`get_property`,{description:`Read a widget’s GObject property by widget path + property name (JSON-safe value).`,inputSchema:n.object({path:n.string(),prop:n.string(),...r})},async({path:e,prop:n,instance:r})=>{try{let i=t.Variant.new_tuple([strv(e),strv(n)]);return c(await s.jsonCall(r,`GetProperty`,i))}catch(e){return u(e,r)}}),want(`get_focused`)&&o.registerTool(`get_focused`,{description:`The currently-focused widget (path, name, type), or null.`,inputSchema:n.object({...r})},async({instance:e})=>{try{return c(await s.jsonCall(e,`GetFocused`))}catch(t){return u(t,e)}}),want(`activate_widget`)&&o.registerTool(`activate_widget`,{description:`Activate the widget at a widget path (from dump_tree) — the click-drive counterpart of get_property: a Button emits clicked, an Entry activates, a Toggle toggles; a list/nav/preference row (GtkListBoxRow/AdwActionRow) is clicked on its GtkListBox (selected + row-activated). Returns whether the widget activated.`,inputSchema:n.object({path:n.string(),...r})},async({path:e,instance:n})=>{try{let[r]=(await s.control(n,`ActivateWidget`,t.Variant.new_tuple([strv(e)]),`(b)`)).recursiveUnpack();return c(r?`Activated widget at "${e}".`:`Widget at "${e}" is not activatable (no default activation).`)}catch(e){return u(e,n)}}),want(`dump_gsettings`)&&o.registerTool(`dump_gsettings`,{description:`Dump every key + value of an installed GSettings schema (read-only).`,inputSchema:n.object({schema:n.string(),...r})},async({schema:e,instance:n})=>{try{return c(await s.jsonCall(n,`DumpGSettings`,t.Variant.new_tuple([strv(e)])))}catch(e){return u(e,n)}}),want(`dump_css`)&&o.registerTool(`dump_css`,{description:`List the devtools-installed CSS provider names.`,inputSchema:n.object({...r})},async({instance:e})=>{try{return c(await s.jsonCall(e,`DumpCss`))}catch(t){return u(t,e)}}),want(`swap_css`)&&o.registerTool(`swap_css`,{description:`Live-install or replace a named CSS provider (high priority) — re-theme the running app without a restart. Returns applied.`,inputSchema:n.object({name:n.string(),css:n.string(),...r})},async({name:e,css:n,instance:r})=>{try{let[i]=(await s.control(r,`SwapCss`,t.Variant.new_tuple([strv(e),strv(n)]),`(b)`)).recursiveUnpack();return c(i?`Applied CSS provider "${e}".`:`No display — CSS not applied.`)}catch(e){return u(e,r)}}),want(`list_instances`)&&o.registerTool(`list_instances`,{description:`List devtools-enabled app instances on the session bus (
|
|
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),r={instance:n.string().optional().describe(`App instance label; omit for the default app`)};function delay(e){return new Promise(n=>{t.timeout_add(t.PRIORITY_DEFAULT,e,()=>(n(),t.SOURCE_REMOVE))})}function registerGenericTools(i,a=`all`){let{server:o,client:s,ok:c,fail:l,dbusError:u}=i,want=e=>a===`all`||a.includes(e);want(`get_status`)&&o.registerTool(`get_status`,{description:`JSON snapshot of the running app: app id, active window, toplevel count, focused widget, pause state, plus any app-specific status contributed by extensions.`,inputSchema:n.object({...r})},async({instance:e})=>{try{return c(await s.jsonCall(e,`GetStatus`))}catch(t){return u(t,e)}}),want(`screenshot`)&&o.registerTool(`screenshot`,{description:`Capture a PNG screenshot of the active window via the GTK GSK pipeline. Needs a visible window (auto-raises + retries once when the capture comes back empty).`,inputSchema:n.object({scope:n.string().optional(),...r})},async({scope:n,instance:r})=>{try{let grab=async()=>(await s.control(r,`Screenshot`,t.Variant.new_tuple([strv(n??`window`)]),`(ay)`)).get_child_value(0).deepUnpack(),i=await grab();return(!i||i.length===0)&&(await s.control(r,`PresentWindow`,null,null),await delay(350),i=await grab()),!i||i.length===0?l(`Screenshot returned no data — the window is likely occluded or minimized. Bring it to the foreground (present_window) and retry.`):e(t.base64_encode(i))}catch(e){return u(e,r)}}),want(`list_actions`)&&o.registerTool(`list_actions`,{description:`List the app.* and win.* GActions (name, enabled, parameter/state types) that activate_action / change_action_state can drive.`,inputSchema:n.object({...r})},async({instance:e})=>{try{return c(await s.jsonCall(e,`ListActions`))}catch(t){return u(t,e)}}),want(`activate_action`)&&o.registerTool(`activate_action`,{description:`Activate a GAction by scope + name, with an optional parameter (a plain JSON value coerced to the action's declared type).`,inputSchema:n.object({scope:n.enum([`app`,`win`]),name:n.string(),value:n.union([n.string(),n.number(),n.boolean()]).optional(),...r})},async({scope:e,name:n,value:r,instance:i})=>{try{let a=r===void 0?``:JSON.stringify(r);return await s.control(i,`ActivateAction`,t.Variant.new_tuple([strv(e),strv(n),strv(a)]),null),c(`Activated ${e}.${n}`)}catch(e){return u(e,i)}}),want(`change_action_state`)&&o.registerTool(`change_action_state`,{description:`Set a stateful GAction’s state — for idempotent toggles like win.toggle-grid.`,inputSchema:n.object({scope:n.enum([`app`,`win`]),name:n.string(),value:n.union([n.string(),n.number(),n.boolean()]),...r})},async({scope:e,name:n,value:r,instance:i})=>{try{return await s.control(i,`ChangeActionState`,t.Variant.new_tuple([strv(e),strv(n),strv(JSON.stringify(r))]),null),c(`Set state ${e}.${n} = ${JSON.stringify(r)}`)}catch(e){return u(e,i)}}),want(`present_window`)&&o.registerTool(`present_window`,{description:`Raise + focus the app’s active window.`,inputSchema:n.object({...r})},async({instance:e})=>{try{return await s.control(e,`PresentWindow`,null,null),c(`Presented the window.`)}catch(t){return u(t,e)}}),want(`resize_window`)&&o.registerTool(`resize_window`,{description:`Resize the active window to an absolute pixel size (exercises responsive breakpoints). Returns the requested size.`,inputSchema:n.object({width:n.number(),height:n.number(),...r})},async({width:e,height:n,instance:r})=>{try{let[i,a]=(await s.control(r,`ResizeWindow`,t.Variant.new_tuple([t.Variant.new_int32(e),t.Variant.new_int32(n)]),`(ii)`)).recursiveUnpack();return c(`Resized to ${i}×${a}.`)}catch(e){return u(e,r)}}),want(`list_toplevels`)&&o.registerTool(`list_toplevels`,{description:`List the app’s live toplevel windows (path, type, title, mapped, focused).`,inputSchema:n.object({...r})},async({instance:e})=>{try{return c(await s.jsonCall(e,`ListToplevels`))}catch(t){return u(t,e)}}),want(`dump_tree`)&&o.registerTool(`dump_tree`,{description:'Dump the widget tree as JSON from `root` (a widget path like "toplevel:0/child:2", or omit for the active window), bounded by `depth`.',inputSchema:n.object({root:n.string().optional(),depth:n.number().optional(),...r})},async({root:e,depth:n,instance:r})=>{try{let i=t.Variant.new_tuple([strv(e??``),t.Variant.new_int32(n??8)]);return c(await s.jsonCall(r,`DumpTree`,i))}catch(e){return u(e,r)}}),want(`get_property`)&&o.registerTool(`get_property`,{description:`Read a widget’s GObject property by widget path + property name (JSON-safe value).`,inputSchema:n.object({path:n.string(),prop:n.string(),...r})},async({path:e,prop:n,instance:r})=>{try{let i=t.Variant.new_tuple([strv(e),strv(n)]);return c(await s.jsonCall(r,`GetProperty`,i))}catch(e){return u(e,r)}}),want(`get_focused`)&&o.registerTool(`get_focused`,{description:`The currently-focused widget (path, name, type), or null.`,inputSchema:n.object({...r})},async({instance:e})=>{try{return c(await s.jsonCall(e,`GetFocused`))}catch(t){return u(t,e)}}),want(`activate_widget`)&&o.registerTool(`activate_widget`,{description:`Activate the widget at a widget path (from dump_tree) — the click-drive counterpart of get_property: a Button emits clicked, an Entry activates, a Toggle toggles; a list/nav/preference row (GtkListBoxRow/AdwActionRow) is clicked on its GtkListBox (selected + row-activated). Returns whether the widget activated.`,inputSchema:n.object({path:n.string(),...r})},async({path:e,instance:n})=>{try{let[r]=(await s.control(n,`ActivateWidget`,t.Variant.new_tuple([strv(e)]),`(b)`)).recursiveUnpack();return c(r?`Activated widget at "${e}".`:`Widget at "${e}" is not activatable (no default activation).`)}catch(e){return u(e,n)}}),want(`dump_gsettings`)&&o.registerTool(`dump_gsettings`,{description:`Dump every key + value of an installed GSettings schema (read-only).`,inputSchema:n.object({schema:n.string(),...r})},async({schema:e,instance:n})=>{try{return c(await s.jsonCall(n,`DumpGSettings`,t.Variant.new_tuple([strv(e)])))}catch(e){return u(e,n)}}),want(`dump_css`)&&o.registerTool(`dump_css`,{description:`List the devtools-installed CSS provider names.`,inputSchema:n.object({...r})},async({instance:e})=>{try{return c(await s.jsonCall(e,`DumpCss`))}catch(t){return u(t,e)}}),want(`swap_css`)&&o.registerTool(`swap_css`,{description:`Live-install or replace a named CSS provider (high priority) — re-theme the running app without a restart. Returns applied.`,inputSchema:n.object({name:n.string(),css:n.string(),...r})},async({name:e,css:n,instance:r})=>{try{let[i]=(await s.control(r,`SwapCss`,t.Variant.new_tuple([strv(e),strv(n)]),`(b)`)).recursiveUnpack();return c(i?`Applied CSS provider "${e}".`:`No display — CSS not applied.`)}catch(e){return u(e,r)}}),want(`list_instances`)&&o.registerTool(`list_instances`,{description:`List reachable devtools-enabled app instances — every default/labelled bus name on the session bus, or (on the bus-less peer transport) the single app this bridge is dialling.`,inputSchema:n.object({})},async()=>{try{return c(JSON.stringify(await s.listInstances(),null,2))}catch(e){return u(e)}})}export{registerGenericTools};
|
package/lib/esm/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{chooseClientTransport as e}from"./transport-choice.js";import{DbusDevtoolsClient as t}from"./dbus-client.js";import{fail as n,image as r,ok as i}from"./tool-result.js";import{dbusError as a}from"./error-map.js";import{registerGenericTools as o}from"./generic-tools.js";import{GjsStdioTransport as s}from"./stdio-transport.js";import{runDevtoolsMcp as c}from"./run-server.js";import{registerStorybookTools as l,storybookProfile as u}from"./profiles/storybook.js";import{browserProfile as d,registerBrowserTools as f}from"./profiles/browser.js";import{cdpProfile as p,registerCdpTools as m}from"./profiles/cdp.js";import{NsCdpClient as h}from"./transports/ns-cdp-client.js";import{nativescriptProfile as g,registerNsGenericTools as _,runNativescriptMcp as v}from"./profiles/nativescript.js";export*from"@gjsify/devtools-protocol";export{t as DbusDevtoolsClient,s as GjsStdioTransport,h as NsCdpClient,d as browserProfile,p as cdpProfile,e as chooseClientTransport,a as dbusError,n as fail,r as image,g as nativescriptProfile,i as ok,f as registerBrowserTools,m as registerCdpTools,o as registerGenericTools,_ as registerNsGenericTools,l as registerStorybookTools,c as runDevtoolsMcp,v as runNativescriptMcp,u as storybookProfile};
|
|
@@ -1 +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)}});let storybookEval=async(e,n)=>{let r=`(function(){var s=window.__storybook;if(!s)return ${JSON.stringify(`(not a storybook page: window.__storybook is undefined)`)};return ${n};})()`,[i]=(await o.control(e,`BrowserEvalJs`,t.Variant.new_tuple([strv(r)]),`(s)`)).recursiveUnpack();return i};a.registerTool(`list_stories`,{description:`Storybook: list the stories on a @gjsify/adwaita-storybook page (title, story, category, controls).`,inputSchema:n.object({...r})},async({instance:e})=>{try{return s(await storybookEval(e,`JSON.stringify(s.listStories())`))}catch(t){return c(t,e)}}),a.registerTool(`open_story`,{description:`Storybook: open a story by its "Category/Name" title; returns the now-current story.`,inputSchema:n.object({title:n.string(),...r})},async({title:e,instance:t})=>{try{return s(await storybookEval(t,`JSON.stringify({opened:s.openStory(${JSON.stringify(e)}),current:s.getCurrentStory()})`))}catch(e){return c(e,t)}}),a.registerTool(`get_current_story`,{description:`Storybook: the active story as {title, story, args}.`,inputSchema:n.object({...r})},async({instance:e})=>{try{return s(await storybookEval(e,`JSON.stringify(s.getCurrentStory())`))}catch(t){return c(t,e)}}),a.registerTool(`set_story_arg`,{description:`Storybook: set one control arg on the active story (drives the live preview); value is string|number|boolean.`,inputSchema:n.object({name:n.string(),value:n.union([n.string(),n.number(),n.boolean()]),...r})},async({name:e,value:t,instance:n})=>{try{return s(await storybookEval(n,`JSON.stringify(s.setArg(${JSON.stringify(e)}, ${JSON.stringify(t)}))`))}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
|
+
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)}});let storybookEval=async(e,n)=>{let r=`(function(){var s=window.__storybook;if(!s)return ${JSON.stringify(`(not a storybook page: window.__storybook is undefined)`)};return ${n};})()`,[i]=(await o.control(e,`BrowserEvalJs`,t.Variant.new_tuple([strv(r)]),`(s)`)).recursiveUnpack();return i};a.registerTool(`list_stories`,{description:`Storybook: list the stories on a @gjsify/adwaita-storybook page (title, story, category, controls).`,inputSchema:n.object({...r})},async({instance:e})=>{try{return s(await storybookEval(e,`JSON.stringify(s.listStories())`))}catch(t){return c(t,e)}}),a.registerTool(`open_story`,{description:`Storybook: open a story by its "Category/Name" title; returns the now-current story.`,inputSchema:n.object({title:n.string(),...r})},async({title:e,instance:t})=>{try{return s(await storybookEval(t,`JSON.stringify({opened:s.openStory(${JSON.stringify(e)}),current:s.getCurrentStory()})`))}catch(e){return c(e,t)}}),a.registerTool(`get_current_story`,{description:`Storybook: the active story as {title, story, args}.`,inputSchema:n.object({...r})},async({instance:e})=>{try{return s(await storybookEval(e,`JSON.stringify(s.getCurrentStory())`))}catch(t){return c(t,e)}}),a.registerTool(`set_story_arg`,{description:`Storybook: set one control arg on the active story (drives the live preview); value is string|number|boolean.`,inputSchema:n.object({name:n.string(),value:n.union([n.string(),n.number(),n.boolean()]),...r})},async({name:e,value:t,instance:n})=>{try{return s(await storybookEval(n,`JSON.stringify(s.setArg(${JSON.stringify(e)}, ${JSON.stringify(t)}))`))}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,t={}){return{name:`gjsify-browser-devtools`,version:`0.10.0`,busNameBase:e,address:t.address,registerTools:registerBrowserTools}}export{browserProfile,registerBrowserTools};
|
package/lib/esm/profiles/cdp.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import"../_virtual/_rolldown/runtime.js";import e from"@girs/glib-2.0";import{z as t}from"zod";const strv=t=>e.Variant.new_string(t),n={instance:t.string().optional().describe(`App instance label; omit for the default app`)},r=new Set([`Runtime.evaluate`,`Runtime.callFunctionOn`,`Runtime.getProperties`,`Runtime.getPreview`,`DOM.getDocument`,`DOM.querySelector`,`DOM.querySelectorAll`,`DOM.getOuterHTML`,`DOM.setAttributeValue`,`CSS.getComputedStyleForNode`,`CSS.getMatchedStylesForNode`,`Debugger.setBreakpoint`,`Debugger.setPauseOnExceptions`]);async function loadDevtoolsCdp(){try{return await import(`@gjsify/devtools-cdp`)}catch(e){let t=e instanceof Error?e.message:String(e);throw Error(`The CDP devtools profile requires the optional peer dependency \`@gjsify/devtools-cdp\` (Tier 3, experimental). Add it to your app to use \`gjsify debug --profile cdp\` / \`cdpProfile\` / \`registerCdpTools\`. (import failed: ${t})`)}}function zodForParam(e,n){if(n&&n.length>0)return t.enum(n);switch(e){case`string`:return t.string();case`number`:return t.number();case`boolean`:return t.boolean();case`array`:return t.array(t.unknown());case`object`:return t.record(t.string(),t.unknown());default:return t.unknown()}}async function cdpSend(t,n,r,i){let[a]=(await t.client.control(n,`CdpSend`,e.Variant.new_tuple([strv(r),strv(JSON.stringify(i))]),`(s)`)).recursiveUnpack();return a}function registerCoreCdpTools(r){let{server:i,client:a,ok:o,dbusError:s}=r;i.registerTool(`cdp_send`,{description:'Send any WebKit Remote Inspector command `Domain.command` (e.g. "Page.reload", "Network.enable") with optional params, and return its result JSON. The universal fallback for commands without a curated tool. Requires a prior cdp_connect.',inputSchema:t.object({method:t.string().describe(`Protocol method, e.g. "Runtime.evaluate".`),params:t.record(t.string(),t.unknown()).optional(),...n})},async({method:e,params:t,instance:n})=>{try{return o(await cdpSend(r,n,e,t??{}))}catch(e){return s(e,n)}}),i.registerTool(`cdp_discover_targets`,{description:`List the inspectable targets (web-page / service-worker / …) on the inspector server.`,inputSchema:t.object({...n})},async({instance:e})=>{try{return o(await a.jsonCall(e,`CdpDiscoverTargets`))}catch(t){return s(t,e)}}),i.registerTool(`cdp_connect`,{description:`Connect to an inspector target (defaults to the first web-page) and auto-enable the Inspector / Runtime / DOM / Console domains. Call before cdp_send / the curated tools.`,inputSchema:t.object({target:t.record(t.string(),t.unknown()).optional().describe(`Optional target selector, e.g. {targetId} or {wsUrl}; omit for the first web-page.`),...n})},async({target:t,instance:n})=>{try{let[i]=(await a.control(n,`CdpConnect`,e.Variant.new_tuple([strv(t?JSON.stringify(t):``)]),`(b)`)).recursiveUnpack();return i?o(`Connected to inspector target.`):r.fail(`No inspectable target found.`)}catch(e){return s(e,n)}}),i.registerTool(`cdp_drain_events`,{description:`Return + clear the buffered protocol events (console messages, DOM mutations, …).`,inputSchema:t.object({...n})},async({instance:e})=>{try{return o(await a.jsonCall(e,`CdpDrainEvents`))}catch(t){return s(t,e)}})}async function registerCuratedCdpTools(e){let{server:i,ok:a,dbusError:o}=e,{PROTOCOL_SPEC:s,generateCdpTools:c}=await loadDevtoolsCdp(),l=c(s,{include:(e,t)=>r.has(`${e}.${t}`)});for(let r of l){let s={};for(let e of r.parameters){let t=zodForParam(e.jsType,e.enum),n=e.description?t.describe(e.description):t;s[e.name]=e.optional?n.optional():n}i.registerTool(r.name,{description:r.description??r.method,inputSchema:t.object({...s,...n})},async t=>{let n=t.instance;try{let i={};for(let e of r.parameters)t[e.name]!==void 0&&(i[e.name]=t[e.name]);return a(await cdpSend(e,n,r.method,i))}catch(e){return o(e,n)}})}}async function registerCdpTools(e){registerCoreCdpTools(e),await registerCuratedCdpTools(e)}function cdpProfile(e){return{name:`gjsify-cdp-devtools`,version:`0.11.0`,busNameBase:e,registerTools:registerCdpTools}}export{cdpProfile,registerCdpTools};
|
|
1
|
+
import"../_virtual/_rolldown/runtime.js";import e from"@girs/glib-2.0";import{z as t}from"zod";const strv=t=>e.Variant.new_string(t),n={instance:t.string().optional().describe(`App instance label; omit for the default app`)},r=new Set([`Runtime.evaluate`,`Runtime.callFunctionOn`,`Runtime.getProperties`,`Runtime.getPreview`,`DOM.getDocument`,`DOM.querySelector`,`DOM.querySelectorAll`,`DOM.getOuterHTML`,`DOM.setAttributeValue`,`CSS.getComputedStyleForNode`,`CSS.getMatchedStylesForNode`,`Debugger.setBreakpoint`,`Debugger.setPauseOnExceptions`]);async function loadDevtoolsCdp(){try{return await import(`@gjsify/devtools-cdp`)}catch(e){let t=e instanceof Error?e.message:String(e);throw Error(`The CDP devtools profile requires the optional peer dependency \`@gjsify/devtools-cdp\` (Tier 3, experimental). Add it to your app to use \`gjsify debug --profile cdp\` / \`cdpProfile\` / \`registerCdpTools\`. (import failed: ${t})`)}}function zodForParam(e,n){if(n&&n.length>0)return t.enum(n);switch(e){case`string`:return t.string();case`number`:return t.number();case`boolean`:return t.boolean();case`array`:return t.array(t.unknown());case`object`:return t.record(t.string(),t.unknown());default:return t.unknown()}}async function cdpSend(t,n,r,i){let[a]=(await t.client.control(n,`CdpSend`,e.Variant.new_tuple([strv(r),strv(JSON.stringify(i))]),`(s)`)).recursiveUnpack();return a}function registerCoreCdpTools(r){let{server:i,client:a,ok:o,dbusError:s}=r;i.registerTool(`cdp_send`,{description:'Send any WebKit Remote Inspector command `Domain.command` (e.g. "Page.reload", "Network.enable") with optional params, and return its result JSON. The universal fallback for commands without a curated tool. Requires a prior cdp_connect.',inputSchema:t.object({method:t.string().describe(`Protocol method, e.g. "Runtime.evaluate".`),params:t.record(t.string(),t.unknown()).optional(),...n})},async({method:e,params:t,instance:n})=>{try{return o(await cdpSend(r,n,e,t??{}))}catch(e){return s(e,n)}}),i.registerTool(`cdp_discover_targets`,{description:`List the inspectable targets (web-page / service-worker / …) on the inspector server.`,inputSchema:t.object({...n})},async({instance:e})=>{try{return o(await a.jsonCall(e,`CdpDiscoverTargets`))}catch(t){return s(t,e)}}),i.registerTool(`cdp_connect`,{description:`Connect to an inspector target (defaults to the first web-page) and auto-enable the Inspector / Runtime / DOM / Console domains. Call before cdp_send / the curated tools.`,inputSchema:t.object({target:t.record(t.string(),t.unknown()).optional().describe(`Optional target selector, e.g. {targetId} or {wsUrl}; omit for the first web-page.`),...n})},async({target:t,instance:n})=>{try{let[i]=(await a.control(n,`CdpConnect`,e.Variant.new_tuple([strv(t?JSON.stringify(t):``)]),`(b)`)).recursiveUnpack();return i?o(`Connected to inspector target.`):r.fail(`No inspectable target found.`)}catch(e){return s(e,n)}}),i.registerTool(`cdp_drain_events`,{description:`Return + clear the buffered protocol events (console messages, DOM mutations, …).`,inputSchema:t.object({...n})},async({instance:e})=>{try{return o(await a.jsonCall(e,`CdpDrainEvents`))}catch(t){return s(t,e)}})}async function registerCuratedCdpTools(e){let{server:i,ok:a,dbusError:o}=e,{PROTOCOL_SPEC:s,generateCdpTools:c}=await loadDevtoolsCdp(),l=c(s,{include:(e,t)=>r.has(`${e}.${t}`)});for(let r of l){let s={};for(let e of r.parameters){let t=zodForParam(e.jsType,e.enum),n=e.description?t.describe(e.description):t;s[e.name]=e.optional?n.optional():n}i.registerTool(r.name,{description:r.description??r.method,inputSchema:t.object({...s,...n})},async t=>{let n=t.instance;try{let i={};for(let e of r.parameters)t[e.name]!==void 0&&(i[e.name]=t[e.name]);return a(await cdpSend(e,n,r.method,i))}catch(e){return o(e,n)}})}}async function registerCdpTools(e){registerCoreCdpTools(e),await registerCuratedCdpTools(e)}function cdpProfile(e,t={}){return{name:`gjsify-cdp-devtools`,version:`0.11.0`,busNameBase:e,address:t.address,registerTools:registerCdpTools}}export{cdpProfile,registerCdpTools};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import"../_virtual/_rolldown/runtime.js";import e from"@girs/glib-2.0";import{z as t}from"zod";const strv=t=>e.Variant.new_string(t),n={instance:t.string().optional().describe(`Storybook instance label; omit for the default app`)};function registerStorybookTools(r){let{server:i,client:a,ok:o,dbusError:s}=r;i.registerTool(`list_stories`,{description:`List every registered story: title, story variant, category, and controls.`,inputSchema:t.object({...n})},async({instance:e})=>{try{return o(await a.jsonCall(e,`ListStories`))}catch(t){return s(t,e)}}),i.registerTool(`get_current_story`,{description:`The currently-displayed story (title, variant, live args), or null.`,inputSchema:t.object({...n})},async({instance:e})=>{try{return o(await a.jsonCall(e,`GetCurrentStory`))}catch(t){return s(t,e)}}),i.registerTool(`open_story`,{description:`Open a story by its full "Category/Name" title (updates the sidebar selection).`,inputSchema:t.object({title:t.string(),...n})},async({title:t,instance:n})=>{try{let[r]=(await a.control(n,`OpenStory`,e.Variant.new_tuple([strv(t)]),`(b)`)).recursiveUnpack();return o(r?`Opened "${t}".`:`No story titled "${t}".`)}catch(e){return s(e,n)}}),i.registerTool(`set_story_arg`,{description:`Set one arg on the active story (drives the same path as the control panel; the row refreshes).`,inputSchema:t.object({name:t.string(),value:t.union([t.string(),t.number(),t.boolean(),t.null()]),...n})},async({name:t,value:n,instance:r})=>{try{let[i]=(await a.control(r,`SetStoryArg`,e.Variant.new_tuple([strv(t),strv(JSON.stringify(n))]),`(b)`)).recursiveUnpack();return o(i?`Set ${t} = ${JSON.stringify(n)}.`:`No active story to set an arg on.`)}catch(e){return s(e,r)}})}function storybookProfile(e){return{name:`gjsify-storybook-devtools`,version:`0.8.0`,busNameBase:e,registerTools:registerStorybookTools}}export{registerStorybookTools,storybookProfile};
|
|
1
|
+
import"../_virtual/_rolldown/runtime.js";import e from"@girs/glib-2.0";import{z as t}from"zod";const strv=t=>e.Variant.new_string(t),n={instance:t.string().optional().describe(`Storybook instance label; omit for the default app`)};function registerStorybookTools(r){let{server:i,client:a,ok:o,dbusError:s}=r;i.registerTool(`list_stories`,{description:`List every registered story: title, story variant, category, and controls.`,inputSchema:t.object({...n})},async({instance:e})=>{try{return o(await a.jsonCall(e,`ListStories`))}catch(t){return s(t,e)}}),i.registerTool(`get_current_story`,{description:`The currently-displayed story (title, variant, live args), or null.`,inputSchema:t.object({...n})},async({instance:e})=>{try{return o(await a.jsonCall(e,`GetCurrentStory`))}catch(t){return s(t,e)}}),i.registerTool(`open_story`,{description:`Open a story by its full "Category/Name" title (updates the sidebar selection).`,inputSchema:t.object({title:t.string(),...n})},async({title:t,instance:n})=>{try{let[r]=(await a.control(n,`OpenStory`,e.Variant.new_tuple([strv(t)]),`(b)`)).recursiveUnpack();return o(r?`Opened "${t}".`:`No story titled "${t}".`)}catch(e){return s(e,n)}}),i.registerTool(`set_story_arg`,{description:`Set one arg on the active story (drives the same path as the control panel; the row refreshes).`,inputSchema:t.object({name:t.string(),value:t.union([t.string(),t.number(),t.boolean(),t.null()]),...n})},async({name:t,value:n,instance:r})=>{try{let[i]=(await a.control(r,`SetStoryArg`,e.Variant.new_tuple([strv(t),strv(JSON.stringify(n))]),`(b)`)).recursiveUnpack();return o(i?`Set ${t} = ${JSON.stringify(n)}.`:`No active story to set an arg on.`)}catch(e){return s(e,r)}})}function storybookProfile(e,t={}){return{name:`gjsify-storybook-devtools`,version:`0.8.0`,busNameBase:e,address:t.address,registerTools:registerStorybookTools}}export{registerStorybookTools,storybookProfile};
|
package/lib/esm/run-server.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import"./_virtual/_rolldown/runtime.js";import{DbusDevtoolsClient as e}from"./dbus-client.js";import{fail as t,ok as n}from"./tool-result.js";import{dbusError as r}from"./error-map.js";import{registerGenericTools as i}from"./generic-tools.js";import{GjsStdioTransport as a}from"./stdio-transport.js";import o from"@girs/glib-2.0";import{McpServer as s}from"@modelcontextprotocol/sdk/server/mcp.js";async function runDevtoolsMcp(c){let l=new e(c.busNameBase),u=new s({name:c.name,version:c.version}),d={server:u,client:l,ok:n,fail:t,dbusError:(e,t)=>r(e,l.resolve(t))};i(d,c.genericTools??`all`),Promise.resolve(c.registerTools?.(d)).catch(e=>console.error(`[gjsify-devtools-mcp] tool registration failed:`,e));let f=o.MainLoop.new(null,!1),p=new a(()=>f.quit());u.connect(p).then(()=>console.error(`[gjsify-devtools-mcp] ${c.name} on stdio (DBus base: ${c.busNameBase})`),e=>{console.error(`[gjsify-devtools-mcp] failed to connect:`,e),f.quit()}),f.run()}export{runDevtoolsMcp};
|
|
1
|
+
import"./_virtual/_rolldown/runtime.js";import{DbusDevtoolsClient as e}from"./dbus-client.js";import{fail as t,ok as n}from"./tool-result.js";import{dbusError as r}from"./error-map.js";import{registerGenericTools as i}from"./generic-tools.js";import{GjsStdioTransport as a}from"./stdio-transport.js";import o from"@girs/glib-2.0";import{McpServer as s}from"@modelcontextprotocol/sdk/server/mcp.js";async function runDevtoolsMcp(c){let l=new e(c.busNameBase,{address:c.address}),u=new s({name:c.name,version:c.version}),d={server:u,client:l,ok:n,fail:t,dbusError:(e,t)=>r(e,l.resolve(t),l.describeTarget(t))};i(d,c.genericTools??`all`),Promise.resolve(c.registerTools?.(d)).catch(e=>console.error(`[gjsify-devtools-mcp] tool registration failed:`,e));let f=o.MainLoop.new(null,!1),p=new a(()=>f.quit());u.connect(p).then(()=>console.error(`[gjsify-devtools-mcp] ${c.name} on stdio (DBus base: ${c.busNameBase}, via ${l.describeTarget()})`),e=>{console.error(`[gjsify-devtools-mcp] failed to connect:`,e),f.quit()}),f.run()}export{runDevtoolsMcp};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import"./_virtual/_rolldown/runtime.js";function chooseClientTransport(e){return e.optionAddress?{kind:`peer`,address:e.optionAddress,source:`option`}:e.envAddress?{kind:`peer`,address:e.envAddress,source:`env`}:e.addressFileValue?{kind:`peer`,address:e.addressFileValue,source:`address-file`}:e.sessionBusAvailable?{kind:`session-bus`}:{kind:`unavailable`}}export{chooseClientTransport};
|
|
@@ -1,24 +1,106 @@
|
|
|
1
|
+
import Gio from '@girs/gio-2.0';
|
|
1
2
|
import GLib from '@girs/glib-2.0';
|
|
2
3
|
import { type BusAddress } from '@gjsify/devtools-protocol';
|
|
4
|
+
import { type ClientTransportChoice } from './transport-choice.js';
|
|
5
|
+
/** Options for {@link DbusDevtoolsClient}. */
|
|
6
|
+
export interface DbusDevtoolsClientOptions {
|
|
7
|
+
/**
|
|
8
|
+
* Peer address to dial (`unix:path=…`, `nonce-tcp:host=…,port=…,noncefile=…`).
|
|
9
|
+
* Overrides everything; default is the `GJSIFY_DEVTOOLS_ADDRESS` env var,
|
|
10
|
+
* then the address file the app publishes, then the session bus.
|
|
11
|
+
*/
|
|
12
|
+
address?: string;
|
|
13
|
+
/** Instance label used to locate the published address file. Default: `GJSIFY_DEVTOOLS_INSTANCE`. */
|
|
14
|
+
instance?: string;
|
|
15
|
+
}
|
|
16
|
+
/** A devtools instance reachable from this client. */
|
|
17
|
+
export interface DevtoolsInstanceRef {
|
|
18
|
+
instance: string;
|
|
19
|
+
/** Present on the session bus; a peer connection has no bus name. */
|
|
20
|
+
busName?: string;
|
|
21
|
+
/** Present in peer mode: the address this client is connected to. */
|
|
22
|
+
address?: string;
|
|
23
|
+
}
|
|
24
|
+
/** Everything {@link connectToDevtools} needs, explicit so the precedence stays inspectable. */
|
|
25
|
+
export interface ConnectContext {
|
|
26
|
+
busNameBase: string;
|
|
27
|
+
instance?: string;
|
|
28
|
+
/** Where an address for this app id WOULD be published — also what gets deleted when it is stale. */
|
|
29
|
+
addressFilePath: string;
|
|
30
|
+
optionAddress?: string | null;
|
|
31
|
+
envAddress: string | null;
|
|
32
|
+
addressFileValue: string | null;
|
|
33
|
+
sessionBusAvailable: boolean;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The two I/O steps of the resolution, overridable ONLY so the specs can drive
|
|
37
|
+
* every row of the precedence — including "stale file falls back to the session
|
|
38
|
+
* bus" — on a host with no bus daemon, which is what CI and macOS are. The
|
|
39
|
+
* defaults are the real GDBus calls.
|
|
40
|
+
*/
|
|
41
|
+
export interface ConnectDeps {
|
|
42
|
+
dial?: (address: string) => Gio.DBusConnection;
|
|
43
|
+
sessionBus?: () => Gio.DBusConnection;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Resolve the precedence into a LIVE connection — dial-then-fallback, not
|
|
47
|
+
* dial-and-hope.
|
|
48
|
+
*
|
|
49
|
+
* The published address file outranks the session bus because it is meant to be
|
|
50
|
+
* positive evidence that an app of exactly this id is listening NOW. Nothing made
|
|
51
|
+
* that true: it is removed on `unexport`/`GApplication::shutdown`, and GApplication
|
|
52
|
+
* installs a handler for SIGTERM only, so Ctrl-C, SIGKILL and a crash all leave it
|
|
53
|
+
* behind — and `Gio.DBusConnection.new_for_address_sync` on a socket that is gone
|
|
54
|
+
* throws `G_IO_ERROR_NOT_FOUND` (measured, gjs 1.88.1) out of the bridge's FIRST
|
|
55
|
+
* statement. Ranking a claim above the bus is only honest if the claim is CHECKED:
|
|
56
|
+
* dial it, and on failure delete the file and continue down the precedence. A stale
|
|
57
|
+
* file then degrades to "the session bus, as on any Linux desktop" or to the
|
|
58
|
+
* three-ways-in diagnostic, never to a raw localised GIO error.
|
|
59
|
+
*
|
|
60
|
+
* An EXPLICIT address (`--address`, `GJSIFY_DEVTOOLS_ADDRESS`) deliberately does
|
|
61
|
+
* NOT fall back: the operator named it on both sides, so quietly talking to
|
|
62
|
+
* something else would hide the typo instead of reporting it. It fails with a
|
|
63
|
+
* message naming that address and what to do — not with GIO's.
|
|
64
|
+
*/
|
|
65
|
+
export declare function connectToDevtools(ctx: ConnectContext, deps?: ConnectDeps): {
|
|
66
|
+
bus: Gio.DBusConnection;
|
|
67
|
+
transport: ClientTransportChoice;
|
|
68
|
+
};
|
|
3
69
|
/**
|
|
4
|
-
* Talks the `org.gjsify.Devtools` control plane of a running app
|
|
5
|
-
*
|
|
6
|
-
*
|
|
70
|
+
* Talks the `org.gjsify.Devtools` control plane of a running app. One per bridge
|
|
71
|
+
* process; resolves a per-instance bus address from the app's base id + an
|
|
72
|
+
* optional instance label, and dials either the session bus or a peer socket.
|
|
7
73
|
*/
|
|
8
74
|
export declare class DbusDevtoolsClient {
|
|
9
75
|
readonly busNameBase: string;
|
|
10
76
|
private readonly _bus;
|
|
11
|
-
|
|
77
|
+
/** The resolved transport — `peer` means calls carry no destination bus name. */
|
|
78
|
+
readonly transport: ClientTransportChoice;
|
|
79
|
+
constructor(busNameBase: string, options?: DbusDevtoolsClientOptions);
|
|
80
|
+
/** Human-readable description of what this client is talking to (for diagnostics). */
|
|
81
|
+
describeTarget(label?: string): string;
|
|
12
82
|
resolve(label?: string): BusAddress;
|
|
83
|
+
/**
|
|
84
|
+
* The destination for a call. A peer connection has no bus daemon and no
|
|
85
|
+
* name registry, so the message must carry NO destination — sending the
|
|
86
|
+
* well-known name there gets `ServiceUnknown` from a peer that has never
|
|
87
|
+
* heard of names.
|
|
88
|
+
*/
|
|
89
|
+
private _destination;
|
|
13
90
|
/** Async call to an instance's Devtools interface; resolves with the reply variant. */
|
|
14
91
|
control(label: string | undefined, method: string, params: GLib.Variant | null, replyType: string | null): Promise<GLib.Variant>;
|
|
15
92
|
/** Call a `() -> (s)` method and return its JSON string (pretty-printed). */
|
|
16
93
|
jsonCall(label: string | undefined, method: string, params?: GLib.Variant | null): Promise<string>;
|
|
17
|
-
/**
|
|
94
|
+
/**
|
|
95
|
+
* Whether `busName` currently has an owner (the app is running). In peer
|
|
96
|
+
* mode there is no name registry: being connected IS the answer, because the
|
|
97
|
+
* socket belongs to exactly one app process.
|
|
98
|
+
*/
|
|
18
99
|
nameHasOwner(busName: string): Promise<boolean>;
|
|
19
|
-
/**
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
100
|
+
/**
|
|
101
|
+
* Enumerate devtools-enabled instances (base + labelled). Enumeration is a
|
|
102
|
+
* BUS feature; a peer address reaches exactly one app, so peer mode reports
|
|
103
|
+
* that one rather than pretending to have searched.
|
|
104
|
+
*/
|
|
105
|
+
listInstances(): Promise<DevtoolsInstanceRef[]>;
|
|
24
106
|
}
|
package/lib/types/error-map.d.ts
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { type ToolResult } from './tool-result.js';
|
|
2
2
|
/**
|
|
3
3
|
* Map a DBus call failure to a clear MCP tool result. Distinguishes:
|
|
4
|
-
* (1) the app isn't
|
|
4
|
+
* (1) the app isn't reachable, (2) a typed devtools rejection the
|
|
5
5
|
* app threw — its `<code>: message` survives as a GDBus remote error (see
|
|
6
6
|
* `formatDbusErrorMessage`), so the original, actionable message is surfaced,
|
|
7
7
|
* (3) any other DBus failure.
|
|
8
|
+
*
|
|
9
|
+
* `target` names WHERE the bridge was dialling (`client.describeTarget()`).
|
|
10
|
+
* Without it the not-running message said "on the session bus" unconditionally,
|
|
11
|
+
* which is wrong — and confusing — in peer mode, where there is no bus at all.
|
|
8
12
|
*/
|
|
9
13
|
export declare function dbusError(error: unknown, resolved: {
|
|
10
14
|
busName: string;
|
|
11
15
|
instance: string;
|
|
12
|
-
}): ToolResult;
|
|
16
|
+
}, target?: string): ToolResult;
|
package/lib/types/index.d.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
export { runDevtoolsMcp } from './run-server.js';
|
|
2
2
|
export { DbusDevtoolsClient } from './dbus-client.js';
|
|
3
|
+
export type { DbusDevtoolsClientOptions, DevtoolsInstanceRef } from './dbus-client.js';
|
|
4
|
+
export { chooseClientTransport } from './transport-choice.js';
|
|
5
|
+
export type { ClientTransportChoice } from './transport-choice.js';
|
|
3
6
|
export { registerGenericTools } from './generic-tools.js';
|
|
4
7
|
export { dbusError } from './error-map.js';
|
|
5
8
|
export { fail, image, ok } from './tool-result.js';
|
|
6
9
|
export type { ToolResult } from './tool-result.js';
|
|
7
10
|
export { GjsStdioTransport } from './stdio-transport.js';
|
|
8
|
-
export type { DevtoolsToolProfile, GenericToolName, McpToolContext } from './profile.js';
|
|
11
|
+
export type { DevtoolsProfileOptions, DevtoolsToolProfile, GenericToolName, McpToolContext } from './profile.js';
|
|
9
12
|
export { registerStorybookTools, storybookProfile } from './profiles/storybook.js';
|
|
10
13
|
export { registerBrowserTools, browserProfile } from './profiles/browser.js';
|
|
11
14
|
export { registerCdpTools, cdpProfile } from './profiles/cdp.js';
|
package/lib/types/profile.d.ts
CHANGED
|
@@ -11,6 +11,11 @@ export interface McpToolContext {
|
|
|
11
11
|
readonly fail: (text: string) => ToolResult;
|
|
12
12
|
readonly dbusError: (error: unknown, instance?: string) => ToolResult;
|
|
13
13
|
}
|
|
14
|
+
/** Extra knobs every built-in profile factory accepts. */
|
|
15
|
+
export interface DevtoolsProfileOptions {
|
|
16
|
+
/** Peer address to dial instead of the session bus. See {@link DevtoolsToolProfile.address}. */
|
|
17
|
+
readonly address?: string;
|
|
18
|
+
}
|
|
14
19
|
/** Describes an MCP bridge for a specific app (or the generic default). */
|
|
15
20
|
export interface DevtoolsToolProfile {
|
|
16
21
|
/** MCP server name. */
|
|
@@ -19,6 +24,13 @@ export interface DevtoolsToolProfile {
|
|
|
19
24
|
readonly version: string;
|
|
20
25
|
/** The app's base DBus name, e.g. `"org.example.App"`. */
|
|
21
26
|
readonly busNameBase: string;
|
|
27
|
+
/**
|
|
28
|
+
* Peer address to dial instead of the session bus (`unix:path=…`,
|
|
29
|
+
* `nonce-tcp:host=…,port=…,noncefile=…`). Omit for the normal precedence:
|
|
30
|
+
* `GJSIFY_DEVTOOLS_ADDRESS` → the address file the app publishes → the
|
|
31
|
+
* session bus (`chooseClientTransport`).
|
|
32
|
+
*/
|
|
33
|
+
readonly address?: string;
|
|
22
34
|
/** Which generic tools to register. Default: `'all'`. */
|
|
23
35
|
readonly genericTools?: GenericToolName[] | 'all';
|
|
24
36
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DevtoolsToolProfile, McpToolContext } from '../profile.js';
|
|
1
|
+
import type { DevtoolsProfileOptions, DevtoolsToolProfile, McpToolContext } from '../profile.js';
|
|
2
2
|
export declare function registerBrowserTools(ctx: McpToolContext): void;
|
|
3
3
|
/** Tool profile for a @gjsify/devtools-browser app. */
|
|
4
|
-
export declare function browserProfile(busNameBase: string): DevtoolsToolProfile;
|
|
4
|
+
export declare function browserProfile(busNameBase: string, options?: DevtoolsProfileOptions): DevtoolsToolProfile;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DevtoolsToolProfile, McpToolContext } from '../profile.js';
|
|
1
|
+
import type { DevtoolsProfileOptions, DevtoolsToolProfile, McpToolContext } from '../profile.js';
|
|
2
2
|
/**
|
|
3
3
|
* Register all CDP tools: the sync transport tools first (so they exist even if
|
|
4
4
|
* the optional peer is missing), then the curated typed tools which lazily load
|
|
@@ -8,4 +8,4 @@ import type { DevtoolsToolProfile, McpToolContext } from '../profile.js';
|
|
|
8
8
|
*/
|
|
9
9
|
export declare function registerCdpTools(ctx: McpToolContext): Promise<void>;
|
|
10
10
|
/** Tool profile for a @gjsify/devtools-cdp-enabled app. */
|
|
11
|
-
export declare function cdpProfile(busNameBase: string): DevtoolsToolProfile;
|
|
11
|
+
export declare function cdpProfile(busNameBase: string, options?: DevtoolsProfileOptions): DevtoolsToolProfile;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { DevtoolsToolProfile, McpToolContext } from '../profile.js';
|
|
1
|
+
import type { DevtoolsProfileOptions, DevtoolsToolProfile, McpToolContext } from '../profile.js';
|
|
2
2
|
/** Register the storybook-specific tools (list/open stories, read/set args). */
|
|
3
3
|
export declare function registerStorybookTools(ctx: McpToolContext): void;
|
|
4
4
|
/** A ready-made profile for a storybook app: generic tools + the story tools. */
|
|
5
|
-
export declare function storybookProfile(busNameBase: string): DevtoolsToolProfile;
|
|
5
|
+
export declare function storybookProfile(busNameBase: string, options?: DevtoolsProfileOptions): DevtoolsToolProfile;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** The transport the bridge uses to reach `org.gjsify.Devtools`. */
|
|
2
|
+
export type ClientTransportChoice =
|
|
3
|
+
/** A `Gio.DBusServer` peer address (no bus daemon, no bus name). */
|
|
4
|
+
{
|
|
5
|
+
kind: 'peer';
|
|
6
|
+
address: string;
|
|
7
|
+
source: 'option' | 'env' | 'address-file';
|
|
8
|
+
}
|
|
9
|
+
/** The session bus, addressed by the app's well-known bus name. */
|
|
10
|
+
| {
|
|
11
|
+
kind: 'session-bus';
|
|
12
|
+
}
|
|
13
|
+
/** Nothing to dial — the caller must explain all three ways in. */
|
|
14
|
+
| {
|
|
15
|
+
kind: 'unavailable';
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* The bridge-side PRECEDENCE, mirroring `installDevtools`' app-side one:
|
|
19
|
+
*
|
|
20
|
+
* | option address | `GJSIFY_DEVTOOLS_ADDRESS` | published address file | session bus | choice |
|
|
21
|
+
* |---|---|---|---|---|
|
|
22
|
+
* | set | any | any | any | `peer` (option) |
|
|
23
|
+
* | unset | set | any | any | `peer` (env) |
|
|
24
|
+
* | unset | unset | present | any | `peer` (address file) |
|
|
25
|
+
* | unset | unset | absent | yes | `session-bus` — **Linux is byte-unchanged** |
|
|
26
|
+
* | unset | unset | absent | no | `unavailable` |
|
|
27
|
+
*
|
|
28
|
+
* The published address FILE deliberately outranks the session bus. It is
|
|
29
|
+
* POSITIVE evidence — it exists only while an app of exactly this id runs on the
|
|
30
|
+
* peer transport, and the app deletes it on shutdown — whereas "a session bus
|
|
31
|
+
* exists" says nothing about the app. With the file ranked below the bus, an app
|
|
32
|
+
* launched with an explicit `GJSIFY_DEVTOOLS_ADDRESS` on a Linux desktop would
|
|
33
|
+
* have been reported as "no devtools-enabled app on the session bus", which is a
|
|
34
|
+
* misleading answer to a question the file already answers correctly.
|
|
35
|
+
*
|
|
36
|
+
* Linux stays unchanged in the default case regardless: with nothing in peer
|
|
37
|
+
* mode no address file is ever written, so that row is a stat() that misses.
|
|
38
|
+
*/
|
|
39
|
+
export declare function chooseClientTransport(input: {
|
|
40
|
+
optionAddress?: string | null;
|
|
41
|
+
envAddress?: string | null;
|
|
42
|
+
addressFileValue?: string | null;
|
|
43
|
+
sessionBusAvailable: boolean;
|
|
44
|
+
}): ClientTransportChoice;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/devtools-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.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",
|
|
@@ -39,12 +39,12 @@
|
|
|
39
39
|
"@girs/gjs": "^4.1.0",
|
|
40
40
|
"@girs/glib-2.0": "^4.1.0",
|
|
41
41
|
"@girs/soup-3.0": "^4.1.0",
|
|
42
|
-
"@gjsify/devtools-protocol": "^0.
|
|
42
|
+
"@gjsify/devtools-protocol": "^0.29.0",
|
|
43
43
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
44
44
|
"zod": "^4.4.3"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
|
-
"@gjsify/devtools-cdp": "^0.
|
|
47
|
+
"@gjsify/devtools-cdp": "^0.29.0"
|
|
48
48
|
},
|
|
49
49
|
"peerDependenciesMeta": {
|
|
50
50
|
"@gjsify/devtools-cdp": {
|
|
@@ -52,9 +52,9 @@
|
|
|
52
52
|
}
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
|
-
"@gjsify/cli": "^0.
|
|
56
|
-
"@gjsify/devtools-cdp": "^0.
|
|
57
|
-
"@gjsify/unit": "^0.
|
|
55
|
+
"@gjsify/cli": "^0.29.0",
|
|
56
|
+
"@gjsify/devtools-cdp": "^0.29.0",
|
|
57
|
+
"@gjsify/unit": "^0.29.0",
|
|
58
58
|
"@types/node": "^25.9.2",
|
|
59
59
|
"typescript": "^6.0.3"
|
|
60
60
|
},
|