Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions apps/web/src/app/api/plugins/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* JSON plugin listing at https://c0mpute.com/api/plugins.
*
* The marketplace page (`/plugins`) renders from `loadAllPlugins()` at build
* time and has no runtime data source. WebMCP tools run client-side, though, so
* they need a fetchable endpoint to enumerate the plugin catalogue for an agent.
* This route exposes the same manifest data as compact JSON.
*/

import { NextResponse } from "next/server";
import { loadAllPlugins, tagline, installCommand } from "@/lib/plugins";

export function GET() {
const plugins = loadAllPlugins().map((p) => ({
id: p.id,
name: p.name,
version: p.version,
kind: p.kind,
tagline: tagline(p),
description: p.description,
keywords: p.keywords ?? [],
homepage: p.homepage,
source: p.source,
install_command: installCommand(p),
install_url: `https://c0mpute.com/plugins/${p.id}/install.sh`,
web: `https://c0mpute.com/plugins/${p.id}`,
}));

return NextResponse.json(
{ count: plugins.length, plugins },
{ headers: { "cache-control": "public, max-age=300, s-maxage=300" } },
);
}
2 changes: 2 additions & 0 deletions apps/web/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Metadata, Viewport } from "next";
import Link from "next/link";
import "./globals.css";
import Script from "next/script";
import { WebMcpProvider } from "@/components/webmcp-provider";

