Showing the changes to examples (29 of 865 files). Show all
Unified · Split
| @@ -1,3 +0,0 @@ | |||
| 1 | # @executor/mcp-elicitation-demo | ||
| 2 | |||
| 3 | ## null | ||
| @@ -1,21 +0,0 @@ | |||
| 1 | { | ||
| 2 | "name": "@executor/mcp-elicitation-demo", | ||
| 3 | "private": true, | ||
| 4 | "type": "module", | ||
| 5 | "exports": { | ||
| 6 | ".": "./src/index.ts" | ||
| 7 | }, | ||
| 8 | "scripts": { | ||
| 9 | "typecheck": "bunx tsc --noEmit -p tsconfig.json", | ||
| 10 | "start": "bun src/main.ts" | ||
| 11 | }, | ||
| 12 | "dependencies": { | ||
| 13 | "@modelcontextprotocol/sdk": "^1.26.0", | ||
| 14 | "zod": "catalog:" | ||
| 15 | }, | ||
| 16 | "devDependencies": { | ||
| 17 | "@types/node": "catalog:", | ||
| 18 | "bun-types": "catalog:" | ||
| 19 | }, | ||
| 20 | "version": "0.0.0" | ||
| 21 | } | ||
| @@ -1,4 +0,0 @@ | |||
| 1 | export { | ||
| 2 | startMcpElicitationDemoServer, | ||
| 3 | type McpElicitationDemoServer, | ||
| 4 | } from "./server"; | ||
| @@ -1,20 +0,0 @@ | |||
| 1 | import { startMcpElicitationDemoServer } from "./server"; | ||
| 2 | |||
| 3 | const host = process.env.HOST ?? "127.0.0.1"; | ||
| 4 | const port = process.env.PORT ? Number(process.env.PORT) : 58506; | ||
| 5 | |||
| 6 | const server = await startMcpElicitationDemoServer({ host, port }); | ||
| 7 | console.error(`mcp-elicitation-demo listening on ${server.endpoint}`); | ||
| 8 | |||
| 9 | const shutdown = async () => { | ||
| 10 | await server.close(); | ||
| 11 | process.exit(0); | ||
| 12 | }; | ||
| 13 | |||
| 14 | process.once("SIGINT", () => { | ||
| 15 | void shutdown(); | ||
| 16 | }); | ||
| 17 | |||
| 18 | process.once("SIGTERM", () => { | ||
| 19 | void shutdown(); | ||
| 20 | }); | ||
| @@ -1,222 +0,0 @@ | |||
| 1 | import { randomUUID } from "node:crypto"; | ||
| 2 | |||
| 3 | import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; | ||
| 4 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; | ||
| 5 | import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; | ||
| 6 | import { z } from "zod/v4"; | ||
| 7 | |||
| 8 | export type McpElicitationDemoServer = { | ||
| 9 | readonly endpoint: string; | ||
| 10 | readonly close: () => Promise<void>; | ||
| 11 | }; | ||
| 12 | |||
| 13 | const registerDemoTools = (server: McpServer) => { | ||
| 14 | server.registerTool( | ||
| 15 | "gated_echo", | ||
| 16 | { | ||
| 17 | description: "Ask for approval before echoing the provided value.", | ||
| 18 | inputSchema: { | ||
| 19 | value: z.string(), | ||
| 20 | }, | ||
| 21 | }, | ||
| 22 | async ({ value }: { value: string }) => { | ||
| 23 | const response = await server.server.elicitInput({ | ||
| 24 | mode: "form", | ||
| 25 | message: `Approve gated echo for ${value}?`, | ||
| 26 | requestedSchema: { | ||
| 27 | type: "object", | ||
| 28 | properties: { | ||
| 29 | approve: { | ||
| 30 | type: "boolean", | ||
| 31 | title: "Approve", | ||
| 32 | }, | ||
| 33 | }, | ||
| 34 | required: ["approve"], | ||
| 35 | }, | ||
| 36 | }); | ||
| 37 | |||
| 38 | if ( | ||
| 39 | response.action !== "accept" | ||
| 40 | || !response.content | ||
| 41 | || response.content.approve !== true | ||
| 42 | ) { | ||
| 43 | return { | ||
| 44 | content: [{ type: "text", text: "denied" }], | ||
| 45 | }; | ||
| 46 | } | ||
| 47 | |||
| 48 | return { | ||
| 49 | content: [{ type: "text", text: `approved:${value}` }], | ||
| 50 | }; | ||
| 51 | }, | ||
| 52 | ); | ||
| 53 | }; | ||
| 54 | |||
| 55 | export const startMcpElicitationDemoServer = async (input: { | ||
| 56 | readonly host?: string; | ||
| 57 | readonly port?: number; | ||
| 58 | } = {}): Promise<McpElicitationDemoServer> => { | ||
| 59 | const host = input.host ?? "127.0.0.1"; | ||
| 60 | const app = createMcpExpressApp({ host }); | ||
| 61 | const transports: Record<string, StreamableHTTPServerTransport> = {}; | ||
| 62 | const servers: Record<string, McpServer> = {}; | ||
| 63 | |||
| 64 | const createServer = () => { | ||
| 65 | const server = new McpServer( | ||
| 66 | { | ||
| 67 | name: "executor-mcp-elicitation-demo", | ||
| 68 | version: "1.0.0", | ||
| 69 | }, | ||
| 70 | { | ||
| 71 | capabilities: { | ||
| 72 | tools: {}, | ||
| 73 | }, | ||
| 74 | }, | ||
| 75 | ); | ||
| 76 | |||
| 77 | registerDemoTools(server); | ||
| 78 | return server; | ||
| 79 | }; | ||
| 80 | |||
| 81 | app.post("/mcp", async (req: any, res: any) => { | ||
| 82 | const sessionIdHeader = req.headers["mcp-session-id"]; | ||
| 83 | const sessionId = | ||
| 84 | typeof sessionIdHeader === "string" | ||
| 85 | ? sessionIdHeader | ||
| 86 | : Array.isArray(sessionIdHeader) | ||
| 87 | ? sessionIdHeader[0] | ||
| 88 | : undefined; | ||
| 89 | |||
| 90 | try { | ||
| 91 | let transport: StreamableHTTPServerTransport; | ||
| 92 | |||
| 93 | if (sessionId && transports[sessionId]) { | ||
| 94 | transport = transports[sessionId]; | ||
| 95 | } else { | ||
| 96 | transport = new StreamableHTTPServerTransport({ | ||
| 97 | sessionIdGenerator: () => randomUUID(), | ||
| 98 | onsessioninitialized: (newSessionId) => { | ||
| 99 | transports[newSessionId] = transport; | ||
| 100 | }, | ||
| 101 | }); | ||
| 102 | |||
| 103 | transport.onclose = () => { | ||
| 104 | const closedSessionId = transport.sessionId; | ||
| 105 | if (closedSessionId && transports[closedSessionId]) { | ||
| 106 | delete transports[closedSessionId]; | ||
| 107 | } | ||
| 108 | if (closedSessionId && servers[closedSessionId]) { | ||
| 109 | void servers[closedSessionId].close().catch(() => undefined); | ||
| 110 | delete servers[closedSessionId]; | ||
| 111 | } | ||
| 112 | }; | ||
| 113 | |||
| 114 | const server = createServer(); | ||
| 115 | await server.connect(transport); | ||
| 116 | const newSessionId = transport.sessionId; | ||
| 117 | if (newSessionId) { | ||
| 118 | servers[newSessionId] = server; | ||
| 119 | } | ||
| 120 | } | ||
| 121 | |||
| 122 | await transport.handleRequest(req, res, req.body); | ||
| 123 | } catch (error) { | ||
| 124 | if (!res.headersSent) { | ||
| 125 | res.status(500).json({ | ||
| 126 | jsonrpc: "2.0", | ||
| 127 | error: { | ||
| 128 | code: -32603, | ||
| 129 | message: error instanceof Error ? error.message : "Internal server error", | ||
| 130 | }, | ||
| 131 | id: null, | ||
| 132 | }); | ||
| 133 | } | ||
| 134 | } | ||
| 135 | }); | ||
| 136 | |||
| 137 | app.get("/mcp", async (req: any, res: any) => { | ||
| 138 | const sessionIdHeader = req.headers["mcp-session-id"]; | ||
| 139 | const sessionId = | ||
| 140 | typeof sessionIdHeader === "string" | ||
| 141 | ? sessionIdHeader | ||
| 142 | : Array.isArray(sessionIdHeader) | ||
| 143 | ? sessionIdHeader[0] | ||
| 144 | : undefined; | ||
| 145 | |||
| 146 | if (!sessionId || !transports[sessionId]) { | ||
| 147 | res.status(400).send("Invalid or missing session ID"); | ||
| 148 | return; | ||
| 149 | } | ||
| 150 | |||
| 151 | await transports[sessionId].handleRequest(req, res); | ||
| 152 | }); | ||
| 153 | |||
| 154 | app.delete("/mcp", async (req: any, res: any) => { | ||
| 155 | const sessionIdHeader = req.headers["mcp-session-id"]; | ||
| 156 | const sessionId = | ||
| 157 | typeof sessionIdHeader === "string" | ||
| 158 | ? sessionIdHeader | ||
| 159 | : Array.isArray(sessionIdHeader) | ||
| 160 | ? sessionIdHeader[0] | ||
| 161 | : undefined; | ||
| 162 | |||
| 163 | if (!sessionId || !transports[sessionId]) { | ||
| 164 | res.status(400).send("Invalid or missing session ID"); | ||
| 165 | return; | ||
| 166 | } | ||
| 167 | |||
| 168 | const transport = transports[sessionId]; | ||
| 169 | await transport.handleRequest(req, res, req.body); | ||
| 170 | await transport.close(); | ||
| 171 | delete transports[sessionId]; | ||
| 172 | |||
| 173 | if (servers[sessionId]) { | ||
| 174 | await servers[sessionId].close().catch(() => undefined); | ||
| 175 | delete servers[sessionId]; | ||
| 176 | } | ||
| 177 | }); | ||
| 178 | |||
| 179 | const listener = await new Promise<import("node:http").Server>((resolve, reject) => { | ||
| 180 | const server = app.listen(input.port ?? 0, host); | ||
| 181 | |||
| 182 | const onListening = () => { | ||
| 183 | server.off("error", onError); | ||
| 184 | resolve(server); | ||
| 185 | }; | ||
| 186 | |||
| 187 | const onError = (error: Error) => { | ||
| 188 | server.off("listening", onListening); | ||
| 189 | reject(error); | ||
| 190 | }; | ||
| 191 | |||
| 192 | server.once("listening", onListening); | ||
| 193 | server.once("error", onError); | ||
| 194 | }); | ||
| 195 | |||
| 196 | const address = listener.address(); | ||
| 197 | if (!address || typeof address === "string") { | ||
| 198 | if (listener.listening) { | ||
| 199 | await new Promise<void>((resolve, reject) => { | ||
| 200 | listener.close((error) => (error ? reject(error) : resolve())); | ||
| 201 | }); | ||
| 202 | } | ||
| 203 | throw new Error("Failed to resolve MCP elicitation demo server address"); | ||
| 204 | } | ||
| 205 | |||
| 206 | return { | ||
| 207 | endpoint: `http://${host}:${address.port}/mcp`, | ||
| 208 | close: async () => { | ||
| 209 | for (const transport of Object.values(transports)) { | ||
| 210 | await transport.close().catch(() => undefined); | ||
| 211 | } | ||
| 212 | |||
| 213 | for (const server of Object.values(servers)) { | ||
| 214 | await server.close().catch(() => undefined); | ||
| 215 | } | ||
| 216 | |||
| 217 | await new Promise<void>((resolve, reject) => { | ||
| 218 | listener.close((error) => (error ? reject(error) : resolve())); | ||
| 219 | }); | ||
| 220 | }, | ||
| 221 | }; | ||
| 222 | }; | ||
| @@ -1,15 +0,0 @@ | |||
| 1 | { | ||
| 2 | "compilerOptions": { | ||
| 3 | "target": "ES2022", | ||
| 4 | "module": "ESNext", | ||
| 5 | "moduleResolution": "Bundler", | ||
| 6 | "strict": true, | ||
| 7 | "skipLibCheck": true, | ||
| 8 | "noEmit": true, | ||
| 9 | "lib": ["ES2022"], | ||
| 10 | "types": ["bun-types", "node"] | ||
| 11 | }, | ||
| 12 | "include": [ | ||
| 13 | "src/**/*.ts" | ||
| 14 | ] | ||
| 15 | } | ||
| @@ -1,3 +0,0 @@ | |||
| 1 | # @executor/serve-skills-via-mcp-demo | ||
| 2 | |||
| 3 | ## null | ||
| @@ -1,38 +0,0 @@ | |||
| 1 | # Serve Skills Via MCP | ||
| 2 | |||
| 3 | This demo shows one practical way to distribute [Agent Skills](https://agentskills.io) | ||
| 4 | over MCP resources. | ||
| 5 | |||
| 6 | It exposes: | ||
| 7 | |||
| 8 | - a catalog resource that lists available skill bundles | ||
| 9 | - one manifest resource per skill bundle | ||
| 10 | - one direct `SKILL.md` resource per bundle | ||
| 11 | - a `skill://demo/{skill}/{version}/{+path}` resource template for bundled files | ||
| 12 | |||
| 13 | The important boundary is that MCP resources only transport the bundle. The host still | ||
| 14 | owns activation logic: | ||
| 15 | |||
| 16 | 1. list resources | ||
| 17 | 2. read the catalog or a manifest | ||
| 18 | 3. choose a skill | ||
| 19 | 4. read that skill's `SKILL.md` | ||
| 20 | 5. fetch additional files from the manifest on demand | ||
| 21 | |||
| 22 | ## Run | ||
| 23 | |||
| 24 | ```bash | ||
| 25 | bun run --filter @executor/serve-skills-via-mcp-demo start | ||
| 26 | ``` | ||
| 27 | |||
| 28 | The server listens on `http://127.0.0.1:<port>/mcp` and logs the final endpoint to | ||
| 29 | stderr. | ||
| 30 | |||
| 31 | ## Smoke Test | ||
| 32 | |||
| 33 | ```bash | ||
| 34 | bun run --filter @executor/serve-skills-via-mcp-demo smoke | ||
| 35 | ``` | ||
| 36 | |||
| 37 | The smoke script starts the server, connects with an MCP client, lists resources, | ||
| 38 | reads a skill manifest, then reads `SKILL.md` and one support file. | ||
| @@ -1,21 +0,0 @@ | |||
| 1 | { | ||
| 2 | "name": "@executor/serve-skills-via-mcp-demo", | ||
| 3 | "private": true, | ||
| 4 | "type": "module", | ||
| 5 | "exports": { | ||
| 6 | ".": "./src/index.ts" | ||
| 7 | }, | ||
| 8 | "scripts": { | ||
| 9 | "typecheck": "bunx tsc --noEmit -p tsconfig.json", | ||
| 10 | "start": "bun src/main.ts", | ||
| 11 | "smoke": "bun src/smoke.ts" | ||
| 12 | }, | ||
| 13 | "dependencies": { | ||
| 14 | "@modelcontextprotocol/sdk": "^1.26.0" | ||
| 15 | }, | ||
| 16 | "devDependencies": { | ||
| 17 | "@types/node": "catalog:", | ||
| 18 | "bun-types": "catalog:" | ||
| 19 | }, | ||
| 20 | "version": "0.0.0" | ||
| 21 | } | ||
| @@ -1,28 +0,0 @@ | |||
| 1 | --- | ||
| 2 | name: postgres-incident-triage | ||
| 3 | description: Triage production PostgreSQL incidents involving lock contention, slow queries, or saturation. Use when the user asks for database incident diagnosis or immediate next steps. | ||
| 4 | compatibility: Assumes SQL access to pg_stat_activity and pg_locks. | ||
| 5 | allowed-tools: Bash(psql:*) Read | ||
| 6 | --- | ||
| 7 | |||
| 8 | # Postgres Incident Triage | ||
| 9 | |||
| 10 | ## When to use this skill | ||
| 11 | |||
| 12 | Use this skill for live operational diagnosis of PostgreSQL incidents, especially when | ||
| 13 | latency spikes or blocked sessions appear. | ||
| 14 | |||
| 15 | ## Workflow | ||
| 16 | |||
| 17 | 1. Read `references/error-codes.md` for common failure patterns. | ||
| 18 | 2. Use `scripts/blocked-session-query.sql` to inspect blockers and waiters. | ||
| 19 | 3. Separate symptoms from root cause: CPU saturation, I/O pressure, lock chains, or | ||
| 20 | connection pileups. | ||
| 21 | 4. Recommend the least-destructive mitigation first. | ||
| 22 | 5. If proposing session termination, identify the blocker and expected blast radius. | ||
| 23 | |||
| 24 | ## Output rules | ||
| 25 | |||
| 26 | - Summarize the current condition in one paragraph. | ||
| 27 | - List the most likely root causes in order. | ||
| 28 | - Provide immediate next commands before longer-term remediation. | ||
| @@ -1,6 +0,0 @@ | |||
| 1 | # Common PostgreSQL Incident Signals | ||
| 2 | |||
| 3 | - `55P03` often points to lock contention or explicit lock timeouts. | ||
| 4 | - `53300` indicates too many connections. | ||
| 5 | - Long idle-in-transaction sessions are frequently the hidden blocker behind lock trees. | ||
| 6 | - A high count of active queries is not enough to prove saturation; check wait events. | ||
| @@ -1,26 +0,0 @@ | |||
| 1 | select | ||
| 2 | blocked.pid as blocked_pid, | ||
| 3 | blocked.query as blocked_query, | ||
| 4 | blocking.pid as blocking_pid, | ||
| 5 | blocking.query as blocking_query, | ||
| 6 | blocked.wait_event_type, | ||
| 7 | blocked.wait_event | ||
| 8 | from pg_stat_activity blocked | ||
| 9 | join pg_locks blocked_locks | ||
| 10 | on blocked.pid = blocked_locks.pid | ||
| 11 | join pg_locks blocking_locks | ||
| 12 | on blocked_locks.locktype = blocking_locks.locktype | ||
| 13 | and blocked_locks.database is not distinct from blocking_locks.database | ||
| 14 | and blocked_locks.relation is not distinct from blocking_locks.relation | ||
| 15 | and blocked_locks.page is not distinct from blocking_locks.page | ||
| 16 | and blocked_locks.tuple is not distinct from blocking_locks.tuple | ||
| 17 | and blocked_locks.virtualxid is not distinct from blocking_locks.virtualxid | ||
| 18 | and blocked_locks.transactionid is not distinct from blocking_locks.transactionid | ||
| 19 | and blocked_locks.classid is not distinct from blocking_locks.classid | ||
| 20 | and blocked_locks.objid is not distinct from blocking_locks.objid | ||
| 21 | and blocked_locks.objsubid is not distinct from blocking_locks.objsubid | ||
| 22 | join pg_stat_activity blocking | ||
| 23 | on blocking.pid = blocking_locks.pid | ||
| 24 | where not blocked_locks.granted | ||
| 25 | and blocking_locks.granted | ||
| 26 | and blocked.pid <> blocking.pid; | ||
| @@ -1,28 +0,0 @@ | |||
| 1 | --- | ||
| 2 | name: release-notes-writer | ||
| 3 | description: Draft product release notes from commits, changelogs, and issue summaries. Use when the user asks for release notes, launch notes, or upgrade summaries. | ||
| 4 | compatibility: Requires markdown output and access to bundled templates. | ||
| 5 | allowed-tools: Read Write | ||
| 6 | --- | ||
| 7 | |||
| 8 | # Release Notes Writer | ||
| 9 | |||
| 10 | ## When to use this skill | ||
| 11 | |||
| 12 | Use this skill when the user wants polished release notes, changelog summaries, or | ||
| 13 | customer-facing upgrade notes. | ||
| 14 | |||
| 15 | ## Workflow | ||
| 16 | |||
| 17 | 1. Read the release template in `assets/release-template.md`. | ||
| 18 | 2. Read the voice and structure rules in `references/style-guide.md`. | ||
| 19 | 3. Group changes into user-facing themes instead of commit-by-commit narration. | ||
| 20 | 4. Prefer concrete impact statements over internal implementation details. | ||
| 21 | 5. Call out breaking changes, migrations, and feature flags explicitly. | ||
| 22 | |||
| 23 | ## Output rules | ||
| 24 | |||
| 25 | - Start with a short overview paragraph. | ||
| 26 | - Include sections only for categories that actually changed. | ||
| 27 | - End with a short rollout or migration note when relevant. | ||
| 28 | - Keep internal ticket IDs out of the final draft unless the user asked for them. | ||
| @@ -1,16 +0,0 @@ | |||
| 1 | # [Release Name] | ||
| 2 | |||
| 3 | ## Overview | ||
| 4 | [Two to four sentences describing the release in user language.] | ||
| 5 | |||
| 6 | ## New | ||
| 7 | - [New capability] | ||
| 8 | |||
| 9 | ## Improved | ||
| 10 | - [Improvement] | ||
| 11 | |||
| 12 | ## Fixed | ||
| 13 | - [Bug fix] | ||
| 14 | |||
| 15 | ## Upgrade Notes | ||
| 16 | - [Migration or rollout note if needed] | ||
| @@ -1,7 +0,0 @@ | |||
| 1 | # Release Notes Style Guide | ||
| 2 | |||
| 3 | - Write for product users first, operators second, engineers last. | ||
| 4 | - Lead with outcomes: faster setup, clearer workflows, lower failure rates. | ||
| 5 | - Replace vague claims like "enhanced performance" with specific behavior changes. | ||
| 6 | - Mention removals and breaking changes directly instead of hiding them in "Improved". | ||
| 7 | - Keep bullets parallel and concrete. | ||
| @@ -1,10 +0,0 @@ | |||
| 1 | const requiredHeadings = [ | ||
| 2 | "## Overview", | ||
| 3 | "## New", | ||
| 4 | "## Improved", | ||
| 5 | "## Fixed", | ||
| 6 | ]; | ||
| 7 | |||
| 8 | for (const heading of requiredHeadings) { | ||
| 9 | console.log(`Expect heading: ${heading}`); | ||
| 10 | } | ||
| @@ -1,243 +0,0 @@ | |||
| 1 | import { readdir, readFile, stat } from "node:fs/promises"; | ||
| 2 | import { extname, join, posix, relative } from "node:path"; | ||
| 3 | import { fileURLToPath } from "node:url"; | ||
| 4 | |||
| 5 | export type SkillFrontmatter = { | ||
| 6 | readonly name: string; | ||
| 7 | readonly description: string; | ||
| 8 | readonly compatibility?: string; | ||
| 9 | readonly license?: string; | ||
| 10 | readonly allowedTools?: string; | ||
| 11 | }; | ||
| 12 | |||
| 13 | export type DistributedSkillFile = { | ||
| 14 | readonly path: string; | ||
| 15 | readonly uri: string; | ||
| 16 | readonly mimeType: string; | ||
| 17 | readonly size: number; | ||
| 18 | readonly role: "instructions" | "script" | "reference" | "asset" | "other"; | ||
| 19 | }; | ||
| 20 | |||
| 21 | export type SkillManifest = { | ||
| 22 | readonly kind: "agent-skill-distribution"; | ||
| 23 | readonly publisher: string; | ||
| 24 | readonly version: string; | ||
| 25 | readonly skill: SkillFrontmatter; | ||
| 26 | readonly instructionsUri: string; | ||
| 27 | readonly manifestUri: string; | ||
| 28 | readonly fileTemplate: string; | ||
| 29 | readonly files: readonly DistributedSkillFile[]; | ||
| 30 | }; | ||
| 31 | |||
| 32 | export type DistributedSkillBundle = { | ||
| 33 | readonly publisher: string; | ||
| 34 | readonly version: string; | ||
| 35 | readonly rootDir: string; | ||
| 36 | readonly skill: SkillFrontmatter; | ||
| 37 | readonly manifestUri: string; | ||
| 38 | readonly instructionsUri: string; | ||
| 39 | readonly fileTemplate: string; | ||
| 40 | readonly files: readonly DistributedSkillFile[]; | ||
| 41 | readonly manifest: SkillManifest; | ||
| 42 | }; | ||
| 43 | |||
| 44 | const demoPublisher = "demo"; | ||
| 45 | const catalogUri = "skill://catalog/index.json"; | ||
| 46 | const fileTemplate = "skill://demo/{skill}/{version}/{+path}"; | ||
| 47 | |||
| 48 | const bundleVersions = { | ||
| 49 | "postgres-incident-triage": "1.1.0", | ||
| 50 | "release-notes-writer": "1.0.0", | ||
| 51 | } as const; | ||
| 52 | |||
| 53 | const skillsDir = fileURLToPath(new URL("../skills", import.meta.url)); | ||
| 54 | |||
| 55 | const mimeTypeFor = (filePath: string): string => { | ||
| 56 | switch (extname(filePath)) { | ||
| 57 | case ".json": | ||
| 58 | return "application/json"; | ||
| 59 | case ".md": | ||
| 60 | return "text/markdown"; | ||
| 61 | case ".sql": | ||
| 62 | return "application/sql"; | ||
| 63 | case ".ts": | ||
| 64 | return "text/typescript"; | ||
| 65 | default: | ||
| 66 | return "text/plain"; | ||
| 67 | } | ||
| 68 | }; | ||
| 69 | |||
| 70 | const roleFor = (filePath: string): DistributedSkillFile["role"] => { | ||
| 71 | if (filePath === "SKILL.md") { | ||
| 72 | return "instructions"; | ||
| 73 | } | ||
| 74 | if (filePath.startsWith("scripts/")) { | ||
| 75 | return "script"; | ||
| 76 | } | ||
| 77 | if (filePath.startsWith("references/")) { | ||
| 78 | return "reference"; | ||
| 79 | } | ||
| 80 | if (filePath.startsWith("assets/")) { | ||
| 81 | return "asset"; | ||
| 82 | } | ||
| 83 | return "other"; | ||
| 84 | }; | ||
| 85 | |||
| 86 | const parseFrontmatter = (content: string): SkillFrontmatter => { | ||
| 87 | const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/u.exec(content); | ||
| 88 | if (!match) { | ||
| 89 | throw new Error("Expected SKILL.md to start with YAML frontmatter"); | ||
| 90 | } | ||
| 91 | |||
| 92 | const fields: Record<string, string> = {}; | ||
| 93 | for (const line of match[1].split(/\r?\n/u)) { | ||
| 94 | const trimmed = line.trim(); | ||
| 95 | if (trimmed.length === 0 || trimmed.startsWith("#")) { | ||
| 96 | continue; | ||
| 97 | } | ||
| 98 | |||
| 99 | const entry = /^([A-Za-z0-9-]+):\s*(.+)$/u.exec(trimmed); | ||
| 100 | if (!entry) { | ||
| 101 | continue; | ||
| 102 | } | ||
| 103 | |||
| 104 | fields[entry[1]] = entry[2]; | ||
| 105 | } | ||
| 106 | |||
| 107 | if (!fields.name || !fields.description) { | ||
| 108 | throw new Error("Expected SKILL.md frontmatter to include name and description"); | ||
| 109 | } | ||
| 110 | |||
| 111 | return { | ||
| 112 | name: fields.name, | ||
| 113 | description: fields.description, | ||
| 114 | compatibility: fields.compatibility, | ||
| 115 | license: fields.license, | ||
| 116 | allowedTools: fields["allowed-tools"], | ||
| 117 | }; | ||
| 118 | }; | ||
| 119 | |||
| 120 | const collectFiles = async (rootDir: string): Promise<string[]> => { | ||
| 121 | const output: string[] = []; | ||
| 122 | |||
| 123 | const visit = async (directory: string) => { | ||
| 124 | const entries = await readdir(directory, { withFileTypes: true }); | ||
| 125 | entries.sort((left, right) => left.name.localeCompare(right.name)); | ||
| 126 | |||
| 127 | for (const entry of entries) { | ||
| 128 | const absolutePath = join(directory, entry.name); | ||
| 129 | if (entry.isDirectory()) { | ||
| 130 | await visit(absolutePath); | ||
| 131 | continue; | ||
| 132 | } | ||
| 133 | output.push(posix.join(...relative(rootDir, absolutePath).split("\\").filter(Boolean))); | ||
| 134 | } | ||
| 135 | }; | ||
| 136 | |||
| 137 | await visit(rootDir); | ||
| 138 | return output; | ||
| 139 | }; | ||
| 140 | |||
| 141 | export const getCatalogUri = (): string => catalogUri; | ||
| 142 | |||
| 143 | export const getFileTemplate = (): string => fileTemplate; | ||
| 144 | |||
| 145 | export const loadDistributedSkillBundles = async (): Promise<readonly DistributedSkillBundle[]> => { | ||
| 146 | const bundles = await Promise.all( | ||
| 147 | Object.entries(bundleVersions).map(async ([directoryName, version]) => { | ||
| 148 | const rootDir = join(skillsDir, directoryName); | ||
| 149 | const skillContent = await readFile(join(rootDir, "SKILL.md"), "utf8"); | ||
| 150 | const skill = parseFrontmatter(skillContent); | ||
| 151 | const files = await collectFiles(rootDir); | ||
| 152 | |||
| 153 | const distributedFiles = await Promise.all( | ||
| 154 | files.map(async (filePath) => { | ||
| 155 | const absolutePath = join(rootDir, filePath); | ||
| 156 | const fileStats = await stat(absolutePath); | ||
| 157 | return { | ||
| 158 | path: filePath, | ||
| 159 | uri: `skill://${demoPublisher}/${skill.name}/${version}/${filePath}`, | ||
| 160 | mimeType: mimeTypeFor(filePath), | ||
| 161 | size: fileStats.size, | ||
| 162 | role: roleFor(filePath), | ||
| 163 | } satisfies DistributedSkillFile; | ||
| 164 | }), | ||
| 165 | ); | ||
| 166 | |||
| 167 | const manifestUri = `skill://${demoPublisher}/${skill.name}/${version}/manifest.json`; | ||
| 168 | const instructionsUri = `skill://${demoPublisher}/${skill.name}/${version}/SKILL.md`; | ||
| 169 | |||
| 170 | const manifest: SkillManifest = { | ||
| 171 | kind: "agent-skill-distribution", | ||
| 172 | publisher: demoPublisher, | ||
| 173 | version, | ||
| 174 | skill, | ||
| 175 | instructionsUri, | ||
| 176 | manifestUri, | ||
| 177 | fileTemplate, | ||
| 178 | files: distributedFiles, | ||
| 179 | }; | ||
| 180 | |||
| 181 | return { | ||
| 182 | publisher: demoPublisher, | ||
| 183 | version, | ||
| 184 | rootDir, | ||
| 185 | skill, | ||
| 186 | manifestUri, | ||
| 187 | instructionsUri, | ||
| 188 | fileTemplate, | ||
| 189 | files: distributedFiles, | ||
| 190 | manifest, | ||
| 191 | } satisfies DistributedSkillBundle; | ||
| 192 | }), | ||
| 193 | ); | ||
| 194 | |||
| 195 | return bundles.sort((left, right) => left.skill.name.localeCompare(right.skill.name)); | ||
| 196 | }; | ||
| 197 | |||
| 198 | export const renderCatalog = (bundles: readonly DistributedSkillBundle[]): string => | ||
| 199 | JSON.stringify( | ||
| 200 | { | ||
| 201 | kind: "agent-skill-catalog", | ||
| 202 | distributionMethod: "mcp-resources", | ||
| 203 | catalogUri, | ||
| 204 | fileTemplate, | ||
| 205 | activationGuidance: [ | ||
| 206 | "Use this catalog for discovery only.", | ||
| 207 | "Read a skill manifest next to find the concrete SKILL.md URI.", | ||
| 208 | "Load SKILL.md into the model only when the task matches the skill description.", | ||
| 209 | "Fetch support files from the manifest on demand.", | ||
| 210 | ], | ||
| 211 | skills: bundles.map((bundle) => ({ | ||
| 212 | name: bundle.skill.name, | ||
| 213 | description: bundle.skill.description, | ||
| 214 | version: bundle.version, | ||
| 215 | manifestUri: bundle.manifestUri, | ||
| 216 | instructionsUri: bundle.instructionsUri, | ||
| 217 | })), | ||
| 218 | }, | ||
| 219 | null, | ||
| 220 | 2, | ||
| 221 | ); | ||
| 222 | |||
| 223 | export const findBundleByName = ( | ||
| 224 | bundles: readonly DistributedSkillBundle[], | ||
| 225 | skillName: string, | ||
| 226 | ): DistributedSkillBundle | undefined => | ||
| 227 | bundles.find((bundle) => bundle.skill.name === skillName); | ||
| 228 | |||
| 229 | export const findBundleFile = ( | ||
| 230 | bundles: readonly DistributedSkillBundle[], | ||
| 231 | skillName: string, | ||
| 232 | version: string, | ||
| 233 | filePath: string, | ||
| 234 | ): DistributedSkillFile | undefined => | ||
| 235 | bundles | ||
| 236 | .find((bundle) => bundle.skill.name === skillName && bundle.version === version) | ||
| 237 | ?.files.find((file) => file.path === filePath); | ||
| 238 | |||
| 239 | export const readBundleFileText = async ( | ||
| 240 | bundle: DistributedSkillBundle, | ||
| 241 | filePath: string, | ||
| 242 | ): Promise<string> => | ||
| 243 | readFile(join(bundle.rootDir, filePath), "utf8"); | ||
| @@ -1,4 +0,0 @@ | |||
| 1 | export { | ||
| 2 | startServeSkillsViaMcpDemoServer, | ||
| 3 | type ServeSkillsViaMcpDemoServer, | ||
| 4 | } from "./server"; | ||
| @@ -1,20 +0,0 @@ | |||
| 1 | import { startServeSkillsViaMcpDemoServer } from "./server"; | ||
| 2 | |||
| 3 | const host = process.env.HOST ?? "127.0.0.1"; | ||
| 4 | const port = process.env.PORT ? Number(process.env.PORT) : 58507; | ||
| 5 | |||
| 6 | const server = await startServeSkillsViaMcpDemoServer({ host, port }); | ||
| 7 | console.error(`serve-skills-via-mcp listening on ${server.endpoint}`); | ||
| 8 | |||
| 9 | const shutdown = async () => { | ||
| 10 | await server.close(); | ||
| 11 | process.exit(0); | ||
| 12 | }; | ||
| 13 | |||
| 14 | process.once("SIGINT", () => { | ||
| 15 | void shutdown(); | ||
| 16 | }); | ||
| 17 | |||
| 18 | process.once("SIGTERM", () => { | ||
| 19 | void shutdown(); | ||
| 20 | }); | ||
| @@ -1,346 +0,0 @@ | |||
| 1 | import { randomUUID } from "node:crypto"; | ||
| 2 | import type { Server as HttpServer } from "node:http"; | ||
| 3 | |||
| 4 | import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; | ||
| 5 | import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; | ||
| 6 | import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; | ||
| 7 | |||
| 8 | import { | ||
| 9 | type DistributedSkillBundle, | ||
| 10 | findBundleByName, | ||
| 11 | findBundleFile, | ||
| 12 | getCatalogUri, | ||
| 13 | getFileTemplate, | ||
| 14 | loadDistributedSkillBundles, | ||
| 15 | readBundleFileText, | ||
| 16 | renderCatalog, | ||
| 17 | } from "./catalog"; | ||
| 18 | |||
| 19 | export type ServeSkillsViaMcpDemoServer = { | ||
| 20 | readonly endpoint: string; | ||
| 21 | readonly close: () => Promise<void>; | ||
| 22 | }; | ||
| 23 | |||
| 24 | const registerSkillResources = ( | ||
| 25 | server: McpServer, | ||
| 26 | bundles: readonly DistributedSkillBundle[], | ||
| 27 | ) => { | ||
| 28 | server.registerResource( | ||
| 29 | "skill-catalog", | ||
| 30 | getCatalogUri(), | ||
| 31 | { | ||
| 32 | title: "Distributed Agent Skills Catalog", | ||
| 33 | description: "Catalog of skill bundles exposed over MCP resources.", | ||
| 34 | mimeType: "application/json", | ||
| 35 | annotations: { | ||
| 36 | audience: ["assistant", "user"], | ||
| 37 | priority: 0.7, | ||
| 38 | }, | ||
| 39 | }, | ||
| 40 | async () => ({ | ||
| 41 | contents: [ | ||
| 42 | { | ||
| 43 | uri: getCatalogUri(), | ||
| 44 | mimeType: "application/json", | ||
| 45 | text: renderCatalog(bundles), | ||
| 46 | }, | ||
| 47 | ], | ||
| 48 | }), | ||
| 49 | ); | ||
| 50 | |||
| 51 | for (const bundle of bundles) { | ||
| 52 | server.registerResource( | ||
| 53 | `${bundle.skill.name}-manifest`, | ||
| 54 | bundle.manifestUri, | ||
| 55 | { | ||
| 56 | title: `${bundle.skill.name} manifest`, | ||
| 57 | description: `Distribution manifest for ${bundle.skill.name}.`, | ||
| 58 | mimeType: "application/json", | ||
| 59 | annotations: { | ||
| 60 | audience: ["assistant"], | ||
| 61 | priority: 0.85, | ||
| 62 | }, | ||
| 63 | }, | ||
| 64 | async () => ({ | ||
| 65 | contents: [ | ||
| 66 | { | ||
| 67 | uri: bundle.manifestUri, | ||
| 68 | mimeType: "application/json", | ||
| 69 | text: JSON.stringify(bundle.manifest, null, 2), | ||
| 70 | }, | ||
| 71 | ], | ||
| 72 | }), | ||
| 73 | ); | ||
| 74 | |||
| 75 | server.registerResource( | ||
| 76 | `${bundle.skill.name}-instructions`, | ||
| 77 | bundle.instructionsUri, | ||
| 78 | { | ||
| 79 | title: `${bundle.skill.name} SKILL.md`, | ||
| 80 | description: bundle.skill.description, | ||
| 81 | mimeType: "text/markdown", | ||
| 82 | annotations: { | ||
| 83 | audience: ["assistant"], | ||
| 84 | priority: 1, | ||
| 85 | }, | ||
| 86 | }, | ||
| 87 | async () => ({ | ||
| 88 | contents: [ | ||
| 89 | { | ||
| 90 | uri: bundle.instructionsUri, | ||
| 91 | mimeType: "text/markdown", | ||
| 92 | text: await readBundleFileText(bundle, "SKILL.md"), | ||
| 93 | }, | ||
| 94 | ], | ||
| 95 | }), | ||
| 96 | ); | ||
| 97 | } | ||
| 98 | |||
| 99 | server.registerResource( | ||
| 100 | "skill-bundle-file", | ||
| 101 | new ResourceTemplate(getFileTemplate(), { | ||
| 102 | list: undefined, | ||
| 103 | complete: { | ||
| 104 | skill: (value) => | ||
| 105 | bundles | ||
| 106 | .map((bundle) => bundle.skill.name) | ||
| 107 | .filter((skillName) => skillName.startsWith(value)), | ||
| 108 | version: (value, context) => { | ||
| 109 | const skillName = context?.arguments?.skill; | ||
| 110 | if (!skillName) { | ||
| 111 | return bundles | ||
| 112 | .map((bundle) => bundle.version) | ||
| 113 | .filter((version) => version.startsWith(value)); | ||
| 114 | } | ||
| 115 | |||
| 116 | const bundle = findBundleByName(bundles, skillName); | ||
| 117 | if (!bundle) { | ||
| 118 | return []; | ||
| 119 | } | ||
| 120 | |||
| 121 | return bundle.version.startsWith(value) ? [bundle.version] : []; | ||
| 122 | }, | ||
| 123 | path: (value, context) => { | ||
| 124 | const skillName = context?.arguments?.skill; | ||
| 125 | const version = context?.arguments?.version; | ||
| 126 | if (!skillName || !version) { | ||
| 127 | return []; | ||
| 128 | } | ||
| 129 | |||
| 130 | const bundle = bundles.find((entry) => | ||
| 131 | entry.skill.name === skillName && entry.version === version | ||
| 132 | ); | ||
| 133 | if (!bundle) { | ||
| 134 | return []; | ||
| 135 | } | ||
| 136 | |||
| 137 | return bundle.files | ||
| 138 | .map((file) => file.path) | ||
| 139 | .filter((filePath) => filePath.startsWith(value)); | ||
| 140 | }, | ||
| 141 | }, | ||
| 142 | }), | ||
| 143 | { | ||
| 144 | title: "Skill bundle files", | ||
| 145 | description: "Read any file inside a distributed skill bundle.", | ||
| 146 | mimeType: "text/plain", | ||
| 147 | annotations: { | ||
| 148 | audience: ["assistant"], | ||
| 149 | priority: 0.5, | ||
| 150 | }, | ||
| 151 | }, | ||
| 152 | async (_uri, variables) => { | ||
| 153 | const skillName = typeof variables.skill === "string" ? variables.skill : undefined; | ||
| 154 | const version = typeof variables.version === "string" ? variables.version : undefined; | ||
| 155 | const filePath = typeof variables.path === "string" ? variables.path : undefined; | ||
| 156 | |||
| 157 | if (!skillName || !version || !filePath) { | ||
| 158 | throw new Error("Expected skill, version, and path variables"); | ||
| 159 | } | ||
| 160 | |||
| 161 | const bundle = bundles.find((entry) => | ||
| 162 | entry.skill.name === skillName && entry.version === version | ||
| 163 | ); | ||
| 164 | const file = findBundleFile(bundles, skillName, version, filePath); | ||
| 165 | if (!bundle || !file) { | ||
| 166 | throw new Error(`Unknown distributed skill file: ${skillName}@${version}/${filePath}`); | ||
| 167 | } | ||
| 168 | |||
| 169 | return { | ||
| 170 | contents: [ | ||
| 171 | { | ||
| 172 | uri: file.uri, | ||
| 173 | mimeType: file.mimeType, | ||
| 174 | text: await readBundleFileText(bundle, filePath), | ||
| 175 | }, | ||
| 176 | ], | ||
| 177 | }; | ||
| 178 | }, | ||
| 179 | ); | ||
| 180 | }; | ||
| 181 | |||
| 182 | const createSessionServer = (bundles: readonly DistributedSkillBundle[]): McpServer => { | ||
| 183 | const server = new McpServer( | ||
| 184 | { | ||
| 185 | name: "executor-serve-skills-via-mcp-demo", | ||
| 186 | version: "1.0.0", | ||
| 187 | }, | ||
| 188 | { | ||
| 189 | capabilities: { | ||
| 190 | resources: { | ||
| 191 | listChanged: true, | ||
| 192 | }, | ||
| 193 | }, | ||
| 194 | instructions: [ | ||
| 195 | "This server demonstrates one way to distribute Agent Skills over MCP resources.", | ||
| 196 | "Read the catalog and a skill manifest first, then load SKILL.md only when you decide to activate that skill.", | ||
| 197 | ].join(" "), | ||
| 198 | }, | ||
| 199 | ); | ||
| 200 | |||
| 201 | registerSkillResources(server, bundles); | ||
| 202 | return server; | ||
| 203 | }; | ||
| 204 | |||
| 205 | const resolveSessionId = (headers: Record<string, string | string[] | undefined>): string | undefined => { | ||
| 206 | const value = headers["mcp-session-id"]; | ||
| 207 | if (typeof value === "string") { | ||
| 208 | return value; | ||
| 209 | } | ||
| 210 | if (Array.isArray(value)) { | ||
| 211 | return value[0]; | ||
| 212 | } | ||
| 213 | return undefined; | ||
| 214 | }; | ||
| 215 | |||
| 216 | const listen = async (app: ReturnType<typeof createMcpExpressApp>, port: number, host: string) => | ||
| 217 | new Promise<HttpServer>((resolve, reject) => { | ||
| 218 | const listener = app.listen(port, host); | ||
| 219 | |||
| 220 | const onListening = () => { | ||
| 221 | listener.off("error", onError); | ||
| 222 | resolve(listener); | ||
| 223 | }; | ||
| 224 | |||
| 225 | const onError = (error: Error) => { | ||
| 226 | listener.off("listening", onListening); | ||
| 227 | reject(error); | ||
| 228 | }; | ||
| 229 | |||
| 230 | listener.once("listening", onListening); | ||
| 231 | listener.once("error", onError); | ||
| 232 | }); | ||
| 233 | |||
| 234 | export const startServeSkillsViaMcpDemoServer = async (input: { | ||
| 235 | readonly host?: string; | ||
| 236 | readonly port?: number; | ||
| 237 | } = {}): Promise<ServeSkillsViaMcpDemoServer> => { | ||
| 238 | const host = input.host ?? "127.0.0.1"; | ||
| 239 | const bundles = await loadDistributedSkillBundles(); | ||
| 240 | const app = createMcpExpressApp({ host }); | ||
| 241 | const transports: Record<string, StreamableHTTPServerTransport> = {}; | ||
| 242 | const servers: Record<string, McpServer> = {}; | ||
| 243 | |||
| 244 | app.post("/mcp", async (req: any, res: any) => { | ||
| 245 | const sessionId = resolveSessionId(req.headers); | ||
| 246 | |||
| 247 | try { | ||
| 248 | let transport: StreamableHTTPServerTransport; | ||
| 249 | |||
| 250 | if (sessionId && transports[sessionId]) { | ||
| 251 | transport = transports[sessionId]; | ||
| 252 | } else { | ||
| 253 | transport = new StreamableHTTPServerTransport({ | ||
| 254 | sessionIdGenerator: () => randomUUID(), | ||
| 255 | onsessioninitialized: (newSessionId) => { | ||
| 256 | transports[newSessionId] = transport; | ||
| 257 | }, | ||
| 258 | }); | ||
| 259 | |||
| 260 | transport.onclose = () => { | ||
| 261 | const closedSessionId = transport.sessionId; | ||
| 262 | if (closedSessionId && transports[closedSessionId]) { | ||
| 263 | delete transports[closedSessionId]; | ||
| 264 | } | ||
| 265 | if (closedSessionId && servers[closedSessionId]) { | ||
| 266 | void servers[closedSessionId].close().catch(() => undefined); | ||
| 267 | delete servers[closedSessionId]; | ||
| 268 | } | ||
| 269 | }; | ||
| 270 | |||
| 271 | const server = createSessionServer(bundles); | ||
| 272 | await server.connect(transport); | ||
| 273 | if (transport.sessionId) { | ||
| 274 | servers[transport.sessionId] = server; | ||
| 275 | } | ||
| 276 | } | ||
| 277 | |||
| 278 | await transport.handleRequest(req, res, req.body); | ||
| 279 | } catch (error) { | ||
| 280 | if (!res.headersSent) { | ||
| 281 | res.status(500).json({ | ||
| 282 | jsonrpc: "2.0", | ||
| 283 | error: { | ||
| 284 | code: -32603, | ||
| 285 | message: error instanceof Error ? error.message : "Internal server error", | ||
| 286 | }, | ||
| 287 | id: null, | ||
| 288 | }); | ||
| 289 | } | ||
| 290 | } | ||
| 291 | }); | ||
| 292 | |||
| 293 | app.get("/mcp", async (req: any, res: any) => { | ||
| 294 | const sessionId = resolveSessionId(req.headers); | ||
| 295 | if (!sessionId || !transports[sessionId]) { | ||
| 296 | res.status(400).send("Invalid or missing session ID"); | ||
| 297 | return; | ||
| 298 | } | ||
| 299 | |||
| 300 | await transports[sessionId].handleRequest(req, res); | ||
| 301 | }); | ||
| 302 | |||
| 303 | app.delete("/mcp", async (req: any, res: any) => { | ||
| 304 | const sessionId = resolveSessionId(req.headers); | ||
| 305 | if (!sessionId || !transports[sessionId]) { | ||
| 306 | res.status(400).send("Invalid or missing session ID"); | ||
| 307 | return; | ||
| 308 | } | ||
| 309 | |||
| 310 | const transport = transports[sessionId]; | ||
| 311 | await transport.handleRequest(req, res, req.body); | ||
| 312 | await transport.close(); | ||
| 313 | delete transports[sessionId]; | ||
| 314 | |||
| 315 | if (servers[sessionId]) { | ||
| 316 | await servers[sessionId].close().catch(() => undefined); | ||
| 317 | delete servers[sessionId]; | ||
| 318 | } | ||
| 319 | }); | ||
| 320 | |||
| 321 | const listener = await listen(app, input.port ?? 0, host); | ||
| 322 | const address = listener.address(); | ||
| 323 | if (!address || typeof address === "string") { | ||
| 324 | await new Promise<void>((resolve, reject) => { | ||
| 325 | listener.close((error) => (error ? reject(error) : resolve())); | ||
| 326 | }); | ||
| 327 | throw new Error("Failed to resolve serve-skills-via-mcp demo address"); | ||
| 328 | } | ||
| 329 | |||
| 330 | return { | ||
| 331 | endpoint: `http://${host}:${address.port}/mcp`, | ||
| 332 | close: async () => { | ||
| 333 | for (const transport of Object.values(transports)) { | ||
| 334 | await transport.close().catch(() => undefined); | ||
| 335 | } | ||
| 336 | |||
| 337 | for (const server of Object.values(servers)) { | ||
| 338 | await server.close().catch(() => undefined); | ||
| 339 | } | ||
| 340 | |||
| 341 | await new Promise<void>((resolve, reject) => { | ||
| 342 | listener.close((error) => (error ? reject(error) : resolve())); | ||
| 343 | }); | ||
| 344 | }, | ||
| 345 | }; | ||
| 346 | }; | ||
| @@ -1,67 +0,0 @@ | |||
| 1 | import { Client } from "@modelcontextprotocol/sdk/client/index.js"; | ||
| 2 | import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; | ||
| 3 | |||
| 4 | import { startServeSkillsViaMcpDemoServer } from "./server"; | ||
| 5 | |||
| 6 | const server = await startServeSkillsViaMcpDemoServer(); | ||
| 7 | const client = new Client({ | ||
| 8 | name: "serve-skills-via-mcp-smoke", | ||
| 9 | version: "1.0.0", | ||
| 10 | }); | ||
| 11 | const transport = new StreamableHTTPClientTransport(new URL(server.endpoint)); | ||
| 12 | |||
| 13 | try { | ||
| 14 | await client.connect(transport); | ||
| 15 | |||
| 16 | const resources = await client.listResources(); | ||
| 17 | console.log(`resources: ${resources.resources.length}`); | ||
| 18 | for (const resource of resources.resources) { | ||
| 19 | console.log(`- ${resource.uri}`); | ||
| 20 | } | ||
| 21 | |||
| 22 | const templates = await client.listResourceTemplates(); | ||
| 23 | console.log(`resourceTemplates: ${templates.resourceTemplates.length}`); | ||
| 24 | for (const template of templates.resourceTemplates) { | ||
| 25 | console.log(`- ${template.uriTemplate}`); | ||
| 26 | } | ||
| 27 | |||
| 28 | const catalog = await client.readResource({ uri: "skill://catalog/index.json" }); | ||
| 29 | const catalogText = "text" in catalog.contents[0]! ? catalog.contents[0]!.text : ""; | ||
| 30 | const parsedCatalog = JSON.parse(catalogText) as { | ||
| 31 | readonly skills: ReadonlyArray<{ | ||
| 32 | readonly name: string; | ||
| 33 | readonly manifestUri: string; | ||
| 34 | readonly instructionsUri: string; | ||
| 35 | }>; | ||
| 36 | }; | ||
| 37 | |||
| 38 | const selectedSkill = parsedCatalog.skills[0]!; | ||
| 39 | console.log(`selectedSkill: ${selectedSkill.name}`); | ||
| 40 | |||
| 41 | const manifest = await client.readResource({ uri: selectedSkill.manifestUri }); | ||
| 42 | const manifestText = "text" in manifest.contents[0]! ? manifest.contents[0]!.text : ""; | ||
| 43 | const parsedManifest = JSON.parse(manifestText) as { | ||
| 44 | readonly instructionsUri: string; | ||
| 45 | readonly files: ReadonlyArray<{ | ||
| 46 | readonly path: string; | ||
| 47 | readonly uri: string; | ||
| 48 | readonly role: string; | ||
| 49 | }>; | ||
| 50 | }; | ||
| 51 | |||
| 52 | const instructions = await client.readResource({ uri: parsedManifest.instructionsUri }); | ||
| 53 | const instructionsText = "text" in instructions.contents[0]! ? instructions.contents[0]!.text : ""; | ||
| 54 | console.log(`instructionsPreview: ${instructionsText.split("\n").slice(0, 6).join("\n")}`); | ||
| 55 | |||
| 56 | const supportFile = parsedManifest.files.find((file) => file.role !== "instructions"); | ||
| 57 | if (supportFile) { | ||
| 58 | const support = await client.readResource({ uri: supportFile.uri }); | ||
| 59 | const supportText = "text" in support.contents[0]! ? support.contents[0]!.text : ""; | ||
| 60 | console.log(`supportFile: ${supportFile.path}`); | ||
| 61 | console.log(`supportPreview: ${supportText.split("\n").slice(0, 4).join("\n")}`); | ||
| 62 | } | ||
| 63 | } finally { | ||
| 64 | await client.close().catch(() => undefined); | ||
| 65 | await transport.close().catch(() => undefined); | ||
| 66 | await server.close().catch(() => undefined); | ||
| 67 | } | ||
| @@ -1,15 +0,0 @@ | |||
| 1 | { | ||
| 2 | "compilerOptions": { | ||
| 3 | "target": "ES2022", | ||
| 4 | "module": "ESNext", | ||
| 5 | "moduleResolution": "Bundler", | ||
| 6 | "strict": true, | ||
| 7 | "skipLibCheck": true, | ||
| 8 | "noEmit": true, | ||
| 9 | "lib": ["ES2022"], | ||
| 10 | "types": ["bun-types", "node"] | ||
| 11 | }, | ||
| 12 | "include": [ | ||
| 13 | "src/**/*.ts" | ||
| 14 | ] | ||
| 15 | } | ||
| @@ -1 +0,0 @@ | |||
| 1 | .data/ | ||
| @@ -1,41 +0,0 @@ | |||
| 1 | # SQLite SDK Consumer Example | ||
| 2 | |||
| 3 | This example shows how an embedder can implement a custom SQLite-backed adapter | ||
| 4 | using the public Promise-facing `@executor/platform-sdk`. | ||
| 5 | |||
| 6 | The adapter is intentionally simple: | ||
| 7 | |||
| 8 | - installation state is stored in SQLite | ||
| 9 | - scope config and scope state are stored in SQLite | ||
| 10 | - source artifacts are stored in SQLite | ||
| 11 | - executor state is stored in SQLite | ||
| 12 | - secret material is stored in SQLite | ||
| 13 | |||
| 14 | There is no direct `effect` usage in the example. | ||
| 15 | |||
| 16 | ## Run | ||
| 17 | |||
| 18 | ```sh | ||
| 19 | bun run --cwd examples/sqlite-sdk-consumer start | ||
| 20 | ``` | ||
| 21 | |||
| 22 | To keep the demo ephemeral: | ||
| 23 | |||
| 24 | ```sh | ||
| 25 | DATABASE_PATH=:memory: bun run --cwd examples/sqlite-sdk-consumer start | ||
| 26 | ``` | ||
| 27 | |||
| 28 | ## Shape | ||
| 29 | |||
| 30 | The important bit is the consumer-facing construction: | ||
| 31 | |||
| 32 | ```ts | ||
| 33 | import { createExecutor } from "@executor/platform-sdk"; | ||
| 34 | import { createSqliteExecutorBackend } from "@executor/sqlite-sdk-consumer-example"; | ||
| 35 | |||
| 36 | const executor = await createExecutor({ | ||
| 37 | backend: createSqliteExecutorBackend({ | ||
| 38 | databasePath: "./executor.sqlite", | ||
| 39 | }), | ||
| 40 | }); | ||
| 41 | ``` | ||
| @@ -1,22 +0,0 @@ | |||
| 1 | { | ||
| 2 | "name": "@executor/sqlite-sdk-consumer-example", | ||
| 3 | "private": true, | ||
| 4 | "type": "module", | ||
| 5 | "exports": { | ||
| 6 | ".": "./src/index.ts" | ||
| 7 | }, | ||
| 8 | "scripts": { | ||
| 9 | "typecheck": "bunx tsc --noEmit -p tsconfig.json", | ||
| 10 | "start": "bun src/main.ts" | ||
| 11 | }, | ||
| 12 | "dependencies": { | ||
| 13 | "@executor/platform-sdk": "workspace:*", | ||
| 14 | "@executor/source-core": "workspace:*", | ||
| 15 | "drizzle-orm": "^0.44.5" | ||
| 16 | }, | ||
| 17 | "devDependencies": { | ||
| 18 | "@types/node": "catalog:", | ||
| 19 | "bun-types": "catalog:" | ||
| 20 | }, | ||
| 21 | "version": "0.0.0" | ||
| 22 | } | ||
| @@ -1,4 +0,0 @@ | |||
| 1 | export { | ||
| 2 | createSqliteExecutorBackend, | ||
| 3 | type CreateSqliteExecutorBackendOptions, | ||
| 4 | } from "./sqlite-backend"; | ||
| @@ -1,54 +0,0 @@ | |||
| 1 | import { createExecutor } from "@executor/platform-sdk"; | ||
| 2 | |||
| 3 | import { createSqliteExecutorBackend } from "./sqlite-backend"; | ||
| 4 | |||
| 5 | const databasePath = process.env.DATABASE_PATH | ||
| 6 | ?? new URL("../.data/executor.sqlite", import.meta.url).pathname; | ||
| 7 | |||
| 8 | const executor = await createExecutor({ | ||
| 9 | backend: createSqliteExecutorBackend({ | ||
| 10 | databasePath, | ||
| 11 | }), | ||
| 12 | }); | ||
| 13 | |||
| 14 | try { | ||
| 15 | const installation = await executor.local.installation(); | ||
| 16 | |||
| 17 | const policy = await executor.policies.create({ | ||
| 18 | resourcePattern: "math.add", | ||
| 19 | effect: "allow", | ||
| 20 | approvalMode: "auto", | ||
| 21 | }); | ||
| 22 | |||
| 23 | const secret = await executor.secrets.create({ | ||
| 24 | name: "demo-api-key", | ||
| 25 | value: "secret-value", | ||
| 26 | }); | ||
| 27 | |||
| 28 | const execution = await executor.executions.create({ | ||
| 29 | code: "return 20 + 22;", | ||
| 30 | }); | ||
| 31 | |||
| 32 | const sources = await executor.sources.list(); | ||
| 33 | const secrets = await executor.secrets.list(); | ||
| 34 | const policies = await executor.policies.list(); | ||
| 35 | |||
| 36 | console.log(JSON.stringify({ | ||
| 37 | databasePath, | ||
| 38 | installation, | ||
| 39 | createdPolicyId: policy.id, | ||
| 40 | createdSecretId: secret.id, | ||
| 41 | execution: { | ||
| 42 | id: execution.execution.id, | ||
| 43 | status: execution.execution.status, | ||
| 44 | resultJson: execution.execution.resultJson, | ||
| 45 | }, | ||
| 46 | counts: { | ||
| 47 | sources: sources.length, | ||
| 48 | secrets: secrets.length, | ||
| 49 | policies: policies.length, | ||
| 50 | }, | ||
| 51 | }, null, 2)); | ||
| 52 | } finally { | ||
| 53 | await executor.close(); | ||
| 54 | } | ||
| @@ -1,733 +0,0 @@ | |||
| 1 | import { createHash, randomUUID } from "node:crypto"; | ||
| 2 | import { mkdirSync } from "node:fs"; | ||
| 3 | import { dirname, resolve as resolvePath } from "node:path"; | ||
| 4 | |||
| 5 | import { Database } from "bun:sqlite"; | ||
| 6 | import { and, eq } from "drizzle-orm"; | ||
| 7 | import { drizzle } from "drizzle-orm/bun-sqlite"; | ||
| 8 | import { | ||
| 9 | integer, | ||
| 10 | sqliteTable, | ||
| 11 | text, | ||
| 12 | } from "drizzle-orm/sqlite-core"; | ||
| 13 | |||
| 14 | import { | ||
| 15 | createExecutorBackend, | ||
| 16 | type ExecutorBackend, | ||
| 17 | type ExecutorBackendRepositories, | ||
| 18 | type ExecutorWorkspaceConfigRepository, | ||
| 19 | type ExecutorWorkspaceStateRepository, | ||
| 20 | type ExecutorWorkspaceSourceArtifactRepository, | ||
| 21 | } from "@executor/platform-sdk"; | ||
| 22 | import type { | ||
| 23 | Execution, | ||
| 24 | ExecutionInteraction, | ||
| 25 | ExecutionStep, | ||
| 26 | ExecutorScopeConfig, | ||
| 27 | LocalInstallation, | ||
| 28 | SecretMaterial, | ||
| 29 | SecretRef, | ||
| 30 | SecretStore, | ||
| 31 | } from "@executor/platform-sdk/schema"; | ||
| 32 | import { | ||
| 33 | ScopeIdSchema, | ||
| 34 | SecretMaterialIdSchema, | ||
| 35 | SourceCatalogIdSchema, | ||
| 36 | SourceCatalogRevisionIdSchema, | ||
| 37 | } from "@executor/platform-sdk/schema"; | ||
| 38 | import type { | ||
| 39 | SecretMaterialStoredDataRecord, | ||
| 40 | } from "@executor/platform-sdk/runtime"; | ||
| 41 | import { | ||
| 42 | contentHash, | ||
| 43 | snapshotFromSourceCatalogSyncResult, | ||
| 44 | } from "@executor/source-core"; | ||
| 45 | |||
| 46 | export type CreateSqliteExecutorBackendOptions = { | ||
| 47 | databasePath?: string; | ||
| 48 | scopeId?: string; | ||
| 49 | actorScopeId?: string; | ||
| 50 | }; | ||
| 51 | |||
| 52 | type ScopeConfig = Awaited<ReturnType<ExecutorWorkspaceConfigRepository["load"]>>; | ||
| 53 | type ScopeState = Awaited<ReturnType<ExecutorWorkspaceStateRepository["load"]>>; | ||
| 54 | type SourceArtifact = | ||
| 55 | ReturnType<ExecutorWorkspaceSourceArtifactRepository["build"]>; | ||
| 56 | type SourceArtifactBuildInput = Parameters< | ||
| 57 | ExecutorWorkspaceSourceArtifactRepository["build"] | ||
| 58 | >[0]; | ||
| 59 | |||
| 60 | type SecretMaterialSummary = { | ||
| 61 | id: string; | ||
| 62 | storeId: string; | ||
| 63 | name: string | null; | ||
| 64 | purpose: string; | ||
| 65 | createdAt: number; | ||
| 66 | updatedAt: number; | ||
| 67 | }; | ||
| 68 | |||
| 69 | type SqliteSecretStoredData = { | ||
| 70 | value: string; | ||
| 71 | }; | ||
| 72 | |||
| 73 | const SQLITE_SECRET_STORE_KIND = "sqlite"; | ||
| 74 | const SQLITE_SECRET_STORE_ID = "sts_sqlite"; | ||
| 75 | |||
| 76 | const installations = sqliteTable("installations", { | ||
| 77 | key: text("key").primaryKey(), | ||
| 78 | scopeId: text("scope_id").notNull(), | ||
| 79 | actorScopeId: text("actor_scope_id").notNull(), | ||
| 80 | }); | ||
| 81 | |||
| 82 | const scopeConfigs = sqliteTable("scope_configs", { | ||
| 83 | key: text("key").primaryKey(), | ||
| 84 | projectConfigJson: text("project_config_json").notNull(), | ||
| 85 | }); | ||
| 86 | |||
| 87 | const scopeStates = sqliteTable("scope_states", { | ||
| 88 | key: text("key").primaryKey(), | ||
| 89 | stateJson: text("state_json").notNull(), | ||
| 90 | }); | ||
| 91 | |||
| 92 | const sourceArtifacts = sqliteTable("source_artifacts", { | ||
| 93 | sourceId: text("source_id").primaryKey(), | ||
| 94 | artifactJson: text("artifact_json").notNull(), | ||
| 95 | }); | ||
| 96 | |||
| 97 | const secretMaterials = sqliteTable("secret_materials", { | ||
| 98 | id: text("id").primaryKey(), | ||
| 99 | storeId: text("store_id").notNull(), | ||
| 100 | name: text("name"), | ||
| 101 | purpose: text("purpose").notNull(), | ||
| 102 | createdAt: integer("created_at").notNull(), | ||
| 103 | updatedAt: integer("updated_at").notNull(), | ||
| 104 | json: text("json").notNull(), | ||
| 105 | }); | ||
| 106 | |||
| 107 | const secretMaterialStoredData = sqliteTable("secret_material_stored_data", { | ||
| 108 | secretId: text("secret_id").primaryKey(), | ||
| 109 | json: text("json").notNull(), | ||
| 110 | }); | ||
| 111 | |||
| 112 | const executions = sqliteTable("executions", { | ||
| 113 | id: text("id").primaryKey(), | ||
| 114 | scopeId: text("scope_id").notNull(), | ||
| 115 | json: text("json").notNull(), | ||
| 116 | }); | ||
| 117 | |||
| 118 | const executionInteractions = sqliteTable("execution_interactions", { | ||
| 119 | id: text("id").primaryKey(), | ||
| 120 | executionId: text("execution_id").notNull(), | ||
| 121 | status: text("status").notNull(), | ||
| 122 | json: text("json").notNull(), | ||
| 123 | }); | ||
| 124 | |||
| 125 | const executionSteps = sqliteTable("execution_steps", { | ||
| 126 | id: text("id").primaryKey(), | ||
| 127 | executionId: text("execution_id").notNull(), | ||
| 128 | sequence: integer("sequence").notNull(), | ||
| 129 | json: text("json").notNull(), | ||
| 130 | }); | ||
| 131 | |||
| 132 | const makeHash = (value: string): string => | ||
| 133 | createHash("sha256").update(value).digest("hex").slice(0, 24); | ||
| 134 | |||
| 135 | const parseJson = <T>(value: string): T => JSON.parse(value) as T; | ||
| 136 | |||
| 137 | const createSourceArtifact = (input: SourceArtifactBuildInput): SourceArtifact => { | ||
| 138 | const snapshot = snapshotFromSourceCatalogSyncResult(input.syncResult); | ||
| 139 | const sourceConfigJson = JSON.stringify({ | ||
| 140 | kind: input.source.kind, | ||
| 141 | namespace: input.source.namespace, | ||
| 142 | name: input.source.name, | ||
| 143 | enabled: input.source.enabled, | ||
| 144 | }); | ||
| 145 | const importMetadataJson = JSON.stringify(snapshot.import); | ||
| 146 | const catalogId = SourceCatalogIdSchema.make(`src_catalog_${makeHash(sourceConfigJson)}`); | ||
| 147 | const revisionId = SourceCatalogRevisionIdSchema.make( | ||
| 148 | `src_catalog_rev_${makeHash(sourceConfigJson)}`, | ||
| 149 | ); | ||
| 150 | |||
| 151 | return { | ||
| 152 | version: 4, | ||
| 153 | sourceId: input.source.id, | ||
| 154 | catalogId, | ||
| 155 | generatedAt: Date.now(), | ||
| 156 | revision: { | ||
| 157 | id: revisionId, | ||
| 158 | catalogId, | ||
| 159 | revisionNumber: 1, | ||
| 160 | sourceConfigJson, | ||
| 161 | importMetadataJson, | ||
| 162 | importMetadataHash: contentHash(importMetadataJson), | ||
| 163 | snapshotHash: contentHash(JSON.stringify(snapshot)), | ||
| 164 | createdAt: input.source.createdAt, | ||
| 165 | updatedAt: input.source.updatedAt, | ||
| 166 | }, | ||
| 167 | snapshot, | ||
| 168 | }; | ||
| 169 | }; | ||
| 170 | |||
| 171 | const defaultScopeState = (): ScopeState => ({ | ||
| 172 | version: 1, | ||
| 173 | sources: {}, | ||
| 174 | policies: {}, | ||
| 175 | }); | ||
| 176 | |||
| 177 | const createInstallation = ( | ||
| 178 | options: CreateSqliteExecutorBackendOptions, | ||
| 179 | ): LocalInstallation => { | ||
| 180 | const scopeId = ScopeIdSchema.make(options.scopeId ?? "scope_sqlite_example"); | ||
| 181 | const actorScopeId = ScopeIdSchema.make( | ||
| 182 | options.actorScopeId ?? "account_sqlite_example", | ||
| 183 | ); | ||
| 184 | |||
| 185 | return { | ||
| 186 | scopeId, | ||
| 187 | actorScopeId, | ||
| 188 | }; | ||
| 189 | }; | ||
| 190 | |||
| 191 | const openSqliteStore = (databasePath: string) => { | ||
| 192 | if (databasePath !== ":memory:") { | ||
| 193 | mkdirSync(dirname(databasePath), { recursive: true }); | ||
| 194 | } | ||
| 195 | |||
| 196 | const sqlite = new Database(databasePath, { create: true, strict: true }); | ||
| 197 | const db = drizzle(sqlite); | ||
| 198 | |||
| 199 | sqlite.exec(` | ||
| 200 | CREATE TABLE IF NOT EXISTS installations ( | ||
| 201 | key TEXT PRIMARY KEY NOT NULL, | ||
| 202 | scope_id TEXT NOT NULL, | ||
| 203 | actor_scope_id TEXT NOT NULL | ||
| 204 | ); | ||
| 205 | |||
| 206 | CREATE TABLE IF NOT EXISTS scope_configs ( | ||
| 207 | key TEXT PRIMARY KEY NOT NULL, | ||
| 208 | project_config_json TEXT NOT NULL | ||
| 209 | ); | ||
| 210 | |||
| 211 | CREATE TABLE IF NOT EXISTS scope_states ( | ||
| 212 | key TEXT PRIMARY KEY NOT NULL, | ||
| 213 | state_json TEXT NOT NULL | ||
| 214 | ); | ||
| 215 | |||
| 216 | CREATE TABLE IF NOT EXISTS source_artifacts ( | ||
| 217 | source_id TEXT PRIMARY KEY NOT NULL, | ||
| 218 | artifact_json TEXT NOT NULL | ||
| 219 | ); | ||
| 220 | |||
| 221 | CREATE TABLE IF NOT EXISTS secret_materials ( | ||
| 222 | id TEXT PRIMARY KEY NOT NULL, | ||
| 223 | store_id TEXT NOT NULL, | ||
| 224 | name TEXT, | ||
| 225 | purpose TEXT NOT NULL, | ||
| 226 | created_at INTEGER NOT NULL, | ||
| 227 | updated_at INTEGER NOT NULL, | ||
| 228 | json TEXT NOT NULL | ||
| 229 | ); | ||
| 230 | |||
| 231 | CREATE TABLE IF NOT EXISTS secret_material_stored_data ( | ||
| 232 | secret_id TEXT PRIMARY KEY NOT NULL, | ||
| 233 | json TEXT NOT NULL | ||
| 234 | ); | ||
| 235 | |||
| 236 | CREATE TABLE IF NOT EXISTS executions ( | ||
| 237 | id TEXT PRIMARY KEY NOT NULL, | ||
| 238 | scope_id TEXT NOT NULL, | ||
| 239 | json TEXT NOT NULL | ||
| 240 | ); | ||
| 241 | |||
| 242 | CREATE TABLE IF NOT EXISTS execution_interactions ( | ||
| 243 | id TEXT PRIMARY KEY NOT NULL, | ||
| 244 | execution_id TEXT NOT NULL, | ||
| 245 | status TEXT NOT NULL, | ||
| 246 | json TEXT NOT NULL | ||
| 247 | ); | ||
| 248 | |||
| 249 | CREATE TABLE IF NOT EXISTS execution_steps ( | ||
| 250 | id TEXT PRIMARY KEY NOT NULL, | ||
| 251 | execution_id TEXT NOT NULL, | ||
| 252 | sequence INTEGER NOT NULL, | ||
| 253 | json TEXT NOT NULL | ||
| 254 | ); | ||
| 255 | `); | ||
| 256 | |||
| 257 | return { | ||
| 258 | sqlite, | ||
| 259 | db, | ||
| 260 | close: () => { | ||
| 261 | sqlite.close(); | ||
| 262 | }, | ||
| 263 | }; | ||
| 264 | }; | ||
| 265 | |||
| 266 | type SqliteStore = ReturnType<typeof openSqliteStore>; | ||
| 267 | |||
| 268 | const createStorageDomains = ( | ||
| 269 | store: SqliteStore, | ||
| 270 | installation: LocalInstallation, | ||
| 271 | ) => ({ | ||
| 272 | secrets: { | ||
| 273 | secretStores: { | ||
| 274 | getById: (id: SecretStore["id"]) => | ||
| 275 | id === SQLITE_SECRET_STORE_ID | ||
| 276 | ? { | ||
| 277 | id: SQLITE_SECRET_STORE_ID as SecretStore["id"], | ||
| 278 | scopeId: installation.scopeId, | ||
| 279 | kind: SQLITE_SECRET_STORE_KIND, | ||
| 280 | name: "SQLite Store", | ||
| 281 | status: "connected" as const, | ||
| 282 | enabled: true, | ||
| 283 | createdAt: 0, | ||
| 284 | updatedAt: 0, | ||
| 285 | } | ||
| 286 | : null, | ||
| 287 | listAll: () => [{ | ||
| 288 | id: SQLITE_SECRET_STORE_ID, | ||
| 289 | scopeId: installation.scopeId, | ||
| 290 | kind: SQLITE_SECRET_STORE_KIND, | ||
| 291 | name: "SQLite Store", | ||
| 292 | status: "connected", | ||
| 293 | enabled: true, | ||
| 294 | createdAt: 0, | ||
| 295 | updatedAt: 0, | ||
| 296 | }], | ||
| 297 | upsert: (_secretStore: SecretStore) => {}, | ||
| 298 | updateById: (_id: SecretStore["id"], _update: Partial<Pick<SecretStore, "name" | "status" | "enabled">>) => null, | ||
| 299 | removeById: (_id: SecretStore["id"]) => false, | ||
| 300 | }, | ||
| 301 | getById: (id: SecretMaterial["id"]) => { | ||
| 302 | const row = store.db.select().from(secretMaterials).where(eq(secretMaterials.id, id)).get(); | ||
| 303 | return row ? parseJson<SecretMaterial>(row.json) : null; | ||
| 304 | }, | ||
| 305 | listAll: (): readonly SecretMaterialSummary[] => | ||
| 306 | store.db.select().from(secretMaterials).all().map((row) => ({ | ||
| 307 | id: row.id, | ||
| 308 | storeId: row.storeId, | ||
| 309 | name: row.name, | ||
| 310 | purpose: row.purpose, | ||
| 311 | createdAt: row.createdAt, | ||
| 312 | updatedAt: row.updatedAt, | ||
| 313 | })), | ||
| 314 | upsert: (material: SecretMaterial) => | ||
| 315 | store.db.insert(secretMaterials).values({ | ||
| 316 | id: material.id, | ||
| 317 | storeId: material.storeId, | ||
| 318 | name: material.name, | ||
| 319 | purpose: material.purpose, | ||
| 320 | createdAt: material.createdAt, | ||
| 321 | updatedAt: material.updatedAt, | ||
| 322 | json: JSON.stringify(material), | ||
| 323 | }).onConflictDoUpdate({ | ||
| 324 | target: secretMaterials.id, | ||
| 325 | set: { | ||
| 326 | storeId: material.storeId, | ||
| 327 | name: material.name, | ||
| 328 | purpose: material.purpose, | ||
| 329 | createdAt: material.createdAt, | ||
| 330 | updatedAt: material.updatedAt, | ||
| 331 | json: JSON.stringify(material), | ||
| 332 | }, | ||
| 333 | }).run(), | ||
| 334 | updateById: ( | ||
| 335 | id: SecretMaterial["id"], | ||
| 336 | update: { name?: string | null }, | ||
| 337 | ) => { | ||
| 338 | const row = store.db.select().from(secretMaterials).where(eq(secretMaterials.id, id)).get(); | ||
| 339 | if (!row) return null; | ||
| 340 | const current = parseJson<SecretMaterial>(row.json); | ||
| 341 | const next = { | ||
| 342 | ...current, | ||
| 343 | name: update.name === undefined ? current.name : update.name, | ||
| 344 | updatedAt: Date.now(), | ||
| 345 | }; | ||
| 346 | store.db.update(secretMaterials).set({ | ||
| 347 | storeId: next.storeId, | ||
| 348 | name: next.name, | ||
| 349 | purpose: next.purpose, | ||
| 350 | createdAt: next.createdAt, | ||
| 351 | updatedAt: next.updatedAt, | ||
| 352 | json: JSON.stringify(next), | ||
| 353 | }).where(eq(secretMaterials.id, id)).run(); | ||
| 354 | return { | ||
| 355 | id: next.id, | ||
| 356 | storeId: next.storeId, | ||
| 357 | name: next.name, | ||
| 358 | purpose: next.purpose, | ||
| 359 | createdAt: next.createdAt, | ||
| 360 | updatedAt: next.updatedAt, | ||
| 361 | } satisfies SecretMaterialSummary; | ||
| 362 | }, | ||
| 363 | removeById: (id: SecretMaterial["id"]) => { | ||
| 364 | const row = store.db.select({ id: secretMaterials.id }).from(secretMaterials).where( | ||
| 365 | eq(secretMaterials.id, id), | ||
| 366 | ).get(); | ||
| 367 | if (!row) return false; | ||
| 368 | store.db.delete(secretMaterials).where(eq(secretMaterials.id, id)).run(); | ||
| 369 | return true; | ||
| 370 | }, | ||
| 371 | secretMaterialStoredData: { | ||
| 372 | getBySecretId: (secretId: SecretMaterial["id"]) => { | ||
| 373 | const row = store.db.select().from(secretMaterialStoredData).where( | ||
| 374 | eq(secretMaterialStoredData.secretId, secretId), | ||
| 375 | ).get(); | ||
| 376 | return row | ||
| 377 | ? { | ||
| 378 | secretId, | ||
| 379 | data: parseJson<SqliteSecretStoredData>(row.json), | ||
| 380 | } | ||
| 381 | : null; | ||
| 382 | }, | ||
| 383 | upsert: (record: SecretMaterialStoredDataRecord) => { | ||
| 384 | store.db.insert(secretMaterialStoredData).values({ | ||
| 385 | secretId: record.secretId, | ||
| 386 | json: JSON.stringify(record.data), | ||
| 387 | }).onConflictDoUpdate({ | ||
| 388 | target: secretMaterialStoredData.secretId, | ||
| 389 | set: { | ||
| 390 | json: JSON.stringify(record.data), | ||
| 391 | }, | ||
| 392 | }).run(); | ||
| 393 | }, | ||
| 394 | removeBySecretId: (secretId: SecretMaterial["id"]) => { | ||
| 395 | const row = store.db.select({ secretId: secretMaterialStoredData.secretId }).from( | ||
| 396 | secretMaterialStoredData, | ||
| 397 | ).where(eq(secretMaterialStoredData.secretId, secretId)).get(); | ||
| 398 | if (!row) return false; | ||
| 399 | store.db.delete(secretMaterialStoredData).where( | ||
| 400 | eq(secretMaterialStoredData.secretId, secretId), | ||
| 401 | ).run(); | ||
| 402 | return true; | ||
| 403 | }, | ||
| 404 | }, | ||
| 405 | }, | ||
| 406 | executions: { | ||
| 407 | runs: { | ||
| 408 | listByScope: (scopeId: Execution["scopeId"]) => | ||
| 409 | store.db.select().from(executions).where( | ||
| 410 | eq(executions.scopeId, scopeId), | ||
| 411 | ).all() | ||
| 412 | .map((row) => parseJson<Execution>(row.json)) | ||
| 413 | .sort((left, right) => right.createdAt - left.createdAt), | ||
| 414 | getById: (executionId: Execution["id"]) => { | ||
| 415 | const row = store.db.select().from(executions).where(eq(executions.id, executionId)).get(); | ||
| 416 | return row ? parseJson<Execution>(row.json) : null; | ||
| 417 | }, | ||
| 418 | getByScopeAndId: ( | ||
| 419 | scopeId: Execution["scopeId"], | ||
| 420 | executionId: Execution["id"], | ||
| 421 | ) => { | ||
| 422 | const row = store.db.select().from(executions).where( | ||
| 423 | and(eq(executions.scopeId, scopeId), eq(executions.id, executionId)), | ||
| 424 | ).get(); | ||
| 425 | return row ? parseJson<Execution>(row.json) : null; | ||
| 426 | }, | ||
| 427 | insert: (execution: Execution) => | ||
| 428 | store.db.insert(executions).values({ | ||
| 429 | id: execution.id, | ||
| 430 | scopeId: execution.scopeId, | ||
| 431 | json: JSON.stringify(execution), | ||
| 432 | }).run(), | ||
| 433 | update: ( | ||
| 434 | executionId: Execution["id"], | ||
| 435 | patch: Partial<Omit<Execution, "id" | "scopeId" | "createdByScopeId" | "createdAt">>, | ||
| 436 | ) => { | ||
| 437 | const row = store.db.select().from(executions).where(eq(executions.id, executionId)).get(); | ||
| 438 | if (!row) return null; | ||
| 439 | const next = { ...parseJson<Execution>(row.json), ...patch }; | ||
| 440 | store.db.update(executions).set({ | ||
| 441 | scopeId: next.scopeId, | ||
| 442 | json: JSON.stringify(next), | ||
| 443 | }).where(eq(executions.id, executionId)).run(); | ||
| 444 | return next; | ||
| 445 | }, | ||
| 446 | }, | ||
| 447 | interactions: { | ||
| 448 | getById: (interactionId: ExecutionInteraction["id"]) => { | ||
| 449 | const row = store.db.select().from(executionInteractions).where( | ||
| 450 | eq(executionInteractions.id, interactionId), | ||
| 451 | ).get(); | ||
| 452 | return row ? parseJson<ExecutionInteraction>(row.json) : null; | ||
| 453 | }, | ||
| 454 | listByExecutionId: (executionId: ExecutionInteraction["executionId"]) => | ||
| 455 | store.db.select().from(executionInteractions).where( | ||
| 456 | eq(executionInteractions.executionId, executionId), | ||
| 457 | ).all().map((row) => parseJson<ExecutionInteraction>(row.json)), | ||
| 458 | getPendingByExecutionId: (executionId: ExecutionInteraction["executionId"]) => { | ||
| 459 | const row = store.db.select().from(executionInteractions).where( | ||
| 460 | and( | ||
| 461 | eq(executionInteractions.executionId, executionId), | ||
| 462 | eq(executionInteractions.status, "pending"), | ||
| 463 | ), | ||
| 464 | ).get(); | ||
| 465 | return row ? parseJson<ExecutionInteraction>(row.json) : null; | ||
| 466 | }, | ||
| 467 | insert: (interaction: ExecutionInteraction) => | ||
| 468 | store.db.insert(executionInteractions).values({ | ||
| 469 | id: interaction.id, | ||
| 470 | executionId: interaction.executionId, | ||
| 471 | status: interaction.status, | ||
| 472 | json: JSON.stringify(interaction), | ||
| 473 | }).run(), | ||
| 474 | update: ( | ||
| 475 | interactionId: ExecutionInteraction["id"], | ||
| 476 | patch: Partial<Omit<ExecutionInteraction, "id" | "executionId" | "createdAt">>, | ||
| 477 | ) => { | ||
| 478 | const row = store.db.select().from(executionInteractions).where( | ||
| 479 | eq(executionInteractions.id, interactionId), | ||
| 480 | ).get(); | ||
| 481 | if (!row) return null; | ||
| 482 | const next = { ...parseJson<ExecutionInteraction>(row.json), ...patch }; | ||
| 483 | store.db.update(executionInteractions).set({ | ||
| 484 | executionId: next.executionId, | ||
| 485 | status: next.status, | ||
| 486 | json: JSON.stringify(next), | ||
| 487 | }).where(eq(executionInteractions.id, interactionId)).run(); | ||
| 488 | return next; | ||
| 489 | }, | ||
| 490 | }, | ||
| 491 | steps: { | ||
| 492 | getByExecutionAndSequence: ( | ||
| 493 | executionId: ExecutionStep["executionId"], | ||
| 494 | sequence: ExecutionStep["sequence"], | ||
| 495 | ) => { | ||
| 496 | const row = store.db.select().from(executionSteps).where( | ||
| 497 | and( | ||
| 498 | eq(executionSteps.executionId, executionId), | ||
| 499 | eq(executionSteps.sequence, sequence), | ||
| 500 | ), | ||
| 501 | ).get(); | ||
| 502 | return row ? parseJson<ExecutionStep>(row.json) : null; | ||
| 503 | }, | ||
| 504 | listByExecutionId: (executionId: ExecutionStep["executionId"]) => | ||
| 505 | store.db.select().from(executionSteps).where( | ||
| 506 | eq(executionSteps.executionId, executionId), | ||
| 507 | ).orderBy(executionSteps.sequence).all().map((row) => parseJson<ExecutionStep>(row.json)), | ||
| 508 | insert: (step: ExecutionStep) => | ||
| 509 | store.db.insert(executionSteps).values({ | ||
| 510 | id: step.id, | ||
| 511 | executionId: step.executionId, | ||
| 512 | sequence: step.sequence, | ||
| 513 | json: JSON.stringify(step), | ||
| 514 | }).run(), | ||
| 515 | deleteByExecutionId: (executionId: ExecutionStep["executionId"]) => { | ||
| 516 | store.db.delete(executionSteps).where(eq(executionSteps.executionId, executionId)).run(); | ||
| 517 | }, | ||
| 518 | updateByExecutionAndSequence: ( | ||
| 519 | executionId: ExecutionStep["executionId"], | ||
| 520 | sequence: ExecutionStep["sequence"], | ||
| 521 | patch: Partial<Omit<ExecutionStep, "id" | "executionId" | "sequence" | "createdAt">>, | ||
| 522 | ) => { | ||
| 523 | const row = store.db.select().from(executionSteps).where( | ||
| 524 | and( | ||
| 525 | eq(executionSteps.executionId, executionId), | ||
| 526 | eq(executionSteps.sequence, sequence), | ||
| 527 | ), | ||
| 528 | ).get(); | ||
| 529 | if (!row) return null; | ||
| 530 | const next = { ...parseJson<ExecutionStep>(row.json), ...patch }; | ||
| 531 | store.db.update(executionSteps).set({ | ||
| 532 | id: next.id, | ||
| 533 | executionId: next.executionId, | ||
| 534 | sequence: next.sequence, | ||
| 535 | json: JSON.stringify(next), | ||
| 536 | }).where(eq(executionSteps.id, row.id)).run(); | ||
| 537 | return next; | ||
| 538 | }, | ||
| 539 | }, | ||
| 540 | }, | ||
| 541 | }); | ||
| 542 | |||
| 543 | export const createSqliteExecutorBackend = ( | ||
| 544 | options: CreateSqliteExecutorBackendOptions = {}, | ||
| 545 | ): ExecutorBackend => { | ||
| 546 | const databasePath = options.databasePath && options.databasePath !== ":memory:" | ||
| 547 | ? resolvePath(options.databasePath) | ||
| 548 | : (options.databasePath ?? ":memory:"); | ||
| 549 | |||
| 550 | return createExecutorBackend({ | ||
| 551 | loadRepositories: () => { | ||
| 552 | const store = openSqliteStore(databasePath); | ||
| 553 | const installation = (() => { | ||
| 554 | const row = store.db.select().from(installations).where(eq(installations.key, "active")).get(); | ||
| 555 | return row | ||
| 556 | ? { | ||
| 557 | scopeId: row.scopeId as LocalInstallation["scopeId"], | ||
| 558 | actorScopeId: row.actorScopeId as LocalInstallation["actorScopeId"], | ||
| 559 | } | ||
| 560 | : createInstallation(options); | ||
| 561 | })(); | ||
| 562 | const { secrets, executions } = createStorageDomains(store, installation); | ||
| 563 | |||
| 564 | return { | ||
| 565 | scope: { | ||
| 566 | metadata: { | ||
| 567 | kind: "sqlite", | ||
| 568 | databasePath, | ||
| 569 | }, | ||
| 570 | }, | ||
| 571 | installation: { | ||
| 572 | load: () => { | ||
| 573 | return installation; | ||
| 574 | }, | ||
| 575 | getOrProvision: () => { | ||
| 576 | store.db.insert(installations).values({ | ||
| 577 | key: "active", | ||
| 578 | scopeId: installation.scopeId, | ||
| 579 | actorScopeId: installation.actorScopeId, | ||
| 580 | }).onConflictDoUpdate({ | ||
| 581 | target: installations.key, | ||
| 582 | set: { | ||
| 583 | scopeId: installation.scopeId, | ||
| 584 | actorScopeId: installation.actorScopeId, | ||
| 585 | }, | ||
| 586 | }).run(); | ||
| 587 | return installation; | ||
| 588 | }, | ||
| 589 | }, | ||
| 590 | workspace: { | ||
| 591 | config: { | ||
| 592 | load: () => { | ||
| 593 | const row = store.db.select().from(scopeConfigs).where(eq(scopeConfigs.key, "project")).get(); | ||
| 594 | const projectConfig = row | ||
| 595 | ? parseJson<ExecutorScopeConfig>(row.projectConfigJson) | ||
| 596 | : {}; | ||
| 597 | return { | ||
| 598 | config: projectConfig, | ||
| 599 | homeConfig: null, | ||
| 600 | projectConfig, | ||
| 601 | } satisfies ScopeConfig; | ||
| 602 | }, | ||
| 603 | writeProject: (config) => { | ||
| 604 | store.db.insert(scopeConfigs).values({ | ||
| 605 | key: "project", | ||
| 606 | projectConfigJson: JSON.stringify(config), | ||
| 607 | }).onConflictDoUpdate({ | ||
| 608 | target: scopeConfigs.key, | ||
| 609 | set: { projectConfigJson: JSON.stringify(config) }, | ||
| 610 | }).run(); | ||
| 611 | }, | ||
| 612 | }, | ||
| 613 | state: { | ||
| 614 | load: () => { | ||
| 615 | const row = store.db.select().from(scopeStates).where(eq(scopeStates.key, "active")).get(); | ||
| 616 | return row ? parseJson<ScopeState>(row.stateJson) : defaultScopeState(); | ||
| 617 | }, | ||
| 618 | write: (state) => { | ||
| 619 | store.db.insert(scopeStates).values({ | ||
| 620 | key: "active", | ||
| 621 | stateJson: JSON.stringify(state), | ||
| 622 | }).onConflictDoUpdate({ | ||
| 623 | target: scopeStates.key, | ||
| 624 | set: { stateJson: JSON.stringify(state) }, | ||
| 625 | }).run(); | ||
| 626 | }, | ||
| 627 | }, | ||
| 628 | sourceArtifacts: { | ||
| 629 | build: createSourceArtifact, | ||
| 630 | read: (sourceId) => { | ||
| 631 | const row = store.db.select().from(sourceArtifacts).where( | ||
| 632 | eq(sourceArtifacts.sourceId, sourceId), | ||
| 633 | ).get(); | ||
| 634 | return row ? parseJson<SourceArtifact>(row.artifactJson) : null; | ||
| 635 | }, | ||
| 636 | write: ({ sourceId, artifact }) => { | ||
| 637 | store.db.insert(sourceArtifacts).values({ | ||
| 638 | sourceId, | ||
| 639 | artifactJson: JSON.stringify(artifact), | ||
| 640 | }).onConflictDoUpdate({ | ||
| 641 | target: sourceArtifacts.sourceId, | ||
| 642 | set: { artifactJson: JSON.stringify(artifact) }, | ||
| 643 | }).run(); | ||
| 644 | }, | ||
| 645 | remove: (sourceId) => { | ||
| 646 | store.db.delete(sourceArtifacts).where(eq(sourceArtifacts.sourceId, sourceId)).run(); | ||
| 647 | }, | ||
| 648 | }, | ||
| 649 | }, | ||
| 650 | secrets: { | ||
| 651 | ...secrets, | ||
| 652 | secretStores: secrets.secretStores, | ||
| 653 | secretMaterialStoredData: secrets.secretMaterialStoredData, | ||
| 654 | resolve: ({ ref }) => { | ||
| 655 | const material = secrets.getById( | ||
| 656 | ref.secretId as SecretMaterial["id"], | ||
| 657 | ); | ||
| 658 | const stored = secrets.secretMaterialStoredData.getBySecretId( | ||
| 659 | ref.secretId as SecretMaterial["id"], | ||
| 660 | ); | ||
| 661 | if (!material || !stored) { | ||
| 662 | throw new Error(`Missing secret material ${ref.secretId}`); | ||
| 663 | } | ||
| 664 | return (stored.data as SqliteSecretStoredData).value; | ||
| 665 | }, | ||
| 666 | store: ({ purpose, value, name, storeId }) => { | ||
| 667 | const now = Date.now(); | ||
| 668 | const id = SecretMaterialIdSchema.make(`secret_${randomUUID()}`); | ||
| 669 | const material: SecretMaterial = { | ||
| 670 | id, | ||
| 671 | storeId: (storeId ?? SQLITE_SECRET_STORE_ID) as SecretMaterial["storeId"], | ||
| 672 | name: name ?? null, | ||
| 673 | purpose, | ||
| 674 | createdAt: now, | ||
| 675 | updatedAt: now, | ||
| 676 | }; | ||
| 677 | secrets.upsert(material); | ||
| 678 | secrets.secretMaterialStoredData.upsert({ | ||
| 679 | secretId: id, | ||
| 680 | data: { | ||
| 681 | value, | ||
| 682 | } satisfies SqliteSecretStoredData, | ||
| 683 | }); | ||
| 684 | return { | ||
| 685 | secretId: material.id, | ||
| 686 | } satisfies SecretRef; | ||
| 687 | }, | ||
| 688 | delete: (ref) => { | ||
| 689 | secrets.secretMaterialStoredData.removeBySecretId( | ||
| 690 | ref.secretId as SecretMaterial["id"], | ||
| 691 | ); | ||
| 692 | return secrets.removeById(ref.secretId as SecretMaterial["id"]); | ||
| 693 | }, | ||
| 694 | update: ({ ref, name, value }) => { | ||
| 695 | const updated = secrets.updateById( | ||
| 696 | ref.secretId as SecretMaterial["id"], | ||
| 697 | { name }, | ||
| 698 | ); | ||
| 699 | if (!updated) { | ||
| 700 | throw new Error(`Missing secret material ${ref.secretId}`); | ||
| 701 | } | ||
| 702 | if (value !== undefined) { | ||
| 703 | secrets.secretMaterialStoredData.upsert({ | ||
| 704 | secretId: ref.secretId as SecretMaterial["id"], | ||
| 705 | data: { | ||
| 706 | value, | ||
| 707 | } satisfies SqliteSecretStoredData, | ||
| 708 | }); | ||
| 709 | } | ||
| 710 | return updated; | ||
| 711 | }, | ||
| 712 | }, | ||
| 713 | executions, | ||
| 714 | instanceConfig: { | ||
| 715 | resolve: () => ({ | ||
| 716 | platform: "sqlite-sdk-example", | ||
| 717 | secretStorePlugins: [ | ||
| 718 | { | ||
| 719 | kind: SQLITE_SECRET_STORE_KIND, | ||
| 720 | displayName: "SQLite", | ||
| 721 | canCreate: false, | ||
| 722 | }, | ||
| 723 | ], | ||
| 724 | defaultSecretStoreId: SQLITE_SECRET_STORE_ID, | ||
| 725 | }), | ||
| 726 | }, | ||
| 727 | close: async () => { | ||
| 728 | store.close(); | ||
| 729 | }, | ||
| 730 | } satisfies ExecutorBackendRepositories; | ||
| 731 | }, | ||
| 732 | }); | ||
| 733 | }; | ||
| @@ -1,15 +0,0 @@ | |||
| 1 | { | ||
| 2 | "compilerOptions": { | ||
| 3 | "target": "ES2022", | ||
| 4 | "module": "ESNext", | ||
| 5 | "moduleResolution": "Bundler", | ||
| 6 | "strict": true, | ||
| 7 | "skipLibCheck": true, | ||
| 8 | "noEmit": true, | ||
| 9 | "lib": ["ES2022"], | ||
| 10 | "types": ["bun-types", "node"] | ||
| 11 | }, | ||
| 12 | "include": [ | ||
| 13 | "src/**/*.ts" | ||
| 14 | ] | ||
| 15 | } | ||