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
73 changes: 73 additions & 0 deletions cloudflare/gallery-tools.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { expect, test } from "bun:test";
import Ajv from "ajv";
import { CLOUD_MCP_TOOLS } from "./tool-contract";
import { galleryAction, gallerySource } from "./gallery-tools";
import { toolErrorResult } from "../src/mcp/tool-result";
import { settingsTool } from "../src/mcp/settings-contract";

test("gallery source returns complete projects and selects the requested kind and workspace", async () => {
const source = Array.from({ length: 250 }, (_, index) => `// line ${index}\n`).join("");
const project = { files: { "helper.ts": "export const value = 1;" }, dependencies: { example: "1.0.0" }, lock: { "node_modules/example/index.js": "export const dependency = 2;" } };
const calls: unknown[] = [];
const service = {
async snapshot(selection: unknown) { calls.push(["artifact", selection]); return { name: "example", source, server_source: "server", project, revision_token: "artifact-revision" }; },
async scriptReadSource(selection: unknown) { calls.push(["script", selection]); return { name: "example", source, project, revision_token: "script-revision" }; },
};
for (const kind of ["artifact", "script"]) {
const result = await gallerySource(service, { workspace: "research", kind, version_id: "historical" });
expect(result.structuredContent).toEqual({ name: "example", kind, workspace: "research", source, server_source: kind === "artifact" ? "server" : null, project, revision_token: `${kind}-revision` });
}
expect(calls).toEqual([["artifact", { workspace: "research", version_id: "historical" }], ["script", { workspace: "research", version_id: "historical" }]]);
await expect(gallerySource(service, { workspace: "research", kind: "artifact", name: "example", version_id: "historical" })).rejects.toThrow("not both");
});

test("gallery action preserves CAS failures and committed validation diagnostics", async () => {
const calls: unknown[] = [];
const service = { async callTool(name: string, args: Record<string, unknown>) {
calls.push({ name, args });
if (args.expected_revision === "stale") throw new Error("Project changed since it was loaded. Reload the saved project or compare it with your edits before saving.");
const payload = { applied: true, ok: false, revision_token: "new-revision", diagnostics: [{ message: "Invalid source", severity: "error" }] };
return { content: [{ type: "text" as const, text: JSON.stringify(payload) }], structuredContent: payload, isError: true };
} };
const failed = await galleryAction(service, { workspace: "research", tool: "artifact_write", arguments: { name: "example", contents: "invalid", expected_revision: "current" } });
expect(failed).toMatchObject({ isError: true, structuredContent: { applied: true, revision_token: "new-revision", diagnostics: [{ message: "Invalid source" }] } });
const conflict = await galleryAction(service, { workspace: "research", tool: "script_write", arguments: { name: "example", contents: "new", expected_revision: "stale" } });
expect(conflict).toMatchObject({ isError: true, structuredContent: { status: 409, conflict: true, applied: false } });
expect(calls).toEqual([
{ name: "artifact_write", args: { workspace: "research", name: "example", contents: "invalid", expected_revision: "current" } },
{ name: "script_write", args: { workspace: "research", name: "example", contents: "new", expected_revision: "stale" } },
]);
await expect(galleryAction(service, { workspace: "research", tool: "app_write", arguments: {} })).rejects.toThrow("Unsupported");
await expect(galleryAction(service, { workspace: "research", tool: "artifact_write", arguments: { workspace: "elsewhere" } })).rejects.toThrow("envelope");
expect(toolErrorResult(Object.assign(new Error("storage failed after save"), { status: 503, applied: true, revision_token: "saved" }))).toMatchObject({ structuredContent: { status: 503, applied: true, revision_token: "saved" } });
});

test("published gallery proxy validates underlying tool schemas without granting blanket app visibility", () => {
const tool = CLOUD_MCP_TOOLS.find(tool => tool.name === "artifacts_tool")!;
const validate = new Ajv({ strict: false }).compile(tool.inputSchema);
expect(validate({ workspace: "research", tool: "artifact_write", arguments: { name: "example", contents: "source", expected_revision: null, project: { files: {}, dependencies: {} } } })).toBe(true);
for (const argumentsValue of [{ name: "example" }, { name: "example", contents: "source", workspace: "wrong" }, { name: "example", contents: "source", expected_revision: 3 }, { name: "example", contents: "source", project: { unexpected: "value" } }]) {
expect(validate({ workspace: "research", tool: "artifact_write", arguments: argumentsValue })).toBe(false);
}
expect(validate({ workspace: "research", tool: "app_write", arguments: {} })).toBe(false);
expect(tool._meta?.ui).toEqual({ visibility: ["app"] });
expect((CLOUD_MCP_TOOLS.find(tool => tool.name === "artifact_write")!._meta?.ui as { visibility: string[] }).visibility).toEqual(["model"]);
const source = CLOUD_MCP_TOOLS.find(tool => tool.name === "artifacts_source")!;
const validateSource = new Ajv({ strict: false }).compile(source.inputSchema);
expect(validateSource({ workspace: "research", kind: "script", version_id: "historical" })).toBe(true);
expect(validateSource({ workspace: "research", kind: "script", version_id: "historical", name: "example" })).toBe(false);
});