const SITE = "https://c0mpute.com";
const DESCRIPTION =
Expand Down Expand Up @@ -43,6 +44,7 @@ export default function RootLayout({
return (
<html lang="en">
<body className="min-h-screen flex flex-col">
<WebMcpProvider />
<header className="border-b border-[var(--color-rule)]">
<nav className="max-w-3xl mx-auto px-6 py-4 flex items-center justify-between text-sm">
<Link
Expand Down
166 changes: 166 additions & 0 deletions apps/web/src/components/webmcp-provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
"use client";

import { useEffect } from "react";

/**
* WebMCP integration for c0mpute.com.
*
* Registers a handful of JavaScript tools on `document.modelContext` so an AI
* agent visiting the site can query the network and the plugin catalogue
* directly, rather than scraping the rendered page. See
* https://webmachinelearning.github.io/webmcp/.
*
* The API is an unshipped draft, so everything here is behind a feature-detect
* and degrades to a no-op in browsers/agents that don't implement it. We support
* both the current `document.modelContext.registerTool` shape and the older
* `provideContext({ tools })` shape.
*/

const ORIGIN =
typeof window !== "undefined" ? window.location.origin : "https://c0mpute.com";

/** Wrap any JSON-serialisable value as an MCP text-content result. */
function json(value: unknown): WebMcpToolResult {
return { content: [{ type: "text", text: JSON.stringify(value, null, 2) }] };
}

function errorResult(message: string): WebMcpToolResult {
return { content: [{ type: "text", text: message }], isError: true };
}

async function fetchJson(path: string): Promise<unknown> {
const res = await fetch(`${ORIGIN}${path}`, {
headers: { accept: "application/json" },
});
if (!res.ok) throw new Error(`${path} → HTTP ${res.status}`);
return res.json();
}

const TOOLS: WebMcpToolDescriptor[] = [
{
name: "c0mpute_list_plugins",
title: "List c0mpute plugins",
description:
"List the plugins/modules available on the c0mpute decentralized compute network (e.g. transcode, coinpay, infernet), including their install commands.",
inputSchema: { type: "object", properties: {}, additionalProperties: false },
execute: async () => {
try {
return json(await fetchJson("/api/plugins"));
} catch (e) {
return errorResult(`Failed to list plugins: ${(e as Error).message}`);
}
},
},
{
name: "c0mpute_network_status",
title: "Get c0mpute network status",
description:
"Get live status of the c0mpute network: workers online, jobs in flight, jobs completed in the last 24h, and average job latency per workload type.",
inputSchema: { type: "object", properties: {}, additionalProperties: false },
execute: async () => {
try {
return json(await fetchJson("/api/status"));
} catch (e) {
return errorResult(`Failed to get network status: ${(e as Error).message}`);
}
},
},
{
name: "c0mpute_latest_release",
title: "Get latest c0mpute release",
description:
"Get the latest published c0mpute CLI release manifest (version, channel, and per-platform download artifacts).",
inputSchema: { type: "object", properties: {}, additionalProperties: false },
execute: async () => {
try {
return json(await fetchJson("/releases/latest.json"));
} catch (e) {
return errorResult(`Failed to get latest release: ${(e as Error).message}`);
}
},
},
{
name: "c0mpute_install_command",
title: "Get c0mpute install command",
description:
"Get the shell command to install the c0mpute CLI, or to install a specific plugin by id. Pass no arguments for the base CLI installer.",
inputSchema: {
type: "object",
properties: {
plugin: {
type: "string",
description:
"Optional plugin id (e.g. 'transcode', 'coinpay', 'infernet'). Omit to get the base CLI installer.",
},
},
additionalProperties: false,
},
execute: (args) => {
const plugin =
typeof args.plugin === "string" ? args.plugin.trim() : "";
if (plugin) {
return json({
plugin,
command: `c0mpute plugin install ${plugin}`,
install_url: `https://c0mpute.com/plugins/${plugin}/install.sh`,
});
}
return json({
command: "curl -fsSL https://c0mpute.com/install.sh | sh",
installs: ["c0mpute", "coinpay", "infernet"],
note: "Installs three CLIs into ~/.c0mpute/bin.",
});
},
},
];

/** Register all tools; returns a cleanup that unregisters them. */
function register(ctx: ModelContext): () => void {
// Preferred: per-tool registration returning an unregister handle.
if (typeof ctx.registerTool === "function") {
const regs: Array<WebMcpRegistration | void> = [];
for (const tool of TOOLS) {
try {
const r = ctx.registerTool(tool);
// registerTool may return a promise; we don't await for cleanup, the
// handle is only needed on unmount which is far later.
if (r && typeof (r as PromiseLike<unknown>).then === "function") {
(r as Promise<WebMcpRegistration | void>).then((h) => regs.push(h));
} else {
regs.push(r as WebMcpRegistration | void);
}
} catch {
// A single bad registration must not break the others.
}
}
return () => {
for (const r of regs) {
try {
r?.unregister?.();
} catch {
/* no-op */
}
}
};
}

// Fallback: older bulk API. No documented removal, so cleanup is a no-op.
if (typeof ctx.provideContext === "function") {
try {
ctx.provideContext({ tools: TOOLS });
} catch {
/* no-op */
}
}
return () => {};
}

export function WebMcpProvider() {
useEffect(() => {
const ctx = document.modelContext ?? navigator.modelContext;
if (!ctx) return; // Agent/browser doesn't implement WebMCP — no-op.
return register(ctx);
}, []);

return null;
}
54 changes: 54 additions & 0 deletions apps/web/src/types/webmcp.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**
* Ambient types for the emerging WebMCP API.
*
* Spec: https://webmachinelearning.github.io/webmcp/
*
* WebMCP lets a web page expose JavaScript "tools" that an AI agent (in the
* browser or an extension) can discover and invoke, instead of driving the page
* only through simulated UI. The surface is a draft and not yet in `lib.dom`, so
* we declare the minimum we depend on here. Everything is optional at runtime —
* the provider feature-detects before touching it.
*/

/** MCP-style content block returned from a tool's `execute`. */
interface WebMcpTextContent {
type: "text";
text: string;
}

interface WebMcpToolResult {
content: WebMcpTextContent[];
isError?: boolean;
}

interface WebMcpToolDescriptor {
name: string;
/** Human-readable title (optional in the draft spec). */
title?: string;
description: string;
/** JSON Schema for the tool's arguments. */
inputSchema?: Record<string, unknown>;
execute: (args: Record<string, unknown>) => Promise<WebMcpToolResult> | WebMcpToolResult;
}

/** Handle returned by `registerTool`, used to remove the tool again. */
interface WebMcpRegistration {
unregister?: () => void;
}

interface ModelContext {
registerTool?: (
tool: WebMcpToolDescriptor,
options?: Record<string, unknown>,
) => WebMcpRegistration | void | Promise<WebMcpRegistration | void>;
/** Older drafts expose a bulk `provideContext({ tools })` instead. */
provideContext?: (context: { tools: WebMcpToolDescriptor[] }) => void;
}

interface Document {
modelContext?: ModelContext;
}

interface Navigator {
modelContext?: ModelContext;
}
Loading