Skip to content
Open
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
176 changes: 172 additions & 4 deletions .wave/repo.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@wave-av/mcp-server",
"kind": "mcp-server",
"domain": "agent-ops",
"purpose": "MCP (Model Context Protocol) server that exposes WAVE streaming APIs as tools for AI coding assistants (Claude Code, Cursor, Windsurf).",
"purpose": "MCP (Model Context Protocol) server that exposes WAVE as tools for AI coding assistants (Claude Code, Cursor, Windsurf): 25 tools spanning streams, studio productions, analytics and billing, the voice/transcribe/captions product spokes, WAVE Dispatch model routing, and the public x402/MPP agent-payment rails.",
"visibility": "public",
"primaryLanguage": "TypeScript",
"topics": [
Expand Down Expand Up @@ -112,16 +112,51 @@
"id": "wave_mark_highlight",
"does": "Mark a moment in a stream as a highlight for later clipping.",
"status": "preview"
},
{
"id": "wave_speak",
"does": "Synthesize speech from text via POST /v1/voice. Returns a receipt — content type, size, and billed usage — rather than the audio itself, since an MCP tool result is a text block and raw audio cannot travel through one.",
"status": "ga"
},
{
"id": "wave_transcribe",
"does": "Transcribe audio from a publicly reachable URL via POST /v1/transcribe, returning the JSON transcript. Optional engine (auto/whisper/deepgram/elevenlabs), language hint, and speaker diarization.",
"status": "ga"
},
{
"id": "wave_caption",
"does": "Generate a caption file from audio at a URL via POST /v1/captions, returning WebVTT, SubRip, or JSON cues as text.",
"status": "ga"
},
{
"id": "wave_route",
"does": "Classify a prompt with WAVE Dispatch and return the model route it selected plus the reasoning, so an agent can pick the cheapest capable model instead of hardcoding one. Calls Dispatch on its own host, not the product gateway.",
"status": "ga"
},
{
"id": "wave_list_routing_profiles",
"does": "List WAVE Dispatch's named routing profiles (Fast / Expert / Heavy / Code). Feature-flagged server-side, so it can legitimately report as unavailable in a given environment.",
"status": "preview"
},
{
"id": "wave_find_paid_services",
"does": "Semantic search over WAVE's MPP directory for machine-payable services an agent can buy from. Public: no API key is sent, because the route takes none.",
"status": "preview"
},
{
"id": "wave_payment_schemes",
"does": "List the payment schemes and networks WAVE's x402 or MPP facilitator supports, so an agent settles on a scheme WAVE accepts. Public: no API key is sent. Only the read half of the rails is exposed — verify and settle are deliberately not wrapped as tools.",
"status": "ga"
}
],
"claims": [
{
"id": "claim-tool-count",
"text": "Exposes 18 MCP tools",
"text": "Exposes 25 MCP tools",
"resolver": {
"type": "grep",
"target": "capabilities.json",
"expect": "18-tools"
"expect": "25-tools"
}
},
{
Expand Down Expand Up @@ -151,6 +186,69 @@
"expect": "docs.wave.online/mcp"
}
},
{
"id": "claim-tool-speak",
"text": "wave_speak tool defined in src/tools/fleet.ts",
"resolver": {
"type": "grep",
"target": "src/tools/fleet.ts",
"expect": "wave_speak"
}
},
{
"id": "claim-tool-transcribe",
"text": "wave_transcribe tool defined in src/tools/fleet.ts",
"resolver": {
"type": "grep",
"target": "src/tools/fleet.ts",
"expect": "wave_transcribe"
}
},
{
"id": "claim-tool-caption",
"text": "wave_caption tool defined in src/tools/fleet.ts",
"resolver": {
"type": "grep",
"target": "src/tools/fleet.ts",
"expect": "wave_caption"
}
},
{
"id": "claim-tool-route",
"text": "wave_route tool defined in src/tools/dispatch.ts",
"resolver": {
"type": "grep",
"target": "src/tools/dispatch.ts",
"expect": "wave_route"
}
},
{
"id": "claim-tool-find-paid-services",
"text": "wave_find_paid_services tool defined in src/tools/payments.ts",
"resolver": {
"type": "grep",
"target": "src/tools/payments.ts",
"expect": "wave_find_paid_services"
}
},
{
"id": "claim-payments-sends-no-key",
"text": "The payment-rail tools send no Authorization header, because those gateway routes are public and a key sent to them would leak for no benefit",
"resolver": {
"type": "grep",
"target": "src/tools/payments.ts",
"expect": "No Authorization header on purpose"
}
},
{
"id": "claim-fleet-paths-grounded-in-spokes",
"text": "The voice/transcribe/captions paths come from each spoke's own router, not from api-spec/openapi.yaml, which over-declares endpoints that 404 in production",
"resolver": {
"type": "grep",
"target": "src/tools/fleet.ts",
"expect": "api-spec#33"
}
},
{
"id": "claim-tool-list-streams",
"text": "wave_list_streams tool defined in src/tools/streams.ts",
Expand Down Expand Up @@ -419,6 +517,64 @@
]
]
},
{
"kind": "table",
"heading": "Available tools — Fleet products",
"columns": [
"Tool",
"Description"
],
"rows": [
[
"`wave_speak`",
"Synthesize speech from text (POST /v1/voice). Returns a receipt — content type, size, billed usage — not the audio, since an MCP tool result is a text block"
],
[
"`wave_transcribe`",
"Transcribe audio from a public URL (POST /v1/transcribe), returning the JSON transcript"
],
[
"`wave_caption`",
"Generate a caption file from audio at a URL (POST /v1/captions) — WebVTT, SubRip, or JSON cues"
]
]
},
{
"kind": "table",
"heading": "Available tools — Model routing",
"columns": [
"Tool",
"Description"
],
"rows": [
[
"`wave_route`",
"Classify a prompt with WAVE Dispatch and get the selected model route plus its reasoning"
],
[
"`wave_list_routing_profiles`",
"List the named routing profiles (Fast / Expert / Heavy / Code). Feature-flagged server-side"
]
]
},
{
"kind": "table",
"heading": "Available tools — Agent payments",
"columns": [
"Tool",
"Description"
],
"rows": [
[
"`wave_find_paid_services`",
"Semantic search over the MPP directory for machine-payable services. Public — no API key required"
],
[
"`wave_payment_schemes`",
"Payment schemes and networks the x402 or MPP facilitator supports. Public — no API key required"
]
]
},
{
"kind": "prose",
"heading": "Resources",
Expand All @@ -445,6 +601,18 @@
"No",
"`https://wave.online`",
"API base URL"
],
[
"`WAVE_API_BASE_URL`",
"No",
"`https://api.wave.online`",
"Product gateway front door — fronts the voice/transcribe/captions spokes and the payment rails. Distinct from `WAVE_BASE_URL`, which is the app surface"
],
[
"`WAVE_DISPATCH_URL`",
"No",
"`https://dispatch.wave.online`",
"WAVE Dispatch host. Dispatch is not behind the gateway"
]
]
},
Expand Down Expand Up @@ -480,4 +648,4 @@
"body": "MIT"
}
]
}
}
2 changes: 1 addition & 1 deletion capabilities.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
"tags": [
"mcp-server",
"public",
"18-tools",
"25-tools",
"agent-ops"
]
}
22 changes: 22 additions & 0 deletions src/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,21 @@
* Reads credentials from environment variables:
* - WAVE_API_KEY: Required. Bearer token for WAVE API authentication.
* - WAVE_BASE_URL: Optional. Defaults to https://wave.online.
* - WAVE_API_BASE_URL: Optional. Defaults to https://api.wave.online — the GATEWAY front door.
* - WAVE_DISPATCH_URL: Optional. Defaults to https://dispatch.wave.online.
*/

