@waniwani/kit 0.1.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/LICENSE +21 -0
- package/README.md +566 -0
- package/cli/account.mjs +264 -0
- package/cli/codegen.mjs +1069 -0
- package/cli/framework.mjs +244 -0
- package/cli/index.mjs +563 -0
- package/cli/log.mjs +177 -0
- package/cli/scan.mjs +84 -0
- package/cli/template.mjs +152 -0
- package/cli/tunnel.mjs +140 -0
- package/cli/validate.mjs +248 -0
- package/dist/index.d.ts +113 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/server.d.ts +71 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +212 -0
- package/dist/server.js.map +1 -0
- package/dist/web.d.ts +39 -0
- package/dist/web.d.ts.map +1 -0
- package/dist/web.js +35 -0
- package/dist/web.js.map +1 -0
- package/package.json +88 -0
- package/src/index.ts +138 -0
- package/src/server.ts +285 -0
- package/src/web.tsx +68 -0
package/cli/validate.mjs
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The build check.
|
|
3
|
+
*
|
|
4
|
+
* Structural rules first (cheap, from the filesystem), then the modules are
|
|
5
|
+
* actually imported so a broken export or a flow that fails to compile is
|
|
6
|
+
* reported here rather than as a stack trace at request time.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readFileSync } from "node:fs";
|
|
10
|
+
import { relative } from "node:path";
|
|
11
|
+
|
|
12
|
+
const NAME_RE = /^[a-z0-9](?:[a-z0-9_-]*[a-z0-9])?$/;
|
|
13
|
+
|
|
14
|
+
class Report {
|
|
15
|
+
constructor(root) {
|
|
16
|
+
this.root = root;
|
|
17
|
+
this.errors = [];
|
|
18
|
+
this.warnings = [];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
error(where, message, hint) {
|
|
22
|
+
this.errors.push({ where, message, hint });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
warn(where, message, hint) {
|
|
26
|
+
this.warnings.push({ where, message, hint });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
get ok() {
|
|
30
|
+
return this.errors.length === 0;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function rel(root, file) {
|
|
35
|
+
return relative(root, file) || ".";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Everything the filesystem alone can tell us. */
|
|
39
|
+
function checkStructure(app, report) {
|
|
40
|
+
const { root } = app;
|
|
41
|
+
|
|
42
|
+
if (!app.configFile) {
|
|
43
|
+
report.error(
|
|
44
|
+
"waniwani.config.ts",
|
|
45
|
+
"missing app config",
|
|
46
|
+
"create waniwani.config.ts with `export default defineApp({ name: '...' })`",
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (app.tools.length + app.widgets.length + app.flows.length + app.docs.length === 0) {
|
|
51
|
+
report.error(
|
|
52
|
+
".",
|
|
53
|
+
"this app exposes nothing",
|
|
54
|
+
"add a tool (tools/<name>.ts), a widget (widgets/<name>/), a flow (flows/<name>.ts), or docs (docs/<slug>.md)",
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
for (const widget of app.widgets) {
|
|
59
|
+
const where = rel(root, widget.dir);
|
|
60
|
+
if (!widget.contract) {
|
|
61
|
+
report.error(
|
|
62
|
+
where,
|
|
63
|
+
"missing widget.ts",
|
|
64
|
+
"every widget folder needs a widget.ts with `export default defineWidget({ ... })`",
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
if (!widget.ui) {
|
|
68
|
+
report.error(
|
|
69
|
+
where,
|
|
70
|
+
"missing ui.tsx",
|
|
71
|
+
"every widget folder needs a ui.tsx with a default-exported React component",
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Styling is Tailwind, out of the template's `src/index.css`. Nothing imports
|
|
77
|
+
// an app's own CSS, so a styles.css is a file whose rules never load — and it
|
|
78
|
+
// fails in the worst way, by rendering an unstyled widget rather than an
|
|
79
|
+
// error. Naming it here costs one deletion; missing it costs a debugging
|
|
80
|
+
// session against a bundle that never mentions the file.
|
|
81
|
+
for (const file of app.strayStyles) {
|
|
82
|
+
report.error(
|
|
83
|
+
rel(root, file),
|
|
84
|
+
"app CSS is not bundled — nothing imports this file",
|
|
85
|
+
"style with Tailwind utility classes in ui.tsx; the template's src/index.css carries the @theme tokens and the `dark` variant",
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// A widget's folder name is its MCP tool name and its bundle entry name, so
|
|
90
|
+
// it has to survive both.
|
|
91
|
+
const named = [
|
|
92
|
+
...app.tools.map((t) => ({ kind: "tool", name: t.name, where: rel(root, t.file) })),
|
|
93
|
+
...app.widgets.map((w) => ({ kind: "widget", name: w.name, where: rel(root, w.dir) })),
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
const seen = new Map();
|
|
97
|
+
for (const entry of named) {
|
|
98
|
+
if (!NAME_RE.test(entry.name)) {
|
|
99
|
+
report.error(
|
|
100
|
+
entry.where,
|
|
101
|
+
`"${entry.name}" is not a valid MCP tool name`,
|
|
102
|
+
"use lowercase letters, digits, dashes and underscores",
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
const previous = seen.get(entry.name);
|
|
106
|
+
if (previous) {
|
|
107
|
+
report.error(
|
|
108
|
+
entry.where,
|
|
109
|
+
`name "${entry.name}" is already taken by ${previous.kind} ${previous.where}`,
|
|
110
|
+
"tool and widget names share one namespace — rename one of them",
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
seen.set(entry.name, entry);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Flows point at widgets by name. Catching a typo here beats catching it
|
|
117
|
+
// when a user is halfway through a conversation.
|
|
118
|
+
const widgetNames = new Set(app.widgets.map((w) => w.name));
|
|
119
|
+
for (const flow of app.flows) {
|
|
120
|
+
const source = readFileSync(flow.file, "utf-8");
|
|
121
|
+
for (const match of source.matchAll(/showWidget\(\s*\{[^}]*?tool:\s*["'`]([^"'`]+)["'`]/gs)) {
|
|
122
|
+
const target = match[1];
|
|
123
|
+
if (!widgetNames.has(target)) {
|
|
124
|
+
report.error(
|
|
125
|
+
rel(root, flow.file),
|
|
126
|
+
`showWidget references the widget "${target}", which does not exist`,
|
|
127
|
+
widgetNames.size > 0
|
|
128
|
+
? `known widgets: ${[...widgetNames].join(", ")}`
|
|
129
|
+
: "this app has no widgets/ folder",
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Import each module and check the shape of what it exports. */
|
|
137
|
+
async function checkModules(app, report) {
|
|
138
|
+
const { root } = app;
|
|
139
|
+
|
|
140
|
+
if (app.configFile) {
|
|
141
|
+
const config = await load(app.configFile, rel(root, app.configFile), report);
|
|
142
|
+
if (config && !config.name) {
|
|
143
|
+
report.error(
|
|
144
|
+
rel(root, app.configFile),
|
|
145
|
+
"defineApp() is missing `name`",
|
|
146
|
+
"the MCP server name, e.g. name: 'oney-split-payment'",
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
for (const tool of app.tools) {
|
|
152
|
+
const where = rel(root, tool.file);
|
|
153
|
+
const def = await load(tool.file, where, report);
|
|
154
|
+
if (!def) continue;
|
|
155
|
+
if (typeof def.run !== "function") {
|
|
156
|
+
report.error(where, "tool is missing run()", "export default defineTool({ ..., run })");
|
|
157
|
+
}
|
|
158
|
+
if (!def.description) {
|
|
159
|
+
report.error(
|
|
160
|
+
where,
|
|
161
|
+
"tool is missing a description",
|
|
162
|
+
"the description is how the model decides to call it — say when to use it",
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
if (!def.title) {
|
|
166
|
+
report.warn(where, "tool is missing a title", "titles show up in connector UIs");
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
for (const widget of app.widgets) {
|
|
171
|
+
if (!widget.contract) continue;
|
|
172
|
+
const where = rel(root, widget.contract);
|
|
173
|
+
const def = await load(widget.contract, where, report);
|
|
174
|
+
if (!def) continue;
|
|
175
|
+
if (!def.data || typeof def.data !== "object") {
|
|
176
|
+
report.error(
|
|
177
|
+
where,
|
|
178
|
+
"widget is missing a `data` schema",
|
|
179
|
+
"data is the single schema for input, output, and the component's props",
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
if (!def.description) {
|
|
183
|
+
report.error(
|
|
184
|
+
where,
|
|
185
|
+
"widget is missing a description",
|
|
186
|
+
"the description is how the model decides to show it",
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
for (const flow of app.flows) {
|
|
192
|
+
const where = rel(root, flow.file);
|
|
193
|
+
const def = await load(flow.file, where, report);
|
|
194
|
+
if (!def) continue;
|
|
195
|
+
if (!def.name || !def.config || typeof def.handler !== "function") {
|
|
196
|
+
report.error(
|
|
197
|
+
where,
|
|
198
|
+
"this is not a compiled flow",
|
|
199
|
+
"export default createFlow({ ... }).addEdge(...).compile()",
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* App modules are TypeScript, and they import each other with the `.js`
|
|
207
|
+
* specifiers TypeScript's ESM output requires — `../lib/plans.js` for a file on
|
|
208
|
+
* disk called `plans.ts`. Node's built-in type stripping does not remap those,
|
|
209
|
+
* so validation registers tsx's resolver before importing anything out of the
|
|
210
|
+
* app folder. Bun does the remapping on its own, which is what hid this while
|
|
211
|
+
* the CLI still ran under bun.
|
|
212
|
+
*
|
|
213
|
+
* Registration is global to the process and idempotent here, so it happens once
|
|
214
|
+
* on the first load rather than at startup — `waniwani start` never validates.
|
|
215
|
+
*/
|
|
216
|
+
let resolverRegistered = false;
|
|
217
|
+
async function registerTypeScriptResolver() {
|
|
218
|
+
if (resolverRegistered) return;
|
|
219
|
+
resolverRegistered = true;
|
|
220
|
+
const { register } = await import("tsx/esm/api");
|
|
221
|
+
register();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async function load(file, where, report) {
|
|
225
|
+
try {
|
|
226
|
+
await registerTypeScriptResolver();
|
|
227
|
+
const module = await import(`${file}?t=${Date.now()}`);
|
|
228
|
+
const def = module.default;
|
|
229
|
+
if (!def) {
|
|
230
|
+
report.error(where, "no default export", "the runtime loads this module's default export");
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
return def;
|
|
234
|
+
} catch (error) {
|
|
235
|
+
report.error(where, "failed to load", error instanceof Error ? error.message : String(error));
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export async function validateApp(app) {
|
|
241
|
+
const report = new Report(app.root);
|
|
242
|
+
checkStructure(app, report);
|
|
243
|
+
// Importing broken modules produces noise on top of structural errors.
|
|
244
|
+
if (report.ok) {
|
|
245
|
+
await checkModules(app, report);
|
|
246
|
+
}
|
|
247
|
+
return report;
|
|
248
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Waniwani MCP authoring API.
|
|
3
|
+
*
|
|
4
|
+
* This is everything an app author imports. There is no server bootstrap, no
|
|
5
|
+
* transport, no MCP wiring, no build config in an app repo — the runtime owns
|
|
6
|
+
* all of it (see `./server.ts`) and the CLI generates the glue (see `../cli/`).
|
|
7
|
+
*
|
|
8
|
+
* An app is a folder:
|
|
9
|
+
*
|
|
10
|
+
* waniwani.config.ts defineApp({ ... })
|
|
11
|
+
* tools/<name>.ts export default defineTool({ ... })
|
|
12
|
+
* widgets/<name>/widget.ts export default defineWidget({ ... })
|
|
13
|
+
* widgets/<name>/ui.tsx export default function Component() { ... }
|
|
14
|
+
* flows/<name>.ts export default createFlow({ ... }).compile()
|
|
15
|
+
* docs/<slug>.md searchable knowledge
|
|
16
|
+
*/
|
|
17
|
+
import type { z } from "zod";
|
|
18
|
+
/** A Zod object shape — `{ name: z.string() }`, not `z.object({ ... })`. */
|
|
19
|
+
export type Shape = z.ZodRawShape;
|
|
20
|
+
/** The TypeScript type a `Shape` describes. */
|
|
21
|
+
export type Infer<S extends Shape> = z.infer<z.ZodObject<S>>;
|
|
22
|
+
/**
|
|
23
|
+
* Behavioural hints handed to the host LLM. The runtime translates these into
|
|
24
|
+
* MCP `annotations` and always fills in the `title` that Claude's Connectors
|
|
25
|
+
* Directory requires, so an app repo cannot get that wrong.
|
|
26
|
+
*/
|
|
27
|
+
export type ToolHints = {
|
|
28
|
+
/** The tool only reads. Defaults to `true` for widgets, `false` for tools. */
|
|
29
|
+
readOnly?: boolean;
|
|
30
|
+
/** The tool can destroy data. */
|
|
31
|
+
destructive?: boolean;
|
|
32
|
+
/** The tool reaches out to the open internet. */
|
|
33
|
+
openWorld?: boolean;
|
|
34
|
+
/** Calling twice with the same input has the same effect as calling once. */
|
|
35
|
+
idempotent?: boolean;
|
|
36
|
+
};
|
|
37
|
+
export type AppConfig = {
|
|
38
|
+
/** MCP server name, e.g. `oney-split-payment`. */
|
|
39
|
+
name: string;
|
|
40
|
+
/** Shown to humans in connector UIs. */
|
|
41
|
+
title?: string;
|
|
42
|
+
/** Defaults to the app `package.json` version. */
|
|
43
|
+
version?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Server-level instructions handed to the host LLM once, before any tool
|
|
46
|
+
* call. Tone, guardrails, what this app is for.
|
|
47
|
+
*/
|
|
48
|
+
instructions?: string;
|
|
49
|
+
};
|
|
50
|
+
export declare function defineApp(config: AppConfig): AppConfig;
|
|
51
|
+
/**
|
|
52
|
+
* What a tool handler may return. A string becomes the model-facing text; an
|
|
53
|
+
* object becomes `structuredContent` plus a JSON text fallback. Returning a
|
|
54
|
+
* full MCP `CallToolResult` is still allowed for the rare tool that needs it.
|
|
55
|
+
*/
|
|
56
|
+
export type ToolResult = string | Record<string, unknown> | {
|
|
57
|
+
content: Array<{
|
|
58
|
+
type: "text";
|
|
59
|
+
text: string;
|
|
60
|
+
}>;
|
|
61
|
+
structuredContent?: Record<string, unknown>;
|
|
62
|
+
};
|
|
63
|
+
export type ToolDefinition<S extends Shape = Shape, R extends ToolResult = ToolResult> = {
|
|
64
|
+
title: string;
|
|
65
|
+
/** LLM-facing. When to call this, and what it does. */
|
|
66
|
+
description: string;
|
|
67
|
+
input?: S;
|
|
68
|
+
output?: Shape;
|
|
69
|
+
hints?: ToolHints;
|
|
70
|
+
run: (input: Infer<S>) => R | Promise<R>;
|
|
71
|
+
};
|
|
72
|
+
export declare function defineTool<S extends Shape, R extends ToolResult>(def: ToolDefinition<S, R>): ToolDefinition<S, R>;
|
|
73
|
+
export type WidgetCsp = {
|
|
74
|
+
/** Domains the widget may `fetch()`. */
|
|
75
|
+
connectDomains?: string[];
|
|
76
|
+
/** Domains the widget may load images/fonts/scripts from. */
|
|
77
|
+
resourceDomains?: string[];
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* A widget contract. This file is imported by *both* the server and the
|
|
81
|
+
* browser bundle, so it must stay free of React and CSS — the component lives
|
|
82
|
+
* next to it in `ui.tsx`.
|
|
83
|
+
*
|
|
84
|
+
* `data` is the single schema for the widget: it is the tool's input schema,
|
|
85
|
+
* its output schema, and the type `useWidget()` hands the component. One
|
|
86
|
+
* schema, so server and UI cannot drift.
|
|
87
|
+
*/
|
|
88
|
+
export type WidgetDefinition<S extends Shape = Shape> = {
|
|
89
|
+
title: string;
|
|
90
|
+
/** LLM-facing. When to show this widget, and how to frame it. */
|
|
91
|
+
description: string;
|
|
92
|
+
data: S;
|
|
93
|
+
hints?: ToolHints;
|
|
94
|
+
csp?: WidgetCsp;
|
|
95
|
+
/**
|
|
96
|
+
* Text handed to the model alongside the rendered widget. Use it to tell
|
|
97
|
+
* the model what NOT to repeat, and what to wait for.
|
|
98
|
+
*/
|
|
99
|
+
llmText?: (data: Infer<S>) => string;
|
|
100
|
+
/**
|
|
101
|
+
* Optional server-side loader, for widgets whose data comes from an API
|
|
102
|
+
* rather than from the model. Defaults to echoing the input through.
|
|
103
|
+
*/
|
|
104
|
+
load?: (input: Infer<S>) => Infer<S> | Promise<Infer<S>>;
|
|
105
|
+
};
|
|
106
|
+
export declare function defineWidget<S extends Shape>(def: WidgetDefinition<S>): WidgetDefinition<S>;
|
|
107
|
+
/** A `docs/<slug>.md` file, parsed at build time and inlined into the bundle. */
|
|
108
|
+
export type DocEntry = {
|
|
109
|
+
slug: string;
|
|
110
|
+
title: string;
|
|
111
|
+
body: string;
|
|
112
|
+
};
|
|
113
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Waniwani MCP authoring API.
|
|
3
|
+
*
|
|
4
|
+
* This is everything an app author imports. There is no server bootstrap, no
|
|
5
|
+
* transport, no MCP wiring, no build config in an app repo — the runtime owns
|
|
6
|
+
* all of it (see `./server.ts`) and the CLI generates the glue (see `../cli/`).
|
|
7
|
+
*
|
|
8
|
+
* An app is a folder:
|
|
9
|
+
*
|
|
10
|
+
* waniwani.config.ts defineApp({ ... })
|
|
11
|
+
* tools/<name>.ts export default defineTool({ ... })
|
|
12
|
+
* widgets/<name>/widget.ts export default defineWidget({ ... })
|
|
13
|
+
* widgets/<name>/ui.tsx export default function Component() { ... }
|
|
14
|
+
* flows/<name>.ts export default createFlow({ ... }).compile()
|
|
15
|
+
* docs/<slug>.md searchable knowledge
|
|
16
|
+
*/
|
|
17
|
+
export function defineApp(config) {
|
|
18
|
+
return config;
|
|
19
|
+
}
|
|
20
|
+
export function defineTool(def) {
|
|
21
|
+
return def;
|
|
22
|
+
}
|
|
23
|
+
export function defineWidget(def) {
|
|
24
|
+
return def;
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AA0CH,MAAM,UAAU,SAAS,CAAC,MAAiB;IAC1C,OAAO,MAAM,CAAC;AACf,CAAC;AAwBD,MAAM,UAAU,UAAU,CACzB,GAAyB;IAEzB,OAAO,GAAG,CAAC;AACZ,CAAC;AAuCD,MAAM,UAAU,YAAY,CAAkB,GAAwB;IACrE,OAAO,GAAG,CAAC;AACZ,CAAC"}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared MCP runtime.
|
|
3
|
+
*
|
|
4
|
+
* Every Waniwani MCP app runs this exact code. App repos contain no server
|
|
5
|
+
* bootstrap, so a runtime fix is one package bump away for all of them — no
|
|
6
|
+
* 30-repo sweep, no per-repo verification.
|
|
7
|
+
*
|
|
8
|
+
* The server itself belongs to the distribution template, which constructs it,
|
|
9
|
+
* registers whatever tools it ships, and runs it. The CLI generates one file
|
|
10
|
+
* against that seam — `src/waniwani.ts` — which imports the app's modules and
|
|
11
|
+
* hands them to `registerApp()`. Nothing else.
|
|
12
|
+
*/
|
|
13
|
+
import { McpServer } from "skybridge/server";
|
|
14
|
+
import type { DocEntry, Shape, ToolHints, WidgetCsp } from "./index.js";
|
|
15
|
+
/**
|
|
16
|
+
* The manifest holds definitions with unrelated schemas side by side, so the
|
|
17
|
+
* handler signatures are widened here. `never` in the parameter position
|
|
18
|
+
* accepts any concrete handler; the call sites cast back.
|
|
19
|
+
*/
|
|
20
|
+
type AnyToolDefinition = {
|
|
21
|
+
title: string;
|
|
22
|
+
description: string;
|
|
23
|
+
input?: Shape;
|
|
24
|
+
output?: Shape;
|
|
25
|
+
hints?: ToolHints;
|
|
26
|
+
run: (input: never) => unknown;
|
|
27
|
+
};
|
|
28
|
+
type AnyWidgetDefinition = {
|
|
29
|
+
title: string;
|
|
30
|
+
description: string;
|
|
31
|
+
data: Shape;
|
|
32
|
+
hints?: ToolHints;
|
|
33
|
+
csp?: WidgetCsp;
|
|
34
|
+
llmText?: (data: never) => string;
|
|
35
|
+
load?: (input: never) => unknown;
|
|
36
|
+
};
|
|
37
|
+
/** A flow compiled by `createFlow(...).compile()` from `@waniwani/sdk/mcp`. */
|
|
38
|
+
export type CompiledFlow = {
|
|
39
|
+
name: string;
|
|
40
|
+
config: any;
|
|
41
|
+
handler: any;
|
|
42
|
+
};
|
|
43
|
+
export type Manifest = {
|
|
44
|
+
tools: Array<{
|
|
45
|
+
name: string;
|
|
46
|
+
def: AnyToolDefinition;
|
|
47
|
+
}>;
|
|
48
|
+
widgets: Array<{
|
|
49
|
+
name: string;
|
|
50
|
+
def: AnyWidgetDefinition;
|
|
51
|
+
}>;
|
|
52
|
+
flows: CompiledFlow[];
|
|
53
|
+
docs: DocEntry[];
|
|
54
|
+
/**
|
|
55
|
+
* Origins the template's Tailwind entry loads from, read off it at build
|
|
56
|
+
* time. Every view imports that stylesheet, so every widget needs them.
|
|
57
|
+
*/
|
|
58
|
+
styleDomains?: string[];
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Register an app's tools, widgets, flows, and docs onto a server the template
|
|
62
|
+
* built.
|
|
63
|
+
*
|
|
64
|
+
* The template owns construction, its own tools, `withWaniwani`, and `run()`.
|
|
65
|
+
* This adds to that server rather than replacing it, so a tool the template
|
|
66
|
+
* ships reaches every app built on it — one publish, not thirty edits — and an
|
|
67
|
+
* app's own tools sit alongside it.
|
|
68
|
+
*/
|
|
69
|
+
export declare function registerApp(server: McpServer, manifest: Manifest): Promise<McpServer>;
|
|
70
|
+
export {};
|
|
71
|
+
//# sourceMappingURL=server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,SAAS,EAAiB,MAAM,kBAAkB,CAAC;AAE5D,OAAO,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAExE;;;;GAIG;AACH,KAAK,iBAAiB,GAAG;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,MAAM,CAAC,EAAE,KAAK,CAAC;IACf,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,GAAG,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,OAAO,CAAC;CAC/B,CAAC;AAEF,KAAK,mBAAmB,GAAG;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,KAAK,CAAC;IACZ,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,GAAG,CAAC,EAAE,SAAS,CAAC;IAChB,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,KAAK,KAAK,MAAM,CAAC;IAClC,IAAI,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,OAAO,CAAC;CACjC,CAAC;AAEF,+EAA+E;AAC/E,MAAM,MAAM,YAAY,GAAG;IAC1B,IAAI,EAAE,MAAM,CAAC;IAEb,MAAM,EAAE,GAAG,CAAC;IAEZ,OAAO,EAAE,GAAG,CAAC;CACb,CAAC;AAEF,MAAM,MAAM,QAAQ,GAAG;IACtB,KAAK,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,iBAAiB,CAAA;KAAE,CAAC,CAAC;IACvD,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,mBAAmB,CAAA;KAAE,CAAC,CAAC;IAC3D,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,IAAI,EAAE,QAAQ,EAAE,CAAC;IACjB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB,CAAC;AAyHF;;;;;;;;GAQG;AACH,wBAAsB,WAAW,CAAC,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,SAAS,CAAC,CA8F3F"}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared MCP runtime.
|
|
3
|
+
*
|
|
4
|
+
* Every Waniwani MCP app runs this exact code. App repos contain no server
|
|
5
|
+
* bootstrap, so a runtime fix is one package bump away for all of them — no
|
|
6
|
+
* 30-repo sweep, no per-repo verification.
|
|
7
|
+
*
|
|
8
|
+
* The server itself belongs to the distribution template, which constructs it,
|
|
9
|
+
* registers whatever tools it ships, and runs it. The CLI generates one file
|
|
10
|
+
* against that seam — `src/waniwani.ts` — which imports the app's modules and
|
|
11
|
+
* hands them to `registerApp()`. Nothing else.
|
|
12
|
+
*/
|
|
13
|
+
import { McpServer } from "skybridge/server";
|
|
14
|
+
import { z } from "zod";
|
|
15
|
+
/**
|
|
16
|
+
* The origins a widget may load assets from: its own, plus the ones its
|
|
17
|
+
* stylesheet needs.
|
|
18
|
+
*
|
|
19
|
+
* A host that enforces the widget CSP drops undeclared requests silently — a
|
|
20
|
+
* blocked webfont is not an error, just a fallback face — so the base
|
|
21
|
+
* stylesheet's origins are added for every widget rather than left to each app
|
|
22
|
+
* to remember. An app's own `csp` is additive, never overwritten.
|
|
23
|
+
*/
|
|
24
|
+
function resourceDomains(csp, styleDomains) {
|
|
25
|
+
const merged = [...new Set([...(csp?.resourceDomains ?? []), ...styleDomains])];
|
|
26
|
+
return merged.length > 0 ? merged : undefined;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Translate `hints` into MCP annotations. `title` is always present because
|
|
30
|
+
* Claude's Connectors Directory rejects tools without one.
|
|
31
|
+
*/
|
|
32
|
+
function annotations(title, hints, defaults) {
|
|
33
|
+
const merged = { ...defaults, ...hints };
|
|
34
|
+
return {
|
|
35
|
+
title,
|
|
36
|
+
readOnlyHint: merged.readOnly ?? false,
|
|
37
|
+
destructiveHint: merged.destructive ?? false,
|
|
38
|
+
openWorldHint: merged.openWorld ?? false,
|
|
39
|
+
idempotentHint: merged.idempotent ?? false,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/** Normalise whatever a tool handler returned into an MCP `CallToolResult`. */
|
|
43
|
+
function toolResult(value) {
|
|
44
|
+
if (typeof value === "string") {
|
|
45
|
+
return { content: [{ type: "text", text: value }] };
|
|
46
|
+
}
|
|
47
|
+
if (value && typeof value === "object" && "content" in value) {
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
const structuredContent = (value ?? {});
|
|
51
|
+
return {
|
|
52
|
+
structuredContent,
|
|
53
|
+
content: [{ type: "text", text: JSON.stringify(structuredContent, null, 2) }],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Default model-facing text for a widget. Widgets render their own detail, so
|
|
58
|
+
* the model is told to stop narrating it — the single most common cause of a
|
|
59
|
+
* widget being read aloud twice.
|
|
60
|
+
*/
|
|
61
|
+
function defaultWidgetText(name) {
|
|
62
|
+
return `The ${name} widget is now rendered for the user. It displays all the detail itself — do NOT list or repeat its contents in text. Acknowledge it in one short sentence, then wait for the user to interact with it.`;
|
|
63
|
+
}
|
|
64
|
+
function widgetError(name, error) {
|
|
65
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
66
|
+
console.error(`[waniwani] widget "${name}" failed to load:`, error);
|
|
67
|
+
return {
|
|
68
|
+
isError: true,
|
|
69
|
+
content: [
|
|
70
|
+
{
|
|
71
|
+
type: "text",
|
|
72
|
+
text: `The ${name} widget could not load its data (${message}). Tell the user something went wrong on our side and offer to try again — do not invent the data.`,
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function registerDocsTool(server, docs) {
|
|
78
|
+
const corpus = docs.map((doc) => ({
|
|
79
|
+
...doc,
|
|
80
|
+
haystack: `${doc.title}\n${doc.body}`.toLowerCase(),
|
|
81
|
+
}));
|
|
82
|
+
server.registerTool({
|
|
83
|
+
name: "search_docs",
|
|
84
|
+
title: "Search the documentation",
|
|
85
|
+
description: "Search this product's documentation and answer general questions — pricing, eligibility, policies, how things work. Always search before answering, and answer only from what comes back. Never invent facts that are not in the results.",
|
|
86
|
+
inputSchema: { question: z.string().describe("The user's question, in their own words.") },
|
|
87
|
+
outputSchema: {
|
|
88
|
+
results: z.array(z.object({ slug: z.string(), title: z.string(), body: z.string() })),
|
|
89
|
+
},
|
|
90
|
+
annotations: annotations("Search the documentation", undefined, { readOnly: true }),
|
|
91
|
+
}, async ({ question }) => {
|
|
92
|
+
const terms = question
|
|
93
|
+
.toLowerCase()
|
|
94
|
+
.split(/[^a-z0-9]+/)
|
|
95
|
+
.filter((term) => term.length > 2);
|
|
96
|
+
const results = corpus
|
|
97
|
+
.map((doc) => ({
|
|
98
|
+
doc,
|
|
99
|
+
score: terms.reduce((sum, term) => sum + (doc.haystack.includes(term) ? 1 : 0), 0),
|
|
100
|
+
}))
|
|
101
|
+
.filter(({ score }) => score > 0)
|
|
102
|
+
.sort((a, b) => b.score - a.score)
|
|
103
|
+
.slice(0, 3)
|
|
104
|
+
.map(({ doc }) => ({ slug: doc.slug, title: doc.title, body: doc.body }));
|
|
105
|
+
if (results.length === 0) {
|
|
106
|
+
const text = "Nothing in the documentation covers that question.";
|
|
107
|
+
return { structuredContent: { results: [] }, content: [{ type: "text", text }] };
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
structuredContent: { results },
|
|
111
|
+
content: [
|
|
112
|
+
{
|
|
113
|
+
type: "text",
|
|
114
|
+
text: results.map((r) => `## ${r.title}\n${r.body}`).join("\n\n---\n\n"),
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
};
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Register an app's tools, widgets, flows, and docs onto a server the template
|
|
122
|
+
* built.
|
|
123
|
+
*
|
|
124
|
+
* The template owns construction, its own tools, `withWaniwani`, and `run()`.
|
|
125
|
+
* This adds to that server rather than replacing it, so a tool the template
|
|
126
|
+
* ships reaches every app built on it — one publish, not thirty edits — and an
|
|
127
|
+
* app's own tools sit alongside it.
|
|
128
|
+
*/
|
|
129
|
+
export async function registerApp(server, manifest) {
|
|
130
|
+
const { tools, widgets, flows, docs, styleDomains = [] } = manifest;
|
|
131
|
+
// Widgets: one `data` schema drives the input schema, the structured output,
|
|
132
|
+
// and the type the component receives.
|
|
133
|
+
//
|
|
134
|
+
// A widget is a tool with a view attached. The view's component name is the
|
|
135
|
+
// widget's folder name, which is also the name of the entry the generator
|
|
136
|
+
// writes into `src/views/` — one name, from the filesystem, so a widget
|
|
137
|
+
// cannot be registered against a component that was never bundled.
|
|
138
|
+
for (const { name, def } of widgets) {
|
|
139
|
+
server.registerTool({
|
|
140
|
+
name,
|
|
141
|
+
title: def.title,
|
|
142
|
+
description: def.description,
|
|
143
|
+
inputSchema: def.data,
|
|
144
|
+
outputSchema: def.data,
|
|
145
|
+
annotations: annotations(def.title, def.hints, { readOnly: true }),
|
|
146
|
+
view: {
|
|
147
|
+
component: name,
|
|
148
|
+
description: def.description,
|
|
149
|
+
csp: {
|
|
150
|
+
...def.csp,
|
|
151
|
+
resourceDomains: resourceDomains(def.csp, styleDomains),
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
}, async (input) => {
|
|
155
|
+
try {
|
|
156
|
+
const data = (def.load ? await def.load(input) : input);
|
|
157
|
+
return {
|
|
158
|
+
structuredContent: data,
|
|
159
|
+
content: [
|
|
160
|
+
{
|
|
161
|
+
type: "text",
|
|
162
|
+
text: def.llmText?.(data) ?? defaultWidgetText(name),
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
return widgetError(name, error);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
for (const { name, def } of tools) {
|
|
173
|
+
server.registerTool({
|
|
174
|
+
name,
|
|
175
|
+
title: def.title,
|
|
176
|
+
description: def.description,
|
|
177
|
+
inputSchema: def.input ?? {},
|
|
178
|
+
outputSchema: def.output,
|
|
179
|
+
annotations: annotations(def.title, def.hints, { readOnly: false }),
|
|
180
|
+
}, async (input) => {
|
|
181
|
+
try {
|
|
182
|
+
return toolResult(await def.run(input));
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
186
|
+
console.error(`[waniwani] tool "${name}" failed:`, error);
|
|
187
|
+
return {
|
|
188
|
+
isError: true,
|
|
189
|
+
content: [
|
|
190
|
+
{
|
|
191
|
+
type: "text",
|
|
192
|
+
text: `The ${name} tool failed (${message}). Tell the user it did not work and offer to retry — do not invent a result.`,
|
|
193
|
+
},
|
|
194
|
+
],
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
// Flows arrive from the SDK shaped for the MCP SDK's `(name, config, handler)`
|
|
200
|
+
// call, which the framework replaced with a single config carrying the name.
|
|
201
|
+
for (const flow of flows) {
|
|
202
|
+
server.registerTool({ ...flow.config, name: flow.name }, flow.handler);
|
|
203
|
+
}
|
|
204
|
+
if (docs.length > 0) {
|
|
205
|
+
registerDocsTool(server, docs);
|
|
206
|
+
}
|
|
207
|
+
// `withWaniwani` is deliberately not called here. It wraps every registered
|
|
208
|
+
// handler in place, so it has to run after the last registration — which is
|
|
209
|
+
// the template's, not this function's. `src/server.ts` calls it.
|
|
210
|
+
return server;
|
|
211
|
+
}
|
|
212
|
+
//# sourceMappingURL=server.js.map
|