Skip to content

Commit c439d3c

Browse files
gxklclaude
andcommitted
docs(service-worker): runnable example, package READMEs, wiki page
Add examples/helloworld-service-worker (an HTTP controller and an MCP tool served from one tegg module over ServiceWorkerApp, with its own vitest suite; the entry lives outside the scanned app/ dir so the module scan does not execute it), README for both service worker packages, and the wiki package page + log entry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent d58477a commit c439d3c

15 files changed

Lines changed: 385 additions & 0 deletions

File tree

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# helloworld-service-worker
2+
3+
Minimal example of `@eggjs/service-worker`: a tegg module served through the
4+
standalone service worker runtime — HTTP controllers and MCP tools over the
5+
same fetch event loop, no egg application required.
6+
7+
## Run
8+
9+
```bash
10+
# from the monorepo root
11+
ut install --from pnpm
12+
node --import=@oxc-node/core/register examples/helloworld-service-worker/main.ts
13+
```
14+
15+
Then:
16+
17+
```bash
18+
curl 'http://127.0.0.1:7001/hello/?name=you'
19+
# {"message":"hello, you"}
20+
21+
curl -X POST 'http://127.0.0.1:7001/mcp/calc/stream' \
22+
-H 'accept: application/json, text/event-stream' \
23+
-H 'content-type: application/json' \
24+
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"add","arguments":{"a":1,"b":41}}}'
25+
```
26+
27+
## What's inside
28+
29+
- `app/` — the tegg module (its `package.json` declares `eggModule.name`):
30+
- `HelloController.ts` — an `@HTTPController` bound to `GET /hello/`.
31+
- `CalcMCPController.ts` — an `@MCPController` exposing an `add` tool over
32+
MCP stateless streamable HTTP at `/mcp/calc`.
33+
- `HelloService.ts` — a `@ContextProto` service injected into both.
34+
- `main.ts` — boots `ServiceWorkerApp` on the module dir and serves it over
35+
`node:http`. The entry lives outside `app/` so the module scan doesn't
36+
execute it.
37+
38+
## Test
39+
40+
```bash
41+
# from the monorepo root
42+
utx vitest run examples/helloworld-service-worker --config examples/helloworld-service-worker/vitest.config.ts
43+
```
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { Inject, MCPController, MCPTool, ToolArgsSchema } from '@eggjs/tegg';
2+
import { z } from 'zod';
3+
4+
import { HelloService } from './HelloService.ts';
5+
6+
const AddArgsSchema = {
7+
a: z.number(),
8+
b: z.number(),
9+
};
10+
11+
@MCPController({ name: 'calc' })
12+
export class CalcMCPController {
13+
@Inject()
14+
private readonly helloService: HelloService;
15+
16+
@MCPTool({ description: 'add two numbers' })
17+
async add(@ToolArgsSchema(AddArgsSchema) args: { a: number; b: number }) {
18+
return {
19+
content: [
20+
{
21+
type: 'text' as const,
22+
text: `${this.helloService.hello('mcp')}: ${args.a + args.b}`,
23+
},
24+
],
25+
};
26+
}
27+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { HTTPController, HTTPMethod, HTTPMethodEnum, HTTPQuery, Inject } from '@eggjs/tegg';
2+
3+
import { HelloService } from './HelloService.ts';
4+
5+
@HTTPController({ path: '/hello' })
6+
export class HelloController {
7+
@Inject()
8+
private readonly helloService: HelloService;
9+
10+
@HTTPMethod({ method: HTTPMethodEnum.GET, path: '/' })
11+
async hello(@HTTPQuery({ name: 'name' }) name: string) {
12+
return { message: this.helloService.hello(name ?? 'service worker') };
13+
}
14+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { ContextProto } from '@eggjs/tegg';
2+
3+
@ContextProto()
4+
export class HelloService {
5+
hello(name: string): string {
6+
return `hello, ${name}`;
7+
}
8+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"name": "helloworld-service-worker-app",
3+
"type": "module",
4+
"eggModule": {
5+
"name": "helloWorldServiceWorker"
6+
}
7+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import path from 'node:path';
2+
import { fileURLToPath } from 'node:url';
3+
4+
import { ServiceWorkerApp } from '@eggjs/service-worker';
5+
6+
const app = new ServiceWorkerApp(path.join(path.dirname(fileURLToPath(import.meta.url)), 'app'));
7+
const server = await app.serve({ port: 7001 });
8+
console.log('service worker listening on http://127.0.0.1:7001');
9+
console.log(' GET /hello/?name=you');
10+
console.log(' POST /mcp/calc (MCP streamable http)');
11+
12+
for (const signal of ['SIGINT', 'SIGTERM'] as const) {
13+
process.once(signal, () => {
14+
console.log(`received ${signal}, shutting down`);
15+
app
16+
.destroy()
17+
.then(() => process.exit(0))
18+
.catch((e) => {
19+
console.error(e);
20+
process.exit(1);
21+
});
22+
});
23+
}
24+
25+
void server;
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"name": "helloworld-service-worker",
3+
"version": "1.0.0",
4+
"private": true,
5+
"description": "Hello World example using the standalone service worker",
6+
"type": "module",
7+
"scripts": {
8+
"start": "node --import=@oxc-node/core/register main.ts",
9+
"test": "vitest run",
10+
"typecheck": "tsgo --noEmit"
11+
},
12+
"dependencies": {
13+
"@eggjs/service-worker": "workspace:*",
14+
"@eggjs/tegg": "workspace:*",
15+
"zod": "catalog:"
16+
},
17+
"devDependencies": {
18+
"@eggjs/tsconfig": "workspace:*",
19+
"@oxc-node/core": "catalog:",
20+
"@types/node": "catalog:",
21+
"typescript": "catalog:",
22+
"vitest": "catalog:"
23+
},
24+
"engines": {
25+
"node": ">=22.18.0"
26+
}
27+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import assert from 'node:assert/strict';
2+
import type { AddressInfo } from 'node:net';
3+
import path from 'node:path';
4+
5+
import { ServiceWorkerApp } from '@eggjs/service-worker';
6+
import { afterAll, beforeAll, describe, it } from 'vitest';
7+
8+
describe('examples/helloworld-service-worker', () => {
9+
let app: ServiceWorkerApp;
10+
let base: string;
11+
12+
beforeAll(async () => {
13+
app = new ServiceWorkerApp(path.join(__dirname, '../app'));
14+
const server = await app.serve();
15+
const { address, port } = server.address() as AddressInfo;
16+
base = `http://${address}:${port}`;
17+
});
18+
19+
afterAll(async () => {
20+
await app.destroy();
21+
});
22+
23+
it('should say hello over http', async () => {
24+
const res = await fetch(`${base}/hello/?name=egg`);
25+
assert.deepEqual(await res.json(), { message: 'hello, egg' });
26+
});
27+
28+
it('should serve the calc MCP tool', async () => {
29+
const res = await fetch(`${base}/mcp/calc/stream`, {
30+
method: 'POST',
31+
headers: {
32+
accept: 'application/json, text/event-stream',
33+
'content-type': 'application/json',
34+
},
35+
body: JSON.stringify({
36+
jsonrpc: '2.0',
37+
id: 1,
38+
method: 'tools/call',
39+
params: { name: 'add', arguments: { a: 1, b: 41 } },
40+
}),
41+
});
42+
assert.equal(res.status, 200);
43+
const text = await res.text();
44+
assert.match(text, /hello, mcp: 42/);
45+
});
46+
});
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"extends": "@eggjs/tsconfig"
3+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { defineProject } from 'vitest/config';
2+
3+
export default defineProject({
4+
test: {
5+
include: ['test/**/*.test.ts'],
6+
},
7+
});

0 commit comments

Comments
 (0)