@olonjs/cli 3.0.166 → 3.0.167
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.
|
@@ -1817,9 +1817,9 @@ cat << 'END_OF_FILE_CONTENT' > "package.json"
|
|
|
1817
1817
|
"@tiptap/extension-link": "^2.11.5",
|
|
1818
1818
|
"@tiptap/react": "^2.11.5",
|
|
1819
1819
|
"@tiptap/starter-kit": "^2.11.5",
|
|
1820
|
-
"@olonjs/core": "^2.0.
|
|
1821
|
-
"@olonjs/react": "^0.2.
|
|
1822
|
-
"@olonjs/studio": "^0.2.
|
|
1820
|
+
"@olonjs/core": "^2.0.3",
|
|
1821
|
+
"@olonjs/react": "^0.2.3",
|
|
1822
|
+
"@olonjs/studio": "^0.2.3",
|
|
1823
1823
|
"class-variance-authority": "^0.7.1",
|
|
1824
1824
|
"clsx": "^2.1.1",
|
|
1825
1825
|
"lucide-react": "^0.474.0",
|
|
@@ -1817,9 +1817,9 @@ cat << 'END_OF_FILE_CONTENT' > "package.json"
|
|
|
1817
1817
|
"@tiptap/extension-link": "^2.11.5",
|
|
1818
1818
|
"@tiptap/react": "^2.11.5",
|
|
1819
1819
|
"@tiptap/starter-kit": "^2.11.5",
|
|
1820
|
-
"@olonjs/core": "^2.0.
|
|
1821
|
-
"@olonjs/react": "^0.2.
|
|
1822
|
-
"@olonjs/studio": "^0.2.
|
|
1820
|
+
"@olonjs/core": "^2.0.3",
|
|
1821
|
+
"@olonjs/react": "^0.2.3",
|
|
1822
|
+
"@olonjs/studio": "^0.2.3",
|
|
1823
1823
|
"class-variance-authority": "^0.7.1",
|
|
1824
1824
|
"clsx": "^2.1.1",
|
|
1825
1825
|
"lucide-react": "^0.474.0",
|
|
@@ -24,6 +24,8 @@ import { getFilePages } from '@/lib/loaders/getFilePages';
|
|
|
24
24
|
import { getFileSiteBundle } from '@/lib/loaders/getFileSiteConfig';
|
|
25
25
|
import { loadVisitorPageBundle } from '@/lib/loaders/loadVisitorPageBundle';
|
|
26
26
|
import { resolveVisitorShell } from '@/lib/resolveVisitorShell';
|
|
27
|
+
import { resolvePublicToolNames } from '@/lib/webmcp/resolvePublicToolNames';
|
|
28
|
+
import { sectionPublicTools } from '@/lib/webmcp/toolRegistry';
|
|
27
29
|
import { VisitorSection } from '@/lib/VisitorSection';
|
|
28
30
|
|
|
29
31
|
export const dynamic = 'force-dynamic';
|
|
@@ -115,6 +117,7 @@ export default async function VisitorCatchAllPage({ params, searchParams }: Page
|
|
|
115
117
|
const pageNum = Number(firstSearchValue(query.page) ?? '1') || 1;
|
|
116
118
|
const shell = resolveVisitorShell(page, resolved.siteConfig ?? siteConfig);
|
|
117
119
|
const sectionExtras = { authorId, page: pageNum, pathname };
|
|
120
|
+
const publicToolNames = resolvePublicToolNames(page.sections, sectionPublicTools as Record<string, readonly string[]>);
|
|
118
121
|
const title = page.meta?.title ?? result.registrySlug;
|
|
119
122
|
const description = typeof page.meta?.description === 'string' ? page.meta.description : '';
|
|
120
123
|
const jsonLd = buildVisitorWebPageJsonLd({
|
|
@@ -125,7 +128,7 @@ export default async function VisitorCatchAllPage({ params, searchParams }: Page
|
|
|
125
128
|
|
|
126
129
|
return (
|
|
127
130
|
<>
|
|
128
|
-
<WebMcpVisitorRuntime />
|
|
131
|
+
<WebMcpVisitorRuntime toolNames={publicToolNames} />
|
|
129
132
|
<link rel="mcp-manifest" href={buildPageManifestHref(requestSlug)} />
|
|
130
133
|
<link rel="olon-contract" href={buildPageContractHref(requestSlug)} />
|
|
131
134
|
<script
|
|
@@ -786,10 +789,11 @@ cat << 'END_OF_FILE_CONTENT' > "package.json"
|
|
|
786
789
|
"dist:dna": "npm run dist"
|
|
787
790
|
},
|
|
788
791
|
"dependencies": {
|
|
789
|
-
"@olonjs/core": "^2.0.
|
|
790
|
-
"@olonjs/next": "^0.0.
|
|
791
|
-
"@olonjs/react": "^0.2.
|
|
792
|
-
"@olonjs/
|
|
792
|
+
"@olonjs/core": "^2.0.3",
|
|
793
|
+
"@olonjs/next": "^0.0.15",
|
|
794
|
+
"@olonjs/react": "^0.2.3",
|
|
795
|
+
"@olonjs/sources": "^0.1.0",
|
|
796
|
+
"@olonjs/studio": "^0.2.3",
|
|
793
797
|
"clsx": "^2.1.1",
|
|
794
798
|
"lucide-react": "^0.474.0",
|
|
795
799
|
"next": "^15.5.0",
|
|
@@ -797,14 +801,14 @@ cat << 'END_OF_FILE_CONTENT' > "package.json"
|
|
|
797
801
|
"react-dom": "^19.0.0",
|
|
798
802
|
"react-router-dom": "^6.29.0",
|
|
799
803
|
"tailwind-merge": "^3.0.1",
|
|
800
|
-
"zod": "^4.6.0"
|
|
801
|
-
"@olonjs/sources": "^0.1.0"
|
|
804
|
+
"zod": "^4.6.0"
|
|
802
805
|
},
|
|
803
806
|
"devDependencies": {
|
|
804
807
|
"@tailwindcss/postcss": "^4.0.0",
|
|
805
808
|
"@types/node": "^22.13.1",
|
|
806
809
|
"@types/react": "^19.0.0",
|
|
807
810
|
"@types/react-dom": "^19.0.0",
|
|
811
|
+
"jsdom": "^26.1.0",
|
|
808
812
|
"tailwindcss": "^4.0.0",
|
|
809
813
|
"typescript": "^5.7.3",
|
|
810
814
|
"vitest": "^3.0.0"
|
|
@@ -1538,6 +1542,7 @@ import { ComponentRegistry } from '@/lib/ComponentRegistry';
|
|
|
1538
1542
|
import { iconMap } from '@/lib/IconResolver';
|
|
1539
1543
|
import { SECTION_SCHEMAS } from '@/lib/schemas';
|
|
1540
1544
|
import { buildEnabledWebMcpConfig } from '@/lib/webmcp/buildEnabledWebMcpConfig';
|
|
1545
|
+
import { adminToolExecutors, declaredAdminTools } from '@/lib/webmcp/toolRegistry';
|
|
1541
1546
|
|
|
1542
1547
|
export type AdminStudioClientProps = {
|
|
1543
1548
|
tenantId?: string;
|
|
@@ -1625,7 +1630,11 @@ export function AdminStudioClient({
|
|
|
1625
1630
|
themeCss: { tenant: '' },
|
|
1626
1631
|
iconRegistry: iconMap,
|
|
1627
1632
|
addSection: addSectionConfig,
|
|
1628
|
-
webmcp:
|
|
1633
|
+
webmcp: {
|
|
1634
|
+
...buildEnabledWebMcpConfig(),
|
|
1635
|
+
tools: declaredAdminTools,
|
|
1636
|
+
toolExecutors: adminToolExecutors,
|
|
1637
|
+
},
|
|
1629
1638
|
persistence: {
|
|
1630
1639
|
saveToFile,
|
|
1631
1640
|
...(coldSave ? { coldSave } : {}),
|
|
@@ -2653,6 +2662,7 @@ cat << 'END_OF_FILE_CONTENT' > "src/components/form-demo/index.ts"
|
|
|
2653
2662
|
export { FormDemoView } from './View';
|
|
2654
2663
|
export { FormDemoClient } from './FormDemoClient';
|
|
2655
2664
|
export { FormDemoSchema, FormDemoSettingsSchema, FormDemoSubmissionSchema } from './schema';
|
|
2665
|
+
export { webmcpTools } from './webmcpTools';
|
|
2656
2666
|
export type { FormDemoData, FormDemoSettings } from './types';
|
|
2657
2667
|
|
|
2658
2668
|
END_OF_FILE_CONTENT
|
|
@@ -2680,6 +2690,173 @@ export const FormDemoSubmissionSchema = z.object({
|
|
|
2680
2690
|
message: z.string().min(1).describe('Free-form message body'),
|
|
2681
2691
|
});
|
|
2682
2692
|
|
|
2693
|
+
END_OF_FILE_CONTENT
|
|
2694
|
+
echo "Creating src/components/form-demo/submitFormLead.test.ts..."
|
|
2695
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/form-demo/submitFormLead.test.ts"
|
|
2696
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2697
|
+
|
|
2698
|
+
const CLOUD_URL = 'https://cloud.example.com';
|
|
2699
|
+
const API_KEY = 'secret-key';
|
|
2700
|
+
|
|
2701
|
+
async function loadSubmitFormLead() {
|
|
2702
|
+
vi.resetModules();
|
|
2703
|
+
vi.stubEnv('NEXT_PUBLIC_OLONJS_CLOUD_URL', CLOUD_URL);
|
|
2704
|
+
vi.stubEnv('NEXT_PUBLIC_OLONJS_API_KEY', API_KEY);
|
|
2705
|
+
return await import('./submitFormLead');
|
|
2706
|
+
}
|
|
2707
|
+
|
|
2708
|
+
beforeEach(() => {
|
|
2709
|
+
vi.stubGlobal('fetch', vi.fn());
|
|
2710
|
+
});
|
|
2711
|
+
|
|
2712
|
+
afterEach(() => {
|
|
2713
|
+
vi.unstubAllEnvs();
|
|
2714
|
+
vi.unstubAllGlobals();
|
|
2715
|
+
});
|
|
2716
|
+
|
|
2717
|
+
function mockFetchOk() {
|
|
2718
|
+
const fetchMock = vi.mocked(fetch);
|
|
2719
|
+
fetchMock.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 }));
|
|
2720
|
+
return fetchMock;
|
|
2721
|
+
}
|
|
2722
|
+
|
|
2723
|
+
describe('submitFormLead', () => {
|
|
2724
|
+
it('POSTs to {cloudUrl}/form/send with parity headers and full payload', async () => {
|
|
2725
|
+
const { submitFormLead } = await loadSubmitFormLead();
|
|
2726
|
+
const fetchMock = mockFetchOk();
|
|
2727
|
+
|
|
2728
|
+
const ctx = {
|
|
2729
|
+
recipientEmail: 'lead@example.com',
|
|
2730
|
+
sectionId: 'form-demo-1',
|
|
2731
|
+
pageSlug: 'contatti',
|
|
2732
|
+
};
|
|
2733
|
+
const args = { name: 'Ada', email: 'ada@example.com', message: 'Ciao' };
|
|
2734
|
+
|
|
2735
|
+
await expect(submitFormLead(args, ctx)).resolves.toEqual({ ok: true });
|
|
2736
|
+
|
|
2737
|
+
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
|
|
2738
|
+
expect(url).toBe(`${CLOUD_URL}/form/send`);
|
|
2739
|
+
const headers = init.headers as Record<string, string>;
|
|
2740
|
+
expect(headers.Authorization).toBe(`Bearer ${API_KEY}`);
|
|
2741
|
+
expect(headers['Content-Type']).toBe('application/json');
|
|
2742
|
+
expect(headers['Idempotency-Key']).toMatch(/^form-form-demo-1-\d+$/);
|
|
2743
|
+
const payload = JSON.parse(init.body as string) as Record<string, unknown>;
|
|
2744
|
+
expect(payload).toMatchObject({
|
|
2745
|
+
name: 'Ada',
|
|
2746
|
+
email: 'ada@example.com',
|
|
2747
|
+
message: 'Ciao',
|
|
2748
|
+
recipientEmail: 'lead@example.com',
|
|
2749
|
+
page: 'contatti',
|
|
2750
|
+
section: 'form-demo-1',
|
|
2751
|
+
source: 'olon-webmcp',
|
|
2752
|
+
});
|
|
2753
|
+
expect(typeof payload.submittedAt).toBe('string');
|
|
2754
|
+
});
|
|
2755
|
+
|
|
2756
|
+
it('uses the dev-declared path, normalizing slashes', async () => {
|
|
2757
|
+
const { submitFormLead } = await loadSubmitFormLead();
|
|
2758
|
+
const fetchMock = mockFetchOk();
|
|
2759
|
+
|
|
2760
|
+
await submitFormLead(
|
|
2761
|
+
{ name: 'A', email: 'a@b.c', message: 'm' },
|
|
2762
|
+
{ recipientEmail: '', sectionId: 's1', pageSlug: 'x', path: '/lead/submit/' },
|
|
2763
|
+
);
|
|
2764
|
+
|
|
2765
|
+
expect((fetchMock.mock.calls[0] as [string])[0]).toBe(`${CLOUD_URL}/lead/submit`);
|
|
2766
|
+
});
|
|
2767
|
+
|
|
2768
|
+
it('throws a readable error on a non-ok response', async () => {
|
|
2769
|
+
const { submitFormLead } = await loadSubmitFormLead();
|
|
2770
|
+
vi.mocked(fetch).mockResolvedValue(
|
|
2771
|
+
new Response(JSON.stringify({ error: 'bad gateway' }), { status: 502 }),
|
|
2772
|
+
);
|
|
2773
|
+
|
|
2774
|
+
await expect(
|
|
2775
|
+
submitFormLead(
|
|
2776
|
+
{ name: 'A', email: 'a@b.c', message: 'm' },
|
|
2777
|
+
{ recipientEmail: '', sectionId: 's1', pageSlug: 'x' },
|
|
2778
|
+
),
|
|
2779
|
+
).rejects.toThrow(/bad gateway/);
|
|
2780
|
+
});
|
|
2781
|
+
|
|
2782
|
+
it('throws an explicit error when the cloud env is not configured', async () => {
|
|
2783
|
+
vi.resetModules();
|
|
2784
|
+
vi.stubEnv('NEXT_PUBLIC_OLONJS_CLOUD_URL', '');
|
|
2785
|
+
vi.stubEnv('NEXT_PUBLIC_OLONJS_API_KEY', '');
|
|
2786
|
+
const { submitFormLead } = await import('./submitFormLead');
|
|
2787
|
+
|
|
2788
|
+
await expect(
|
|
2789
|
+
submitFormLead(
|
|
2790
|
+
{ name: 'A', email: 'a@b.c', message: 'm' },
|
|
2791
|
+
{ recipientEmail: '', sectionId: 's1', pageSlug: 'x' },
|
|
2792
|
+
),
|
|
2793
|
+
).rejects.toThrow(/Cloud env not configured/);
|
|
2794
|
+
});
|
|
2795
|
+
});
|
|
2796
|
+
|
|
2797
|
+
END_OF_FILE_CONTENT
|
|
2798
|
+
echo "Creating src/components/form-demo/submitFormLead.ts..."
|
|
2799
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/form-demo/submitFormLead.ts"
|
|
2800
|
+
import { cloudPolicy } from '@/lib/env/tenantEnv';
|
|
2801
|
+
|
|
2802
|
+
export interface SubmitFormArgs {
|
|
2803
|
+
name: string;
|
|
2804
|
+
email: string;
|
|
2805
|
+
message: string;
|
|
2806
|
+
}
|
|
2807
|
+
|
|
2808
|
+
export interface SubmitFormContext {
|
|
2809
|
+
recipientEmail: string;
|
|
2810
|
+
sectionId: string;
|
|
2811
|
+
pageSlug: string;
|
|
2812
|
+
/** The dev-declared endpoint path (the variable piece); default 'form/send'. */
|
|
2813
|
+
path?: string;
|
|
2814
|
+
}
|
|
2815
|
+
|
|
2816
|
+
/**
|
|
2817
|
+
* Shared submit executor for form sections (spec 03-webmcp-tool-submit-plugin).
|
|
2818
|
+
*
|
|
2819
|
+
* Base URL always comes from the cloud env chain (`tenantEnv`/`cloudPolicy`);
|
|
2820
|
+
* the dev declares only the variable path. Header parity with tenant-alpha's
|
|
2821
|
+
* `useOlonForms` (Bearer + Idempotency-Key).
|
|
2822
|
+
*/
|
|
2823
|
+
export async function submitFormLead(
|
|
2824
|
+
args: SubmitFormArgs,
|
|
2825
|
+
ctx: SubmitFormContext,
|
|
2826
|
+
): Promise<{ ok: true }> {
|
|
2827
|
+
const base = (cloudPolicy.apiUrl ?? '').replace(/\/$/, '');
|
|
2828
|
+
if (!base || !cloudPolicy.apiKey) {
|
|
2829
|
+
throw new Error('Cloud env not configured (NEXT_PUBLIC_OLONJS_*).');
|
|
2830
|
+
}
|
|
2831
|
+
|
|
2832
|
+
const path = (ctx.path ?? 'form/send').replace(/^\/+|\/+$/g, '');
|
|
2833
|
+
const endpoint = `${base}/${path}`;
|
|
2834
|
+
|
|
2835
|
+
const response = await fetch(endpoint, {
|
|
2836
|
+
method: 'POST',
|
|
2837
|
+
headers: {
|
|
2838
|
+
Authorization: `Bearer ${cloudPolicy.apiKey}`,
|
|
2839
|
+
'Content-Type': 'application/json',
|
|
2840
|
+
'Idempotency-Key': `form-${ctx.sectionId}-${Date.now()}`,
|
|
2841
|
+
},
|
|
2842
|
+
body: JSON.stringify({
|
|
2843
|
+
...args,
|
|
2844
|
+
recipientEmail: ctx.recipientEmail,
|
|
2845
|
+
page: ctx.pageSlug,
|
|
2846
|
+
section: ctx.sectionId,
|
|
2847
|
+
source: 'olon-webmcp',
|
|
2848
|
+
submittedAt: new Date().toISOString(),
|
|
2849
|
+
}),
|
|
2850
|
+
});
|
|
2851
|
+
|
|
2852
|
+
const body = (await response.json().catch(() => ({}))) as { error?: string };
|
|
2853
|
+
if (!response.ok) {
|
|
2854
|
+
throw new Error(body.error ?? `Submit failed (${response.status})`);
|
|
2855
|
+
}
|
|
2856
|
+
|
|
2857
|
+
return { ok: true };
|
|
2858
|
+
}
|
|
2859
|
+
|
|
2683
2860
|
END_OF_FILE_CONTENT
|
|
2684
2861
|
echo "Creating src/components/form-demo/types.ts..."
|
|
2685
2862
|
cat << 'END_OF_FILE_CONTENT' > "src/components/form-demo/types.ts"
|
|
@@ -2689,6 +2866,27 @@ import { FormDemoSchema, FormDemoSettingsSchema } from './schema';
|
|
|
2689
2866
|
export type FormDemoData = z.infer<typeof FormDemoSchema>;
|
|
2690
2867
|
export type FormDemoSettings = z.infer<typeof FormDemoSettingsSchema>;
|
|
2691
2868
|
|
|
2869
|
+
END_OF_FILE_CONTENT
|
|
2870
|
+
echo "Creating src/components/form-demo/webmcpTools.ts..."
|
|
2871
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/form-demo/webmcpTools.ts"
|
|
2872
|
+
import type { WebMcpToolDef } from '@olonjs/core';
|
|
2873
|
+
import { FormDemoSubmissionSchema } from './schema';
|
|
2874
|
+
|
|
2875
|
+
/**
|
|
2876
|
+
* Public WebMCP tools declared by the form-demo capsule (spec 03).
|
|
2877
|
+
* Serializable defs only — the executor is registered client-side in
|
|
2878
|
+
* `src/lib/webmcp/submitFormTool.ts` (functions cannot cross the RSC boundary).
|
|
2879
|
+
*/
|
|
2880
|
+
export const webmcpTools: readonly WebMcpToolDef[] = [
|
|
2881
|
+
{
|
|
2882
|
+
name: 'submit-form',
|
|
2883
|
+
description: 'Submit the contact form on this page (name, email, message).',
|
|
2884
|
+
surface: 'public',
|
|
2885
|
+
inputSchema: FormDemoSubmissionSchema.toJSONSchema() as Record<string, unknown>,
|
|
2886
|
+
annotations: { consequentialHint: true },
|
|
2887
|
+
},
|
|
2888
|
+
];
|
|
2889
|
+
|
|
2692
2890
|
END_OF_FILE_CONTENT
|
|
2693
2891
|
mkdir -p "src/components/header"
|
|
2694
2892
|
echo "Creating src/components/header/View.tsx..."
|
|
@@ -2963,16 +3161,38 @@ cat << 'END_OF_FILE_CONTENT' > "src/components/webmcp/WebMcpVisitorRuntime.tsx"
|
|
|
2963
3161
|
'use client';
|
|
2964
3162
|
|
|
2965
3163
|
import { useEffect } from 'react';
|
|
3164
|
+
import { registerWebMcpTool } from '@olonjs/core';
|
|
2966
3165
|
import { bootstrapVisitorWebMcpRuntime } from '@/lib/webmcp/bootstrapVisitorWebMcpRuntime';
|
|
3166
|
+
// Side effect: registers the submit-form executor (spec 03). Client-only import.
|
|
3167
|
+
import '@/lib/webmcp/submitFormTool';
|
|
3168
|
+
import { visitorToolDefs } from '@/lib/webmcp/visitorToolDefs';
|
|
2967
3169
|
|
|
2968
3170
|
/**
|
|
2969
|
-
* Client island: WebMCP imperative API on public pages
|
|
2970
|
-
*
|
|
3171
|
+
* Client island: WebMCP imperative API on public pages.
|
|
3172
|
+
* Installs the polyfill and registers the public tools enabled for the page
|
|
3173
|
+
* (spec 02-webmcp-tool-public-runtime). Must not import @olonjs/studio.
|
|
3174
|
+
*
|
|
3175
|
+
* `toolNames` are computed server-side from the sections present on the page
|
|
3176
|
+
* (functions cannot cross the RSC boundary — only names travel).
|
|
2971
3177
|
*/
|
|
2972
|
-
export function WebMcpVisitorRuntime() {
|
|
3178
|
+
export function WebMcpVisitorRuntime({ toolNames = [] }: { toolNames?: string[] }) {
|
|
2973
3179
|
useEffect(() => {
|
|
2974
3180
|
bootstrapVisitorWebMcpRuntime();
|
|
2975
|
-
|
|
3181
|
+
const unregister = toolNames.map((name) => {
|
|
3182
|
+
const def = visitorToolDefs[name];
|
|
3183
|
+
if (!def) {
|
|
3184
|
+
console.warn(`[WebMCP] visitor tool "${name}" has no executor definition.`);
|
|
3185
|
+
return () => undefined;
|
|
3186
|
+
}
|
|
3187
|
+
return registerWebMcpTool({
|
|
3188
|
+
...def,
|
|
3189
|
+
inputSchema: def.inputSchema ?? { type: 'object', additionalProperties: false, properties: {} },
|
|
3190
|
+
});
|
|
3191
|
+
});
|
|
3192
|
+
return () => {
|
|
3193
|
+
unregister.forEach((stop) => stop());
|
|
3194
|
+
};
|
|
3195
|
+
}, [toolNames]);
|
|
2976
3196
|
|
|
2977
3197
|
return null;
|
|
2978
3198
|
}
|
|
@@ -5966,6 +6186,66 @@ export function buildEnabledWebMcpConfig(namespace?: string): {
|
|
|
5966
6186
|
};
|
|
5967
6187
|
}
|
|
5968
6188
|
|
|
6189
|
+
END_OF_FILE_CONTENT
|
|
6190
|
+
echo "Creating src/lib/webmcp/resolvePublicToolNames.test.ts..."
|
|
6191
|
+
cat << 'END_OF_FILE_CONTENT' > "src/lib/webmcp/resolvePublicToolNames.test.ts"
|
|
6192
|
+
import { describe, expect, it } from 'vitest';
|
|
6193
|
+
import { resolvePublicToolNames } from './resolvePublicToolNames';
|
|
6194
|
+
|
|
6195
|
+
const sectionPublicTools = {
|
|
6196
|
+
'form-demo': ['submit-form'],
|
|
6197
|
+
'shop-catalog': ['search-product', 'add-to-cart'],
|
|
6198
|
+
};
|
|
6199
|
+
|
|
6200
|
+
describe('resolvePublicToolNames', () => {
|
|
6201
|
+
it('returns the public tools of the sections present on the page, deduplicated, in page order', () => {
|
|
6202
|
+
const sections = [
|
|
6203
|
+
{ type: 'form-demo' },
|
|
6204
|
+
{ type: 'hero' },
|
|
6205
|
+
{ type: 'form-demo' },
|
|
6206
|
+
{ type: 'shop-catalog' },
|
|
6207
|
+
];
|
|
6208
|
+
expect(resolvePublicToolNames(sections, sectionPublicTools)).toEqual([
|
|
6209
|
+
'submit-form',
|
|
6210
|
+
'search-product',
|
|
6211
|
+
'add-to-cart',
|
|
6212
|
+
]);
|
|
6213
|
+
});
|
|
6214
|
+
|
|
6215
|
+
it('returns an empty list when no section declares tools', () => {
|
|
6216
|
+
expect(resolvePublicToolNames([{ type: 'hero' }], sectionPublicTools)).toEqual([]);
|
|
6217
|
+
});
|
|
6218
|
+
|
|
6219
|
+
it('returns an empty list for an empty page', () => {
|
|
6220
|
+
expect(resolvePublicToolNames([], sectionPublicTools)).toEqual([]);
|
|
6221
|
+
});
|
|
6222
|
+
|
|
6223
|
+
it('ignores tool names not present in the section map', () => {
|
|
6224
|
+
expect(resolvePublicToolNames([{ type: 'footer' }], sectionPublicTools)).toEqual([]);
|
|
6225
|
+
});
|
|
6226
|
+
});
|
|
6227
|
+
|
|
6228
|
+
END_OF_FILE_CONTENT
|
|
6229
|
+
echo "Creating src/lib/webmcp/resolvePublicToolNames.ts..."
|
|
6230
|
+
cat << 'END_OF_FILE_CONTENT' > "src/lib/webmcp/resolvePublicToolNames.ts"
|
|
6231
|
+
/**
|
|
6232
|
+
* Pure helper (spec 02-webmcp-tool-public-runtime): computes which public
|
|
6233
|
+
* tool names are enabled for a page, from the section types present.
|
|
6234
|
+
* Deduplicated, in page order.
|
|
6235
|
+
*/
|
|
6236
|
+
export function resolvePublicToolNames(
|
|
6237
|
+
sections: ReadonlyArray<{ type: string }>,
|
|
6238
|
+
sectionPublicTools: Record<string, readonly string[]>,
|
|
6239
|
+
): string[] {
|
|
6240
|
+
const out: string[] = [];
|
|
6241
|
+
for (const section of sections) {
|
|
6242
|
+
for (const name of sectionPublicTools[section.type] ?? []) {
|
|
6243
|
+
if (!out.includes(name)) out.push(name);
|
|
6244
|
+
}
|
|
6245
|
+
}
|
|
6246
|
+
return out;
|
|
6247
|
+
}
|
|
6248
|
+
|
|
5969
6249
|
END_OF_FILE_CONTENT
|
|
5970
6250
|
mkdir -p "src/lib/webmcp/runtime"
|
|
5971
6251
|
echo "Creating src/lib/webmcp/runtime/agenticSurface.test.ts..."
|
|
@@ -6123,6 +6403,36 @@ describe('robots + sitemap', () => {
|
|
|
6123
6403
|
expect(xml).toContain('<lastmod>2026-09-10T10:00:00Z</lastmod>');
|
|
6124
6404
|
});
|
|
6125
6405
|
});
|
|
6406
|
+
|
|
6407
|
+
describe('declared public tools in runtime contracts', () => {
|
|
6408
|
+
const surfaceInput = {
|
|
6409
|
+
bundle,
|
|
6410
|
+
schemas,
|
|
6411
|
+
declaredTools: [
|
|
6412
|
+
{
|
|
6413
|
+
name: 'submit-form',
|
|
6414
|
+
description: 'Submit the contact form',
|
|
6415
|
+
surface: 'public',
|
|
6416
|
+
inputSchema: { type: 'object', properties: {} },
|
|
6417
|
+
},
|
|
6418
|
+
] as const,
|
|
6419
|
+
};
|
|
6420
|
+
|
|
6421
|
+
it('emits declared tools with their surface in the page contract', () => {
|
|
6422
|
+
const contract = buildRuntimePageContract({ ...surfaceInput, slug: 'home' });
|
|
6423
|
+
const submit = contract!.tools.find((t) => t.name === 'submit-form');
|
|
6424
|
+
expect(submit).toBeDefined();
|
|
6425
|
+
expect((submit as { surface?: string }).surface).toBe('public');
|
|
6426
|
+
expect(contract!.tools.map((t) => t.name)).toEqual(['update-section', 'save', 'submit-form']);
|
|
6427
|
+
});
|
|
6428
|
+
|
|
6429
|
+
it('emits declared tools in the page manifest tools list with surface', () => {
|
|
6430
|
+
const manifest = buildRuntimePageManifest({ ...surfaceInput, slug: 'home' });
|
|
6431
|
+
const submit = manifest!.tools.find((t) => t.name === 'submit-form');
|
|
6432
|
+
expect(submit).toBeDefined();
|
|
6433
|
+
expect((submit as { surface?: string }).surface).toBe('public');
|
|
6434
|
+
});
|
|
6435
|
+
});
|
|
6126
6436
|
|
|
6127
6437
|
END_OF_FILE_CONTENT
|
|
6128
6438
|
echo "Creating src/lib/webmcp/runtime/agenticSurface.ts..."
|
|
@@ -6141,6 +6451,7 @@ import {
|
|
|
6141
6451
|
webmcp,
|
|
6142
6452
|
type JsonPagesConfig,
|
|
6143
6453
|
type PageConfig,
|
|
6454
|
+
type WebMcpToolDef,
|
|
6144
6455
|
} from '@olonjs/core';
|
|
6145
6456
|
import type { PublicPageContentBundle } from '@olonjs/next/server';
|
|
6146
6457
|
|
|
@@ -6157,6 +6468,8 @@ export type RuntimeSurfaceInput = {
|
|
|
6157
6468
|
bundle: PublicPageContentBundle;
|
|
6158
6469
|
schemas: JsonPagesConfig['schemas'];
|
|
6159
6470
|
submissionSchemas?: JsonPagesConfig['submissionSchemas'];
|
|
6471
|
+
/** Tenant-declared tools (public surface), appended to the contract's built-ins. */
|
|
6472
|
+
declaredTools?: readonly WebMcpToolDef[];
|
|
6160
6473
|
};
|
|
6161
6474
|
|
|
6162
6475
|
function joinSegments(segments: readonly string[]): string {
|
|
@@ -6245,6 +6558,7 @@ export function buildRuntimeSiteManifest(input: RuntimeSurfaceInput) {
|
|
|
6245
6558
|
siteConfig: input.bundle.siteConfig,
|
|
6246
6559
|
// Bundle carries SchemaLike; the manifest builder expects the Zod registry (same objects).
|
|
6247
6560
|
collectionSchemas: input.bundle.collectionSchemas as never,
|
|
6561
|
+
declaredTools: input.declaredTools,
|
|
6248
6562
|
});
|
|
6249
6563
|
}
|
|
6250
6564
|
|
|
@@ -6257,6 +6571,7 @@ export function buildRuntimePageManifest(input: RuntimeSurfaceInput & { slug: st
|
|
|
6257
6571
|
schemas: input.schemas,
|
|
6258
6572
|
submissionSchemas: input.submissionSchemas,
|
|
6259
6573
|
siteConfig: input.bundle.siteConfig,
|
|
6574
|
+
declaredTools: input.declaredTools,
|
|
6260
6575
|
});
|
|
6261
6576
|
}
|
|
6262
6577
|
|
|
@@ -6269,6 +6584,7 @@ export function buildRuntimePageContract(input: RuntimeSurfaceInput & { slug: st
|
|
|
6269
6584
|
schemas: input.schemas,
|
|
6270
6585
|
submissionSchemas: input.submissionSchemas,
|
|
6271
6586
|
siteConfig: input.bundle.siteConfig,
|
|
6587
|
+
declaredTools: input.declaredTools,
|
|
6272
6588
|
});
|
|
6273
6589
|
}
|
|
6274
6590
|
|
|
@@ -6287,6 +6603,7 @@ export function buildRuntimeLlmsTxt(input: RuntimeSurfaceInput): string {
|
|
|
6287
6603
|
schemas: input.schemas,
|
|
6288
6604
|
submissionSchemas: input.submissionSchemas,
|
|
6289
6605
|
siteConfig: input.bundle.siteConfig,
|
|
6606
|
+
declaredTools: input.declaredTools,
|
|
6290
6607
|
});
|
|
6291
6608
|
}
|
|
6292
6609
|
|
|
@@ -6396,6 +6713,7 @@ import { readServerCloudPolicy } from '@/lib/env/serverCloudPolicy';
|
|
|
6396
6713
|
import { loadLocalPublicPageBundle } from '@/lib/loaders/loadLocalPublicPageBundle';
|
|
6397
6714
|
import { loadPublicPageBundleForRequest } from '@/lib/loaders/loadPublicPageBundleForRequest';
|
|
6398
6715
|
import { SECTION_SCHEMAS, SECTION_SUBMISSION_SCHEMAS } from '@/lib/schemas';
|
|
6716
|
+
import { declaredPublicTools } from '@/lib/webmcp/toolRegistry';
|
|
6399
6717
|
import type { RuntimeSurfaceInput } from './agenticSurface';
|
|
6400
6718
|
|
|
6401
6719
|
const schemas = SECTION_SCHEMAS as unknown as JsonPagesConfig['schemas'];
|
|
@@ -6418,7 +6736,7 @@ export async function loadRuntimeSurfaceForSlug(input: {
|
|
|
6418
6736
|
apiUrl: policy.apiUrl,
|
|
6419
6737
|
apiKey: policy.apiKey,
|
|
6420
6738
|
});
|
|
6421
|
-
return { bundle, schemas, submissionSchemas };
|
|
6739
|
+
return { bundle, schemas, submissionSchemas, declaredTools: declaredPublicTools };
|
|
6422
6740
|
}
|
|
6423
6741
|
|
|
6424
6742
|
/**
|
|
@@ -6427,7 +6745,7 @@ export async function loadRuntimeSurfaceForSlug(input: {
|
|
|
6427
6745
|
*/
|
|
6428
6746
|
export function loadRuntimeSurfaceForSite(): RuntimeSurfaceInput {
|
|
6429
6747
|
const bundle = loadLocalPublicPageBundle(path.resolve(process.cwd()));
|
|
6430
|
-
return { bundle, schemas, submissionSchemas };
|
|
6748
|
+
return { bundle, schemas, submissionSchemas, declaredTools: declaredPublicTools };
|
|
6431
6749
|
}
|
|
6432
6750
|
|
|
6433
6751
|
/** Public base URL for absolute links (robots/sitemap). */
|
|
@@ -6487,6 +6805,236 @@ describe('next.config rewrites — runtime agentic surface', () => {
|
|
|
6487
6805
|
});
|
|
6488
6806
|
});
|
|
6489
6807
|
|
|
6808
|
+
END_OF_FILE_CONTENT
|
|
6809
|
+
echo "Creating src/lib/webmcp/submitFormTool.test.ts..."
|
|
6810
|
+
cat << 'END_OF_FILE_CONTENT' > "src/lib/webmcp/submitFormTool.test.ts"
|
|
6811
|
+
// @vitest-environment jsdom
|
|
6812
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
6813
|
+
|
|
6814
|
+
const CLOUD_URL = 'https://cloud.example.com';
|
|
6815
|
+
|
|
6816
|
+
async function loadModules() {
|
|
6817
|
+
vi.resetModules();
|
|
6818
|
+
vi.stubEnv('NEXT_PUBLIC_OLONJS_CLOUD_URL', CLOUD_URL);
|
|
6819
|
+
vi.stubEnv('NEXT_PUBLIC_OLONJS_API_KEY', 'k');
|
|
6820
|
+
const [{ sectionPublicTools, declaredPublicTools }, { visitorToolDefs }] = await Promise.all([
|
|
6821
|
+
import('@/lib/webmcp/toolRegistry'),
|
|
6822
|
+
import('@/lib/webmcp/visitorToolDefs'),
|
|
6823
|
+
]);
|
|
6824
|
+
// Same side-effect import the visitor island performs: registers the executor.
|
|
6825
|
+
await import('@/lib/webmcp/submitFormTool');
|
|
6826
|
+
return { sectionPublicTools, declaredPublicTools, visitorToolDefs };
|
|
6827
|
+
}
|
|
6828
|
+
|
|
6829
|
+
beforeEach(() => {
|
|
6830
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })));
|
|
6831
|
+
});
|
|
6832
|
+
|
|
6833
|
+
afterEach(() => {
|
|
6834
|
+
vi.unstubAllEnvs();
|
|
6835
|
+
vi.unstubAllGlobals();
|
|
6836
|
+
document.body.innerHTML = '';
|
|
6837
|
+
});
|
|
6838
|
+
|
|
6839
|
+
describe('submit-form tool wiring', () => {
|
|
6840
|
+
it('maps form-demo to submit-form and declares the public tool def', async () => {
|
|
6841
|
+
const { sectionPublicTools, declaredPublicTools } = await loadModules();
|
|
6842
|
+
expect(sectionPublicTools['form-demo']).toEqual(['submit-form']);
|
|
6843
|
+
|
|
6844
|
+
const def = declaredPublicTools.find((t) => t.name === 'submit-form');
|
|
6845
|
+
expect(def).toBeDefined();
|
|
6846
|
+
expect(def!.surface).toBe('public');
|
|
6847
|
+
expect(def!.inputSchema).toMatchObject({
|
|
6848
|
+
type: 'object',
|
|
6849
|
+
properties: { name: { type: 'string' }, email: { type: 'string' }, message: { type: 'string' } },
|
|
6850
|
+
});
|
|
6851
|
+
});
|
|
6852
|
+
|
|
6853
|
+
it('registers the executor and submits a valid payload to {cloudUrl}/form/send', async () => {
|
|
6854
|
+
const { visitorToolDefs } = await loadModules();
|
|
6855
|
+
document.body.innerHTML =
|
|
6856
|
+
'<form id="form-demo" data-olon-recipient="lead@example.com"></form>';
|
|
6857
|
+
|
|
6858
|
+
const tool = visitorToolDefs['submit-form'];
|
|
6859
|
+
expect(tool).toBeDefined();
|
|
6860
|
+
|
|
6861
|
+
await expect(
|
|
6862
|
+
tool.execute({ name: 'Ada', email: 'ada@example.com', message: 'Ciao' }),
|
|
6863
|
+
).resolves.toEqual({ ok: true });
|
|
6864
|
+
|
|
6865
|
+
const [url, init] = vi.mocked(fetch).mock.calls[0] as [string, RequestInit];
|
|
6866
|
+
expect(url).toBe(`${CLOUD_URL}/form/send`);
|
|
6867
|
+
const payload = JSON.parse(init.body as string) as Record<string, unknown>;
|
|
6868
|
+
expect(payload).toMatchObject({
|
|
6869
|
+
recipientEmail: 'lead@example.com',
|
|
6870
|
+
section: 'form-demo',
|
|
6871
|
+
source: 'olon-webmcp',
|
|
6872
|
+
});
|
|
6873
|
+
});
|
|
6874
|
+
|
|
6875
|
+
it('rejects an invalid payload with a Zod validation error before any fetch', async () => {
|
|
6876
|
+
const { visitorToolDefs } = await loadModules();
|
|
6877
|
+
const tool = visitorToolDefs['submit-form'];
|
|
6878
|
+
|
|
6879
|
+
await expect(tool.execute({ name: '', email: 'not-an-email', message: '' })).rejects.toThrow();
|
|
6880
|
+
expect(vi.mocked(fetch)).not.toHaveBeenCalled();
|
|
6881
|
+
});
|
|
6882
|
+
});
|
|
6883
|
+
|
|
6884
|
+
END_OF_FILE_CONTENT
|
|
6885
|
+
echo "Creating src/lib/webmcp/submitFormTool.ts..."
|
|
6886
|
+
cat << 'END_OF_FILE_CONTENT' > "src/lib/webmcp/submitFormTool.ts"
|
|
6887
|
+
import { FormDemoSubmissionSchema } from '@/components/form-demo/schema';
|
|
6888
|
+
import { submitFormLead, type SubmitFormArgs } from '@/components/form-demo/submitFormLead';
|
|
6889
|
+
import { webmcpTools } from '@/components/form-demo/webmcpTools';
|
|
6890
|
+
import { visitorToolDefs } from './visitorToolDefs';
|
|
6891
|
+
|
|
6892
|
+
const submitFormDef = webmcpTools.find((tool) => tool.name === 'submit-form');
|
|
6893
|
+
if (!submitFormDef) {
|
|
6894
|
+
throw new Error('[WebMCP] form-demo capsule must declare the submit-form tool.');
|
|
6895
|
+
}
|
|
6896
|
+
|
|
6897
|
+
/**
|
|
6898
|
+
* Client-side executor registration for submit-form (spec 03).
|
|
6899
|
+
*
|
|
6900
|
+
* Imported as a side effect ONLY by the visitor island — never by server code:
|
|
6901
|
+
* the executor chain (`submitFormLead` → `tenantEnv` → `@olonjs/react`) is
|
|
6902
|
+
* client-bound. Validation uses the section's submission schema (ADR-0002),
|
|
6903
|
+
* the same shape agents see in `sectionSubmissionSchemas` of the page contract.
|
|
6904
|
+
*/
|
|
6905
|
+
visitorToolDefs['submit-form'] = {
|
|
6906
|
+
...submitFormDef,
|
|
6907
|
+
execute: async (rawArgs: unknown) => {
|
|
6908
|
+
const parsed = FormDemoSubmissionSchema.parse(rawArgs);
|
|
6909
|
+
const form = document.querySelector<HTMLFormElement>('form[data-olon-recipient]');
|
|
6910
|
+
const args = rawArgs as Record<string, unknown>;
|
|
6911
|
+
const sectionId =
|
|
6912
|
+
typeof args.sectionId === 'string' && args.sectionId.trim()
|
|
6913
|
+
? args.sectionId.trim()
|
|
6914
|
+
: form?.id || 'form-demo';
|
|
6915
|
+
return submitFormLead(parsed as SubmitFormArgs, {
|
|
6916
|
+
recipientEmail: form?.dataset.olonRecipient ?? '',
|
|
6917
|
+
sectionId,
|
|
6918
|
+
pageSlug: window.location.pathname,
|
|
6919
|
+
path: 'form/send',
|
|
6920
|
+
});
|
|
6921
|
+
},
|
|
6922
|
+
};
|
|
6923
|
+
|
|
6924
|
+
END_OF_FILE_CONTENT
|
|
6925
|
+
echo "Creating src/lib/webmcp/toolRegistry.ts..."
|
|
6926
|
+
cat << 'END_OF_FILE_CONTENT' > "src/lib/webmcp/toolRegistry.ts"
|
|
6927
|
+
import type { WebMcpToolDef } from '@olonjs/core';
|
|
6928
|
+
import { webmcpTools as formDemoTools } from '@/components/form-demo';
|
|
6929
|
+
|
|
6930
|
+
/**
|
|
6931
|
+
* Tenant tool registry (spec 01-webmcp-tool-contract).
|
|
6932
|
+
*
|
|
6933
|
+
* Section capsules declare tools by exporting `webmcpTools` from their
|
|
6934
|
+
* `index.ts`. Aggregate them here:
|
|
6935
|
+
* - admin-surface defs + executors flow into the Studio island
|
|
6936
|
+
* (`AdminStudioClient` → `JsonPagesConfig.webmcp`);
|
|
6937
|
+
* - public-surface tool names flow into the visitor page via
|
|
6938
|
+
* `sectionPublicTools` (module 02).
|
|
6939
|
+
*/
|
|
6940
|
+
|
|
6941
|
+
/** Admin-surface tool defs registered in the Studio island. */
|
|
6942
|
+
export const declaredAdminTools: readonly WebMcpToolDef[] = [];
|
|
6943
|
+
|
|
6944
|
+
/** Runtime executors for admin tools, keyed by tool name. */
|
|
6945
|
+
export const adminToolExecutors: Readonly<
|
|
6946
|
+
Record<string, (args: unknown) => Promise<unknown> | unknown>
|
|
6947
|
+
> = {};
|
|
6948
|
+
|
|
6949
|
+
/** Public tool names exposed per section type (consumed by module 02). */
|
|
6950
|
+
export const sectionPublicTools: Record<string, readonly string[]> = {
|
|
6951
|
+
'form-demo': formDemoTools.map((tool) => tool.name),
|
|
6952
|
+
};
|
|
6953
|
+
|
|
6954
|
+
/** Public-surface tool defs emitted in the runtime page contracts (module 02). */
|
|
6955
|
+
export const declaredPublicTools: readonly WebMcpToolDef[] = [...formDemoTools];
|
|
6956
|
+
|
|
6957
|
+
END_OF_FILE_CONTENT
|
|
6958
|
+
echo "Creating src/lib/webmcp/visitorIsland.test.ts..."
|
|
6959
|
+
cat << 'END_OF_FILE_CONTENT' > "src/lib/webmcp/visitorIsland.test.ts"
|
|
6960
|
+
// @vitest-environment jsdom
|
|
6961
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
6962
|
+
import { act, createElement } from 'react';
|
|
6963
|
+
import { createRoot, type Root } from 'react-dom/client';
|
|
6964
|
+
import { WebMcpVisitorRuntime } from '@/components/webmcp/WebMcpVisitorRuntime';
|
|
6965
|
+
import { visitorToolDefs } from '@/lib/webmcp/visitorToolDefs';
|
|
6966
|
+
|
|
6967
|
+
function protocol() {
|
|
6968
|
+
return (document as unknown as {
|
|
6969
|
+
modelContextProtocol: {
|
|
6970
|
+
listTools: () => Array<{ name: string }>;
|
|
6971
|
+
executeTool: (name: string, inputArgsJson: string) => Promise<string>;
|
|
6972
|
+
};
|
|
6973
|
+
}).modelContextProtocol;
|
|
6974
|
+
}
|
|
6975
|
+
|
|
6976
|
+
let root: Root | null = null;
|
|
6977
|
+
let container: HTMLDivElement | null = null;
|
|
6978
|
+
|
|
6979
|
+
afterEach(() => {
|
|
6980
|
+
delete visitorToolDefs['test-tool'];
|
|
6981
|
+
if (root) {
|
|
6982
|
+
act(() => root!.unmount());
|
|
6983
|
+
root = null;
|
|
6984
|
+
}
|
|
6985
|
+
container?.remove();
|
|
6986
|
+
container = null;
|
|
6987
|
+
const doc = document as unknown as { modelContext?: unknown; modelContextProtocol?: unknown };
|
|
6988
|
+
delete doc.modelContext;
|
|
6989
|
+
delete doc.modelContextProtocol;
|
|
6990
|
+
const win = window as unknown as { __olonWebMcpTools__?: unknown; __olonWebMcpControllers__?: unknown };
|
|
6991
|
+
delete win.__olonWebMcpTools__;
|
|
6992
|
+
delete win.__olonWebMcpControllers__;
|
|
6993
|
+
vi.restoreAllMocks();
|
|
6994
|
+
});
|
|
6995
|
+
|
|
6996
|
+
function mount(toolNames: string[]) {
|
|
6997
|
+
container = document.createElement('div');
|
|
6998
|
+
document.body.appendChild(container);
|
|
6999
|
+
root = createRoot(container);
|
|
7000
|
+
act(() => {
|
|
7001
|
+
root!.render(createElement(WebMcpVisitorRuntime, { toolNames }));
|
|
7002
|
+
});
|
|
7003
|
+
}
|
|
7004
|
+
|
|
7005
|
+
describe('WebMcpVisitorRuntime — public tool registration', () => {
|
|
7006
|
+
it('registers the declared tools for the page and executes them', async () => {
|
|
7007
|
+
visitorToolDefs['test-tool'] = {
|
|
7008
|
+
name: 'test-tool',
|
|
7009
|
+
description: 'Test tool',
|
|
7010
|
+
surface: 'public',
|
|
7011
|
+
inputSchema: { type: 'object', additionalProperties: false, properties: {} },
|
|
7012
|
+
execute: async (args: unknown) => ({ ok: true, args }),
|
|
7013
|
+
};
|
|
7014
|
+
|
|
7015
|
+
mount(['test-tool', 'unknown-tool']);
|
|
7016
|
+
|
|
7017
|
+
await vi.waitFor(() => {
|
|
7018
|
+
expect(protocol().listTools().map((t) => t.name)).toContain('test-tool');
|
|
7019
|
+
});
|
|
7020
|
+
|
|
7021
|
+
const result = await protocol().executeTool('test-tool', '{"q":1}');
|
|
7022
|
+
expect(JSON.parse(result)).toEqual({ ok: true, args: { q: 1 } });
|
|
7023
|
+
|
|
7024
|
+
act(() => root!.unmount());
|
|
7025
|
+
root = null;
|
|
7026
|
+
|
|
7027
|
+
await vi.waitFor(() => {
|
|
7028
|
+
expect(protocol().listTools().map((t) => t.name)).not.toContain('test-tool');
|
|
7029
|
+
});
|
|
7030
|
+
});
|
|
7031
|
+
|
|
7032
|
+
it('does not register anything when the page has no tool names', () => {
|
|
7033
|
+
mount([]);
|
|
7034
|
+
expect(protocol().listTools()).toEqual([]);
|
|
7035
|
+
});
|
|
7036
|
+
});
|
|
7037
|
+
|
|
6490
7038
|
END_OF_FILE_CONTENT
|
|
6491
7039
|
echo "Creating src/lib/webmcp/visitorPageWebMcpWiring.test.ts..."
|
|
6492
7040
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/webmcp/visitorPageWebMcpWiring.test.ts"
|
|
@@ -6506,6 +7054,28 @@ describe('visitor page WebMCP wiring', () => {
|
|
|
6506
7054
|
});
|
|
6507
7055
|
});
|
|
6508
7056
|
|
|
7057
|
+
END_OF_FILE_CONTENT
|
|
7058
|
+
echo "Creating src/lib/webmcp/visitorToolDefs.ts..."
|
|
7059
|
+
cat << 'END_OF_FILE_CONTENT' > "src/lib/webmcp/visitorToolDefs.ts"
|
|
7060
|
+
import type { WebMcpToolDef } from '@olonjs/core';
|
|
7061
|
+
|
|
7062
|
+
export interface VisitorToolRegistration extends WebMcpToolDef {
|
|
7063
|
+
/** Client-side executor — the dev's domain logic (fetch, route handler, external service). */
|
|
7064
|
+
execute: (args: unknown) => Promise<unknown> | unknown;
|
|
7065
|
+
}
|
|
7066
|
+
|
|
7067
|
+
/**
|
|
7068
|
+
* Client-side registry of public tools: name → def with executor
|
|
7069
|
+
* (spec 02-webmcp-tool-public-runtime).
|
|
7070
|
+
*
|
|
7071
|
+
* The RSC page passes only the enabled tool NAMES (functions cannot cross the
|
|
7072
|
+
* RSC boundary); this registry resolves them to their executors at
|
|
7073
|
+
* registration time in the visitor island.
|
|
7074
|
+
*
|
|
7075
|
+
* Section capsules register here — module 03 adds 'submit-form'.
|
|
7076
|
+
*/
|
|
7077
|
+
export const visitorToolDefs: Record<string, VisitorToolRegistration> = {};
|
|
7078
|
+
|
|
6509
7079
|
END_OF_FILE_CONTENT
|
|
6510
7080
|
mkdir -p "src/sources"
|
|
6511
7081
|
echo "Creating src/sources/catalogo.test.ts..."
|