diff --git a/.nanites/run.json b/.nanites/run.json new file mode 100644 index 0000000..14fb6a9 --- /dev/null +++ b/.nanites/run.json @@ -0,0 +1,58 @@ +{ + "managerKey": "installation:122731586:repo:937499295", + "githubInstallationId": 122731586, + "githubRepositoryId": 937499295, + "repository": { + "id": 937499295, + "name": "demos", + "full_name": "hyparam/demos", + "owner": { + "login": "hyparam" + }, + "default_branch": "master", + "private": false, + "permissions": { + "admin": true, + "push": true, + "pull": true + } + }, + "runKey": "manual:27646492-52e1-432c-8d70-f8421affeac7", + "nanite": { + "id": "webmcp-maintainer", + "label": "WebMCP maintainer", + "purpose": "Implement a minimal WebMCP slice in the repo's main public-facing web app, prefer a landing or marketing surface when one exists, prefer the CDN script tag over package installs for browser-side WebMCP when possible, verify the real page behavior, and produce a support artifact suitable for a durable support PR.", + "variant": "workspace-browser", + "soul": "webmcp-soul", + "skills": [ + "webmcp-maintainer" + ], + "toolSurface": [ + "workspace", + "git", + "browser", + "publishing" + ], + "mcpServers": [ + { + "name": "WebMCP Documentation", + "url": "https://docs.mcp-b.ai/mcp" + } + ] + }, + "naniteId": "webmcp-maintainer", + "variant": "workspace-browser", + "trigger": { + "kind": "manual", + "label": "Manual WebMCP maintainer" + }, + "task": "Implement a minimal WebMCP slice in the repo's main public-facing web app, prefer a landing or marketing surface when one exists, prefer the CDN script tag over package installs for browser-side WebMCP when possible, verify the real page behavior, and produce a support artifact suitable for a durable support PR. Dogfood the existing codebase: inspect only enough to find the main public-facing web app, prefer a landing or marketing surface over dashboards when both exist, use the configured docs MCP server for the current WebMCP syntax, prefer the CDN script tag over package installs when adding browser-side WebMCP, ship one narrow tool instead of a catalog, verify the result, and finish with a clean artifact.", + "scope": { + "repositoryFullName": "hyparam/demos", + "branch": "master", + "changedFiles": [], + "detailsUrl": "https://app.sigvelo.com/repos/937499295?run=manual%3A27646492-52e1-432c-8d70-f8421affeac7" + }, + "initialGitHubResult": null, + "startedAt": "2026-04-09T20:17:44.595Z" +} \ No newline at end of file diff --git a/.sigvelo/nanites/bootstrap/webmcp-maintainer.md b/.sigvelo/nanites/bootstrap/webmcp-maintainer.md new file mode 100644 index 0000000..f328bef --- /dev/null +++ b/.sigvelo/nanites/bootstrap/webmcp-maintainer.md @@ -0,0 +1,4 @@ +# Sigvelo support PR bootstrap + +This file exists only so the support PR for `webmcp-maintainer` can be created immediately. +The runtime removes it on the first substantive publish when real repo changes are available. \ No newline at end of file diff --git a/hyparquet/index.html b/hyparquet/index.html index b2c7781..7fbf41f 100644 --- a/hyparquet/index.html +++ b/hyparquet/index.html @@ -24,6 +24,7 @@ - + + diff --git a/hyparquet/src/App.tsx b/hyparquet/src/App.tsx index fce0665..5cbf710 100644 --- a/hyparquet/src/App.tsx +++ b/hyparquet/src/App.tsx @@ -7,6 +7,7 @@ import { byteLengthFromUrl, parquetMetadataAsync } from 'hyparquet' import { AsyncBufferFrom, asyncBufferFrom, parquetDataFrame } from 'hyperparam' import { useCallback, useEffect, useState } from 'react' import Dropzone from './Dropzone.js' +import { setCurrentFile, setLoadUrlFn } from './webmcp.js' import Layout from './Layout.js' export default function App(): ReactNode { @@ -16,6 +17,9 @@ export default function App(): ReactNode { const [error, setError] = useState() const [pageProps, setPageProps] = useState() + // Register the URL loader with WebMCP + setLoadUrlFn(onUrlDrop) + const setUnknownError = useCallback((e: unknown) => { setError(e instanceof Error ? e : new Error(String(e))) }, []) @@ -25,6 +29,7 @@ export default function App(): ReactNode { const metadata = await parquetMetadataAsync(asyncBuffer) const df = sortableDataFrame(parquetDataFrame(from, metadata)) setPageProps({ metadata, df, name, byteLength: from.byteLength, setError: setUnknownError }) + setCurrentFile(name, 'url' in from ? from.url : undefined) }, [setUnknownError]) const onUrlDrop = useCallback( diff --git a/hyparquet/src/main.tsx b/hyparquet/src/main.tsx index 94a6585..1af6b67 100644 --- a/hyparquet/src/main.tsx +++ b/hyparquet/src/main.tsx @@ -4,8 +4,12 @@ import 'hyperparam/hyperparam.css' import { StrictMode } from 'react' import ReactDOM from 'react-dom/client' import App from './App.js' +import { initWebMCP } from './webmcp.js' import './index.css' +// Initialize WebMCP for AI agent integration +initWebMCP() + const app = document.getElementById('app') if (!app) throw new Error('missing app element') diff --git a/hyparquet/src/webmcp.ts b/hyparquet/src/webmcp.ts new file mode 100644 index 0000000..f5c5234 --- /dev/null +++ b/hyparquet/src/webmcp.ts @@ -0,0 +1,135 @@ +// WebMCP Integration for hyparquet demo +// Exposes tools for AI agents to interact with the parquet viewer + +declare global { + interface Window { + __hyparquetApp?: { + getCurrentFile: () => { name: string; url?: string } | null + loadUrl: (url: string) => void + getAvailableExamples: () => Array<{ name: string; url: string }> + } + } +} + +const exampleFiles = [ + { name: 'wiki-en-00000-of-00041.parquet', url: 'https://hyperparam-public.s3.amazonaws.com/wiki-en-00000-of-00041.parquet' }, + { name: 'starcoderdata-js-00000-of-00065.parquet', url: 'https://hyperparam.blob.core.windows.net/hyperparam/starcoderdata-js-00000-of-00065.parquet' }, + { name: 'github-code-00000-of-01126.parquet', url: 'https://huggingface.co/datasets/codeparrot/github-code/resolve/main/data/train-00000-of-01126.parquet?download=true' }, + { name: 'rowgroups.parquet', url: 'https://raw.githubusercontent.com/hyparam/hyparquet/master/test/files/rowgroups.parquet' }, +] + +// Store for current app state +let appState: { + currentFile: { name: string; url?: string } | null + loadUrlFn: ((url: string) => void) | null +} = { + currentFile: null, + loadUrlFn: null, +} + +export function setCurrentFile(name: string, url?: string) { + appState.currentFile = { name, url } +} + +export function setLoadUrlFn(fn: (url: string) => void) { + appState.loadUrlFn = fn +} + +export function initWebMCP() { + // Expose app API for tools + window.__hyparquetApp = { + getCurrentFile: () => appState.currentFile, + loadUrl: (url: string) => { + if (appState.loadUrlFn) { + appState.loadUrlFn(url) + } else { + console.warn('[WebMCP] loadUrl function not available') + } + }, + getAvailableExamples: () => exampleFiles, + } + + // Wait for WebMCP to be available (from CDN script) + const registerTools = () => { + if (!('modelContext' in navigator)) { + console.log('[WebMCP] navigator.modelContext not available, retrying...') + setTimeout(registerTools, 100) + return + } + + const mc = navigator.modelContext + + // Tool: Get current file info + mc.registerTool({ + name: 'hyparquet_get_current_file', + description: 'Get information about the currently loaded parquet file', + inputSchema: { + type: 'object', + properties: {}, + }, + async execute() { + const file = appState.currentFile + if (!file) { + return { + content: [{ type: 'text', text: 'No file is currently loaded. Use hyparquet_load_url to load a parquet file.' }], + } + } + return { + content: [{ type: 'text', text: JSON.stringify(file, null, 2) }], + } + }, + }) + + // Tool: Load a parquet URL + mc.registerTool({ + name: 'hyparquet_load_url', + description: 'Load a parquet file from a URL into the viewer', + inputSchema: { + type: 'object', + properties: { + url: { + type: 'string', + description: 'URL of the parquet file to load', + }, + }, + required: ['url'], + }, + async execute({ url }: { url: string }) { + if (!appState.loadUrlFn) { + return { + content: [{ type: 'text', text: 'Error: hyparquet viewer not ready' }], + isError: true, + } + } + appState.loadUrlFn(url) + return { + content: [{ type: 'text', text: `Loading parquet file: ${url}` }], + } + }, + }) + + // Tool: List available example files + mc.registerTool({ + name: 'hyparquet_list_examples', + description: 'List available example parquet files that can be loaded', + inputSchema: { + type: 'object', + properties: {}, + }, + async execute() { + return { + content: [{ type: 'text', text: JSON.stringify(exampleFiles, null, 2) }], + } + }, + }) + + console.log('[WebMCP] Tools registered successfully') + } + + // Start registration + registerTools() +} + +export function getAppState() { + return appState +}