duckpond-mcp-server 0.4.4 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # DuckPond MCP Server
2
2
 
3
- [![Node.js CI](https://github.com/jordanburke/duckpond-mcp-server/actions/workflows/node.js.yml/badge.svg)](https://github.com/jordanburke/duckpond-mcp-server/actions/workflows/node.js.yml)
4
- [![CodeQL](https://github.com/jordanburke/duckpond-mcp-server/actions/workflows/codeql.yml/badge.svg)](https://github.com/jordanburke/duckpond-mcp-server/actions/workflows/codeql.yml)
3
+ [![CI](https://github.com/jordanburke/duckpond/actions/workflows/ci.yml/badge.svg)](https://github.com/jordanburke/duckpond/actions/workflows/ci.yml)
4
+ [![CodeQL](https://github.com/jordanburke/duckpond/actions/workflows/codeql.yml/badge.svg)](https://github.com/jordanburke/duckpond/actions/workflows/codeql.yml)
5
5
 
6
6
  **Model Context Protocol (MCP) server for multi-tenant DuckDB management with R2/S3 cloud storage.**
7
7
 
@@ -506,7 +506,7 @@ The MCP server is a **thin transport layer** over the [duckpond](https://github.
506
506
  └───────┬────────┘
507
507
 
508
508
  ┌───────▼────────┐
509
- │ DuckPond │ npm: duckpond@^0.1.0
509
+ │ DuckPond │ workspace:^ duckpond
510
510
  │ - Multi-tenant │
511
511
  │ - LRU Cache │
512
512
  │ - R2/S3 │
@@ -520,11 +520,12 @@ The MCP server is a **thin transport layer** over the [duckpond](https://github.
520
520
 
521
521
  ### Key Components
522
522
 
523
- - **`src/index.ts`** - CLI entry point, transport selection
524
- - **`src/server-core.ts`** - DuckPond wrapper with MCP result types
525
- - **`src/server-stdio.ts`** - stdio transport for Claude Desktop
526
- - **`src/server-fastmcp.ts`** - HTTP transport with FastMCP
527
- - **`src/tools/index.ts`** - MCP tool schemas and implementations
523
+ - **`src/index.ts`** - CLI entry point (commander), transport selection
524
+ - **`src/server-core.ts`** - DuckPond wrapper exposing `MCPResult<T>` tool result types
525
+ - **`src/server.ts`** - FastMCP-based HTTP transport with OAuth 2.0 / JWT auth
526
+ - **`src/ui-server.ts`** - Hono server bridging to the built-in DuckDB UI
527
+ - **`src/tools/index.ts`** - MCP tool schemas and implementations (zod)
528
+ - **`src/lib.ts`** - Library exports (`startServer`, hooks) for extending the server (`duckpond-mcp-server/lib`)
528
529
 
529
530
  ### Error Handling
530
531
 
@@ -636,7 +637,7 @@ console.log(`Memory: ${stats.memoryUsage} bytes`)
636
637
 
637
638
  ## Contributing
638
639
 
639
- Contributions welcome! Please see [CONTRIBUTING.md](./CONTRIBUTING.md) for guidelines.
640
+ Contributions welcome! This package lives in the [duckpond monorepo](https://github.com/jordanburke/duckpond) — see the monorepo README for development setup. Please ensure `pnpm validate` passes before opening a PR.
640
641
 
641
642
  ## License
642
643
 
@@ -652,4 +653,4 @@ MIT
652
653
 
653
654
  - **Issues**: [GitHub Issues](https://github.com/jordanburke/duckpond-mcp-server/issues)
654
655
  - **Discussions**: [GitHub Discussions](https://github.com/jordanburke/duckpond-mcp-server/discussions)
655
- - **Documentation**: [docs/](./docs/)
656
+ - **Documentation**: [docs/](https://github.com/jordanburke/duckpond/tree/main/packages/duckpond-mcp-server/docs)
@@ -0,0 +1,23 @@
1
+ import { DuckPondServerConfig } from "../server-core.js";
2
+
3
+ //#region src/config/path-resolver.d.ts
4
+ /**
5
+ * Default local data directory, used only when DUCKPOND_DATA_DIR is unset.
6
+ */
7
+ declare function getDefaultDataDir(): string;
8
+ /**
9
+ * Resolve a raw DUCKPOND_DATA_DIR value to an absolute path.
10
+ *
11
+ * Expands `~`, then platform tokens (`$OneDrive`, `$WINHOME`, …), then any
12
+ * remaining real `$VAR`/`${VAR}` env references. Throws with a descriptive
13
+ * message if a platform token is present but cannot be resolved — failing fast
14
+ * is preferable to silently creating a database in the wrong location.
15
+ */
16
+ declare function resolveDataDir(raw: string): string;
17
+ /**
18
+ * Parse environment variables into DuckPond configuration.
19
+ */
20
+ declare function getConfigFromEnv(): DuckPondServerConfig;
21
+ //#endregion
22
+ export { getConfigFromEnv, getDefaultDataDir, resolveDataDir };
23
+ //# sourceMappingURL=path-resolver.d.ts.map
@@ -0,0 +1,2 @@
1
+ import{basename as e}from"node:path";import{Env as t,Path as n,Platform as r}from"functype-os";function i(){return`${process.env.HOME??process.env.USERPROFILE??`.`}/.duckpond/data`}function a(e){for(let n of e){let e=t.get(n).orElse(``);if(e!==``)return e}}function o(t){return/\bOneDrive\s+-\s+/i.test(e(t))}function s(e,t){let n=r.cloudStorageDirs().toArray().filter(t=>t.provider===e).map(e=>e.path),i=t===void 0?n:n.filter(t),a=i.length>0?i:n;if(a.length===0)return;let o=r.windowsHomeDir().orUndefined();return(o===void 0?void 0:a.find(e=>e.startsWith(o)))??a[0]}function c(){return s(`onedrive`,e=>!o(e))??a([`OneDrive`,`OneDriveConsumer`])}function l(){return s(`onedrive`,o)??a([`OneDriveCommercial`])}const u=[{token:`WINHOME`,resolve:()=>r.windowsHomeDir().orElse(r.homeDir())},{token:`OneDriveCommercial`,resolve:l},{token:`OneDriveConsumer`,resolve:c},{token:`OneDrive`,resolve:c},{token:`GOOGLE_DRIVE`,resolve:()=>s(`gdrive`)??a([`GOOGLE_DRIVE`])},{token:`DROPBOX`,resolve:()=>s(`dropbox`)??a([`DROPBOX`,`DROPBOX_PATH`])}];function d(e,t){return RegExp(`\\$\\{?${t}\\b`,`i`).test(e)}function f(e,t,n){return e.replace(RegExp(`\\$\\{${t}\\}`,`gi`),n).replace(RegExp(`\\$${t}\\b`,`gi`),n)}function p(){return`homeDirs=[${r.homeDirs().toArray().join(`, `)}] cloudStorageDirs=[${r.cloudStorageDirs().toArray().map(e=>`${e.provider}:${e.path}`).join(`, `)}]`}function m(e){let t=n.expandTilde(e),r=u.reduce((t,{token:n,resolve:r})=>{if(!d(t,n))return t;let i=r();if(i===void 0||i===``)throw Error(`Cannot resolve "$${n}" in DUCKPOND_DATA_DIR="${e}". Searched ${p()}. Set the $${n} environment variable or use an explicit absolute path.`);return f(t,n,i)},t);return n.expandVars(r).fold(()=>r,e=>e)}function h(){let e=m(process.env.DUCKPOND_DATA_DIR??i()),t={memoryLimit:process.env.DUCKPOND_MEMORY_LIMIT??`4GB`,threads:parseInt(process.env.DUCKPOND_THREADS??`4`),maxActiveUsers:parseInt(process.env.DUCKPOND_MAX_ACTIVE_USERS??`10`),evictionTimeout:parseInt(process.env.DUCKPOND_EVICTION_TIMEOUT??`300000`),cacheType:process.env.DUCKPOND_CACHE_TYPE||`disk`,strategy:process.env.DUCKPOND_STRATEGY||`duckdb`,tempDir:process.env.DUCKPOND_TEMP_DIR,cacheDir:process.env.DUCKPOND_CACHE_DIR??e,dataDir:e};return process.env.DUCKPOND_R2_ACCOUNT_ID&&(t.r2={accountId:process.env.DUCKPOND_R2_ACCOUNT_ID,accessKeyId:process.env.DUCKPOND_R2_ACCESS_KEY_ID??``,secretAccessKey:process.env.DUCKPOND_R2_SECRET_ACCESS_KEY??``,bucket:process.env.DUCKPOND_R2_BUCKET??``}),process.env.DUCKPOND_S3_REGION&&(t.s3={region:process.env.DUCKPOND_S3_REGION,accessKeyId:process.env.DUCKPOND_S3_ACCESS_KEY_ID??``,secretAccessKey:process.env.DUCKPOND_S3_SECRET_ACCESS_KEY??``,bucket:process.env.DUCKPOND_S3_BUCKET??``},process.env.DUCKPOND_S3_ENDPOINT&&(t.s3.endpoint=process.env.DUCKPOND_S3_ENDPOINT)),t}export{h as getConfigFromEnv,i as getDefaultDataDir,m as resolveDataDir};
2
+ //# sourceMappingURL=path-resolver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"path-resolver.js","names":[],"sources":["../../src/config/path-resolver.ts"],"sourcesContent":["/**\n * Cross-platform resolution of DUCKPOND_DATA_DIR.\n *\n * Beyond plain `~` expansion, this understands platform \"tokens\" that let a\n * single config value resolve correctly on macOS, native Windows, and WSL:\n *\n * $WINHOME → the Windows home dir (on WSL, discovered under /mnt/c/Users)\n * $OneDrive → the OneDrive root (auto-discovered, else the OneDrive* env var)\n * $OneDriveCommercial → \"\n * $OneDriveConsumer → \"\n * $GOOGLE_DRIVE → the Google Drive root\n * $DROPBOX → the Dropbox root\n *\n * These tokens are NOT standard env vars on WSL, so functype-os's plain\n * `expandVars` can't resolve them — we map them to `Platform` discovery here.\n * Tokens survive Claude Code's `.mcp.json` expansion (it only expands `${VAR}`),\n * so `DUCKPOND_DATA_DIR=$OneDrive/Apps/duckdb` reaches the server intact.\n *\n * Mirrors the discovery convention in envpkt (src/core/config.ts).\n */\nimport { basename } from \"node:path\"\n\nimport { type CloudProvider, Env, Path, Platform } from \"functype-os\"\n\nimport type { DuckPondServerConfig } from \"../server-core\"\n\n/**\n * Default local data directory, used only when DUCKPOND_DATA_DIR is unset.\n */\nexport function getDefaultDataDir(): string {\n const home = process.env.HOME ?? process.env.USERPROFILE ?? \".\"\n return `${home}/.duckpond/data`\n}\n\n/** First non-empty value among the given env var names. */\nfunction firstEnv(names: ReadonlyArray<string>): string | undefined {\n for (const name of names) {\n const value = Env.get(name).orElse(\"\")\n if (value !== \"\") {\n return value\n }\n }\n return undefined\n}\n\n/** A commercial/work OneDrive has an org suffix, e.g. \"OneDrive - Contoso\". */\nfunction isCommercialOneDrive(p: string): boolean {\n return /\\bOneDrive\\s+-\\s+/i.test(basename(p))\n}\n\n/**\n * Pick the best discovered dir for a cloud provider. `homeDirs()` is scanned in\n * order (on WSL: Linux home first, then Windows home), so a provider can appear\n * multiple times. We prefer the copy under the Windows home on WSL — that's the\n * real synced location — and otherwise take the first match.\n */\nfunction selectCloud(provider: CloudProvider, filter?: (p: string) => boolean): string | undefined {\n const all = Platform.cloudStorageDirs()\n .toArray()\n .filter((d) => d.provider === provider)\n .map((d) => d.path)\n const matched = filter === undefined ? all : all.filter(filter)\n const pool = matched.length > 0 ? matched : all\n if (pool.length === 0) {\n return undefined\n }\n const winHome = Platform.windowsHomeDir().orUndefined()\n const winPreferred = winHome === undefined ? undefined : pool.find((p) => p.startsWith(winHome))\n return winPreferred ?? pool[0]\n}\n\nfunction oneDrivePersonal(): string | undefined {\n return selectCloud(\"onedrive\", (p) => !isCommercialOneDrive(p)) ?? firstEnv([\"OneDrive\", \"OneDriveConsumer\"])\n}\n\nfunction oneDriveCommercial(): string | undefined {\n return selectCloud(\"onedrive\", isCommercialOneDrive) ?? firstEnv([\"OneDriveCommercial\"])\n}\n\n/**\n * Platform tokens, ordered longest-first so a prefix token (e.g. `$OneDrive`)\n * never partially matches a longer one (`$OneDriveCommercial`). Each resolver\n * returns the absolute path, or undefined when it cannot be located.\n */\nconst PLATFORM_TOKENS: ReadonlyArray<{ token: string; resolve: () => string | undefined }> = [\n { token: \"WINHOME\", resolve: () => Platform.windowsHomeDir().orElse(Platform.homeDir()) },\n { token: \"OneDriveCommercial\", resolve: oneDriveCommercial },\n { token: \"OneDriveConsumer\", resolve: oneDrivePersonal },\n { token: \"OneDrive\", resolve: oneDrivePersonal },\n { token: \"GOOGLE_DRIVE\", resolve: () => selectCloud(\"gdrive\") ?? firstEnv([\"GOOGLE_DRIVE\"]) },\n { token: \"DROPBOX\", resolve: () => selectCloud(\"dropbox\") ?? firstEnv([\"DROPBOX\", \"DROPBOX_PATH\"]) },\n]\n\nfunction hasToken(input: string, token: string): boolean {\n return new RegExp(`\\\\$\\\\{?${token}\\\\b`, \"i\").test(input)\n}\n\nfunction replaceToken(input: string, token: string, value: string): string {\n return input.replace(new RegExp(`\\\\$\\\\{${token}\\\\}`, \"gi\"), value).replace(new RegExp(`\\\\$${token}\\\\b`, \"gi\"), value)\n}\n\nfunction describeSearch(): string {\n const homes = Platform.homeDirs().toArray().join(\", \")\n const clouds = Platform.cloudStorageDirs()\n .toArray()\n .map((d) => `${d.provider}:${d.path}`)\n .join(\", \")\n return `homeDirs=[${homes}] cloudStorageDirs=[${clouds}]`\n}\n\n/**\n * Resolve a raw DUCKPOND_DATA_DIR value to an absolute path.\n *\n * Expands `~`, then platform tokens (`$OneDrive`, `$WINHOME`, …), then any\n * remaining real `$VAR`/`${VAR}` env references. Throws with a descriptive\n * message if a platform token is present but cannot be resolved — failing fast\n * is preferable to silently creating a database in the wrong location.\n */\nexport function resolveDataDir(raw: string): string {\n const tildeExpanded = Path.expandTilde(raw)\n\n const withTokens = PLATFORM_TOKENS.reduce((acc, { token, resolve }) => {\n if (!hasToken(acc, token)) {\n return acc\n }\n const value = resolve()\n if (value === undefined || value === \"\") {\n throw new Error(\n `Cannot resolve \"$${token}\" in DUCKPOND_DATA_DIR=\"${raw}\". Searched ${describeSearch()}. ` +\n `Set the $${token} environment variable or use an explicit absolute path.`,\n )\n }\n return replaceToken(acc, token, value)\n }, tildeExpanded)\n\n // Expand any remaining real env vars; keep the literal if expansion fails.\n return Path.expandVars(withTokens).fold(\n () => withTokens,\n (expanded) => expanded,\n )\n}\n\n/**\n * Parse environment variables into DuckPond configuration.\n */\nexport function getConfigFromEnv(): DuckPondServerConfig {\n // Default to local disk storage (resolve ~, platform tokens, and env vars)\n const dataDir = resolveDataDir(process.env.DUCKPOND_DATA_DIR ?? getDefaultDataDir())\n\n const config: DuckPondServerConfig = {\n memoryLimit: process.env.DUCKPOND_MEMORY_LIMIT ?? \"4GB\",\n threads: parseInt(process.env.DUCKPOND_THREADS ?? \"4\"),\n maxActiveUsers: parseInt(process.env.DUCKPOND_MAX_ACTIVE_USERS ?? \"10\"),\n evictionTimeout: parseInt(process.env.DUCKPOND_EVICTION_TIMEOUT ?? \"300000\"),\n cacheType: (process.env.DUCKPOND_CACHE_TYPE as \"disk\" | \"memory\" | \"noop\") || \"disk\",\n strategy: (process.env.DUCKPOND_STRATEGY as \"parquet\" | \"duckdb\" | \"hybrid\") || \"duckdb\",\n tempDir: process.env.DUCKPOND_TEMP_DIR,\n cacheDir: process.env.DUCKPOND_CACHE_DIR ?? dataDir,\n dataDir,\n }\n\n // R2 configuration\n if (process.env.DUCKPOND_R2_ACCOUNT_ID) {\n config.r2 = {\n accountId: process.env.DUCKPOND_R2_ACCOUNT_ID,\n accessKeyId: process.env.DUCKPOND_R2_ACCESS_KEY_ID ?? \"\",\n secretAccessKey: process.env.DUCKPOND_R2_SECRET_ACCESS_KEY ?? \"\",\n bucket: process.env.DUCKPOND_R2_BUCKET ?? \"\",\n }\n }\n\n // S3 configuration\n if (process.env.DUCKPOND_S3_REGION) {\n config.s3 = {\n region: process.env.DUCKPOND_S3_REGION,\n accessKeyId: process.env.DUCKPOND_S3_ACCESS_KEY_ID ?? \"\",\n secretAccessKey: process.env.DUCKPOND_S3_SECRET_ACCESS_KEY ?? \"\",\n bucket: process.env.DUCKPOND_S3_BUCKET ?? \"\",\n }\n\n if (process.env.DUCKPOND_S3_ENDPOINT) {\n config.s3.endpoint = process.env.DUCKPOND_S3_ENDPOINT\n }\n }\n\n return config\n}\n"],"mappings":"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"}
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env node
2
- import{loggers as e}from"./utils/logger.js";import{getDefaultUserId as t}from"./tools/index.js";import{startServer as n}from"./server.js";import{webcrypto as r}from"crypto";import{Command as i}from"commander";import{createRequire as a}from"module";globalThis.crypto||(globalThis.crypto=r);const o=a(import.meta.url)(`../package.json`),s=e.main;function c(e){return e.startsWith(`~/`)?`${process.env.HOME||process.env.USERPROFILE||`.`}${e.slice(1)}`:e}function l(){return`${process.env.HOME||process.env.USERPROFILE||`.`}/.duckpond/data`}function u(){let e=c(process.env.DUCKPOND_DATA_DIR||l()),t={memoryLimit:process.env.DUCKPOND_MEMORY_LIMIT||`4GB`,threads:parseInt(process.env.DUCKPOND_THREADS||`4`),maxActiveUsers:parseInt(process.env.DUCKPOND_MAX_ACTIVE_USERS||`10`),evictionTimeout:parseInt(process.env.DUCKPOND_EVICTION_TIMEOUT||`300000`),cacheType:process.env.DUCKPOND_CACHE_TYPE||`disk`,strategy:process.env.DUCKPOND_STRATEGY||`duckdb`,tempDir:process.env.DUCKPOND_TEMP_DIR,cacheDir:process.env.DUCKPOND_CACHE_DIR||e,dataDir:e};return process.env.DUCKPOND_R2_ACCOUNT_ID&&(t.r2={accountId:process.env.DUCKPOND_R2_ACCOUNT_ID,accessKeyId:process.env.DUCKPOND_R2_ACCESS_KEY_ID||``,secretAccessKey:process.env.DUCKPOND_R2_SECRET_ACCESS_KEY||``,bucket:process.env.DUCKPOND_R2_BUCKET||``}),process.env.DUCKPOND_S3_REGION&&(t.s3={region:process.env.DUCKPOND_S3_REGION,accessKeyId:process.env.DUCKPOND_S3_ACCESS_KEY_ID||``,secretAccessKey:process.env.DUCKPOND_S3_SECRET_ACCESS_KEY||``,bucket:process.env.DUCKPOND_S3_BUCKET||``},process.env.DUCKPOND_S3_ENDPOINT&&(t.s3.endpoint=process.env.DUCKPOND_S3_ENDPOINT)),t}const d=new i;d.name(`duckpond-mcp-server`).description(`MCP server for multi-tenant DuckDB management with R2/S3 storage`).version(o.version).option(`-t, --transport <type>`,`Transport mode: stdio or http`,`stdio`).option(`-p, --port <port>`,`HTTP port (when using http transport)`,`3000`).option(`--ui`,`Enable DuckDB UI (auto-starts for DUCKPOND_DEFAULT_USER)`).option(`--ui-port <port>`,`UI management server port, only used when no default user (default: 4000)`,`4000`).option(`--ui-internal-port <port>`,`DuckDB UI port (default: 4213)`,`4213`).action(async e=>{try{let r=u(),i=t();s(`Starting DuckPond MCP Server with ${e.transport} transport`),s(`Configuration:`,{memoryLimit:r.memoryLimit,threads:r.threads,maxActiveUsers:r.maxActiveUsers,strategy:r.strategy,dataDir:r.dataDir,tempDir:r.tempDir,cacheDir:r.cacheDir,cacheType:r.cacheType,hasR2:!!r.r2,hasS3:!!r.s3,defaultUser:i||`(not set)`}),r.r2?console.error(`☁️ Storage: Cloudflare R2`):r.s3?console.error(`☁️ Storage: AWS S3`):console.error(`💾 Storage: Local disk (${r.dataDir})`),i&&console.error(`👤 Default user: ${i}`);let a;if(process.env.DUCKPOND_OAUTH_ENABLED===`true`){let t=process.env.DUCKPOND_OAUTH_USERNAME,n=process.env.DUCKPOND_OAUTH_PASSWORD;(!t||!n)&&(console.error(`❌ OAuth enabled but DUCKPOND_OAUTH_USERNAME and DUCKPOND_OAUTH_PASSWORD are required`),process.exit(1)),a={enabled:!0,username:t,password:n,userId:process.env.DUCKPOND_OAUTH_USER_ID||t,email:process.env.DUCKPOND_OAUTH_EMAIL,issuer:process.env.DUCKPOND_OAUTH_ISSUER||`http://localhost:${parseInt(e.port)||3e3}`,resource:process.env.DUCKPOND_OAUTH_RESOURCE},console.error(`🔐 OAuth enabled with username/password authentication`),console.error(` Username: ${a.username}`),console.error(` User ID: ${a.userId}`),console.error(` ✓ Login form will be shown at authorization endpoint`)}let o;process.env.DUCKPOND_BASIC_AUTH_USERNAME&&process.env.DUCKPOND_BASIC_AUTH_PASSWORD&&(o={username:process.env.DUCKPOND_BASIC_AUTH_USERNAME,password:process.env.DUCKPOND_BASIC_AUTH_PASSWORD,userId:process.env.DUCKPOND_BASIC_AUTH_USER_ID,email:process.env.DUCKPOND_BASIC_AUTH_EMAIL},console.error(`🔐 Basic authentication enabled`),console.error(` Username: ${o.username}`),console.error(` User ID: ${o.userId||o.username}`));let c;process.env.DUCKPOND_BEARER_TOKEN&&(c={token:process.env.DUCKPOND_BEARER_TOKEN,userId:process.env.DUCKPOND_BEARER_TOKEN_USER_ID},console.error(`🔐 Bearer token authentication enabled`),c.userId&&console.error(` User ID: ${c.userId}`));let l=e.ui||process.env.DUCKPOND_UI_ENABLED===`true`,d=parseInt(e.uiPort)||4e3,f=parseInt(e.uiInternalPort)||4213;l&&e.transport===`stdio`&&(i?console.error(`🖥️ DuckDB UI will start at http://localhost:${f}`):(console.error(`🖥️ UI management server at http://localhost:${d}/ui`),console.error(` Visit /ui/:userId to start DuckDB UI for a user`))),e.transport===`stdio`||e.transport===`http`?await n({config:r,port:parseInt(e.port)||3e3,endpoint:`/mcp`,oauth:a,basicAuth:o,bearerToken:c,ui:l?{enabled:!0,port:d,internalPort:f,autoStartUser:i}:void 0},e.transport===`stdio`?`stdio`:`http`):(s(`Unknown transport: ${e.transport}`),process.exit(1))}catch(e){s(`Fatal error:`,e),console.error(`Fatal error:`,e),process.exit(1)}}),d.parse();export{};
2
+ import{loggers as e}from"./utils/logger.js";import{getDefaultUserId as t}from"./tools/index.js";import{getConfigFromEnv as n}from"./config/path-resolver.js";import{startServer as r}from"./server.js";import{webcrypto as i}from"crypto";import{Command as a}from"commander";import{createRequire as o}from"module";globalThis.crypto||(globalThis.crypto=i);const s=o(import.meta.url)(`../package.json`),c=e.main,l=new a;l.name(`duckpond-mcp-server`).description(`MCP server for multi-tenant DuckDB management with R2/S3 storage`).version(s.version).option(`-t, --transport <type>`,`Transport mode: stdio or http`,`stdio`).option(`-p, --port <port>`,`HTTP port (when using http transport)`,`3000`).option(`--ui`,`Enable DuckDB UI (auto-starts for DUCKPOND_DEFAULT_USER)`).option(`--ui-port <port>`,`UI management server port, only used when no default user (default: 4000)`,`4000`).option(`--ui-internal-port <port>`,`DuckDB UI port (default: 4213)`,`4213`).action(async e=>{try{let i=n(),a=t();c(`Starting DuckPond MCP Server with ${e.transport} transport`),c(`Configuration:`,{memoryLimit:i.memoryLimit,threads:i.threads,maxActiveUsers:i.maxActiveUsers,strategy:i.strategy,dataDir:i.dataDir,tempDir:i.tempDir,cacheDir:i.cacheDir,cacheType:i.cacheType,hasR2:!!i.r2,hasS3:!!i.s3,defaultUser:a||`(not set)`}),i.r2?console.error(`☁️ Storage: Cloudflare R2`):i.s3?console.error(`☁️ Storage: AWS S3`):console.error(`💾 Storage: Local disk (${i.dataDir})`),a&&console.error(`👤 Default user: ${a}`);let o;if(process.env.DUCKPOND_OAUTH_ENABLED===`true`){let t=process.env.DUCKPOND_OAUTH_USERNAME,n=process.env.DUCKPOND_OAUTH_PASSWORD;(!t||!n)&&(console.error(`❌ OAuth enabled but DUCKPOND_OAUTH_USERNAME and DUCKPOND_OAUTH_PASSWORD are required`),process.exit(1)),o={enabled:!0,username:t,password:n,userId:process.env.DUCKPOND_OAUTH_USER_ID||t,email:process.env.DUCKPOND_OAUTH_EMAIL,issuer:process.env.DUCKPOND_OAUTH_ISSUER||`http://localhost:${parseInt(e.port)||3e3}`,resource:process.env.DUCKPOND_OAUTH_RESOURCE},console.error(`🔐 OAuth enabled with username/password authentication`),console.error(` Username: ${o.username}`),console.error(` User ID: ${o.userId}`),console.error(` ✓ Login form will be shown at authorization endpoint`)}let s;process.env.DUCKPOND_BASIC_AUTH_USERNAME&&process.env.DUCKPOND_BASIC_AUTH_PASSWORD&&(s={username:process.env.DUCKPOND_BASIC_AUTH_USERNAME,password:process.env.DUCKPOND_BASIC_AUTH_PASSWORD,userId:process.env.DUCKPOND_BASIC_AUTH_USER_ID,email:process.env.DUCKPOND_BASIC_AUTH_EMAIL},console.error(`🔐 Basic authentication enabled`),console.error(` Username: ${s.username}`),console.error(` User ID: ${s.userId||s.username}`));let l;process.env.DUCKPOND_BEARER_TOKEN&&(l={token:process.env.DUCKPOND_BEARER_TOKEN,userId:process.env.DUCKPOND_BEARER_TOKEN_USER_ID},console.error(`🔐 Bearer token authentication enabled`),l.userId&&console.error(` User ID: ${l.userId}`));let u=e.ui||process.env.DUCKPOND_UI_ENABLED===`true`,d=parseInt(e.uiPort)||4e3,f=parseInt(e.uiInternalPort)||4213;u&&e.transport===`stdio`&&(a?console.error(`🖥️ DuckDB UI will start at http://localhost:${f}`):(console.error(`🖥️ UI management server at http://localhost:${d}/ui`),console.error(` Visit /ui/:userId to start DuckDB UI for a user`))),e.transport===`stdio`||e.transport===`http`?await r({config:i,port:parseInt(e.port)||3e3,endpoint:`/mcp`,oauth:o,basicAuth:s,bearerToken:l,ui:u?{enabled:!0,port:d,internalPort:f,autoStartUser:a}:void 0},e.transport===`stdio`?`stdio`:`http`):(c(`Unknown transport: ${e.transport}`),process.exit(1))}catch(e){c(`Fatal error:`,e),console.error(`Fatal error:`,e),process.exit(1)}}),l.parse();export{};
3
3
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["#!/usr/bin/env node\n\n// Polyfill for Web Crypto API in Node.js environments\nimport { webcrypto } from \"crypto\"\n\nif (!globalThis.crypto) {\n globalThis.crypto = webcrypto as Crypto\n}\n\nimport { Command } from \"commander\"\nimport { createRequire } from \"module\"\n\nimport type { OAuthConfig } from \"./server\"\nimport { getDefaultUserId } from \"./tools\"\n\nconst require = createRequire(import.meta.url)\nconst packageJson = require(\"../package.json\") as { version: string }\nimport { startServer } from \"./server\"\nimport type { DuckPondServerConfig } from \"./server-core\"\nimport { startUIServer } from \"./ui-server\"\nimport { loggers } from \"./utils/logger\"\n\nconst log = loggers.main\n\n/**\n * Expand ~ to home directory in paths\n */\nfunction expandTilde(path: string): string {\n if (path.startsWith(\"~/\")) {\n const home = process.env.HOME || process.env.USERPROFILE || \".\"\n return `${home}${path.slice(1)}`\n }\n return path\n}\n\n/**\n * Get the default data directory for persistent storage\n */\nfunction getDefaultDataDir(): string {\n const home = process.env.HOME || process.env.USERPROFILE || \".\"\n return `${home}/.duckpond/data`\n}\n\n/**\n * Parse environment variables into DuckPond configuration\n */\nfunction getConfigFromEnv(): DuckPondServerConfig {\n // Default to local disk storage (expand ~ if present)\n const dataDir = expandTilde(process.env.DUCKPOND_DATA_DIR || getDefaultDataDir())\n\n const config: DuckPondServerConfig = {\n memoryLimit: process.env.DUCKPOND_MEMORY_LIMIT || \"4GB\",\n threads: parseInt(process.env.DUCKPOND_THREADS || \"4\"),\n maxActiveUsers: parseInt(process.env.DUCKPOND_MAX_ACTIVE_USERS || \"10\"),\n evictionTimeout: parseInt(process.env.DUCKPOND_EVICTION_TIMEOUT || \"300000\"),\n cacheType: (process.env.DUCKPOND_CACHE_TYPE as \"disk\" | \"memory\" | \"noop\") || \"disk\",\n strategy: (process.env.DUCKPOND_STRATEGY as \"parquet\" | \"duckdb\" | \"hybrid\") || \"duckdb\",\n tempDir: process.env.DUCKPOND_TEMP_DIR,\n cacheDir: process.env.DUCKPOND_CACHE_DIR || dataDir,\n dataDir,\n }\n\n // R2 configuration\n if (process.env.DUCKPOND_R2_ACCOUNT_ID) {\n config.r2 = {\n accountId: process.env.DUCKPOND_R2_ACCOUNT_ID,\n accessKeyId: process.env.DUCKPOND_R2_ACCESS_KEY_ID || \"\",\n secretAccessKey: process.env.DUCKPOND_R2_SECRET_ACCESS_KEY || \"\",\n bucket: process.env.DUCKPOND_R2_BUCKET || \"\",\n }\n }\n\n // S3 configuration\n if (process.env.DUCKPOND_S3_REGION) {\n config.s3 = {\n region: process.env.DUCKPOND_S3_REGION,\n accessKeyId: process.env.DUCKPOND_S3_ACCESS_KEY_ID || \"\",\n secretAccessKey: process.env.DUCKPOND_S3_SECRET_ACCESS_KEY || \"\",\n bucket: process.env.DUCKPOND_S3_BUCKET || \"\",\n }\n\n if (process.env.DUCKPOND_S3_ENDPOINT) {\n config.s3.endpoint = process.env.DUCKPOND_S3_ENDPOINT\n }\n }\n\n return config\n}\n\n/**\n * Main CLI program\n */\nconst program = new Command()\n\nprogram\n .name(\"duckpond-mcp-server\")\n .description(\"MCP server for multi-tenant DuckDB management with R2/S3 storage\")\n .version(packageJson.version)\n .option(\"-t, --transport <type>\", \"Transport mode: stdio or http\", \"stdio\")\n .option(\"-p, --port <port>\", \"HTTP port (when using http transport)\", \"3000\")\n .option(\"--ui\", \"Enable DuckDB UI (auto-starts for DUCKPOND_DEFAULT_USER)\")\n .option(\"--ui-port <port>\", \"UI management server port, only used when no default user (default: 4000)\", \"4000\")\n .option(\"--ui-internal-port <port>\", \"DuckDB UI port (default: 4213)\", \"4213\")\n .action(async (options) => {\n try {\n const config = getConfigFromEnv()\n\n const defaultUser = getDefaultUserId()\n log(`Starting DuckPond MCP Server with ${options.transport} transport`)\n log(\"Configuration:\", {\n memoryLimit: config.memoryLimit,\n threads: config.threads,\n maxActiveUsers: config.maxActiveUsers,\n strategy: config.strategy,\n dataDir: config.dataDir,\n tempDir: config.tempDir,\n cacheDir: config.cacheDir,\n cacheType: config.cacheType,\n hasR2: !!config.r2,\n hasS3: !!config.s3,\n defaultUser: defaultUser || \"(not set)\",\n })\n\n // Log storage mode\n if (config.r2) {\n console.error(\"☁️ Storage: Cloudflare R2\")\n } else if (config.s3) {\n console.error(\"☁️ Storage: AWS S3\")\n } else {\n console.error(`💾 Storage: Local disk (${config.dataDir})`)\n }\n\n if (defaultUser) {\n console.error(`👤 Default user: ${defaultUser}`)\n }\n\n // Load OAuth configuration from environment variables (for HTTP transport)\n let oauthConfig: OAuthConfig | undefined\n if (process.env.DUCKPOND_OAUTH_ENABLED === \"true\") {\n const username = process.env.DUCKPOND_OAUTH_USERNAME\n const password = process.env.DUCKPOND_OAUTH_PASSWORD\n\n if (!username || !password) {\n console.error(\"❌ OAuth enabled but DUCKPOND_OAUTH_USERNAME and DUCKPOND_OAUTH_PASSWORD are required\")\n process.exit(1)\n }\n\n oauthConfig = {\n enabled: true,\n username,\n password,\n userId: process.env.DUCKPOND_OAUTH_USER_ID || username,\n email: process.env.DUCKPOND_OAUTH_EMAIL,\n issuer: process.env.DUCKPOND_OAUTH_ISSUER || `http://localhost:${parseInt(options.port) || 3000}`,\n resource: process.env.DUCKPOND_OAUTH_RESOURCE,\n }\n\n console.error(\"🔐 OAuth enabled with username/password authentication\")\n console.error(` Username: ${oauthConfig.username}`)\n console.error(` User ID: ${oauthConfig.userId}`)\n console.error(\" ✓ Login form will be shown at authorization endpoint\")\n }\n\n // Load Basic Auth configuration from environment variables (for HTTP transport)\n let basicAuthConfig: { username: string; password: string; userId?: string; email?: string } | undefined\n if (process.env.DUCKPOND_BASIC_AUTH_USERNAME && process.env.DUCKPOND_BASIC_AUTH_PASSWORD) {\n basicAuthConfig = {\n username: process.env.DUCKPOND_BASIC_AUTH_USERNAME,\n password: process.env.DUCKPOND_BASIC_AUTH_PASSWORD,\n userId: process.env.DUCKPOND_BASIC_AUTH_USER_ID,\n email: process.env.DUCKPOND_BASIC_AUTH_EMAIL,\n }\n\n console.error(\"🔐 Basic authentication enabled\")\n console.error(` Username: ${basicAuthConfig.username}`)\n console.error(` User ID: ${basicAuthConfig.userId || basicAuthConfig.username}`)\n }\n\n // Load Bearer Token configuration from environment variables\n let bearerTokenConfig: { token: string; userId?: string } | undefined\n if (process.env.DUCKPOND_BEARER_TOKEN) {\n bearerTokenConfig = {\n token: process.env.DUCKPOND_BEARER_TOKEN,\n userId: process.env.DUCKPOND_BEARER_TOKEN_USER_ID,\n }\n\n console.error(\"🔐 Bearer token authentication enabled\")\n if (bearerTokenConfig.userId) {\n console.error(` User ID: ${bearerTokenConfig.userId}`)\n }\n }\n\n // Parse UI options\n const uiEnabled = options.ui || process.env.DUCKPOND_UI_ENABLED === \"true\"\n const uiPort = parseInt(options.uiPort) || 4000\n const uiInternalPort = parseInt(options.uiInternalPort) || 4213\n\n if (uiEnabled && options.transport === \"stdio\") {\n if (defaultUser) {\n // Will auto-start UI for default user - show where to access it\n console.error(`🖥️ DuckDB UI will start at http://localhost:${uiInternalPort}`)\n } else {\n // No default user - management server needed\n console.error(`🖥️ UI management server at http://localhost:${uiPort}/ui`)\n console.error(` Visit /ui/:userId to start DuckDB UI for a user`)\n }\n }\n\n // Start unified FastMCP server with appropriate transport\n if (options.transport === \"stdio\" || options.transport === \"http\") {\n await startServer(\n {\n config,\n port: parseInt(options.port) || 3000,\n endpoint: \"/mcp\",\n oauth: oauthConfig,\n basicAuth: basicAuthConfig,\n bearerToken: bearerTokenConfig,\n ui: uiEnabled\n ? {\n enabled: true,\n port: uiPort,\n internalPort: uiInternalPort,\n autoStartUser: defaultUser,\n }\n : undefined,\n },\n options.transport === \"stdio\" ? \"stdio\" : \"http\",\n )\n } else {\n log(`Unknown transport: ${options.transport}`)\n process.exit(1)\n }\n } catch (error) {\n log(\"Fatal error:\", error)\n console.error(\"Fatal error:\", error)\n process.exit(1)\n }\n })\n\nprogram.parse()\n"],"mappings":";wPAKK,WAAW,SACd,WAAW,OAAS,GAUtB,MAAM,EADU,EAAc,OAAO,KAAK,IAAI,CAClB,kBAAkB,CAMxC,EAAM,EAAQ,KAKpB,SAAS,EAAY,EAAsB,CAKzC,OAJI,EAAK,WAAW,KAAK,CAEhB,GADM,QAAQ,IAAI,MAAQ,QAAQ,IAAI,aAAe,MAC3C,EAAK,MAAM,EAAE,GAEzB,EAMT,SAAS,GAA4B,CAEnC,MAAO,GADM,QAAQ,IAAI,MAAQ,QAAQ,IAAI,aAAe,IAC7C,iBAMjB,SAAS,GAAyC,CAEhD,IAAM,EAAU,EAAY,QAAQ,IAAI,mBAAqB,GAAmB,CAAC,CAE3E,EAA+B,CACnC,YAAa,QAAQ,IAAI,uBAAyB,MAClD,QAAS,SAAS,QAAQ,IAAI,kBAAoB,IAAI,CACtD,eAAgB,SAAS,QAAQ,IAAI,2BAA6B,KAAK,CACvE,gBAAiB,SAAS,QAAQ,IAAI,2BAA6B,SAAS,CAC5E,UAAY,QAAQ,IAAI,qBAAsD,OAC9E,SAAW,QAAQ,IAAI,mBAAyD,SAChF,QAAS,QAAQ,IAAI,kBACrB,SAAU,QAAQ,IAAI,oBAAsB,EAC5C,UACD,CA0BD,OAvBI,QAAQ,IAAI,yBACd,EAAO,GAAK,CACV,UAAW,QAAQ,IAAI,uBACvB,YAAa,QAAQ,IAAI,2BAA6B,GACtD,gBAAiB,QAAQ,IAAI,+BAAiC,GAC9D,OAAQ,QAAQ,IAAI,oBAAsB,GAC3C,EAIC,QAAQ,IAAI,qBACd,EAAO,GAAK,CACV,OAAQ,QAAQ,IAAI,mBACpB,YAAa,QAAQ,IAAI,2BAA6B,GACtD,gBAAiB,QAAQ,IAAI,+BAAiC,GAC9D,OAAQ,QAAQ,IAAI,oBAAsB,GAC3C,CAEG,QAAQ,IAAI,uBACd,EAAO,GAAG,SAAW,QAAQ,IAAI,uBAI9B,EAMT,MAAM,EAAU,IAAI,EAEpB,EACG,KAAK,sBAAsB,CAC3B,YAAY,mEAAmE,CAC/E,QAAQ,EAAY,QAAQ,CAC5B,OAAO,yBAA0B,gCAAiC,QAAQ,CAC1E,OAAO,oBAAqB,wCAAyC,OAAO,CAC5E,OAAO,OAAQ,2DAA2D,CAC1E,OAAO,mBAAoB,4EAA6E,OAAO,CAC/G,OAAO,4BAA6B,iCAAkC,OAAO,CAC7E,OAAO,KAAO,IAAY,CACzB,GAAI,CACF,IAAM,EAAS,GAAkB,CAE3B,EAAc,GAAkB,CACtC,EAAI,qCAAqC,EAAQ,UAAU,YAAY,CACvE,EAAI,iBAAkB,CACpB,YAAa,EAAO,YACpB,QAAS,EAAO,QAChB,eAAgB,EAAO,eACvB,SAAU,EAAO,SACjB,QAAS,EAAO,QAChB,QAAS,EAAO,QAChB,SAAU,EAAO,SACjB,UAAW,EAAO,UAClB,MAAO,CAAC,CAAC,EAAO,GAChB,MAAO,CAAC,CAAC,EAAO,GAChB,YAAa,GAAe,YAC7B,CAAC,CAGE,EAAO,GACT,QAAQ,MAAM,6BAA6B,CAClC,EAAO,GAChB,QAAQ,MAAM,sBAAsB,CAEpC,QAAQ,MAAM,2BAA2B,EAAO,QAAQ,GAAG,CAGzD,GACF,QAAQ,MAAM,oBAAoB,IAAc,CAIlD,IAAI,EACJ,GAAI,QAAQ,IAAI,yBAA2B,OAAQ,CACjD,IAAM,EAAW,QAAQ,IAAI,wBACvB,EAAW,QAAQ,IAAI,yBAEzB,CAAC,GAAY,CAAC,KAChB,QAAQ,MAAM,uFAAuF,CACrG,QAAQ,KAAK,EAAE,EAGjB,EAAc,CACZ,QAAS,GACT,WACA,WACA,OAAQ,QAAQ,IAAI,wBAA0B,EAC9C,MAAO,QAAQ,IAAI,qBACnB,OAAQ,QAAQ,IAAI,uBAAyB,oBAAoB,SAAS,EAAQ,KAAK,EAAI,MAC3F,SAAU,QAAQ,IAAI,wBACvB,CAED,QAAQ,MAAM,yDAAyD,CACvE,QAAQ,MAAM,gBAAgB,EAAY,WAAW,CACrD,QAAQ,MAAM,eAAe,EAAY,SAAS,CAClD,QAAQ,MAAM,0DAA0D,CAI1E,IAAI,EACA,QAAQ,IAAI,8BAAgC,QAAQ,IAAI,+BAC1D,EAAkB,CAChB,SAAU,QAAQ,IAAI,6BACtB,SAAU,QAAQ,IAAI,6BACtB,OAAQ,QAAQ,IAAI,4BACpB,MAAO,QAAQ,IAAI,0BACpB,CAED,QAAQ,MAAM,kCAAkC,CAChD,QAAQ,MAAM,gBAAgB,EAAgB,WAAW,CACzD,QAAQ,MAAM,eAAe,EAAgB,QAAU,EAAgB,WAAW,EAIpF,IAAI,EACA,QAAQ,IAAI,wBACd,EAAoB,CAClB,MAAO,QAAQ,IAAI,sBACnB,OAAQ,QAAQ,IAAI,8BACrB,CAED,QAAQ,MAAM,yCAAyC,CACnD,EAAkB,QACpB,QAAQ,MAAM,eAAe,EAAkB,SAAS,EAK5D,IAAM,EAAY,EAAQ,IAAM,QAAQ,IAAI,sBAAwB,OAC9D,EAAS,SAAS,EAAQ,OAAO,EAAI,IACrC,EAAiB,SAAS,EAAQ,eAAe,EAAI,KAEvD,GAAa,EAAQ,YAAc,UACjC,EAEF,QAAQ,MAAM,iDAAiD,IAAiB,EAGhF,QAAQ,MAAM,iDAAiD,EAAO,KAAK,CAC3E,QAAQ,MAAM,qDAAqD,GAKnE,EAAQ,YAAc,SAAW,EAAQ,YAAc,OACzD,MAAM,EACJ,CACE,SACA,KAAM,SAAS,EAAQ,KAAK,EAAI,IAChC,SAAU,OACV,MAAO,EACP,UAAW,EACX,YAAa,EACb,GAAI,EACA,CACE,QAAS,GACT,KAAM,EACN,aAAc,EACd,cAAe,EAChB,CACD,IAAA,GACL,CACD,EAAQ,YAAc,QAAU,QAAU,OAC3C,EAED,EAAI,sBAAsB,EAAQ,YAAY,CAC9C,QAAQ,KAAK,EAAE,QAEV,EAAO,CACd,EAAI,eAAgB,EAAM,CAC1B,QAAQ,MAAM,eAAgB,EAAM,CACpC,QAAQ,KAAK,EAAE,GAEjB,CAEJ,EAAQ,OAAO"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["#!/usr/bin/env node\n\n// Polyfill for Web Crypto API in Node.js environments\nimport { webcrypto } from \"crypto\"\n\nif (!globalThis.crypto) {\n globalThis.crypto = webcrypto as Crypto\n}\n\nimport { Command } from \"commander\"\nimport { createRequire } from \"module\"\n\nimport type { OAuthConfig } from \"./server\"\nimport { getDefaultUserId } from \"./tools\"\n\nconst require = createRequire(import.meta.url)\nconst packageJson = require(\"../package.json\") as { version: string }\nimport { getConfigFromEnv } from \"./config/path-resolver\"\nimport { startServer } from \"./server\"\nimport { startUIServer } from \"./ui-server\"\nimport { loggers } from \"./utils/logger\"\n\nconst log = loggers.main\n\n/**\n * Main CLI program\n */\nconst program = new Command()\n\nprogram\n .name(\"duckpond-mcp-server\")\n .description(\"MCP server for multi-tenant DuckDB management with R2/S3 storage\")\n .version(packageJson.version)\n .option(\"-t, --transport <type>\", \"Transport mode: stdio or http\", \"stdio\")\n .option(\"-p, --port <port>\", \"HTTP port (when using http transport)\", \"3000\")\n .option(\"--ui\", \"Enable DuckDB UI (auto-starts for DUCKPOND_DEFAULT_USER)\")\n .option(\"--ui-port <port>\", \"UI management server port, only used when no default user (default: 4000)\", \"4000\")\n .option(\"--ui-internal-port <port>\", \"DuckDB UI port (default: 4213)\", \"4213\")\n .action(async (options) => {\n try {\n const config = getConfigFromEnv()\n\n const defaultUser = getDefaultUserId()\n log(`Starting DuckPond MCP Server with ${options.transport} transport`)\n log(\"Configuration:\", {\n memoryLimit: config.memoryLimit,\n threads: config.threads,\n maxActiveUsers: config.maxActiveUsers,\n strategy: config.strategy,\n dataDir: config.dataDir,\n tempDir: config.tempDir,\n cacheDir: config.cacheDir,\n cacheType: config.cacheType,\n hasR2: !!config.r2,\n hasS3: !!config.s3,\n defaultUser: defaultUser || \"(not set)\",\n })\n\n // Log storage mode\n if (config.r2) {\n console.error(\"☁️ Storage: Cloudflare R2\")\n } else if (config.s3) {\n console.error(\"☁️ Storage: AWS S3\")\n } else {\n console.error(`💾 Storage: Local disk (${config.dataDir})`)\n }\n\n if (defaultUser) {\n console.error(`👤 Default user: ${defaultUser}`)\n }\n\n // Load OAuth configuration from environment variables (for HTTP transport)\n let oauthConfig: OAuthConfig | undefined\n if (process.env.DUCKPOND_OAUTH_ENABLED === \"true\") {\n const username = process.env.DUCKPOND_OAUTH_USERNAME\n const password = process.env.DUCKPOND_OAUTH_PASSWORD\n\n if (!username || !password) {\n console.error(\"❌ OAuth enabled but DUCKPOND_OAUTH_USERNAME and DUCKPOND_OAUTH_PASSWORD are required\")\n process.exit(1)\n }\n\n oauthConfig = {\n enabled: true,\n username,\n password,\n userId: process.env.DUCKPOND_OAUTH_USER_ID || username,\n email: process.env.DUCKPOND_OAUTH_EMAIL,\n issuer: process.env.DUCKPOND_OAUTH_ISSUER || `http://localhost:${parseInt(options.port) || 3000}`,\n resource: process.env.DUCKPOND_OAUTH_RESOURCE,\n }\n\n console.error(\"🔐 OAuth enabled with username/password authentication\")\n console.error(` Username: ${oauthConfig.username}`)\n console.error(` User ID: ${oauthConfig.userId}`)\n console.error(\" ✓ Login form will be shown at authorization endpoint\")\n }\n\n // Load Basic Auth configuration from environment variables (for HTTP transport)\n let basicAuthConfig: { username: string; password: string; userId?: string; email?: string } | undefined\n if (process.env.DUCKPOND_BASIC_AUTH_USERNAME && process.env.DUCKPOND_BASIC_AUTH_PASSWORD) {\n basicAuthConfig = {\n username: process.env.DUCKPOND_BASIC_AUTH_USERNAME,\n password: process.env.DUCKPOND_BASIC_AUTH_PASSWORD,\n userId: process.env.DUCKPOND_BASIC_AUTH_USER_ID,\n email: process.env.DUCKPOND_BASIC_AUTH_EMAIL,\n }\n\n console.error(\"🔐 Basic authentication enabled\")\n console.error(` Username: ${basicAuthConfig.username}`)\n console.error(` User ID: ${basicAuthConfig.userId || basicAuthConfig.username}`)\n }\n\n // Load Bearer Token configuration from environment variables\n let bearerTokenConfig: { token: string; userId?: string } | undefined\n if (process.env.DUCKPOND_BEARER_TOKEN) {\n bearerTokenConfig = {\n token: process.env.DUCKPOND_BEARER_TOKEN,\n userId: process.env.DUCKPOND_BEARER_TOKEN_USER_ID,\n }\n\n console.error(\"🔐 Bearer token authentication enabled\")\n if (bearerTokenConfig.userId) {\n console.error(` User ID: ${bearerTokenConfig.userId}`)\n }\n }\n\n // Parse UI options\n const uiEnabled = options.ui || process.env.DUCKPOND_UI_ENABLED === \"true\"\n const uiPort = parseInt(options.uiPort) || 4000\n const uiInternalPort = parseInt(options.uiInternalPort) || 4213\n\n if (uiEnabled && options.transport === \"stdio\") {\n if (defaultUser) {\n // Will auto-start UI for default user - show where to access it\n console.error(`🖥️ DuckDB UI will start at http://localhost:${uiInternalPort}`)\n } else {\n // No default user - management server needed\n console.error(`🖥️ UI management server at http://localhost:${uiPort}/ui`)\n console.error(` Visit /ui/:userId to start DuckDB UI for a user`)\n }\n }\n\n // Start unified FastMCP server with appropriate transport\n if (options.transport === \"stdio\" || options.transport === \"http\") {\n await startServer(\n {\n config,\n port: parseInt(options.port) || 3000,\n endpoint: \"/mcp\",\n oauth: oauthConfig,\n basicAuth: basicAuthConfig,\n bearerToken: bearerTokenConfig,\n ui: uiEnabled\n ? {\n enabled: true,\n port: uiPort,\n internalPort: uiInternalPort,\n autoStartUser: defaultUser,\n }\n : undefined,\n },\n options.transport === \"stdio\" ? \"stdio\" : \"http\",\n )\n } else {\n log(`Unknown transport: ${options.transport}`)\n process.exit(1)\n }\n } catch (error) {\n log(\"Fatal error:\", error)\n console.error(\"Fatal error:\", error)\n process.exit(1)\n }\n })\n\nprogram.parse()\n"],"mappings":"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"}
package/dist/lib.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { DuckPondServer, DuckPondServerConfig } from "./server-core.js";
2
+ import { getConfigFromEnv, getDefaultDataDir, resolveDataDir } from "./config/path-resolver.js";
2
3
  import { BearerTokenConfig, FastMCPServerOptions, StartServerOptions, createFastMCPServer, startServer } from "./server.js";
3
4
  import { getDefaultUserId } from "./tools/index.js";
4
- export { type BearerTokenConfig, DuckPondServer, type DuckPondServerConfig, type FastMCPServerOptions, type StartServerOptions, createFastMCPServer, getDefaultUserId, startServer };
5
+ export { type BearerTokenConfig, DuckPondServer, type DuckPondServerConfig, type FastMCPServerOptions, type StartServerOptions, createFastMCPServer, getConfigFromEnv, getDefaultDataDir, getDefaultUserId, resolveDataDir, startServer };
package/dist/lib.js CHANGED
@@ -1 +1 @@
1
- import{getDefaultUserId as e}from"./tools/index.js";import{DuckPondServer as t}from"./server-core.js";import{createFastMCPServer as n,startServer as r}from"./server.js";export{t as DuckPondServer,n as createFastMCPServer,e as getDefaultUserId,r as startServer};
1
+ import{getDefaultUserId as e}from"./tools/index.js";import{getConfigFromEnv as t,getDefaultDataDir as n,resolveDataDir as r}from"./config/path-resolver.js";import{t as i}from"./server-core-Cj3cIk43.js";import{createFastMCPServer as a,startServer as o}from"./server.js";export{i as DuckPondServer,a as createFastMCPServer,t as getConfigFromEnv,n as getDefaultDataDir,e as getDefaultUserId,r as resolveDataDir,o as startServer};
@@ -0,0 +1,2 @@
1
+ import{loggers as e}from"./utils/logger.js";import{DuckPond as t}from"duckpond";function n(e){"@babel/helpers - typeof";return n=typeof Symbol==`function`&&typeof Symbol.iterator==`symbol`?function(e){return typeof e}:function(e){return e&&typeof Symbol==`function`&&e.constructor===Symbol&&e!==Symbol.prototype?`symbol`:typeof e},n(e)}function r(e,t){if(n(e)!=`object`||!e)return e;var r=e[Symbol.toPrimitive];if(r!==void 0){var i=r.call(e,t||`default`);if(n(i)!=`object`)return i;throw TypeError(`@@toPrimitive must return a primitive value.`)}return(t===`string`?String:Number)(e)}function i(e){var t=r(e,`string`);return n(t)==`symbol`?t:t+``}function a(e,t,n){return(t=i(t))in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}const o=e.core;var s=class{constructor(e){this.config=e,a(this,`pond`,null),a(this,`initialized`,!1),a(this,`currentUIUserId`,null),a(this,`uiInternalPort`,4213),a(this,`uiKeepaliveTimer`,null),a(this,`UI_KEEPALIVE_INTERVAL`,3e4),o(`DuckPondServer created`)}async init(){if(this.initialized)return o(`Already initialized`),{success:!0,data:void 0};o(`Initializing DuckPond...`);let e=Date.now();this.pond=new t(this.config);let n=await this.pond.init();return this.handleEither(n,Date.now()-e)}async query(e,t){if(!this.pond)return this.notInitializedError();o(`Query for user ${e}: ${t.substring(0,100)}...`);let n=Date.now(),r=await this.pond.query(e,t);return this.handleEither(r,Date.now()-n)}async execute(e,t){if(!this.pond)return this.notInitializedError();o(`Execute for user ${e}: ${t.substring(0,100)}...`);let n=Date.now(),r=await this.pond.execute(e,t);return this.handleEither(r,Date.now()-n)}async getUserStats(e){if(!this.pond)return this.notInitializedError();o(`Getting stats for user ${e}`);let t=Date.now(),n=await this.pond.getUserStats(e);return this.handleEither(n,Date.now()-t)}isAttached(e){if(!this.pond)return this.notInitializedError();let t=this.pond.isAttached(e);return o(`User ${e} attached: ${t}`),{success:!0,data:t}}async detachUser(e){if(!this.pond)return this.notInitializedError();o(`Detaching user ${e}`);let t=Date.now(),n=await this.pond.detachUser(e);return this.handleEither(n,Date.now()-t)}listUsers(){if(!this.pond)return this.notInitializedError();o(`Listing cached users`);let e=this.pond.listUsers();return{success:!0,data:{users:e.users.toArray(),count:e.count,maxActiveUsers:e.maxActiveUsers,utilizationPercent:e.utilizationPercent}}}async close(){if(!this.pond)return{success:!0,data:void 0};o(`Closing DuckPond...`);let e=Date.now();this.currentUIUserId&&await this.stopUI();let t=await this.pond.close();return this.initialized=!1,this.pond=null,this.handleEither(t,Date.now()-e)}async startUI(e){if(!this.pond)return this.notInitializedError();o(`Starting UI for user ${e}`);let t=Date.now();if(this.currentUIUserId&&this.currentUIUserId!==e&&(o(`Stopping UI for previous user ${this.currentUIUserId}`),await this.stopUI()),this.currentUIUserId===e)return o(`UI already running for user ${e}`),{success:!0,data:{port:this.uiInternalPort},executionTime:Date.now()-t};let n=await this.execute(e,`INSTALL ui; LOAD ui;`);if(!n.success)return o(`Failed to install UI extension: ${n.error.message}`),n;let r=await this.execute(e,`SET ui_local_port = ${this.uiInternalPort}`);if(!r.success)return o(`Failed to set UI port: ${r.error.message}`),r;let i=await this.execute(e,`CALL start_ui_server()`);return i.success?(this.currentUIUserId=e,this.startUIKeepalive(e),o(`UI started for user ${e} on port ${this.uiInternalPort}`),{success:!0,data:{port:this.uiInternalPort},executionTime:Date.now()-t}):(o(`Failed to start UI server: ${i.error.message}`),i)}startUIKeepalive(e){this.stopUIKeepalive(),o(`Starting UI keepalive for user ${e}`),this.uiKeepaliveTimer=setInterval(async()=>{this.pond&&this.currentUIUserId===e&&(o(`UI keepalive ping for user ${e}`),await this.pond.query(e,`SELECT 1`))},this.UI_KEEPALIVE_INTERVAL)}stopUIKeepalive(){this.uiKeepaliveTimer&&(o(`Stopping UI keepalive`),clearInterval(this.uiKeepaliveTimer),this.uiKeepaliveTimer=null)}async stopUI(){if(this.stopUIKeepalive(),!this.currentUIUserId)return o(`No UI running to stop`),{success:!0,data:void 0};if(!this.pond)return this.currentUIUserId=null,{success:!0,data:void 0};o(`Stopping UI for user ${this.currentUIUserId}`);let e=Date.now(),t=await this.execute(this.currentUIUserId,`CALL stop_ui_server()`),n=this.currentUIUserId;return this.currentUIUserId=null,t.success?(o(`UI stopped for user ${n}`),{success:!0,data:void 0,executionTime:Date.now()-e}):(o(`Failed to stop UI: ${t.error.message}`),t)}getCurrentUIUser(){return this.currentUIUserId}setUIPort(e){this.uiInternalPort=e}getUIPort(){return this.uiInternalPort}handleEither(e,t){return e.fold(e=>({success:!1,error:{code:this.mapErrorCode(e.code),message:e.message,details:{originalCode:e.code,context:e.context,cause:e.cause?.message}}}),e=>({success:!0,data:e,executionTime:t}))}mapErrorCode(e){return{CONNECTION_FAILED:`SERVICE_UNAVAILABLE`,CONNECTION_TIMEOUT:`TIMEOUT`,R2_CONNECTION_ERROR:`SERVICE_UNAVAILABLE`,S3_CONNECTION_ERROR:`SERVICE_UNAVAILABLE`,USER_NOT_FOUND:`NOT_FOUND`,USER_ALREADY_EXISTS:`ALREADY_EXISTS`,USER_NOT_ATTACHED:`NOT_FOUND`,QUERY_EXECUTION_ERROR:`INVALID_REQUEST`,QUERY_TIMEOUT:`TIMEOUT`,INVALID_SQL:`INVALID_REQUEST`,MEMORY_LIMIT_EXCEEDED:`RESOURCE_EXHAUSTED`,STORAGE_ERROR:`SERVICE_UNAVAILABLE`,STORAGE_QUOTA_EXCEEDED:`RESOURCE_EXHAUSTED`,INVALID_CONFIG:`INVALID_ARGUMENT`,NOT_INITIALIZED:`FAILED_PRECONDITION`,UNKNOWN_ERROR:`INTERNAL_ERROR`}[e]||`INTERNAL_ERROR`}notInitializedError(){return{success:!1,error:{code:`FAILED_PRECONDITION`,message:`DuckPond not initialized. Call init() first.`}}}};export{s as t};
2
+ //# sourceMappingURL=server-core-Cj3cIk43.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server-core-Cj3cIk43.js","names":[],"sources":["../src/server-core.ts"],"sourcesContent":["import type { Either } from \"duckpond\"\nimport { DuckPond, type DuckPondConfig, type ErrorCode, type ListUsersResult, type UserStats } from \"duckpond\"\n\nimport { loggers } from \"./utils/logger\"\n\nconst log = loggers.core\n\n/**\n * Result type for MCP tool responses\n */\nexport type MCPResult<T> =\n | {\n success: true\n data: T\n executionTime?: number\n }\n | {\n success: false\n error: {\n code: string\n message: string\n details?: {\n originalCode?: ErrorCode\n context?: Record<string, unknown>\n cause?: string\n }\n }\n }\n\n/**\n * Configuration for DuckPond MCP Server\n * Extends DuckPondConfig with server-specific options\n */\nexport type DuckPondServerConfig = DuckPondConfig & {\n /** Directory for persistent database storage (default: ~/.duckpond/data) */\n dataDir?: string\n}\n\n/**\n * Core DuckPond MCP Server\n *\n * Wraps DuckPond library with MCP-compatible result types\n */\nexport class DuckPondServer {\n private pond: DuckPond | null = null\n private initialized = false\n private currentUIUserId: string | null = null\n private uiInternalPort: number = 4213\n private uiKeepaliveTimer: ReturnType<typeof setInterval> | null = null\n private readonly UI_KEEPALIVE_INTERVAL = 30000 // 30 seconds\n\n constructor(private config: DuckPondServerConfig) {\n log(\"DuckPondServer created\")\n }\n\n /**\n * Initialize the DuckPond instance\n */\n async init(): Promise<MCPResult<void>> {\n if (this.initialized) {\n log(\"Already initialized\")\n return { success: true, data: undefined }\n }\n\n log(\"Initializing DuckPond...\")\n const startTime = Date.now()\n\n this.pond = new DuckPond(this.config)\n const result = await this.pond.init()\n\n return this.handleEither(result, Date.now() - startTime)\n }\n\n /**\n * Execute a SQL query for a user\n */\n async query<T = unknown>(userId: string, sql: string): Promise<MCPResult<T[]>> {\n if (!this.pond) {\n return this.notInitializedError()\n }\n\n log(`Query for user ${userId}: ${sql.substring(0, 100)}...`)\n const startTime = Date.now()\n\n const result = await this.pond.query<T>(userId, sql)\n\n return this.handleEither(result, Date.now() - startTime)\n }\n\n /**\n * Execute DDL/DML without returning results\n */\n async execute(userId: string, sql: string): Promise<MCPResult<void>> {\n if (!this.pond) {\n return this.notInitializedError()\n }\n\n log(`Execute for user ${userId}: ${sql.substring(0, 100)}...`)\n const startTime = Date.now()\n\n const result = await this.pond.execute(userId, sql)\n\n return this.handleEither(result, Date.now() - startTime)\n }\n\n /**\n * Get statistics about a user's database\n */\n async getUserStats(userId: string): Promise<MCPResult<UserStats>> {\n if (!this.pond) {\n return this.notInitializedError()\n }\n\n log(`Getting stats for user ${userId}`)\n const startTime = Date.now()\n\n const result = await this.pond.getUserStats(userId)\n\n return this.handleEither(result, Date.now() - startTime)\n }\n\n /**\n * Check if a user is currently cached\n */\n isAttached(userId: string): MCPResult<boolean> {\n if (!this.pond) {\n return this.notInitializedError()\n }\n\n const attached = this.pond.isAttached(userId)\n log(`User ${userId} attached: ${attached}`)\n\n return {\n success: true,\n data: attached,\n }\n }\n\n /**\n * Manually detach a user from the cache\n */\n async detachUser(userId: string): Promise<MCPResult<void>> {\n if (!this.pond) {\n return this.notInitializedError()\n }\n\n log(`Detaching user ${userId}`)\n const startTime = Date.now()\n\n const result = await this.pond.detachUser(userId)\n\n return this.handleEither(result, Date.now() - startTime)\n }\n\n /**\n * List all currently cached users\n */\n listUsers(): MCPResult<{ users: string[]; count: number; maxActiveUsers: number; utilizationPercent: number }> {\n if (!this.pond) {\n return this.notInitializedError()\n }\n\n log(\"Listing cached users\")\n const result = this.pond.listUsers()\n\n return {\n success: true,\n data: {\n users: result.users.toArray(), // Convert List<string> to string[]\n count: result.count,\n maxActiveUsers: result.maxActiveUsers,\n utilizationPercent: result.utilizationPercent,\n },\n }\n }\n\n /**\n * Close the DuckPond instance\n */\n async close(): Promise<MCPResult<void>> {\n if (!this.pond) {\n return { success: true, data: undefined }\n }\n\n log(\"Closing DuckPond...\")\n const startTime = Date.now()\n\n // Stop UI server if running\n if (this.currentUIUserId) {\n await this.stopUI()\n }\n\n const result = await this.pond.close()\n this.initialized = false\n this.pond = null\n\n return this.handleEither(result, Date.now() - startTime)\n }\n\n /**\n * Start DuckDB UI for a specific user\n * Only one user's UI can be active at a time\n */\n async startUI(userId: string): Promise<MCPResult<{ port: number }>> {\n if (!this.pond) {\n return this.notInitializedError()\n }\n\n log(`Starting UI for user ${userId}`)\n const startTime = Date.now()\n\n // Stop existing UI if different user\n if (this.currentUIUserId && this.currentUIUserId !== userId) {\n log(`Stopping UI for previous user ${this.currentUIUserId}`)\n await this.stopUI()\n }\n\n // If already running for this user, just return success\n if (this.currentUIUserId === userId) {\n log(`UI already running for user ${userId}`)\n return {\n success: true,\n data: { port: this.uiInternalPort },\n executionTime: Date.now() - startTime,\n }\n }\n\n // Install and load UI extension\n const installResult = await this.execute(userId, \"INSTALL ui; LOAD ui;\")\n if (!installResult.success) {\n log(`Failed to install UI extension: ${installResult.error.message}`)\n return installResult as MCPResult<{ port: number }>\n }\n\n // Set the UI port\n const portResult = await this.execute(userId, `SET ui_local_port = ${this.uiInternalPort}`)\n if (!portResult.success) {\n log(`Failed to set UI port: ${portResult.error.message}`)\n return portResult as MCPResult<{ port: number }>\n }\n\n // Start the UI server (without opening browser)\n const startResult = await this.execute(userId, \"CALL start_ui_server()\")\n if (!startResult.success) {\n log(`Failed to start UI server: ${startResult.error.message}`)\n return startResult as MCPResult<{ port: number }>\n }\n\n this.currentUIUserId = userId\n this.startUIKeepalive(userId)\n log(`UI started for user ${userId} on port ${this.uiInternalPort}`)\n\n return {\n success: true,\n data: { port: this.uiInternalPort },\n executionTime: Date.now() - startTime,\n }\n }\n\n /**\n * Start keepalive timer to prevent UI user from being evicted\n */\n private startUIKeepalive(userId: string): void {\n this.stopUIKeepalive()\n log(`Starting UI keepalive for user ${userId}`)\n this.uiKeepaliveTimer = setInterval(async () => {\n if (this.pond && this.currentUIUserId === userId) {\n log(`UI keepalive ping for user ${userId}`)\n await this.pond.query(userId, \"SELECT 1\")\n }\n }, this.UI_KEEPALIVE_INTERVAL)\n }\n\n /**\n * Stop the keepalive timer\n */\n private stopUIKeepalive(): void {\n if (this.uiKeepaliveTimer) {\n log(\"Stopping UI keepalive\")\n clearInterval(this.uiKeepaliveTimer)\n this.uiKeepaliveTimer = null\n }\n }\n\n /**\n * Stop the currently running DuckDB UI\n */\n async stopUI(): Promise<MCPResult<void>> {\n this.stopUIKeepalive()\n\n if (!this.currentUIUserId) {\n log(\"No UI running to stop\")\n return { success: true, data: undefined }\n }\n\n if (!this.pond) {\n this.currentUIUserId = null\n return { success: true, data: undefined }\n }\n\n log(`Stopping UI for user ${this.currentUIUserId}`)\n const startTime = Date.now()\n\n const result = await this.execute(this.currentUIUserId, \"CALL stop_ui_server()\")\n const previousUser = this.currentUIUserId\n this.currentUIUserId = null\n\n if (result.success) {\n log(`UI stopped for user ${previousUser}`)\n return {\n success: true,\n data: undefined,\n executionTime: Date.now() - startTime,\n }\n }\n\n log(`Failed to stop UI: ${result.error.message}`)\n return result\n }\n\n /**\n * Get the user ID for the currently running UI\n */\n getCurrentUIUser(): string | null {\n return this.currentUIUserId\n }\n\n /**\n * Set the internal port for DuckDB UI\n */\n setUIPort(port: number): void {\n this.uiInternalPort = port\n }\n\n /**\n * Get the internal port for DuckDB UI\n */\n getUIPort(): number {\n return this.uiInternalPort\n }\n\n /**\n * Convert Either<Error, T> to MCPResult<T>\n */\n private handleEither<T>(\n result: Either<{ code: ErrorCode; message: string; cause?: Error; context?: Record<string, unknown> }, T>,\n executionTime: number,\n ): MCPResult<T> {\n return result.fold<MCPResult<T>>(\n (error) => ({\n success: false as const,\n error: {\n code: this.mapErrorCode(error.code),\n message: error.message,\n details: {\n originalCode: error.code,\n context: error.context,\n cause: error.cause?.message,\n },\n },\n }),\n (data) => ({\n success: true as const,\n data,\n executionTime,\n }),\n )\n }\n\n /**\n * Map DuckPond ErrorCode to MCP error code\n */\n private mapErrorCode(code: ErrorCode): string {\n const mapping: Record<ErrorCode, string> = {\n CONNECTION_FAILED: \"SERVICE_UNAVAILABLE\",\n CONNECTION_TIMEOUT: \"TIMEOUT\",\n R2_CONNECTION_ERROR: \"SERVICE_UNAVAILABLE\",\n S3_CONNECTION_ERROR: \"SERVICE_UNAVAILABLE\",\n USER_NOT_FOUND: \"NOT_FOUND\",\n USER_ALREADY_EXISTS: \"ALREADY_EXISTS\",\n USER_NOT_ATTACHED: \"NOT_FOUND\",\n QUERY_EXECUTION_ERROR: \"INVALID_REQUEST\",\n QUERY_TIMEOUT: \"TIMEOUT\",\n INVALID_SQL: \"INVALID_REQUEST\",\n MEMORY_LIMIT_EXCEEDED: \"RESOURCE_EXHAUSTED\",\n STORAGE_ERROR: \"SERVICE_UNAVAILABLE\",\n STORAGE_QUOTA_EXCEEDED: \"RESOURCE_EXHAUSTED\",\n INVALID_CONFIG: \"INVALID_ARGUMENT\",\n NOT_INITIALIZED: \"FAILED_PRECONDITION\",\n UNKNOWN_ERROR: \"INTERNAL_ERROR\",\n }\n\n return mapping[code] || \"INTERNAL_ERROR\"\n }\n\n /**\n * Helper for not initialized error\n */\n private notInitializedError<T>(): MCPResult<T> {\n return {\n success: false,\n error: {\n code: \"FAILED_PRECONDITION\",\n message: \"DuckPond not initialized. Call init() first.\",\n },\n }\n }\n}\n"],"mappings":"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"}
@@ -1,2 +1 @@
1
- import{loggers as e}from"./utils/logger.js";import{t}from"./defineProperty-BEGP2H1W.js";import{DuckPond as n}from"duckpond";const r=e.core;var i=class{constructor(e){this.config=e,t(this,`pond`,null),t(this,`initialized`,!1),t(this,`currentUIUserId`,null),t(this,`uiInternalPort`,4213),t(this,`uiKeepaliveTimer`,null),t(this,`UI_KEEPALIVE_INTERVAL`,3e4),r(`DuckPondServer created`)}async init(){if(this.initialized)return r(`Already initialized`),{success:!0,data:void 0};r(`Initializing DuckPond...`);let e=Date.now();this.pond=new n(this.config);let t=await this.pond.init();return this.handleEither(t,Date.now()-e)}async query(e,t){if(!this.pond)return this.notInitializedError();r(`Query for user ${e}: ${t.substring(0,100)}...`);let n=Date.now(),i=await this.pond.query(e,t);return this.handleEither(i,Date.now()-n)}async execute(e,t){if(!this.pond)return this.notInitializedError();r(`Execute for user ${e}: ${t.substring(0,100)}...`);let n=Date.now(),i=await this.pond.execute(e,t);return this.handleEither(i,Date.now()-n)}async getUserStats(e){if(!this.pond)return this.notInitializedError();r(`Getting stats for user ${e}`);let t=Date.now(),n=await this.pond.getUserStats(e);return this.handleEither(n,Date.now()-t)}isAttached(e){if(!this.pond)return this.notInitializedError();let t=this.pond.isAttached(e);return r(`User ${e} attached: ${t}`),{success:!0,data:t}}async detachUser(e){if(!this.pond)return this.notInitializedError();r(`Detaching user ${e}`);let t=Date.now(),n=await this.pond.detachUser(e);return this.handleEither(n,Date.now()-t)}listUsers(){if(!this.pond)return this.notInitializedError();r(`Listing cached users`);let e=this.pond.listUsers();return{success:!0,data:{users:e.users.toArray(),count:e.count,maxActiveUsers:e.maxActiveUsers,utilizationPercent:e.utilizationPercent}}}async close(){if(!this.pond)return{success:!0,data:void 0};r(`Closing DuckPond...`);let e=Date.now();this.currentUIUserId&&await this.stopUI();let t=await this.pond.close();return this.initialized=!1,this.pond=null,this.handleEither(t,Date.now()-e)}async startUI(e){if(!this.pond)return this.notInitializedError();r(`Starting UI for user ${e}`);let t=Date.now();if(this.currentUIUserId&&this.currentUIUserId!==e&&(r(`Stopping UI for previous user ${this.currentUIUserId}`),await this.stopUI()),this.currentUIUserId===e)return r(`UI already running for user ${e}`),{success:!0,data:{port:this.uiInternalPort},executionTime:Date.now()-t};let n=await this.execute(e,`INSTALL ui; LOAD ui;`);if(!n.success)return r(`Failed to install UI extension: ${n.error.message}`),n;let i=await this.execute(e,`SET ui_local_port = ${this.uiInternalPort}`);if(!i.success)return r(`Failed to set UI port: ${i.error.message}`),i;let a=await this.execute(e,`CALL start_ui_server()`);return a.success?(this.currentUIUserId=e,this.startUIKeepalive(e),r(`UI started for user ${e} on port ${this.uiInternalPort}`),{success:!0,data:{port:this.uiInternalPort},executionTime:Date.now()-t}):(r(`Failed to start UI server: ${a.error.message}`),a)}startUIKeepalive(e){this.stopUIKeepalive(),r(`Starting UI keepalive for user ${e}`),this.uiKeepaliveTimer=setInterval(async()=>{this.pond&&this.currentUIUserId===e&&(r(`UI keepalive ping for user ${e}`),await this.pond.query(e,`SELECT 1`))},this.UI_KEEPALIVE_INTERVAL)}stopUIKeepalive(){this.uiKeepaliveTimer&&(r(`Stopping UI keepalive`),clearInterval(this.uiKeepaliveTimer),this.uiKeepaliveTimer=null)}async stopUI(){if(this.stopUIKeepalive(),!this.currentUIUserId)return r(`No UI running to stop`),{success:!0,data:void 0};if(!this.pond)return this.currentUIUserId=null,{success:!0,data:void 0};r(`Stopping UI for user ${this.currentUIUserId}`);let e=Date.now(),t=await this.execute(this.currentUIUserId,`CALL stop_ui_server()`),n=this.currentUIUserId;return this.currentUIUserId=null,t.success?(r(`UI stopped for user ${n}`),{success:!0,data:void 0,executionTime:Date.now()-e}):(r(`Failed to stop UI: ${t.error.message}`),t)}getCurrentUIUser(){return this.currentUIUserId}setUIPort(e){this.uiInternalPort=e}getUIPort(){return this.uiInternalPort}handleEither(e,t){return e.fold(e=>({success:!1,error:{code:this.mapErrorCode(e.code),message:e.message,details:{originalCode:e.code,context:e.context,cause:e.cause?.message}}}),e=>({success:!0,data:e,executionTime:t}))}mapErrorCode(e){return{CONNECTION_FAILED:`SERVICE_UNAVAILABLE`,CONNECTION_TIMEOUT:`TIMEOUT`,R2_CONNECTION_ERROR:`SERVICE_UNAVAILABLE`,S3_CONNECTION_ERROR:`SERVICE_UNAVAILABLE`,USER_NOT_FOUND:`NOT_FOUND`,USER_ALREADY_EXISTS:`ALREADY_EXISTS`,USER_NOT_ATTACHED:`NOT_FOUND`,QUERY_EXECUTION_ERROR:`INVALID_REQUEST`,QUERY_TIMEOUT:`TIMEOUT`,INVALID_SQL:`INVALID_REQUEST`,MEMORY_LIMIT_EXCEEDED:`RESOURCE_EXHAUSTED`,STORAGE_ERROR:`SERVICE_UNAVAILABLE`,STORAGE_QUOTA_EXCEEDED:`RESOURCE_EXHAUSTED`,INVALID_CONFIG:`INVALID_ARGUMENT`,NOT_INITIALIZED:`FAILED_PRECONDITION`,UNKNOWN_ERROR:`INTERNAL_ERROR`}[e]||`INTERNAL_ERROR`}notInitializedError(){return{success:!1,error:{code:`FAILED_PRECONDITION`,message:`DuckPond not initialized. Call init() first.`}}}};export{i as DuckPondServer};
2
- //# sourceMappingURL=server-core.js.map
1
+ import{t as e}from"./server-core-Cj3cIk43.js";export{e as DuckPondServer};
package/dist/server.js CHANGED
@@ -1,4 +1,4 @@
1
- import{loggers as e}from"./utils/logger.js";import{detachUserSchema as t,executeSchema as n,getUserStatsSchema as r,isAttachedSchema as i,listUsersSchema as a,querySchema as o,resolveUserId as s}from"./tools/index.js";import{DuckPondServer as c}from"./server-core.js";import{startUIServer as l}from"./ui-server.js";import{createHash as u,randomBytes as d,webcrypto as f}from"crypto";import{createRequire as p}from"module";import{FastMCP as m}from"fastmcp";import*as h from"jsonwebtoken";import{URL as g}from"url";globalThis.crypto||(globalThis.crypto=f);const _=p(import.meta.url)(`../package.json`),v=e.fastmcp,y=process.env.DUCKPOND_JWT_SECRET||d(32).toString(`hex`),b=process.env.DUCKPOND_JWT_EXPIRES_IN?parseInt(process.env.DUCKPOND_JWT_EXPIRES_IN,10):365*24*60*60,x=new Map,S=new Map;function C(e){v(`🚀 Initializing FastMCP server...`);let l=new c(e.config),u={name:`duckpond`,version:_.version,health:{enabled:!0,path:`/health`,status:200,message:JSON.stringify({status:`healthy`,service:`duckpond-mcp-server`,version:_.version,timestamp:new Date().toISOString()})}},d=e.oauth?.enabled||e.basicAuth||e.bearerToken?new m({...u,oauth:{enabled:!0,authorizationServer:{issuer:e.oauth?.issuer||`http://localhost:${e.port||3e3}`,authorizationEndpoint:`${e.oauth?.issuer||`http://localhost:${e.port||3e3}`}/oauth/authorize`,tokenEndpoint:`${e.oauth?.issuer||`http://localhost:${e.port||3e3}`}/oauth/token`,jwksUri:`${e.oauth?.issuer||`http://localhost:${e.port||3e3}`}/oauth/jwks`,registrationEndpoint:`${e.oauth?.issuer||`http://localhost:${e.port||3e3}`}/oauth/register`,responseTypesSupported:[`code`],grantTypesSupported:[`authorization_code`],tokenEndpointAuthMethodsSupported:[`client_secret_post`,`client_secret_basic`],codeChallengeMethodsSupported:[`S256`,`plain`]},protectedResource:{resource:process.env.DUCKPOND_OAUTH_RESOURCE||e.oauth?.resource||`${e.oauth?.issuer||`http://localhost:${e.port||3e3}`}/mcp`,authorizationServers:[e.oauth?.issuer||`http://localhost:${e.port||3e3}`]}},authenticate:t=>{let n=t.headers?.authorization,r=e.oauth?.issuer||`http://localhost:${e.port||3e3}`;if(!n)throw e.oauth?.enabled?new Response(JSON.stringify({error:`unauthorized`,error_description:`Authorization required. Please authenticate via OAuth.`}),{status:401,statusText:`Unauthorized`,headers:{"Content-Type":`application/json`,"WWW-Authenticate":`Bearer realm="MCP", authorization_uri="${r}/oauth/authorize", resource="${r}/.well-known/oauth-protected-resource"`}}):new Response(JSON.stringify({error:`unauthorized`,error_description:`Authorization required.`}),{status:401,statusText:`Unauthorized`,headers:{"Content-Type":`application/json`}});if(e.basicAuth&&n.startsWith(`Basic `)){let[t,r]=Buffer.from(n.slice(6),`base64`).toString(`utf-8`).split(`:`);if(t===e.basicAuth.username&&r===e.basicAuth.password)return Promise.resolve({userId:e.basicAuth.userId||t,email:e.basicAuth.email||`${t}@example.com`,scope:`read write`});throw new Response(JSON.stringify({error:`unauthorized`,error_description:`Invalid username or password`}),{status:401,statusText:`Unauthorized`,headers:{"Content-Type":`application/json`,"WWW-Authenticate":`Basic realm="MCP"`}})}if(e.bearerToken&&n.startsWith(`Bearer `)){if(n.slice(7)===e.bearerToken.token)return Promise.resolve({userId:e.bearerToken.userId||`bearer-user`,email:``,scope:`read write`});if(!e.oauth?.enabled)throw new Response(JSON.stringify({error:`invalid_token`,error_description:`Invalid bearer token`}),{status:401,statusText:`Unauthorized`,headers:{"Content-Type":`application/json`,"WWW-Authenticate":`Bearer realm="MCP", error="invalid_token"`}})}if(e.oauth?.enabled&&n.startsWith(`Bearer `)){let t=n.slice(7);try{let n=h.verify(t,y);if(!n.sub||!n.iat||!n.exp)throw new Response(JSON.stringify({error:`invalid_token`,error_description:`Invalid token structure`}),{status:401,statusText:`Unauthorized`,headers:{"Content-Type":`application/json`,"WWW-Authenticate":`Bearer realm="MCP", error="invalid_token", error_description="Invalid token structure"`}});let i=e.oauth?.resource||`${r}/mcp`;if(n.aud&&n.aud!==i)throw new Response(JSON.stringify({error:`invalid_token`,error_description:`Token audience mismatch`}),{status:401,statusText:`Unauthorized`,headers:{"Content-Type":`application/json`,"WWW-Authenticate":`Bearer realm="MCP", error="invalid_token", error_description="Token audience mismatch"`}});return Promise.resolve({userId:n.sub,email:n.email||``,scope:n.scope||`read write`})}catch(e){throw e instanceof Response?e:new Response(JSON.stringify({error:`invalid_token`,error_description:`Invalid or expired token`}),{status:401,statusText:`Unauthorized`,headers:{"Content-Type":`application/json`,"WWW-Authenticate":`Bearer realm="MCP", error="invalid_token", error_description="Invalid or expired token"`}})}}throw new Response(JSON.stringify({error:`unauthorized`,error_description:`Invalid authorization header format`}),{status:401,statusText:`Unauthorized`,headers:{"Content-Type":`application/json`,"WWW-Authenticate":`Bearer realm="MCP", authorization_uri="${r}/oauth/authorize", resource="${r}/.well-known/oauth-protected-resource"`}})}}):new m(u),f=(e,t)=>{if(typeof t==`bigint`)return Number.isSafeInteger(Number(t))?Number(t):t.toString();if(t&&typeof t==`object`&&`micros`in t){let e=t.micros,n=typeof e==`bigint`?Number(e/1000n):Number(e)/1e3;return new Date(n).toISOString()}return t};d.addTool({name:`query`,description:`Execute a SQL query for a specific user and return results`,parameters:o,execute:async e=>{try{let t=s(e.userId),n=await l.query(t,e.sql);return n.success?JSON.stringify({rows:n.data,rowCount:n.data.length,executionTime:n.executionTime},f,2):`ERROR: ${n.error.message}`}catch(e){v(`Error in query tool:`,e);let t=e instanceof Error?e.message:String(e);return`ERROR: ${JSON.stringify({error:t},null,2)}`}}}),d.addTool({name:`execute`,description:`Execute SQL statement (DDL/DML) for a specific user without returning results`,parameters:n,execute:async e=>{try{let t=s(e.userId),n=await l.execute(t,e.sql);return n.success?JSON.stringify({success:!0,message:`Statement executed successfully`,executionTime:n.executionTime},null,2):`ERROR: ${n.error.message}`}catch(e){v(`Error in execute tool:`,e);let t=e instanceof Error?e.message:String(e);return`ERROR: ${JSON.stringify({error:t},null,2)}`}}}),d.addTool({name:`getUserStats`,description:`Get statistics about a user's database (memory usage, query count, etc.)`,parameters:r,execute:async e=>{try{let t=s(e.userId),n=await l.getUserStats(t);return n.success?JSON.stringify({...n.data,lastAccess:n.data.lastAccess.toISOString()},null,2):`ERROR: ${n.error.message}`}catch(e){v(`Error in getUserStats tool:`,e);let t=e instanceof Error?e.message:String(e);return`ERROR: ${JSON.stringify({error:t},null,2)}`}}}),d.addTool({name:`isAttached`,description:`Check if a user's database is currently cached in memory`,parameters:i,execute:async e=>{try{let t=s(e.userId),n=l.isAttached(t);return n.success?JSON.stringify({attached:n.data,userId:t},null,2):`ERROR: ${n.error.message}`}catch(e){v(`Error in isAttached tool:`,e);let t=e instanceof Error?e.message:String(e);return`ERROR: ${JSON.stringify({error:t},null,2)}`}}}),d.addTool({name:`detachUser`,description:`Manually detach a user's database from the cache to free resources`,parameters:t,execute:async e=>{try{let t=s(e.userId),n=await l.detachUser(t);return n.success?JSON.stringify({success:!0,message:`User ${t} detached successfully`},null,2):`ERROR: ${n.error.message}`}catch(e){v(`Error in detachUser tool:`,e);let t=e instanceof Error?e.message:String(e);return`ERROR: ${JSON.stringify({error:t},null,2)}`}}}),d.addTool({name:`listUsers`,description:`List all currently cached users and cache statistics`,parameters:a,execute:async()=>{try{let e=l.listUsers();return e.success?JSON.stringify(e.data,null,2):`ERROR: ${e.error.message}`}catch(e){v(`Error in listUsers tool:`,e);let t=e instanceof Error?e.message:String(e);return`ERROR: ${JSON.stringify({error:t},null,2)}`}}}),e.oauth?.enabled&&w(d,e);let p=d.getApp();return p.get(`/`,t=>{let n=e.oauth?.issuer||`http://localhost:${e.port||3e3}`,r={name:`DuckPond MCP Server`,version:_.version,description:`Model Context Protocol server for multi-tenant DuckDB with R2/S3 storage`,service:`duckpond-mcp-server`,capabilities:{tools:[`query`,`execute`,`getUserStats`,`isAttached`,`detachUser`,`listUsers`],transports:[`stdio`,`http`],authentication:{oauth:e.oauth?.enabled||!1,basicAuth:!!e.basicAuth,bearerToken:!!e.bearerToken}},endpoints:{mcp:`${n}${e.endpoint||`/mcp`}`,health:`${n}/health`,ui:`${n}/ui/:userId`,...e.oauth?.enabled&&{oauth:{authorization:`${n}/oauth/authorize`,token:`${n}/oauth/token`,jwks:`${n}/oauth/jwks`,register:`${n}/oauth/register`}}},timestamp:new Date().toISOString()};return t.json(r)}),T(p,l,e),v(`✓ FastMCP server created`),{server:d,duckpond:l}}function w(e,t){let n=e.getApp();setInterval(()=>{let e=Date.now();for(let[t,n]of x.entries())e-n.createdAt>6e5&&x.delete(t);for(let[t,n]of S.entries())e-n.createdAt>2592e6&&S.delete(t)},6e4),n.get(`/oauth/authorize`,e=>{let t=e.req.query(),n=t.response_type,r=t.redirect_uri,i=t.state,a=t.code_challenge,o=t.code_challenge_method,s=t.client_id;if(n!==`code`)return e.json({error:`unsupported_response_type`,error_description:`Only 'code' response type is supported`},400);if(!r)return e.json({error:`invalid_request`,error_description:`redirect_uri is required`},400);if(a&&(!o||![`S256`,`plain`].includes(o)))return e.json({error:`invalid_request`,error_description:`Invalid code_challenge_method. Only 'S256' and 'plain' are supported`},400);let c=`
1
+ import{loggers as e}from"./utils/logger.js";import{detachUserSchema as t,executeSchema as n,getUserStatsSchema as r,isAttachedSchema as i,listUsersSchema as a,querySchema as o,resolveUserId as s}from"./tools/index.js";import{t as c}from"./server-core-Cj3cIk43.js";import{startUIServer as l}from"./ui-server.js";import{createHash as u,randomBytes as d,webcrypto as f}from"crypto";import{createRequire as p}from"module";import{FastMCP as m}from"fastmcp";import*as h from"jsonwebtoken";import{URL as g}from"url";globalThis.crypto||(globalThis.crypto=f);const _=p(import.meta.url)(`../package.json`),v=e.fastmcp,y=process.env.DUCKPOND_JWT_SECRET||d(32).toString(`hex`),b=process.env.DUCKPOND_JWT_EXPIRES_IN?parseInt(process.env.DUCKPOND_JWT_EXPIRES_IN,10):365*24*60*60,x=new Map,S=new Map;function C(e){v(`🚀 Initializing FastMCP server...`);let l=new c(e.config),u={name:`duckpond`,version:_.version,health:{enabled:!0,path:`/health`,status:200,message:JSON.stringify({status:`healthy`,service:`duckpond-mcp-server`,version:_.version,timestamp:new Date().toISOString()})}},d=e.oauth?.enabled||e.basicAuth||e.bearerToken?new m({...u,oauth:{enabled:!0,authorizationServer:{issuer:e.oauth?.issuer||`http://localhost:${e.port||3e3}`,authorizationEndpoint:`${e.oauth?.issuer||`http://localhost:${e.port||3e3}`}/oauth/authorize`,tokenEndpoint:`${e.oauth?.issuer||`http://localhost:${e.port||3e3}`}/oauth/token`,jwksUri:`${e.oauth?.issuer||`http://localhost:${e.port||3e3}`}/oauth/jwks`,registrationEndpoint:`${e.oauth?.issuer||`http://localhost:${e.port||3e3}`}/oauth/register`,responseTypesSupported:[`code`],grantTypesSupported:[`authorization_code`],tokenEndpointAuthMethodsSupported:[`client_secret_post`,`client_secret_basic`],codeChallengeMethodsSupported:[`S256`,`plain`]},protectedResource:{resource:process.env.DUCKPOND_OAUTH_RESOURCE||e.oauth?.resource||`${e.oauth?.issuer||`http://localhost:${e.port||3e3}`}/mcp`,authorizationServers:[e.oauth?.issuer||`http://localhost:${e.port||3e3}`]}},authenticate:t=>{let n=t.headers?.authorization,r=e.oauth?.issuer||`http://localhost:${e.port||3e3}`;if(!n)throw e.oauth?.enabled?new Response(JSON.stringify({error:`unauthorized`,error_description:`Authorization required. Please authenticate via OAuth.`}),{status:401,statusText:`Unauthorized`,headers:{"Content-Type":`application/json`,"WWW-Authenticate":`Bearer realm="MCP", authorization_uri="${r}/oauth/authorize", resource="${r}/.well-known/oauth-protected-resource"`}}):new Response(JSON.stringify({error:`unauthorized`,error_description:`Authorization required.`}),{status:401,statusText:`Unauthorized`,headers:{"Content-Type":`application/json`}});if(e.basicAuth&&n.startsWith(`Basic `)){let[t,r]=Buffer.from(n.slice(6),`base64`).toString(`utf-8`).split(`:`);if(t===e.basicAuth.username&&r===e.basicAuth.password)return Promise.resolve({userId:e.basicAuth.userId||t,email:e.basicAuth.email||`${t}@example.com`,scope:`read write`});throw new Response(JSON.stringify({error:`unauthorized`,error_description:`Invalid username or password`}),{status:401,statusText:`Unauthorized`,headers:{"Content-Type":`application/json`,"WWW-Authenticate":`Basic realm="MCP"`}})}if(e.bearerToken&&n.startsWith(`Bearer `)){if(n.slice(7)===e.bearerToken.token)return Promise.resolve({userId:e.bearerToken.userId||`bearer-user`,email:``,scope:`read write`});if(!e.oauth?.enabled)throw new Response(JSON.stringify({error:`invalid_token`,error_description:`Invalid bearer token`}),{status:401,statusText:`Unauthorized`,headers:{"Content-Type":`application/json`,"WWW-Authenticate":`Bearer realm="MCP", error="invalid_token"`}})}if(e.oauth?.enabled&&n.startsWith(`Bearer `)){let t=n.slice(7);try{let n=h.verify(t,y);if(!n.sub||!n.iat||!n.exp)throw new Response(JSON.stringify({error:`invalid_token`,error_description:`Invalid token structure`}),{status:401,statusText:`Unauthorized`,headers:{"Content-Type":`application/json`,"WWW-Authenticate":`Bearer realm="MCP", error="invalid_token", error_description="Invalid token structure"`}});let i=e.oauth?.resource||`${r}/mcp`;if(n.aud&&n.aud!==i)throw new Response(JSON.stringify({error:`invalid_token`,error_description:`Token audience mismatch`}),{status:401,statusText:`Unauthorized`,headers:{"Content-Type":`application/json`,"WWW-Authenticate":`Bearer realm="MCP", error="invalid_token", error_description="Token audience mismatch"`}});return Promise.resolve({userId:n.sub,email:n.email||``,scope:n.scope||`read write`})}catch(e){throw e instanceof Response?e:new Response(JSON.stringify({error:`invalid_token`,error_description:`Invalid or expired token`}),{status:401,statusText:`Unauthorized`,headers:{"Content-Type":`application/json`,"WWW-Authenticate":`Bearer realm="MCP", error="invalid_token", error_description="Invalid or expired token"`}})}}throw new Response(JSON.stringify({error:`unauthorized`,error_description:`Invalid authorization header format`}),{status:401,statusText:`Unauthorized`,headers:{"Content-Type":`application/json`,"WWW-Authenticate":`Bearer realm="MCP", authorization_uri="${r}/oauth/authorize", resource="${r}/.well-known/oauth-protected-resource"`}})}}):new m(u),f=(e,t)=>{if(typeof t==`bigint`)return Number.isSafeInteger(Number(t))?Number(t):t.toString();if(t&&typeof t==`object`&&`micros`in t){let e=t.micros,n=typeof e==`bigint`?Number(e/1000n):Number(e)/1e3;return new Date(n).toISOString()}return t};d.addTool({name:`query`,description:`Execute a SQL query for a specific user and return results`,parameters:o,execute:async e=>{try{let t=s(e.userId),n=await l.query(t,e.sql);return n.success?JSON.stringify({rows:n.data,rowCount:n.data.length,executionTime:n.executionTime},f,2):`ERROR: ${n.error.message}`}catch(e){v(`Error in query tool:`,e);let t=e instanceof Error?e.message:String(e);return`ERROR: ${JSON.stringify({error:t},null,2)}`}}}),d.addTool({name:`execute`,description:`Execute SQL statement (DDL/DML) for a specific user without returning results`,parameters:n,execute:async e=>{try{let t=s(e.userId),n=await l.execute(t,e.sql);return n.success?JSON.stringify({success:!0,message:`Statement executed successfully`,executionTime:n.executionTime},null,2):`ERROR: ${n.error.message}`}catch(e){v(`Error in execute tool:`,e);let t=e instanceof Error?e.message:String(e);return`ERROR: ${JSON.stringify({error:t},null,2)}`}}}),d.addTool({name:`getUserStats`,description:`Get statistics about a user's database (memory usage, query count, etc.)`,parameters:r,execute:async e=>{try{let t=s(e.userId),n=await l.getUserStats(t);return n.success?JSON.stringify({...n.data,lastAccess:n.data.lastAccess.toISOString()},null,2):`ERROR: ${n.error.message}`}catch(e){v(`Error in getUserStats tool:`,e);let t=e instanceof Error?e.message:String(e);return`ERROR: ${JSON.stringify({error:t},null,2)}`}}}),d.addTool({name:`isAttached`,description:`Check if a user's database is currently cached in memory`,parameters:i,execute:async e=>{try{let t=s(e.userId),n=l.isAttached(t);return n.success?JSON.stringify({attached:n.data,userId:t},null,2):`ERROR: ${n.error.message}`}catch(e){v(`Error in isAttached tool:`,e);let t=e instanceof Error?e.message:String(e);return`ERROR: ${JSON.stringify({error:t},null,2)}`}}}),d.addTool({name:`detachUser`,description:`Manually detach a user's database from the cache to free resources`,parameters:t,execute:async e=>{try{let t=s(e.userId),n=await l.detachUser(t);return n.success?JSON.stringify({success:!0,message:`User ${t} detached successfully`},null,2):`ERROR: ${n.error.message}`}catch(e){v(`Error in detachUser tool:`,e);let t=e instanceof Error?e.message:String(e);return`ERROR: ${JSON.stringify({error:t},null,2)}`}}}),d.addTool({name:`listUsers`,description:`List all currently cached users and cache statistics`,parameters:a,execute:async()=>{try{let e=l.listUsers();return e.success?JSON.stringify(e.data,null,2):`ERROR: ${e.error.message}`}catch(e){v(`Error in listUsers tool:`,e);let t=e instanceof Error?e.message:String(e);return`ERROR: ${JSON.stringify({error:t},null,2)}`}}}),e.oauth?.enabled&&w(d,e);let p=d.getApp();return p.get(`/`,t=>{let n=e.oauth?.issuer||`http://localhost:${e.port||3e3}`,r={name:`DuckPond MCP Server`,version:_.version,description:`Model Context Protocol server for multi-tenant DuckDB with R2/S3 storage`,service:`duckpond-mcp-server`,capabilities:{tools:[`query`,`execute`,`getUserStats`,`isAttached`,`detachUser`,`listUsers`],transports:[`stdio`,`http`],authentication:{oauth:e.oauth?.enabled||!1,basicAuth:!!e.basicAuth,bearerToken:!!e.bearerToken}},endpoints:{mcp:`${n}${e.endpoint||`/mcp`}`,health:`${n}/health`,ui:`${n}/ui/:userId`,...e.oauth?.enabled&&{oauth:{authorization:`${n}/oauth/authorize`,token:`${n}/oauth/token`,jwks:`${n}/oauth/jwks`,register:`${n}/oauth/register`}}},timestamp:new Date().toISOString()};return t.json(r)}),T(p,l,e),v(`✓ FastMCP server created`),{server:d,duckpond:l}}function w(e,t){let n=e.getApp();setInterval(()=>{let e=Date.now();for(let[t,n]of x.entries())e-n.createdAt>6e5&&x.delete(t);for(let[t,n]of S.entries())e-n.createdAt>2592e6&&S.delete(t)},6e4),n.get(`/oauth/authorize`,e=>{let t=e.req.query(),n=t.response_type,r=t.redirect_uri,i=t.state,a=t.code_challenge,o=t.code_challenge_method,s=t.client_id;if(n!==`code`)return e.json({error:`unsupported_response_type`,error_description:`Only 'code' response type is supported`},400);if(!r)return e.json({error:`invalid_request`,error_description:`redirect_uri is required`},400);if(a&&(!o||![`S256`,`plain`].includes(o)))return e.json({error:`invalid_request`,error_description:`Invalid code_challenge_method. Only 'S256' and 'plain' are supported`},400);let c=`
2
2
  <!DOCTYPE html>
3
3
  <html>
4
4
  <head>
@@ -1 +1 @@
1
- {"version":3,"file":"server.js","names":[],"sources":["../src/server.ts"],"sourcesContent":["// Polyfill for Web Crypto API in Node.js environments\nimport { webcrypto } from \"crypto\"\n\nif (!globalThis.crypto) {\n globalThis.crypto = webcrypto as Crypto\n}\n\nimport { createHash, randomBytes } from \"crypto\"\nimport { FastMCP } from \"fastmcp\"\nimport { createRequire } from \"module\"\n\nconst require = createRequire(import.meta.url)\nconst packageJson = require(\"../package.json\") as { version: string }\nimport * as jwt from \"jsonwebtoken\"\nimport { URL } from \"url\"\nimport { z } from \"zod\"\n\nimport { DuckPondServer, type DuckPondServerConfig } from \"./server-core\"\nimport {\n detachUserSchema,\n executeSchema,\n getDefaultUserId,\n getUserStatsSchema,\n isAttachedSchema,\n listUsersSchema,\n querySchema,\n resolveUserId,\n} from \"./tools\"\nimport { startUIServer } from \"./ui-server\"\nimport { loggers } from \"./utils/logger\"\n\nconst log = loggers.fastmcp\n\nexport type OAuthConfig = {\n enabled: boolean\n username: string\n password: string\n userId: string\n email?: string\n issuer?: string\n resource?: string\n}\n\nexport type BearerTokenConfig = {\n token: string\n userId?: string\n}\n\nexport type FastMCPServerOptions = {\n config: DuckPondServerConfig\n port?: number\n endpoint?: string\n oauth?: OAuthConfig\n basicAuth?: {\n username: string\n password: string\n userId?: string\n email?: string\n }\n bearerToken?: BearerTokenConfig\n ui?: {\n enabled: boolean\n port: number\n internalPort?: number\n autoStartUser?: string\n }\n}\n\n// JWT secret for token signing/validation\nconst JWT_SECRET = process.env.DUCKPOND_JWT_SECRET || randomBytes(32).toString(\"hex\")\n\n// JWT token expiration configuration (default: 1 year)\nconst JWT_EXPIRES_IN = process.env.DUCKPOND_JWT_EXPIRES_IN\n ? parseInt(process.env.DUCKPOND_JWT_EXPIRES_IN, 10)\n : 365 * 24 * 60 * 60 // 1 year in seconds\n\n// In-memory stores for OAuth flow\nconst authorizationCodes = new Map<\n string,\n {\n createdAt: number\n redirectUri?: string\n codeChallenge?: string\n codeChallengeMethod?: string\n userId: string\n }\n>()\n\nconst refreshTokens = new Map<\n string,\n {\n createdAt: number\n userId: string\n email?: string\n }\n>()\n\n// AuthSession type for FastMCP authentication\ntype AuthSession = {\n userId: string\n email: string\n scope: string\n [key: string]: unknown // Allow additional properties\n}\n\ntype OAuthClientRegistrationRequest = {\n grant_types?: string[]\n response_types?: string[]\n redirect_uris?: string[]\n token_endpoint_auth_method?: string\n client_name?: string\n scope?: string\n}\n\ntype OAuthClientRegistrationResponse = {\n client_id: string\n client_secret: string\n client_id_issued_at: number\n client_secret_expires_at: number\n grant_types: string[]\n response_types: string[]\n redirect_uris: string[]\n token_endpoint_auth_method: string\n client_name?: string\n scope?: string\n}\n\nexport function createFastMCPServer(options: FastMCPServerOptions): {\n server: FastMCP\n duckpond: DuckPondServer\n} {\n log(\"🚀 Initializing FastMCP server...\")\n\n // Create DuckPond server instance\n const duckpond = new DuckPondServer(options.config)\n\n // Build server configuration\n const baseConfig = {\n name: \"duckpond\",\n version: packageJson.version as `${number}.${number}.${number}`,\n health: {\n enabled: true,\n path: \"/health\",\n status: 200,\n message: JSON.stringify({\n status: \"healthy\",\n service: \"duckpond-mcp-server\",\n version: packageJson.version,\n timestamp: new Date().toISOString(),\n }),\n },\n }\n\n // Create server with authentication (OAuth, Basic Auth, Bearer Token, or none)\n const server =\n options.oauth?.enabled || options.basicAuth || options.bearerToken\n ? new FastMCP<AuthSession>({\n ...baseConfig,\n oauth: {\n enabled: true,\n authorizationServer: {\n issuer: options.oauth?.issuer || `http://localhost:${options.port || 3000}`,\n authorizationEndpoint: `${options.oauth?.issuer || `http://localhost:${options.port || 3000}`}/oauth/authorize`,\n tokenEndpoint: `${options.oauth?.issuer || `http://localhost:${options.port || 3000}`}/oauth/token`,\n jwksUri: `${options.oauth?.issuer || `http://localhost:${options.port || 3000}`}/oauth/jwks`,\n registrationEndpoint: `${options.oauth?.issuer || `http://localhost:${options.port || 3000}`}/oauth/register`,\n responseTypesSupported: [\"code\"],\n grantTypesSupported: [\"authorization_code\"],\n tokenEndpointAuthMethodsSupported: [\"client_secret_post\", \"client_secret_basic\"],\n codeChallengeMethodsSupported: [\"S256\", \"plain\"],\n },\n protectedResource: {\n resource:\n process.env.DUCKPOND_OAUTH_RESOURCE ||\n options.oauth?.resource ||\n `${options.oauth?.issuer || `http://localhost:${options.port || 3000}`}/mcp`,\n authorizationServers: [options.oauth?.issuer || `http://localhost:${options.port || 3000}`],\n },\n },\n authenticate: (request) => {\n const authHeader = request.headers?.authorization\n const baseUrl = options.oauth?.issuer || `http://localhost:${options.port || 3000}`\n\n // For OAuth-enabled servers, require authentication\n if (!authHeader) {\n if (options.oauth?.enabled) {\n // Return HTTP 401 with WWW-Authenticate header for proper OAuth discovery\n throw new Response(\n JSON.stringify({\n error: \"unauthorized\",\n error_description: \"Authorization required. Please authenticate via OAuth.\",\n }),\n {\n status: 401,\n statusText: \"Unauthorized\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"WWW-Authenticate\": `Bearer realm=\"MCP\", authorization_uri=\"${baseUrl}/oauth/authorize\", resource=\"${baseUrl}/.well-known/oauth-protected-resource\"`,\n },\n },\n )\n }\n\n // For non-OAuth servers, also require some form of auth\n throw new Response(\n JSON.stringify({\n error: \"unauthorized\",\n error_description: \"Authorization required.\",\n }),\n {\n status: 401,\n statusText: \"Unauthorized\",\n headers: {\n \"Content-Type\": \"application/json\",\n },\n },\n )\n }\n\n // Handle Basic Authentication\n if (options.basicAuth && authHeader.startsWith(\"Basic \")) {\n const credentials = Buffer.from(authHeader.slice(6), \"base64\").toString(\"utf-8\")\n const [username, password] = credentials.split(\":\")\n\n if (username === options.basicAuth.username && password === options.basicAuth.password) {\n return Promise.resolve({\n userId: options.basicAuth.userId || username,\n email: options.basicAuth.email || `${username}@example.com`,\n scope: \"read write\",\n })\n } else {\n throw new Response(\n JSON.stringify({\n error: \"unauthorized\",\n error_description: \"Invalid username or password\",\n }),\n {\n status: 401,\n statusText: \"Unauthorized\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"WWW-Authenticate\": `Basic realm=\"MCP\"`,\n },\n },\n )\n }\n }\n\n // Handle static Bearer Token authentication\n if (options.bearerToken && authHeader.startsWith(\"Bearer \")) {\n const token = authHeader.slice(7)\n\n if (token === options.bearerToken.token) {\n return Promise.resolve({\n userId: options.bearerToken.userId || \"bearer-user\",\n email: \"\",\n scope: \"read write\",\n })\n }\n\n // If bearer token is configured but doesn't match, and OAuth is not enabled, reject\n if (!options.oauth?.enabled) {\n throw new Response(\n JSON.stringify({\n error: \"invalid_token\",\n error_description: \"Invalid bearer token\",\n }),\n {\n status: 401,\n statusText: \"Unauthorized\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"WWW-Authenticate\": `Bearer realm=\"MCP\", error=\"invalid_token\"`,\n },\n },\n )\n }\n\n // Fall through to OAuth JWT validation if OAuth is also enabled\n }\n\n // Handle Bearer Token (OAuth) - Validate JWT\n if (options.oauth?.enabled && authHeader.startsWith(\"Bearer \")) {\n const token = authHeader.slice(7) // Remove 'Bearer ' prefix\n\n try {\n // Verify JWT token\n const decoded = jwt.verify(token, JWT_SECRET) as jwt.JwtPayload\n\n if (!decoded.sub || !decoded.iat || !decoded.exp) {\n throw new Response(\n JSON.stringify({\n error: \"invalid_token\",\n error_description: \"Invalid token structure\",\n }),\n {\n status: 401,\n statusText: \"Unauthorized\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"WWW-Authenticate\": `Bearer realm=\"MCP\", error=\"invalid_token\", error_description=\"Invalid token structure\"`,\n },\n },\n )\n }\n\n // Validate audience\n const expectedAudience = options.oauth?.resource || `${baseUrl}/mcp`\n if (decoded.aud && decoded.aud !== expectedAudience) {\n throw new Response(\n JSON.stringify({\n error: \"invalid_token\",\n error_description: \"Token audience mismatch\",\n }),\n {\n status: 401,\n statusText: \"Unauthorized\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"WWW-Authenticate\": `Bearer realm=\"MCP\", error=\"invalid_token\", error_description=\"Token audience mismatch\"`,\n },\n },\n )\n }\n\n // Return user info from JWT claims\n return Promise.resolve({\n userId: decoded.sub,\n email: (decoded.email as string) || \"\",\n scope: (decoded.scope as string) || \"read write\",\n })\n } catch (error) {\n if (error instanceof Response) {\n throw error // Re-throw our custom Response errors\n }\n\n throw new Response(\n JSON.stringify({\n error: \"invalid_token\",\n error_description: \"Invalid or expired token\",\n }),\n {\n status: 401,\n statusText: \"Unauthorized\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"WWW-Authenticate\": `Bearer realm=\"MCP\", error=\"invalid_token\", error_description=\"Invalid or expired token\"`,\n },\n },\n )\n }\n }\n\n throw new Response(\n JSON.stringify({\n error: \"unauthorized\",\n error_description: \"Invalid authorization header format\",\n }),\n {\n status: 401,\n statusText: \"Unauthorized\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"WWW-Authenticate\": `Bearer realm=\"MCP\", authorization_uri=\"${baseUrl}/oauth/authorize\", resource=\"${baseUrl}/.well-known/oauth-protected-resource\"`,\n },\n },\n )\n },\n })\n : new FastMCP(baseConfig)\n\n // DuckDB type-safe JSON serializer (handles BigInt and timestamp objects)\n const duckDBReplacer = (_key: string, value: unknown): unknown => {\n if (typeof value === \"bigint\") {\n // Convert to number if safe, otherwise string\n return Number.isSafeInteger(Number(value)) ? Number(value) : value.toString()\n }\n // Handle DuckDB timestamp objects {micros: bigint}\n if (value && typeof value === \"object\" && \"micros\" in value) {\n const micros = (value as { micros: bigint }).micros\n const ms = typeof micros === \"bigint\" ? Number(micros / 1000n) : Number(micros) / 1000\n return new Date(ms).toISOString()\n }\n return value\n }\n\n // Add query tool\n server.addTool({\n name: \"query\",\n description: \"Execute a SQL query for a specific user and return results\",\n parameters: querySchema,\n execute: async (args) => {\n try {\n const userId = resolveUserId(args.userId)\n const result = await duckpond.query(userId, args.sql)\n\n if (!result.success) {\n return `ERROR: ${result.error.message}`\n }\n\n return JSON.stringify(\n {\n rows: result.data,\n rowCount: result.data.length,\n executionTime: result.executionTime,\n },\n duckDBReplacer,\n 2,\n )\n } catch (error) {\n log(\"Error in query tool:\", error)\n const errorMessage = error instanceof Error ? error.message : String(error)\n return `ERROR: ${JSON.stringify({ error: errorMessage }, null, 2)}`\n }\n },\n })\n\n // Add execute tool\n server.addTool({\n name: \"execute\",\n description: \"Execute SQL statement (DDL/DML) for a specific user without returning results\",\n parameters: executeSchema,\n execute: async (args) => {\n try {\n const userId = resolveUserId(args.userId)\n const result = await duckpond.execute(userId, args.sql)\n\n if (!result.success) {\n return `ERROR: ${result.error.message}`\n }\n\n return JSON.stringify(\n {\n success: true,\n message: \"Statement executed successfully\",\n executionTime: result.executionTime,\n },\n null,\n 2,\n )\n } catch (error) {\n log(\"Error in execute tool:\", error)\n const errorMessage = error instanceof Error ? error.message : String(error)\n return `ERROR: ${JSON.stringify({ error: errorMessage }, null, 2)}`\n }\n },\n })\n\n // Add getUserStats tool\n server.addTool({\n name: \"getUserStats\",\n description: \"Get statistics about a user's database (memory usage, query count, etc.)\",\n parameters: getUserStatsSchema,\n execute: async (args) => {\n try {\n const userId = resolveUserId(args.userId)\n const result = await duckpond.getUserStats(userId)\n\n if (!result.success) {\n return `ERROR: ${result.error.message}`\n }\n\n return JSON.stringify(\n {\n ...result.data,\n lastAccess: result.data.lastAccess.toISOString(),\n },\n null,\n 2,\n )\n } catch (error) {\n log(\"Error in getUserStats tool:\", error)\n const errorMessage = error instanceof Error ? error.message : String(error)\n return `ERROR: ${JSON.stringify({ error: errorMessage }, null, 2)}`\n }\n },\n })\n\n // Add isAttached tool\n server.addTool({\n name: \"isAttached\",\n description: \"Check if a user's database is currently cached in memory\",\n parameters: isAttachedSchema,\n execute: async (args) => {\n try {\n const userId = resolveUserId(args.userId)\n const result = duckpond.isAttached(userId)\n\n if (!result.success) {\n return `ERROR: ${result.error.message}`\n }\n\n return JSON.stringify(\n {\n attached: result.data,\n userId,\n },\n null,\n 2,\n )\n } catch (error) {\n log(\"Error in isAttached tool:\", error)\n const errorMessage = error instanceof Error ? error.message : String(error)\n return `ERROR: ${JSON.stringify({ error: errorMessage }, null, 2)}`\n }\n },\n })\n\n // Add detachUser tool\n server.addTool({\n name: \"detachUser\",\n description: \"Manually detach a user's database from the cache to free resources\",\n parameters: detachUserSchema,\n execute: async (args) => {\n try {\n const userId = resolveUserId(args.userId)\n const result = await duckpond.detachUser(userId)\n\n if (!result.success) {\n return `ERROR: ${result.error.message}`\n }\n\n return JSON.stringify(\n {\n success: true,\n message: `User ${userId} detached successfully`,\n },\n null,\n 2,\n )\n } catch (error) {\n log(\"Error in detachUser tool:\", error)\n const errorMessage = error instanceof Error ? error.message : String(error)\n return `ERROR: ${JSON.stringify({ error: errorMessage }, null, 2)}`\n }\n },\n })\n\n // Add listUsers tool\n server.addTool({\n name: \"listUsers\",\n description: \"List all currently cached users and cache statistics\",\n parameters: listUsersSchema,\n execute: async () => {\n try {\n const result = duckpond.listUsers()\n\n if (!result.success) {\n return `ERROR: ${result.error.message}`\n }\n\n return JSON.stringify(result.data, null, 2)\n } catch (error) {\n log(\"Error in listUsers tool:\", error)\n const errorMessage = error instanceof Error ? error.message : String(error)\n return `ERROR: ${JSON.stringify({ error: errorMessage }, null, 2)}`\n }\n },\n })\n\n // Add OAuth flow endpoints if OAuth is enabled\n if (options.oauth?.enabled) {\n setupOAuthEndpoints(server, options)\n }\n\n // Add root info endpoint using Hono\n const app = server.getApp()\n app.get(\"/\", (c) => {\n const baseUrl = options.oauth?.issuer || `http://localhost:${options.port || 3000}`\n\n const serverInfo = {\n name: \"DuckPond MCP Server\",\n version: packageJson.version,\n description: \"Model Context Protocol server for multi-tenant DuckDB with R2/S3 storage\",\n service: \"duckpond-mcp-server\",\n capabilities: {\n tools: [\"query\", \"execute\", \"getUserStats\", \"isAttached\", \"detachUser\", \"listUsers\"],\n transports: [\"stdio\", \"http\"],\n authentication: {\n oauth: options.oauth?.enabled || false,\n basicAuth: !!options.basicAuth,\n bearerToken: !!options.bearerToken,\n },\n },\n endpoints: {\n mcp: `${baseUrl}${options.endpoint || \"/mcp\"}`,\n health: `${baseUrl}/health`,\n ui: `${baseUrl}/ui/:userId`,\n ...(options.oauth?.enabled && {\n oauth: {\n authorization: `${baseUrl}/oauth/authorize`,\n token: `${baseUrl}/oauth/token`,\n jwks: `${baseUrl}/oauth/jwks`,\n register: `${baseUrl}/oauth/register`,\n },\n }),\n },\n timestamp: new Date().toISOString(),\n }\n\n return c.json(serverInfo)\n })\n\n // Add UI endpoints for DuckDB UI access\n setupUIEndpoints(app, duckpond, options)\n\n log(\"✓ FastMCP server created\")\n\n return { server, duckpond }\n}\n\nfunction setupOAuthEndpoints(server: FastMCP, options: FastMCPServerOptions): void {\n const app = server.getApp()\n\n // Clean up old codes and refresh tokens every minute\n setInterval(() => {\n const now = Date.now()\n // Clean authorization codes (10 minutes)\n for (const [code, data] of authorizationCodes.entries()) {\n if (now - data.createdAt > 600000) {\n authorizationCodes.delete(code)\n }\n }\n // Clean refresh tokens (30 days)\n for (const [token, data] of refreshTokens.entries()) {\n if (now - data.createdAt > 2592000000) {\n refreshTokens.delete(token)\n }\n }\n }, 60000)\n\n // OAuth Authorization Endpoint - Login Form\n app.get(\"/oauth/authorize\", (c) => {\n const params = c.req.query()\n const responseType = params.response_type\n const redirectUri = params.redirect_uri\n const state = params.state\n const codeChallenge = params.code_challenge\n const codeChallengeMethod = params.code_challenge_method\n const clientId = params.client_id\n\n if (responseType !== \"code\") {\n return c.json(\n {\n error: \"unsupported_response_type\",\n error_description: \"Only 'code' response type is supported\",\n },\n 400,\n )\n }\n\n if (!redirectUri) {\n return c.json(\n {\n error: \"invalid_request\",\n error_description: \"redirect_uri is required\",\n },\n 400,\n )\n }\n\n // Validate PKCE parameters if present\n if (codeChallenge) {\n if (!codeChallengeMethod || ![\"S256\", \"plain\"].includes(codeChallengeMethod)) {\n return c.json(\n {\n error: \"invalid_request\",\n error_description: \"Invalid code_challenge_method. Only 'S256' and 'plain' are supported\",\n },\n 400,\n )\n }\n }\n\n // Serve login form\n const loginForm = `\n<!DOCTYPE html>\n<html>\n<head>\n <title>OAuth Login - DuckPond MCP Server</title>\n <style>\n body { font-family: Arial, sans-serif; max-width: 400px; margin: 100px auto; padding: 20px; }\n .form-group { margin-bottom: 15px; }\n label { display: block; margin-bottom: 5px; font-weight: bold; }\n input[type=\"text\"], input[type=\"password\"] { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; }\n button { width: 100%; padding: 12px; background: #007cba; color: white; border: none; border-radius: 4px; font-size: 16px; cursor: pointer; }\n button:hover { background: #005a87; }\n .app-info { background: #f5f5f5; padding: 15px; border-radius: 4px; margin-bottom: 20px; }\n </style>\n</head>\n<body>\n <div class=\"app-info\">\n <h3>🔐 OAuth Authorization</h3>\n <p><strong>Application:</strong> ${clientId || \"MCP Client\"}</p>\n <p><strong>Permissions:</strong> Read and write access to DuckDB databases</p>\n </div>\n\n <form method=\"POST\" action=\"/oauth/authorize\">\n <input type=\"hidden\" name=\"response_type\" value=\"${responseType}\">\n <input type=\"hidden\" name=\"redirect_uri\" value=\"${redirectUri}\">\n <input type=\"hidden\" name=\"state\" value=\"${state || \"\"}\">\n <input type=\"hidden\" name=\"code_challenge\" value=\"${codeChallenge || \"\"}\">\n <input type=\"hidden\" name=\"code_challenge_method\" value=\"${codeChallengeMethod || \"\"}\">\n <input type=\"hidden\" name=\"client_id\" value=\"${clientId || \"\"}\">\n\n <div class=\"form-group\">\n <label for=\"username\">Username:</label>\n <input type=\"text\" id=\"username\" name=\"username\" required>\n </div>\n\n <div class=\"form-group\">\n <label for=\"password\">Password:</label>\n <input type=\"password\" id=\"password\" name=\"password\" required>\n </div>\n\n <button type=\"submit\">Authorize Application</button>\n </form>\n</body>\n</html>`\n\n return c.html(loginForm)\n })\n\n // OAuth Authorization POST - Process Login\n app.post(\"/oauth/authorize\", async (c) => {\n try {\n const body = await c.req.text()\n const params = new URLSearchParams(body)\n\n const username = params.get(\"username\")\n const password = params.get(\"password\")\n const redirectUri = params.get(\"redirect_uri\")\n const state = params.get(\"state\")\n const codeChallenge = params.get(\"code_challenge\")\n const codeChallengeMethod = params.get(\"code_challenge_method\")\n\n // Validate credentials\n if (username !== options.oauth?.username || password !== options.oauth?.password) {\n const errorForm = `\n<!DOCTYPE html>\n<html><head><title>Login Failed</title><style>body{font-family:Arial;max-width:400px;margin:100px auto;padding:20px;}.error{color:red;background:#fee;padding:10px;border-radius:4px;margin-bottom:15px;}</style></head>\n<body><div class=\"error\">❌ Invalid username or password</div><a href=\"javascript:history.back()\">← Try Again</a></body></html>`\n return c.html(errorForm, 401)\n }\n\n // Generate authorization code\n const code = randomBytes(16).toString(\"hex\")\n authorizationCodes.set(code, {\n createdAt: Date.now(),\n redirectUri: redirectUri || \"\",\n codeChallenge: codeChallenge || undefined,\n codeChallengeMethod: codeChallengeMethod || undefined,\n userId: options.oauth?.userId || username || \"oauth-user\",\n })\n\n // Redirect with authorization code\n const redirectUrl = new URL(redirectUri || \"\")\n redirectUrl.searchParams.set(\"code\", code)\n if (state) {\n redirectUrl.searchParams.set(\"state\", state)\n }\n\n return c.redirect(redirectUrl.toString(), 302)\n } catch {\n return c.json(\n {\n error: \"invalid_request\",\n error_description: \"Failed to process authorization request\",\n },\n 400,\n )\n }\n })\n\n // OAuth Token Endpoint\n app.post(\"/oauth/token\", async (c) => {\n const body = await c.req.text()\n const params = new URLSearchParams(body)\n const grantType = params.get(\"grant_type\")\n const code = params.get(\"code\")\n const redirectUri = params.get(\"redirect_uri\")\n const codeVerifier = params.get(\"code_verifier\")\n const refreshTokenParam = params.get(\"refresh_token\")\n\n if (grantType === \"refresh_token\") {\n // Handle refresh token flow\n if (!refreshTokenParam) {\n return c.json(\n {\n error: \"invalid_request\",\n error_description: \"refresh_token is required for refresh_token grant type\",\n },\n 400,\n )\n }\n\n const tokenData = refreshTokens.get(refreshTokenParam)\n if (!tokenData) {\n return c.json(\n {\n error: \"invalid_grant\",\n error_description: \"Invalid or expired refresh token\",\n },\n 400,\n )\n }\n\n // Remove old refresh token (token rotation)\n refreshTokens.delete(refreshTokenParam)\n\n // Generate new JWT access token\n const accessTokenPayload = {\n sub: tokenData.userId,\n email: tokenData.email || \"\",\n scope: \"read write\",\n iat: Math.floor(Date.now() / 1000),\n exp: Math.floor(Date.now() / 1000) + JWT_EXPIRES_IN,\n iss: options.oauth?.issuer || `http://localhost:${options.port || 3000}`,\n aud: options.oauth?.resource || `${options.oauth?.issuer || `http://localhost:${options.port || 3000}`}/mcp`,\n }\n\n // Generate new refresh token\n const newRefreshToken = randomBytes(32).toString(\"hex\")\n refreshTokens.set(newRefreshToken, {\n createdAt: Date.now(),\n userId: tokenData.userId,\n email: tokenData.email,\n })\n\n const accessToken = jwt.sign(accessTokenPayload, JWT_SECRET)\n\n return c.json({\n access_token: accessToken,\n token_type: \"Bearer\",\n expires_in: JWT_EXPIRES_IN,\n scope: \"read write\",\n refresh_token: newRefreshToken,\n })\n }\n\n if (grantType !== \"authorization_code\") {\n return c.json(\n {\n error: \"unsupported_grant_type\",\n error_description: \"Only 'authorization_code' and 'refresh_token' grant types are supported\",\n },\n 400,\n )\n }\n\n const codeData = authorizationCodes.get(code || \"\")\n if (!codeData) {\n return c.json(\n {\n error: \"invalid_grant\",\n error_description: \"Invalid or expired authorization code\",\n },\n 400,\n )\n }\n\n // Validate redirect_uri matches\n if (codeData.redirectUri && codeData.redirectUri !== redirectUri) {\n return c.json(\n {\n error: \"invalid_grant\",\n error_description: \"redirect_uri mismatch\",\n },\n 400,\n )\n }\n\n // Validate PKCE if code_challenge was provided\n if (codeData.codeChallenge) {\n if (!codeVerifier) {\n return c.json(\n {\n error: \"invalid_grant\",\n error_description: \"code_verifier is required when code_challenge was used\",\n },\n 400,\n )\n }\n\n let expectedChallenge: string\n if (codeData.codeChallengeMethod === \"S256\") {\n expectedChallenge = createHash(\"sha256\").update(codeVerifier).digest().toString(\"base64url\")\n } else {\n // 'plain' method\n expectedChallenge = codeVerifier\n }\n\n if (expectedChallenge !== codeData.codeChallenge) {\n return c.json(\n {\n error: \"invalid_grant\",\n error_description: \"Invalid code_verifier\",\n },\n 400,\n )\n }\n }\n\n // Remove used code\n authorizationCodes.delete(code!)\n\n // Generate JWT access token\n const accessTokenPayload = {\n sub: codeData.userId,\n email: options.oauth?.email || \"\",\n scope: \"read write\",\n iat: Math.floor(Date.now() / 1000),\n exp: Math.floor(Date.now() / 1000) + JWT_EXPIRES_IN,\n iss: options.oauth?.issuer || `http://localhost:${options.port || 3000}`,\n aud: options.oauth?.resource || `${options.oauth?.issuer || `http://localhost:${options.port || 3000}`}/mcp`,\n }\n\n // Generate refresh token\n const refreshToken = randomBytes(32).toString(\"hex\")\n refreshTokens.set(refreshToken, {\n createdAt: Date.now(),\n userId: codeData.userId,\n email: options.oauth?.email,\n })\n\n const accessToken = jwt.sign(accessTokenPayload, JWT_SECRET)\n\n return c.json({\n access_token: accessToken,\n token_type: \"Bearer\",\n expires_in: JWT_EXPIRES_IN,\n scope: \"read write\",\n refresh_token: refreshToken,\n })\n })\n\n // JWKS Endpoint\n app.get(\"/oauth/jwks\", (c) => {\n return c.json({\n keys: [\n {\n kty: \"oct\", // Octet sequence for symmetric keys\n use: \"sig\",\n kid: \"duckpond-hmac-key\",\n alg: \"HS256\",\n },\n ],\n })\n })\n\n // Dynamic Client Registration\n app.post(\"/oauth/register\", async (c) => {\n try {\n let registrationRequest: OAuthClientRegistrationRequest = {}\n\n try {\n const body = await c.req.text()\n if (body && body !== \"[object Object]\") {\n try {\n registrationRequest = JSON.parse(body) as OAuthClientRegistrationRequest\n } catch {\n const formData = Object.fromEntries(new URLSearchParams(body))\n registrationRequest = formData as OAuthClientRegistrationRequest\n }\n }\n } catch (parseError) {\n log(\"Error parsing request body:\", parseError)\n }\n\n const clientId = `client-${randomBytes(8).toString(\"hex\")}`\n const clientSecret = randomBytes(16).toString(\"hex\")\n\n const response: OAuthClientRegistrationResponse = {\n client_id: clientId,\n client_secret: clientSecret,\n client_id_issued_at: Math.floor(Date.now() / 1000),\n client_secret_expires_at: 0, // Never expires\n grant_types: registrationRequest.grant_types || [\"authorization_code\"],\n response_types: registrationRequest.response_types || [\"code\"],\n redirect_uris: registrationRequest.redirect_uris || [],\n token_endpoint_auth_method: registrationRequest.token_endpoint_auth_method || \"client_secret_post\",\n }\n\n if (registrationRequest.client_name) {\n response.client_name = registrationRequest.client_name\n }\n if (registrationRequest.scope) {\n response.scope = registrationRequest.scope\n }\n\n return c.json(response, 201)\n } catch (error) {\n return c.json(\n {\n error: \"invalid_client_metadata\",\n error_description:\n \"Invalid client registration request: \" + (error instanceof Error ? error.message : String(error)),\n },\n 400,\n )\n }\n })\n\n log(\"✓ OAuth flow endpoints added\")\n}\n\nfunction setupUIEndpoints(\n app: ReturnType<FastMCP[\"getApp\"]>,\n duckpond: DuckPondServer,\n options: FastMCPServerOptions,\n): void {\n const baseUrl = options.oauth?.issuer || `http://localhost:${options.port || 3000}`\n const uiInternalPort = duckpond.getUIPort()\n\n // GET /ui - Info endpoint\n app.get(\"/ui\", (c) => {\n const currentUser = duckpond.getCurrentUIUser()\n const listResult = duckpond.listUsers()\n return c.json({\n message: currentUser\n ? `UI active for user: ${currentUser}. Access directly at http://localhost:${uiInternalPort}`\n : \"No UI active. Visit /ui/:userId to start DuckDB UI for a user.\",\n currentUser,\n uiUrl: currentUser ? `http://localhost:${uiInternalPort}` : null,\n availableUsers: listResult.success ? listResult.data.users : [],\n endpoints: {\n startUI: `${baseUrl}/ui/:userId`,\n },\n })\n })\n\n // GET /ui/:userId - Start UI for a specific user\n app.get(\"/ui/:userId\", async (c) => {\n const userId = c.req.param(\"userId\")\n\n log(`Starting UI for user: ${userId}`)\n const result = await duckpond.startUI(userId)\n\n if (!result.success) {\n return c.json(\n {\n error: \"Failed to start UI\",\n message: result.error.message,\n details: result.error.details,\n },\n 500,\n )\n }\n\n return c.json({\n success: true,\n message: `UI started for user: ${userId}`,\n uiUrl: `http://localhost:${uiInternalPort}`,\n hint: \"Access the DuckDB UI directly at the uiUrl above\",\n })\n })\n\n log(\"✓ UI endpoints added\")\n}\n\nexport type StartServerOptions = {\n options: FastMCPServerOptions\n transport: \"stdio\" | \"http\"\n /** Hook called after DuckPond init but before server start - use to register custom tools */\n beforeStart?: (ctx: { server: FastMCP; duckpond: DuckPondServer }) => void | Promise<void>\n}\n\n/**\n * Start the DuckPond MCP server with full configuration\n *\n * @example\n * ```typescript\n * import { startServer, getDefaultUserId } from \"duckpond-mcp-server/lib\"\n *\n * await startServer({\n * options: { config: { dataDir: \"~/data\" }, ui: { enabled: true, port: 4000, autoStartUser: \"claude\" } },\n * transport: \"stdio\",\n * beforeStart: async ({ server, duckpond }) => {\n * // Register custom tools before server starts\n * server.addTool({ name: \"my_tool\", ... })\n * }\n * })\n * ```\n */\nexport async function startServer(opts: StartServerOptions): Promise<void>\n/** @deprecated Use object form: startServer({ options, transport, beforeStart }) */\nexport async function startServer(options: FastMCPServerOptions, transport: \"stdio\" | \"http\"): Promise<void>\nexport async function startServer(\n optsOrOptions: StartServerOptions | FastMCPServerOptions,\n transportArg?: \"stdio\" | \"http\",\n): Promise<void> {\n // Handle both old and new signatures\n const isNewSignature = \"options\" in optsOrOptions && \"transport\" in optsOrOptions\n const options = isNewSignature ? optsOrOptions.options : optsOrOptions\n const transport = isNewSignature ? optsOrOptions.transport : transportArg!\n const beforeStart = isNewSignature ? optsOrOptions.beforeStart : undefined\n\n const { server, duckpond } = createFastMCPServer(options)\n\n // Initialize DuckPond\n const initResult = await duckpond.init()\n if (!initResult.success) {\n throw new Error(`Failed to initialize DuckPond: ${initResult.error.message}`)\n }\n\n log(\"DuckPond initialized successfully\")\n\n // Call beforeStart hook if provided (allows registering custom tools)\n if (beforeStart) {\n await beforeStart({ server, duckpond })\n }\n\n // Set UI internal port if configured\n if (options.ui?.internalPort) {\n duckpond.setUIPort(options.ui.internalPort)\n }\n\n // Start the server with appropriate transport\n if (transport === \"stdio\") {\n await server.start({\n transportType: \"stdio\",\n })\n log(\"✓ FastMCP server running with stdio transport\")\n\n // Start UI if enabled in stdio mode\n if (options.ui?.enabled) {\n if (options.ui.autoStartUser) {\n // Auto-start UI directly for default user (no management server needed)\n log(`Auto-starting UI for user: ${options.ui.autoStartUser}`)\n const uiResult = await duckpond.startUI(options.ui.autoStartUser)\n if (uiResult.success) {\n console.error(`🖥️ DuckDB UI running at http://localhost:${duckpond.getUIPort()}`)\n } else {\n log(`Failed to auto-start UI: ${uiResult.error.message}`)\n }\n } else {\n // No default user - start management server for manual user selection\n await startUIServer({\n port: options.ui.port,\n duckpond,\n })\n }\n }\n } else {\n await server.start({\n transportType: \"httpStream\",\n httpStream: {\n port: options.port || 3000,\n endpoint: (options.endpoint || \"/mcp\") as `/${string}`,\n },\n })\n log(`✓ FastMCP server running on http://0.0.0.0:${options.port || 3000}${options.endpoint || \"/mcp\"}`)\n log(\"🔌 Connect with StreamableHTTPClientTransport\")\n }\n\n // Handle cleanup on exit\n process.on(\"SIGINT\", async () => {\n log(\"Received SIGINT, closing server...\")\n await duckpond.close()\n process.exit(0)\n })\n\n process.on(\"SIGTERM\", async () => {\n log(\"Received SIGTERM, closing server...\")\n await duckpond.close()\n process.exit(0)\n })\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"server.js","names":[],"sources":["../src/server.ts"],"sourcesContent":["// Polyfill for Web Crypto API in Node.js environments\nimport { webcrypto } from \"crypto\"\n\nif (!globalThis.crypto) {\n globalThis.crypto = webcrypto as Crypto\n}\n\nimport { createHash, randomBytes } from \"crypto\"\nimport { FastMCP } from \"fastmcp\"\nimport { createRequire } from \"module\"\n\nconst require = createRequire(import.meta.url)\nconst packageJson = require(\"../package.json\") as { version: string }\nimport * as jwt from \"jsonwebtoken\"\nimport { URL } from \"url\"\nimport { z } from \"zod\"\n\nimport { DuckPondServer, type DuckPondServerConfig } from \"./server-core\"\nimport {\n detachUserSchema,\n executeSchema,\n getDefaultUserId,\n getUserStatsSchema,\n isAttachedSchema,\n listUsersSchema,\n querySchema,\n resolveUserId,\n} from \"./tools\"\nimport { startUIServer } from \"./ui-server\"\nimport { loggers } from \"./utils/logger\"\n\nconst log = loggers.fastmcp\n\nexport type OAuthConfig = {\n enabled: boolean\n username: string\n password: string\n userId: string\n email?: string\n issuer?: string\n resource?: string\n}\n\nexport type BearerTokenConfig = {\n token: string\n userId?: string\n}\n\nexport type FastMCPServerOptions = {\n config: DuckPondServerConfig\n port?: number\n endpoint?: string\n oauth?: OAuthConfig\n basicAuth?: {\n username: string\n password: string\n userId?: string\n email?: string\n }\n bearerToken?: BearerTokenConfig\n ui?: {\n enabled: boolean\n port: number\n internalPort?: number\n autoStartUser?: string\n }\n}\n\n// JWT secret for token signing/validation\nconst JWT_SECRET = process.env.DUCKPOND_JWT_SECRET || randomBytes(32).toString(\"hex\")\n\n// JWT token expiration configuration (default: 1 year)\nconst JWT_EXPIRES_IN = process.env.DUCKPOND_JWT_EXPIRES_IN\n ? parseInt(process.env.DUCKPOND_JWT_EXPIRES_IN, 10)\n : 365 * 24 * 60 * 60 // 1 year in seconds\n\n// In-memory stores for OAuth flow\nconst authorizationCodes = new Map<\n string,\n {\n createdAt: number\n redirectUri?: string\n codeChallenge?: string\n codeChallengeMethod?: string\n userId: string\n }\n>()\n\nconst refreshTokens = new Map<\n string,\n {\n createdAt: number\n userId: string\n email?: string\n }\n>()\n\n// AuthSession type for FastMCP authentication\ntype AuthSession = {\n userId: string\n email: string\n scope: string\n [key: string]: unknown // Allow additional properties\n}\n\ntype OAuthClientRegistrationRequest = {\n grant_types?: string[]\n response_types?: string[]\n redirect_uris?: string[]\n token_endpoint_auth_method?: string\n client_name?: string\n scope?: string\n}\n\ntype OAuthClientRegistrationResponse = {\n client_id: string\n client_secret: string\n client_id_issued_at: number\n client_secret_expires_at: number\n grant_types: string[]\n response_types: string[]\n redirect_uris: string[]\n token_endpoint_auth_method: string\n client_name?: string\n scope?: string\n}\n\nexport function createFastMCPServer(options: FastMCPServerOptions): {\n server: FastMCP\n duckpond: DuckPondServer\n} {\n log(\"🚀 Initializing FastMCP server...\")\n\n // Create DuckPond server instance\n const duckpond = new DuckPondServer(options.config)\n\n // Build server configuration\n const baseConfig = {\n name: \"duckpond\",\n version: packageJson.version as `${number}.${number}.${number}`,\n health: {\n enabled: true,\n path: \"/health\",\n status: 200,\n message: JSON.stringify({\n status: \"healthy\",\n service: \"duckpond-mcp-server\",\n version: packageJson.version,\n timestamp: new Date().toISOString(),\n }),\n },\n }\n\n // Create server with authentication (OAuth, Basic Auth, Bearer Token, or none)\n const server =\n options.oauth?.enabled || options.basicAuth || options.bearerToken\n ? new FastMCP<AuthSession>({\n ...baseConfig,\n oauth: {\n enabled: true,\n authorizationServer: {\n issuer: options.oauth?.issuer || `http://localhost:${options.port || 3000}`,\n authorizationEndpoint: `${options.oauth?.issuer || `http://localhost:${options.port || 3000}`}/oauth/authorize`,\n tokenEndpoint: `${options.oauth?.issuer || `http://localhost:${options.port || 3000}`}/oauth/token`,\n jwksUri: `${options.oauth?.issuer || `http://localhost:${options.port || 3000}`}/oauth/jwks`,\n registrationEndpoint: `${options.oauth?.issuer || `http://localhost:${options.port || 3000}`}/oauth/register`,\n responseTypesSupported: [\"code\"],\n grantTypesSupported: [\"authorization_code\"],\n tokenEndpointAuthMethodsSupported: [\"client_secret_post\", \"client_secret_basic\"],\n codeChallengeMethodsSupported: [\"S256\", \"plain\"],\n },\n protectedResource: {\n resource:\n process.env.DUCKPOND_OAUTH_RESOURCE ||\n options.oauth?.resource ||\n `${options.oauth?.issuer || `http://localhost:${options.port || 3000}`}/mcp`,\n authorizationServers: [options.oauth?.issuer || `http://localhost:${options.port || 3000}`],\n },\n },\n authenticate: (request) => {\n const authHeader = request.headers?.authorization\n const baseUrl = options.oauth?.issuer || `http://localhost:${options.port || 3000}`\n\n // For OAuth-enabled servers, require authentication\n if (!authHeader) {\n if (options.oauth?.enabled) {\n // Return HTTP 401 with WWW-Authenticate header for proper OAuth discovery\n throw new Response(\n JSON.stringify({\n error: \"unauthorized\",\n error_description: \"Authorization required. Please authenticate via OAuth.\",\n }),\n {\n status: 401,\n statusText: \"Unauthorized\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"WWW-Authenticate\": `Bearer realm=\"MCP\", authorization_uri=\"${baseUrl}/oauth/authorize\", resource=\"${baseUrl}/.well-known/oauth-protected-resource\"`,\n },\n },\n )\n }\n\n // For non-OAuth servers, also require some form of auth\n throw new Response(\n JSON.stringify({\n error: \"unauthorized\",\n error_description: \"Authorization required.\",\n }),\n {\n status: 401,\n statusText: \"Unauthorized\",\n headers: {\n \"Content-Type\": \"application/json\",\n },\n },\n )\n }\n\n // Handle Basic Authentication\n if (options.basicAuth && authHeader.startsWith(\"Basic \")) {\n const credentials = Buffer.from(authHeader.slice(6), \"base64\").toString(\"utf-8\")\n const [username, password] = credentials.split(\":\")\n\n if (username === options.basicAuth.username && password === options.basicAuth.password) {\n return Promise.resolve({\n userId: options.basicAuth.userId || username,\n email: options.basicAuth.email || `${username}@example.com`,\n scope: \"read write\",\n })\n } else {\n throw new Response(\n JSON.stringify({\n error: \"unauthorized\",\n error_description: \"Invalid username or password\",\n }),\n {\n status: 401,\n statusText: \"Unauthorized\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"WWW-Authenticate\": `Basic realm=\"MCP\"`,\n },\n },\n )\n }\n }\n\n // Handle static Bearer Token authentication\n if (options.bearerToken && authHeader.startsWith(\"Bearer \")) {\n const token = authHeader.slice(7)\n\n if (token === options.bearerToken.token) {\n return Promise.resolve({\n userId: options.bearerToken.userId || \"bearer-user\",\n email: \"\",\n scope: \"read write\",\n })\n }\n\n // If bearer token is configured but doesn't match, and OAuth is not enabled, reject\n if (!options.oauth?.enabled) {\n throw new Response(\n JSON.stringify({\n error: \"invalid_token\",\n error_description: \"Invalid bearer token\",\n }),\n {\n status: 401,\n statusText: \"Unauthorized\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"WWW-Authenticate\": `Bearer realm=\"MCP\", error=\"invalid_token\"`,\n },\n },\n )\n }\n\n // Fall through to OAuth JWT validation if OAuth is also enabled\n }\n\n // Handle Bearer Token (OAuth) - Validate JWT\n if (options.oauth?.enabled && authHeader.startsWith(\"Bearer \")) {\n const token = authHeader.slice(7) // Remove 'Bearer ' prefix\n\n try {\n // Verify JWT token\n const decoded = jwt.verify(token, JWT_SECRET) as jwt.JwtPayload\n\n if (!decoded.sub || !decoded.iat || !decoded.exp) {\n throw new Response(\n JSON.stringify({\n error: \"invalid_token\",\n error_description: \"Invalid token structure\",\n }),\n {\n status: 401,\n statusText: \"Unauthorized\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"WWW-Authenticate\": `Bearer realm=\"MCP\", error=\"invalid_token\", error_description=\"Invalid token structure\"`,\n },\n },\n )\n }\n\n // Validate audience\n const expectedAudience = options.oauth?.resource || `${baseUrl}/mcp`\n if (decoded.aud && decoded.aud !== expectedAudience) {\n throw new Response(\n JSON.stringify({\n error: \"invalid_token\",\n error_description: \"Token audience mismatch\",\n }),\n {\n status: 401,\n statusText: \"Unauthorized\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"WWW-Authenticate\": `Bearer realm=\"MCP\", error=\"invalid_token\", error_description=\"Token audience mismatch\"`,\n },\n },\n )\n }\n\n // Return user info from JWT claims\n return Promise.resolve({\n userId: decoded.sub,\n email: (decoded.email as string) || \"\",\n scope: (decoded.scope as string) || \"read write\",\n })\n } catch (error) {\n if (error instanceof Response) {\n throw error // Re-throw our custom Response errors\n }\n\n throw new Response(\n JSON.stringify({\n error: \"invalid_token\",\n error_description: \"Invalid or expired token\",\n }),\n {\n status: 401,\n statusText: \"Unauthorized\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"WWW-Authenticate\": `Bearer realm=\"MCP\", error=\"invalid_token\", error_description=\"Invalid or expired token\"`,\n },\n },\n )\n }\n }\n\n throw new Response(\n JSON.stringify({\n error: \"unauthorized\",\n error_description: \"Invalid authorization header format\",\n }),\n {\n status: 401,\n statusText: \"Unauthorized\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"WWW-Authenticate\": `Bearer realm=\"MCP\", authorization_uri=\"${baseUrl}/oauth/authorize\", resource=\"${baseUrl}/.well-known/oauth-protected-resource\"`,\n },\n },\n )\n },\n })\n : new FastMCP(baseConfig)\n\n // DuckDB type-safe JSON serializer (handles BigInt and timestamp objects)\n const duckDBReplacer = (_key: string, value: unknown): unknown => {\n if (typeof value === \"bigint\") {\n // Convert to number if safe, otherwise string\n return Number.isSafeInteger(Number(value)) ? Number(value) : value.toString()\n }\n // Handle DuckDB timestamp objects {micros: bigint}\n if (value && typeof value === \"object\" && \"micros\" in value) {\n const micros = (value as { micros: bigint }).micros\n const ms = typeof micros === \"bigint\" ? Number(micros / 1000n) : Number(micros) / 1000\n return new Date(ms).toISOString()\n }\n return value\n }\n\n // Add query tool\n server.addTool({\n name: \"query\",\n description: \"Execute a SQL query for a specific user and return results\",\n parameters: querySchema,\n execute: async (args) => {\n try {\n const userId = resolveUserId(args.userId)\n const result = await duckpond.query(userId, args.sql)\n\n if (!result.success) {\n return `ERROR: ${result.error.message}`\n }\n\n return JSON.stringify(\n {\n rows: result.data,\n rowCount: result.data.length,\n executionTime: result.executionTime,\n },\n duckDBReplacer,\n 2,\n )\n } catch (error) {\n log(\"Error in query tool:\", error)\n const errorMessage = error instanceof Error ? error.message : String(error)\n return `ERROR: ${JSON.stringify({ error: errorMessage }, null, 2)}`\n }\n },\n })\n\n // Add execute tool\n server.addTool({\n name: \"execute\",\n description: \"Execute SQL statement (DDL/DML) for a specific user without returning results\",\n parameters: executeSchema,\n execute: async (args) => {\n try {\n const userId = resolveUserId(args.userId)\n const result = await duckpond.execute(userId, args.sql)\n\n if (!result.success) {\n return `ERROR: ${result.error.message}`\n }\n\n return JSON.stringify(\n {\n success: true,\n message: \"Statement executed successfully\",\n executionTime: result.executionTime,\n },\n null,\n 2,\n )\n } catch (error) {\n log(\"Error in execute tool:\", error)\n const errorMessage = error instanceof Error ? error.message : String(error)\n return `ERROR: ${JSON.stringify({ error: errorMessage }, null, 2)}`\n }\n },\n })\n\n // Add getUserStats tool\n server.addTool({\n name: \"getUserStats\",\n description: \"Get statistics about a user's database (memory usage, query count, etc.)\",\n parameters: getUserStatsSchema,\n execute: async (args) => {\n try {\n const userId = resolveUserId(args.userId)\n const result = await duckpond.getUserStats(userId)\n\n if (!result.success) {\n return `ERROR: ${result.error.message}`\n }\n\n return JSON.stringify(\n {\n ...result.data,\n lastAccess: result.data.lastAccess.toISOString(),\n },\n null,\n 2,\n )\n } catch (error) {\n log(\"Error in getUserStats tool:\", error)\n const errorMessage = error instanceof Error ? error.message : String(error)\n return `ERROR: ${JSON.stringify({ error: errorMessage }, null, 2)}`\n }\n },\n })\n\n // Add isAttached tool\n server.addTool({\n name: \"isAttached\",\n description: \"Check if a user's database is currently cached in memory\",\n parameters: isAttachedSchema,\n execute: async (args) => {\n try {\n const userId = resolveUserId(args.userId)\n const result = duckpond.isAttached(userId)\n\n if (!result.success) {\n return `ERROR: ${result.error.message}`\n }\n\n return JSON.stringify(\n {\n attached: result.data,\n userId,\n },\n null,\n 2,\n )\n } catch (error) {\n log(\"Error in isAttached tool:\", error)\n const errorMessage = error instanceof Error ? error.message : String(error)\n return `ERROR: ${JSON.stringify({ error: errorMessage }, null, 2)}`\n }\n },\n })\n\n // Add detachUser tool\n server.addTool({\n name: \"detachUser\",\n description: \"Manually detach a user's database from the cache to free resources\",\n parameters: detachUserSchema,\n execute: async (args) => {\n try {\n const userId = resolveUserId(args.userId)\n const result = await duckpond.detachUser(userId)\n\n if (!result.success) {\n return `ERROR: ${result.error.message}`\n }\n\n return JSON.stringify(\n {\n success: true,\n message: `User ${userId} detached successfully`,\n },\n null,\n 2,\n )\n } catch (error) {\n log(\"Error in detachUser tool:\", error)\n const errorMessage = error instanceof Error ? error.message : String(error)\n return `ERROR: ${JSON.stringify({ error: errorMessage }, null, 2)}`\n }\n },\n })\n\n // Add listUsers tool\n server.addTool({\n name: \"listUsers\",\n description: \"List all currently cached users and cache statistics\",\n parameters: listUsersSchema,\n execute: async () => {\n try {\n const result = duckpond.listUsers()\n\n if (!result.success) {\n return `ERROR: ${result.error.message}`\n }\n\n return JSON.stringify(result.data, null, 2)\n } catch (error) {\n log(\"Error in listUsers tool:\", error)\n const errorMessage = error instanceof Error ? error.message : String(error)\n return `ERROR: ${JSON.stringify({ error: errorMessage }, null, 2)}`\n }\n },\n })\n\n // Add OAuth flow endpoints if OAuth is enabled\n if (options.oauth?.enabled) {\n setupOAuthEndpoints(server, options)\n }\n\n // Add root info endpoint using Hono\n const app = server.getApp()\n app.get(\"/\", (c) => {\n const baseUrl = options.oauth?.issuer || `http://localhost:${options.port || 3000}`\n\n const serverInfo = {\n name: \"DuckPond MCP Server\",\n version: packageJson.version,\n description: \"Model Context Protocol server for multi-tenant DuckDB with R2/S3 storage\",\n service: \"duckpond-mcp-server\",\n capabilities: {\n tools: [\"query\", \"execute\", \"getUserStats\", \"isAttached\", \"detachUser\", \"listUsers\"],\n transports: [\"stdio\", \"http\"],\n authentication: {\n oauth: options.oauth?.enabled || false,\n basicAuth: !!options.basicAuth,\n bearerToken: !!options.bearerToken,\n },\n },\n endpoints: {\n mcp: `${baseUrl}${options.endpoint || \"/mcp\"}`,\n health: `${baseUrl}/health`,\n ui: `${baseUrl}/ui/:userId`,\n ...(options.oauth?.enabled && {\n oauth: {\n authorization: `${baseUrl}/oauth/authorize`,\n token: `${baseUrl}/oauth/token`,\n jwks: `${baseUrl}/oauth/jwks`,\n register: `${baseUrl}/oauth/register`,\n },\n }),\n },\n timestamp: new Date().toISOString(),\n }\n\n return c.json(serverInfo)\n })\n\n // Add UI endpoints for DuckDB UI access\n setupUIEndpoints(app, duckpond, options)\n\n log(\"✓ FastMCP server created\")\n\n return { server, duckpond }\n}\n\nfunction setupOAuthEndpoints(server: FastMCP, options: FastMCPServerOptions): void {\n const app = server.getApp()\n\n // Clean up old codes and refresh tokens every minute\n setInterval(() => {\n const now = Date.now()\n // Clean authorization codes (10 minutes)\n for (const [code, data] of authorizationCodes.entries()) {\n if (now - data.createdAt > 600000) {\n authorizationCodes.delete(code)\n }\n }\n // Clean refresh tokens (30 days)\n for (const [token, data] of refreshTokens.entries()) {\n if (now - data.createdAt > 2592000000) {\n refreshTokens.delete(token)\n }\n }\n }, 60000)\n\n // OAuth Authorization Endpoint - Login Form\n app.get(\"/oauth/authorize\", (c) => {\n const params = c.req.query()\n const responseType = params.response_type\n const redirectUri = params.redirect_uri\n const state = params.state\n const codeChallenge = params.code_challenge\n const codeChallengeMethod = params.code_challenge_method\n const clientId = params.client_id\n\n if (responseType !== \"code\") {\n return c.json(\n {\n error: \"unsupported_response_type\",\n error_description: \"Only 'code' response type is supported\",\n },\n 400,\n )\n }\n\n if (!redirectUri) {\n return c.json(\n {\n error: \"invalid_request\",\n error_description: \"redirect_uri is required\",\n },\n 400,\n )\n }\n\n // Validate PKCE parameters if present\n if (codeChallenge) {\n if (!codeChallengeMethod || ![\"S256\", \"plain\"].includes(codeChallengeMethod)) {\n return c.json(\n {\n error: \"invalid_request\",\n error_description: \"Invalid code_challenge_method. Only 'S256' and 'plain' are supported\",\n },\n 400,\n )\n }\n }\n\n // Serve login form\n const loginForm = `\n<!DOCTYPE html>\n<html>\n<head>\n <title>OAuth Login - DuckPond MCP Server</title>\n <style>\n body { font-family: Arial, sans-serif; max-width: 400px; margin: 100px auto; padding: 20px; }\n .form-group { margin-bottom: 15px; }\n label { display: block; margin-bottom: 5px; font-weight: bold; }\n input[type=\"text\"], input[type=\"password\"] { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; }\n button { width: 100%; padding: 12px; background: #007cba; color: white; border: none; border-radius: 4px; font-size: 16px; cursor: pointer; }\n button:hover { background: #005a87; }\n .app-info { background: #f5f5f5; padding: 15px; border-radius: 4px; margin-bottom: 20px; }\n </style>\n</head>\n<body>\n <div class=\"app-info\">\n <h3>🔐 OAuth Authorization</h3>\n <p><strong>Application:</strong> ${clientId || \"MCP Client\"}</p>\n <p><strong>Permissions:</strong> Read and write access to DuckDB databases</p>\n </div>\n\n <form method=\"POST\" action=\"/oauth/authorize\">\n <input type=\"hidden\" name=\"response_type\" value=\"${responseType}\">\n <input type=\"hidden\" name=\"redirect_uri\" value=\"${redirectUri}\">\n <input type=\"hidden\" name=\"state\" value=\"${state || \"\"}\">\n <input type=\"hidden\" name=\"code_challenge\" value=\"${codeChallenge || \"\"}\">\n <input type=\"hidden\" name=\"code_challenge_method\" value=\"${codeChallengeMethod || \"\"}\">\n <input type=\"hidden\" name=\"client_id\" value=\"${clientId || \"\"}\">\n\n <div class=\"form-group\">\n <label for=\"username\">Username:</label>\n <input type=\"text\" id=\"username\" name=\"username\" required>\n </div>\n\n <div class=\"form-group\">\n <label for=\"password\">Password:</label>\n <input type=\"password\" id=\"password\" name=\"password\" required>\n </div>\n\n <button type=\"submit\">Authorize Application</button>\n </form>\n</body>\n</html>`\n\n return c.html(loginForm)\n })\n\n // OAuth Authorization POST - Process Login\n app.post(\"/oauth/authorize\", async (c) => {\n try {\n const body = await c.req.text()\n const params = new URLSearchParams(body)\n\n const username = params.get(\"username\")\n const password = params.get(\"password\")\n const redirectUri = params.get(\"redirect_uri\")\n const state = params.get(\"state\")\n const codeChallenge = params.get(\"code_challenge\")\n const codeChallengeMethod = params.get(\"code_challenge_method\")\n\n // Validate credentials\n if (username !== options.oauth?.username || password !== options.oauth?.password) {\n const errorForm = `\n<!DOCTYPE html>\n<html><head><title>Login Failed</title><style>body{font-family:Arial;max-width:400px;margin:100px auto;padding:20px;}.error{color:red;background:#fee;padding:10px;border-radius:4px;margin-bottom:15px;}</style></head>\n<body><div class=\"error\">❌ Invalid username or password</div><a href=\"javascript:history.back()\">← Try Again</a></body></html>`\n return c.html(errorForm, 401)\n }\n\n // Generate authorization code\n const code = randomBytes(16).toString(\"hex\")\n authorizationCodes.set(code, {\n createdAt: Date.now(),\n redirectUri: redirectUri || \"\",\n codeChallenge: codeChallenge || undefined,\n codeChallengeMethod: codeChallengeMethod || undefined,\n userId: options.oauth?.userId || username || \"oauth-user\",\n })\n\n // Redirect with authorization code\n const redirectUrl = new URL(redirectUri || \"\")\n redirectUrl.searchParams.set(\"code\", code)\n if (state) {\n redirectUrl.searchParams.set(\"state\", state)\n }\n\n return c.redirect(redirectUrl.toString(), 302)\n } catch {\n return c.json(\n {\n error: \"invalid_request\",\n error_description: \"Failed to process authorization request\",\n },\n 400,\n )\n }\n })\n\n // OAuth Token Endpoint\n app.post(\"/oauth/token\", async (c) => {\n const body = await c.req.text()\n const params = new URLSearchParams(body)\n const grantType = params.get(\"grant_type\")\n const code = params.get(\"code\")\n const redirectUri = params.get(\"redirect_uri\")\n const codeVerifier = params.get(\"code_verifier\")\n const refreshTokenParam = params.get(\"refresh_token\")\n\n if (grantType === \"refresh_token\") {\n // Handle refresh token flow\n if (!refreshTokenParam) {\n return c.json(\n {\n error: \"invalid_request\",\n error_description: \"refresh_token is required for refresh_token grant type\",\n },\n 400,\n )\n }\n\n const tokenData = refreshTokens.get(refreshTokenParam)\n if (!tokenData) {\n return c.json(\n {\n error: \"invalid_grant\",\n error_description: \"Invalid or expired refresh token\",\n },\n 400,\n )\n }\n\n // Remove old refresh token (token rotation)\n refreshTokens.delete(refreshTokenParam)\n\n // Generate new JWT access token\n const accessTokenPayload = {\n sub: tokenData.userId,\n email: tokenData.email || \"\",\n scope: \"read write\",\n iat: Math.floor(Date.now() / 1000),\n exp: Math.floor(Date.now() / 1000) + JWT_EXPIRES_IN,\n iss: options.oauth?.issuer || `http://localhost:${options.port || 3000}`,\n aud: options.oauth?.resource || `${options.oauth?.issuer || `http://localhost:${options.port || 3000}`}/mcp`,\n }\n\n // Generate new refresh token\n const newRefreshToken = randomBytes(32).toString(\"hex\")\n refreshTokens.set(newRefreshToken, {\n createdAt: Date.now(),\n userId: tokenData.userId,\n email: tokenData.email,\n })\n\n const accessToken = jwt.sign(accessTokenPayload, JWT_SECRET)\n\n return c.json({\n access_token: accessToken,\n token_type: \"Bearer\",\n expires_in: JWT_EXPIRES_IN,\n scope: \"read write\",\n refresh_token: newRefreshToken,\n })\n }\n\n if (grantType !== \"authorization_code\") {\n return c.json(\n {\n error: \"unsupported_grant_type\",\n error_description: \"Only 'authorization_code' and 'refresh_token' grant types are supported\",\n },\n 400,\n )\n }\n\n const codeData = authorizationCodes.get(code || \"\")\n if (!codeData) {\n return c.json(\n {\n error: \"invalid_grant\",\n error_description: \"Invalid or expired authorization code\",\n },\n 400,\n )\n }\n\n // Validate redirect_uri matches\n if (codeData.redirectUri && codeData.redirectUri !== redirectUri) {\n return c.json(\n {\n error: \"invalid_grant\",\n error_description: \"redirect_uri mismatch\",\n },\n 400,\n )\n }\n\n // Validate PKCE if code_challenge was provided\n if (codeData.codeChallenge) {\n if (!codeVerifier) {\n return c.json(\n {\n error: \"invalid_grant\",\n error_description: \"code_verifier is required when code_challenge was used\",\n },\n 400,\n )\n }\n\n let expectedChallenge: string\n if (codeData.codeChallengeMethod === \"S256\") {\n expectedChallenge = createHash(\"sha256\").update(codeVerifier).digest().toString(\"base64url\")\n } else {\n // 'plain' method\n expectedChallenge = codeVerifier\n }\n\n if (expectedChallenge !== codeData.codeChallenge) {\n return c.json(\n {\n error: \"invalid_grant\",\n error_description: \"Invalid code_verifier\",\n },\n 400,\n )\n }\n }\n\n // Remove used code\n authorizationCodes.delete(code!)\n\n // Generate JWT access token\n const accessTokenPayload = {\n sub: codeData.userId,\n email: options.oauth?.email || \"\",\n scope: \"read write\",\n iat: Math.floor(Date.now() / 1000),\n exp: Math.floor(Date.now() / 1000) + JWT_EXPIRES_IN,\n iss: options.oauth?.issuer || `http://localhost:${options.port || 3000}`,\n aud: options.oauth?.resource || `${options.oauth?.issuer || `http://localhost:${options.port || 3000}`}/mcp`,\n }\n\n // Generate refresh token\n const refreshToken = randomBytes(32).toString(\"hex\")\n refreshTokens.set(refreshToken, {\n createdAt: Date.now(),\n userId: codeData.userId,\n email: options.oauth?.email,\n })\n\n const accessToken = jwt.sign(accessTokenPayload, JWT_SECRET)\n\n return c.json({\n access_token: accessToken,\n token_type: \"Bearer\",\n expires_in: JWT_EXPIRES_IN,\n scope: \"read write\",\n refresh_token: refreshToken,\n })\n })\n\n // JWKS Endpoint\n app.get(\"/oauth/jwks\", (c) => {\n return c.json({\n keys: [\n {\n kty: \"oct\", // Octet sequence for symmetric keys\n use: \"sig\",\n kid: \"duckpond-hmac-key\",\n alg: \"HS256\",\n },\n ],\n })\n })\n\n // Dynamic Client Registration\n app.post(\"/oauth/register\", async (c) => {\n try {\n let registrationRequest: OAuthClientRegistrationRequest = {}\n\n try {\n const body = await c.req.text()\n if (body && body !== \"[object Object]\") {\n try {\n registrationRequest = JSON.parse(body) as OAuthClientRegistrationRequest\n } catch {\n const formData = Object.fromEntries(new URLSearchParams(body))\n registrationRequest = formData as OAuthClientRegistrationRequest\n }\n }\n } catch (parseError) {\n log(\"Error parsing request body:\", parseError)\n }\n\n const clientId = `client-${randomBytes(8).toString(\"hex\")}`\n const clientSecret = randomBytes(16).toString(\"hex\")\n\n const response: OAuthClientRegistrationResponse = {\n client_id: clientId,\n client_secret: clientSecret,\n client_id_issued_at: Math.floor(Date.now() / 1000),\n client_secret_expires_at: 0, // Never expires\n grant_types: registrationRequest.grant_types || [\"authorization_code\"],\n response_types: registrationRequest.response_types || [\"code\"],\n redirect_uris: registrationRequest.redirect_uris || [],\n token_endpoint_auth_method: registrationRequest.token_endpoint_auth_method || \"client_secret_post\",\n }\n\n if (registrationRequest.client_name) {\n response.client_name = registrationRequest.client_name\n }\n if (registrationRequest.scope) {\n response.scope = registrationRequest.scope\n }\n\n return c.json(response, 201)\n } catch (error) {\n return c.json(\n {\n error: \"invalid_client_metadata\",\n error_description:\n \"Invalid client registration request: \" + (error instanceof Error ? error.message : String(error)),\n },\n 400,\n )\n }\n })\n\n log(\"✓ OAuth flow endpoints added\")\n}\n\nfunction setupUIEndpoints(\n app: ReturnType<FastMCP[\"getApp\"]>,\n duckpond: DuckPondServer,\n options: FastMCPServerOptions,\n): void {\n const baseUrl = options.oauth?.issuer || `http://localhost:${options.port || 3000}`\n const uiInternalPort = duckpond.getUIPort()\n\n // GET /ui - Info endpoint\n app.get(\"/ui\", (c) => {\n const currentUser = duckpond.getCurrentUIUser()\n const listResult = duckpond.listUsers()\n return c.json({\n message: currentUser\n ? `UI active for user: ${currentUser}. Access directly at http://localhost:${uiInternalPort}`\n : \"No UI active. Visit /ui/:userId to start DuckDB UI for a user.\",\n currentUser,\n uiUrl: currentUser ? `http://localhost:${uiInternalPort}` : null,\n availableUsers: listResult.success ? listResult.data.users : [],\n endpoints: {\n startUI: `${baseUrl}/ui/:userId`,\n },\n })\n })\n\n // GET /ui/:userId - Start UI for a specific user\n app.get(\"/ui/:userId\", async (c) => {\n const userId = c.req.param(\"userId\")\n\n log(`Starting UI for user: ${userId}`)\n const result = await duckpond.startUI(userId)\n\n if (!result.success) {\n return c.json(\n {\n error: \"Failed to start UI\",\n message: result.error.message,\n details: result.error.details,\n },\n 500,\n )\n }\n\n return c.json({\n success: true,\n message: `UI started for user: ${userId}`,\n uiUrl: `http://localhost:${uiInternalPort}`,\n hint: \"Access the DuckDB UI directly at the uiUrl above\",\n })\n })\n\n log(\"✓ UI endpoints added\")\n}\n\nexport type StartServerOptions = {\n options: FastMCPServerOptions\n transport: \"stdio\" | \"http\"\n /** Hook called after DuckPond init but before server start - use to register custom tools */\n beforeStart?: (ctx: { server: FastMCP; duckpond: DuckPondServer }) => void | Promise<void>\n}\n\n/**\n * Start the DuckPond MCP server with full configuration\n *\n * @example\n * ```typescript\n * import { startServer, getDefaultUserId } from \"duckpond-mcp-server/lib\"\n *\n * await startServer({\n * options: { config: { dataDir: \"~/data\" }, ui: { enabled: true, port: 4000, autoStartUser: \"claude\" } },\n * transport: \"stdio\",\n * beforeStart: async ({ server, duckpond }) => {\n * // Register custom tools before server starts\n * server.addTool({ name: \"my_tool\", ... })\n * }\n * })\n * ```\n */\nexport async function startServer(opts: StartServerOptions): Promise<void>\n/** @deprecated Use object form: startServer({ options, transport, beforeStart }) */\nexport async function startServer(options: FastMCPServerOptions, transport: \"stdio\" | \"http\"): Promise<void>\nexport async function startServer(\n optsOrOptions: StartServerOptions | FastMCPServerOptions,\n transportArg?: \"stdio\" | \"http\",\n): Promise<void> {\n // Handle both old and new signatures\n const isNewSignature = \"options\" in optsOrOptions && \"transport\" in optsOrOptions\n const options = isNewSignature ? optsOrOptions.options : optsOrOptions\n const transport = isNewSignature ? optsOrOptions.transport : transportArg!\n const beforeStart = isNewSignature ? optsOrOptions.beforeStart : undefined\n\n const { server, duckpond } = createFastMCPServer(options)\n\n // Initialize DuckPond\n const initResult = await duckpond.init()\n if (!initResult.success) {\n throw new Error(`Failed to initialize DuckPond: ${initResult.error.message}`)\n }\n\n log(\"DuckPond initialized successfully\")\n\n // Call beforeStart hook if provided (allows registering custom tools)\n if (beforeStart) {\n await beforeStart({ server, duckpond })\n }\n\n // Set UI internal port if configured\n if (options.ui?.internalPort) {\n duckpond.setUIPort(options.ui.internalPort)\n }\n\n // Start the server with appropriate transport\n if (transport === \"stdio\") {\n await server.start({\n transportType: \"stdio\",\n })\n log(\"✓ FastMCP server running with stdio transport\")\n\n // Start UI if enabled in stdio mode\n if (options.ui?.enabled) {\n if (options.ui.autoStartUser) {\n // Auto-start UI directly for default user (no management server needed)\n log(`Auto-starting UI for user: ${options.ui.autoStartUser}`)\n const uiResult = await duckpond.startUI(options.ui.autoStartUser)\n if (uiResult.success) {\n console.error(`🖥️ DuckDB UI running at http://localhost:${duckpond.getUIPort()}`)\n } else {\n log(`Failed to auto-start UI: ${uiResult.error.message}`)\n }\n } else {\n // No default user - start management server for manual user selection\n await startUIServer({\n port: options.ui.port,\n duckpond,\n })\n }\n }\n } else {\n await server.start({\n transportType: \"httpStream\",\n httpStream: {\n port: options.port || 3000,\n endpoint: (options.endpoint || \"/mcp\") as `/${string}`,\n },\n })\n log(`✓ FastMCP server running on http://0.0.0.0:${options.port || 3000}${options.endpoint || \"/mcp\"}`)\n log(\"🔌 Connect with StreamableHTTPClientTransport\")\n }\n\n // Handle cleanup on exit\n process.on(\"SIGINT\", async () => {\n log(\"Received SIGINT, closing server...\")\n await duckpond.close()\n process.exit(0)\n })\n\n process.on(\"SIGTERM\", async () => {\n log(\"Received SIGTERM, closing server...\")\n await duckpond.close()\n process.exit(0)\n })\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/tools/index.ts"],"sourcesContent":["import { z } from \"zod\"\n\nimport type { DuckPondServer } from \"../server-core\"\nimport { loggers } from \"../utils/logger\"\n\nconst log = loggers.tools\n\n/**\n * Get the default user ID from environment variable\n */\nexport function getDefaultUserId(): string | undefined {\n return process.env.DUCKPOND_DEFAULT_USER\n}\n\n/**\n * Resolve user ID from input or default\n * @throws Error if no user ID is available\n */\nexport function resolveUserId(inputUserId?: string): string {\n const userId = inputUserId || getDefaultUserId()\n if (!userId) {\n throw new Error(\n \"userId is required. Either provide it in the request or set DUCKPOND_DEFAULT_USER environment variable.\",\n )\n }\n return userId\n}\n\n/**\n * Zod schema for query tool\n */\nexport const querySchema = z.object({\n userId: z.string().min(1).optional().describe(\"User identifier (optional if DUCKPOND_DEFAULT_USER is set)\"),\n sql: z.string().min(1).describe(\"SQL query to execute\"),\n})\n\n/**\n * Zod schema for execute tool\n */\nexport const executeSchema = z.object({\n userId: z.string().min(1).optional().describe(\"User identifier (optional if DUCKPOND_DEFAULT_USER is set)\"),\n sql: z.string().min(1).describe(\"SQL statement to execute (DDL/DML)\"),\n})\n\n/**\n * Zod schema for getUserStats tool\n */\nexport const getUserStatsSchema = z.object({\n userId: z.string().min(1).optional().describe(\"User identifier (optional if DUCKPOND_DEFAULT_USER is set)\"),\n})\n\n/**\n * Zod schema for isAttached tool\n */\nexport const isAttachedSchema = z.object({\n userId: z.string().min(1).optional().describe(\"User identifier (optional if DUCKPOND_DEFAULT_USER is set)\"),\n})\n\n/**\n * Zod schema for detachUser tool\n */\nexport const detachUserSchema = z.object({\n userId: z.string().min(1).optional().describe(\"User identifier (optional if DUCKPOND_DEFAULT_USER is set)\"),\n})\n\n/**\n * Zod schema for listUsers tool\n */\nexport const listUsersSchema = z.object({})\n\n/**\n * Tool implementations for MCP server\n */\nexport const tools = {\n /**\n * Execute a SQL query for a user\n */\n async query(server: DuckPondServer, input: z.infer<typeof querySchema>) {\n const userId = resolveUserId(input.userId)\n log(`Tool: query for user ${userId}`)\n const result = await server.query(userId, input.sql)\n\n if (!result.success) {\n throw new Error(result.error.message)\n }\n\n return {\n content: [\n {\n type: \"text\" as const,\n text: JSON.stringify(\n {\n rows: result.data,\n rowCount: result.data.length,\n executionTime: result.executionTime,\n },\n null,\n 2,\n ),\n },\n ],\n }\n },\n\n /**\n * Execute DDL/DML statement\n */\n async execute(server: DuckPondServer, input: z.infer<typeof executeSchema>) {\n const userId = resolveUserId(input.userId)\n log(`Tool: execute for user ${userId}`)\n const result = await server.execute(userId, input.sql)\n\n if (!result.success) {\n throw new Error(result.error.message)\n }\n\n return {\n content: [\n {\n type: \"text\" as const,\n text: JSON.stringify(\n {\n success: true,\n message: \"Statement executed successfully\",\n executionTime: result.executionTime,\n },\n null,\n 2,\n ),\n },\n ],\n }\n },\n\n /**\n * Get user database statistics\n */\n async getUserStats(server: DuckPondServer, input: z.infer<typeof getUserStatsSchema>) {\n const userId = resolveUserId(input.userId)\n log(`Tool: getUserStats for user ${userId}`)\n const result = await server.getUserStats(userId)\n\n if (!result.success) {\n throw new Error(result.error.message)\n }\n\n return {\n content: [\n {\n type: \"text\" as const,\n text: JSON.stringify(\n {\n ...result.data,\n lastAccess: result.data.lastAccess.toISOString(),\n },\n null,\n 2,\n ),\n },\n ],\n }\n },\n\n /**\n * Check if user is attached\n */\n isAttached(server: DuckPondServer, input: z.infer<typeof isAttachedSchema>) {\n const userId = resolveUserId(input.userId)\n log(`Tool: isAttached for user ${userId}`)\n const result = server.isAttached(userId)\n\n if (!result.success) {\n throw new Error(result.error.message)\n }\n\n return {\n content: [\n {\n type: \"text\" as const,\n text: JSON.stringify(\n {\n attached: result.data,\n userId,\n },\n null,\n 2,\n ),\n },\n ],\n }\n },\n\n /**\n * Detach a user from cache\n */\n async detachUser(server: DuckPondServer, input: z.infer<typeof detachUserSchema>) {\n const userId = resolveUserId(input.userId)\n log(`Tool: detachUser for user ${userId}`)\n const result = await server.detachUser(userId)\n\n if (!result.success) {\n throw new Error(result.error.message)\n }\n\n return {\n content: [\n {\n type: \"text\" as const,\n text: JSON.stringify(\n {\n success: true,\n message: `User ${userId} detached successfully`,\n },\n null,\n 2,\n ),\n },\n ],\n }\n },\n\n /**\n * List all currently cached users\n */\n listUsers(server: DuckPondServer, _input: z.infer<typeof listUsersSchema>) {\n log(\"Tool: listUsers\")\n const result = server.listUsers()\n\n if (!result.success) {\n throw new Error(result.error.message)\n }\n\n return {\n content: [\n {\n type: \"text\" as const,\n text: JSON.stringify(result.data, null, 2),\n },\n ],\n }\n },\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/tools/index.ts"],"sourcesContent":["import { z } from \"zod\"\n\nimport type { DuckPondServer } from \"../server-core\"\nimport { loggers } from \"../utils/logger\"\n\nconst log = loggers.tools\n\n/**\n * Get the default user ID from environment variable\n */\nexport function getDefaultUserId(): string | undefined {\n return process.env.DUCKPOND_DEFAULT_USER\n}\n\n/**\n * Resolve user ID from input or default\n * @throws Error if no user ID is available\n */\nexport function resolveUserId(inputUserId?: string): string {\n const userId = inputUserId || getDefaultUserId()\n if (!userId) {\n throw new Error(\n \"userId is required. Either provide it in the request or set DUCKPOND_DEFAULT_USER environment variable.\",\n )\n }\n return userId\n}\n\n/**\n * Zod schema for query tool\n */\nexport const querySchema = z.object({\n userId: z.string().min(1).optional().describe(\"User identifier (optional if DUCKPOND_DEFAULT_USER is set)\"),\n sql: z.string().min(1).describe(\"SQL query to execute\"),\n})\n\n/**\n * Zod schema for execute tool\n */\nexport const executeSchema = z.object({\n userId: z.string().min(1).optional().describe(\"User identifier (optional if DUCKPOND_DEFAULT_USER is set)\"),\n sql: z.string().min(1).describe(\"SQL statement to execute (DDL/DML)\"),\n})\n\n/**\n * Zod schema for getUserStats tool\n */\nexport const getUserStatsSchema = z.object({\n userId: z.string().min(1).optional().describe(\"User identifier (optional if DUCKPOND_DEFAULT_USER is set)\"),\n})\n\n/**\n * Zod schema for isAttached tool\n */\nexport const isAttachedSchema = z.object({\n userId: z.string().min(1).optional().describe(\"User identifier (optional if DUCKPOND_DEFAULT_USER is set)\"),\n})\n\n/**\n * Zod schema for detachUser tool\n */\nexport const detachUserSchema = z.object({\n userId: z.string().min(1).optional().describe(\"User identifier (optional if DUCKPOND_DEFAULT_USER is set)\"),\n})\n\n/**\n * Zod schema for listUsers tool\n */\nexport const listUsersSchema = z.object({})\n\n/**\n * Tool implementations for MCP server\n */\nexport const tools = {\n /**\n * Execute a SQL query for a user\n */\n async query(server: DuckPondServer, input: z.infer<typeof querySchema>) {\n const userId = resolveUserId(input.userId)\n log(`Tool: query for user ${userId}`)\n const result = await server.query(userId, input.sql)\n\n if (!result.success) {\n throw new Error(result.error.message)\n }\n\n return {\n content: [\n {\n type: \"text\" as const,\n text: JSON.stringify(\n {\n rows: result.data,\n rowCount: result.data.length,\n executionTime: result.executionTime,\n },\n null,\n 2,\n ),\n },\n ],\n }\n },\n\n /**\n * Execute DDL/DML statement\n */\n async execute(server: DuckPondServer, input: z.infer<typeof executeSchema>) {\n const userId = resolveUserId(input.userId)\n log(`Tool: execute for user ${userId}`)\n const result = await server.execute(userId, input.sql)\n\n if (!result.success) {\n throw new Error(result.error.message)\n }\n\n return {\n content: [\n {\n type: \"text\" as const,\n text: JSON.stringify(\n {\n success: true,\n message: \"Statement executed successfully\",\n executionTime: result.executionTime,\n },\n null,\n 2,\n ),\n },\n ],\n }\n },\n\n /**\n * Get user database statistics\n */\n async getUserStats(server: DuckPondServer, input: z.infer<typeof getUserStatsSchema>) {\n const userId = resolveUserId(input.userId)\n log(`Tool: getUserStats for user ${userId}`)\n const result = await server.getUserStats(userId)\n\n if (!result.success) {\n throw new Error(result.error.message)\n }\n\n return {\n content: [\n {\n type: \"text\" as const,\n text: JSON.stringify(\n {\n ...result.data,\n lastAccess: result.data.lastAccess.toISOString(),\n },\n null,\n 2,\n ),\n },\n ],\n }\n },\n\n /**\n * Check if user is attached\n */\n isAttached(server: DuckPondServer, input: z.infer<typeof isAttachedSchema>) {\n const userId = resolveUserId(input.userId)\n log(`Tool: isAttached for user ${userId}`)\n const result = server.isAttached(userId)\n\n if (!result.success) {\n throw new Error(result.error.message)\n }\n\n return {\n content: [\n {\n type: \"text\" as const,\n text: JSON.stringify(\n {\n attached: result.data,\n userId,\n },\n null,\n 2,\n ),\n },\n ],\n }\n },\n\n /**\n * Detach a user from cache\n */\n async detachUser(server: DuckPondServer, input: z.infer<typeof detachUserSchema>) {\n const userId = resolveUserId(input.userId)\n log(`Tool: detachUser for user ${userId}`)\n const result = await server.detachUser(userId)\n\n if (!result.success) {\n throw new Error(result.error.message)\n }\n\n return {\n content: [\n {\n type: \"text\" as const,\n text: JSON.stringify(\n {\n success: true,\n message: `User ${userId} detached successfully`,\n },\n null,\n 2,\n ),\n },\n ],\n }\n },\n\n /**\n * List all currently cached users\n */\n listUsers(server: DuckPondServer, _input: z.infer<typeof listUsersSchema>) {\n log(\"Tool: listUsers\")\n const result = server.listUsers()\n\n if (!result.success) {\n throw new Error(result.error.message)\n }\n\n return {\n content: [\n {\n type: \"text\" as const,\n text: JSON.stringify(result.data, null, 2),\n },\n ],\n }\n },\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"ui-server.js","names":[],"sources":["../src/ui-server.ts"],"sourcesContent":["import { serve } from \"@hono/node-server\"\nimport { Hono } from \"hono\"\n\nimport type { DuckPondServer } from \"./server-core\"\nimport { loggers } from \"./utils/logger\"\n\nconst log = loggers.core\n\nexport type UIServerOptions = {\n port: number\n duckpond: DuckPondServer\n}\n\n/**\n * Start a lightweight HTTP server for UI access in stdio mode\n * This allows starting DuckDB UI even when MCP is running over stdio\n */\nexport async function startUIServer(options: UIServerOptions): Promise<void> {\n const { port, duckpond } = options\n const uiInternalPort = duckpond.getUIPort()\n\n const app = new Hono()\n\n // GET /ui - Info endpoint\n app.get(\"/ui\", (c) => {\n const currentUser = duckpond.getCurrentUIUser()\n const listResult = duckpond.listUsers()\n return c.json({\n message: currentUser\n ? `UI active for user: ${currentUser}. Access directly at http://localhost:${uiInternalPort}`\n : \"No UI active. Visit /ui/:userId to start DuckDB UI for a user.\",\n currentUser,\n uiUrl: currentUser ? `http://localhost:${uiInternalPort}` : null,\n availableUsers: listResult.success ? listResult.data.users : [],\n })\n })\n\n // GET /ui/:userId - Start UI for a specific user\n app.get(\"/ui/:userId\", async (c) => {\n const userId = c.req.param(\"userId\")\n\n log(`[UI Server] Starting UI for user: ${userId}`)\n const result = await duckpond.startUI(userId)\n\n if (!result.success) {\n return c.json(\n {\n error: \"Failed to start UI\",\n message: result.error.message,\n details: result.error.details,\n },\n 500,\n )\n }\n\n return c.json({\n success: true,\n message: `UI started for user: ${userId}`,\n uiUrl: `http://localhost:${uiInternalPort}`,\n hint: \"Access the DuckDB UI directly at the uiUrl above\",\n })\n })\n\n // Root endpoint with info\n app.get(\"/\", (c) => {\n const currentUser = duckpond.getCurrentUIUser()\n return c.json({\n name: \"DuckPond UI Server\",\n description: \"Lightweight HTTP server for DuckDB UI access in stdio mode\",\n currentUIUser: currentUser,\n uiUrl: currentUser ? `http://localhost:${uiInternalPort}` : null,\n endpoints: {\n startUI: `http://localhost:${port}/ui/:userId`,\n status: `http://localhost:${port}/ui`,\n },\n })\n })\n\n // Start the server\n serve({\n fetch: app.fetch,\n port,\n })\n\n log(`✓ UI server running at http://localhost:${port}/ui`)\n log(` Visit http://localhost:${port}/ui/:userId to start DuckDB UI`)\n log(` Then access DuckDB UI directly at http://localhost:${uiInternalPort}`)\n}\n"],"mappings":"kHAMA,MAAM,EAAM,EAAQ,KAWpB,eAAsB,EAAc,EAAyC,CAC3E,GAAM,CAAE,OAAM,YAAa,EACrB,EAAiB,EAAS,WAAW,CAErC,EAAM,IAAI,EAGhB,EAAI,IAAI,MAAQ,GAAM,CACpB,IAAM,EAAc,EAAS,kBAAkB,CACzC,EAAa,EAAS,WAAW,CACvC,OAAO,EAAE,KAAK,CACZ,QAAS,EACL,uBAAuB,EAAY,wCAAwC,IAC3E,iEACJ,cACA,MAAO,EAAc,oBAAoB,IAAmB,KAC5D,eAAgB,EAAW,QAAU,EAAW,KAAK,MAAQ,EAAE,CAChE,CAAC,EACF,CAGF,EAAI,IAAI,cAAe,KAAO,IAAM,CAClC,IAAM,EAAS,EAAE,IAAI,MAAM,SAAS,CAEpC,EAAI,qCAAqC,IAAS,CAClD,IAAM,EAAS,MAAM,EAAS,QAAQ,EAAO,CAa7C,OAXK,EAAO,QAWL,EAAE,KAAK,CACZ,QAAS,GACT,QAAS,wBAAwB,IACjC,MAAO,oBAAoB,IAC3B,KAAM,mDACP,CAAC,CAfO,EAAE,KACP,CACE,MAAO,qBACP,QAAS,EAAO,MAAM,QACtB,QAAS,EAAO,MAAM,QACvB,CACD,IACD,EASH,CAGF,EAAI,IAAI,IAAM,GAAM,CAClB,IAAM,EAAc,EAAS,kBAAkB,CAC/C,OAAO,EAAE,KAAK,CACZ,KAAM,qBACN,YAAa,6DACb,cAAe,EACf,MAAO,EAAc,oBAAoB,IAAmB,KAC5D,UAAW,CACT,QAAS,oBAAoB,EAAK,aAClC,OAAQ,oBAAoB,EAAK,KAClC,CACF,CAAC,EACF,CAGF,EAAM,CACJ,MAAO,EAAI,MACX,OACD,CAAC,CAEF,EAAI,2CAA2C,EAAK,KAAK,CACzD,EAAI,4BAA4B,EAAK,gCAAgC,CACrE,EAAI,wDAAwD,IAAiB"}
1
+ {"version":3,"file":"ui-server.js","names":[],"sources":["../src/ui-server.ts"],"sourcesContent":["import { serve } from \"@hono/node-server\"\nimport { Hono } from \"hono\"\n\nimport type { DuckPondServer } from \"./server-core\"\nimport { loggers } from \"./utils/logger\"\n\nconst log = loggers.core\n\nexport type UIServerOptions = {\n port: number\n duckpond: DuckPondServer\n}\n\n/**\n * Start a lightweight HTTP server for UI access in stdio mode\n * This allows starting DuckDB UI even when MCP is running over stdio\n */\nexport async function startUIServer(options: UIServerOptions): Promise<void> {\n const { port, duckpond } = options\n const uiInternalPort = duckpond.getUIPort()\n\n const app = new Hono()\n\n // GET /ui - Info endpoint\n app.get(\"/ui\", (c) => {\n const currentUser = duckpond.getCurrentUIUser()\n const listResult = duckpond.listUsers()\n return c.json({\n message: currentUser\n ? `UI active for user: ${currentUser}. Access directly at http://localhost:${uiInternalPort}`\n : \"No UI active. Visit /ui/:userId to start DuckDB UI for a user.\",\n currentUser,\n uiUrl: currentUser ? `http://localhost:${uiInternalPort}` : null,\n availableUsers: listResult.success ? listResult.data.users : [],\n })\n })\n\n // GET /ui/:userId - Start UI for a specific user\n app.get(\"/ui/:userId\", async (c) => {\n const userId = c.req.param(\"userId\")\n\n log(`[UI Server] Starting UI for user: ${userId}`)\n const result = await duckpond.startUI(userId)\n\n if (!result.success) {\n return c.json(\n {\n error: \"Failed to start UI\",\n message: result.error.message,\n details: result.error.details,\n },\n 500,\n )\n }\n\n return c.json({\n success: true,\n message: `UI started for user: ${userId}`,\n uiUrl: `http://localhost:${uiInternalPort}`,\n hint: \"Access the DuckDB UI directly at the uiUrl above\",\n })\n })\n\n // Root endpoint with info\n app.get(\"/\", (c) => {\n const currentUser = duckpond.getCurrentUIUser()\n return c.json({\n name: \"DuckPond UI Server\",\n description: \"Lightweight HTTP server for DuckDB UI access in stdio mode\",\n currentUIUser: currentUser,\n uiUrl: currentUser ? `http://localhost:${uiInternalPort}` : null,\n endpoints: {\n startUI: `http://localhost:${port}/ui/:userId`,\n status: `http://localhost:${port}/ui`,\n },\n })\n })\n\n // Start the server\n serve({\n fetch: app.fetch,\n port,\n })\n\n log(`✓ UI server running at http://localhost:${port}/ui`)\n log(` Visit http://localhost:${port}/ui/:userId to start DuckDB UI`)\n log(` Then access DuckDB UI directly at http://localhost:${uiInternalPort}`)\n}\n"],"mappings":"kHAMA,MAAM,EAAM,EAAQ,KAWpB,eAAsB,EAAc,EAAyC,CAC3E,GAAM,CAAE,OAAM,YAAa,EACrB,EAAiB,EAAS,UAAU,EAEpC,EAAM,IAAI,EAGhB,EAAI,IAAI,MAAQ,GAAM,CACpB,IAAM,EAAc,EAAS,iBAAiB,EACxC,EAAa,EAAS,UAAU,EACtC,OAAO,EAAE,KAAK,CACZ,QAAS,EACL,uBAAuB,EAAY,wCAAwC,IAC3E,iEACJ,cACA,MAAO,EAAc,oBAAoB,IAAmB,KAC5D,eAAgB,EAAW,QAAU,EAAW,KAAK,MAAQ,CAAC,CAChE,CAAC,CACH,CAAC,EAGD,EAAI,IAAI,cAAe,KAAO,IAAM,CAClC,IAAM,EAAS,EAAE,IAAI,MAAM,QAAQ,EAEnC,EAAI,qCAAqC,GAAQ,EACjD,IAAM,EAAS,MAAM,EAAS,QAAQ,CAAM,EAa5C,OAXK,EAAO,QAWL,EAAE,KAAK,CACZ,QAAS,GACT,QAAS,wBAAwB,IACjC,MAAO,oBAAoB,IAC3B,KAAM,kDACR,CAAC,EAfQ,EAAE,KACP,CACE,MAAO,qBACP,QAAS,EAAO,MAAM,QACtB,QAAS,EAAO,MAAM,OACxB,EACA,GACF,CASJ,CAAC,EAGD,EAAI,IAAI,IAAM,GAAM,CAClB,IAAM,EAAc,EAAS,iBAAiB,EAC9C,OAAO,EAAE,KAAK,CACZ,KAAM,qBACN,YAAa,6DACb,cAAe,EACf,MAAO,EAAc,oBAAoB,IAAmB,KAC5D,UAAW,CACT,QAAS,oBAAoB,EAAK,aAClC,OAAQ,oBAAoB,EAAK,IACnC,CACF,CAAC,CACH,CAAC,EAGD,EAAM,CACJ,MAAO,EAAI,MACX,MACF,CAAC,EAED,EAAI,2CAA2C,EAAK,IAAI,EACxD,EAAI,4BAA4B,EAAK,+BAA+B,EACpE,EAAI,wDAAwD,GAAgB,CAC9E"}
@@ -1 +1 @@
1
- {"version":3,"file":"logger.js","names":[],"sources":["../../src/utils/logger.ts"],"sourcesContent":["import debug from \"debug\"\n\n/**\n * Debug loggers for different modules\n */\nexport const loggers = {\n core: debug(\"duckpond-mcp:core\"),\n stdio: debug(\"duckpond-mcp:stdio\"),\n fastmcp: debug(\"duckpond-mcp:fastmcp\"),\n tools: debug(\"duckpond-mcp:tools\"),\n main: debug(\"duckpond-mcp:main\"),\n}\n\n/**\n * Create a custom logger\n */\nexport function createLogger(namespace: string) {\n return debug(`duckpond-mcp:${namespace}`)\n}\n"],"mappings":"qBAKA,MAAa,EAAU,CACrB,KAAM,EAAM,oBAAoB,CAChC,MAAO,EAAM,qBAAqB,CAClC,QAAS,EAAM,uBAAuB,CACtC,MAAO,EAAM,qBAAqB,CAClC,KAAM,EAAM,oBAAoB,CACjC,CAKD,SAAgB,EAAa,EAAmB,CAC9C,OAAO,EAAM,gBAAgB,IAAY"}
1
+ {"version":3,"file":"logger.js","names":[],"sources":["../../src/utils/logger.ts"],"sourcesContent":["import debug from \"debug\"\n\n/**\n * Debug loggers for different modules\n */\nexport const loggers = {\n core: debug(\"duckpond-mcp:core\"),\n stdio: debug(\"duckpond-mcp:stdio\"),\n fastmcp: debug(\"duckpond-mcp:fastmcp\"),\n tools: debug(\"duckpond-mcp:tools\"),\n main: debug(\"duckpond-mcp:main\"),\n}\n\n/**\n * Create a custom logger\n */\nexport function createLogger(namespace: string) {\n return debug(`duckpond-mcp:${namespace}`)\n}\n"],"mappings":"qBAKA,MAAa,EAAU,CACrB,KAAM,EAAM,mBAAmB,EAC/B,MAAO,EAAM,oBAAoB,EACjC,QAAS,EAAM,sBAAsB,EACrC,MAAO,EAAM,oBAAoB,EACjC,KAAM,EAAM,mBAAmB,CACjC,EAKA,SAAgB,EAAa,EAAmB,CAC9C,OAAO,EAAM,gBAAgB,GAAW,CAC1C"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "duckpond-mcp-server",
3
- "version": "0.4.4",
3
+ "version": "0.6.0",
4
4
  "description": "MCP server for multi-tenant DuckDB management with R2/S3 storage",
5
5
  "keywords": [
6
6
  "mcp",
@@ -16,50 +16,36 @@
16
16
  ],
17
17
  "author": "jordan.burke@gmail.com",
18
18
  "license": "MIT",
19
- "homepage": "https://github.com/jordanburke/duckpond-mcp-server",
19
+ "homepage": "https://github.com/jordanburke/duckpond/tree/main/packages/duckpond-mcp-server",
20
20
  "repository": {
21
21
  "type": "git",
22
- "url": "https://github.com/jordanburke/duckpond-mcp-server"
22
+ "url": "https://github.com/jordanburke/duckpond",
23
+ "directory": "packages/duckpond-mcp-server"
23
24
  },
24
25
  "type": "module",
25
26
  "bin": {
26
27
  "duckpond-mcp-server": "./dist/index.js"
27
28
  },
28
- "scripts": {
29
- "validate": "ts-builds validate",
30
- "format": "ts-builds format",
31
- "format:check": "ts-builds format:check",
32
- "lint": "ts-builds lint",
33
- "lint:check": "ts-builds lint:check",
34
- "typecheck": "ts-builds typecheck",
35
- "test": "ts-builds test",
36
- "test:watch": "ts-builds test:watch",
37
- "test:coverage": "ts-builds test:coverage",
38
- "build": "ts-builds build",
39
- "dev": "ts-builds dev",
40
- "serve:test": "tsx src/index.ts",
41
- "serve:test:http": "tsx src/index.ts --transport http",
42
- "prepublishOnly": "pnpm validate"
43
- },
44
29
  "dependencies": {
45
- "@hono/node-server": "^1.19.9",
46
- "fastmcp": "3.33.0",
30
+ "@hono/node-server": "^1.19.14",
47
31
  "@types/express": "^5.0.6",
48
32
  "@types/jsonwebtoken": "^9.0.10",
49
- "commander": "^14.0.3",
33
+ "commander": "^15.0.0",
50
34
  "debug": "^4.4.3",
51
- "duckpond": "^0.4.1",
52
35
  "express": "^5.2.1",
53
- "hono": "^4.11.9",
36
+ "fastmcp": "4.0.2",
37
+ "functype-os": "^1.3.1",
38
+ "hono": "^4.12.23",
54
39
  "jsonwebtoken": "^9.0.3",
55
- "zod": "^4.3.6"
40
+ "zod": "^4.4.3",
41
+ "duckpond": "^0.6.0"
56
42
  },
57
43
  "devDependencies": {
58
- "@types/node": "~24.10.13",
59
- "ts-builds": "^2.4.0",
60
- "tsdown": "^0.20.3",
61
- "tsx": "^4.21.0",
62
- "typescript": "^5.9.3"
44
+ "@types/node": "~24.10.15",
45
+ "ts-builds": "^3.0.1",
46
+ "tsdown": "^0.22.2",
47
+ "tsx": "^4.22.4",
48
+ "typescript": "^6.0.3"
63
49
  },
64
50
  "main": "./dist/index.js",
65
51
  "module": "./dist/index.js",
@@ -79,5 +65,19 @@
79
65
  "dist"
80
66
  ],
81
67
  "prettier": "ts-builds/prettier",
82
- "packageManager": "pnpm@10.29.3+sha512.498e1fb4cca5aa06c1dcf2611e6fafc50972ffe7189998c409e90de74566444298ffe43e6cd2acdc775ba1aa7cc5e092a8b7054c811ba8c5770f84693d33d2dc"
83
- }
68
+ "scripts": {
69
+ "validate": "ts-builds validate",
70
+ "format": "ts-builds format",
71
+ "format:check": "ts-builds format:check",
72
+ "lint": "ts-builds lint",
73
+ "lint:check": "ts-builds lint:check",
74
+ "typecheck": "ts-builds typecheck",
75
+ "test": "ts-builds test",
76
+ "test:watch": "ts-builds test:watch",
77
+ "test:coverage": "ts-builds test:coverage",
78
+ "build": "ts-builds build",
79
+ "dev": "ts-builds dev",
80
+ "serve:test": "tsx src/index.ts",
81
+ "serve:test:http": "tsx src/index.ts --transport http"
82
+ }
83
+ }
@@ -1 +0,0 @@
1
- function e(t){"@babel/helpers - typeof";return e=typeof Symbol==`function`&&typeof Symbol.iterator==`symbol`?function(e){return typeof e}:function(e){return e&&typeof Symbol==`function`&&e.constructor===Symbol&&e!==Symbol.prototype?`symbol`:typeof e},e(t)}function t(t,n){if(e(t)!=`object`||!t)return t;var r=t[Symbol.toPrimitive];if(r!==void 0){var i=r.call(t,n||`default`);if(e(i)!=`object`)return i;throw TypeError(`@@toPrimitive must return a primitive value.`)}return(n===`string`?String:Number)(t)}function n(n){var r=t(n,`string`);return e(r)==`symbol`?r:r+``}function r(e,t,r){return(t=n(t))in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}export{r as t};
@@ -1 +0,0 @@
1
- {"version":3,"file":"server-core.js","names":[],"sources":["../src/server-core.ts"],"sourcesContent":["import type { Either } from \"duckpond\"\nimport { DuckPond, type DuckPondConfig, type ErrorCode, type ListUsersResult, type UserStats } from \"duckpond\"\n\nimport { loggers } from \"./utils/logger\"\n\nconst log = loggers.core\n\n/**\n * Result type for MCP tool responses\n */\nexport type MCPResult<T> =\n | {\n success: true\n data: T\n executionTime?: number\n }\n | {\n success: false\n error: {\n code: string\n message: string\n details?: {\n originalCode?: ErrorCode\n context?: Record<string, unknown>\n cause?: string\n }\n }\n }\n\n/**\n * Configuration for DuckPond MCP Server\n * Extends DuckPondConfig with server-specific options\n */\nexport type DuckPondServerConfig = DuckPondConfig & {\n /** Directory for persistent database storage (default: ~/.duckpond/data) */\n dataDir?: string\n}\n\n/**\n * Core DuckPond MCP Server\n *\n * Wraps DuckPond library with MCP-compatible result types\n */\nexport class DuckPondServer {\n private pond: DuckPond | null = null\n private initialized = false\n private currentUIUserId: string | null = null\n private uiInternalPort: number = 4213\n private uiKeepaliveTimer: ReturnType<typeof setInterval> | null = null\n private readonly UI_KEEPALIVE_INTERVAL = 30000 // 30 seconds\n\n constructor(private config: DuckPondServerConfig) {\n log(\"DuckPondServer created\")\n }\n\n /**\n * Initialize the DuckPond instance\n */\n async init(): Promise<MCPResult<void>> {\n if (this.initialized) {\n log(\"Already initialized\")\n return { success: true, data: undefined }\n }\n\n log(\"Initializing DuckPond...\")\n const startTime = Date.now()\n\n this.pond = new DuckPond(this.config)\n const result = await this.pond.init()\n\n return this.handleEither(result, Date.now() - startTime)\n }\n\n /**\n * Execute a SQL query for a user\n */\n async query<T = unknown>(userId: string, sql: string): Promise<MCPResult<T[]>> {\n if (!this.pond) {\n return this.notInitializedError()\n }\n\n log(`Query for user ${userId}: ${sql.substring(0, 100)}...`)\n const startTime = Date.now()\n\n const result = await this.pond.query<T>(userId, sql)\n\n return this.handleEither(result, Date.now() - startTime)\n }\n\n /**\n * Execute DDL/DML without returning results\n */\n async execute(userId: string, sql: string): Promise<MCPResult<void>> {\n if (!this.pond) {\n return this.notInitializedError()\n }\n\n log(`Execute for user ${userId}: ${sql.substring(0, 100)}...`)\n const startTime = Date.now()\n\n const result = await this.pond.execute(userId, sql)\n\n return this.handleEither(result, Date.now() - startTime)\n }\n\n /**\n * Get statistics about a user's database\n */\n async getUserStats(userId: string): Promise<MCPResult<UserStats>> {\n if (!this.pond) {\n return this.notInitializedError()\n }\n\n log(`Getting stats for user ${userId}`)\n const startTime = Date.now()\n\n const result = await this.pond.getUserStats(userId)\n\n return this.handleEither(result, Date.now() - startTime)\n }\n\n /**\n * Check if a user is currently cached\n */\n isAttached(userId: string): MCPResult<boolean> {\n if (!this.pond) {\n return this.notInitializedError()\n }\n\n const attached = this.pond.isAttached(userId)\n log(`User ${userId} attached: ${attached}`)\n\n return {\n success: true,\n data: attached,\n }\n }\n\n /**\n * Manually detach a user from the cache\n */\n async detachUser(userId: string): Promise<MCPResult<void>> {\n if (!this.pond) {\n return this.notInitializedError()\n }\n\n log(`Detaching user ${userId}`)\n const startTime = Date.now()\n\n const result = await this.pond.detachUser(userId)\n\n return this.handleEither(result, Date.now() - startTime)\n }\n\n /**\n * List all currently cached users\n */\n listUsers(): MCPResult<{ users: string[]; count: number; maxActiveUsers: number; utilizationPercent: number }> {\n if (!this.pond) {\n return this.notInitializedError()\n }\n\n log(\"Listing cached users\")\n const result = this.pond.listUsers()\n\n return {\n success: true,\n data: {\n users: result.users.toArray(), // Convert List<string> to string[]\n count: result.count,\n maxActiveUsers: result.maxActiveUsers,\n utilizationPercent: result.utilizationPercent,\n },\n }\n }\n\n /**\n * Close the DuckPond instance\n */\n async close(): Promise<MCPResult<void>> {\n if (!this.pond) {\n return { success: true, data: undefined }\n }\n\n log(\"Closing DuckPond...\")\n const startTime = Date.now()\n\n // Stop UI server if running\n if (this.currentUIUserId) {\n await this.stopUI()\n }\n\n const result = await this.pond.close()\n this.initialized = false\n this.pond = null\n\n return this.handleEither(result, Date.now() - startTime)\n }\n\n /**\n * Start DuckDB UI for a specific user\n * Only one user's UI can be active at a time\n */\n async startUI(userId: string): Promise<MCPResult<{ port: number }>> {\n if (!this.pond) {\n return this.notInitializedError()\n }\n\n log(`Starting UI for user ${userId}`)\n const startTime = Date.now()\n\n // Stop existing UI if different user\n if (this.currentUIUserId && this.currentUIUserId !== userId) {\n log(`Stopping UI for previous user ${this.currentUIUserId}`)\n await this.stopUI()\n }\n\n // If already running for this user, just return success\n if (this.currentUIUserId === userId) {\n log(`UI already running for user ${userId}`)\n return {\n success: true,\n data: { port: this.uiInternalPort },\n executionTime: Date.now() - startTime,\n }\n }\n\n // Install and load UI extension\n const installResult = await this.execute(userId, \"INSTALL ui; LOAD ui;\")\n if (!installResult.success) {\n log(`Failed to install UI extension: ${installResult.error.message}`)\n return installResult as MCPResult<{ port: number }>\n }\n\n // Set the UI port\n const portResult = await this.execute(userId, `SET ui_local_port = ${this.uiInternalPort}`)\n if (!portResult.success) {\n log(`Failed to set UI port: ${portResult.error.message}`)\n return portResult as MCPResult<{ port: number }>\n }\n\n // Start the UI server (without opening browser)\n const startResult = await this.execute(userId, \"CALL start_ui_server()\")\n if (!startResult.success) {\n log(`Failed to start UI server: ${startResult.error.message}`)\n return startResult as MCPResult<{ port: number }>\n }\n\n this.currentUIUserId = userId\n this.startUIKeepalive(userId)\n log(`UI started for user ${userId} on port ${this.uiInternalPort}`)\n\n return {\n success: true,\n data: { port: this.uiInternalPort },\n executionTime: Date.now() - startTime,\n }\n }\n\n /**\n * Start keepalive timer to prevent UI user from being evicted\n */\n private startUIKeepalive(userId: string): void {\n this.stopUIKeepalive()\n log(`Starting UI keepalive for user ${userId}`)\n this.uiKeepaliveTimer = setInterval(async () => {\n if (this.pond && this.currentUIUserId === userId) {\n log(`UI keepalive ping for user ${userId}`)\n await this.pond.query(userId, \"SELECT 1\")\n }\n }, this.UI_KEEPALIVE_INTERVAL)\n }\n\n /**\n * Stop the keepalive timer\n */\n private stopUIKeepalive(): void {\n if (this.uiKeepaliveTimer) {\n log(\"Stopping UI keepalive\")\n clearInterval(this.uiKeepaliveTimer)\n this.uiKeepaliveTimer = null\n }\n }\n\n /**\n * Stop the currently running DuckDB UI\n */\n async stopUI(): Promise<MCPResult<void>> {\n this.stopUIKeepalive()\n\n if (!this.currentUIUserId) {\n log(\"No UI running to stop\")\n return { success: true, data: undefined }\n }\n\n if (!this.pond) {\n this.currentUIUserId = null\n return { success: true, data: undefined }\n }\n\n log(`Stopping UI for user ${this.currentUIUserId}`)\n const startTime = Date.now()\n\n const result = await this.execute(this.currentUIUserId, \"CALL stop_ui_server()\")\n const previousUser = this.currentUIUserId\n this.currentUIUserId = null\n\n if (result.success) {\n log(`UI stopped for user ${previousUser}`)\n return {\n success: true,\n data: undefined,\n executionTime: Date.now() - startTime,\n }\n }\n\n log(`Failed to stop UI: ${result.error.message}`)\n return result\n }\n\n /**\n * Get the user ID for the currently running UI\n */\n getCurrentUIUser(): string | null {\n return this.currentUIUserId\n }\n\n /**\n * Set the internal port for DuckDB UI\n */\n setUIPort(port: number): void {\n this.uiInternalPort = port\n }\n\n /**\n * Get the internal port for DuckDB UI\n */\n getUIPort(): number {\n return this.uiInternalPort\n }\n\n /**\n * Convert Either<Error, T> to MCPResult<T>\n */\n private handleEither<T>(\n result: Either<{ code: ErrorCode; message: string; cause?: Error; context?: Record<string, unknown> }, T>,\n executionTime: number,\n ): MCPResult<T> {\n return result.fold(\n (error) => ({\n success: false as const,\n error: {\n code: this.mapErrorCode(error.code),\n message: error.message,\n details: {\n originalCode: error.code,\n context: error.context,\n cause: error.cause?.message,\n },\n },\n }),\n (data) => ({\n success: true as const,\n data,\n executionTime,\n }),\n )\n }\n\n /**\n * Map DuckPond ErrorCode to MCP error code\n */\n private mapErrorCode(code: ErrorCode): string {\n const mapping: Record<ErrorCode, string> = {\n CONNECTION_FAILED: \"SERVICE_UNAVAILABLE\",\n CONNECTION_TIMEOUT: \"TIMEOUT\",\n R2_CONNECTION_ERROR: \"SERVICE_UNAVAILABLE\",\n S3_CONNECTION_ERROR: \"SERVICE_UNAVAILABLE\",\n USER_NOT_FOUND: \"NOT_FOUND\",\n USER_ALREADY_EXISTS: \"ALREADY_EXISTS\",\n USER_NOT_ATTACHED: \"NOT_FOUND\",\n QUERY_EXECUTION_ERROR: \"INVALID_REQUEST\",\n QUERY_TIMEOUT: \"TIMEOUT\",\n INVALID_SQL: \"INVALID_REQUEST\",\n MEMORY_LIMIT_EXCEEDED: \"RESOURCE_EXHAUSTED\",\n STORAGE_ERROR: \"SERVICE_UNAVAILABLE\",\n STORAGE_QUOTA_EXCEEDED: \"RESOURCE_EXHAUSTED\",\n INVALID_CONFIG: \"INVALID_ARGUMENT\",\n NOT_INITIALIZED: \"FAILED_PRECONDITION\",\n UNKNOWN_ERROR: \"INTERNAL_ERROR\",\n }\n\n return mapping[code] || \"INTERNAL_ERROR\"\n }\n\n /**\n * Helper for not initialized error\n */\n private notInitializedError<T>(): MCPResult<T> {\n return {\n success: false,\n error: {\n code: \"FAILED_PRECONDITION\",\n message: \"DuckPond not initialized. Call init() first.\",\n },\n }\n }\n}\n"],"mappings":"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"}