@studiometa/forge-mcp 0.2.3 → 0.2.5

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
@@ -71,13 +71,17 @@ When enabled, the `forge_write` tool is not registered at all — only `forge`,
71
71
 
72
72
  Safe, read-only queries. Annotated `readOnlyHint: true` so MCP clients can auto-approve.
73
73
 
74
- **Actions**: `list`, `get`, `help`, `schema`
74
+ **Actions**: `list`, `get`, `resolve`, `context`, `help`, `schema`
75
75
 
76
76
  ```json
77
77
  { "resource": "servers", "action": "list" }
78
78
  { "resource": "servers", "action": "get", "id": "123" }
79
79
  { "resource": "sites", "action": "list", "server_id": "123" }
80
80
  { "resource": "servers", "action": "help" }
81
+ { "resource": "servers", "action": "resolve", "query": "prod" }
82
+ { "resource": "servers", "action": "context", "id": "123" }
83
+ { "resource": "sites", "action": "context", "server_id": "123", "id": "456" }
84
+ { "resource": "batch", "action": "run", "operations": [{ "resource": "servers", "action": "list" }, { "resource": "recipes", "action": "list" }] }
81
85
  ```
82
86
 
83
87
  ### `forge_write` — Write Operations
@@ -95,31 +99,69 @@ Mutating operations. Annotated `destructiveHint: true` so MCP clients always pro
95
99
 
96
100
  ### Resources & Actions
97
101
 
98
- | Resource | Read Actions | Write Actions | Required Fields |
99
- | --------------- | ------------ | ------------------------ | -------------------------- |
100
- | servers | list, get | create, delete, reboot | id (for get/delete/reboot) |
101
- | sites | list, get | create, delete | server_id |
102
- | deployments | list | deploy, update | server_id, site_id |
103
- | env | get | update | server_id, site_id |
104
- | nginx | get | update | server_id, site_id |
105
- | certificates | list, get | create, delete, activate | server_id, site_id |
106
- | databases | list, get | create, delete | server_id |
107
- | daemons | list, get | create, delete, restart | server_id |
108
- | firewall-rules | list, get | create, delete | server_id |
109
- | ssh-keys | list, get | create, delete | server_id |
110
- | security-rules | list, get | create, delete | server_id, site_id |
111
- | redirect-rules | list, get | create, delete | server_id, site_id |
112
- | monitors | list, get | create, delete | server_id |
113
- | nginx-templates | list, get | create, update, delete | server_id |
114
- | recipes | list, get | create, delete, run | id (for get/delete/run) |
102
+ | Resource | Read Actions | Write Actions | Required Fields |
103
+ | --------------- | --------------------------- | ------------------------ | -------------------------- |
104
+ | servers | list, get, resolve, context | create, delete, reboot | id (for get/delete/reboot) |
105
+ | sites | list, get, resolve, context | create, delete | server_id |
106
+ | deployments | list, get | deploy, update | server_id, site_id |
107
+ | env | get | update | server_id, site_id |
108
+ | nginx | get | update | server_id, site_id |
109
+ | certificates | list, get | create, delete, activate | server_id, site_id |
110
+ | databases | list, get | create, delete | server_id |
111
+ | database-users | list, get | create, delete | server_id |
112
+ | daemons | list, get | create, delete, restart | server_id |
113
+ | firewall-rules | list, get | create, delete | server_id |
114
+ | ssh-keys | list, get | create, delete | server_id |
115
+ | security-rules | list, get | create, delete | server_id, site_id |
116
+ | redirect-rules | list, get | create, delete | server_id, site_id |
117
+ | monitors | list, get | create, delete | server_id |
118
+ | nginx-templates | list, get | create, update, delete | server_id |
119
+ | scheduled-jobs | list, get | create, delete | server_id |
120
+ | backups | list, get | create, delete | server_id |
121
+ | commands | list, get | create | server_id, site_id |
122
+ | recipes | list, get | create, delete, run | id (for get/delete/run) |
123
+ | user | get | — | — |
124
+ | batch | run | — | operations array |
125
+
126
+ ### Auto-Resolve: Names Instead of IDs
127
+
128
+ The `server_id` and `site_id` fields accept **names** in addition to numeric IDs. When a non-numeric value is provided, it is resolved automatically via partial, case-insensitive match against the list of resources.
129
+
130
+ - `server_id: "prod"` → resolves to the server whose name contains "prod" (must be unique)
131
+ - `site_id: "example"` → resolves to the site whose domain contains "example" (requires `server_id`)
132
+
133
+ Use `action: "resolve"` explicitly when you want to search before committing to an ID:
134
+
135
+ ```json
136
+ { "resource": "servers", "action": "resolve", "query": "prod" }
137
+ { "resource": "sites", "action": "resolve", "server_id": "123", "query": "example" }
138
+ ```
139
+
140
+ Resolution fails (and returns an error) when the query matches zero or more than one resource — in that case, use the numeric ID directly.
115
141
 
116
142
  ### Discovery
117
143
 
118
- Use `action: "help"` with any resource:
144
+ Use `action: "help"` with any resource, or `action: "context"` to fetch a resource and all its sub-resources in one call:
119
145
 
120
146
  ```json
121
147
  { "resource": "servers", "action": "help" }
122
148
  { "resource": "deployments", "action": "help" }
149
+ { "resource": "servers", "action": "context", "id": "123" }
150
+ { "resource": "sites", "action": "context", "server_id": "123", "id": "456" }
151
+ ```
152
+
153
+ Use `resource: "batch"` to fan out multiple reads in a single round-trip (max 10 operations):
154
+
155
+ ```json
156
+ {
157
+ "resource": "batch",
158
+ "action": "run",
159
+ "operations": [
160
+ { "resource": "servers", "action": "list" },
161
+ { "resource": "sites", "action": "list", "server_id": "123" },
162
+ { "resource": "recipes", "action": "list" }
163
+ ]
164
+ }
123
165
  ```
