@gjsify/devtools-mcp 0.41.0 → 0.43.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.
@@ -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 via the GTK GSK pipeline. `scope` chooses WHAT is captured: the active window by default, or a single widget inside it by path. Needs a visible window (auto-raises + retries once when the capture comes back empty).",inputSchema:n.object({scope:n.string().optional().describe('What to capture: omitted, "window" or "active" for the active window, or a `toplevel:N/child:M` path from dump_tree for one widget. An explicit path matching no live widget is an error, not a silent whole-window shot.'),...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};
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 via the GTK GSK pipeline. `scope` chooses WHAT is captured: the active window by default, or a single widget inside it by path. Needs a visible window (auto-raises + retries once when the capture comes back empty).",inputSchema:n.object({scope:n.string().optional().describe('What to capture: omitted, "window" or "active" for the active window, or a `toplevel:N/child:M` path from dump_tree for one widget. An explicit path matching no live widget is an error, not a silent whole-window shot.'),...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(`find_widget`)&&o.registerTool(`find_widget`,{description:"Find the first widget matching a selector and return its widget path, searched depth-first from the active window. Selector is a GType name, a `:css-class`, or both (`GtkButton:suggested-action`). Invisible and unmapped subtrees are skipped, so a hit is something a user can actually reach. Saves dumping the whole tree to hunt for a path — and unlike a path written down earlier, a selector survives a widget being inserted above the target. Returns an empty result when nothing matches.",inputSchema:n.object({selector:n.string(),...r})},async({selector:e,instance:n})=>{try{let[r]=(await s.control(n,`FindWidget`,t.Variant.new_tuple([strv(e)]),`(s)`)).recursiveUnpack();return c(r||`No visible widget matches "${e}".`)}catch(e){return u(e,n)}}),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(`send_key`)&&o.registerTool(`send_key`,{description:`Deliver a key to a widget's key controllers — the half of headless GUI driving that activate_widget does not cover. Accelerator syntax is GTK's own: "Delete", "Left", "<primary>s", "<shift>Up". Omit path to target whatever has the keyboard focus. Proves the HANDLER runs and what it changes, not that GDK would route a real key press there — for that, also read get_property(path, "focusable"), since an unfocusable widget swallows every key silently.`,inputSchema:n.object({accelerator:n.string(),path:n.string().optional(),...r})},async({accelerator:e,path:n,instance:r})=>{try{let[i]=(await s.control(r,`SendKey`,t.Variant.new_tuple([strv(e),strv(n??``)]),`(b)`)).recursiveUnpack();return c(i?`Sent "${e}" to ${n||`the focused widget`}.`:`${n||`The focused widget`} has no key controller — "${e}" went nowhere.`)}catch(e){return u(e,r)}}),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};
@@ -2,7 +2,7 @@ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import type { DbusDevtoolsClient } from './dbus-client.js';
3
3
  import type { ToolResult } from './tool-result.js';
4
4
  /** The generic per-app tool names the bridge can register. */
5
- export type GenericToolName = 'get_status' | 'screenshot' | 'list_actions' | 'activate_action' | 'change_action_state' | 'present_window' | 'resize_window' | 'list_instances' | 'list_toplevels' | 'dump_tree' | 'get_property' | 'get_focused' | 'activate_widget' | 'dump_gsettings' | 'dump_css' | 'swap_css';
5
+ export type GenericToolName = 'get_status' | 'screenshot' | 'list_actions' | 'activate_action' | 'change_action_state' | 'present_window' | 'resize_window' | 'list_instances' | 'list_toplevels' | 'dump_tree' | 'get_property' | 'get_focused' | 'find_widget' | 'activate_widget' | 'send_key' | 'dump_gsettings' | 'dump_css' | 'swap_css';
6
6
  /** Context handed to the generic tool registrations and to a profile's `registerTools`. */
7
7
  export interface McpToolContext {
8
8
  readonly server: McpServer;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/devtools-mcp",
3
- "version": "0.41.0",
3
+ "version": "0.43.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.41.0",
42
+ "@gjsify/devtools-protocol": "^0.43.0",
43
43
  "@modelcontextprotocol/sdk": "^1.29.0",
44
44
  "zod": "^4.4.3"
45
45
  },
46
46
  "peerDependencies": {
47
- "@gjsify/devtools-cdp": "^0.41.0"
47
+ "@gjsify/devtools-cdp": "^0.43.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.41.0",
56
- "@gjsify/devtools-cdp": "^0.41.0",
57
- "@gjsify/unit": "^0.41.0",
55
+ "@gjsify/cli": "^0.43.0",
56
+ "@gjsify/devtools-cdp": "^0.43.0",
57
+ "@gjsify/unit": "^0.43.0",
58
58
  "@types/node": "^25.9.2",
59
59
  "typescript": "^6.0.3"
60
60
  },