@pikku/bun-server 0.12.2 → 0.12.4

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/CHANGELOG.md CHANGED
@@ -1,5 +1,39 @@
1
1
  # @pikku/bun-server
2
2
 
3
+ ## 0.12.4
4
+
5
+ ### Patch Changes
6
+
7
+ - d4a2503: Serve the console same-origin at /console (#861). Both dev servers gain
8
+ `staticMounts` (prefix → directory static serving with SPA fallback and path
9
+ traversal protection); `pikku serve` / `pikku dev` mount the bundled console
10
+ app at `/console` on the API port whenever it is bundled, so auth cookies are
11
+ first-party and no `?server=` param is needed. The console is built with
12
+ `base: '/console/'` (its router already derives the basename from BASE_URL).
13
+ The separate `--console <port>` static server is removed; `pikku console`
14
+ serves the bundle under /console and redirects the root there.
15
+ - Updated dependencies [61c9ce9]
16
+ - Updated dependencies [f1f39f8]
17
+ - Updated dependencies [c45e98d]
18
+ - Updated dependencies [472a349]
19
+ - @pikku/core@0.12.52
20
+
21
+ ## 0.12.3
22
+
23
+ ### Patch Changes
24
+
25
+ - 6f6abfe: Mount MCP on the bun runtime. `@pikku/bun-server` now accepts `mcpJson`/`mcpPath`
26
+ options and serves the MCP endpoint (default `/mcp`) via a new fetch-native
27
+ handler on `PikkuMCPServer.createFetchHandler()`, which uses the MCP SDK's
28
+ Web-Standard (`Request`→`Response`) streamable-HTTP transport — no `node:http`
29
+ req/res. The standalone `--runtime bun` entry now wires the same `mcpImport` +
30
+ `mcpJson` option the node entry already used, so a bun standalone build serves
31
+ `/mcp` with the project's tools/resources/prompts instead of silently dropping
32
+ them. `@pikku/modelcontextprotocol` is an optional peer dep of `@pikku/bun-server`
33
+ (only imported when `mcpJson` is non-empty).
34
+ - Updated dependencies [6f6abfe]
35
+ - @pikku/modelcontextprotocol@0.12.6
36
+
3
37
  ## 0.12.2
4
38
 
5
39
  ### Patch Changes
@@ -2,13 +2,25 @@ import type { CoreConfig } from '@pikku/core';
2
2
  import type { Logger } from '@pikku/core/services';
3
3
  import { type RunHTTPWiringOptions } from '@pikku/core/http';
4
4
  import { BunEventHubService } from './bun-event-hub-service.js';
5
+ export type StaticMount = {
6
+ urlPrefix: string;
7
+ directory: string;
8
+ spaFallback?: boolean;
9
+ };
5
10
  export type BunServerConfig = CoreConfig & {
6
11
  port: number;
7
12
  hostname?: string;
13
+ staticMounts?: StaticMount[];
8
14
  healthCheckPath?: string;
9
15
  };
10
16
  export type PikkuBunServerOptions = RunHTTPWiringOptions & {
11
17
  eventHub?: BunEventHubService;
18
+ mcpJson?: {
19
+ tools?: unknown[];
20
+ resources?: unknown[];
21
+ prompts?: unknown[];
22
+ };
23
+ mcpPath?: string;
12
24
  };
13
25
  export declare class PikkuBunServer {
14
26
  private readonly config;
@@ -16,9 +28,16 @@ export declare class PikkuBunServer {
16
28
  private server;
17
29
  private readonly eventHub;
18
30
  private readonly options;
31
+ private readonly mcpJson?;
32
+ private readonly mcpPath;
33
+ private mcpHandler?;
19
34
  constructor(config: BunServerConfig, logger: Logger, options?: PikkuBunServerOptions);
20
35
  init(): Promise<void>;
36
+ private initMCP;
21
37
  start(): Promise<void>;
38
+ get port(): number;
39
+ private serveStaticMounts;
40
+ private resolveStaticFile;
22
41
  stop(): Promise<void>;
23
42
  enableExitOnSignals(): void;
24
43
  }
@@ -3,6 +3,7 @@ import { fetchData, PikkuFetchHTTPRequest, PikkuFetchHTTPResponse, logRoutes as
3
3
  import { logChannels } from '@pikku/core/channel';
4
4
  import { runLocalChannel } from '@pikku/core/channel/local';
5
5
  import { compileAllSchemas } from '@pikku/core/schema';
6
+ import { resolve } from 'node:path';
6
7
  import { BunEventHubService } from './bun-event-hub-service.js';
7
8
  const isSerializable = (data) => !(typeof data === 'string' ||
8
9
  data instanceof ArrayBuffer ||
@@ -20,20 +21,54 @@ export class PikkuBunServer {
20
21
  server = null;
21
22
  eventHub;
22
23
  options;
24
+ mcpJson;
25
+ mcpPath;
26
+ mcpHandler;
23
27
  constructor(config, logger, options = {}) {
24
28
  this.config = config;
25
29
  this.logger = logger;
26
- const { eventHub, ...httpOptions } = options;
30
+ const { eventHub, mcpJson, mcpPath, ...httpOptions } = options;
27
31
  this.eventHub = eventHub ?? new BunEventHubService();
32
+ this.mcpJson = mcpJson;
33
+ this.mcpPath = mcpPath ?? '/mcp';
28
34
  this.options = httpOptions;
29
35
  }
30
36
  async init() {
31
37
  compileAllSchemas(this.logger);
32
38
  logRegisterRoutes(this.logger);
33
39
  logChannels(this.logger);
40
+ await this.initMCP();
41
+ }
42
+ async initMCP() {
43
+ const mcpJson = this.mcpJson;
44
+ if (!mcpJson)
45
+ return;
46
+ const { tools = [], resources = [], prompts = [] } = mcpJson;
47
+ if (tools.length + resources.length + prompts.length === 0)
48
+ return;
49
+ try {
50
+ const { PikkuMCPServer } = await import('@pikku/modelcontextprotocol');
51
+ const mcpServer = new PikkuMCPServer({
52
+ name: 'pikku',
53
+ version: '1.0.0',
54
+ mcpJSON: mcpJson,
55
+ capabilities: {
56
+ ...(tools.length > 0 && { tools: {} }),
57
+ ...(resources.length > 0 && { resources: {} }),
58
+ ...(prompts.length > 0 && { prompts: {} }),
59
+ },
60
+ }, this.logger);
61
+ await mcpServer.init();
62
+ const { handler } = mcpServer.createFetchHandler({ path: this.mcpPath });
63
+ this.mcpHandler = handler;
64
+ this.logger.info(`pikku-bun-server: MCP mounted at ${this.mcpPath}`);
65
+ }
66
+ catch (err) {
67
+ this.logger.warn(`pikku-bun-server: MCP could not be mounted — ${err instanceof Error ? err.message : String(err)}`);
68
+ }
34
69
  }
35
70
  async start() {
36
- const { config, logger, options, eventHub } = this;
71
+ const { config, logger, options, eventHub, mcpHandler, mcpPath } = this;
37
72
  this.server = Bun.serve({
38
73
  port: config.port,
39
74
  hostname: config.hostname,
@@ -61,6 +96,16 @@ export class PikkuBunServer {
61
96
  headers: { 'content-type': 'application/json' },
62
97
  });
63
98
  }
99
+ if (mcpHandler) {
100
+ const pathname = new URL(req.url).pathname;
101
+ if (pathname === mcpPath || pathname.startsWith(`${mcpPath}/`)) {
102
+ return await mcpHandler(req);
103
+ }
104
+ }
105
+ const staticResponse = await this.serveStaticMounts(req);
106
+ if (staticResponse) {
107
+ return staticResponse;
108
+ }
64
109
  const pikkuReq = new PikkuFetchHTTPRequest(req);
65
110
  const pikkuRes = new PikkuFetchHTTPResponse();
66
111
  await fetchData(pikkuReq, pikkuRes, {
@@ -110,6 +155,54 @@ export class PikkuBunServer {
110
155
  eventHub.setServer(this.server);
111
156
  logger.info(`pikku-bun-server: listening on http://${config.hostname ?? 'localhost'}:${config.port}`);
112
157
  }
158
+ get port() {
159
+ return this.server?.port ?? this.config.port;
160
+ }
161
+ async serveStaticMounts(req) {
162
+ const mounts = this.config.staticMounts;
163
+ if (!mounts?.length || (req.method !== 'GET' && req.method !== 'HEAD')) {
164
+ return null;
165
+ }
166
+ const pathname = decodeURIComponent(new URL(req.url).pathname);
167
+ for (const mount of mounts) {
168
+ if (pathname !== mount.urlPrefix &&
169
+ !pathname.startsWith(`${mount.urlPrefix}/`)) {
170
+ continue;
171
+ }
172
+ const key = pathname.slice(mount.urlPrefix.length).replace(/^\/+/, '');
173
+ const file = await this.resolveStaticFile(mount, key);
174
+ if (file) {
175
+ return new Response(req.method === 'HEAD' ? null : file);
176
+ }
177
+ if (mount.spaFallback) {
178
+ const index = await this.resolveStaticFile(mount, 'index.html');
179
+ if (index) {
180
+ return new Response(req.method === 'HEAD' ? null : index);
181
+ }
182
+ }
183
+ return new Response('Not Found', { status: 404 });
184
+ }
185
+ return null;
186
+ }
187
+ async resolveStaticFile(mount, key) {
188
+ const directory = resolve(mount.directory);
189
+ const targetPath = key === '' ? resolve(directory, 'index.html') : resolve(directory, key);
190
+ if (targetPath !== directory && !targetPath.startsWith(`${directory}/`)) {
191
+ return null;
192
+ }
193
+ let file = Bun.file(targetPath);
194
+ if (!(await file.exists())) {
195
+ const indexPath = resolve(targetPath, 'index.html');
196
+ if (!indexPath.startsWith(`${directory}/`)) {
197
+ return null;
198
+ }
199
+ file = Bun.file(indexPath);
200
+ if (!(await file.exists())) {
201
+ return null;
202
+ }
203
+ }
204
+ return file;
205
+ }
113
206
  async stop() {
114
207
  await this.server?.stop();
115
208
  this.server = null;
@@ -1 +1 @@
1
- {"version":3,"file":"pikku-bun-server.js","sourceRoot":"","sources":["../src/pikku-bun-server.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAA;AAEnD,OAAO,EACL,SAAS,EACT,qBAAqB,EACrB,sBAAsB,EACtB,SAAS,IAAI,iBAAiB,GAE/B,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AAEjD,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAA;AAC3D,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAEtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAA;AAqB/D,MAAM,cAAc,GAAG,CAAC,IAAa,EAAW,EAAE,CAChD,CAAC,CACC,OAAO,IAAI,KAAK,QAAQ;IACxB,IAAI,YAAY,WAAW;IAC3B,IAAI,YAAY,UAAU;IAC1B,IAAI,YAAY,SAAS;IACzB,IAAI,YAAY,WAAW;IAC3B,IAAI,YAAY,UAAU;IAC1B,IAAI,YAAY,WAAW;IAC3B,IAAI,YAAY,UAAU;IAC1B,IAAI,YAAY,YAAY;IAC5B,IAAI,YAAY,YAAY,CAC7B,CAAA;AAQH,MAAM,OAAO,cAAc;IAMN;IACA;IANX,MAAM,GAA6B,IAAI,CAAA;IAC9B,QAAQ,CAAoB;IAC5B,OAAO,CAAsB;IAE9C,YACmB,MAAuB,EACvB,MAAc,EAC/B,UAAiC,EAAE;QAFlB,WAAM,GAAN,MAAM,CAAiB;QACvB,WAAM,GAAN,MAAM,CAAQ;QAG/B,MAAM,EAAE,QAAQ,EAAE,GAAG,WAAW,EAAE,GAAG,OAAO,CAAA;QAC5C,IAAI,CAAC,QAAQ,GAAG,QAAQ,IAAI,IAAI,kBAAkB,EAAE,CAAA;QACpD,IAAI,CAAC,OAAO,GAAG,WAAW,CAAA;IAC5B,CAAC;IAEM,KAAK,CAAC,IAAI;QACf,iBAAiB,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;QAC9B,iBAAiB,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;QAC9B,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;IAC1B,CAAC;IAEM,KAAK,CAAC,KAAK;QAChB,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,IAAI,CAAA;QAElD,IAAI,CAAC,MAAM,GAAG,GAAG,CAAC,KAAK,CAAS;YAC9B,IAAI,EAAE,MAAM,CAAC,IAAI;YACjB,QAAQ,EAAE,MAAM,CAAC,QAAQ;YAEzB,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE;gBAC3B,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,WAAW,EAAE,KAAK,WAAW,EAAE,CAAC;oBAC9D,MAAM,QAAQ,GAAG,IAAI,qBAAqB,CAAC,GAAG,CAAC,CAAA;oBAC/C,MAAM,QAAQ,GAAG,IAAI,sBAAsB,EAAE,CAAA;oBAC7C,MAAM,cAAc,GAAG,MAAM,eAAe,CAAC;wBAC3C,SAAS,EAAE,MAAM,CAAC,UAAU,EAAE;wBAC9B,OAAO,EAAE,QAAQ;wBACjB,QAAQ,EAAE,QAAQ;wBAClB,KAAK,EAAE,IAAI,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ;qBACjC,CAAC,CAAA;oBACF,IAAI,CAAC,cAAc,EAAE,CAAC;wBACpB,OAAO,IAAI,QAAQ,CAAC,WAAW,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;oBACnD,CAAC;oBACD,MAAM,QAAQ,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,EAAE,cAAc,EAAE,EAAE,CAAC,CAAA;oBAClE,IAAI,QAAQ;wBAAE,OAAO,SAAgC,CAAA;oBACrD,OAAO,IAAI,QAAQ,CAAC,0BAA0B,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;gBAClE,CAAC;gBAED,IACE,MAAM,CAAC,eAAe;oBACtB,IAAI,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,KAAK,MAAM,CAAC,eAAe,EACpD,CAAC;oBACD,OAAO,IAAI,QAAQ,CAAC,aAAa,EAAE;wBACjC,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;qBAChD,CAAC,CAAA;gBACJ,CAAC;gBAED,MAAM,QAAQ,GAAG,IAAI,qBAAqB,CAAC,GAAG,CAAC,CAAA;gBAC/C,MAAM,QAAQ,GAAG,IAAI,sBAAsB,EAAE,CAAA;gBAC7C,MAAM,SAAS,CAAC,QAAQ,EAAE,QAAQ,EAAE;oBAClC,cAAc,EAAE,IAAI;oBACpB,GAAG,OAAO;iBACX,CAAC,CAAA;gBACF,OAAO,QAAQ,CAAC,UAAU,EAAE,CAAA;YAC9B,CAAC;YAED,SAAS,EAAE;gBACT,IAAI,EAAE,CAAC,EAA2B,EAAE,EAAE;oBACpC,MAAM,EAAE,cAAc,EAAE,GAAG,EAAE,CAAC,IAAI,CAAA;oBAClC,cAAc,CAAC,cAAc,CAAC,CAAC,IAAI,EAAE,EAAE;wBACrC,EAAE,CAAC,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,CAAE,IAAY,CAAC,CAAA;oBACtE,CAAC,CAAC,CAAA;oBACF,cAAc,CAAC,oBAAoB,CAAC,CAAC,IAAI,EAAE,EAAE;wBAC3C,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAA;oBACrB,CAAC,CAAC,CAAA;oBACF,cAAc,CAAC,eAAe,CAAC,GAAG,EAAE;wBAClC,EAAE,CAAC,KAAK,EAAE,CAAA;oBACZ,CAAC,CAAC,CAAA;oBACF,QAAQ,CAAC,eAAe,CAAC,cAAc,CAAC,SAAS,EAAE,EAAE,CAAC,CAAA;oBACtD,cAAc,CAAC,IAAI,EAAE,CAAA;gBACvB,CAAC;gBAED,OAAO,EAAE,KAAK,EAAE,EAA2B,EAAE,OAAO,EAAE,EAAE;oBACtD,MAAM,EAAE,cAAc,EAAE,GAAG,EAAE,CAAC,IAAI,CAAA;oBAClC,IAAI,OAAO,OAAO,KAAK,QAAQ,EAAE,CAAC;wBAChC,MAAM,MAAM,GAAG,MAAM,cAAc,CAAC,OAAO,CAAC,OAAO,CAAC,CAAA;wBACpD,IAAI,MAAM;4BAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAA;oBAC7C,CAAC;yBAAM,CAAC;wBACN,MAAM,KAAK,GACT,OAAO,YAAY,WAAW;4BAC5B,CAAC,CAAC,IAAI,UAAU,CAAC,OAAO,CAAC;4BACzB,CAAC,CAAC,IAAI,UAAU,CACZ,OAAO,CAAC,MAAM,EACd,OAAO,CAAC,UAAU,EAClB,OAAO,CAAC,UAAU,CACnB,CAAA;wBACP,MAAM,MAAM,GAAG,MAAM,cAAc,CAAC,aAAa,CAAC,KAAK,CAAC,CAAA;wBACxD,IAAI,MAAM;4BAAE,cAAc,CAAC,UAAU,CAAC,MAAM,CAAC,CAAA;oBAC/C,CAAC;gBACH,CAAC;gBAED,KAAK,EAAE,CAAC,EAA2B,EAAE,EAAE;oBACrC,MAAM,EAAE,cAAc,EAAE,GAAG,EAAE,CAAC,IAAI,CAAA;oBAClC,QAAQ,CAAC,eAAe,CAAC,cAAc,CAAC,SAAS,CAAC,CAAA;oBAClD,cAAc,CAAC,KAAK,EAAE,CAAA;gBACxB,CAAC;aACF;SACF,CAAC,CAAA;QAEF,QAAQ,CAAC,SAAS,CAAC,IAAI,CAAC,MAA4B,CAAC,CAAA;QACrD,MAAM,CAAC,IAAI,CACT,yCAAyC,MAAM,CAAC,QAAQ,IAAI,WAAW,IAAI,MAAM,CAAC,IAAI,EAAE,CACzF,CAAA;IACH,CAAC;IAEM,KAAK,CAAC,IAAI;QACf,MAAM,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,CAAA;QACzB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAA;IACpB,CAAC;IAEM,mBAAmB;QACxB,MAAM,QAAQ,GAAG,KAAK,EAAE,MAAc,EAAE,EAAE;YACxC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,qBAAqB,MAAM,qBAAqB,CAAC,CAAA;YAClE,IAAI,CAAC;gBACH,MAAM,qBAAqB,EAAE,CAAA;gBAC7B,MAAM,IAAI,CAAC,IAAI,EAAE,CAAA;YACnB,CAAC;oBAAS,CAAC;gBACT,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;YACjB,CAAC;QACH,CAAC,CAAA;QACD,OAAO,CAAC,IAAI,CAAC,QAAQ,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAA;QAChD,OAAO,CAAC,IAAI,CAAC,SAAS,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAA;IACpD,CAAC;CACF"}
1
+ {"version":3,"file":"pikku-bun-server.js","sourceRoot":"","sources":["../src/pikku-bun-server.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAA;AAEnD,OAAO,EACL,SAAS,EACT,qBAAqB,EACrB,sBAAsB,EACtB,SAAS,IAAI,iBAAiB,GAE/B,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AAEjD,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAA;AAC3D,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAEtD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAEnC,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAA;AAwC/D,MAAM,cAAc,GAAG,CAAC,IAAa,EAAW,EAAE,CAChD,CAAC,CACC,OAAO,IAAI,KAAK,QAAQ;IACxB,IAAI,YAAY,WAAW;IAC3B,IAAI,YAAY,UAAU;IAC1B,IAAI,YAAY,SAAS;IACzB,IAAI,YAAY,WAAW;IAC3B,IAAI,YAAY,UAAU;IAC1B,IAAI,YAAY,WAAW;IAC3B,IAAI,YAAY,UAAU;IAC1B,IAAI,YAAY,YAAY;IAC5B,IAAI,YAAY,YAAY,CAC7B,CAAA;AAQH,MAAM,OAAO,cAAc;IASN;IACA;IATX,MAAM,GAA6B,IAAI,CAAA;IAC9B,QAAQ,CAAoB;IAC5B,OAAO,CAAsB;IAC7B,OAAO,CAAmC;IAC1C,OAAO,CAAQ;IACxB,UAAU,CAA0C;IAE5D,YACmB,MAAuB,EACvB,MAAc,EAC/B,UAAiC,EAAE;QAFlB,WAAM,GAAN,MAAM,CAAiB;QACvB,WAAM,GAAN,MAAM,CAAQ;QAG/B,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,WAAW,EAAE,GAAG,OAAO,CAAA;QAC9D,IAAI,CAAC,QAAQ,GAAG,QAAQ,IAAI,IAAI,kBAAkB,EAAE,CAAA;QACpD,IAAI,CAAC,OAAO,GAAG,OAAO,CAAA;QACtB,IAAI,CAAC,OAAO,GAAG,OAAO,IAAI,MAAM,CAAA;QAChC,IAAI,CAAC,OAAO,GAAG,WAAW,CAAA;IAC5B,CAAC;IAEM,KAAK,CAAC,IAAI;QACf,iBAAiB,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;QAC9B,iBAAiB,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;QAC9B,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;QACxB,MAAM,IAAI,CAAC,OAAO,EAAE,CAAA;IACtB,CAAC;IAEO,KAAK,CAAC,OAAO;QACnB,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAA;QAC5B,IAAI,CAAC,OAAO;YAAE,OAAM;QACpB,MAAM,EAAE,KAAK,GAAG,EAAE,EAAE,SAAS,GAAG,EAAE,EAAE,OAAO,GAAG,EAAE,EAAE,GAAG,OAAO,CAAA;QAC5D,IAAI,KAAK,CAAC,MAAM,GAAG,SAAS,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,KAAK,CAAC;YAAE,OAAM;QAClE,IAAI,CAAC;YACH,MAAM,EAAE,cAAc,EAAE,GAAG,MAAM,MAAM,CAAC,6BAA6B,CAAC,CAAA;YACtE,MAAM,SAAS,GAAG,IAAI,cAAc,CAClC;gBACE,IAAI,EAAE,OAAO;gBACb,OAAO,EAAE,OAAO;gBAChB,OAAO,EAAE,OAAO;gBAChB,YAAY,EAAE;oBACZ,GAAG,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC;oBACtC,GAAG,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,SAAS,EAAE,EAAE,EAAE,CAAC;oBAC9C,GAAG,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;iBAC3C;aACF,EACD,IAAI,CAAC,MAAM,CACZ,CAAA;YACD,MAAM,SAAS,CAAC,IAAI,EAAE,CAAA;YACtB,MAAM,EAAE,OAAO,EAAE,GAAG,SAAS,CAAC,kBAAkB,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,CAAA;YACxE,IAAI,CAAC,UAAU,GAAG,OAAO,CAAA;YACzB,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,oCAAoC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAA;QACtE,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,MAAM,CAAC,IAAI,CACd,gDAAgD,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CACnG,CAAA;QACH,CAAC;IACH,CAAC;IAEM,KAAK,CAAC,KAAK;QAChB,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,OAAO,EAAE,GAAG,IAAI,CAAA;QAEvE,IAAI,CAAC,MAAM,GAAG,GAAG,CAAC,KAAK,CAAS;YAC9B,IAAI,EAAE,MAAM,CAAC,IAAI;YACjB,QAAQ,EAAE,MAAM,CAAC,QAAQ;YAEzB,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE;gBAC3B,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,WAAW,EAAE,KAAK,WAAW,EAAE,CAAC;oBAC9D,MAAM,QAAQ,GAAG,IAAI,qBAAqB,CAAC,GAAG,CAAC,CAAA;oBAC/C,MAAM,QAAQ,GAAG,IAAI,sBAAsB,EAAE,CAAA;oBAC7C,MAAM,cAAc,GAAG,MAAM,eAAe,CAAC;wBAC3C,SAAS,EAAE,MAAM,CAAC,UAAU,EAAE;wBAC9B,OAAO,EAAE,QAAQ;wBACjB,QAAQ,EAAE,QAAQ;wBAClB,KAAK,EAAE,IAAI,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ;qBACjC,CAAC,CAAA;oBACF,IAAI,CAAC,cAAc,EAAE,CAAC;wBACpB,OAAO,IAAI,QAAQ,CAAC,WAAW,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;oBACnD,CAAC;oBACD,MAAM,QAAQ,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,EAAE,cAAc,EAAE,EAAE,CAAC,CAAA;oBAClE,IAAI,QAAQ;wBAAE,OAAO,SAAgC,CAAA;oBACrD,OAAO,IAAI,QAAQ,CAAC,0BAA0B,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;gBAClE,CAAC;gBAED,IACE,MAAM,CAAC,eAAe;oBACtB,IAAI,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,KAAK,MAAM,CAAC,eAAe,EACpD,CAAC;oBACD,OAAO,IAAI,QAAQ,CAAC,aAAa,EAAE;wBACjC,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;qBAChD,CAAC,CAAA;gBACJ,CAAC;gBAED,IAAI,UAAU,EAAE,CAAC;oBACf,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAA;oBAC1C,IAAI,QAAQ,KAAK,OAAO,IAAI,QAAQ,CAAC,UAAU,CAAC,GAAG,OAAO,GAAG,CAAC,EAAE,CAAC;wBAC/D,OAAO,MAAM,UAAU,CAAC,GAAG,CAAC,CAAA;oBAC9B,CAAC;gBACH,CAAC;gBAED,MAAM,cAAc,GAAG,MAAM,IAAI,CAAC,iBAAiB,CAAC,GAAG,CAAC,CAAA;gBACxD,IAAI,cAAc,EAAE,CAAC;oBACnB,OAAO,cAAc,CAAA;gBACvB,CAAC;gBAED,MAAM,QAAQ,GAAG,IAAI,qBAAqB,CAAC,GAAG,CAAC,CAAA;gBAC/C,MAAM,QAAQ,GAAG,IAAI,sBAAsB,EAAE,CAAA;gBAC7C,MAAM,SAAS,CAAC,QAAQ,EAAE,QAAQ,EAAE;oBAClC,cAAc,EAAE,IAAI;oBACpB,GAAG,OAAO;iBACX,CAAC,CAAA;gBACF,OAAO,QAAQ,CAAC,UAAU,EAAE,CAAA;YAC9B,CAAC;YAED,SAAS,EAAE;gBACT,IAAI,EAAE,CAAC,EAA2B,EAAE,EAAE;oBACpC,MAAM,EAAE,cAAc,EAAE,GAAG,EAAE,CAAC,IAAI,CAAA;oBAClC,cAAc,CAAC,cAAc,CAAC,CAAC,IAAI,EAAE,EAAE;wBACrC,EAAE,CAAC,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,CAAE,IAAY,CAAC,CAAA;oBACtE,CAAC,CAAC,CAAA;oBACF,cAAc,CAAC,oBAAoB,CAAC,CAAC,IAAI,EAAE,EAAE;wBAC3C,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAA;oBACrB,CAAC,CAAC,CAAA;oBACF,cAAc,CAAC,eAAe,CAAC,GAAG,EAAE;wBAClC,EAAE,CAAC,KAAK,EAAE,CAAA;oBACZ,CAAC,CAAC,CAAA;oBACF,QAAQ,CAAC,eAAe,CAAC,cAAc,CAAC,SAAS,EAAE,EAAE,CAAC,CAAA;oBACtD,cAAc,CAAC,IAAI,EAAE,CAAA;gBACvB,CAAC;gBAED,OAAO,EAAE,KAAK,EAAE,EAA2B,EAAE,OAAO,EAAE,EAAE;oBACtD,MAAM,EAAE,cAAc,EAAE,GAAG,EAAE,CAAC,IAAI,CAAA;oBAClC,IAAI,OAAO,OAAO,KAAK,QAAQ,EAAE,CAAC;wBAChC,MAAM,MAAM,GAAG,MAAM,cAAc,CAAC,OAAO,CAAC,OAAO,CAAC,CAAA;wBACpD,IAAI,MAAM;4BAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAA;oBAC7C,CAAC;yBAAM,CAAC;wBACN,MAAM,KAAK,GACT,OAAO,YAAY,WAAW;4BAC5B,CAAC,CAAC,IAAI,UAAU,CAAC,OAAO,CAAC;4BACzB,CAAC,CAAC,IAAI,UAAU,CACZ,OAAO,CAAC,MAAM,EACd,OAAO,CAAC,UAAU,EAClB,OAAO,CAAC,UAAU,CACnB,CAAA;wBACP,MAAM,MAAM,GAAG,MAAM,cAAc,CAAC,aAAa,CAAC,KAAK,CAAC,CAAA;wBACxD,IAAI,MAAM;4BAAE,cAAc,CAAC,UAAU,CAAC,MAAM,CAAC,CAAA;oBAC/C,CAAC;gBACH,CAAC;gBAED,KAAK,EAAE,CAAC,EAA2B,EAAE,EAAE;oBACrC,MAAM,EAAE,cAAc,EAAE,GAAG,EAAE,CAAC,IAAI,CAAA;oBAClC,QAAQ,CAAC,eAAe,CAAC,cAAc,CAAC,SAAS,CAAC,CAAA;oBAClD,cAAc,CAAC,KAAK,EAAE,CAAA;gBACxB,CAAC;aACF;SACF,CAAC,CAAA;QAEF,QAAQ,CAAC,SAAS,CAAC,IAAI,CAAC,MAA4B,CAAC,CAAA;QACrD,MAAM,CAAC,IAAI,CACT,yCAAyC,MAAM,CAAC,QAAQ,IAAI,WAAW,IAAI,MAAM,CAAC,IAAI,EAAE,CACzF,CAAA;IACH,CAAC;IAED,IAAW,IAAI;QACb,OAAO,IAAI,CAAC,MAAM,EAAE,IAAI,IAAI,IAAI,CAAC,MAAM,CAAC,IAAI,CAAA;IAC9C,CAAC;IAEO,KAAK,CAAC,iBAAiB,CAAC,GAAY;QAC1C,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,YAAY,CAAA;QACvC,IAAI,CAAC,MAAM,EAAE,MAAM,IAAI,CAAC,GAAG,CAAC,MAAM,KAAK,KAAK,IAAI,GAAG,CAAC,MAAM,KAAK,MAAM,CAAC,EAAE,CAAC;YACvE,OAAO,IAAI,CAAA;QACb,CAAC;QACD,MAAM,QAAQ,GAAG,kBAAkB,CAAC,IAAI,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAA;QAE9D,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YAC3B,IACE,QAAQ,KAAK,KAAK,CAAC,SAAS;gBAC5B,CAAC,QAAQ,CAAC,UAAU,CAAC,GAAG,KAAK,CAAC,SAAS,GAAG,CAAC,EAC3C,CAAC;gBACD,SAAQ;YACV,CAAC;YACD,MAAM,GAAG,GAAG,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAA;YACtE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,iBAAiB,CAAC,KAAK,EAAE,GAAG,CAAC,CAAA;YACrD,IAAI,IAAI,EAAE,CAAC;gBACT,OAAO,IAAI,QAAQ,CAAC,GAAG,CAAC,MAAM,KAAK,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAA;YAC1D,CAAC;YACD,IAAI,KAAK,CAAC,WAAW,EAAE,CAAC;gBACtB,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,iBAAiB,CAAC,KAAK,EAAE,YAAY,CAAC,CAAA;gBAC/D,IAAI,KAAK,EAAE,CAAC;oBACV,OAAO,IAAI,QAAQ,CAAC,GAAG,CAAC,MAAM,KAAK,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAA;gBAC3D,CAAC;YACH,CAAC;YACD,OAAO,IAAI,QAAQ,CAAC,WAAW,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;QACnD,CAAC;QAED,OAAO,IAAI,CAAA;IACb,CAAC;IAEO,KAAK,CAAC,iBAAiB,CAC7B,KAAkB,EAClB,GAAW;QAEX,MAAM,SAAS,GAAG,OAAO,CAAC,KAAK,CAAC,SAAS,CAAC,CAAA;QAC1C,MAAM,UAAU,GACd,GAAG,KAAK,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,SAAS,EAAE,GAAG,CAAC,CAAA;QACzE,IAAI,UAAU,KAAK,SAAS,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,GAAG,SAAS,GAAG,CAAC,EAAE,CAAC;YACxE,OAAO,IAAI,CAAA;QACb,CAAC;QACD,IAAI,IAAI,GAAG,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,CAAA;QAC/B,IAAI,CAAC,CAAC,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC;YAC3B,MAAM,SAAS,GAAG,OAAO,CAAC,UAAU,EAAE,YAAY,CAAC,CAAA;YACnD,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,GAAG,SAAS,GAAG,CAAC,EAAE,CAAC;gBAC3C,OAAO,IAAI,CAAA;YACb,CAAC;YACD,IAAI,GAAG,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;YAC1B,IAAI,CAAC,CAAC,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC;gBAC3B,OAAO,IAAI,CAAA;YACb,CAAC;QACH,CAAC;QACD,OAAO,IAAI,CAAA;IACb,CAAC;IAEM,KAAK,CAAC,IAAI;QACf,MAAM,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,CAAA;QACzB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAA;IACpB,CAAC;IAEM,mBAAmB;QACxB,MAAM,QAAQ,GAAG,KAAK,EAAE,MAAc,EAAE,EAAE;YACxC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,qBAAqB,MAAM,qBAAqB,CAAC,CAAA;YAClE,IAAI,CAAC;gBACH,MAAM,qBAAqB,EAAE,CAAA;gBAC7B,MAAM,IAAI,CAAC,IAAI,EAAE,CAAA;YACnB,CAAC;oBAAS,CAAC;gBACT,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;YACjB,CAAC;QACH,CAAC,CAAA;QACD,OAAO,CAAC,IAAI,CAAC,QAAQ,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAA;QAChD,OAAO,CAAC,IAAI,CAAC,SAAS,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAA;IACpD,CAAC;CACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/bun-server",
3
- "version": "0.12.2",
3
+ "version": "0.12.4",
4
4
  "description": "Pikku server adapter for Bun (Bun.serve)",
5
5
  "license": "MIT",
6
6
  "author": "yasser.fadl@gmail.com",
@@ -16,9 +16,16 @@
16
16
  "prepublishOnly": "bun run build"
17
17
  },
18
18
  "peerDependencies": {
19
- "@pikku/core": "^0.12.44"
19
+ "@pikku/core": "^0.12.52",
20
+ "@pikku/modelcontextprotocol": "^0.12.6"
21
+ },
22
+ "peerDependenciesMeta": {
23
+ "@pikku/modelcontextprotocol": {
24
+ "optional": true
25
+ }
20
26
  },
21
27
  "devDependencies": {
28
+ "@pikku/modelcontextprotocol": "^0.12.6",
22
29
  "@types/bun": "latest",
23
30
  "typescript": "^6.0.3"
24
31
  },
@@ -15,11 +15,20 @@ import type { PikkuLocalChannelHandler } from '@pikku/core/channel/local'
15
15
  import { runLocalChannel } from '@pikku/core/channel/local'
16
16
  import { compileAllSchemas } from '@pikku/core/schema'
17
17
 
18
+ import { resolve } from 'node:path'
19
+
18
20
  import { BunEventHubService } from './bun-event-hub-service.js'
19
21
 
22
+ export type StaticMount = {
23
+ urlPrefix: string
24
+ directory: string
25
+ spaFallback?: boolean
26
+ }
27
+
20
28
  export type BunServerConfig = CoreConfig & {
21
29
  port: number
22
30
  hostname?: string
31
+ staticMounts?: StaticMount[]
23
32
  healthCheckPath?: string
24
33
  }
25
34
 
@@ -32,6 +41,18 @@ export type PikkuBunServerOptions = RunHTTPWiringOptions & {
32
41
  * connected clients. Defaults to a fresh `BunEventHubService`.
33
42
  */
34
43
  eventHub?: BunEventHubService
44
+ /**
45
+ * Parsed content of `.pikku/mcp/mcp.gen.json`. When provided and non-empty,
46
+ * `@pikku/modelcontextprotocol` is dynamically imported and the MCP server is
47
+ * mounted at `mcpPath` via the SDK's fetch-native (Web Standard) transport.
48
+ * Import the JSON statically so bundlers (esbuild) inline it — no file read.
49
+ * Mirrors `PikkuNodeHTTPServerOptions.mcpJson`.
50
+ */
51
+ mcpJson?: { tools?: unknown[]; resources?: unknown[]; prompts?: unknown[] }
52
+ /**
53
+ * Path the MCP server is mounted at when `mcpJson` is provided. Default `/mcp`.
54
+ */
55
+ mcpPath?: string
35
56
  }
36
57
 
37
58
  type WsData = { channelHandler: PikkuLocalChannelHandler }
@@ -60,14 +81,19 @@ export class PikkuBunServer {
60
81
  private server: BunServer<WsData> | null = null
61
82
  private readonly eventHub: BunEventHubService
62
83
  private readonly options: RunHTTPWiringOptions
84
+ private readonly mcpJson?: PikkuBunServerOptions['mcpJson']
85
+ private readonly mcpPath: string
86
+ private mcpHandler?: (request: Request) => Promise<Response>
63
87
 
64
88
  constructor(
65
89
  private readonly config: BunServerConfig,
66
90
  private readonly logger: Logger,
67
91
  options: PikkuBunServerOptions = {}
68
92
  ) {
69
- const { eventHub, ...httpOptions } = options
93
+ const { eventHub, mcpJson, mcpPath, ...httpOptions } = options
70
94
  this.eventHub = eventHub ?? new BunEventHubService()
95
+ this.mcpJson = mcpJson
96
+ this.mcpPath = mcpPath ?? '/mcp'
71
97
  this.options = httpOptions
72
98
  }
73
99
 
@@ -75,10 +101,42 @@ export class PikkuBunServer {
75
101
  compileAllSchemas(this.logger)
76
102
  logRegisterRoutes(this.logger)
77
103
  logChannels(this.logger)
104
+ await this.initMCP()
105
+ }
106
+
107
+ private async initMCP(): Promise<void> {
108
+ const mcpJson = this.mcpJson
109
+ if (!mcpJson) return
110
+ const { tools = [], resources = [], prompts = [] } = mcpJson
111
+ if (tools.length + resources.length + prompts.length === 0) return
112
+ try {
113
+ const { PikkuMCPServer } = await import('@pikku/modelcontextprotocol')
114
+ const mcpServer = new PikkuMCPServer(
115
+ {
116
+ name: 'pikku',
117
+ version: '1.0.0',
118
+ mcpJSON: mcpJson,
119
+ capabilities: {
120
+ ...(tools.length > 0 && { tools: {} }),
121
+ ...(resources.length > 0 && { resources: {} }),
122
+ ...(prompts.length > 0 && { prompts: {} }),
123
+ },
124
+ },
125
+ this.logger
126
+ )
127
+ await mcpServer.init()
128
+ const { handler } = mcpServer.createFetchHandler({ path: this.mcpPath })
129
+ this.mcpHandler = handler
130
+ this.logger.info(`pikku-bun-server: MCP mounted at ${this.mcpPath}`)
131
+ } catch (err) {
132
+ this.logger.warn(
133
+ `pikku-bun-server: MCP could not be mounted — ${err instanceof Error ? err.message : String(err)}`
134
+ )
135
+ }
78
136
  }
79
137
 
80
138
  public async start(): Promise<void> {
81
- const { config, logger, options, eventHub } = this
139
+ const { config, logger, options, eventHub, mcpHandler, mcpPath } = this
82
140
 
83
141
  this.server = Bun.serve<WsData>({
84
142
  port: config.port,
@@ -111,6 +169,18 @@ export class PikkuBunServer {
111
169
  })
112
170
  }
113
171
 
172
+ if (mcpHandler) {
173
+ const pathname = new URL(req.url).pathname
174
+ if (pathname === mcpPath || pathname.startsWith(`${mcpPath}/`)) {
175
+ return await mcpHandler(req)
176
+ }
177
+ }
178
+
179
+ const staticResponse = await this.serveStaticMounts(req)
180
+ if (staticResponse) {
181
+ return staticResponse
182
+ }
183
+
114
184
  const pikkuReq = new PikkuFetchHTTPRequest(req)
115
185
  const pikkuRes = new PikkuFetchHTTPResponse()
116
186
  await fetchData(pikkuReq, pikkuRes, {
@@ -169,6 +239,65 @@ export class PikkuBunServer {
169
239
  )
170
240
  }
171
241
 
242
+ public get port(): number {
243
+ return this.server?.port ?? this.config.port
244
+ }
245
+
246
+ private async serveStaticMounts(req: Request): Promise<Response | null> {
247
+ const mounts = this.config.staticMounts
248
+ if (!mounts?.length || (req.method !== 'GET' && req.method !== 'HEAD')) {
249
+ return null
250
+ }
251
+ const pathname = decodeURIComponent(new URL(req.url).pathname)
252
+
253
+ for (const mount of mounts) {
254
+ if (
255
+ pathname !== mount.urlPrefix &&
256
+ !pathname.startsWith(`${mount.urlPrefix}/`)
257
+ ) {
258
+ continue
259
+ }
260
+ const key = pathname.slice(mount.urlPrefix.length).replace(/^\/+/, '')
261
+ const file = await this.resolveStaticFile(mount, key)
262
+ if (file) {
263
+ return new Response(req.method === 'HEAD' ? null : file)
264
+ }
265
+ if (mount.spaFallback) {
266
+ const index = await this.resolveStaticFile(mount, 'index.html')
267
+ if (index) {
268
+ return new Response(req.method === 'HEAD' ? null : index)
269
+ }
270
+ }
271
+ return new Response('Not Found', { status: 404 })
272
+ }
273
+
274
+ return null
275
+ }
276
+
277
+ private async resolveStaticFile(
278
+ mount: StaticMount,
279
+ key: string
280
+ ): Promise<ReturnType<typeof Bun.file> | null> {
281
+ const directory = resolve(mount.directory)
282
+ const targetPath =
283
+ key === '' ? resolve(directory, 'index.html') : resolve(directory, key)
284
+ if (targetPath !== directory && !targetPath.startsWith(`${directory}/`)) {
285
+ return null
286
+ }
287
+ let file = Bun.file(targetPath)
288
+ if (!(await file.exists())) {
289
+ const indexPath = resolve(targetPath, 'index.html')
290
+ if (!indexPath.startsWith(`${directory}/`)) {
291
+ return null
292
+ }
293
+ file = Bun.file(indexPath)
294
+ if (!(await file.exists())) {
295
+ return null
296
+ }
297
+ }
298
+ return file
299
+ }
300
+
172
301
  public async stop(): Promise<void> {
173
302
  await this.server?.stop()
174
303
  this.server = null
@@ -0,0 +1,108 @@
1
+ import { describe, test, before, after } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'
4
+ import { tmpdir } from 'node:os'
5
+ import { join } from 'node:path'
6
+ import { setSingletonServices } from '@pikku/core'
7
+ import { resetPikkuState } from '@pikku/core/internal'
8
+ import type { Logger } from '@pikku/core/services'
9
+ import { PikkuBunServer } from './pikku-bun-server.js'
10
+
11
+ const noopLogger = {
12
+ info: () => {},
13
+ error: () => {},
14
+ warn: () => {},
15
+ debug: () => {},
16
+ trace: () => {},
17
+ setLevel: () => {},
18
+ } as unknown as Logger
19
+
20
+ describe('PikkuBunServer static mounts', () => {
21
+ let server: PikkuBunServer
22
+ let tmpDir: string
23
+ let origin: string
24
+
25
+ before(async () => {
26
+ resetPikkuState()
27
+ setSingletonServices({
28
+ logger: noopLogger,
29
+ schema: {
30
+ compileSchema: () => {},
31
+ getSchemaNames: () => new Set<string>(),
32
+ },
33
+ } as any)
34
+ tmpDir = mkdtempSync(join(tmpdir(), 'pikku-bun-static-'))
35
+ writeFileSync(
36
+ join(tmpDir, 'index.html'),
37
+ '<!doctype html><title>console</title>'
38
+ )
39
+ mkdirSync(join(tmpDir, 'assets'))
40
+ writeFileSync(join(tmpDir, 'assets', 'app.js'), 'console.log("app")')
41
+
42
+ server = new PikkuBunServer(
43
+ {
44
+ port: 0,
45
+ hostname: '127.0.0.1',
46
+ staticMounts: [
47
+ { urlPrefix: '/console', directory: tmpDir, spaFallback: true },
48
+ ],
49
+ } as any,
50
+ noopLogger
51
+ )
52
+ await server.init()
53
+ await server.start()
54
+ origin = `http://127.0.0.1:${server.port}`
55
+ })
56
+
57
+ after(async () => {
58
+ await server.stop()
59
+ rmSync(tmpDir, { recursive: true, force: true })
60
+ })
61
+
62
+ test('serves files under the prefix with a content type', async () => {
63
+ const response = await fetch(`${origin}/console/assets/app.js`)
64
+ assert.equal(response.status, 200)
65
+ assert.match(response.headers.get('content-type') ?? '', /javascript/)
66
+ assert.equal(await response.text(), 'console.log("app")')
67
+ })
68
+
69
+ test('serves index.html at the mount root, with and without trailing slash', async () => {
70
+ for (const path of ['/console', '/console/']) {
71
+ const response = await fetch(`${origin}${path}`)
72
+ assert.equal(response.status, 200)
73
+ assert.match(await response.text(), /<title>console<\/title>/)
74
+ }
75
+ })
76
+
77
+ test('SPA fallback serves index.html for unknown paths under the prefix', async () => {
78
+ const response = await fetch(`${origin}/console/tests/userflows?id=x`)
79
+ assert.equal(response.status, 200)
80
+ assert.match(await response.text(), /<title>console<\/title>/)
81
+ })
82
+
83
+ test('does not intercept paths outside the prefix', async () => {
84
+ const response = await fetch(`${origin}/consolerelated`)
85
+ assert.equal(response.status, 404)
86
+ })
87
+
88
+ test('does not intercept non-GET methods', async () => {
89
+ const response = await fetch(`${origin}/console/assets/app.js`, {
90
+ method: 'POST',
91
+ })
92
+ assert.equal(response.status, 404)
93
+ })
94
+
95
+ test('blocks path traversal out of the mount directory', async () => {
96
+ writeFileSync(join(tmpDir, '..', 'pikku-bun-static-secret.txt'), 'secret')
97
+ try {
98
+ const response = await fetch(
99
+ `${origin}/console/%2e%2e/pikku-bun-static-secret.txt`
100
+ )
101
+ assert.notEqual(response.status, 200)
102
+ } finally {
103
+ rmSync(join(tmpDir, '..', 'pikku-bun-static-secret.txt'), {
104
+ force: true,
105
+ })
106
+ }
107
+ })
108
+ })