124
166
 
125
167
  ## Stdio-Only Tools
@@ -0,0 +1,24 @@
1
+ import type { ExecutorContext } from "@studiometa/forge-core";
2
+ import type { CommonArgs } from "./types.ts";
3
+ import type { ToolResult } from "./types.ts";
4
+ /**
5
+ * Result of auto-resolution. Either the args are updated with resolved IDs,
6
+ * or an error result is returned.
7
+ */
8
+ export type AutoResolveResult = {
9
+ ok: true;
10
+ args: CommonArgs;
11
+ } | {
12
+ ok: false;
13
+ error: ToolResult;
14
+ };
15
+ /**
16
+ * Auto-resolve non-numeric server_id and site_id fields.
17
+ *
18
+ * Order matters: server_id is resolved first because site_id resolution
19
+ * requires a numeric server_id.
20
+ *
21
+ * Skipped for the 'resolve' action itself (it IS the resolve action).
22
+ */
23
+ export declare function autoResolveIds(args: CommonArgs, executorContext: ExecutorContext): Promise<AutoResolveResult>;
24
+ //# sourceMappingURL=auto-resolve.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auto-resolve.d.ts","sourceRoot":"","sources":["../../src/handlers/auto-resolve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAE9D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAS7C;;;GAGG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,UAAU,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,UAAU,CAAA;CAAE,CAAC;AAElG;;;;;;;GAOG;AACH,wBAAsB,cAAc,CAClC,IAAI,EAAE,UAAU,EAChB,eAAe,EAAE,eAAe,GAC/B,OAAO,CAAC,iBAAiB,CAAC,CA6E5B"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Batch handler — executes multiple read operations in a single MCP call.
3
+ *
4
+ * Reduces round-trips for AI agents by running operations in parallel
5
+ * with Promise.allSettled, isolating per-operation failures.
6
+ */
7
+ import type { CommonArgs, HandlerContext, ToolResult } from "./types.ts";
8
+ type RouteHandler = (resource: string, action: string, args: CommonArgs, ctx: HandlerContext) => Promise<ToolResult>;
9
+ /**
10
+ * Handle batch action — executes multiple read operations in parallel.
11
+ */
12
+ export declare function handleBatch(action: string, args: CommonArgs, ctx: HandlerContext, routeToHandler: RouteHandler): Promise<ToolResult>;
13
+ export {};
14
+ //# sourceMappingURL=batch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"batch.d.ts","sourceRoot":"","sources":["../../src/handlers/batch.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAKzE,KAAK,YAAY,GAAG,CAClB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,UAAU,EAChB,GAAG,EAAE,cAAc,KAChB,OAAO,CAAC,UAAU,CAAC,CAAC;AAIzB;;GAEG;AACH,wBAAsB,WAAW,CAC/B,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,UAAU,EAChB,GAAG,EAAE,cAAc,EACnB,cAAc,EAAE,YAAY,GAC3B,OAAO,CAAC,UAAU,CAAC,CAuGrB"}
@@ -0,0 +1,14 @@
1
+ import type { CommonArgs, HandlerContext, ToolResult } from "./types.ts";
2
+ /**
3
+ * Handle server context action — fetches server details plus all sub-resources
4
+ * (sites, databases, database users, daemons, firewall rules, scheduled jobs)
5
+ * in parallel in a single call.
6
+ */
7
+ export declare function handleServerContext(args: CommonArgs, ctx: HandlerContext): Promise<ToolResult>;
8
+ /**
9
+ * Handle site context action — fetches site details plus all sub-resources
10
+ * (recent deployments, certificates, redirect rules, security rules)
11
+ * in parallel in a single call. Deployments are limited to the last 5.
12
+ */
13
+ export declare function handleSiteContext(args: CommonArgs, ctx: HandlerContext): Promise<ToolResult>;
14
+ //# sourceMappingURL=context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/handlers/context.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAGzE;;;;GAIG;AACH,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,UAAU,EAChB,GAAG,EAAE,cAAc,GAClB,OAAO,CAAC,UAAU,CAAC,CA0BrB;AAED;;;;GAIG;AACH,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,UAAU,EAChB,GAAG,EAAE,cAAc,GAClB,OAAO,CAAC,UAAU,CAAC,CA2BrB"}
@@ -1 +1 @@
1
- {"version":3,"file":"help.d.ts","sourceRoot":"","sources":["../../src/handlers/help.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AA0oB7C;;GAEG;AACH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,CAQvD;AAED;;GAEG;AACH,wBAAgB,kBAAkB,IAAI,UAAU,CAa/C"}
1
+ {"version":3,"file":"help.d.ts","sourceRoot":"","sources":["../../src/handlers/help.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAyrB7C;;GAEG;AACH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,CAQvD;AAED;;GAEG;AACH,wBAAgB,kBAAkB,IAAI,UAAU,CAa/C"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/handlers/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAOH,OAAO,KAAK,EAA8B,UAAU,EAAE,MAAM,YAAY,CAAC;AA8BzE,YAAY,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAwE7C;;;;;GAKG;AACH,wBAAsB,0BAA0B,CAC9C,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,WAAW,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,GAChC,OAAO,CAAC,UAAU,CAAC,CAyFrB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/handlers/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAOH,OAAO,KAAK,EAA8B,UAAU,EAAE,MAAM,YAAY,CAAC;AAgCzE,YAAY,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AA0E7C;;;;;GAKG;AACH,wBAAsB,0BAA0B,CAC9C,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,WAAW,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,GAChC,OAAO,CAAC,UAAU,CAAC,CAoHrB"}
@@ -1 +1 @@
1
- {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/handlers/schema.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAsU7C;;GAEG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,CAWzD;AAED;;GAEG;AACH,wBAAgB,oBAAoB,IAAI,UAAU,CAWjD"}
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/handlers/schema.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAkV7C;;GAEG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,CAWzD;AAED;;GAEG;AACH,wBAAgB,oBAAoB,IAAI,UAAU,CAWjD"}
@@ -1,2 +1,7 @@
1
- export declare const handleServers: (action: string, args: import("./types.ts").CommonArgs, ctx: import("./types.ts").HandlerContext) => Promise<import("./types.ts").ToolResult>;
1
+ import type { CommonArgs, HandlerContext, ToolResult } from "./types.ts";
2
+ /**
3
+ * Handle servers resource actions, intercepting the `context` action
4
+ * for rich single-call resource fetching.
5
+ */
6
+ export declare function handleServers(action: string, args: CommonArgs, ctx: HandlerContext): Promise<ToolResult>;
2
7
  //# sourceMappingURL=servers.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"servers.d.ts","sourceRoot":"","sources":["../../src/handlers/servers.ts"],"names":[],"mappings":"AAcA,eAAO,MAAM,aAAa,+IAwDxB,CAAC"}
1
+ {"version":3,"file":"servers.d.ts","sourceRoot":"","sources":["../../src/handlers/servers.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAsEzE;;;GAGG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,UAAU,EAChB,GAAG,EAAE,cAAc,GAClB,OAAO,CAAC,UAAU,CAAC,CAKrB"}
@@ -1,2 +1,7 @@
1
- export declare const handleSites: (action: string, args: import("./types.ts").CommonArgs, ctx: import("./types.ts").HandlerContext) => Promise<import("./types.ts").ToolResult>;
1
+ import type { CommonArgs, HandlerContext, ToolResult } from "./types.ts";
2
+ /**
3
+ * Handle sites resource actions, intercepting the `context` action
4
+ * for rich single-call resource fetching.
5
+ */
6
+ export declare function handleSites(action: string, args: CommonArgs, ctx: HandlerContext): Promise<ToolResult>;
2
7
  //# sourceMappingURL=sites.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sites.d.ts","sourceRoot":"","sources":["../../src/handlers/sites.ts"],"names":[],"mappings":"AAQA,eAAO,MAAM,WAAW,+IAsDtB,CAAC"}
1
+ {"version":3,"file":"sites.d.ts","sourceRoot":"","sources":["../../src/handlers/sites.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAqEzE;;;GAGG;AACH,wBAAsB,WAAW,CAC/B,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,UAAU,EAChB,GAAG,EAAE,cAAc,GAClB,OAAO,CAAC,UAAU,CAAC,CAKrB"}
@@ -1,4 +1,4 @@
1
- import { a as INSTRUCTIONS, i as getTools, n as executeToolWithCredentials, t as VERSION } from "./version-BTMdX8xQ.js";
1
+ import { a as INSTRUCTIONS, i as getTools, n as executeToolWithCredentials, t as VERSION } from "./version-DnjoiQWK.js";
2
2
  import { parseAuthHeader } from "./auth.js";
3
3
  import { authorizeGetHandler, authorizePostHandler, oauthMetadataHandler, protectedResourceHandler, registerHandler, tokenHandler } from "./oauth.js";
4
4
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
@@ -286,4 +286,4 @@ function createHealthApp() {
286
286
  }
287
287
  export { SessionManager as a, handleMcpRequest as i, createMcpRequestHandler as n, createMcpServer as r, createHealthApp as t };
288
288
 
289
- //# sourceMappingURL=http-CK8WsamV.js.map
289
+ //# sourceMappingURL=http-uQlxqOji.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"http-CK8WsamV.js","names":[],"sources":["../src/sessions.ts","../src/http.ts"],"sourcesContent":["/**\n * Session manager for multi-tenant Streamable HTTP transport.\n *\n * Each MCP client session gets its own transport + server pair.\n * Sessions are identified by UUID and tracked in a Map.\n *\n * Supports automatic TTL-based cleanup of idle sessions to prevent\n * memory leaks from abandoned clients.\n */\n\nimport type { Server } from \"@modelcontextprotocol/sdk/server/index.js\";\nimport type { StreamableHTTPServerTransport } from \"@modelcontextprotocol/sdk/server/streamableHttp.js\";\n\n/**\n * A managed session: transport + MCP server pair.\n */\nexport interface ManagedSession {\n transport: StreamableHTTPServerTransport;\n server: Server;\n createdAt: number;\n lastActiveAt: number;\n}\n\nexport interface SessionManagerOptions {\n /**\n * Maximum idle time in milliseconds before a session is reaped.\n * Default: 30 minutes. Set to 0 to disable automatic cleanup.\n */\n ttl?: number;\n\n /**\n * How often to check for expired sessions, in milliseconds.\n * Default: 60 seconds.\n */\n sweepInterval?: number;\n}\n\nconst DEFAULT_TTL = 30 * 60 * 1000; // 30 minutes\nconst DEFAULT_SWEEP_INTERVAL = 60 * 1000; // 60 seconds\n\nexport class SessionManager {\n private sessions = new Map<string, ManagedSession>();\n private sweepTimer: ReturnType<typeof setInterval> | undefined;\n private readonly ttl: number;\n\n constructor(options?: SessionManagerOptions) {\n this.ttl = options?.ttl ?? DEFAULT_TTL;\n\n if (this.ttl > 0) {\n const interval = options?.sweepInterval ?? DEFAULT_SWEEP_INTERVAL;\n this.sweepTimer = setInterval(() => {\n this.sweep();\n }, interval);\n // Don't keep the process alive just for the sweep timer\n this.sweepTimer.unref();\n }\n }\n\n /**\n * Register a session after its ID has been assigned by the transport.\n */\n register(transport: StreamableHTTPServerTransport, server: Server): void {\n const sessionId = transport.sessionId;\n if (sessionId) {\n const now = Date.now();\n this.sessions.set(sessionId, {\n transport,\n server,\n createdAt: now,\n lastActiveAt: now,\n });\n }\n }\n\n /**\n * Look up a session by its ID and refresh its activity timestamp.\n */\n get(sessionId: string): ManagedSession | undefined {\n const session = this.sessions.get(sessionId);\n if (session) {\n session.lastActiveAt = Date.now();\n }\n return session;\n }\n\n /**\n * Remove a session and close its transport + server.\n */\n async remove(sessionId: string): Promise<void> {\n const session = this.sessions.get(sessionId);\n if (session) {\n this.sessions.delete(sessionId);\n await session.transport.close();\n await session.server.close();\n }\n }\n\n /**\n * Get the number of active sessions.\n */\n get size(): number {\n return this.sessions.size;\n }\n\n /**\n * Sweep expired sessions. Called automatically by the sweep timer.\n * Returns the number of sessions reaped.\n */\n sweep(): number {\n if (this.ttl <= 0) return 0;\n\n const now = Date.now();\n const expired: string[] = [];\n\n for (const [id, session] of this.sessions) {\n if (now - session.lastActiveAt > this.ttl) {\n expired.push(id);\n }\n }\n\n for (const id of expired) {\n // Fire-and-forget cleanup — don't block the sweep\n /* v8 ignore start */\n this.remove(id).catch(() => {});\n /* v8 ignore stop */\n }\n\n return expired.length;\n }\n\n /**\n * Close all sessions, stop the sweep timer, and clean up.\n */\n async closeAll(): Promise<void> {\n if (this.sweepTimer) {\n clearInterval(this.sweepTimer);\n this.sweepTimer = undefined;\n }\n\n const promises: Promise<void>[] = [];\n for (const [, session] of this.sessions) {\n promises.push(session.transport.close());\n promises.push(session.server.close());\n }\n await Promise.all(promises);\n this.sessions.clear();\n }\n}\n","/**\n * Streamable HTTP transport for Forge MCP Server\n *\n * Implements the official MCP Streamable HTTP transport specification (2025-03-26)\n * using the SDK's StreamableHTTPServerTransport.\n *\n * Architecture:\n * - Stateful mode with per-session transport+server pairs (multi-tenant)\n * - Auth via Bearer token → authInfo.token → handler extra.authInfo\n * - Session manager (injected) maps session IDs to transport+server instances\n * - Health/status endpoints handled by h3, MCP endpoint by the SDK transport\n */\n\nimport { randomUUID } from \"node:crypto\";\nimport type { IncomingMessage, ServerResponse } from \"node:http\";\n\nimport { Server } from \"@modelcontextprotocol/sdk/server/index.js\";\nimport { StreamableHTTPServerTransport } from \"@modelcontextprotocol/sdk/server/streamableHttp.js\";\nimport { CallToolRequestSchema, ListToolsRequestSchema } from \"@modelcontextprotocol/sdk/types.js\";\nimport { createApp, defineEventHandler, type H3 } from \"h3\";\n\nimport { parseAuthHeader } from \"./auth.ts\";\nimport { executeToolWithCredentials } from \"./handlers/index.ts\";\nimport { INSTRUCTIONS } from \"./instructions.ts\";\nimport {\n oauthMetadataHandler,\n protectedResourceHandler,\n registerHandler,\n authorizeGetHandler,\n authorizePostHandler,\n tokenHandler,\n} from \"./oauth.ts\";\nimport { SessionManager } from \"./sessions.ts\";\nimport { getTools } from \"./tools.ts\";\nimport { VERSION } from \"./version.ts\";\n\nexport { SessionManager } from \"./sessions.ts\";\n\n/**\n * Options for the HTTP MCP server.\n */\nexport interface HttpServerOptions {\n /** When true, forge_write tool is not registered and write operations are rejected. */\n readOnly?: boolean;\n}\n\n/**\n * Create a configured MCP Server instance for HTTP transport.\n *\n * Unlike stdio, HTTP mode does NOT include forge_configure/forge_get_config\n * because credentials come from the Authorization header per-request.\n */\nexport function createMcpServer(options?: HttpServerOptions): Server {\n const readOnly = options?.readOnly ?? false;\n const tools = getTools({ readOnly });\n\n const server = new Server(\n {\n name: \"forge-mcp\",\n version: VERSION,\n },\n {\n capabilities: {\n tools: {},\n },\n instructions: INSTRUCTIONS,\n },\n );\n\n server.setRequestHandler(ListToolsRequestSchema, async () => {\n return { tools };\n });\n\n server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {\n const { name, arguments: args } = request.params;\n const token = extra.authInfo?.token;\n\n /* v8 ignore start */\n if (!token) {\n return {\n content: [\n {\n type: \"text\" as const,\n text: \"Error: Authentication required. No token found in request.\",\n },\n ],\n structuredContent: {\n success: false,\n error: \"Authentication required. No token found in request.\",\n },\n isError: true,\n };\n }\n /* v8 ignore stop */\n\n // Reject write operations in read-only mode\n if (readOnly && name === \"forge_write\") {\n return {\n content: [\n {\n type: \"text\" as const,\n text: \"Error: Server is running in read-only mode. Write operations are disabled.\",\n },\n ],\n structuredContent: {\n success: false,\n error: \"Server is running in read-only mode. Write operations are disabled.\",\n },\n isError: true,\n };\n }\n\n try {\n const result = await executeToolWithCredentials(\n name,\n /* v8 ignore next */ (args as Record<string, unknown>) ?? {},\n { apiToken: token },\n );\n return result as unknown as Record<string, unknown>;\n } catch (error) {\n /* v8 ignore start */\n const message = error instanceof Error ? error.message : String(error);\n /* v8 ignore stop */\n return {\n content: [{ type: \"text\" as const, text: `Error: ${message}` }],\n structuredContent: { success: false, error: message },\n isError: true,\n };\n }\n });\n\n return server;\n}\n\n/**\n * Handle an MCP request using the Streamable HTTP transport.\n *\n * Routes requests based on whether they have a session ID:\n * - No session ID + initialize request → create new session\n * - Has session ID → route to existing session's transport\n *\n * @param req - Node.js IncomingMessage\n * @param res - Node.js ServerResponse\n * @param sessions - Session manager instance (injected)\n * @param options - Server options (read-only mode, etc.)\n */\nexport async function handleMcpRequest(\n req: IncomingMessage,\n res: ServerResponse,\n sessions: SessionManager,\n options?: HttpServerOptions,\n): Promise<void> {\n // Extract and validate auth\n const authHeader = req.headers.authorization;\n const credentials = parseAuthHeader(authHeader);\n\n if (!credentials) {\n // Build resource_metadata URL for the WWW-Authenticate header (RFC 9728)\n const host = req.headers.host || \"localhost:3000\";\n const protocol = (req.headers[\"x-forwarded-proto\"] as string) || \"http\";\n const resourceMetadataUrl = `${protocol}://${host}/.well-known/oauth-protected-resource`;\n\n res.writeHead(401, {\n \"Content-Type\": \"application/json\",\n \"WWW-Authenticate\": `Bearer resource_metadata=\"${resourceMetadataUrl}\"`,\n });\n res.end(\n JSON.stringify({\n jsonrpc: \"2.0\",\n error: {\n code: -32001,\n message: \"Authentication required. Provide a Bearer token with your Forge API token.\",\n },\n id: null,\n }),\n );\n return;\n }\n\n // Inject auth info for the SDK transport\n const authenticatedReq = req as IncomingMessage & {\n auth?: { token: string; clientId: string; scopes: string[] };\n };\n authenticatedReq.auth = {\n token: credentials.apiToken,\n clientId: \"forge-http-client\",\n scopes: [],\n };\n\n const sessionId = req.headers[\"mcp-session-id\"] as string | undefined;\n\n if (sessionId) {\n // Existing session — route to its transport\n const session = sessions.get(sessionId);\n if (!session) {\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n jsonrpc: \"2.0\",\n error: {\n code: -32000,\n message: \"Session not found. The session may have expired or been terminated.\",\n },\n id: null,\n }),\n );\n return;\n }\n\n await session.transport.handleRequest(authenticatedReq, res);\n return;\n }\n\n // No session ID — this should be an initialize request.\n // Create a new transport + server pair.\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: () => randomUUID(),\n });\n\n const server = createMcpServer(options);\n await server.connect(transport);\n\n // Set up cleanup on close\n transport.onclose = () => {\n const sid = transport.sessionId;\n /* v8 ignore start */\n if (sid) {\n sessions.remove(sid).catch(() => {\n // Ignore cleanup errors\n });\n }\n /* v8 ignore stop */\n };\n\n // Handle the request (this will set transport.sessionId during initialize)\n await transport.handleRequest(authenticatedReq, res);\n\n // After handling, register the session if the transport got a session ID\n /* v8 ignore start */\n if (transport.sessionId) {\n sessions.register(transport, server);\n } else {\n // No session was created (e.g., invalid request) — clean up\n await transport.close();\n await server.close();\n }\n /* v8 ignore stop */\n}\n\n/**\n * Create a request handler bound to a SessionManager instance.\n * Convenience factory for server.ts.\n */\nexport function createMcpRequestHandler(\n sessions: SessionManager,\n options?: HttpServerOptions,\n): (req: IncomingMessage, res: ServerResponse) => Promise<void> {\n /* v8 ignore start */\n return (req, res) => handleMcpRequest(req, res, sessions, options);\n /* v8 ignore stop */\n}\n\n/**\n * Create h3 app for health check, service info, and OAuth endpoints.\n * The MCP endpoint is handled separately by handleMcpRequest.\n */\nexport function createHealthApp(): H3 {\n const app = createApp();\n\n // Service info & health\n app.get(\n \"/\",\n defineEventHandler(() => {\n return { status: \"ok\", service: \"forge-mcp\", version: VERSION };\n }),\n );\n\n app.get(\n \"/health\",\n defineEventHandler(() => {\n return { status: \"ok\" };\n }),\n );\n\n // OAuth 2.1 endpoints\n app.get(\"/.well-known/oauth-authorization-server\", oauthMetadataHandler);\n app.get(\"/.well-known/oauth-protected-resource\", protectedResourceHandler);\n app.post(\"/register\", registerHandler);\n app.get(\"/authorize\", authorizeGetHandler);\n app.post(\"/authorize\", authorizePostHandler);\n app.post(\"/token\", tokenHandler);\n\n return app;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"http-uQlxqOji.js","names":[],"sources":["../src/sessions.ts","../src/http.ts"],"sourcesContent":["/**\n * Session manager for multi-tenant Streamable HTTP transport.\n *\n * Each MCP client session gets its own transport + server pair.\n * Sessions are identified by UUID and tracked in a Map.\n *\n * Supports automatic TTL-based cleanup of idle sessions to prevent\n * memory leaks from abandoned clients.\n */\n\nimport type { Server } from \"@modelcontextprotocol/sdk/server/index.js\";\nimport type { StreamableHTTPServerTransport } from \"@modelcontextprotocol/sdk/server/streamableHttp.js\";\n\n/**\n * A managed session: transport + MCP server pair.\n */\nexport interface ManagedSession {\n transport: StreamableHTTPServerTransport;\n server: Server;\n createdAt: number;\n lastActiveAt: number;\n}\n\nexport interface SessionManagerOptions {\n /**\n * Maximum idle time in milliseconds before a session is reaped.\n * Default: 30 minutes. Set to 0 to disable automatic cleanup.\n */\n ttl?: number;\n\n /**\n * How often to check for expired sessions, in milliseconds.\n * Default: 60 seconds.\n */\n sweepInterval?: number;\n}\n\nconst DEFAULT_TTL = 30 * 60 * 1000; // 30 minutes\nconst DEFAULT_SWEEP_INTERVAL = 60 * 1000; // 60 seconds\n\nexport class SessionManager {\n private sessions = new Map<string, ManagedSession>();\n private sweepTimer: ReturnType<typeof setInterval> | undefined;\n private readonly ttl: number;\n\n constructor(options?: SessionManagerOptions) {\n this.ttl = options?.ttl ?? DEFAULT_TTL;\n\n if (this.ttl > 0) {\n const interval = options?.sweepInterval ?? DEFAULT_SWEEP_INTERVAL;\n this.sweepTimer = setInterval(() => {\n this.sweep();\n }, interval);\n // Don't keep the process alive just for the sweep timer\n this.sweepTimer.unref();\n }\n }\n\n /**\n * Register a session after its ID has been assigned by the transport.\n */\n register(transport: StreamableHTTPServerTransport, server: Server): void {\n const sessionId = transport.sessionId;\n if (sessionId) {\n const now = Date.now();\n this.sessions.set(sessionId, {\n transport,\n server,\n createdAt: now,\n lastActiveAt: now,\n });\n }\n }\n\n /**\n * Look up a session by its ID and refresh its activity timestamp.\n */\n get(sessionId: string): ManagedSession | undefined {\n const session = this.sessions.get(sessionId);\n if (session) {\n session.lastActiveAt = Date.now();\n }\n return session;\n }\n\n /**\n * Remove a session and close its transport + server.\n */\n async remove(sessionId: string): Promise<void> {\n const session = this.sessions.get(sessionId);\n if (session) {\n this.sessions.delete(sessionId);\n await session.transport.close();\n await session.server.close();\n }\n }\n\n /**\n * Get the number of active sessions.\n */\n get size(): number {\n return this.sessions.size;\n }\n\n /**\n * Sweep expired sessions. Called automatically by the sweep timer.\n * Returns the number of sessions reaped.\n */\n sweep(): number {\n if (this.ttl <= 0) return 0;\n\n const now = Date.now();\n const expired: string[] = [];\n\n for (const [id, session] of this.sessions) {\n if (now - session.lastActiveAt > this.ttl) {\n expired.push(id);\n }\n }\n\n for (const id of expired) {\n // Fire-and-forget cleanup — don't block the sweep\n /* v8 ignore start */\n this.remove(id).catch(() => {});\n /* v8 ignore stop */\n }\n\n return expired.length;\n }\n\n /**\n * Close all sessions, stop the sweep timer, and clean up.\n */\n async closeAll(): Promise<void> {\n if (this.sweepTimer) {\n clearInterval(this.sweepTimer);\n this.sweepTimer = undefined;\n }\n\n const promises: Promise<void>[] = [];\n for (const [, session] of this.sessions) {\n promises.push(session.transport.close());\n promises.push(session.server.close());\n }\n await Promise.all(promises);\n this.sessions.clear();\n }\n}\n","/**\n * Streamable HTTP transport for Forge MCP Server\n *\n * Implements the official MCP Streamable HTTP transport specification (2025-03-26)\n * using the SDK's StreamableHTTPServerTransport.\n *\n * Architecture:\n * - Stateful mode with per-session transport+server pairs (multi-tenant)\n * - Auth via Bearer token → authInfo.token → handler extra.authInfo\n * - Session manager (injected) maps session IDs to transport+server instances\n * - Health/status endpoints handled by h3, MCP endpoint by the SDK transport\n */\n\nimport { randomUUID } from \"node:crypto\";\nimport type { IncomingMessage, ServerResponse } from \"node:http\";\n\nimport { Server } from \"@modelcontextprotocol/sdk/server/index.js\";\nimport { StreamableHTTPServerTransport } from \"@modelcontextprotocol/sdk/server/streamableHttp.js\";\nimport { CallToolRequestSchema, ListToolsRequestSchema } from \"@modelcontextprotocol/sdk/types.js\";\nimport { createApp, defineEventHandler, type H3 } from \"h3\";\n\nimport { parseAuthHeader } from \"./auth.ts\";\nimport { executeToolWithCredentials } from \"./handlers/index.ts\";\nimport { INSTRUCTIONS } from \"./instructions.ts\";\nimport {\n oauthMetadataHandler,\n protectedResourceHandler,\n registerHandler,\n authorizeGetHandler,\n authorizePostHandler,\n tokenHandler,\n} from \"./oauth.ts\";\nimport { SessionManager } from \"./sessions.ts\";\nimport { getTools } from \"./tools.ts\";\nimport { VERSION } from \"./version.ts\";\n\nexport { SessionManager } from \"./sessions.ts\";\n\n/**\n * Options for the HTTP MCP server.\n */\nexport interface HttpServerOptions {\n /** When true, forge_write tool is not registered and write operations are rejected. */\n readOnly?: boolean;\n}\n\n/**\n * Create a configured MCP Server instance for HTTP transport.\n *\n * Unlike stdio, HTTP mode does NOT include forge_configure/forge_get_config\n * because credentials come from the Authorization header per-request.\n */\nexport function createMcpServer(options?: HttpServerOptions): Server {\n const readOnly = options?.readOnly ?? false;\n const tools = getTools({ readOnly });\n\n const server = new Server(\n {\n name: \"forge-mcp\",\n version: VERSION,\n },\n {\n capabilities: {\n tools: {},\n },\n instructions: INSTRUCTIONS,\n },\n );\n\n server.setRequestHandler(ListToolsRequestSchema, async () => {\n return { tools };\n });\n\n server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {\n const { name, arguments: args } = request.params;\n const token = extra.authInfo?.token;\n\n /* v8 ignore start */\n if (!token) {\n return {\n content: [\n {\n type: \"text\" as const,\n text: \"Error: Authentication required. No token found in request.\",\n },\n ],\n structuredContent: {\n success: false,\n error: \"Authentication required. No token found in request.\",\n },\n isError: true,\n };\n }\n /* v8 ignore stop */\n\n // Reject write operations in read-only mode\n if (readOnly && name === \"forge_write\") {\n return {\n content: [\n {\n type: \"text\" as const,\n text: \"Error: Server is running in read-only mode. Write operations are disabled.\",\n },\n ],\n structuredContent: {\n success: false,\n error: \"Server is running in read-only mode. Write operations are disabled.\",\n },\n isError: true,\n };\n }\n\n try {\n const result = await executeToolWithCredentials(\n name,\n /* v8 ignore next */ (args as Record<string, unknown>) ?? {},\n { apiToken: token },\n );\n return result as unknown as Record<string, unknown>;\n } catch (error) {\n /* v8 ignore start */\n const message = error instanceof Error ? error.message : String(error);\n /* v8 ignore stop */\n return {\n content: [{ type: \"text\" as const, text: `Error: ${message}` }],\n structuredContent: { success: false, error: message },\n isError: true,\n };\n }\n });\n\n return server;\n}\n\n/**\n * Handle an MCP request using the Streamable HTTP transport.\n *\n * Routes requests based on whether they have a session ID:\n * - No session ID + initialize request → create new session\n * - Has session ID → route to existing session's transport\n *\n * @param req - Node.js IncomingMessage\n * @param res - Node.js ServerResponse\n * @param sessions - Session manager instance (injected)\n * @param options - Server options (read-only mode, etc.)\n */\nexport async function handleMcpRequest(\n req: IncomingMessage,\n res: ServerResponse,\n sessions: SessionManager,\n options?: HttpServerOptions,\n): Promise<void> {\n // Extract and validate auth\n const authHeader = req.headers.authorization;\n const credentials = parseAuthHeader(authHeader);\n\n if (!credentials) {\n // Build resource_metadata URL for the WWW-Authenticate header (RFC 9728)\n const host = req.headers.host || \"localhost:3000\";\n const protocol = (req.headers[\"x-forwarded-proto\"] as string) || \"http\";\n const resourceMetadataUrl = `${protocol}://${host}/.well-known/oauth-protected-resource`;\n\n res.writeHead(401, {\n \"Content-Type\": \"application/json\",\n \"WWW-Authenticate\": `Bearer resource_metadata=\"${resourceMetadataUrl}\"`,\n });\n res.end(\n JSON.stringify({\n jsonrpc: \"2.0\",\n error: {\n code: -32001,\n message: \"Authentication required. Provide a Bearer token with your Forge API token.\",\n },\n id: null,\n }),\n );\n return;\n }\n\n // Inject auth info for the SDK transport\n const authenticatedReq = req as IncomingMessage & {\n auth?: { token: string; clientId: string; scopes: string[] };\n };\n authenticatedReq.auth = {\n token: credentials.apiToken,\n clientId: \"forge-http-client\",\n scopes: [],\n };\n\n const sessionId = req.headers[\"mcp-session-id\"] as string | undefined;\n\n if (sessionId) {\n // Existing session — route to its transport\n const session = sessions.get(sessionId);\n if (!session) {\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n jsonrpc: \"2.0\",\n error: {\n code: -32000,\n message: \"Session not found. The session may have expired or been terminated.\",\n },\n id: null,\n }),\n );\n return;\n }\n\n await session.transport.handleRequest(authenticatedReq, res);\n return;\n }\n\n // No session ID — this should be an initialize request.\n // Create a new transport + server pair.\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: () => randomUUID(),\n });\n\n const server = createMcpServer(options);\n await server.connect(transport);\n\n // Set up cleanup on close\n transport.onclose = () => {\n const sid = transport.sessionId;\n /* v8 ignore start */\n if (sid) {\n sessions.remove(sid).catch(() => {\n // Ignore cleanup errors\n });\n }\n /* v8 ignore stop */\n };\n\n // Handle the request (this will set transport.sessionId during initialize)\n await transport.handleRequest(authenticatedReq, res);\n\n // After handling, register the session if the transport got a session ID\n /* v8 ignore start */\n if (transport.sessionId) {\n sessions.register(transport, server);\n } else {\n // No session was created (e.g., invalid request) — clean up\n await transport.close();\n await server.close();\n }\n /* v8 ignore stop */\n}\n\n/**\n * Create a request handler bound to a SessionManager instance.\n * Convenience factory for server.ts.\n */\nexport function createMcpRequestHandler(\n sessions: SessionManager,\n options?: HttpServerOptions,\n): (req: IncomingMessage, res: ServerResponse) => Promise<void> {\n /* v8 ignore start */\n return (req, res) => handleMcpRequest(req, res, sessions, options);\n /* v8 ignore stop */\n}\n\n/**\n * Create h3 app for health check, service info, and OAuth endpoints.\n * The MCP endpoint is handled separately by handleMcpRequest.\n */\nexport function createHealthApp(): H3 {\n const app = createApp();\n\n // Service info & health\n app.get(\n \"/\",\n defineEventHandler(() => {\n return { status: \"ok\", service: \"forge-mcp\", version: VERSION };\n }),\n );\n\n app.get(\n \"/health\",\n defineEventHandler(() => {\n return { status: \"ok\" };\n }),\n );\n\n // OAuth 2.1 endpoints\n app.get(\"/.well-known/oauth-authorization-server\", oauthMetadataHandler);\n app.get(\"/.well-known/oauth-protected-resource\", protectedResourceHandler);\n app.post(\"/register\", registerHandler);\n app.get(\"/authorize\", authorizeGetHandler);\n app.post(\"/authorize\", authorizePostHandler);\n app.post(\"/token\", tokenHandler);\n\n return app;\n}\n"],"mappings":"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"}
package/dist/http.js CHANGED
@@ -1,3 +1,3 @@
1
- import "./version-BTMdX8xQ.js";
2
- import { a as SessionManager, i as handleMcpRequest, n as createMcpRequestHandler, r as createMcpServer, t as createHealthApp } from "./http-CK8WsamV.js";
1
+ import "./version-DnjoiQWK.js";
2
+ import { a as SessionManager, i as handleMcpRequest, n as createMcpRequestHandler, r as createMcpServer, t as createHealthApp } from "./http-uQlxqOji.js";
3
3
  export { SessionManager, createHealthApp, createMcpRequestHandler, createMcpServer, handleMcpRequest };
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { t as parseReadOnlyFlag } from "./flags-LFbdErsZ.js";
3
- import { a as INSTRUCTIONS, i as getTools, n as executeToolWithCredentials, r as STDIO_ONLY_TOOLS, t as VERSION } from "./version-BTMdX8xQ.js";
3
+ import { a as INSTRUCTIONS, i as getTools, n as executeToolWithCredentials, r as STDIO_ONLY_TOOLS, t as VERSION } from "./version-DnjoiQWK.js";
4
4
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
5
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
6
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
package/dist/server.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { t as parseReadOnlyFlag } from "./flags-LFbdErsZ.js";
3
- import { t as VERSION } from "./version-BTMdX8xQ.js";
4
- import { a as SessionManager, n as createMcpRequestHandler, t as createHealthApp } from "./http-CK8WsamV.js";
3
+ import { t as VERSION } from "./version-DnjoiQWK.js";
4
+ import { a as SessionManager, n as createMcpRequestHandler, t as createHealthApp } from "./http-uQlxqOji.js";
5
5
  import { toNodeHandler } from "h3/node";
6
6
  import { createServer } from "node:http";
7
7
  /**
package/dist/tools.d.ts CHANGED
@@ -2,7 +2,7 @@ import type { Tool } from "@modelcontextprotocol/sdk/types.js";
2
2
  /**
3
3
  * Read-only actions — safe operations that don't modify server state.
4
4
  */
5
- export declare const READ_ACTIONS: readonly ["list", "get", "help", "schema"];
5
+ export declare const READ_ACTIONS: readonly ["list", "get", "resolve", "help", "schema", "context"];
6
6
  /**
7
7
  * Write actions — operations that modify server state.
8
8
  * These are separated into the `forge_write` tool for safety.
@@ -1 +1 @@
1
- {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,oCAAoC,CAAC;AAI/D;;GAEG;AACH,eAAO,MAAM,YAAY,4CAA6C,CAAC;AAEvE;;;GAGG;AACH,eAAO,MAAM,aAAa,2FAShB,CAAC;AAEX,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,CAAC,CAAC;AACvD,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,CAAC,CAAC;AAEzD;;GAEG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,IAAI,WAAW,CAEnE;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,IAAI,UAAU,CAEjE;AA0OD;;;;;;GAMG;AACH,eAAO,MAAM,KAAK,EAAE,IAAI,EAAwC,CAAC;AAEjE;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,OAAO,CAAC,EAAE,eAAe,GAAG,IAAI,EAAE,CAK1D;AAED;;GAEG;AACH,eAAO,MAAM,gBAAgB,EAAE,IAAI,EAsDlC,CAAC"}
1
+ {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,oCAAoC,CAAC;AAI/D;;GAEG;AACH,eAAO,MAAM,YAAY,kEAAmE,CAAC;AAE7F;;;GAGG;AACH,eAAO,MAAM,aAAa,2FAShB,CAAC;AAEX,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,CAAC,CAAC;AACvD,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,CAAC,CAAC;AAEzD;;GAEG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,IAAI,WAAW,CAEnE;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,IAAI,UAAU,CAEjE;AAkQD;;;;;;GAMG;AACH,eAAO,MAAM,KAAK,EAAE,IAAI,EAAwC,CAAC;AAEjE;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,OAAO,CAAC,EAAE,eAAe,GAAG,IAAI,EAAE,CAK1D;AAED;;GAEG;AACH,eAAO,MAAM,gBAAgB,EAAE,IAAI,EAsDlC,CAAC"}