test("native connection settings describe the real connection and check authorized access", async () => {
let reads = 0;
const service = { workspace: "research", productUrl: "https://artifacts.example/?workspace=research", async gallery(all: boolean) {
expect(all).toBe(true); reads++;
return { workspace: "research", artifacts: [], capabilities: { editing: true, scripts: true } };
} };
const settings = await settingsTool(service, "artifacts_settings_read", {});
expect(settings.structuredContent).toMatchObject({ schema: { properties: {} }, values: {}, layout: [{ title: "Workspace: research", items: [{ tool: "artifacts_connection_check", description: expect.stringContaining(service.productUrl) }] }] });
expect(reads).toBe(0);
expect(await settingsTool(service, "artifacts_connection_check", {})).toMatchObject({ structuredContent: { ok: true, workspace: "research", productUrl: service.productUrl, capabilities: { editing: true, scripts: true } } });
expect(reads).toBe(1);
await expect(settingsTool(service, "artifacts_settings_update", { set: { workspace: "wrong" } })).rejects.toThrow("managed by the MCP host");
});
31 changes: 31 additions & 0 deletions cloudflare/gallery-tools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { GALLERY_TOOL_NAMES } from "../src/mcp/gallery-contract";
import { toolErrorResult } from "../src/mcp/tool-result";

type Selection = { workspace?: string; name?: string; version_id?: string };
type Source = { name: string; source: string; server_source?: string | null };
type SourceService = {
snapshot(selection: Selection): Promise<Source>;
scriptReadSource(selection: Selection): Promise<Source>;
};

export async function gallerySource(service: SourceService, args: Record<string, unknown>): Promise<CallToolResult> {
if (typeof args.workspace !== "string" || !args.workspace.trim() || (args.kind !== "artifact" && args.kind !== "script")) throw new Error("Select a workspace and artifact or script kind");
if ((typeof args.name === "string") === (typeof args.version_id === "string")) throw new Error("Select name or version_id, but not both");
const selection = { workspace: args.workspace, ...(typeof args.name === "string" ? { name: args.name } : { version_id: args.version_id as string }) };
const source = args.kind === "script" ? await service.scriptReadSource(selection) : await service.snapshot(selection);
const payload = { ...source, kind: args.kind, workspace: args.workspace, server_source: source.server_source ?? null };
return { content: [{ type: "text", text: JSON.stringify(payload) }], structuredContent: payload };
}

export async function galleryAction(service: { callTool(name: string, args: Record<string, unknown>): Promise<CallToolResult> }, args: Record<string, unknown>): Promise<CallToolResult> {
if (!(GALLERY_TOOL_NAMES as readonly unknown[]).includes(args.tool)) throw new Error("Unsupported gallery action");
if (typeof args.workspace !== "string" || !args.workspace.trim()) throw new Error("Select a workspace");
if (!args.arguments || typeof args.arguments !== "object" || Array.isArray(args.arguments)) throw new Error("Gallery arguments must be an object");
if (Object.hasOwn(args.arguments, "workspace")) throw new Error("Workspace must be selected in the gallery action envelope");
try {
return await service.callTool(args.tool as string, { ...args.arguments, workspace: args.workspace });
} catch (error) {
return toolErrorResult(error);
}
}
16 changes: 16 additions & 0 deletions cloudflare/http.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { expect, test } from "bun:test";
import { readToolBody } from "./http";

