@gjsify/mcp 0.59.0 → 0.60.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 CHANGED
@@ -17,6 +17,7 @@ await serveStdio(server, 'demo');
17
17
  | Export | Purpose |
18
18
  |---|---|
19
19
  | `applyReadOnlyGate(server, allowWrite)` | Default-deny: without `allowWrite`, only tools with `annotations.readOnlyHint === true` are registered. A forgotten annotation drops the tool (visible), it never exposes a mutation (silent). Call it before registering tools. |
20
+ | `applyGrantGate(server, grants)` + `requiresCapability(config, { capability, targetOf })` | Per-capability successor ([ADR 0106](../../../docs/adr/0106-mcp-tools-register-per-granted-capability.md)). `grants` is the app's `{ capability, target }[]`. A tool registers if read-only, or if it declares a capability with ≥1 grant. The gate's wrapper also checks each call: the handler runs only when `targetOf(args)` equals a grant's target exactly; otherwise an MCP error names the capability. No capability names live here; no wildcards; empty targets are refused. `applyReadOnlyGate` is unchanged. |
20
21
  | `serveUntilClosed(server, { label?, stdin?, stdout? })` | Connect over stdio and resolve when the client goes away (stdin `end`/`close` or transport close). The SDK does not report stdin EOF itself, so a server without this outlives its client. Streams are injectable for tests. |
21
22
  | `serveStdio(server, label?)` | `serveUntilClosed`, then `process.exit(0)` — needed under GJS, where the GLib main loop keeps an idle process alive. |
22
23
  | `mcpSuccess(data)` / `mcpError(msg)` / `mcpErrorFrom(err)` | One result shape: a single JSON text block. Errors carry the message only, never a stack. |
@@ -0,0 +1 @@
1
+ import"./_virtual/_rolldown/runtime.js";import{mcpError as e}from"./tool-result.js";const t=new WeakMap;function requiresCapability(e,n){return t.set(e,n),e}function applyGrantGate(n,r){let i=new Map;for(let e of r){if(typeof e.capability!=`string`||e.capability===``)throw Error(`grant: empty capability`);if(typeof e.target!=`string`||e.target===``)throw Error(`grant for ${e.capability}: empty target`);let t=i.get(e.capability);t||i.set(e.capability,t=new Set),t.add(e.target)}let a=n.registerTool.bind(n);n.registerTool=((n,r,o)=>{let s=r&&typeof r==`object`?t.get(r):void 0,c=r?.annotations?.readOnlyHint===!0;if(!s)return c?a(n,r,o):void 0;let l=i.get(s.capability);if(!c&&!l)return;let u=r?.inputSchema!==void 0,checked=async(...t)=>{let n=u?t[0]:{},r;try{r=s.targetOf(n)}catch{r=void 0}return typeof r!=`string`||r===``||!l?.has(r)?e(`capability ${s.capability} is not granted for this call`):o(...t)};return a(n,r,checked)})}export{applyGrantGate,requiresCapability};
package/lib/esm/index.js CHANGED
@@ -1 +1 @@
1
- import{applyReadOnlyGate as e}from"./read-only-gate.js";import{serveStdio as t,serveUntilClosed as n}from"./serve-stdio.js";import{mcpError as r,mcpErrorFrom as i,mcpSuccess as a}from"./tool-result.js";export{e as applyReadOnlyGate,r as mcpError,i as mcpErrorFrom,a as mcpSuccess,t as serveStdio,n as serveUntilClosed};
1
+ import{mcpError as e,mcpErrorFrom as t,mcpSuccess as n}from"./tool-result.js";import{applyGrantGate as r,requiresCapability as i}from"./grant-gate.js";import{applyReadOnlyGate as a}from"./read-only-gate.js";import{serveStdio as o,serveUntilClosed as s}from"./serve-stdio.js";export{r as applyGrantGate,a as applyReadOnlyGate,e as mcpError,t as mcpErrorFrom,n as mcpSuccess,i as requiresCapability,o as serveStdio,s as serveUntilClosed};
@@ -0,0 +1,37 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ /** One permission the app holds: a capability name the APP defines, bound to exactly one target. */
3
+ export interface Grant {
4
+ capability: string;
5
+ target: string;
6
+ }
7
+ /** How a tool declares the write it performs and where the call aims it. */
8
+ export interface CapabilitySpec<Args = Record<string, unknown>> {
9
+ capability: string;
10
+ /** Derive the target from the call's arguments. Empty, non-string or a throw refuses the call. */
11
+ targetOf(args: Args): string;
12
+ }
13
+ /** Declare the capability a tool needs. Returns `config` itself, so it composes inline. */
14
+ export declare function requiresCapability<C extends object, Args = Record<string, unknown>>(config: C, spec: CapabilitySpec<Args>): C;
15
+ /**
16
+ * Gate tools by grant, DEFAULT-DENY — the successor to {@link applyReadOnlyGate} for apps whose
17
+ * writes are permitted per capability.
18
+ *
19
+ * Registration: a tool registers if `annotations.readOnlyHint === true`, or if it declared a
20
+ * capability (via {@link requiresCapability}) and at least one grant for that capability exists.
21
+ * Anything else — no annotations, `readOnlyHint: false` without a capability, an ungranted
22
+ * capability — is DROPPED, so a forgotten declaration shows up as a missing tool, never as a
23
+ * reachable mutation.
24
+ *
25
+ * Call: THIS wrapper, not the tool, performs the check. A capability tool's handler is reached only
26
+ * after `targetOf(args)` returned a non-empty target that equals a grant's target for that
27
+ * capability, EXACTLY (no wildcards, no prefixes). Otherwise the call answers with an MCP error
28
+ * naming the capability. Because the check lives in the registration path, a tool cannot be
29
+ * registered with the check left out — and a forgotten check is the one failure that looks exactly
30
+ * like a working gate.
31
+ *
32
+ * gjsify holds no capability names: they, and the grants, belong to the app. The grant set is
33
+ * copied at call time of this function; later edits to the caller's array do not apply.
34
+ *
35
+ * Call this BEFORE registering anything — it wraps `registerTool`.
36
+ */
37
+ export declare function applyGrantGate(server: McpServer, grants: readonly Grant[]): void;
@@ -1,3 +1,4 @@
1
1
  export { applyReadOnlyGate } from './read-only-gate.js';