const DEFAULT_BASE_URL = "https://wave.online";

/** The gateway front door. DISTINCT from `WAVE_BASE_URL`: the app surface (`/api/v1/billing/...`)
* lives on wave.online, while the metered product spokes (voice / transcribe / captions, and the
* x402+MPP payment rails) are served through the gateway at api.wave.online under `/v1/...`.
* Pointing product calls at the app host silently 404s, so the two are kept separate on purpose. */
const DEFAULT_API_BASE_URL = "https://api.wave.online";

/** wave-dispatch runs on its OWN host, not behind the product gateway, and takes a plain bearer. */
const DEFAULT_DISPATCH_URL = "https://dispatch.wave.online";

export function getApiKey(): string {
const key = process.env["WAVE_API_KEY"];
if (!key) {
Expand All @@ -24,6 +35,17 @@ export function getBaseUrl(): string {
return process.env["WAVE_BASE_URL"] ?? DEFAULT_BASE_URL;
}

/** Base URL for gateway-fronted product calls (`/v1/voice`, `/v1/transcribe`, `/v1/captions`,
* `/v1/mpp/*`). See {@link DEFAULT_API_BASE_URL} for why this is not `getBaseUrl()`. */
export function getApiBaseUrl(): string {
return process.env["WAVE_API_BASE_URL"] ?? DEFAULT_API_BASE_URL;
}

/** Base URL for wave-dispatch, which is NOT behind the product gateway. */
export function getDispatchUrl(): string {
return process.env["WAVE_DISPATCH_URL"] ?? DEFAULT_DISPATCH_URL;
}

export function getAuthHeaders(): Record<string, string> {
return {
Authorization: `Bearer ${getApiKey()}`,
Expand Down
74 changes: 74 additions & 0 deletions src/tools/dispatch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// wave-dispatch routing tools.
//
// Dispatch is NOT behind the product gateway: it runs on its own host and takes a plain bearer, so
// these tools use `getDispatchUrl()` rather than the gateway helpers in ./shared.ts. Paths come from
// wave-dispatch's own committed facts (`.wave/repo.json` endpoints, resolver-verified):
//
// POST / classify a prompt → { route, decision }
// GET /profiles named routing profiles (Fast/Expert/Heavy/Code) — flag-gated
// GET /sovereign sovereign-tier routing profile — flag-gated
//
// Both GETs are behind a server-side feature flag (WAVE_PROFILES), so they can legitimately return a
// not-enabled response in a given environment. That is surfaced to the caller rather than hidden.
import { z } from "zod";
import { defineTool, errorContent, textContent, type WaveToolDef } from "./shared.js";
import { getApiKey, getDispatchUrl } from "../auth.js";

async function dispatchFetch(
path: string,
init?: RequestInit,
): Promise<{ ok: boolean; status: number; body: string }> {
const res = await fetch(`${getDispatchUrl()}${path}`, {
...init,
headers: {
Authorization: `Bearer ${getApiKey()}`,
"User-Agent": "wave-mcp-server/0.1.0",
...init?.headers,
},
});
return { ok: res.ok, status: res.status, body: await res.text() };
}

export const dispatchTools: WaveToolDef[] = [
defineTool({
name: "wave_route",
description:
"Classify a prompt with WAVE Dispatch and get back the model route it selected plus the " +
"reasoning behind that decision. Use this to pick the cheapest capable model for a task " +
"instead of hardcoding one.",
inputSchema: {
prompt: z.string().min(1).describe("The prompt to classify and route"),
profile: z
.string()
.optional()
.describe("Named routing profile to route under (see wave_list_routing_profiles)"),
},
handler: async ({ prompt, profile }) => {
const body: Record<string, string> = { prompt };
if (profile) body["profile"] = profile;

const res = await dispatchFetch("/", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (!res.ok) return errorContent(res.status, res.body);

return textContent(res.body);
},
}),

defineTool({
name: "wave_list_routing_profiles",
description:
"List WAVE Dispatch's named routing profiles (Fast / Expert / Heavy / Code chains). This is " +
"feature-flagged server-side and may report as unavailable in some environments.",
inputSchema: {},
handler: async () => {
const res = await dispatchFetch("/profiles");
if (!res.ok) return errorContent(res.status, res.body);

return textContent(res.body);
},
}),
];
Loading
Loading