test("gallery import envelopes retain the existing archive request allowance", async () => {
const archive = { large: "x".repeat(1024 * 1024) };
for (const mcp of [false, true]) {
for (const tool of ["artifact_import", "script_import"]) {
const call = { name: "artifacts_tool", arguments: { workspace: "research", tool, arguments: { new_name: "copy", archive } } };
const body = mcp ? { jsonrpc: "2.0", method: "tools/call", params: call } : call;
expect(await readToolBody(new Request("https://artifacts.example/", { method: "POST", body: JSON.stringify(body) }), mcp)).toEqual(body);
}
const call = { name: "artifacts_tool", arguments: { workspace: "research", tool: "artifact_write", arguments: { contents: archive.large } } };
const body = mcp ? { jsonrpc: "2.0", method: "tools/call", params: call } : call;
await expect(readToolBody(new Request("https://artifacts.example/", { method: "POST", body: JSON.stringify(body) }), mcp)).rejects.toThrow("exceeds 1 MiB");
}
});
6 changes: 4 additions & 2 deletions cloudflare/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import { PROJECT_IMPORT_REQUEST_BYTES } from "../src/project-archive-contract";
export async function readToolBody(request: Request, mcp = false): Promise<unknown> {
const text = await readRequestText(request, PROJECT_IMPORT_REQUEST_BYTES);
const oversized = new TextEncoder().encode(text).byteLength > 1024 * 1024;
let value: { name?: string; method?: string; params?: { name?: string } } | null;
type ToolEnvelope = { name?: string; arguments?: { tool?: string } };
let value: (ToolEnvelope & { method?: string; params?: ToolEnvelope }) | null;
try { value = JSON.parse(text); }
catch (error) { if (oversized) throw new RangeError("Request exceeds 1 MiB"); throw error; }
const name = mcp ? value?.method === "tools/call" ? value.params?.name : undefined : value?.name;
const call = mcp ? value?.method === "tools/call" ? value.params : undefined : value;
const name = call?.name === "artifacts_tool" ? call.arguments?.tool : call?.name;
if (oversized && name !== "artifact_import" && name !== "script_import") throw new RangeError("Request exceeds 1 MiB");
return value;
}
Expand Down
15 changes: 13 additions & 2 deletions cloudflare/managed-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import type { ArtifactFileRequest } from "../src/sdk/files";
import type { GalleryArtifact, GalleryData } from "../src/gallery/types";
import { CloudArtifactService } from "./service";
import { workspaceTool } from "./workspace-tools";
import { galleryAction, gallerySource } from "./gallery-tools";
import { ARTIFACTS_SETTINGS_TOOLS, settingsTool } from "../src/mcp/settings-contract";
import { parseProjectArchive } from "../src/project-archive";
import { ownershipName } from "./ownership";

Expand All @@ -17,6 +19,12 @@ type Selection = { workspace?: string; name?: string; version_id?: string; event

/** Management requests select an owner; execution continues on immutable storage. */
export class ManagedArtifactService {
get productUrl() {
const url = new URL("/", this.origin);
url.searchParams.set("workspace", this.workspace);
if (this.libraryKey === "team") url.searchParams.set("library", "team");
return url.href;
}
readonly fileStorage;
private readonly links;
constructor(private readonly env: Env, readonly workspace: string, readonly libraryKey: string,
Expand Down Expand Up @@ -67,6 +75,9 @@ export class ManagedArtifactService {
}

async callTool(name: string, args: Record<string, unknown>): Promise<CallToolResult> {
if (ARTIFACTS_SETTINGS_TOOLS.some(tool => tool.name === name)) return settingsTool(this, name, args);
if (name === "artifacts_tool") return galleryAction(this, args);
if (name === "artifacts_source") return gallerySource(this, args);
if (["artifacts_library", "artifacts_working", "artifacts_search", "artifacts_mentions"].includes(name)) return workspaceTool(this, name, args);
if (name === "artifacts_preview") name = "artifact_open";
if (name.startsWith("app_")) return this.nativeTool(name, args);
Expand All @@ -81,7 +92,7 @@ export class ManagedArtifactService {
const selection = this.selection(kind, { name: args.name as string | undefined, version_id: args.version_id as string | undefined, ...(typeof args.workspace === "string" ? { workspace: args.workspace } : {}) });
if (name.endsWith("_import")) {
parseProjectArchive(args.archive, kind);
const target = await this.links.admit(this.selection(kind, { name: args.new_name as string }), true);
const target = await this.links.admit(this.selection(kind, { name: args.new_name as string, ...(typeof args.workspace === "string" ? { workspace: args.workspace } : {}) }), true);
return this.callSelectedTool(target, name, args);
}
const target = name.endsWith("_remix")
Expand Down Expand Up @@ -177,7 +188,7 @@ export class ManagedArtifactService {
? await this.nativeController().list({ owner: this.libraryKey, workspace: all ? undefined : this.workspace, offset })
: null;
hasMore ||= workers?.next_offset != null;
return { capabilities: { scripts: true, links: true, moves: true, nativeApps: !!this.env.NATIVE_APPS, subscriptions: true }, workspace: this.workspace,
return { libraryScope: this.libraryKey === "team" ? "team" : "private", capabilities: { editing: true, scripts: true, links: true, moves: true, nativeApps: !!this.env.NATIVE_APPS, subscriptions: true }, workspace: this.workspace,
...(workers ? { workerApps: workers.apps.map(app => ({ ...app, kind: "worker" as const, key: JSON.stringify(["worker", app.workspace, app.name]) })), nativeAppProviders: workers.providers } : {}),
artifacts: [...artifacts.values()].sort((a, b) => a.name.localeCompare(b.name) || a.workspace.localeCompare(b.workspace)), nextOffset: hasMore ? offset + 100 : null };
}
Expand Down
Loading
Loading