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
17 changes: 17 additions & 0 deletions app/api/capabilities/standard/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* GET /api/capabilities/standard — the machine-readable Capability Standard
* (zero-code onboarding PR-8). Public + cacheable: a capability provider (or
* their coding agent) fetches it to author a compliant descriptor — the same
* standard the generator + validator + ingest use. Derived from the committed
* registry snapshot, so it's safe to cache at the edge.
*/
import { NextResponse } from "next/server";
import { getCapabilityStandard } from "@/lib/capabilities/standard";

export const dynamic = "force-static";

export function GET() {
return NextResponse.json(getCapabilityStandard(), {
headers: { "cache-control": "public, max-age=300, stale-while-revalidate=3600" },
});
}
105 changes: 105 additions & 0 deletions lib/capabilities/standard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/**
* The Capability Standard — zero-code onboarding PR-8 (plan §3.5).
*
* The single machine-readable statement of storyboard's capability conventions:
* the valid kinds / output-kinds, the modality vocabulary (+ the modalities
* actually in the live registry), the agent-param names an io.input may map
* `from`, the naming grammar, and the supported input transforms. The generator
* (draft_capability), the validator (cap-check), and the discovery→registry sync
* all read the SAME standard, so "authored against the standard" ≡ "accepted on
* ingest". Browser-safe (no server-only imports); served at
* /api/capabilities/standard for providers + their agents.
*/
import {
DESCRIPTOR_KINDS,
DESCRIPTOR_OUTPUT_KINDS,
IO_OUTPUT_KINDS,
} from "./descriptor";
import { getRegistrySnapshot } from "./registry-store";

/**
* The modality vocabulary. Media suffixes (the naming convention) + the
* analysis/understanding modalities that produce text. A provider's modality
* SHOULD be one of these; a new one is allowed but flagged for deliberate review.
*/
export const MODALITY_VOCABULARY = [
// image
"t2i", "i2i", "edit", "fill", "erase", "bg-remove", "upscale", "vto",
// video
"t2v", "i2v", "ref2v", "a2v", "transition", "hdr", "ingredient", "lipsync",
// audio
"tts", "t2m", "v2m", "inpaint", "extend", "asr",
// 3d
"i3d", "t3d",
// adapters
"lora",
// analysis / understanding (text output)
"video-understanding", "image-understanding", "text",
] as const;

/** The agent-facing param names an io input may map `from`. */
export const AGENT_PARAM_NAMES = [
"prompt",
"source_url",
"audio_url",
"image_url",
"video_url",
"text",
] as const;

/** Supported declared input transforms (applied by the SDK dispatch). */
export const INPUT_TRANSFORMS = ["url_to_base64"] as const;

export const NAMING_GRAMMAR = {
media: "<family>[-<variant>]-<modality> (e.g. flux-dev-t2i; existing caps keep grandfathered names like 'flux-dev')",
tool_mcp: "<family>-<verb> (e.g. ffmpeg-concat)",
semantic_key: "<kind>:<modality>:<family>:<variant|-> (lowercased) — the dedup key; NEVER hand-typed",
} as const;

export interface CapabilityStandard {
kinds: readonly string[];
output_kinds: readonly string[];
io_output_kinds: readonly string[];
modality_vocabulary: readonly string[];
/** Modalities present in the committed registry — the empirically-in-use set. */
live_registry_modalities: string[];
agent_param_names: readonly string[];
input_transforms: readonly string[];
naming_grammar: typeof NAMING_GRAMMAR;
}

/** The full standard, folding in the modalities the live registry actually uses. */
export function getCapabilityStandard(): CapabilityStandard {
const live = Array.from(
new Set(getRegistrySnapshot().map((e) => e.modality).filter(Boolean)),
).sort();
return {
kinds: DESCRIPTOR_KINDS,
output_kinds: DESCRIPTOR_OUTPUT_KINDS,
io_output_kinds: IO_OUTPUT_KINDS,
modality_vocabulary: MODALITY_VOCABULARY,
live_registry_modalities: live,
agent_param_names: AGENT_PARAM_NAMES,
input_transforms: INPUT_TRANSFORMS,
naming_grammar: NAMING_GRAMMAR,
};
}

/** Is a modality known (in the vocabulary or already in the live registry)? */
export function isKnownModality(modality: string): boolean {
const m = modality.toLowerCase();
if ((MODALITY_VOCABULARY as readonly string[]).includes(m)) return true;
return getRegistrySnapshot().some((e) => (e.modality ?? "").toLowerCase() === m);
}

