@webmcp-auto-ui/sdk 2.5.25 → 2.5.26
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/package.json +2 -1
- package/src/index.ts +4 -0
- package/src/recipes/index.ts +3 -0
- package/src/recipes/parse.ts +53 -0
- package/src/recipes/runner.ts +270 -0
- package/src/recipes/types.ts +55 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webmcp-auto-ui/sdk",
|
|
3
|
-
"version": "2.5.
|
|
3
|
+
"version": "2.5.26",
|
|
4
4
|
"description": "Skills CRUD, HyperSkill format, Svelte 5 stores",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"type": "module",
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@sveltejs/package": "^2.3.0",
|
|
37
|
+
"@webmcp-auto-ui/core": "file:../core",
|
|
37
38
|
"svelte": "^5.0.0",
|
|
38
39
|
"svelte-check": "^4.0.0",
|
|
39
40
|
"typescript": "^5.0.0"
|
package/src/index.ts
CHANGED
|
@@ -90,3 +90,7 @@ export type { McpDemoServer } from './mcp-demo-servers.js';
|
|
|
90
90
|
|
|
91
91
|
// Canvas store — browser-only (Svelte 5 runes), import directly from src:
|
|
92
92
|
// import { canvas } from '@webmcp-auto-ui/sdk/canvas'
|
|
93
|
+
|
|
94
|
+
// Recipe runner — markdown-fence parser + JS/TS/SQL/etc executor over MCP
|
|
95
|
+
export { parseBody, runCode, estimateTokens, safeStringify } from './recipes/index.js';
|
|
96
|
+
export type { ParsedSegment, RunResult, RunLog, RunTab, RecipeData } from './recipes/index.js';
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { ParsedSegment } from './types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Split a markdown body into alternating markdown / code-block segments.
|
|
5
|
+
* Fenced code blocks of the form:
|
|
6
|
+
*
|
|
7
|
+
* ```lang
|
|
8
|
+
* code
|
|
9
|
+
* ```
|
|
10
|
+
*
|
|
11
|
+
* are extracted as `{ type: 'code', content, lang }`. Everything else stays as
|
|
12
|
+
* `{ type: 'markdown', content }` so it can be rendered via MarkdownView.
|
|
13
|
+
*/
|
|
14
|
+
export function parseBody(body: string): ParsedSegment[] {
|
|
15
|
+
if (!body) return [];
|
|
16
|
+
|
|
17
|
+
const segments: ParsedSegment[] = [];
|
|
18
|
+
// Match fenced code blocks with optional language tag.
|
|
19
|
+
// Non-greedy body; allow any chars between the fences.
|
|
20
|
+
const re = /```([a-zA-Z0-9_+-]*)\r?\n([\s\S]*?)```/g;
|
|
21
|
+
|
|
22
|
+
let lastIndex = 0;
|
|
23
|
+
let match: RegExpExecArray | null;
|
|
24
|
+
|
|
25
|
+
while ((match = re.exec(body)) !== null) {
|
|
26
|
+
const [full, langRaw, codeRaw] = match;
|
|
27
|
+
const start = match.index;
|
|
28
|
+
|
|
29
|
+
if (start > lastIndex) {
|
|
30
|
+
const chunk = body.slice(lastIndex, start);
|
|
31
|
+
if (chunk.trim().length > 0) {
|
|
32
|
+
segments.push({ type: 'markdown', content: chunk });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
segments.push({
|
|
37
|
+
type: 'code',
|
|
38
|
+
content: codeRaw.replace(/\r?\n$/, ''),
|
|
39
|
+
lang: (langRaw || '').trim().toLowerCase() || 'text',
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
lastIndex = start + full.length;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (lastIndex < body.length) {
|
|
46
|
+
const tail = body.slice(lastIndex);
|
|
47
|
+
if (tail.trim().length > 0) {
|
|
48
|
+
segments.push({ type: 'markdown', content: tail });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return segments;
|
|
53
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import type { McpMultiClient } from '@webmcp-auto-ui/core';
|
|
2
|
+
import type { RunResult, RunLog } from './types.js';
|
|
3
|
+
|
|
4
|
+
/** Rough token estimator: 4 characters per token heuristic. */
|
|
5
|
+
export function estimateTokens(s: string): number {
|
|
6
|
+
if (!s) return 0;
|
|
7
|
+
return Math.ceil(s.length / 4);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const JS_LANGS = new Set(['js', 'javascript', 'mjs', 'cjs']);
|
|
11
|
+
const TS_LANGS = new Set(['ts', 'typescript']);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Mapping of language → preferred MCP tool name.
|
|
15
|
+
*
|
|
16
|
+
* Rationale: on the `tricoteuses` / code4code MCP server, SQL amendments are
|
|
17
|
+
* exposed via `query_sql`, while `run_script` is intended for JS/TS adapters
|
|
18
|
+
* calling `agentTask(tricoteuses)`. Dispatching a raw ```sql``` block through
|
|
19
|
+
* `run_script` fails validation.
|
|
20
|
+
*/
|
|
21
|
+
const LANG_TO_TOOL: Record<string, string> = {
|
|
22
|
+
sql: 'query_sql',
|
|
23
|
+
// js/ts runs locally (handled before), not here
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* `new Function` flavor that returns a promise (via AsyncFunction).
|
|
28
|
+
* We wrap user code in an async IIFE so users can `await` at top level and
|
|
29
|
+
* `return` a value.
|
|
30
|
+
*/
|
|
31
|
+
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor as new (
|
|
32
|
+
...args: string[]
|
|
33
|
+
) => (...a: unknown[]) => Promise<unknown>;
|
|
34
|
+
|
|
35
|
+
interface RunnerCtx {
|
|
36
|
+
log: (msg: string) => void;
|
|
37
|
+
start: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function makeCtx(): RunnerCtx & { logs: RunLog[] } {
|
|
41
|
+
const start = performance.now();
|
|
42
|
+
const logs: RunLog[] = [];
|
|
43
|
+
return {
|
|
44
|
+
start,
|
|
45
|
+
logs,
|
|
46
|
+
log(msg: string) {
|
|
47
|
+
logs.push({ t: Math.round(performance.now() - start), msg });
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function runJsLike(code: string, ctx: RunnerCtx): Promise<unknown> {
|
|
53
|
+
// Wrap user code as the body of an async function that executes itself.
|
|
54
|
+
// Users can use `await`, define vars, and return a final value.
|
|
55
|
+
const wrapped = `return (async () => {\n${code}\n})();`;
|
|
56
|
+
const fn = new AsyncFunction(wrapped);
|
|
57
|
+
ctx.log('dispatched (inline async)');
|
|
58
|
+
const out = await fn();
|
|
59
|
+
ctx.log('resolved');
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface McpToolDef {
|
|
64
|
+
name: string;
|
|
65
|
+
description?: string;
|
|
66
|
+
inputSchema?: unknown;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Inspects a tool's inputSchema to find the string parameter that likely
|
|
71
|
+
* holds the code/script/query. Returns the param name or null.
|
|
72
|
+
*/
|
|
73
|
+
function findCodeParamName(schema: unknown): string | null {
|
|
74
|
+
const s = schema as
|
|
75
|
+
| { properties?: Record<string, { type?: string }>; required?: string[] }
|
|
76
|
+
| null
|
|
77
|
+
| undefined;
|
|
78
|
+
if (!s?.properties) return null;
|
|
79
|
+
const candidates = ['script', 'code', 'query', 'sql', 'source'];
|
|
80
|
+
// Prefer named candidates matching a string property
|
|
81
|
+
for (const name of candidates) {
|
|
82
|
+
const prop = s.properties[name];
|
|
83
|
+
if (prop?.type === 'string') return name;
|
|
84
|
+
}
|
|
85
|
+
// Fallback: first required string param
|
|
86
|
+
for (const req of s.required ?? []) {
|
|
87
|
+
if (s.properties[req]?.type === 'string') return req;
|
|
88
|
+
}
|
|
89
|
+
// Last resort: first string param
|
|
90
|
+
for (const [name, prop] of Object.entries(s.properties)) {
|
|
91
|
+
if (prop?.type === 'string') return name;
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Heuristics for filling required params. Currently handles:
|
|
98
|
+
* - `schema` (string enum): extract from `FROM <schema>.<table>` or
|
|
99
|
+
* `JOIN <schema>.<table>` in SQL queries. Match against enum values
|
|
100
|
+
* (case-insensitive). Falls back to first enum value if no match.
|
|
101
|
+
*/
|
|
102
|
+
function inferParamValue(
|
|
103
|
+
name: string,
|
|
104
|
+
prop: { type?: string; enum?: unknown[] } | undefined,
|
|
105
|
+
code: string,
|
|
106
|
+
lang: string,
|
|
107
|
+
): unknown | undefined {
|
|
108
|
+
if (!prop) return undefined;
|
|
109
|
+
|
|
110
|
+
// Param `schema` on sql tools: sniff FROM/JOIN <schema>.<table>
|
|
111
|
+
if (name === 'schema' && lang === 'sql' && Array.isArray(prop.enum) && prop.enum.length > 0) {
|
|
112
|
+
const re = /\b(?:FROM|JOIN)\s+([a-zA-Z_][a-zA-Z0-9_]*)\.[a-zA-Z_]/gi;
|
|
113
|
+
const matches = new Set<string>();
|
|
114
|
+
let m: RegExpExecArray | null;
|
|
115
|
+
while ((m = re.exec(code)) !== null) matches.add(m[1].toLowerCase());
|
|
116
|
+
const enumLower = prop.enum.map((v) => String(v).toLowerCase());
|
|
117
|
+
for (const sch of matches) {
|
|
118
|
+
const idx = enumLower.indexOf(sch);
|
|
119
|
+
if (idx >= 0) return prop.enum[idx];
|
|
120
|
+
}
|
|
121
|
+
// Fallback: no detectable schema, no good guess → leave unset
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Build the full arg object for a tool call. Fills:
|
|
130
|
+
* - the code-carrying param (found by findCodeParamName)
|
|
131
|
+
* - every OTHER required param by inferring a value from the code (heuristics),
|
|
132
|
+
* or leaving it unset if nothing can be inferred (MCP will error explicitly
|
|
133
|
+
* so the user knows what to add).
|
|
134
|
+
*/
|
|
135
|
+
function buildToolArgs(
|
|
136
|
+
schema: unknown,
|
|
137
|
+
codeParam: string,
|
|
138
|
+
code: string,
|
|
139
|
+
lang: string,
|
|
140
|
+
): Record<string, unknown> {
|
|
141
|
+
const s = schema as { properties?: Record<string, any>; required?: string[] } | null | undefined;
|
|
142
|
+
const args: Record<string, unknown> = { [codeParam]: code };
|
|
143
|
+
if (!s?.properties) return args;
|
|
144
|
+
|
|
145
|
+
for (const req of s.required ?? []) {
|
|
146
|
+
if (req === codeParam) continue;
|
|
147
|
+
if (args[req] !== undefined) continue;
|
|
148
|
+
|
|
149
|
+
const prop = s.properties[req];
|
|
150
|
+
const inferred = inferParamValue(req, prop, code, lang);
|
|
151
|
+
if (inferred !== undefined) args[req] = inferred;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return args;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Find the first connected MCP server that exposes a given tool name, along
|
|
159
|
+
* with the tool definition (for inputSchema introspection).
|
|
160
|
+
*/
|
|
161
|
+
function findToolOnAnyServer(
|
|
162
|
+
multiClient: McpMultiClient | undefined,
|
|
163
|
+
toolName: string
|
|
164
|
+
): { url: string; name: string; tool: McpToolDef } | null {
|
|
165
|
+
if (!multiClient) return null;
|
|
166
|
+
for (const s of multiClient.listServers()) {
|
|
167
|
+
const tool = s.tools.find((t) => t.name === toolName) as McpToolDef | undefined;
|
|
168
|
+
if (tool) return { url: s.url, name: s.name, tool };
|
|
169
|
+
}
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function runViaMcp(
|
|
174
|
+
code: string,
|
|
175
|
+
lang: string,
|
|
176
|
+
multiClient: McpMultiClient | undefined,
|
|
177
|
+
ctx: RunnerCtx
|
|
178
|
+
): Promise<unknown> {
|
|
179
|
+
const toolName = LANG_TO_TOOL[lang] ?? 'run_script';
|
|
180
|
+
const found = findToolOnAnyServer(multiClient, toolName);
|
|
181
|
+
if (!found || !multiClient) {
|
|
182
|
+
throw new Error(
|
|
183
|
+
`No MCP server exposes tool "${toolName}" (needed for language "${lang}")`
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
const paramName = findCodeParamName(found.tool.inputSchema) ?? 'script';
|
|
187
|
+
ctx.log(
|
|
188
|
+
`dispatched to ${found.name} (tool=${toolName}, param=${paramName}, lang=${lang})`
|
|
189
|
+
);
|
|
190
|
+
const args = buildToolArgs(found.tool.inputSchema, paramName, code, lang);
|
|
191
|
+
const extraKeys = Object.keys(args).filter((k) => k !== paramName);
|
|
192
|
+
if (extraKeys.length) {
|
|
193
|
+
ctx.log(
|
|
194
|
+
`inferred args: ${extraKeys.map((k) => `${k}=${JSON.stringify(args[k])}`).join(', ')}`
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
const res = await multiClient.callToolOn(found.url, toolName, args);
|
|
198
|
+
ctx.log('response received');
|
|
199
|
+
// Normalize: extract text content if present, else raw result
|
|
200
|
+
const textPart = res?.content?.find((c: { type: string }) => c.type === 'text') as
|
|
201
|
+
| { text?: string }
|
|
202
|
+
| undefined;
|
|
203
|
+
if (textPart?.text) {
|
|
204
|
+
try {
|
|
205
|
+
return JSON.parse(textPart.text);
|
|
206
|
+
} catch {
|
|
207
|
+
return textPart.text;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return res;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Run a snippet of code in a given language.
|
|
215
|
+
*
|
|
216
|
+
* - JS / TS: executed inline via AsyncFunction (TS is NOT transpiled; code must
|
|
217
|
+
* be valid JS or the caller should keep type annotations minimal).
|
|
218
|
+
* - SQL: dispatched to `query_sql` on any connected MCP server that exposes it.
|
|
219
|
+
* - Other languages: dispatched to `run_script`. The param name (`script`,
|
|
220
|
+
* `code`, `query`, ...) is picked dynamically from the tool's inputSchema.
|
|
221
|
+
*/
|
|
222
|
+
export async function runCode(
|
|
223
|
+
code: string,
|
|
224
|
+
lang: string,
|
|
225
|
+
multiClient?: McpMultiClient
|
|
226
|
+
): Promise<RunResult> {
|
|
227
|
+
const ctx = makeCtx();
|
|
228
|
+
const normLang = (lang || '').toLowerCase();
|
|
229
|
+
const startedAt = ctx.start;
|
|
230
|
+
try {
|
|
231
|
+
let output: unknown;
|
|
232
|
+
if (JS_LANGS.has(normLang) || TS_LANGS.has(normLang) || normLang === '') {
|
|
233
|
+
output = await runJsLike(code, ctx);
|
|
234
|
+
} else {
|
|
235
|
+
output = await runViaMcp(code, normLang, multiClient, ctx);
|
|
236
|
+
}
|
|
237
|
+
const durationMs = Math.round(performance.now() - ctx.start);
|
|
238
|
+
const tokens = estimateTokens(code) + estimateTokens(safeStringify(output));
|
|
239
|
+
return {
|
|
240
|
+
status: 'done',
|
|
241
|
+
startedAt,
|
|
242
|
+
durationMs,
|
|
243
|
+
tokens,
|
|
244
|
+
output,
|
|
245
|
+
logs: ctx.logs,
|
|
246
|
+
};
|
|
247
|
+
} catch (err) {
|
|
248
|
+
const durationMs = Math.round(performance.now() - ctx.start);
|
|
249
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
250
|
+
ctx.log(`error: ${message}`);
|
|
251
|
+
return {
|
|
252
|
+
status: 'error',
|
|
253
|
+
startedAt,
|
|
254
|
+
durationMs,
|
|
255
|
+
tokens: estimateTokens(code),
|
|
256
|
+
error: message,
|
|
257
|
+
logs: ctx.logs,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function safeStringify(value: unknown): string {
|
|
263
|
+
if (value === undefined) return 'undefined';
|
|
264
|
+
if (typeof value === 'string') return value;
|
|
265
|
+
try {
|
|
266
|
+
return JSON.stringify(value, null, 2);
|
|
267
|
+
} catch {
|
|
268
|
+
return String(value);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for the RecipeModal code-block execution feature.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface RecipeData {
|
|
6
|
+
id?: string;
|
|
7
|
+
name?: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
when?: string;
|
|
10
|
+
components_used?: string[];
|
|
11
|
+
servers?: string[];
|
|
12
|
+
serverName?: string;
|
|
13
|
+
layout?: { type: string; columns?: number; arrangement?: string };
|
|
14
|
+
body?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type SegmentType = 'markdown' | 'code';
|
|
18
|
+
|
|
19
|
+
export interface ParsedSegment {
|
|
20
|
+
type: SegmentType;
|
|
21
|
+
content: string;
|
|
22
|
+
/** Language tag for code segments (e.g. "js", "python"). Undefined for markdown. */
|
|
23
|
+
lang?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type RunStatus = 'idle' | 'running' | 'done' | 'error';
|
|
27
|
+
|
|
28
|
+
export interface RunLog {
|
|
29
|
+
/** Offset in ms since run start. */
|
|
30
|
+
t: number;
|
|
31
|
+
msg: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface RunStats {
|
|
35
|
+
durationMs?: number;
|
|
36
|
+
tokens?: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface RunResult {
|
|
40
|
+
status: RunStatus;
|
|
41
|
+
startedAt?: number;
|
|
42
|
+
durationMs?: number;
|
|
43
|
+
tokens?: number;
|
|
44
|
+
output?: unknown;
|
|
45
|
+
error?: string;
|
|
46
|
+
logs: RunLog[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface RunTab {
|
|
50
|
+
id: string;
|
|
51
|
+
label: string;
|
|
52
|
+
lang: string;
|
|
53
|
+
code: string;
|
|
54
|
+
result: RunResult;
|
|
55
|
+
}
|