@webmcp-auto-ui/agent 2.5.11 → 2.5.14
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/README.md +10 -9
- package/package.json +3 -3
- package/src/discovery-cache.ts +33 -0
- package/src/index.ts +5 -0
- package/src/recipe-browser.ts +85 -0
- package/src/server/{anthropicProxy.ts → llmProxy.ts} +2 -2
- package/src/tool-browser.ts +36 -0
package/README.md
CHANGED
|
@@ -4,12 +4,14 @@ LLM agent loop that connects MCP and WebMCP servers to a UI. Given a user messag
|
|
|
4
4
|
|
|
5
5
|
## Providers
|
|
6
6
|
|
|
7
|
-
**
|
|
7
|
+
**RemoteLLMProvider** — proxies to a `+server.ts` endpoint that holds the API key. Compatible with any OpenAI-compatible API backend (Anthropic, OpenAI, Google, Mistral, etc.). Prompt caching enabled by default. Retry on 503 with exponential backoff. Returns stats in `LLMResponse`: tok/s, totalTokens, latencyMs.
|
|
8
8
|
|
|
9
9
|
**GemmaProvider (LiteRT)** — runs Gemma 4 models via `@mediapipe/tasks-genai` (LiteRT, formerly known as MediaPipe) directly on the **main thread**. Uses WebGPU when available. No API key required. Models are cached in **OPFS** (Origin Private File System) for instant reload after first download.
|
|
10
10
|
|
|
11
11
|
> **v0.5.0 migration**: GemmaProvider was migrated from ONNX (`@huggingface/transformers`) to LiteRT (`@mediapipe/tasks-genai`). LiteRT is 2-4x faster on WebGPU and provides native Gemma 4 support. The provider now runs on the main thread because MediaPipe is incompatible with ES module workers.
|
|
12
12
|
|
|
13
|
+
**LocalLLMProvider** — runs against a local Ollama instance (or any OpenAI-compatible local server: vLLM, LM Studio, llamafile). No API key required. Converts messages and tools to the OpenAI chat completions format automatically.
|
|
14
|
+
|
|
13
15
|
**Gemma 4 prompt format** — uses `<|turn>...<turn|>` delimiters (instead of the Gemma 2/3 `<start_of_turn>...<end_of_turn>`).
|
|
14
16
|
|
|
15
17
|
**Native tool calling** — Gemma 4 tool calls are parsed from `<|tool_call>call:name{args}<tool_call|>` format. No regex heuristics needed.
|
|
@@ -46,10 +48,10 @@ npm install @webmcp-auto-ui/agent
|
|
|
46
48
|
## Usage
|
|
47
49
|
|
|
48
50
|
```ts
|
|
49
|
-
import { autoui, runAgentLoop,
|
|
51
|
+
import { autoui, runAgentLoop, RemoteLLMProvider } from '@webmcp-auto-ui/agent';
|
|
50
52
|
|
|
51
53
|
const result = await runAgentLoop('Show me sales data', {
|
|
52
|
-
provider: new
|
|
54
|
+
provider: new RemoteLLMProvider({ proxyUrl: '/api/chat' }),
|
|
53
55
|
layers: [mcpClient.layer(), autoui.layer()],
|
|
54
56
|
maxIterations: 5,
|
|
55
57
|
callbacks: {
|
|
@@ -64,8 +66,6 @@ const result = await runAgentLoop('Show me sales data', {
|
|
|
64
66
|
});
|
|
65
67
|
```
|
|
66
68
|
|
|
67
|
-
> **Migration from Phase 7**: `onBlock` still works as a deprecated alias for `onWidget`. The `UILayer`, `SkillLayer`, `COMPONENT_TOOL`, `executeComponent`, and `componentRegistry` exports are removed — use `autoui.layer()` instead.
|
|
68
|
-
|
|
69
69
|
## TokenTracker
|
|
70
70
|
|
|
71
71
|
Real-time usage metrics tracking across requests:
|
|
@@ -125,18 +125,19 @@ Requires `Cross-Origin-Opener-Policy: same-origin` and `Cross-Origin-Embedder-Po
|
|
|
125
125
|
|
|
126
126
|
## API proxy (`+server.ts`)
|
|
127
127
|
|
|
128
|
-
The `
|
|
128
|
+
The `RemoteLLMProvider` sends requests to a local `+server.ts` endpoint that forwards them to the configured LLM API. The endpoint reads `LLM_API_KEY` from the environment, or from `body.__apiKey` as a fallback (for cases where the key is provided at runtime).
|
|
129
129
|
|
|
130
130
|
```ts
|
|
131
131
|
// src/routes/api/chat/+server.ts
|
|
132
132
|
import { env } from '$env/dynamic/private';
|
|
133
133
|
export const POST: RequestHandler = async ({ request }) => {
|
|
134
134
|
const body = await request.json();
|
|
135
|
-
const apiKey = body.__apiKey || env.
|
|
135
|
+
const apiKey = body.__apiKey || env.LLM_API_KEY;
|
|
136
136
|
delete body.__apiKey;
|
|
137
|
-
|
|
137
|
+
// Forward to your LLM provider (Anthropic, OpenAI, Mistral, etc.)
|
|
138
|
+
const res = await fetch(LLM_ENDPOINT, {
|
|
138
139
|
method: 'POST',
|
|
139
|
-
headers: { '
|
|
140
|
+
headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
|
|
140
141
|
body: JSON.stringify(body),
|
|
141
142
|
});
|
|
142
143
|
return Response.json(await res.json());
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webmcp-auto-ui/agent",
|
|
3
|
-
"version": "2.5.
|
|
4
|
-
"description": "LLM agent loop +
|
|
3
|
+
"version": "2.5.14",
|
|
4
|
+
"description": "LLM agent loop + remote/WASM/local providers + MCP wrapper",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"sideEffects": true,
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"import": "./src/index.ts"
|
|
12
12
|
},
|
|
13
13
|
"./server": {
|
|
14
|
-
"import": "./src/server/
|
|
14
|
+
"import": "./src/server/llmProxy.ts"
|
|
15
15
|
}
|
|
16
16
|
},
|
|
17
17
|
"scripts": {
|
package/src/discovery-cache.ts
CHANGED
|
@@ -41,6 +41,39 @@ export class DiscoveryCache {
|
|
|
41
41
|
this.servers.clear();
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/** All registered server prefixes */
|
|
45
|
+
serverPrefixes(): string[] {
|
|
46
|
+
return [...this.servers.keys()];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** All recipes across all servers, tagged with their server prefix */
|
|
50
|
+
allRecipes(): Array<CachedRecipe & { server: string }> {
|
|
51
|
+
const result: Array<CachedRecipe & { server: string }> = [];
|
|
52
|
+
for (const [prefix, cache] of this.servers) {
|
|
53
|
+
for (const r of cache.recipes) result.push({ ...r, server: prefix });
|
|
54
|
+
}
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** All tools across all servers, tagged with their server prefix */
|
|
59
|
+
allTools(): Array<{ name: string; description?: string; inputSchema?: Record<string, unknown>; server: string }> {
|
|
60
|
+
const result: Array<{ name: string; description?: string; inputSchema?: Record<string, unknown>; server: string }> = [];
|
|
61
|
+
for (const [prefix, cache] of this.servers) {
|
|
62
|
+
for (const t of cache.tools) result.push({ ...t, server: prefix });
|
|
63
|
+
}
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Recipe count for a specific server */
|
|
68
|
+
recipeCount(serverPrefix: string): number {
|
|
69
|
+
return this.servers.get(serverPrefix)?.recipes.length ?? 0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Tool count for a specific server */
|
|
73
|
+
toolCount(serverPrefix: string): number {
|
|
74
|
+
return this.servers.get(serverPrefix)?.tools.length ?? 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
44
77
|
/**
|
|
45
78
|
* Try to resolve a discovery tool call from cache.
|
|
46
79
|
* Returns the result string if handled, or null if not a discovery tool.
|
package/src/index.ts
CHANGED
|
@@ -38,6 +38,11 @@ export type { WebMcpServer, WebMcpToolDef, WidgetEntry } from '@webmcp-auto-ui/c
|
|
|
38
38
|
// Recipes
|
|
39
39
|
export { WEBMCP_RECIPES, parseRecipe, parseRecipes } from './recipes/index.js';
|
|
40
40
|
export { recipeRegistry, registerRecipes, filterRecipesByServer, formatRecipesForPrompt, formatMcpRecipesForPrompt } from './recipe-registry.js';
|
|
41
|
+
export { filterRecipes, sortRecipes, recipeToMarkdown, recipeToDownloadBlob } from './recipe-browser.js';
|
|
42
|
+
|
|
43
|
+
// Tool browser
|
|
44
|
+
export { groupToolsByServer, formatToolSchema } from './tool-browser.js';
|
|
45
|
+
export type { BrowsableTool } from './tool-browser.js';
|
|
41
46
|
|
|
42
47
|
// Summarize
|
|
43
48
|
export { summarizeChat } from './summarize.js';
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// recipe-browser — pure utility functions for browsing/filtering/exporting recipes
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Case-insensitive filter on name and description fields.
|
|
5
|
+
* Empty query returns all recipes.
|
|
6
|
+
*/
|
|
7
|
+
export function filterRecipes<T extends { name: string; description?: string }>(
|
|
8
|
+
recipes: T[],
|
|
9
|
+
query: string,
|
|
10
|
+
): T[] {
|
|
11
|
+
const q = query.trim().toLowerCase();
|
|
12
|
+
if (!q) return recipes;
|
|
13
|
+
return recipes.filter(
|
|
14
|
+
(r) =>
|
|
15
|
+
r.name.toLowerCase().includes(q) ||
|
|
16
|
+
(r.description && r.description.toLowerCase().includes(q)),
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Returns a new array sorted alphabetically by name (case-insensitive).
|
|
22
|
+
* Does NOT mutate the input array.
|
|
23
|
+
*/
|
|
24
|
+
export function sortRecipes<T extends { name: string }>(recipes: T[]): T[] {
|
|
25
|
+
return [...recipes].sort((a, b) =>
|
|
26
|
+
a.name.toLowerCase().localeCompare(b.name.toLowerCase()),
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Keys that belong in Recipe frontmatter (order matters for readability)
|
|
31
|
+
const RECIPE_FM_KEYS = ['id', 'name', 'description', 'when', 'components_used', 'servers', 'layout'] as const;
|
|
32
|
+
// Keys for McpRecipe frontmatter
|
|
33
|
+
const MCP_FM_KEYS = ['name', 'description'] as const;
|
|
34
|
+
|
|
35
|
+
function yamlValue(value: unknown): string {
|
|
36
|
+
if (value === null || value === undefined) return '""';
|
|
37
|
+
if (typeof value === 'string') return value.includes(':') || value.includes('#') || value.includes('\n') ? JSON.stringify(value) : value;
|
|
38
|
+
if (Array.isArray(value)) return `[${value.map((v) => JSON.stringify(v)).join(', ')}]`;
|
|
39
|
+
if (typeof value === 'object') return JSON.stringify(value);
|
|
40
|
+
return String(value);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Reconstructs a markdown file from a recipe object.
|
|
45
|
+
* Handles both Recipe (id, when, components_used, servers, layout, body)
|
|
46
|
+
* and McpRecipe (name, description, body?) shapes.
|
|
47
|
+
*/
|
|
48
|
+
export function recipeToMarkdown(recipe: Record<string, unknown>): string {
|
|
49
|
+
const isFullRecipe = 'id' in recipe || 'when' in recipe;
|
|
50
|
+
const fmKeys = isFullRecipe ? RECIPE_FM_KEYS : MCP_FM_KEYS;
|
|
51
|
+
|
|
52
|
+
const lines: string[] = ['---'];
|
|
53
|
+
for (const key of fmKeys) {
|
|
54
|
+
if (key in recipe && recipe[key] !== undefined) {
|
|
55
|
+
lines.push(`${key}: ${yamlValue(recipe[key])}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
lines.push('---');
|
|
59
|
+
|
|
60
|
+
const body = typeof recipe.body === 'string' ? recipe.body.trim() : '';
|
|
61
|
+
if (body) {
|
|
62
|
+
lines.push('', body);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return lines.join('\n') + '\n';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function sanitizeFilename(raw: string): string {
|
|
69
|
+
return raw.replace(/\s+/g, '-').toLowerCase().replace(/[^a-z0-9_.-]/g, '');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Creates a downloadable Blob from a recipe object.
|
|
74
|
+
* Filename is derived from name or id, sanitized.
|
|
75
|
+
*/
|
|
76
|
+
export function recipeToDownloadBlob(
|
|
77
|
+
recipe: Record<string, unknown>,
|
|
78
|
+
): { blob: Blob; filename: string } {
|
|
79
|
+
const md = recipeToMarkdown(recipe);
|
|
80
|
+
const rawName = (typeof recipe.name === 'string' && recipe.name)
|
|
81
|
+
|| (typeof recipe.id === 'string' && recipe.id)
|
|
82
|
+
|| 'recipe';
|
|
83
|
+
const filename = sanitizeFilename(rawName) + '.md';
|
|
84
|
+
return { blob: new Blob([md], { type: 'text/markdown' }), filename };
|
|
85
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shared
|
|
2
|
+
* Shared LLM proxy handler — used by all apps' /api/chat/+server.ts
|
|
3
3
|
* Accepts the parsed body (with __apiKey already extracted), the resolved
|
|
4
4
|
* API key, and the optional model override from X-Model header.
|
|
5
5
|
*/
|
|
6
|
-
export async function
|
|
6
|
+
export async function llmProxy(
|
|
7
7
|
body: Record<string, unknown>,
|
|
8
8
|
apiKey: string,
|
|
9
9
|
model?: string | null,
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// tool-browser — pure utility functions for browsing/filtering tools
|
|
2
|
+
|
|
3
|
+
export interface BrowsableTool {
|
|
4
|
+
name: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
server?: string;
|
|
7
|
+
inputSchema?: Record<string, unknown>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Group tools by server name.
|
|
12
|
+
* Returns a Map<serverName, tools[]> with alphabetically sorted tools in each group.
|
|
13
|
+
*/
|
|
14
|
+
export function groupToolsByServer(tools: BrowsableTool[]): Map<string, BrowsableTool[]> {
|
|
15
|
+
const map = new Map<string, BrowsableTool[]>();
|
|
16
|
+
for (const t of tools) {
|
|
17
|
+
const key = t.server || 'Unknown';
|
|
18
|
+
const arr = map.get(key) ?? [];
|
|
19
|
+
arr.push(t);
|
|
20
|
+
map.set(key, arr);
|
|
21
|
+
}
|
|
22
|
+
// Sort tools within each group
|
|
23
|
+
for (const [key, arr] of map) {
|
|
24
|
+
map.set(key, arr.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase())));
|
|
25
|
+
}
|
|
26
|
+
return map;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Format a tool's input schema as a readable string.
|
|
31
|
+
* Returns pretty-printed JSON, or null if no schema.
|
|
32
|
+
*/
|
|
33
|
+
export function formatToolSchema(tool: BrowsableTool): string | null {
|
|
34
|
+
if (!tool.inputSchema) return null;
|
|
35
|
+
return JSON.stringify(tool.inputSchema, null, 2);
|
|
36
|
+
}
|