/** Nearest known modalities to a miss (cheap prefix/substring match) — for suggestions. */
export function suggestModalities(modality: string, limit = 3): string[] {
const m = modality.toLowerCase();
const pool = new Set<string>([
...MODALITY_VOCABULARY,
...getRegistrySnapshot().map((e) => (e.modality ?? "").toLowerCase()).filter(Boolean),
]);
return Array.from(pool)
.filter((k) => k.includes(m) || m.includes(k) || k[0] === m[0])
.slice(0, limit);
}
13 changes: 13 additions & 0 deletions lib/mcp-server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { listCapabilities, ListCapabilitiesInput } from "./tools/list-capabiliti
import { getPricing, GetPricingInput } from "./tools/get-pricing";
import { describeCapability, DescribeCapabilityInput } from "./tools/describe-capability";
import { runCapability, RunCapabilityInput } from "./tools/run-capability";
import { draftCapability, DraftCapabilityInput } from "./tools/draft-capability";
import {
capDeprecate,
CapDeprecateInput,
Expand Down Expand Up @@ -453,6 +454,18 @@ export function buildStoryboardMcpServer(
async (args: unknown) => runCapability(args as RunCapabilityInput, auth),
);

registerTool(
server,
"draft_capability",
{
title: "Draft a compliant Capability Descriptor (author-by-derivation)",
description:
"Given the irreducible facts (family, modality, one-line 'does', your runner's io contract + offering), GENERATE a complete, compliant Capability Descriptor: it derives the semantic_key + canonical name + output_kind (never hand-typed), validates against the schema, checks the modality vocabulary, and returns the dedup verdict (NEW / ADD-CAPACITY / SYNONYM). Publish the returned descriptor on your orchestrator's /discovery. See GET /api/capabilities/standard for the vocabulary.",
inputSchema: DraftCapabilityInput,
},
async (args: unknown) => draftCapability(args as DraftCapabilityInput, auth),
);

registerTool(
server,
"cap_deprecate",
Expand Down
2 changes: 1 addition & 1 deletion lib/mcp-server/tools/describe-capability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const OUTPUT_TO_ACTIONS: Record<string, string[]> = {
// json analysis caps) via the generic run_capability verb. Used only when the
// cap has no authored usage.invoke_via.
const MEDIA_OUTPUT_KINDS = new Set(["image", "video", "audio", "3d"]);
function defaultInvokeVia(outputKind: string | undefined): string {
function defaultInvokeVia(outputKind: string | null | undefined): string {
return MEDIA_OUTPUT_KINDS.has(outputKind ?? "image") ? "create_media" : "run_capability";
}

Expand Down
153 changes: 153 additions & 0 deletions lib/mcp-server/tools/draft-capability.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/**
* draft_capability — zero-code onboarding PR-8 (plan §3.5 "authoring by derivation").
*
* A provider (or their agent) supplies only the IRREDUCIBLE facts — family,
* modality, a one-line "what it does", their runner's io contract + offering —
* and this GENERATES a complete, compliant Capability Descriptor: it derives the
* semantic_key + canonical name + output_kind (never hand-typed — that's where
* descriptors go wrong), assembles + VALIDATES against the PR-1 schema, checks
* the modality against the standard, and returns the dedup verdict against the
* live registry (NEW / ADD-CAPACITY / SYNONYM). The output is a ready-to-publish
* descriptor the provider drops into their orchestrator's /discovery.
*/
import { z } from "zod";
import type { AuthContext } from "../auth";
import {
validateDescriptor,
computeSemanticKey,
resolveOutputKind,
IoSchema,
OfferingSchema,
DESCRIPTOR_KINDS,
DESCRIPTOR_OUTPUT_KINDS,
type CapabilityDescriptor,
} from "@/lib/capabilities/descriptor";
import { canonicalName } from "@/lib/capabilities/discovery-sync";
import { isKnownModality, suggestModalities } from "@/lib/capabilities/standard";
import { readRegistry } from "@/lib/capabilities/registry-overlay";
import type { RegistryEntry } from "@/lib/capabilities/generate-registry";

export const DraftCapabilityInput = z.object({
family: z.string().min(1).describe("Model/tool family, e.g. 'screen-agent', 'flux'."),
modality: z.string().min(1).describe("What it does — a modality (t2i/i2v/video-understanding/…)."),
does: z.string().min(1).describe("One line: what it does (becomes good_for)."),
kind: z.enum(DESCRIPTOR_KINDS).default("live-runner"),
variant: z.string().min(1).optional().describe("Tier/variant, e.g. 'quality'."),
output_kind: z.enum(DESCRIPTOR_OUTPUT_KINDS).optional().describe("Omit to infer from modality."),
avoid_for: z.string().optional(),
aliases: z.array(z.string()).optional(),
io: IoSchema,
offering: OfferingSchema,
});
export type DraftCapabilityInput = z.infer<typeof DraftCapabilityInput>;

export type DedupVerdict =
| { verdict: "new" }
| { verdict: "add_capacity"; owner: string }
| { verdict: "synonym"; owner: string };

/** PURE — the dedup verdict for a drafted identity against the registry view. */
export function dedupVerdict(
name: string,
key: string,
existing: RegistryEntry[],
): DedupVerdict {
const owner = existing.find(
(e) =>
(e.semantic_key ??
computeSemanticKey({
kind: e.kind,
modality: e.modality ?? "",
family: e.family ?? "",
variant: e.variant ?? null,
})) === key,
)?.name;
if (!owner) return { verdict: "new" };
return owner === name ? { verdict: "add_capacity", owner } : { verdict: "synonym", owner };
}

export interface DraftResult {
ok: boolean;
descriptor?: CapabilityDescriptor;
name: string;
semantic_key: string;
modality_known: boolean;
modality_suggestions: string[];
errors?: string[];
}

/** PURE — assemble + derive + validate a descriptor from the irreducible facts. */
export function buildDraft(input: DraftCapabilityInput): DraftResult {
const variant = input.variant ?? null;
const capability = {
kind: input.kind,
family: input.family,
modality: input.modality,
variant,
output_kind: resolveOutputKind({ output_kind: input.output_kind, modality: input.modality }),
aliases: input.aliases ?? [],
good_for: input.does,
...(input.avoid_for ? { avoid_for: input.avoid_for } : {}),
io: input.io,
offering: input.offering,
};
const name = canonicalName(capability as never);
const key = computeSemanticKey({ kind: input.kind, modality: input.modality, family: input.family, variant });
const validated = validateDescriptor({ capability });

return {
ok: validated.ok,
descriptor: validated.ok ? validated.value : undefined,
name,
semantic_key: key,
modality_known: isKnownModality(input.modality),
modality_suggestions: isKnownModality(input.modality) ? [] : suggestModalities(input.modality),
errors: validated.ok ? undefined : validated.errors,
};
}

export async function draftCapability(input: DraftCapabilityInput, auth: AuthContext) {
const draft = buildDraft(input);
let dedup: DedupVerdict = { verdict: "new" };
try {
dedup = dedupVerdict(draft.name, draft.semantic_key, await readRegistry());
} catch {
// fail-open: no registry read → treat as new (the sync re-checks on ingest)
}

const lines: string[] = [];
if (!draft.ok) {
lines.push(`✗ descriptor invalid — fix and re-draft:`, ...(draft.errors ?? []).map((e) => ` · ${e}`));
} else {
lines.push(`✓ drafted "${draft.name}" (${draft.semantic_key})`);
if (!draft.modality_known) {
lines.push(
`⚠︎ modality "${input.modality}" isn't in the standard vocabulary${draft.modality_suggestions.length ? ` — did you mean: ${draft.modality_suggestions.join(", ")}?` : ""} (a new modality is allowed, but confirm it's intentional).`,
);
}
if (dedup.verdict === "synonym") {
lines.push(
`✗ SYNONYM: that identity already belongs to "${dedup.owner}". Reuse "${dedup.owner}" and add your offering under it — don't mint a new name.`,
);
} else if (dedup.verdict === "add_capacity") {
lines.push(`↑ ADD-CAPACITY: "${dedup.owner}" already exists — you're a new provider of it (offering only, no new identity).`);
} else {
lines.push(`+ NEW identity — publish this descriptor on your /discovery.`);
}
}

return {
isError: !draft.ok || dedup.verdict === "synonym",
content: [{ type: "text" as const, text: lines.join("\n") }],
structuredContent: {
ok: draft.ok && dedup.verdict !== "synonym",
name: draft.name,
semantic_key: draft.semantic_key,
dedup,
modality_known: draft.modality_known,
modality_suggestions: draft.modality_suggestions,
descriptor: draft.descriptor,
errors: draft.errors,
},
};
}
Loading
Loading