create-nwire 0.12.1 → 0.13.1
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/dist/index.d.ts +1 -1
- package/dist/index.js +6 -2
- package/package.json +1 -1
- package/templates/enterprise/AGENTS.md +11 -11
- package/templates/enterprise/README.md +15 -10
- package/templates/enterprise/__tests__/auto-moderate.test.ts +22 -6
- package/templates/enterprise/__tests__/submit-flow.test.ts +22 -6
- package/templates/enterprise/app/api.ts +5 -1
- package/templates/enterprise/app/app.ts +23 -15
- package/templates/enterprise/app/main.ts +20 -45
- package/templates/enterprise/config/app.ts +18 -0
- package/templates/enterprise/config/env.ts +32 -0
- package/templates/enterprise/config/http.ts +15 -0
- package/templates/enterprise/modules/posts/actions/approve-post.ts +4 -3
- package/templates/enterprise/modules/posts/actions/reject-post.ts +3 -3
- package/templates/enterprise/modules/posts/actions/submit-post.ts +4 -3
- package/templates/enterprise/modules/posts/events/post-was-approved.ts +1 -1
- package/templates/enterprise/modules/posts/events/post-was-rejected.ts +2 -1
- package/templates/enterprise/modules/posts/events/post-was-submitted.ts +2 -1
- package/templates/enterprise/modules/posts/projections/queue-dashboard.ts +34 -27
- package/templates/enterprise/modules/posts/queries/posts-by-author.ts +7 -5
- package/templates/enterprise/modules/posts/routes/approve-post.ts +3 -3
- package/templates/enterprise/modules/posts/routes/get-post.ts +3 -3
- package/templates/enterprise/modules/posts/routes/list-queue.ts +4 -4
- package/templates/enterprise/modules/posts/routes/posts-by-author.ts +28 -0
- package/templates/enterprise/modules/posts/routes/reject-post.ts +3 -3
- package/templates/enterprise/modules/posts/routes/submit-post.ts +3 -3
- package/templates/enterprise/modules/posts/workflows/auto-moderate.ts +3 -3
- package/templates/enterprise/package.json +10 -11
- package/templates/enterprise/tsconfig.json +1 -1
- package/templates/mcp/AGENTS.md +73 -0
- package/templates/mcp/__tests__/mcp-server.test.ts +205 -0
- package/templates/mcp/_gitignore +5 -0
- package/templates/mcp/_npmrc +4 -0
- package/templates/mcp/_pnpm-workspace.yaml +12 -0
- package/templates/mcp/app/app.ts +24 -0
- package/templates/mcp/app/main.ts +197 -0
- package/templates/mcp/app/store/facts-store.ts +17 -0
- package/templates/mcp/app/tools/add-fact.ts +27 -0
- package/templates/mcp/app/tools/list-facts.ts +17 -0
- package/templates/mcp/app/tools/lookup-fact.ts +28 -0
- package/templates/mcp/app/tools.ts +19 -0
- package/templates/mcp/config/app.ts +16 -0
- package/templates/mcp/config/env.ts +27 -0
- package/templates/mcp/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/templates/mcp/package.json +25 -0
- package/templates/mcp/tsconfig.json +15 -0
- package/templates/mcp/vitest.config.ts +8 -0
- package/templates/minimal/AGENTS.md +16 -9
- package/templates/minimal/README.md +20 -10
- package/templates/minimal/__tests__/hello.test.ts +3 -3
- package/templates/minimal/app/app.ts +21 -0
- package/templates/minimal/app/main.ts +15 -24
- package/templates/minimal/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/templates/minimal/package.json +7 -8
- package/templates/service/AGENTS.md +13 -11
- package/templates/service/README.md +5 -3
- package/templates/service/__tests__/todo-api.test.ts +5 -5
- package/templates/service/app/app.ts +25 -0
- package/templates/service/app/main.ts +20 -30
- package/templates/service/app/store/todo-store.ts +6 -22
- package/templates/service/config/app.ts +18 -0
- package/templates/service/config/env.ts +32 -0
- package/templates/service/config/http.ts +15 -0
- package/templates/service/package.json +8 -9
- package/templates/service/tsconfig.json +1 -1
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App — the composition root.
|
|
3
|
+
*
|
|
4
|
+
* `createApp` constructs the container and wires all MCP tools in this
|
|
5
|
+
* module body. The exported `app` is COMPLETE on import — no caller needs
|
|
6
|
+
* to wire or start it. No ports are bound here; that is `main.ts`'s job.
|
|
7
|
+
*
|
|
8
|
+
* Tests import `{ app }` from this file and drive it with `mcpAdapter()`
|
|
9
|
+
* directly — never import `main.ts` from tests, it owns the stdio loop
|
|
10
|
+
* and will hang the runner.
|
|
11
|
+
*
|
|
12
|
+
* Graduate: add `@nwire/mongo` or `@nwire/drizzle` as a plugin here when
|
|
13
|
+
* the in-memory store outgrows its welcome. The tool handlers keep calling
|
|
14
|
+
* `ctx.resolve("db")`; nothing else changes.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { createApp } from "@nwire/app";
|
|
18
|
+
import { tools } from "./tools";
|
|
19
|
+
|
|
20
|
+
export const app = createApp({ appName: "{{PROJECT_NAME}}" });
|
|
21
|
+
|
|
22
|
+
for (const { binding, handler } of tools) {
|
|
23
|
+
app.wire(binding, handler);
|
|
24
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Entry — the only file that owns side effects.
|
|
3
|
+
*
|
|
4
|
+
* Boots the nwire app under the MCP adapter, then runs the JSON-RPC
|
|
5
|
+
* framing loop so AI clients (Claude, Cursor, IDE plugins) can drive this
|
|
6
|
+
* server over stdio.
|
|
7
|
+
*
|
|
8
|
+
* ┌─ STDOUT IS THE JSON-RPC CHANNEL ─────────────────────────────────┐
|
|
9
|
+
* │ │
|
|
10
|
+
* │ Every byte written to stdout MUST be a complete, newline-framed │
|
|
11
|
+
* │ JSON-RPC message. Non-JSON noise (console.log, banners, debug │
|
|
12
|
+
* │ text) breaks every MCP client silently. │
|
|
13
|
+
* │ │
|
|
14
|
+
* │ Log to stderr: console.error("msg") or process.stderr.write() │
|
|
15
|
+
* │ NEVER: console.log() process.stdout.write(plain text) │
|
|
16
|
+
* └────────────────────────────────────────────────────────────────────┘
|
|
17
|
+
*
|
|
18
|
+
* The mcpAdapter boots the nwire runtime so tools run through the normal
|
|
19
|
+
* dispatch path (validation, DI, observability). The stdio loop below
|
|
20
|
+
* translates JSON-RPC calls into adapter invocations.
|
|
21
|
+
*
|
|
22
|
+
* Tests import `{ app }` from `./app` and bypass this file entirely.
|
|
23
|
+
* Never import `main.ts` from a test — the stdio loop holds the process
|
|
24
|
+
* open and hangs the runner.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { endpoint } from "@nwire/endpoint";
|
|
28
|
+
import { mcpAdapter } from "@nwire/mcp";
|
|
29
|
+
import { appConfig } from "../config/app";
|
|
30
|
+
import { app } from "./app";
|
|
31
|
+
|
|
32
|
+
const cfg = appConfig();
|
|
33
|
+
|
|
34
|
+
const mcp = mcpAdapter();
|
|
35
|
+
|
|
36
|
+
await endpoint(cfg.name, { banner: false, probes: { enabled: false } })
|
|
37
|
+
.use(mcp)
|
|
38
|
+
.mount(app)
|
|
39
|
+
.run();
|
|
40
|
+
|
|
41
|
+
// ─── JSON-RPC 2.0 stdio framing ─────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
const SERVER_NAME = cfg.name;
|
|
44
|
+
const SERVER_VERSION = "0.0.0";
|
|
45
|
+
|
|
46
|
+
type JsonRpcResponse = {
|
|
47
|
+
jsonrpc: "2.0";
|
|
48
|
+
id: string | number | null;
|
|
49
|
+
result?: unknown;
|
|
50
|
+
error?: { code: number; message: string };
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const send = (msg: JsonRpcResponse): void => {
|
|
54
|
+
process.stdout.write(JSON.stringify(msg) + "\n");
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const log = (text: string): void => {
|
|
58
|
+
process.stderr.write(`[${SERVER_NAME}] ${text}\n`);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
log("MCP server ready");
|
|
62
|
+
|
|
63
|
+
let buffer = "";
|
|
64
|
+
|
|
65
|
+
process.stdin.setEncoding("utf8");
|
|
66
|
+
process.stdin.on("data", (chunk: string) => {
|
|
67
|
+
buffer += chunk;
|
|
68
|
+
const lines = buffer.split(/\r?\n/);
|
|
69
|
+
buffer = lines.pop() ?? "";
|
|
70
|
+
for (const line of lines) {
|
|
71
|
+
if (!line.trim()) continue;
|
|
72
|
+
void handleLine(line);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
process.stdin.on("end", () => {
|
|
77
|
+
log("stdin closed — exiting");
|
|
78
|
+
process.exit(0);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
async function handleLine(line: string): Promise<void> {
|
|
82
|
+
let req: { jsonrpc: string; id?: unknown; method: string; params?: Record<string, unknown> };
|
|
83
|
+
try {
|
|
84
|
+
req = JSON.parse(line) as typeof req;
|
|
85
|
+
} catch {
|
|
86
|
+
send({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error" } });
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Notifications (no id, or notifications/* methods) must not be answered.
|
|
91
|
+
const isNotification = req.id === undefined || String(req.method).startsWith("notifications/");
|
|
92
|
+
if (isNotification) return;
|
|
93
|
+
|
|
94
|
+
const id = (req.id as string | number | null) ?? null;
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
switch (req.method) {
|
|
98
|
+
case "initialize":
|
|
99
|
+
send({
|
|
100
|
+
jsonrpc: "2.0",
|
|
101
|
+
id,
|
|
102
|
+
result: {
|
|
103
|
+
protocolVersion: "2024-11-05",
|
|
104
|
+
serverInfo: { name: SERVER_NAME, version: SERVER_VERSION },
|
|
105
|
+
capabilities: { tools: { listChanged: false } },
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
break;
|
|
109
|
+
|
|
110
|
+
case "tools/list": {
|
|
111
|
+
const listed = mcp.list().map((t) => ({
|
|
112
|
+
name: t.name,
|
|
113
|
+
description: t.description ?? "",
|
|
114
|
+
inputSchema: t.inputSchema
|
|
115
|
+
? zodToJsonSchema(t.inputSchema)
|
|
116
|
+
: { type: "object", properties: {}, additionalProperties: false },
|
|
117
|
+
}));
|
|
118
|
+
send({ jsonrpc: "2.0", id, result: { tools: listed } });
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
case "tools/call": {
|
|
123
|
+
const name = req.params?.name as string | undefined;
|
|
124
|
+
const args = (req.params?.arguments ?? {}) as Record<string, unknown>;
|
|
125
|
+
if (!name) {
|
|
126
|
+
send({
|
|
127
|
+
jsonrpc: "2.0",
|
|
128
|
+
id,
|
|
129
|
+
error: { code: -32602, message: "tools/call requires `name`" },
|
|
130
|
+
});
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
const result = await mcp.call(name, args);
|
|
135
|
+
send({
|
|
136
|
+
jsonrpc: "2.0",
|
|
137
|
+
id,
|
|
138
|
+
result: {
|
|
139
|
+
content: [{ type: "text", text: JSON.stringify(result) }],
|
|
140
|
+
isError: false,
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
} catch (err) {
|
|
144
|
+
send({
|
|
145
|
+
jsonrpc: "2.0",
|
|
146
|
+
id,
|
|
147
|
+
result: {
|
|
148
|
+
content: [{ type: "text", text: (err as Error).message }],
|
|
149
|
+
isError: true,
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
default:
|
|
157
|
+
send({
|
|
158
|
+
jsonrpc: "2.0",
|
|
159
|
+
id,
|
|
160
|
+
error: { code: -32601, message: `unknown method "${req.method}"` },
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
} catch (err) {
|
|
164
|
+
send({ jsonrpc: "2.0", id, error: { code: -32603, message: (err as Error).message } });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Minimal Zod v4 → JSON Schema converter for the tools/list response.
|
|
170
|
+
* Handles the shapes the MCP tools in this template actually use —
|
|
171
|
+
* object with string/number properties. Extend as you add new types.
|
|
172
|
+
*
|
|
173
|
+
* Zod v4 uses `_def.type` (not `_def.typeName`) and `_def.shape` is a
|
|
174
|
+
* plain object (not a getter function).
|
|
175
|
+
*/
|
|
176
|
+
function zodToJsonSchema(schema: { _def?: unknown }): unknown {
|
|
177
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
178
|
+
const def = (schema as any)._def;
|
|
179
|
+
if (!def) return { type: "object", additionalProperties: true };
|
|
180
|
+
|
|
181
|
+
if (def.type === "object") {
|
|
182
|
+
const props: Record<string, unknown> = {};
|
|
183
|
+
const required: string[] = [];
|
|
184
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
185
|
+
for (const [k, v] of Object.entries(def.shape as Record<string, any>)) {
|
|
186
|
+
props[k] = zodToJsonSchema(v);
|
|
187
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
188
|
+
if ((v as any)._def?.type !== "optional") required.push(k);
|
|
189
|
+
}
|
|
190
|
+
return { type: "object", properties: props, required, additionalProperties: false };
|
|
191
|
+
}
|
|
192
|
+
if (def.type === "string") return { type: "string" };
|
|
193
|
+
if (def.type === "number") return { type: "number" };
|
|
194
|
+
if (def.type === "boolean") return { type: "boolean" };
|
|
195
|
+
if (def.type === "optional") return zodToJsonSchema(def.innerType);
|
|
196
|
+
return { type: "object", additionalProperties: true };
|
|
197
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory knowledge base — a simple mutable array that all tool handlers
|
|
3
|
+
* share within the process. Replace this with `@nwire/drizzle` or
|
|
4
|
+
* `@nwire/mongo` when you need persistence across restarts.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface Fact {
|
|
8
|
+
id: number;
|
|
9
|
+
topic: string;
|
|
10
|
+
body: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Seed data — something concrete so `list-facts` returns content on first run. */
|
|
14
|
+
export const facts: Fact[] = [
|
|
15
|
+
{ id: 1, topic: "nwire", body: "One handler, every transport." },
|
|
16
|
+
{ id: 2, topic: "mcp", body: "Model Context Protocol — AI clients call tools over JSON-RPC." },
|
|
17
|
+
];
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* add-fact — store a new fact in the knowledge base.
|
|
3
|
+
*
|
|
4
|
+
* The AI client supplies a topic and body; the tool persists it so future
|
|
5
|
+
* `lookup-fact` calls can surface it. Duplicate topics are allowed —
|
|
6
|
+
* the knowledge base treats each entry as an independent note.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
import { tool } from "@nwire/wires/mcp";
|
|
11
|
+
import { facts } from "../store/facts-store";
|
|
12
|
+
|
|
13
|
+
const AddFactInput = z.object({
|
|
14
|
+
topic: z.string().min(1).max(100).describe("Short label for this fact"),
|
|
15
|
+
body: z.string().min(1).max(1000).describe("The fact content"),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export const addFactTool = tool("add-fact", {
|
|
19
|
+
description: "Add a new fact to the knowledge base. Returns the stored entry.",
|
|
20
|
+
input: AddFactInput,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export const addFactHandler = async (input: z.infer<typeof AddFactInput>) => {
|
|
24
|
+
const entry = { id: facts.length + 1, topic: input.topic, body: input.body };
|
|
25
|
+
facts.push(entry);
|
|
26
|
+
return { stored: entry, total: facts.length };
|
|
27
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* list-facts — enumerate every fact in the knowledge base.
|
|
3
|
+
*
|
|
4
|
+
* Useful when the AI client wants a full picture of what is stored before
|
|
5
|
+
* deciding which facts to update or which lookup term to use.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { tool } from "@nwire/wires/mcp";
|
|
9
|
+
import { facts } from "../store/facts-store";
|
|
10
|
+
|
|
11
|
+
export const listFactsTool = tool("list-facts", {
|
|
12
|
+
description: "Return all facts currently stored in the knowledge base.",
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export const listFactsHandler = async () => {
|
|
16
|
+
return { count: facts.length, facts };
|
|
17
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lookup-fact — find a fact by keyword from the in-memory knowledge base.
|
|
3
|
+
*
|
|
4
|
+
* The AI client submits a search term; the tool scans all stored facts
|
|
5
|
+
* for matches and returns the hits. Zero results is a valid (empty) answer,
|
|
6
|
+
* not an error.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
import { tool } from "@nwire/wires/mcp";
|
|
11
|
+
import { facts } from "../store/facts-store";
|
|
12
|
+
|
|
13
|
+
const LookupInput = z.object({
|
|
14
|
+
query: z.string().min(1).describe("Keyword or phrase to search for"),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export const lookupFactTool = tool("lookup-fact", {
|
|
18
|
+
description: "Search the knowledge base by keyword and return matching facts.",
|
|
19
|
+
input: LookupInput,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export const lookupFactHandler = async (input: z.infer<typeof LookupInput>) => {
|
|
23
|
+
const term = input.query.toLowerCase();
|
|
24
|
+
const matches = facts.filter(
|
|
25
|
+
(f) => f.topic.toLowerCase().includes(term) || f.body.toLowerCase().includes(term),
|
|
26
|
+
);
|
|
27
|
+
return { query: input.query, count: matches.length, results: matches };
|
|
28
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tools — the MCP surface this app exposes.
|
|
3
|
+
*
|
|
4
|
+
* Each tool file under `./tools/` exports a `{name}Tool` (binding) +
|
|
5
|
+
* `{name}Handler` (function). `tools` is the flat array fed into
|
|
6
|
+
* `app.wire(...)` in `app.ts`.
|
|
7
|
+
*
|
|
8
|
+
* Adding a tool = one new file + one entry in this array.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { lookupFactTool, lookupFactHandler } from "./tools/lookup-fact";
|
|
12
|
+
import { addFactTool, addFactHandler } from "./tools/add-fact";
|
|
13
|
+
import { listFactsTool, listFactsHandler } from "./tools/list-facts";
|
|
14
|
+
|
|
15
|
+
export const tools = [
|
|
16
|
+
{ binding: lookupFactTool, handler: lookupFactHandler },
|
|
17
|
+
{ binding: addFactTool, handler: addFactHandler },
|
|
18
|
+
{ binding: listFactsTool, handler: listFactsHandler },
|
|
19
|
+
] as const;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App-level settings — the process identity and lifecycle budgets the
|
|
3
|
+
* endpoint owns. A function of the typed `env`, so the same config file
|
|
4
|
+
* reads differently per environment without an `if` in sight.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { env, type Env } from "./env";
|
|
8
|
+
|
|
9
|
+
export const appConfig = (e: Env = env) => ({
|
|
10
|
+
/** Endpoint + app name — shows in logs and telemetry. */
|
|
11
|
+
name: "{{PROJECT_NAME}}",
|
|
12
|
+
/** Print the boot banner outside of tests. */
|
|
13
|
+
banner: e.NODE_ENV !== "test",
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export type AppConfig = ReturnType<typeof appConfig>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed environment — read `process.env` once, through a schema, and never
|
|
3
|
+
* touch it again from app code. Missing or malformed values fail loudly at
|
|
4
|
+
* boot with a readable message, not three layers deep at first request.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
|
|
9
|
+
const EnvSchema = z.object({
|
|
10
|
+
NODE_ENV: z.enum(["development", "test", "production"]).default("development"),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export type Env = z.output<typeof EnvSchema>;
|
|
14
|
+
|
|
15
|
+
export function loadEnv(source: NodeJS.ProcessEnv = process.env): Env {
|
|
16
|
+
const parsed = EnvSchema.safeParse(source);
|
|
17
|
+
if (!parsed.success) {
|
|
18
|
+
const summary = parsed.error.issues
|
|
19
|
+
.map((i) => ` ${i.path.join(".") || "(root)"}: ${i.message}`)
|
|
20
|
+
.join("\n");
|
|
21
|
+
throw new Error(`Invalid environment:\n${summary}`);
|
|
22
|
+
}
|
|
23
|
+
return parsed.data;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The validated environment, loaded once. */
|
|
27
|
+
export const env: Env = loadEnv();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":"4.1.6","results":[[":__tests__/mcp-server.test.ts",{"duration":316.55000000000007,"failed":false}]]}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{PROJECT_NAME}}",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"start": "node --import tsx/esm app/main.ts",
|
|
8
|
+
"dev": "node --import tsx/esm --watch app/main.ts",
|
|
9
|
+
"test": "vitest run",
|
|
10
|
+
"typecheck": "tsc --noEmit"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@nwire/app": "^0.13.1",
|
|
14
|
+
"@nwire/endpoint": "^0.13.1",
|
|
15
|
+
"@nwire/mcp": "^0.13.1",
|
|
16
|
+
"@nwire/wires": "^0.13.1",
|
|
17
|
+
"zod": "^4.0.0"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@types/node": "^22.19.9",
|
|
21
|
+
"tsx": "^4.19.4",
|
|
22
|
+
"typescript": "^5.9.0",
|
|
23
|
+
"vitest": "^4.0.18"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"strict": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"forceConsistentCasingInFileNames": true,
|
|
11
|
+
"noEmit": true,
|
|
12
|
+
"types": ["node"]
|
|
13
|
+
},
|
|
14
|
+
"include": ["app/**/*", "config/**/*", "__tests__/**/*"]
|
|
15
|
+
}
|
|
@@ -9,22 +9,29 @@ handler never knows which transport called it.
|
|
|
9
9
|
```ts
|
|
10
10
|
import { createApp } from "@nwire/app";
|
|
11
11
|
import { endpoint } from "@nwire/endpoint";
|
|
12
|
-
import {
|
|
12
|
+
import { post } from "@nwire/wires/http";
|
|
13
13
|
import { httpKoa } from "@nwire/koa";
|
|
14
|
+
import { z } from "zod";
|
|
14
15
|
|
|
15
16
|
const app = createApp({ appName: "app" });
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
// The route declares its schema; the handler receives the validated input.
|
|
18
|
+
app.wire(post("/hello", { body: z.object({ name: z.string().min(1) }) }), async (input) => ({
|
|
19
|
+
message: `Hello, ${input.name}!`,
|
|
20
|
+
}));
|
|
21
|
+
await endpoint("app", { port: Number(process.env.PORT) || 3000 })
|
|
22
|
+
.use(httpKoa())
|
|
23
|
+
.mount(app)
|
|
24
|
+
.run();
|
|
18
25
|
```
|
|
19
26
|
|
|
20
27
|
## What's installed (don't import beyond these)
|
|
21
28
|
|
|
22
|
-
| Primitive
|
|
23
|
-
|
|
24
|
-
| `createApp`, `definePlugin`
|
|
25
|
-
| `endpoint`
|
|
29
|
+
| Primitive | Package |
|
|
30
|
+
| -------------------------------- | ------------------- |
|
|
31
|
+
| `createApp`, `definePlugin` | `@nwire/app` |
|
|
32
|
+
| `endpoint` | `@nwire/endpoint` |
|
|
26
33
|
| `get` `post` `put` `patch` `del` | `@nwire/wires/http` |
|
|
27
|
-
| `httpKoa`
|
|
34
|
+
| `httpKoa` | `@nwire/koa` |
|
|
28
35
|
|
|
29
36
|
This tier is HTTP-only. Add capabilities by installing the package first:
|
|
30
37
|
`@nwire/handler` for `defineResource`/`defineError`, `@nwire/messages` for
|
|
@@ -33,5 +40,5 @@ This tier is HTTP-only. Add capabilities by installing the package first:
|
|
|
33
40
|
|
|
34
41
|
## Commands
|
|
35
42
|
|
|
36
|
-
- `pnpm dev` —
|
|
43
|
+
- `pnpm dev` — wire + Studio on http://localhost:4000. `pnpm test` — tests.
|
|
37
44
|
- `pnpm doctor` — health-check the setup. `pnpm studio` — trace console.
|
|
@@ -16,8 +16,10 @@ pnpm dev
|
|
|
16
16
|
|
|
17
17
|
## Try
|
|
18
18
|
|
|
19
|
+
`pnpm dev` serves the wire + Studio on http://localhost:4000.
|
|
20
|
+
|
|
19
21
|
```bash
|
|
20
|
-
curl -X POST http://localhost:
|
|
22
|
+
curl -X POST http://localhost:4000/hello \
|
|
21
23
|
-H "content-type: application/json" \
|
|
22
24
|
-d '{"name":"Alice"}'
|
|
23
25
|
```
|
|
@@ -33,8 +35,9 @@ pnpm test
|
|
|
33
35
|
```
|
|
34
36
|
{{PROJECT_NAME}}/
|
|
35
37
|
├── app/
|
|
36
|
-
│ ├── main.ts ← endpoint().
|
|
37
|
-
│ ├──
|
|
38
|
+
│ ├── main.ts ← endpoint().use(httpKoa()).mount(app).run()
|
|
39
|
+
│ ├── app.ts ← createApp().wire(...) — the app value
|
|
40
|
+
│ ├── api.ts ← wires[] (route + handler pairs)
|
|
38
41
|
│ └── routes/
|
|
39
42
|
│ └── hello.ts ← POST /hello — route + handler pair
|
|
40
43
|
└── __tests__/
|
|
@@ -51,13 +54,20 @@ pnpm test
|
|
|
51
54
|
|
|
52
55
|
## What you get free
|
|
53
56
|
|
|
54
|
-
| Feature
|
|
55
|
-
|
|
|
56
|
-
| Zod request validation
|
|
57
|
-
|
|
|
58
|
-
|
|
|
59
|
-
|
|
60
|
-
|
|
57
|
+
| Feature | Comes from |
|
|
58
|
+
| ------------------------------------ | ----------------- |
|
|
59
|
+
| Zod request validation | `@nwire/koa` |
|
|
60
|
+
| Graceful SIGTERM drain | `@nwire/endpoint` |
|
|
61
|
+
| K8s probes (port 9400/`$PROBE_PORT`) | `@nwire/endpoint` |
|
|
62
|
+
|
|
63
|
+
OpenAPI + Scalar docs are one opt-in away — pass them to the transport:
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
endpoint("{{PROJECT_NAME}}")
|
|
67
|
+
.use(httpKoa({ openapi: { auto: true }, docs: true })) // /openapi.json + /docs
|
|
68
|
+
.mount(app)
|
|
69
|
+
.run();
|
|
70
|
+
```
|
|
61
71
|
|
|
62
72
|
## Grow up
|
|
63
73
|
|
|
@@ -3,19 +3,19 @@
|
|
|
3
3
|
* empty `name` is rejected with a structured validation error.
|
|
4
4
|
*
|
|
5
5
|
* Boots the app through endpoint + httpKoa on an ephemeral port.
|
|
6
|
+
* Imports `{ app }` from `../app/app` (the pure value). Never imports
|
|
7
|
+
* `main.ts` — that file boots a real server and hangs the test runner.
|
|
6
8
|
*/
|
|
7
9
|
|
|
8
10
|
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
9
11
|
import { endpoint } from "@nwire/endpoint";
|
|
10
12
|
import { httpKoa } from "@nwire/koa";
|
|
11
|
-
import {
|
|
13
|
+
import { app } from "../app/app";
|
|
12
14
|
|
|
13
15
|
let running: Awaited<ReturnType<ReturnType<typeof endpoint>["run"]>>;
|
|
14
|
-
let app: ReturnType<typeof buildApp>;
|
|
15
16
|
let url: string;
|
|
16
17
|
|
|
17
18
|
beforeAll(async () => {
|
|
18
|
-
app = buildApp();
|
|
19
19
|
const koa = httpKoa({ port: 0 });
|
|
20
20
|
running = await endpoint("hello-test", {
|
|
21
21
|
exitOnShutdown: false,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App — the composition root.
|
|
3
|
+
*
|
|
4
|
+
* `createApp` constructs the container and wires all routes in this module
|
|
5
|
+
* body. The exported `app` is COMPLETE on import — no caller needs to wire
|
|
6
|
+
* or start it. No ports are bound here; that is `main.ts`'s job.
|
|
7
|
+
*
|
|
8
|
+
* Tests import `{ app }` from this file and build an isolated endpoint.
|
|
9
|
+
* Never import `main.ts` from tests: it boots a server and hangs the runner.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { createApp } from "@nwire/app";
|
|
13
|
+
import { wires } from "./api";
|
|
14
|
+
|
|
15
|
+
const _app = createApp({ appName: "{{PROJECT_NAME}}" });
|
|
16
|
+
|
|
17
|
+
for (const { binding, handler } of wires) {
|
|
18
|
+
_app.wire(binding, handler);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const app = _app;
|
|
@@ -1,33 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Entry —
|
|
2
|
+
* Entry — the only file that boots a real HTTP server.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* app.wire(...) — pair a binding with its handler
|
|
6
|
-
* endpoint().use(...) — install a transport adapter (HTTP, queue, MCP, …)
|
|
7
|
-
* .mount(app).run() — boot the app under the adapters; ready for traffic
|
|
4
|
+
* pnpm dev # wire + Studio on http://localhost:4000
|
|
8
5
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
6
|
+
* curl -X POST http://localhost:4000/hello \
|
|
7
|
+
* -H "content-type: application/json" \
|
|
8
|
+
* -d '{"name":"Alice"}'
|
|
12
9
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
10
|
+
* `app` is a pure value (no side effects on import — see `./app`). This
|
|
11
|
+
* file adds the one side effect: binding a port and serving traffic.
|
|
12
|
+
* Tests import `{ app }` from `./app` and build their own ephemeral
|
|
13
|
+
* endpoint — never import this file from tests or it boots a server and
|
|
14
|
+
* hangs the runner.
|
|
17
15
|
*/
|
|
18
16
|
|
|
19
|
-
import { createApp } from "@nwire/app";
|
|
20
17
|
import { endpoint } from "@nwire/endpoint";
|
|
21
18
|
import { httpKoa } from "@nwire/koa";
|
|
22
|
-
import {
|
|
19
|
+
import { app } from "./app";
|
|
23
20
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
return app;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const app = buildApp();
|
|
33
|
-
await endpoint("{{PROJECT_NAME}}", { port: 3000 }).use(httpKoa()).mount(app).run();
|
|
21
|
+
await endpoint("{{PROJECT_NAME}}", { port: Number(process.env.PORT) || 3000 })
|
|
22
|
+
.use(httpKoa())
|
|
23
|
+
.mount(app)
|
|
24
|
+
.run();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":"4.1.6","results":[[":__tests__/hello.test.ts",{"duration":38.90391699999998,"failed":false}]]}
|
|
@@ -4,24 +4,23 @@
|
|
|
4
4
|
"private": true,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"dev": "
|
|
7
|
+
"dev": "nwire dev",
|
|
8
8
|
"test": "vitest run",
|
|
9
9
|
"doctor": "nwire doctor",
|
|
10
10
|
"studio": "nwire studio",
|
|
11
11
|
"cache": "nwire cache"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@nwire/app": "^0.
|
|
15
|
-
"@nwire/endpoint": "^0.
|
|
16
|
-
"@nwire/koa": "^0.
|
|
17
|
-
"@nwire/wires": "^0.
|
|
14
|
+
"@nwire/app": "^0.13.1",
|
|
15
|
+
"@nwire/endpoint": "^0.13.1",
|
|
16
|
+
"@nwire/koa": "^0.13.1",
|
|
17
|
+
"@nwire/wires": "^0.13.1",
|
|
18
18
|
"zod": "^4.0.0"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
|
+
"@nwire/cli": "^0.13.1",
|
|
21
22
|
"@types/node": "^22.19.9",
|
|
22
23
|
"typescript": "^5.9.0",
|
|
23
|
-
"
|
|
24
|
-
"vitest": "^4.0.18",
|
|
25
|
-
"@nwire/cli": "^0.12.1"
|
|
24
|
+
"vitest": "^4.0.18"
|
|
26
25
|
}
|
|
27
26
|
}
|