2
+ export { applyGrantGate, requiresCapability, type Grant, type CapabilitySpec } from './grant-gate.js';
2
3
  export { serveStdio, serveUntilClosed, type ServeOptions } from './serve-stdio.js';
3
4
  export { mcpSuccess, mcpError, mcpErrorFrom, type McpTextResult } from './tool-result.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/mcp",
3
- "version": "0.59.0",
3
+ "version": "0.60.0",
4
4
  "description": "MCP server runtime for GJS and Node — default-deny read-only gate, stdio serve loop that ends on client EOF, uniform tool results",
5
5
  "type": "module",
6
6
  "module": "lib/esm/index.js",
@@ -36,15 +36,16 @@
36
36
  ],
37
37
  "dependencies": {},
38
38
  "peerDependencies": {
39
- "@modelcontextprotocol/sdk": "^1.29.0"
39
+ "@modelcontextprotocol/sdk": "^1.32.1"
40
40
  },
41
41
  "devDependencies": {
42
- "@gjsify/cli": "^0.59.0",
43
- "@gjsify/stream": "^0.59.0",
44
- "@gjsify/unit": "^0.59.0",
45
- "@modelcontextprotocol/sdk": "^1.29.0",
42
+ "@gjsify/cli": "^0.60.0",
43
+ "@gjsify/stream": "^0.60.0",
44
+ "@gjsify/unit": "^0.60.0",
45
+ "@modelcontextprotocol/sdk": "^1.32.1",
46
46
  "@types/node": "^25.9.2",
47
- "typescript": "^6.0.3"
47
+ "typescript": "^6.0.3",
48
+ "zod": "^4.4.3"
48
49
  },
49
50
  "gjsify": {
50
51
  "runtimes": {