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 | −} | |