From 38441eb8f186ba475dfe3f0411389b97e26afc72 Mon Sep 17 00:00:00 2001 From: Suleiman Shahbari Date: Fri, 26 Jun 2026 19:00:56 +0300 Subject: [PATCH] docs(mcp): add framework-neutral quickstart example (Phase 3) Closes #21. A runnable, CI-verified quickstart proving @gemstack/mcp stands alone with zero @rudderjs/* packages: one tool + resource + prompt, @Handle DI via createResolver (no container), and OAuth 2.1, served over both raw node:http and Hono. - examples/mcp-quickstart: server.ts (definitions + DI + verifyToken), node-http.ts (OAuth-protected raw node:http + a small res adapter), hono.ts (same server on the Fetch handler), quickstart.test.ts (CI smoke: authed round-trip, 401 without token, Hono mount). Private, not published. - Wire examples/* into the workspace; example is in turbo typecheck+test but excluded from build/publish. - README: Hono mounting snippet, an Awilix resolver-adapter snippet, and a pointer to the runnable example. Verified: full turbo build/typecheck/test green; example smoke 3/3; frozen lockfile clean; tsx entry boots and returns 401 without a token. --- examples/mcp-quickstart/README.md | 51 +++ examples/mcp-quickstart/package.json | 27 ++ examples/mcp-quickstart/src/hono.ts | 29 ++ examples/mcp-quickstart/src/node-http.ts | 50 +++ .../mcp-quickstart/src/quickstart.test.ts | 95 ++++++ examples/mcp-quickstart/src/server.ts | 74 ++++ examples/mcp-quickstart/tsconfig.json | 5 + examples/mcp-quickstart/tsconfig.test.json | 5 + packages/mcp/README.md | 28 +- pnpm-lock.yaml | 318 ++++++++++++++++++ pnpm-workspace.yaml | 1 + 11 files changed, 681 insertions(+), 2 deletions(-) create mode 100644 examples/mcp-quickstart/README.md create mode 100644 examples/mcp-quickstart/package.json create mode 100644 examples/mcp-quickstart/src/hono.ts create mode 100644 examples/mcp-quickstart/src/node-http.ts create mode 100644 examples/mcp-quickstart/src/quickstart.test.ts create mode 100644 examples/mcp-quickstart/src/server.ts create mode 100644 examples/mcp-quickstart/tsconfig.json create mode 100644 examples/mcp-quickstart/tsconfig.test.json diff --git a/examples/mcp-quickstart/README.md b/examples/mcp-quickstart/README.md new file mode 100644 index 0000000..6027048 --- /dev/null +++ b/examples/mcp-quickstart/README.md @@ -0,0 +1,51 @@ +# @gemstack/mcp quickstart + +A runnable, framework-neutral MCP server built with `@gemstack/mcp` and **zero `@rudderjs/*` packages**. It proves the "agent-agnostic, standalone" claim: one tool, one resource, one prompt, dependency injection without a container, and OAuth 2.1 protection, served over both raw `node:http` and Hono. + +## What's here + +| File | Shows | +|---|---| +| `src/server.ts` | Define a tool / resource / prompt; inject a service with `@Handle` + `createResolver` (no DI container); supply a `verifyToken` for OAuth. | +| `src/node-http.ts` | Serve it over raw `node:http`, protected by OAuth 2.1, via a ~10-line `res` adapter. | +| `src/hono.ts` | Serve the same server on Hono via the Fetch-style `createWebRequestHandler`. | +| `src/quickstart.test.ts` | A CI smoke: authenticated round-trip, a `401` for missing token, and the Hono mount. | + +## Run it + +From this directory (after `pnpm install` at the repo root): + +```bash +# raw node:http, OAuth-protected, on :3000 +pnpm start:node + +# the same server on Hono +pnpm start:hono +``` + +Then drive it with any MCP client pointed at `http://localhost:3000/mcp`. For the `node:http` server, send `Authorization: Bearer demo-token` (see `DEMO_TOKEN` in `src/server.ts`). + +## Verify (CI) + +```bash +pnpm test +``` + +This boots the servers on ephemeral ports and runs a real MCP session against each (the SDK `Client` over `StreamableHTTPClientTransport`), asserting the authenticated call succeeds, an unauthenticated call is rejected with `401`, and the Hono mount serves the same tools. + +## Dependency injection + +`makeServer()` passes an **instance-scoped** resolver to the server. `createResolver()` needs no container. To back it with a real container, implement the one-method `McpResolver` over it: + +```ts +import { createContainer, asValue } from 'awilix' +import type { McpResolver } from '@gemstack/mcp' + +const container = createContainer().register({ greeter: asValue(new Greeter()) }) +const resolver: McpResolver = { resolve: (token) => container.resolve((token as { name: string }).name) } +new QuickstartServer({ resolver }) +``` + +## OAuth on the Fetch path + +`oauth2McpMiddleware` is Connect-shaped, so `src/node-http.ts` uses it directly. To protect the Hono/Fetch mount, read the `Authorization` header in a Hono middleware and call the same `verifyToken` before delegating to the handler. diff --git a/examples/mcp-quickstart/package.json b/examples/mcp-quickstart/package.json new file mode 100644 index 0000000..8f67be3 --- /dev/null +++ b/examples/mcp-quickstart/package.json @@ -0,0 +1,27 @@ +{ + "name": "@gemstack/example-mcp-quickstart", + "version": "0.0.0", + "private": true, + "description": "Runnable framework-neutral quickstart for @gemstack/mcp: a protected MCP server on node:http and Hono, with zero @rudderjs/* packages.", + "type": "module", + "scripts": { + "typecheck": "tsc --noEmit", + "test": "tsc -p tsconfig.test.json && cd dist-test && node --test", + "clean": "rm -rf dist-test", + "start:node": "tsx src/node-http.ts", + "start:hono": "tsx src/hono.ts" + }, + "dependencies": { + "@gemstack/mcp": "workspace:^", + "reflect-metadata": "^0.2.0", + "zod": "^4.0.0" + }, + "devDependencies": { + "@hono/node-server": "^1.13.0", + "@modelcontextprotocol/sdk": "^1.29.0", + "@types/node": "^20.0.0", + "hono": "^4.6.0", + "tsx": "^4.19.0", + "typescript": "^5.4.0" + } +} diff --git a/examples/mcp-quickstart/src/hono.ts b/examples/mcp-quickstart/src/hono.ts new file mode 100644 index 0000000..d5a9356 --- /dev/null +++ b/examples/mcp-quickstart/src/hono.ts @@ -0,0 +1,29 @@ +import 'reflect-metadata' +import { fileURLToPath } from 'node:url' +import { Hono } from 'hono' +import { createWebRequestHandler } from '@gemstack/mcp/runtime' +import { makeServer } from './server.js' + +// The same server, mounted on a framework via the Fetch-style handler. This +// proves @gemstack/mcp is transport-agnostic: createWebRequestHandler returns a +// `(Request) => Promise`, which is what Hono (and Vike, Bun, Deno, +// Cloudflare Workers) speak natively. +// +// This demo mount is unprotected to keep it short. To protect the Fetch path, +// read the Authorization header in a Hono middleware and call the SAME +// verifyToken from ./server.js before delegating to the handler. +export function createHonoApp(): Hono { + const app = new Hono() + const handler = createWebRequestHandler(makeServer()) + app.all('/mcp', (c) => handler(c.req.raw)) + return app +} + +// Runnable on Node via @hono/node-server: `npx tsx src/hono.ts`. +if (process.argv[1] && process.argv[1] === fileURLToPath(import.meta.url)) { + const { serve } = await import('@hono/node-server') + const port = Number(process.env.PORT ?? 3000) + serve({ fetch: createHonoApp().fetch, port }, (info) => { + console.log(`MCP server on http://localhost:${info.port}/mcp (Hono)`) + }) +} diff --git a/examples/mcp-quickstart/src/node-http.ts b/examples/mcp-quickstart/src/node-http.ts new file mode 100644 index 0000000..67bf9b1 --- /dev/null +++ b/examples/mcp-quickstart/src/node-http.ts @@ -0,0 +1,50 @@ +import 'reflect-metadata' +import { createServer, type IncomingMessage, type ServerResponse } from 'node:http' +import { fileURLToPath } from 'node:url' +import { + createMcpHttpHandler, oauth2McpMiddleware, + type OAuth2Request, type OAuth2Response, +} from '@gemstack/mcp' +import { makeServer, verifyToken, REQUIRED_SCOPES } from './server.js' + +const MCP_PATH = '/mcp' + +// The OAuth middleware is Connect-shaped (req, res, next) with an Express-like +// `res`. node:http's ServerResponse isn't Express-shaped, so adapt it. This tiny +// adapter is the only glue needed to protect a raw node:http server. +function asOAuth2Res(res: ServerResponse): OAuth2Response { + const extra: Record = {} + return { + header(key, value) { extra[key] = value }, + status(code) { + return { + json(data: unknown) { + res.writeHead(code, { 'content-type': 'application/json', ...extra }) + res.end(JSON.stringify(data)) + }, + } + }, + } +} + +// A plain (req, res) handler: OAuth first, then the MCP transport. Mounts on +// node:http directly; the same shape works on Express/Connect. +export function createNodeHandler(): (req: IncomingMessage, res: ServerResponse) => void { + const mcp = createMcpHttpHandler(makeServer()) + const auth = oauth2McpMiddleware(MCP_PATH, { + scopes: REQUIRED_SCOPES, + scopesSupported: ['mcp.read', 'mcp.write'], + verifyToken, + }) + return (req, res) => { + void auth(req as unknown as OAuth2Request, asOAuth2Res(res), () => { void mcp(req, res) }) + } +} + +// Runnable entry: `npx tsx src/node-http.ts` (or run the compiled file). +if (process.argv[1] && process.argv[1] === fileURLToPath(import.meta.url)) { + const port = Number(process.env.PORT ?? 3000) + createServer(createNodeHandler()).listen(port, () => { + console.log(`MCP server on http://localhost:${port}${MCP_PATH} (Bearer token required)`) + }) +} diff --git a/examples/mcp-quickstart/src/quickstart.test.ts b/examples/mcp-quickstart/src/quickstart.test.ts new file mode 100644 index 0000000..ccfafc6 --- /dev/null +++ b/examples/mcp-quickstart/src/quickstart.test.ts @@ -0,0 +1,95 @@ +import 'reflect-metadata' +import { describe, it, before, after } from 'node:test' +import assert from 'node:assert/strict' +import { createServer, type Server } from 'node:http' +import { Client } from '@modelcontextprotocol/sdk/client/index.js' +import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js' +import { serve } from '@hono/node-server' +import { createNodeHandler } from './node-http.js' +import { createHonoApp } from './hono.js' +import { DEMO_TOKEN } from './server.js' + +// Drive a full MCP session (initialize handshake + tools/call) with the real +// SDK client, optionally sending a bearer token on every request. +async function roundTrip(baseUrl: string, token?: string): Promise<{ toolNames: string[]; text: string }> { + const client = new Client({ name: 'quickstart-test', version: '1.0.0' }, { capabilities: {} }) + const transport = new StreamableHTTPClientTransport( + new URL(`${baseUrl}/mcp`), + token ? { requestInit: { headers: { Authorization: `Bearer ${token}` } } } : undefined, + ) + // `as never`: the SDK Transport type trips exactOptionalPropertyTypes but is + // runtime-compatible (see the package's own acceptance test). + await client.connect(transport as never) + try { + const list = await client.listTools() + const call = await client.callTool({ name: 'greet', arguments: { name: 'Ada' } }) + const content = call.content as Array<{ type: string; text: string }> + return { toolNames: list.tools.map((t) => t.name), text: content[0]!.text } + } finally { + await client.close().catch(() => {}) + await transport.close().catch(() => {}) + } +} + +function listen(server: Server): Promise { + return new Promise((resolve) => + server.listen(0, '127.0.0.1', () => resolve((server.address() as { port: number }).port))) +} + +describe('quickstart: node:http (OAuth-protected)', () => { + let server: Server + let baseUrl: string + + before(async () => { + server = createServer(createNodeHandler()) + baseUrl = `http://127.0.0.1:${await listen(server)}` + }) + + after(async () => { + await new Promise((r) => { server.close(() => r()); server.closeAllConnections?.() }) + }) + + it('rejects an unauthenticated request with 401 + WWW-Authenticate', async () => { + const res = await fetch(`${baseUrl}/mcp`, { + method: 'POST', + headers: { 'content-type': 'application/json', accept: 'application/json, text/event-stream' }, + body: JSON.stringify({ + jsonrpc: '2.0', id: 1, method: 'initialize', + params: { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'x', version: '1' } }, + }), + }) + assert.equal(res.status, 401) + assert.match(res.headers.get('www-authenticate') ?? '', /invalid_token/) + await res.text() + }) + + it('serves tools/call with a valid bearer token (DI resolved)', async () => { + const { toolNames, text } = await roundTrip(baseUrl, DEMO_TOKEN) + assert.ok(toolNames.includes('greet')) + assert.match(text, /Hello, Ada!/) + }) +}) + +describe('quickstart: Hono (Fetch transport)', () => { + let server: ReturnType + let baseUrl: string + + before(async () => { + await new Promise((resolve) => { + server = serve({ fetch: createHonoApp().fetch, port: 0 }, (info) => { + baseUrl = `http://127.0.0.1:${info.port}` + resolve() + }) + }) + }) + + after(async () => { + await new Promise((r) => (server as unknown as Server).close(() => r())) + }) + + it('serves the same MCP server mounted on a framework', async () => { + const { toolNames, text } = await roundTrip(baseUrl) // demo Hono mount is unprotected + assert.ok(toolNames.includes('greet')) + assert.match(text, /Hello, Ada!/) + }) +}) diff --git a/examples/mcp-quickstart/src/server.ts b/examples/mcp-quickstart/src/server.ts new file mode 100644 index 0000000..6ba4cfb --- /dev/null +++ b/examples/mcp-quickstart/src/server.ts @@ -0,0 +1,74 @@ +import 'reflect-metadata' +import { + McpServer, McpTool, McpResource, McpPrompt, McpResponse, + Name, Version, Instructions, Description, Handle, + createResolver, + type McpResolver, type VerifyToken, +} from '@gemstack/mcp' +import { z } from 'zod' + +// A plain service. No framework, no container, no AI runtime. The tool below +// asks for it by type via @Handle, and the server's resolver provides it. +export class Greeter { + greet(name: string): string { + return `Hello, ${name}! Served by @gemstack/mcp with zero framework.` + } +} + +@Description('Greet someone by name') +class GreetTool extends McpTool { + schema() { + return z.object({ name: z.string().describe('Who to greet') }) + } + + // @Handle injects the Greeter (resolved from the server's resolver) after the + // validated input. The token is explicit, so no decorator metadata is needed. + @Handle(Greeter) + async handle(input: { name: string }, greeter: Greeter) { + return McpResponse.text(greeter.greet(input.name)) + } +} + +@Description('The server version, exposed as a readable resource') +class VersionResource extends McpResource { + uri() { return 'info://version' } + async handle() { return '1.0.0' } +} + +@Description('A reusable greeting prompt') +class GreetingPrompt extends McpPrompt { + arguments() { return z.object({ name: z.string() }) } + async handle(args: { name: string }) { + return [{ role: 'user' as const, content: `Please greet ${args.name} warmly.` }] + } +} + +@Name('quickstart') +@Version('1.0.0') +@Instructions('A demo MCP server: one tool, one resource, one prompt. No Rudder, no AI runtime.') +class QuickstartServer extends McpServer { + protected tools = [GreetTool] + protected resources = [VersionResource] + protected prompts = [GreetingPrompt] +} + +// Build a fully-wired server instance. The resolver is INSTANCE-SCOPED: it is +// passed at construction and never read off a global. createResolver() needs no +// DI container; to use one, implement McpResolver = { resolve(token) } over it +// (see the README for an Awilix/tsyringe adapter). +export function makeServer(): McpServer { + const resolver: McpResolver = createResolver().register(Greeter, new Greeter()) + return new QuickstartServer({ resolver }) +} + +// ─── OAuth 2.1 ──────────────────────────────────────────── +// The core is auth-agnostic: you supply verifyToken. Here we accept a single +// demo token and grant it the read scope. In production, validate the JWT +// (signature, expiry, revocation) and return its real claims, or null/throw. +export const REQUIRED_SCOPES = ['mcp.read'] +export const DEMO_TOKEN = 'demo-token' + +export const verifyToken: VerifyToken = (jwt) => { + if (jwt === DEMO_TOKEN) return { sub: 'demo-user', scopes: ['mcp.read'] } + return null +} diff --git a/examples/mcp-quickstart/tsconfig.json b/examples/mcp-quickstart/tsconfig.json new file mode 100644 index 0000000..404aab4 --- /dev/null +++ b/examples/mcp-quickstart/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { "noEmit": true, "rootDir": "src" }, + "include": ["src"] +} diff --git a/examples/mcp-quickstart/tsconfig.test.json b/examples/mcp-quickstart/tsconfig.test.json new file mode 100644 index 0000000..eebda2f --- /dev/null +++ b/examples/mcp-quickstart/tsconfig.test.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { "outDir": "dist-test", "rootDir": "src" }, + "include": ["src"] +} diff --git a/packages/mcp/README.md b/packages/mcp/README.md index 18db78a..80a5cc9 100644 --- a/packages/mcp/README.md +++ b/packages/mcp/README.md @@ -57,7 +57,20 @@ const handler = createMcpHttpHandler(new DemoServer()) createServer((req, res) => { void handler(req, res) }).listen(3000) ``` -`createMcpHttpHandler` returns a plain `(req, res)` handler, so it also mounts on Express/Connect. For Hono, Vike, or any Fetch-style runtime, use `createWebRequestHandler` from `@gemstack/mcp/runtime` (`(request: Request) => Promise`). For a CLI/stdio server, use `startStdio` from the same subpath. +`createMcpHttpHandler` returns a plain `(req, res)` handler, so it also mounts on Express/Connect. For Hono, Vike, or any Fetch-style runtime, use `createWebRequestHandler` from `@gemstack/mcp/runtime` (`(request: Request) => Promise`): + +```ts +import { Hono } from 'hono' +import { createWebRequestHandler } from '@gemstack/mcp/runtime' + +const handler = createWebRequestHandler(new DemoServer()) +const app = new Hono() +app.all('/mcp', (c) => handler(c.req.raw)) +``` + +For a CLI/stdio server, use `startStdio` from the same subpath. + +> **Runnable example:** [`examples/mcp-quickstart`](../../examples/mcp-quickstart) is a complete, framework-neutral server (tool + resource + prompt, `@Handle` DI, OAuth 2.1) served over both `node:http` and Hono, with a CI smoke test, and **zero `@rudderjs/*` packages**. ### Resources and prompts @@ -103,7 +116,18 @@ const resolver = createResolver().register(Logger, new Logger()) const server = new LogServer({ resolver }) ``` -The resolver is **instance-scoped** — passed at construction, never read off a global. Wire it to any container (Awilix, tsyringe, InversifyJS, a framework binding) with a one-function adapter implementing `McpResolver = { resolve(token): unknown }`. If a `@Handle` method requests a dependency and no resolver is provided — or the resolver yields `undefined` — the call fails loudly, naming the member and token; it never injects `undefined`. +The resolver is **instance-scoped** — passed at construction, never read off a global. Wire it to any container (Awilix, tsyringe, InversifyJS, a framework binding) with a one-function adapter implementing `McpResolver = { resolve(token): unknown }`: + +```ts +import { createContainer, asValue } from 'awilix' +import type { McpResolver } from '@gemstack/mcp' + +const container = createContainer().register({ logger: asValue(new Logger()) }) +const resolver: McpResolver = { resolve: (token) => container.resolve((token as { name: string }).name) } +new LogServer({ resolver }) +``` + +If a `@Handle` method requests a dependency and no resolver is provided — or the resolver yields `undefined` — the call fails loudly, naming the member and token; it never injects `undefined`. ## OAuth 2.1 diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 910ed8c..cb73d56 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,6 +18,37 @@ importers: specifier: ^5.4.0 version: 5.9.3 + examples/mcp-quickstart: + dependencies: + '@gemstack/mcp': + specifier: workspace:^ + version: link:../../packages/mcp + reflect-metadata: + specifier: ^0.2.0 + version: 0.2.2 + zod: + specifier: ^4.0.0 + version: 4.4.3 + devDependencies: + '@hono/node-server': + specifier: ^1.13.0 + version: 1.19.14(hono@4.12.27) + '@modelcontextprotocol/sdk': + specifier: ^1.29.0 + version: 1.29.0(zod@4.4.3) + '@types/node': + specifier: ^20.0.0 + version: 20.19.43 + hono: + specifier: ^4.6.0 + version: 4.12.27 + tsx: + specifier: ^4.19.0 + version: 4.22.4 + typescript: + specifier: ^5.4.0 + version: 5.9.3 + packages/ai-autopilot: dependencies: '@gemstack/ai-sdk': @@ -314,6 +345,162 @@ packages: resolution: {integrity: sha512-EYlRokl8szrP9Z25qT5aepMdBjzBvHF9ZEhzIiUBc9guz/T31EqRgvD0QSgZcpE93xiwrr+OkB4nz0BZyF6fSA==} engines: {node: '>= 20.12.0'} + '@esbuild/aix-ppc64@0.28.1': + resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.1': + resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.1': + resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.1': + resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.1': + resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.1': + resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.1': + resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.1': + resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.1': + resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.1': + resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.1': + resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.1': + resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.28.1': + resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.28.1': + resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.1': + resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.1': + resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.1': + resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.1': + resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.1': + resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.1': + resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.1': + resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.1': + resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.1': + resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.1': + resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.1': + resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.1': + resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@google/genai@2.10.0': resolution: {integrity: sha512-e4cFxj3tiuMtsgOT4G9c1hXyGJhg7/Buj7VVeBacRY3fRtkRZZ59Q3nuVp2xbq8BGQXLXCDB253qMhklMOeUDg==} engines: {node: '>=20.0.0'} @@ -736,6 +923,11 @@ packages: resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} engines: {node: '>= 0.4'} + esbuild@0.28.1: + resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} + engines: {node: '>=18'} + hasBin: true + escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} @@ -853,6 +1045,11 @@ packages: resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} engines: {node: '>=6 <7 || >=8'} + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} @@ -1335,6 +1532,11 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsx@4.22.4: + resolution: {integrity: sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==} + engines: {node: '>=18.0.0'} + hasBin: true + turbo@2.9.18: resolution: {integrity: sha512-bwabv6PupzeavybzEoArBAkwq5fnzwf8OFnRtpHwnviFWuwJPFxtyH+aVp36TmIqK3aYYgtTJ3J0m2ysxxSzQg==} hasBin: true @@ -1819,6 +2021,84 @@ snapshots: fast-wrap-ansi: 0.2.2 sisteransi: 1.0.5 + '@esbuild/aix-ppc64@0.28.1': + optional: true + + '@esbuild/android-arm64@0.28.1': + optional: true + + '@esbuild/android-arm@0.28.1': + optional: true + + '@esbuild/android-x64@0.28.1': + optional: true + + '@esbuild/darwin-arm64@0.28.1': + optional: true + + '@esbuild/darwin-x64@0.28.1': + optional: true + + '@esbuild/freebsd-arm64@0.28.1': + optional: true + + '@esbuild/freebsd-x64@0.28.1': + optional: true + + '@esbuild/linux-arm64@0.28.1': + optional: true + + '@esbuild/linux-arm@0.28.1': + optional: true + + '@esbuild/linux-ia32@0.28.1': + optional: true + + '@esbuild/linux-loong64@0.28.1': + optional: true + + '@esbuild/linux-mips64el@0.28.1': + optional: true + + '@esbuild/linux-ppc64@0.28.1': + optional: true + + '@esbuild/linux-riscv64@0.28.1': + optional: true + + '@esbuild/linux-s390x@0.28.1': + optional: true + + '@esbuild/linux-x64@0.28.1': + optional: true + + '@esbuild/netbsd-arm64@0.28.1': + optional: true + + '@esbuild/netbsd-x64@0.28.1': + optional: true + + '@esbuild/openbsd-arm64@0.28.1': + optional: true + + '@esbuild/openbsd-x64@0.28.1': + optional: true + + '@esbuild/openharmony-arm64@0.28.1': + optional: true + + '@esbuild/sunos-x64@0.28.1': + optional: true + + '@esbuild/win32-arm64@0.28.1': + optional: true + + '@esbuild/win32-ia32@0.28.1': + optional: true + + '@esbuild/win32-x64@0.28.1': + optional: true + '@google/genai@2.10.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))': dependencies: google-auth-library: 10.7.0 @@ -2243,6 +2523,35 @@ snapshots: hasown: 2.0.4 optional: true + esbuild@0.28.1: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.1 + '@esbuild/android-arm': 0.28.1 + '@esbuild/android-arm64': 0.28.1 + '@esbuild/android-x64': 0.28.1 + '@esbuild/darwin-arm64': 0.28.1 + '@esbuild/darwin-x64': 0.28.1 + '@esbuild/freebsd-arm64': 0.28.1 + '@esbuild/freebsd-x64': 0.28.1 + '@esbuild/linux-arm': 0.28.1 + '@esbuild/linux-arm64': 0.28.1 + '@esbuild/linux-ia32': 0.28.1 + '@esbuild/linux-loong64': 0.28.1 + '@esbuild/linux-mips64el': 0.28.1 + '@esbuild/linux-ppc64': 0.28.1 + '@esbuild/linux-riscv64': 0.28.1 + '@esbuild/linux-s390x': 0.28.1 + '@esbuild/linux-x64': 0.28.1 + '@esbuild/netbsd-arm64': 0.28.1 + '@esbuild/netbsd-x64': 0.28.1 + '@esbuild/openbsd-arm64': 0.28.1 + '@esbuild/openbsd-x64': 0.28.1 + '@esbuild/openharmony-arm64': 0.28.1 + '@esbuild/sunos-x64': 0.28.1 + '@esbuild/win32-arm64': 0.28.1 + '@esbuild/win32-ia32': 0.28.1 + '@esbuild/win32-x64': 0.28.1 + escape-html@1.0.3: {} esprima@4.0.1: {} @@ -2395,6 +2704,9 @@ snapshots: jsonfile: 4.0.0 universalify: 0.1.2 + fsevents@2.3.3: + optional: true + function-bind@1.1.2: {} gaxios@7.1.5: @@ -2890,6 +3202,12 @@ snapshots: tslib@2.8.1: optional: true + tsx@4.22.4: + dependencies: + esbuild: 0.28.1 + optionalDependencies: + fsevents: 2.3.3 + turbo@2.9.18: optionalDependencies: '@turbo/darwin-64': 2.9.18 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 04a7116..b1af21d 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,5 +1,6 @@ packages: - packages/* + - examples/* # pnpm 11 prompts before purging node_modules on a store-version change; # disable so non-TTY installs (CI, scripts) don't abort.