diff --git a/README.md b/README.md index 5233623..3f7eeec 100644 --- a/README.md +++ b/README.md @@ -208,10 +208,24 @@ When the Are.na API spec changes: yarn generate ``` -This fetches the latest spec and regenerates TypeScript types and tool registrations. To regenerate from the checked-in spec without fetching it again: +This fetches the latest spec and regenerates TypeScript types, tool registrations, and output schemas. To regenerate from the checked-in spec without fetching it again: ```bash yarn generate --offline ``` The server uses `openapi-fetch` with generated types directly. Custom tools use the same generated response types as the endpoint tools. + +## Tool results + +Every tool advertises an `outputSchema` and returns successful results in the same shape in `structuredContent` and JSON text: + +```json +{ "result": { "id": 123, "type": "Text" } } +``` + +`result` contains the complete API response (including `data` and `meta` for list endpoints), or `null` for an empty response. The example above is abbreviated. Text consumers should read the payload under `result`; earlier versions returned it directly. + +Generated output schemas preserve the OpenAPI success-response contracts and include the referenced components as local `$defs`. The two custom tools have explicit schemas. Responses are validated before being reported as successful; API errors retain `isError: true` and are not validated against a success schema. An output-validation failure can occur after a write has completed, so it should not trigger an automatic write retry. + +The server uses the MCP SDK's low-level tool handlers to publish JSON Schemas directly, with Zod for input parsing and a Workers-compatible JSON Schema validator for outputs. diff --git a/package.json b/package.json index 4a04ad3..6c0b5b6 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "dev:http": "wrangler dev src/transports/http.ts", "deploy": "wrangler deploy", "typecheck": "tsc --noEmit", - "test": "vitest run --exclude 'dist/**'", + "test": "node --test .github/scripts/review-policy.test.cjs && vitest run --exclude 'dist/**' --exclude '.github/**'", "format": "prettier --write .", "prepare": "husky" }, @@ -18,10 +18,12 @@ "*.{ts,js,json,md}": "prettier --write" }, "dependencies": { + "@cfworker/json-schema": "^4.1.1", "@modelcontextprotocol/sdk": "^1.27.0", "hono": "^4.0.0", "openapi-fetch": "^0.17.0", - "zod": "^3.25.0" + "zod": "^3.25.0", + "zod-to-json-schema": "^3.25.1" }, "devDependencies": { "@cloudflare/workers-types": "^4.0.0", diff --git a/scripts/generate-tools.ts b/scripts/generate-tools.ts index d53c51b..18579ac 100644 --- a/scripts/generate-tools.ts +++ b/scripts/generate-tools.ts @@ -2,6 +2,10 @@ import { readFileSync, writeFileSync } from "node:fs"; import { execFileSync, execSync } from "node:child_process"; import { jsonSchemaToZodString } from "../src/lib/json-schema-to-zod"; +import { generateOutputSchemas } from "./output-schemas"; + +const OUTPUTS_PATH = "src/generated/output-schemas.ts"; + const SPEC_URL = "https://api.are.na/v3/openapi.json"; const SPEC_PATH = "src/generated/openapi.json"; const TYPES_PATH = "src/generated/schema.d.ts"; @@ -24,6 +28,7 @@ interface Param { interface Operation { operationId: string; + responses?: Record>; summary?: string; description?: string; parameters?: Array; @@ -298,7 +303,7 @@ function generateHandler(tool: ToolDef): string { return withArenaClient(extra, async (client) => { const { data, error } = await client.${tool.method}(${pathType}${optionsStr}); if (error) return errorResult(error); - return textResult(data); + return successResult(data); }).catch((err) => errorResult(err.message)); }`; } @@ -333,13 +338,17 @@ function main() { const paths = spec.paths as Record>; const tools: ToolDef[] = []; + const operations: Operation[] = []; for (const [path, methods] of Object.entries(paths)) { for (const httpMethod of HTTP_METHODS) { const op = methods[httpMethod]; if (!op) continue; const tool = buildToolDef(path, httpMethod, op, spec); - if (tool) tools.push(tool); + if (tool) { + tools.push(tool); + operations.push(op); + } } } @@ -348,15 +357,14 @@ function main() { `// Auto-generated by scripts/generate-tools.ts — do not edit manually`, ); lines.push(`import { z } from "zod";`); + lines.push(`import type { ToolRegistry } from "../lib/tool-registry";`); lines.push( - `import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";`, - ); - lines.push( - `import { withArenaClient, textResult, errorResult } from "../lib/tool-helpers";`, + `import { withArenaClient, successResult, errorResult } from "../lib/tool-helpers";`, ); + lines.push(`import { outputSchemas } from "./output-schemas";`); lines.push(``); lines.push( - `export function registerGeneratedTools(server: McpServer): void {`, + `export function registerGeneratedTools(server: ToolRegistry): void {`, ); for (const tool of tools) { @@ -374,6 +382,9 @@ function main() { lines.push(` {`); lines.push(` description: ${JSON.stringify(desc)},`); lines.push(` inputSchema: ${schemaInner},`); + lines.push( + ` outputSchema: outputSchemas[${JSON.stringify(tool.operationId)}],`, + ); lines.push(` },`); lines.push(generateHandler(tool)); lines.push(` );`); @@ -382,11 +393,15 @@ function main() { lines.push(`}`); lines.push(``); + writeFileSync(OUTPUTS_PATH, generateOutputSchemas(spec, operations)); writeFileSync(TOOLS_PATH, lines.join("\n")); writeFileSync(SPEC_PATH, `${JSON.stringify(spec, null, 2)}\n`); - execSync(`npx prettier --write ${TOOLS_PATH} ${SPEC_PATH} ${TYPES_PATH}`, { - stdio: "inherit", - }); + execSync( + `npx prettier --write ${TOOLS_PATH} ${SPEC_PATH} ${TYPES_PATH} ${OUTPUTS_PATH}`, + { + stdio: "inherit", + }, + ); console.log(`Generated ${tools.length} tools → ${TOOLS_PATH}`); } diff --git a/scripts/output-schemas.ts b/scripts/output-schemas.ts new file mode 100644 index 0000000..dcd9f46 --- /dev/null +++ b/scripts/output-schemas.ts @@ -0,0 +1,204 @@ +import { expandTypeArray } from "../src/lib/portable-json-schema"; + +type Schema = Record; +export interface ResponseOperation { + operationId: string; + responses?: Record; +} + +function resolvePointer(spec: Schema, ref: string): Schema { + if (!ref.startsWith("#/")) + throw new Error(`External schema reference: ${ref}`); + let value: unknown = spec; + for (const segment of ref.slice(2).split("/")) { + const key = segment.replace(/~1/g, "/").replace(/~0/g, "~"); + if (!value || typeof value !== "object" || !(key in value)) { + throw new Error(`Unresolved schema reference: ${ref}`); + } + value = (value as Schema)[key]; + } + if (!value || typeof value !== "object" || Array.isArray(value)) { + throw new Error(`Expected a schema at ${ref}`); + } + return value as Schema; +} + +export function generateOutputSchemas( + spec: Schema, + operations: ResponseOperation[], +): string { + const definitions = new Map(); + const dependencies = new Map>(); + + function copySchema(value: unknown, refs: Set): unknown { + if (Array.isArray(value)) + return value.map((item) => copySchema(item, refs)); + if (!value || typeof value !== "object") return value; + const result: Schema = {}; + for (const [key, entry] of Object.entries(value)) { + // Examples and OpenAPI-only annotations aren't validation rules. Omitting + // examples also avoids treating example data containing "$ref" as schemas. + if ( + [ + "example", + "examples", + "discriminator", + "xml", + "externalDocs", + ].includes(key) + ) + continue; + if (key === "$ref") { + if ( + typeof entry !== "string" || + !entry.startsWith("#/components/schemas/") + ) { + throw new Error(`Unsupported response schema reference: ${entry}`); + } + const name = entry.slice("#/components/schemas/".length); + refs.add(name); + if (!definitions.has(name)) { + definitions.set(name, {}); + const nested = new Set(); + dependencies.set(name, nested); + definitions.set( + name, + copySchema(resolvePointer(spec, entry), nested) as Schema, + ); + } + result[key] = `#/$defs/${name}`; + } else if ( + [ + "properties", + "patternProperties", + "$defs", + "dependentSchemas", + ].includes(key) + ) { + result[key] = Object.fromEntries( + Object.entries(entry as Schema).map(([name, child]) => [ + name, + copySchema(child, refs), + ]), + ); + } else if ( + [ + "allOf", + "anyOf", + "oneOf", + "prefixItems", + "items", + "additionalProperties", + "not", + "contains", + "if", + "then", + "else", + "propertyNames", + "unevaluatedProperties", + "unevaluatedItems", + ].includes(key) + ) { + result[key] = copySchema(entry, refs); + } else { + result[key] = entry; + } + } + // OpenAPI sometimes places documentation beside a reference in allOf. + // Lift those annotations so clients don't mistake them for untyped schemas. + if (Array.isArray(result.allOf)) { + const descriptions = result.allOf.filter( + (child) => + child && + typeof child === "object" && + Object.keys(child).length === 1 && + typeof child.description === "string", + ); + const constraints = result.allOf.filter( + (child) => !descriptions.includes(child), + ); + if (descriptions.length && constraints.length) { + result.allOf = constraints; + result.description ??= descriptions + .map((child) => child.description) + .join("\n\n"); + } + } + return expandTypeArray(result); + } + + const outputs = operations.map((operation) => { + const schemas: Schema[] = []; + const refs = new Set(); + for (const [status, original] of Object.entries( + operation.responses ?? {}, + )) { + if (!/^2\d\d$|^2XX$/.test(status)) continue; + let response = original; + const seen = new Set(); + while (typeof response.$ref === "string") { + if (seen.has(response.$ref)) + throw new Error(`Cyclic response reference: ${response.$ref}`); + seen.add(response.$ref); + response = resolvePointer(spec, response.$ref); + } + const content = response.content as + | Record + | undefined; + const schema = content?.["application/json"]?.schema; + if (!schema && status !== "204" && status !== "205") { + throw new Error( + `${operation.operationId} ${status} has no JSON response schema`, + ); + } + const copied = copySchema(schema ?? { type: "null" }, refs) as Schema; + if ( + !schemas.some((item) => JSON.stringify(item) === JSON.stringify(copied)) + ) + schemas.push(copied); + } + if (!schemas.length) + throw new Error( + `${operation.operationId} has no successful response schema`, + ); + const collect = (name: string) => { + for (const child of dependencies.get(name) ?? []) { + if (!refs.has(child)) { + refs.add(child); + collect(child); + } + } + }; + for (const name of refs) collect(name); + return { + name: operation.operationId, + schema: schemas.length === 1 ? schemas[0] : { anyOf: schemas }, + refs, + }; + }); + + return [ + "// Auto-generated by scripts/generate-tools.ts — do not edit manually", + 'import type { OutputSchema } from "../lib/tool-registry";', + `const definitions = ${JSON.stringify(Object.fromEntries(definitions), null, 2)};`, + "export const outputSchemas = {", + ...outputs.map( + ({ name, schema, refs }) => `${JSON.stringify(name)}: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { result: ${JSON.stringify(schema)} }, + required: ["result"], + additionalProperties: false, + $defs: { ${[...refs] + .sort() + .map( + (ref) => + `${JSON.stringify(ref)}: definitions[${JSON.stringify(ref)}]`, + ) + .join(",")} } + },`, + ), + "} satisfies Record;", + "", + ].join("\n"); +} diff --git a/src/__tests__/api-tools.test.ts b/src/__tests__/api-tools.test.ts index cd0af61..abf325e 100644 --- a/src/__tests__/api-tools.test.ts +++ b/src/__tests__/api-tools.test.ts @@ -1,5 +1,6 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import app from "../transports/http"; +import { textBlock } from "./fixtures"; import spec from "../generated/openapi.json"; afterEach(() => vi.unstubAllGlobals()); @@ -60,7 +61,9 @@ describe("current Are.na API tools", () => { arguments: { id: 1 }, }); expect(result.isError).toBeUndefined(); - expect(result.content).toEqual([{ type: "text", text: "null" }]); + expect(result.content).toEqual([ + { type: "text", text: JSON.stringify({ result: null }, null, 2) }, + ]); }); it("connects v3 search results using type and skips non-connectable entities", async () => { @@ -80,7 +83,7 @@ describe("current Are.na API tools", () => { arguments: { query: "architecture", target_channel_id: "research" }, }); expect(result.isError).toBeUndefined(); - expect(JSON.parse(result.content[0].text)).toMatchObject({ + expect(JSON.parse(result.content[0].text).result).toMatchObject({ found: 3, connected: 2, skipped: [{ id: 3, type: "User" }], @@ -153,7 +156,7 @@ describe("current Are.na API tools", () => { arguments: { channel_id: "research", sample_size: 2 }, }); expect(result.isError).toBeUndefined(); - expect(JSON.parse(result.content[0].text)).toMatchObject({ + expect(JSON.parse(result.content[0].text).result).toMatchObject({ channel: { description: "Plain description" }, owner: { type: "Group" }, collaborators: [{ type: "User" }], @@ -171,7 +174,7 @@ describe("current Are.na API tools", () => { }); it("forwards structured destinations, cover URLs, and scalar metadata", async () => { - const fetch = mockApi([{ id: 1, type: "Link" }]); + const fetch = mockApi([textBlock]); const args = { value: "https://example.com", cover_url: "https://example.com/cover.jpg", @@ -190,7 +193,7 @@ describe("current Are.na API tools", () => { }); it("preserves null metadata deletions", async () => { - const fetch = mockApi([{ id: 1 }]); + const fetch = mockApi([textBlock]); const result = await rpc("tools/call", { name: "updateBlock", arguments: { id: 1, metadata: { status: null } }, diff --git a/src/__tests__/fixtures.ts b/src/__tests__/fixtures.ts new file mode 100644 index 0000000..78ef51d --- /dev/null +++ b/src/__tests__/fixtures.ts @@ -0,0 +1,28 @@ +import type { components } from "../generated/schema"; + +export const textBlock = { + id: 1, + base_type: "Block", + type: "Text", + title: null, + description: null, + state: "available", + visibility: "public", + comment_count: 0, + created_at: "2026-10-20T12:00:00Z", + updated_at: "2026-10-20T12:00:00Z", + user: { + id: 2, + type: "User", + name: "Example", + slug: "example", + avatar: null, + initials: "E", + }, + _links: { + self: { href: "https://api.are.na/v3/blocks/1" }, + comments: { href: "https://api.are.na/v3/blocks/1/comments" }, + }, + content: { markdown: "Hello", html: "

Hello

", plain: "Hello" }, + metadata: { status: "reviewed", score: 0.5, featured: true }, +} satisfies components["schemas"]["TextBlock"]; diff --git a/src/__tests__/output-schema-generation.test.ts b/src/__tests__/output-schema-generation.test.ts new file mode 100644 index 0000000..c9274f2 --- /dev/null +++ b/src/__tests__/output-schema-generation.test.ts @@ -0,0 +1,169 @@ +import { describe, expect, it } from "vitest"; +import ts from "typescript"; +import { CfWorkerJsonSchemaValidator } from "@modelcontextprotocol/sdk/validation/cfworker"; +import { + generateOutputSchemas, + type ResponseOperation, +} from "../../scripts/output-schemas"; +import type { OutputSchema } from "../lib/tool-registry"; + +function generate( + spec: Record, + responses: ResponseOperation["responses"], +): OutputSchema { + const source = generateOutputSchemas(spec, [ + { operationId: "test", responses }, + ]); + const js = ts.transpileModule(source, { + compilerOptions: { module: ts.ModuleKind.CommonJS }, + }).outputText; + const exports: { outputSchemas?: { test: OutputSchema } } = {}; + // Execute only this test's generated module, never API-provided code. + new Function("exports", js)(exports); + return exports.outputSchemas!.test; +} +const jsonResponse = (schema: Record) => ({ + content: { "application/json": { schema } }, +}); + +describe("output schema generation", () => { + it("normalizes type arrays without losing nullability, bounds, or existing unions", () => { + const original = { + type: ["string", "integer", "null"], + minLength: 2, + minimum: 1, + anyOf: [{ enum: [null, "ok", "x", 2, 0] }], + allOf: [{ not: { const: 3 } }], + }; + const schema = generate({}, { "200": jsonResponse(original) }); + expect(JSON.stringify(schema)).not.toMatch(/"type":\[/); + const provider = new CfWorkerJsonSchemaValidator(); + const before = provider.getValidator(original); + const after = provider.getValidator(schema); + for (const value of [ + null, + "ok", + "x", + "other", + 2, + 0, + 3, + 1.5, + true, + {}, + [], + ]) { + expect(after({ result: value }).valid).toBe(before(value).valid); + } + }); + + it("lifts description-only allOf branches without weakening validation", () => { + const schema = generate( + {}, + { + "200": jsonResponse({ + allOf: [{ type: "string" }, { description: "A label" }], + }), + }, + ); + expect(schema.properties?.result).toEqual({ + allOf: [{ type: "string" }], + description: "A label", + }); + const validate = new CfWorkerJsonSchemaValidator().getValidator(schema); + expect(validate({ result: "ok" }).valid).toBe(true); + expect(validate({ result: 1 }).valid).toBe(false); + }); + + it("includes every success status, deduplicates identical schemas, and excludes errors", () => { + const schema = generate( + {}, + { + "200": jsonResponse({ type: "string" }), + "201": jsonResponse({ type: "number" }), + "202": jsonResponse({ type: "number" }), + "204": {}, + "400": jsonResponse({ type: "boolean" }), + }, + ); + expect(schema.properties).toEqual({ + result: { + anyOf: [{ type: "string" }, { type: "number" }, { type: "null" }], + }, + }); + const validate = new CfWorkerJsonSchemaValidator().getValidator(schema); + for (const result of ["ok", 1, null]) + expect(validate({ result }).valid).toBe(true); + expect(validate({ result: false }).valid).toBe(false); + }); + + it("resolves response references and preserves recursive component references locally", () => { + const schema = generate( + { + components: { + responses: { + Success: jsonResponse({ $ref: "#/components/schemas/Node" }), + }, + schemas: { + Node: { + type: "object", + properties: { + value: { type: "string" }, + child: { $ref: "#/components/schemas/Node" }, + }, + required: ["value"], + }, + Unused: { type: "string" }, + }, + }, + }, + { "200": { $ref: "#/components/responses/Success" } }, + ); + expect(Object.keys(schema.$defs as object)).toEqual(["Node"]); + const validate = new CfWorkerJsonSchemaValidator().getValidator(schema); + expect( + validate({ result: { value: "a", child: { value: "b" } } }).valid, + ).toBe(true); + expect( + validate({ result: { value: "a", child: { value: 3 } } }).valid, + ).toBe(false); + }); + + it("does not remove fields named example or rewrite references in default data", () => { + const schema = generate( + {}, + { + "200": jsonResponse({ + type: "object", + properties: { example: { type: "string" } }, + default: { $ref: "literal-data" }, + required: ["example"], + example: { example: "documentation" }, + }), + }, + ); + expect(schema.properties).toEqual({ + result: { + type: "object", + properties: { example: { type: "string" } }, + default: { $ref: "literal-data" }, + required: ["example"], + }, + }); + }); + + it("fails generation on undocumented successes or unresolved references", () => { + expect(() => generate({}, { "200": {} })).toThrow( + "no JSON response schema", + ); + expect(() => + generate( + {}, + { "200": jsonResponse({ $ref: "#/components/schemas/Missing" }) }, + ), + ).toThrow("Unresolved schema reference"); + expect(() => generate({}, { "400": {} })).toThrow( + "no successful response schema", + ); + }); +}); diff --git a/src/__tests__/output-schemas.test.ts b/src/__tests__/output-schemas.test.ts new file mode 100644 index 0000000..f707649 --- /dev/null +++ b/src/__tests__/output-schemas.test.ts @@ -0,0 +1,198 @@ +import { + afterAll, + afterEach, + beforeAll, + describe, + expect, + it, + vi, +} from "vitest"; +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; +import { CfWorkerJsonSchemaValidator } from "@modelcontextprotocol/sdk/validation/cfworker"; +import type { Tool } from "@modelcontextprotocol/sdk/types.js"; +import { createMcpServer } from "../server"; +import { outputSchemas } from "../generated/output-schemas"; +import { textBlock } from "./fixtures"; + +const provider = new CfWorkerJsonSchemaValidator(); + +describe("published output contracts", () => { + const server = createMcpServer(); + // Use the default SDK client validator, independently of the server validator. + const client = new Client({ name: "output-contract-test", version: "1" }); + let tools: Tool[]; + + beforeAll(async () => { + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + const send = clientTransport.send.bind(clientTransport); + clientTransport.send = (message, options) => + send(message, { + ...options, + authInfo: { + token: "test-token", + clientId: "test", + scopes: ["read", "write"], + }, + }); + await server.connect(serverTransport); + await client.connect(clientTransport); + tools = (await client.listTools()).tools; + }); + afterAll(async () => { + await client.close(); + await server.close(); + }); + afterEach(() => vi.unstubAllGlobals()); + + it("publishes usable schemas for all 52 tools", () => { + expect(tools).toHaveLength(52); + for (const tool of tools) { + expect(JSON.stringify(tool.inputSchema), tool.name).not.toMatch( + /"type":\[/, + ); + expect(JSON.stringify(tool.outputSchema), tool.name).not.toMatch( + /"type":\[/, + ); + expect(tool.outputSchema?.required).toEqual(["result"]); + const validate = provider.getValidator(tool.outputSchema!); + expect(validate({}).valid, tool.name).toBe(false); + expect(validate({ result: "not an API response" }).valid, tool.name).toBe( + false, + ); + } + }); + + it("returns the same validated payload in text and structured content", async () => { + vi.stubGlobal( + "fetch", + vi.fn(async () => Response.json(textBlock)), + ); + const response = await client.callTool({ + name: "getBlock", + arguments: { id: 1 }, + }); + expect(response.isError).toBeUndefined(); + expect(response.structuredContent).toEqual({ result: textBlock }); + expect(response.content).toEqual([ + { type: "text", text: JSON.stringify({ result: textBlock }, null, 2) }, + ]); + }); + + it("rejects malformed upstream payloads instead of reporting success", async () => { + vi.stubGlobal( + "fetch", + vi.fn(async () => Response.json({ ...textBlock, id: "wrong" })), + ); + const response = await client.callTool({ + name: "getBlock", + arguments: { id: 1 }, + }); + expect(response.isError).toBe(true); + expect(response.structuredContent).toBeUndefined(); + expect(response.content).toEqual([ + { + type: "text", + text: expect.stringContaining("Output validation failed for getBlock"), + }, + ]); + }); + + it("preserves API errors without validating against the success schema", async () => { + vi.stubGlobal( + "fetch", + vi.fn(async () => + Response.json({ error: "Forbidden", code: 403 }, { status: 403 }), + ), + ); + const response = await client.callTool({ + name: "getBlock", + arguments: { id: 1 }, + }); + expect(response.isError).toBe(true); + expect(response.content).toEqual([ + { type: "text", text: JSON.stringify({ error: "Forbidden", code: 403 }) }, + ]); + }); + + it("uses an explicit null result for empty successes", async () => { + vi.stubGlobal( + "fetch", + vi.fn(async () => new Response(null, { status: 204 })), + ); + const response = await client.callTool({ + name: "deleteComment", + arguments: { id: 1 }, + }); + expect(response.isError).toBeUndefined(); + expect(response.structuredContent).toEqual({ result: null }); + }); + + it("validates current custom-tool outputs including skipped types and omitted optional fields", async () => { + vi.stubGlobal( + "fetch", + vi.fn(async () => Response.json({ data: [{ id: 2, type: "User" }] })), + ); + const response = await client.callTool({ + name: "searchAndConnect", + arguments: { query: "test", target_channel_id: "research" }, + }); + expect(response.isError).toBeUndefined(); + expect(response.structuredContent).toEqual({ + result: { + query: "test", + target_channel: "research", + found: 1, + connected: 0, + skipped: [{ id: 2, type: "User" }], + items: [], + }, + }); + }); + + it("retains unions, nullability, formats, dictionaries and additive fields", () => { + const validate = provider.getValidator(outputSchemas.getBlock); + expect( + validate({ result: { ...textBlock, future_field: "preserved" } }).valid, + ).toBe(true); + expect(validate({ result: { ...textBlock, type: "Image" } }).valid).toBe( + false, + ); + expect( + validate({ result: { ...textBlock, created_at: "yesterday" } }).valid, + ).toBe(false); + expect( + validate({ result: { ...textBlock, metadata: { nested: {} } } }).valid, + ).toBe(false); + expect( + validate({ + result: { + ...textBlock, + metadata: Object.fromEntries( + Array.from({ length: 51 }, (_, i) => [`key_${i}`, i]), + ), + }, + }).valid, + ).toBe(false); + expect( + validate({ + result: { + ...textBlock, + _links: { + self: { href: "https://example.com" }, + next: { href: 123 }, + }, + }, + }).valid, + ).toBe(false); + }); + + it("exposes real response fields instead of an untyped result", () => { + const schema = outputSchemas.getBlock; + expect(schema.properties.result).toEqual({ $ref: "#/$defs/Block" }); + expect(schema.$defs.Block.oneOf).toHaveLength(6); + expect(schema.$defs.BaseBlockProperties.required).toContain("id"); + expect(schema.$defs.BaseBlockProperties.properties.id.type).toBe("integer"); + }); +}); diff --git a/src/__tests__/tool-helpers.test.ts b/src/__tests__/tool-helpers.test.ts index 7b89258..51e6eaf 100644 --- a/src/__tests__/tool-helpers.test.ts +++ b/src/__tests__/tool-helpers.test.ts @@ -1,18 +1,21 @@ import { describe, it, expect } from "vitest"; import { - textResult, + successResult, errorResult, requireAuth, AuthRequiredError, } from "../lib/tool-helpers"; -describe("textResult", () => { +describe("successResult", () => { it("wraps data as pretty-printed JSON text content", () => { - const result = textResult({ id: 1, title: "test" }); + const result = successResult({ id: 1, title: "test" }); expect(result.content).toHaveLength(1); expect(result.content[0]).toEqual({ type: "text", - text: JSON.stringify({ id: 1, title: "test" }, null, 2), + text: JSON.stringify({ result: { id: 1, title: "test" } }, null, 2), + }); + expect(result.structuredContent).toEqual({ + result: { id: 1, title: "test" }, }); expect(result.isError).toBeUndefined(); }); diff --git a/src/generated/output-schemas.ts b/src/generated/output-schemas.ts new file mode 100644 index 0000000..2b9ae64 --- /dev/null +++ b/src/generated/output-schemas.ts @@ -0,0 +1,3477 @@ +// Auto-generated by scripts/generate-tools.ts — do not edit manually +import type { OutputSchema } from "../lib/tool-registry"; +const definitions = { + PresignResponse: { + type: "object", + description: "Response containing presigned S3 upload URLs", + properties: { + files: { + type: "array", + description: "Presigned URLs for each requested file", + items: { + $ref: "#/$defs/PresignedFile", + }, + }, + expires_in: { + type: "integer", + description: "Seconds until the presigned URLs expire", + }, + }, + required: ["files", "expires_in"], + }, + PresignedFile: { + type: "object", + description: "A presigned S3 upload URL for a single file", + properties: { + upload_url: { + type: "string", + format: "uri", + description: + "Presigned S3 PUT URL. Upload your file by sending a PUT request\nto this URL with the file bytes as the body and the `Content-Type`\nheader matching the content_type you specified.\n", + }, + key: { + type: "string", + description: "The S3 object key where the file will be stored", + }, + content_type: { + type: "string", + description: + "The content type that was validated and should be used in the PUT request", + }, + }, + required: ["upload_url", "key", "content_type"], + }, + Block: { + description: + "A block is a piece of content on Are.na. Blocks come in different types,\neach with its own set of fields. Use the `type` field to determine which\nfields are available.\n", + oneOf: [ + { + $ref: "#/$defs/TextBlock", + }, + { + $ref: "#/$defs/ImageBlock", + }, + { + $ref: "#/$defs/LinkBlock", + }, + { + $ref: "#/$defs/AttachmentBlock", + }, + { + $ref: "#/$defs/EmbedBlock", + }, + { + $ref: "#/$defs/PendingBlock", + }, + ], + }, + TextBlock: { + description: "A text block containing markdown content", + allOf: [ + { + $ref: "#/$defs/BaseBlockProperties", + }, + { + type: "object", + properties: { + type: { + type: "string", + enum: ["Text"], + description: 'Block type (always "Text" for TextBlock)', + }, + content: { + $ref: "#/$defs/MarkdownContent", + description: + "Text content with markdown, HTML, and plain text renderings", + }, + }, + required: ["type", "content"], + }, + ], + }, + BaseBlockProperties: { + type: "object", + description: "Common properties shared by all block types", + properties: { + id: { + type: "integer", + description: "Unique identifier for the block", + }, + base_type: { + type: "string", + description: 'Base type of the block (always "Block")', + enum: ["Block"], + }, + title: { + description: "Block title", + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + }, + description: { + oneOf: [ + { + $ref: "#/$defs/MarkdownContent", + }, + { + type: "null", + }, + ], + description: "Block description with multiple renderings", + }, + state: { + $ref: "#/$defs/BlockState", + }, + visibility: { + $ref: "#/$defs/BlockVisibility", + }, + comment_count: { + type: "integer", + description: "Number of comments on the block", + }, + created_at: { + type: "string", + format: "date-time", + description: "When the block was created", + }, + updated_at: { + type: "string", + format: "date-time", + description: "When the block was last updated", + }, + user: { + $ref: "#/$defs/EmbeddedUser", + }, + metadata: { + oneOf: [ + { + $ref: "#/$defs/Metadata", + }, + { + type: "null", + }, + ], + description: "Custom key-value metadata", + }, + source: { + oneOf: [ + { + $ref: "#/$defs/BlockSource", + }, + { + type: "null", + }, + ], + description: + "Source URL and metadata (if block was created from a URL)", + }, + _links: { + $ref: "#/$defs/Links", + description: "HATEOAS links for navigation", + }, + connection: { + oneOf: [ + { + $ref: "#/$defs/EmbeddedConnection", + }, + { + type: "null", + }, + ], + description: + "Connection context (only present when block is returned as part of channel contents).\nContains position, pinned status, and information about who connected the block.\n", + }, + can: { + oneOf: [ + { + $ref: "#/$defs/BlockAbilities", + }, + { + type: "null", + }, + ], + description: + "Abilities object (only present for full block responses, not in channel contents).\nIndicates what actions the current user can perform on this block.\n", + }, + }, + required: [ + "id", + "base_type", + "state", + "visibility", + "comment_count", + "created_at", + "updated_at", + "user", + "_links", + ], + }, + MarkdownContent: { + type: "object", + description: "Markdown content with multiple renderings", + properties: { + markdown: { + type: "string", + description: "Original markdown value", + }, + html: { + type: "string", + description: "HTML rendering of the markdown", + }, + plain: { + type: "string", + description: "Plain text rendering of the markdown", + }, + }, + required: ["markdown", "html", "plain"], + }, + BlockState: { + type: "string", + description: + "Processing state of a block.\n- `processing`: Being processed (e.g., image resizing)\n- `available`: Ready for display\n- `failed`: Processing failed\n", + enum: ["processing", "available", "failed"], + }, + BlockVisibility: { + type: "string", + description: + "Visibility of a block.\n- `public`: Visible to everyone\n- `private`: Only visible to owner\n- `orphan`: Not connected to any channel\n", + enum: ["public", "private", "orphan"], + }, + EmbeddedUser: { + type: "object", + description: + "Embedded user representation (used when user is nested in other resources)", + properties: { + id: { + type: "integer", + description: "Unique identifier for the user", + }, + type: { + type: "string", + description: "User type", + enum: ["User"], + }, + name: { + type: "string", + description: "User's display name", + }, + slug: { + type: "string", + description: "URL-safe identifier (use this in API paths)", + }, + username: { + readOnly: true, + description: + "User's username (either automatically generated or customized)", + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + }, + avatar: { + format: "uri", + description: "URL to user's avatar image", + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + }, + initials: { + type: "string", + description: "User's initials", + }, + }, + required: ["id", "type", "name", "slug", "avatar", "initials"], + }, + Metadata: { + type: "object", + description: + "Arbitrary key-value pairs stored on an entity.\nKeys are alphanumeric/underscore, max 40 characters.\nValues are scalars (string, number, boolean). Max 50 keys, 32KB total.\n", + additionalProperties: { + oneOf: [ + { + type: "string", + maxLength: 2000, + }, + { + type: "number", + }, + { + type: "boolean", + }, + ], + }, + maxProperties: 50, + }, + BlockSource: { + type: "object", + properties: { + url: { + type: "string", + format: "uri", + description: "Source URL", + }, + title: { + description: "Source title", + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + }, + provider: { + oneOf: [ + { + $ref: "#/$defs/BlockProvider", + }, + { + type: "null", + }, + ], + }, + }, + required: ["url"], + }, + BlockProvider: { + type: "object", + properties: { + name: { + type: "string", + description: "Provider name (from parsed URI host)", + }, + url: { + type: "string", + format: "uri", + description: "Provider URL (from parsed URI scheme and host)", + }, + }, + required: ["name", "url"], + }, + Links: { + type: "object", + description: + 'HATEOAS links for navigation and discovery.\nFollows HAL (Hypertext Application Language) format where link relationships \nare expressed as object keys (e.g., "self", "user", "channels").\n', + properties: { + self: { + $ref: "#/$defs/Link", + description: "Link to the current resource (always present)", + }, + }, + required: ["self"], + additionalProperties: { + $ref: "#/$defs/Link", + }, + }, + Link: { + type: "object", + description: + "A hypermedia link containing the URL of a linked resource.\nThe relationship type is expressed by the key in the parent _links object.\n", + properties: { + href: { + type: "string", + format: "uri", + description: "The URL of the linked resource", + }, + }, + required: ["href"], + }, + EmbeddedConnection: { + type: "object", + description: + "Embedded connection representation used when connection is nested in other resources", + properties: { + id: { + type: "integer", + description: "Unique identifier for the connection", + }, + position: { + type: "integer", + description: "Position of the item within the channel (1-indexed)", + }, + pinned: { + type: "boolean", + description: "Whether the item is pinned", + }, + metadata: { + oneOf: [ + { + $ref: "#/$defs/Metadata", + }, + { + type: "null", + }, + ], + description: "Custom key-value metadata", + }, + connected_at: { + type: "string", + format: "date-time", + description: "When the item was connected", + }, + connected_by: { + oneOf: [ + { + $ref: "#/$defs/EmbeddedUser", + }, + { + type: "null", + }, + ], + description: "User who created this connection", + }, + }, + required: ["id", "position", "pinned", "connected_at", "connected_by"], + }, + BlockAbilities: { + type: "object", + description: "Actions the current user can perform on the block", + properties: { + manage: { + type: "boolean", + description: "Whether the user can manage (update/delete) this block", + }, + comment: { + type: "boolean", + description: "Whether the user can comment on this block", + }, + connect: { + type: "boolean", + description: "Whether the user can connect this block to channels", + }, + }, + required: ["manage", "comment", "connect"], + }, + ImageBlock: { + description: "An image block containing an uploaded or scraped image", + allOf: [ + { + $ref: "#/$defs/BaseBlockProperties", + }, + { + type: "object", + properties: { + type: { + type: "string", + enum: ["Image"], + description: 'Block type (always "Image" for ImageBlock)', + }, + image: { + $ref: "#/$defs/BlockImage", + description: "Image data with multiple resolutions", + }, + }, + required: ["type", "image"], + }, + ], + }, + BlockImage: { + type: "object", + properties: { + alt_text: { + description: "Alternative text associated with the image", + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + }, + blurhash: { + description: + "BlurHash representation of the image for progressive loading", + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + }, + width: { + description: "Original image width in pixels", + anyOf: [ + { + type: "integer", + }, + { + type: "null", + }, + ], + }, + height: { + description: "Original image height in pixels", + anyOf: [ + { + type: "integer", + }, + { + type: "null", + }, + ], + }, + src: { + type: "string", + format: "uri", + description: "URL to the original image", + }, + aspect_ratio: { + format: "float", + description: "Image aspect ratio (width / height)", + anyOf: [ + { + type: "number", + }, + { + type: "null", + }, + ], + }, + content_type: { + type: "string", + description: "Image content type", + }, + filename: { + type: "string", + description: "Image filename", + }, + file_size: { + description: "File size in bytes", + anyOf: [ + { + type: "integer", + }, + { + type: "null", + }, + ], + }, + updated_at: { + type: "string", + format: "date-time", + description: "When the image was last updated", + }, + small: { + $ref: "#/$defs/ImageVersion", + description: "Small image version (thumb)", + }, + medium: { + $ref: "#/$defs/ImageVersion", + description: "Medium image version (display)", + }, + large: { + $ref: "#/$defs/ImageVersion", + description: "Large image version", + }, + square: { + $ref: "#/$defs/ImageVersion", + description: "Square cropped image version", + }, + }, + required: ["small", "medium", "large", "square"], + }, + ImageVersion: { + type: "object", + description: + "A resized/processed version of an image with multiple resolution URLs", + properties: { + src: { + type: "string", + format: "uri", + description: "Default image URL (1x resolution)", + }, + src_2x: { + type: "string", + format: "uri", + description: "2x resolution image URL for high DPI displays", + }, + width: { + description: "Width of the resized image in pixels", + anyOf: [ + { + type: "integer", + }, + { + type: "null", + }, + ], + }, + height: { + description: "Height of the resized image in pixels", + anyOf: [ + { + type: "integer", + }, + { + type: "null", + }, + ], + }, + }, + required: ["src", "src_2x"], + }, + LinkBlock: { + description: "A link block representing a URL with optional preview", + allOf: [ + { + $ref: "#/$defs/BaseBlockProperties", + }, + { + type: "object", + properties: { + type: { + type: "string", + enum: ["Link"], + description: 'Block type (always "Link" for LinkBlock)', + }, + image: { + oneOf: [ + { + $ref: "#/$defs/BlockImage", + }, + { + type: "null", + }, + ], + description: "Preview image (if available)", + }, + content: { + oneOf: [ + { + $ref: "#/$defs/MarkdownContent", + }, + { + type: "null", + }, + ], + description: "Extracted text content from the link", + }, + }, + required: ["type"], + }, + ], + }, + AttachmentBlock: { + description: "An attachment block containing an uploaded file", + allOf: [ + { + $ref: "#/$defs/BaseBlockProperties", + }, + { + type: "object", + properties: { + type: { + type: "string", + enum: ["Attachment"], + description: 'Block type (always "Attachment" for AttachmentBlock)', + }, + attachment: { + $ref: "#/$defs/BlockAttachment", + description: "Attachment file data", + }, + image: { + oneOf: [ + { + $ref: "#/$defs/BlockImage", + }, + { + type: "null", + }, + ], + description: "Preview image (for PDFs and other previewable files)", + }, + }, + required: ["type", "attachment"], + }, + ], + }, + BlockAttachment: { + type: "object", + properties: { + filename: { + description: "Attachment filename", + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + }, + content_type: { + description: "Attachment content type", + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + }, + file_size: { + description: "File size in bytes", + anyOf: [ + { + type: "integer", + }, + { + type: "null", + }, + ], + }, + file_extension: { + description: "File extension", + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + }, + updated_at: { + format: "date-time", + description: "When the attachment was last updated", + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + }, + url: { + type: "string", + format: "uri", + description: "Attachment download URL", + }, + }, + required: ["url"], + }, + EmbedBlock: { + description: + "An embed block containing embedded media (video, audio, etc.)", + allOf: [ + { + $ref: "#/$defs/BaseBlockProperties", + }, + { + type: "object", + properties: { + type: { + type: "string", + enum: ["Embed"], + description: 'Block type (always "Embed" for EmbedBlock)', + }, + embed: { + $ref: "#/$defs/BlockEmbed", + description: "Embed data including HTML and dimensions", + }, + image: { + oneOf: [ + { + $ref: "#/$defs/BlockImage", + }, + { + type: "null", + }, + ], + description: "Thumbnail image (if available)", + }, + }, + required: ["type", "embed"], + }, + ], + }, + BlockEmbed: { + type: "object", + properties: { + url: { + format: "uri", + description: "Embed URL", + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + }, + type: { + description: "Embed type", + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + }, + title: { + description: "Embed title", + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + }, + author_name: { + description: "Author name", + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + }, + author_url: { + format: "uri", + description: "Author URL", + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + }, + source_url: { + format: "uri", + description: "Embed source URL", + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + }, + width: { + description: "Embed width", + anyOf: [ + { + type: "integer", + }, + { + type: "null", + }, + ], + }, + height: { + description: "Embed height", + anyOf: [ + { + type: "integer", + }, + { + type: "null", + }, + ], + }, + html: { + description: "Embed HTML", + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + }, + thumbnail_url: { + format: "uri", + description: "Thumbnail URL", + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + }, + }, + }, + PendingBlock: { + description: + "A block that is currently being processed. The final type (Text, Image, Link, etc.)\nwill be determined once processing completes. Check the `state` field for processing status.\n", + allOf: [ + { + $ref: "#/$defs/BaseBlockProperties", + }, + { + type: "object", + properties: { + type: { + type: "string", + enum: ["PendingBlock"], + description: + 'Block type (always "PendingBlock" for blocks being processed)', + }, + }, + required: ["type"], + }, + ], + }, + BatchResponse: { + type: "object", + description: "Response returned when a batch is accepted for processing", + properties: { + batch_id: { + type: "string", + format: "uuid", + description: "Unique identifier for this batch", + }, + status: { + type: "string", + enum: ["pending"], + description: "Initial status of the batch", + }, + total: { + type: "integer", + description: "Total number of blocks queued for creation", + }, + }, + required: ["batch_id", "status", "total"], + }, + BatchStatus: { + type: "object", + description: "Current status of a batch processing job", + properties: { + batch_id: { + type: "string", + format: "uuid", + description: "Unique identifier for this batch", + }, + status: { + type: "string", + enum: ["pending", "processing", "completed", "failed"], + description: "Current processing status", + }, + total: { + type: "integer", + description: "Total number of blocks in the batch", + }, + successful_count: { + type: "integer", + description: "Number of blocks created so far", + }, + failed_count: { + type: "integer", + description: "Number of blocks that failed", + }, + successful: { + type: "array", + description: "Successfully created blocks", + items: { + type: "object", + properties: { + index: { + type: "integer", + description: "Original index in the request array", + }, + block_id: { + type: "integer", + description: "ID of the created block", + }, + }, + required: ["index", "block_id"], + }, + }, + failed: { + type: "array", + description: "Blocks that failed to create", + items: { + type: "object", + properties: { + index: { + type: "integer", + description: "Original index in the request array", + }, + error: { + type: "string", + description: "Error message", + }, + }, + required: ["index", "error"], + }, + }, + created_at: { + type: "string", + format: "date-time", + description: "When the batch was submitted", + }, + completed_at: { + type: "string", + format: "date-time", + description: + "When the batch finished processing (present when completed or failed)", + }, + error: { + type: "string", + description: "Top-level error message if the entire batch failed", + }, + }, + required: [ + "batch_id", + "status", + "total", + "successful_count", + "failed_count", + ], + }, + ChannelListResponse: { + description: "Paginated list of channels with total count", + allOf: [ + { + $ref: "#/$defs/ChannelList", + }, + { + $ref: "#/$defs/PaginatedResponse", + }, + ], + }, + ChannelList: { + type: "object", + description: "Data payload containing an array of channels", + properties: { + data: { + type: "array", + description: "Array of channels", + items: { + $ref: "#/$defs/Channel", + }, + }, + }, + required: ["data"], + }, + Channel: { + type: "object", + properties: { + id: { + type: "integer", + description: "Unique identifier for the channel", + }, + type: { + type: "string", + description: "Channel type", + enum: ["Channel"], + }, + slug: { + type: "string", + description: "Channel URL slug", + }, + title: { + type: "string", + description: "Channel title", + }, + description: { + oneOf: [ + { + $ref: "#/$defs/MarkdownContent", + }, + { + type: "null", + }, + ], + description: "Channel description with multiple renderings", + }, + state: { + $ref: "#/$defs/ChannelState", + }, + visibility: { + $ref: "#/$defs/ChannelVisibility", + }, + created_at: { + type: "string", + format: "date-time", + description: "When the channel was created", + }, + updated_at: { + type: "string", + format: "date-time", + description: "When the channel was last updated", + }, + metadata: { + oneOf: [ + { + $ref: "#/$defs/Metadata", + }, + { + type: "null", + }, + ], + description: "Custom key-value metadata", + }, + owner: { + $ref: "#/$defs/ChannelOwner", + }, + counts: { + $ref: "#/$defs/ChannelCounts", + }, + collaborators: { + type: "array", + description: + "Collaborators on this channel (users and groups).\nOnly present when channel is returned as a full resource, not when embedded.\n", + items: { + $ref: "#/$defs/ChannelCollaborator", + }, + }, + _links: { + allOf: [ + { + $ref: "#/$defs/Links", + }, + ], + description: "HATEOAS links for navigation", + }, + connection: { + oneOf: [ + { + $ref: "#/$defs/EmbeddedConnection", + }, + { + type: "null", + }, + ], + description: + "Connection context (only present when channel is returned as part of another channel's contents).\nContains position, pinned status, and information about who connected the channel.\n", + }, + can: { + oneOf: [ + { + $ref: "#/$defs/ChannelAbilities", + }, + { + type: "null", + }, + ], + description: + "Actions the current user can perform on this channel.\nOnly present when channel is returned as a full resource, not when embedded.\n", + }, + }, + required: [ + "id", + "type", + "slug", + "title", + "state", + "visibility", + "created_at", + "updated_at", + "owner", + "counts", + "_links", + ], + }, + ChannelState: { + type: "string", + description: + "Lifecycle state of a channel.\n- `available`: Active and accessible\n- `deleted`: Soft deleted\n", + enum: ["available", "deleted"], + }, + ChannelVisibility: { + type: "string", + description: + "Visibility level of a channel:\n- `public`: Anyone can view and connect to the channel\n- `private`: Only the owner and collaborators can view\n- `closed`: Anyone can view, but only collaborators can add content\n", + enum: ["public", "private", "closed"], + }, + ChannelOwner: { + description: "Channel owner (User or Group)", + oneOf: [ + { + $ref: "#/$defs/EmbeddedUser", + }, + { + $ref: "#/$defs/EmbeddedGroup", + }, + ], + }, + EmbeddedGroup: { + type: "object", + description: + "Embedded group representation (used when group is nested in other resources)", + properties: { + id: { + type: "integer", + description: "Unique identifier for the group", + }, + type: { + type: "string", + description: "Group type", + enum: ["Group"], + }, + name: { + type: "string", + description: "Group's name", + }, + slug: { + type: "string", + description: "Group's URL slug", + }, + avatar: { + format: "uri", + description: "URL to group's avatar image", + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + }, + initials: { + type: "string", + description: "Group's initials", + }, + }, + required: ["id", "type", "name", "slug", "avatar", "initials"], + }, + ChannelCounts: { + type: "object", + description: "Counts of various items in the channel", + properties: { + blocks: { + type: "integer", + description: "Number of blocks in the channel", + }, + channels: { + type: "integer", + description: "Number of channels connected to this channel", + }, + contents: { + type: "integer", + description: "Total number of contents (blocks + channels)", + }, + collaborators: { + type: "integer", + description: "Number of collaborators on the channel", + }, + }, + required: ["blocks", "channels", "contents", "collaborators"], + }, + ChannelCollaborator: { + description: "Channel collaborator (User or Group)", + oneOf: [ + { + $ref: "#/$defs/EmbeddedUser", + }, + { + $ref: "#/$defs/EmbeddedGroup", + }, + ], + }, + ChannelAbilities: { + type: "object", + description: "Actions the current user can perform on the channel", + properties: { + add_to: { + type: "boolean", + description: "Whether the user can add blocks to this channel", + }, + update: { + type: "boolean", + description: "Whether the user can update this channel", + }, + destroy: { + type: "boolean", + description: "Whether the user can delete this channel", + }, + manage_collaborators: { + type: "boolean", + description: "Whether the user can add/remove collaborators", + }, + }, + required: ["add_to", "update", "destroy", "manage_collaborators"], + }, + PaginatedResponse: { + type: "object", + description: + "Base schema for all paginated responses (use allOf to extend with specific data type)", + properties: { + meta: { + $ref: "#/$defs/PaginationMeta", + }, + }, + required: ["data", "meta"], + }, + PaginationMeta: { + type: "object", + description: "Pagination metadata", + properties: { + current_page: { + type: "integer", + description: "Current page number", + }, + next_page: { + description: "Next page number (null if last page)", + anyOf: [ + { + type: "integer", + }, + { + type: "null", + }, + ], + }, + prev_page: { + description: "Previous page number (null if first page)", + anyOf: [ + { + type: "integer", + }, + { + type: "null", + }, + ], + }, + per_page: { + type: "integer", + description: "Number of items per page", + }, + total_pages: { + type: "integer", + description: "Total number of pages available", + }, + total_count: { + type: "integer", + description: "Total number of items available", + }, + has_more_pages: { + type: "boolean", + description: "Whether there are more pages available", + }, + }, + required: [ + "current_page", + "per_page", + "total_pages", + "total_count", + "has_more_pages", + ], + }, + CommentListResponse: { + description: "Paginated list of comments with total count", + allOf: [ + { + $ref: "#/$defs/CommentList", + }, + { + $ref: "#/$defs/PaginatedResponse", + }, + ], + }, + CommentList: { + type: "object", + description: "Data payload containing an array of comments", + properties: { + data: { + type: "array", + description: "Array of comments", + items: { + $ref: "#/$defs/Comment", + }, + }, + }, + required: ["data"], + }, + Comment: { + type: "object", + description: "A comment on a block", + properties: { + id: { + type: "integer", + description: "Unique identifier for the comment", + }, + type: { + type: "string", + description: "Comment type", + enum: ["Comment"], + }, + body: { + oneOf: [ + { + $ref: "#/$defs/MarkdownContent", + }, + { + type: "null", + }, + ], + description: + "Comment body with markdown, HTML, and plain text renderings", + }, + created_at: { + type: "string", + format: "date-time", + description: "When the comment was created", + }, + updated_at: { + type: "string", + format: "date-time", + description: "When the comment was last updated", + }, + user: { + $ref: "#/$defs/EmbeddedUser", + }, + _links: { + allOf: [ + { + $ref: "#/$defs/Links", + }, + ], + description: "HATEOAS links for navigation", + }, + }, + required: ["id", "type", "created_at", "updated_at", "user", "_links"], + }, + Connection: { + description: + "Full connection resource with abilities and links.\nUsed for GET /v3/connections/:id\n", + allOf: [ + { + $ref: "#/$defs/EmbeddedConnection", + }, + { + type: "object", + properties: { + can: { + $ref: "#/$defs/ConnectionAbilities", + }, + _links: { + $ref: "#/$defs/Links", + }, + }, + required: ["can", "_links"], + }, + ], + }, + ConnectionAbilities: { + type: "object", + description: "Actions the current user can perform on this connection", + properties: { + remove: { + type: "boolean", + description: "Whether the user can remove this connection", + }, + }, + required: ["remove"], + }, + ConnectableListResponse: { + description: "Paginated list of connectable content (blocks and channels)", + allOf: [ + { + $ref: "#/$defs/ConnectableList", + }, + { + $ref: "#/$defs/PaginatedResponse", + }, + ], + }, + ConnectableList: { + type: "object", + description: + "Data payload containing mixed content that can be connected to channels (blocks and channels)", + properties: { + data: { + type: "array", + description: "Array of blocks and channels", + items: { + oneOf: [ + { + $ref: "#/$defs/TextBlock", + }, + { + $ref: "#/$defs/ImageBlock", + }, + { + $ref: "#/$defs/LinkBlock", + }, + { + $ref: "#/$defs/AttachmentBlock", + }, + { + $ref: "#/$defs/EmbedBlock", + }, + { + $ref: "#/$defs/Channel", + }, + ], + }, + }, + }, + required: ["data"], + }, + UserListResponse: { + description: "Paginated list of users with total count", + allOf: [ + { + $ref: "#/$defs/UserList", + }, + { + $ref: "#/$defs/PaginatedResponse", + }, + ], + }, + UserList: { + type: "object", + description: "Data payload containing an array of users", + properties: { + data: { + type: "array", + description: "Array of users", + items: { + $ref: "#/$defs/User", + }, + }, + }, + required: ["data"], + }, + User: { + description: "Full user representation", + allOf: [ + { + $ref: "#/$defs/EmbeddedUser", + }, + { + type: "object", + properties: { + created_at: { + type: "string", + format: "date-time", + description: "When the user was created", + }, + updated_at: { + type: "string", + format: "date-time", + description: "When the user was last updated", + }, + bio: { + oneOf: [ + { + $ref: "#/$defs/MarkdownContent", + }, + { + type: "null", + }, + ], + description: + "User biography with markdown, HTML, and plain text renderings", + }, + badge: { + oneOf: [ + { + $ref: "#/$defs/UserBadge", + }, + { + type: "null", + }, + ], + description: "Denotes plan or other distinction", + }, + tier: { + $ref: "#/$defs/UserTier", + description: "User's subscription tier", + }, + counts: { + $ref: "#/$defs/UserCounts", + }, + _links: { + $ref: "#/$defs/Links", + description: "HATEOAS links for navigation", + }, + }, + required: [ + "created_at", + "updated_at", + "badge", + "tier", + "counts", + "_links", + ], + }, + ], + }, + UserBadge: { + type: "string", + description: + "Denotes plan or other distinction:\n- `staff`: Are.na staff member\n- `investor`: Investor\n- `supporter`: Supporter subscriber\n- `premium`: Premium subscriber\n", + enum: ["staff", "investor", "supporter", "premium"], + }, + UserTier: { + type: "string", + description: + "User subscription tier:\n- `guest`: Unauthenticated user\n- `free`: Free account\n- `premium`: Premium subscriber\n- `supporter`: Supporter tier\n", + enum: ["guest", "free", "premium", "supporter"], + }, + UserCounts: { + type: "object", + description: "Counts of various items for the user", + properties: { + channels: { + type: "integer", + description: "Number of channels owned by the user", + }, + followers: { + type: "integer", + description: "Number of followers", + }, + following: { + type: "integer", + description: "Number of users being followed", + }, + }, + required: ["channels", "followers", "following"], + }, + Me: { + description: + "The authenticated user's own profile, returned by `GET /v3/me`.\nExtends `User` with the token holder's email address. This field is\nintentionally not present on `/v3/users/{id}` or on embedded `User`\nreferences nested in other resources.\n", + allOf: [ + { + $ref: "#/$defs/User", + }, + { + type: "object", + properties: { + counts: { + $ref: "#/$defs/MeCounts", + }, + email: { + type: "string", + format: "email", + description: "The authenticated user's email address.", + }, + }, + required: ["email"], + }, + ], + }, + MeCounts: { + description: + "Counts for the authenticated user, including private notification state.", + allOf: [ + { + $ref: "#/$defs/UserCounts", + }, + { + type: "object", + properties: { + notifications: { + type: "integer", + description: + "Number of unread notifications for the authenticated user", + }, + }, + required: ["notifications"], + }, + ], + }, + FeedListResponse: { + type: "object", + description: "Cursor-paginated feed response.", + properties: { + meta: { + $ref: "#/$defs/CursorMeta", + }, + data: { + type: "array", + items: { + $ref: "#/$defs/Activity", + }, + }, + }, + required: ["meta", "data"], + }, + CursorMeta: { + type: "object", + description: + 'Cursor pagination metadata for the newest-first feeds. `next_cursor`\npages toward older items (the "load more" direction); `prev_cursor`\npages back toward newer items.\n', + properties: { + limit: { + type: "integer", + description: "Maximum number of items requested.", + }, + next_cursor: { + description: + "Cursor for the next (older) page. Pass it back as the `next`\nparameter to continue paging toward older items. Null when no\nolder items remain.\n", + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + }, + prev_cursor: { + description: + "Cursor for the previous (newer) page. Pass it back as the `prev`\nparameter to page toward newer items. Null when this page is\nempty.\n", + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + }, + has_more: { + type: "boolean", + description: + "Whether more (older) items exist beyond this page, reachable via\n`next_cursor`.\n", + }, + }, + required: ["limit", "next_cursor", "prev_cursor", "has_more"], + }, + Activity: { + description: "A single feed activity.", + allOf: [ + { + $ref: "#/$defs/ActivityBase", + }, + { + type: "object", + properties: { + type: { + type: "string", + enum: ["Activity"], + }, + }, + required: ["type"], + }, + ], + }, + ActivityBase: { + type: "object", + description: "Common fields for feed activities and notifications.", + properties: { + id: { + type: "integer", + description: "Unique identifier for the underlying activity.", + }, + created_at: { + type: "string", + format: "date-time", + description: "When the activity occurred.", + }, + kind: { + $ref: "#/$defs/ActivityKind", + }, + actor: { + $ref: "#/$defs/ActivityActor", + }, + item: { + oneOf: [ + { + $ref: "#/$defs/ActivitySubject", + }, + { + type: "null", + }, + ], + }, + target: { + oneOf: [ + { + $ref: "#/$defs/ActivitySubject", + }, + { + type: "null", + }, + ], + }, + parent: { + oneOf: [ + { + $ref: "#/$defs/ActivitySubject", + }, + { + type: "null", + }, + ], + }, + }, + required: ["id", "created_at", "kind", "actor", "item", "target", "parent"], + }, + ActivityKind: { + type: "string", + description: + "The normalized activity kind. Encodes the actor's action and the item /\ntarget involved, so clients can render and switch on a single value\nrather than stitching together separate action and connector phrases.\n\nKind determines the expected subject types:\n- `followed_user`: item=User, target=null, parent=null\n- `followed_channel`: item=Channel, target=null, parent=null\n- `followed_group`: item=Group, target=null, parent=null\n- `added_block_to_channel`: item=Block (Text/Image/Link/Attachment/Embed), target=Channel, parent=null\n- `added_channel_to_channel`: item=Channel, target=Channel, parent=null\n- `created_channel`: item=Channel, target=null, parent=null\n- `collaborating_with_user_on_channel`: item=User, target=Channel, parent=null\n- `collaborating_with_group_on_channel`: item=Group, target=Channel, parent=null\n- `commented_on_block`: item=Comment, target=Block, parent=Channel\n- `mentioned_you`: item=Comment, target=User, parent=Block\n- `added_user_to_group`: item=User, target=Group, parent=null\n", + enum: [ + "followed_user", + "followed_channel", + "followed_group", + "added_block_to_channel", + "added_channel_to_channel", + "created_channel", + "collaborating_with_user_on_channel", + "collaborating_with_group_on_channel", + "commented_on_block", + "mentioned_you", + "added_user_to_group", + ], + }, + ActivityActor: { + oneOf: [ + { + $ref: "#/$defs/EmbeddedUser", + }, + { + $ref: "#/$defs/EmbeddedGroup", + }, + ], + description: "User or group who performed the activity.", + }, + ActivitySubject: { + oneOf: [ + { + $ref: "#/$defs/EmbeddedUser", + }, + { + $ref: "#/$defs/EmbeddedGroup", + }, + { + $ref: "#/$defs/Channel", + }, + { + $ref: "#/$defs/TextBlock", + }, + { + $ref: "#/$defs/ImageBlock", + }, + { + $ref: "#/$defs/LinkBlock", + }, + { + $ref: "#/$defs/AttachmentBlock", + }, + { + $ref: "#/$defs/EmbedBlock", + }, + { + $ref: "#/$defs/Comment", + }, + ], + description: "Resource involved in a feed activity.", + }, + NotificationListResponse: { + type: "object", + description: "Cursor-paginated notification feed response.", + properties: { + meta: { + $ref: "#/$defs/CursorMeta", + }, + data: { + type: "array", + items: { + $ref: "#/$defs/Notification", + }, + }, + }, + required: ["meta", "data"], + }, + Notification: { + description: "A notification feed activity with read state.", + allOf: [ + { + $ref: "#/$defs/ActivityBase", + }, + { + type: "object", + properties: { + type: { + type: "string", + enum: ["Notification"], + }, + is_read: { + type: "boolean", + description: + "Whether the authenticated user has read this notification.", + }, + }, + required: ["type", "is_read"], + }, + ], + }, + NotificationsReadAllResponse: { + type: "object", + description: "Response to marking all notifications as read.", + properties: { + meta: { + $ref: "#/$defs/NotificationReadMeta", + }, + }, + required: ["meta"], + }, + NotificationReadMeta: { + type: "object", + description: + "The authenticated user's unread notification state after a mutation.", + properties: { + notifications: { + type: "integer", + description: "Number of unread notifications remaining.", + }, + }, + required: ["notifications"], + }, + NotificationReadResponse: { + type: "object", + description: + "Response to marking a single notification as read. Returns the updated\nnotification alongside the new unread count, so clients can refresh\nboth list state and badge without a follow-up request.\n", + properties: { + meta: { + $ref: "#/$defs/NotificationReadMeta", + }, + data: { + $ref: "#/$defs/Notification", + }, + }, + required: ["meta", "data"], + }, + FollowableListResponse: { + description: + "Paginated list of followable items (users, channels, and groups) with total count", + allOf: [ + { + $ref: "#/$defs/FollowableList", + }, + { + $ref: "#/$defs/PaginatedResponse", + }, + ], + }, + FollowableList: { + type: "object", + description: + "Data payload containing followable items (users, channels, and groups)", + properties: { + data: { + type: "array", + description: "Array of users, channels, and/or groups", + items: { + oneOf: [ + { + $ref: "#/$defs/User", + }, + { + $ref: "#/$defs/Channel", + }, + { + $ref: "#/$defs/Group", + }, + ], + }, + }, + }, + required: ["data"], + }, + Group: { + description: "Full group representation", + allOf: [ + { + $ref: "#/$defs/EmbeddedGroup", + }, + { + type: "object", + properties: { + description: { + oneOf: [ + { + $ref: "#/$defs/MarkdownContent", + }, + { + type: "null", + }, + ], + description: + "Group description with markdown, HTML, and plain text renderings", + }, + default_role: { + $ref: "#/$defs/GroupMembershipRole", + description: "Role assigned to new members joining the group", + }, + created_at: { + type: "string", + format: "date-time", + description: "When the group was created", + }, + updated_at: { + type: "string", + format: "date-time", + description: "When the group was last updated", + }, + user: { + $ref: "#/$defs/EmbeddedUser", + description: "User who owns/created the group", + }, + counts: { + $ref: "#/$defs/GroupCounts", + }, + can: { + $ref: "#/$defs/GroupAbilities", + description: "Actions the current user can perform on the group", + }, + _links: { + $ref: "#/$defs/Links", + description: "HATEOAS links for navigation", + }, + }, + required: [ + "default_role", + "created_at", + "updated_at", + "user", + "counts", + "can", + "_links", + ], + }, + ], + }, + GroupMembershipRole: { + type: "string", + description: + "The role a member has within a group. `viewer` can see the group's\nchannels (including private ones) but not add to or edit them;\n`member` has full write access to the group's channels and can invite\nnew members; `admin` can additionally manage the group and its members.\n", + enum: ["viewer", "member", "admin"], + }, + GroupCounts: { + type: "object", + description: "Counts of various items for the group", + properties: { + channels: { + type: "integer", + description: "Number of channels owned by the group", + }, + users: { + type: "integer", + description: "Number of users in the group", + }, + }, + required: ["channels", "users"], + }, + GroupAbilities: { + type: "object", + description: "Actions the current user can perform on the group", + properties: { + update: { + type: "boolean", + description: "Whether the user can update this group", + }, + destroy: { + type: "boolean", + description: "Whether the user can delete this group", + }, + manage_members: { + type: "boolean", + description: "Whether the user can add/remove group members", + }, + manage_invitations: { + type: "boolean", + description: "Whether the user can create/revoke group invitations", + }, + }, + required: ["update", "destroy", "manage_members", "manage_invitations"], + }, + GroupListResponse: { + description: "Paginated list of groups with total count", + allOf: [ + { + $ref: "#/$defs/GroupList", + }, + { + $ref: "#/$defs/PaginatedResponse", + }, + ], + }, + GroupList: { + type: "object", + description: "Data payload containing an array of groups", + properties: { + data: { + type: "array", + description: "Array of groups", + items: { + $ref: "#/$defs/Group", + }, + }, + }, + required: ["data"], + }, + GroupMemberListResponse: { + description: "Paginated list of group members with total count", + allOf: [ + { + $ref: "#/$defs/GroupMemberList", + }, + { + $ref: "#/$defs/PaginatedResponse", + }, + ], + }, + GroupMemberList: { + type: "object", + description: "Data payload containing an array of group members", + properties: { + data: { + type: "array", + description: "Array of group members (owner first on page 1)", + items: { + $ref: "#/$defs/GroupMember", + }, + }, + }, + required: ["data"], + }, + GroupMember: { + description: + "Group member entry, including the group owner and any collaborators", + allOf: [ + { + $ref: "#/$defs/EmbeddedUser", + }, + { + type: "object", + properties: { + role: { + type: "string", + enum: ["owner", "viewer", "member", "admin"], + description: + "The user's role on the group. `owner` is the group's creator;\neveryone else with a group membership has a `viewer`, `member`,\nor `admin` role.\n", + }, + }, + required: ["role"], + }, + ], + }, + MembershipInvitationListResponse: { + description: "Paginated list of pending membership invitations", + allOf: [ + { + $ref: "#/$defs/MembershipInvitationList", + }, + { + $ref: "#/$defs/PaginatedResponse", + }, + ], + }, + MembershipInvitationList: { + type: "object", + description: "Data payload containing an array of membership invitations", + properties: { + data: { + type: "array", + description: "Array of pending membership invitations", + items: { + $ref: "#/$defs/MembershipInvitation", + }, + }, + }, + required: ["data"], + }, + MembershipInvitation: { + type: "object", + description: + "Pending invitation for a user to join a group.\n\nInvitee acceptance and decline currently happen through the Are.na web\nUI using tokenized invitation links sent by email. The web UI uses the\nGraphQL `acceptMembershipInvitation` and `declineMembershipInvitation`\nmutations; this REST surface only exposes group-manager list and revoke\noperations.\n", + properties: { + id: { + type: "integer", + description: "Unique identifier for the invitation", + }, + type: { + type: "string", + enum: ["MembershipInvitation"], + }, + target: { + $ref: "#/$defs/EmbeddedGroup", + description: "Group the invitee is invited to join", + }, + invitee: { + oneOf: [ + { + $ref: "#/$defs/EmbeddedUser", + }, + { + type: "null", + }, + ], + description: "User being invited, when available", + }, + invitee_email: { + format: "email", + description: + "Email address being invited, visible to users who can manage invitations", + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + }, + invited_by: { + $ref: "#/$defs/EmbeddedUser", + description: "User who created the invitation", + }, + state: { + type: "string", + enum: ["pending", "accepted", "declined", "revoked"], + }, + accepted_at: { + format: "date-time", + description: "When the invitation was accepted", + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + }, + created_at: { + type: "string", + format: "date-time", + description: "When the invitation was created", + }, + updated_at: { + type: "string", + format: "date-time", + description: "When the invitation was last updated", + }, + _links: { + type: "object", + description: + "Links for navigation. Empty today because no public endpoint\naddresses an invitation directly; reserved for future use.\n", + additionalProperties: true, + }, + }, + required: [ + "id", + "type", + "target", + "invitee", + "invitee_email", + "invited_by", + "state", + "accepted_at", + "created_at", + "updated_at", + "_links", + ], + }, + GroupMemberInviteResponse: { + type: "object", + description: + "Result of `POST /v3/groups/{id}/invitations`. Inspect `outcome` to\ndetermine what happened; HTTP status mirrors it (`201` for created outcomes,\n`200` for no-op outcomes).\n", + properties: { + outcome: { + $ref: "#/$defs/GroupMemberInviteOutcome", + }, + user: { + oneOf: [ + { + $ref: "#/$defs/EmbeddedUser", + }, + { + type: "null", + }, + ], + description: + "The user that this request resolved to, when known. Null only when\nan invitation was issued by email and no matching user exists yet.\n", + }, + invitation: { + oneOf: [ + { + $ref: "#/$defs/MembershipInvitation", + }, + { + type: "null", + }, + ], + description: + "The invitation associated with this request, for `invited` and\n`invitation_pending` outcomes.\n", + }, + }, + required: ["outcome", "user", "invitation"], + }, + GroupMemberInviteOutcome: { + type: "string", + description: + "Discriminator for the result of `POST /v3/groups/{id}/invitations`.\n\n- `added`: A new group membership was created (the invitee already\n follows the caller, so the invite/accept round trip was skipped).\n- `invited`: A new pending invitation was created and an email was sent.\n- `invitation_pending`: An open invitation for this user/email already\n existed; that invitation is returned unchanged.\n- `already_member`: The user is already a group member; no action was\n taken.\n", + enum: ["added", "invited", "invitation_pending", "already_member"], + }, + GroupInvite: { + type: "object", + description: "Shareable invite code for joining a group", + properties: { + id: { + type: "integer", + description: "Unique identifier for the invite code", + }, + type: { + type: "string", + enum: ["GroupInvite"], + }, + code: { + type: "string", + description: "Invite code required to join the group", + }, + url: { + type: "string", + format: "uri", + description: "Public Are.na URL for accepting the group invite", + }, + created_at: { + type: "string", + format: "date-time", + description: "When the invite code was created", + }, + updated_at: { + type: "string", + format: "date-time", + description: "When the invite code was last updated", + }, + _links: { + $ref: "#/$defs/Links", + description: "HATEOAS links for navigation", + }, + }, + required: [ + "id", + "type", + "code", + "url", + "created_at", + "updated_at", + "_links", + ], + }, + EverythingListResponse: { + description: "Paginated list of all content types with total count", + allOf: [ + { + $ref: "#/$defs/EverythingList", + }, + { + $ref: "#/$defs/PaginatedResponse", + }, + ], + }, + EverythingList: { + type: "object", + description: "Data payload containing all content types", + properties: { + data: { + type: "array", + description: "Array of results (blocks, channels, users, or groups)", + items: { + oneOf: [ + { + $ref: "#/$defs/TextBlock", + }, + { + $ref: "#/$defs/ImageBlock", + }, + { + $ref: "#/$defs/LinkBlock", + }, + { + $ref: "#/$defs/AttachmentBlock", + }, + { + $ref: "#/$defs/EmbedBlock", + }, + { + $ref: "#/$defs/Channel", + }, + { + $ref: "#/$defs/User", + }, + { + $ref: "#/$defs/Group", + }, + ], + }, + }, + }, + required: ["data"], + }, +}; +export const outputSchemas = { + presignUpload: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { result: { $ref: "#/$defs/PresignResponse" } }, + required: ["result"], + additionalProperties: false, + $defs: { + PresignResponse: definitions["PresignResponse"], + PresignedFile: definitions["PresignedFile"], + }, + }, + createBlock: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { result: { $ref: "#/$defs/Block" } }, + required: ["result"], + additionalProperties: false, + $defs: { + AttachmentBlock: definitions["AttachmentBlock"], + BaseBlockProperties: definitions["BaseBlockProperties"], + Block: definitions["Block"], + BlockAbilities: definitions["BlockAbilities"], + BlockAttachment: definitions["BlockAttachment"], + BlockEmbed: definitions["BlockEmbed"], + BlockImage: definitions["BlockImage"], + BlockProvider: definitions["BlockProvider"], + BlockSource: definitions["BlockSource"], + BlockState: definitions["BlockState"], + BlockVisibility: definitions["BlockVisibility"], + EmbedBlock: definitions["EmbedBlock"], + EmbeddedConnection: definitions["EmbeddedConnection"], + EmbeddedUser: definitions["EmbeddedUser"], + ImageBlock: definitions["ImageBlock"], + ImageVersion: definitions["ImageVersion"], + Link: definitions["Link"], + LinkBlock: definitions["LinkBlock"], + Links: definitions["Links"], + MarkdownContent: definitions["MarkdownContent"], + Metadata: definitions["Metadata"], + PendingBlock: definitions["PendingBlock"], + TextBlock: definitions["TextBlock"], + }, + }, + batchCreateBlocks: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { result: { $ref: "#/$defs/BatchResponse" } }, + required: ["result"], + additionalProperties: false, + $defs: { BatchResponse: definitions["BatchResponse"] }, + }, + getBatchStatus: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { result: { $ref: "#/$defs/BatchStatus" } }, + required: ["result"], + additionalProperties: false, + $defs: { BatchStatus: definitions["BatchStatus"] }, + }, + getBlock: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { result: { $ref: "#/$defs/Block" } }, + required: ["result"], + additionalProperties: false, + $defs: { + AttachmentBlock: definitions["AttachmentBlock"], + BaseBlockProperties: definitions["BaseBlockProperties"], + Block: definitions["Block"], + BlockAbilities: definitions["BlockAbilities"], + BlockAttachment: definitions["BlockAttachment"], + BlockEmbed: definitions["BlockEmbed"], + BlockImage: definitions["BlockImage"], + BlockProvider: definitions["BlockProvider"], + BlockSource: definitions["BlockSource"], + BlockState: definitions["BlockState"], + BlockVisibility: definitions["BlockVisibility"], + EmbedBlock: definitions["EmbedBlock"], + EmbeddedConnection: definitions["EmbeddedConnection"], + EmbeddedUser: definitions["EmbeddedUser"], + ImageBlock: definitions["ImageBlock"], + ImageVersion: definitions["ImageVersion"], + Link: definitions["Link"], + LinkBlock: definitions["LinkBlock"], + Links: definitions["Links"], + MarkdownContent: definitions["MarkdownContent"], + Metadata: definitions["Metadata"], + PendingBlock: definitions["PendingBlock"], + TextBlock: definitions["TextBlock"], + }, + }, + updateBlock: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { result: { $ref: "#/$defs/Block" } }, + required: ["result"], + additionalProperties: false, + $defs: { + AttachmentBlock: definitions["AttachmentBlock"], + BaseBlockProperties: definitions["BaseBlockProperties"], + Block: definitions["Block"], + BlockAbilities: definitions["BlockAbilities"], + BlockAttachment: definitions["BlockAttachment"], + BlockEmbed: definitions["BlockEmbed"], + BlockImage: definitions["BlockImage"], + BlockProvider: definitions["BlockProvider"], + BlockSource: definitions["BlockSource"], + BlockState: definitions["BlockState"], + BlockVisibility: definitions["BlockVisibility"], + EmbedBlock: definitions["EmbedBlock"], + EmbeddedConnection: definitions["EmbeddedConnection"], + EmbeddedUser: definitions["EmbeddedUser"], + ImageBlock: definitions["ImageBlock"], + ImageVersion: definitions["ImageVersion"], + Link: definitions["Link"], + LinkBlock: definitions["LinkBlock"], + Links: definitions["Links"], + MarkdownContent: definitions["MarkdownContent"], + Metadata: definitions["Metadata"], + PendingBlock: definitions["PendingBlock"], + TextBlock: definitions["TextBlock"], + }, + }, + getBlockConnections: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { result: { $ref: "#/$defs/ChannelListResponse" } }, + required: ["result"], + additionalProperties: false, + $defs: { + Channel: definitions["Channel"], + ChannelAbilities: definitions["ChannelAbilities"], + ChannelCollaborator: definitions["ChannelCollaborator"], + ChannelCounts: definitions["ChannelCounts"], + ChannelList: definitions["ChannelList"], + ChannelListResponse: definitions["ChannelListResponse"], + ChannelOwner: definitions["ChannelOwner"], + ChannelState: definitions["ChannelState"], + ChannelVisibility: definitions["ChannelVisibility"], + EmbeddedConnection: definitions["EmbeddedConnection"], + EmbeddedGroup: definitions["EmbeddedGroup"], + EmbeddedUser: definitions["EmbeddedUser"], + Link: definitions["Link"], + Links: definitions["Links"], + MarkdownContent: definitions["MarkdownContent"], + Metadata: definitions["Metadata"], + PaginatedResponse: definitions["PaginatedResponse"], + PaginationMeta: definitions["PaginationMeta"], + }, + }, + getBlockComments: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { result: { $ref: "#/$defs/CommentListResponse" } }, + required: ["result"], + additionalProperties: false, + $defs: { + Comment: definitions["Comment"], + CommentList: definitions["CommentList"], + CommentListResponse: definitions["CommentListResponse"], + EmbeddedUser: definitions["EmbeddedUser"], + Link: definitions["Link"], + Links: definitions["Links"], + MarkdownContent: definitions["MarkdownContent"], + PaginatedResponse: definitions["PaginatedResponse"], + PaginationMeta: definitions["PaginationMeta"], + }, + }, + createBlockComment: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { result: { $ref: "#/$defs/Comment" } }, + required: ["result"], + additionalProperties: false, + $defs: { + Comment: definitions["Comment"], + EmbeddedUser: definitions["EmbeddedUser"], + Link: definitions["Link"], + Links: definitions["Links"], + MarkdownContent: definitions["MarkdownContent"], + }, + }, + deleteComment: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { result: { type: "null" } }, + required: ["result"], + additionalProperties: false, + $defs: {}, + }, + createChannel: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { result: { $ref: "#/$defs/Channel" } }, + required: ["result"], + additionalProperties: false, + $defs: { + Channel: definitions["Channel"], + ChannelAbilities: definitions["ChannelAbilities"], + ChannelCollaborator: definitions["ChannelCollaborator"], + ChannelCounts: definitions["ChannelCounts"], + ChannelOwner: definitions["ChannelOwner"], + ChannelState: definitions["ChannelState"], + ChannelVisibility: definitions["ChannelVisibility"], + EmbeddedConnection: definitions["EmbeddedConnection"], + EmbeddedGroup: definitions["EmbeddedGroup"], + EmbeddedUser: definitions["EmbeddedUser"], + Link: definitions["Link"], + Links: definitions["Links"], + MarkdownContent: definitions["MarkdownContent"], + Metadata: definitions["Metadata"], + }, + }, + getChannel: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { result: { $ref: "#/$defs/Channel" } }, + required: ["result"], + additionalProperties: false, + $defs: { + Channel: definitions["Channel"], + ChannelAbilities: definitions["ChannelAbilities"], + ChannelCollaborator: definitions["ChannelCollaborator"], + ChannelCounts: definitions["ChannelCounts"], + ChannelOwner: definitions["ChannelOwner"], + ChannelState: definitions["ChannelState"], + ChannelVisibility: definitions["ChannelVisibility"], + EmbeddedConnection: definitions["EmbeddedConnection"], + EmbeddedGroup: definitions["EmbeddedGroup"], + EmbeddedUser: definitions["EmbeddedUser"], + Link: definitions["Link"], + Links: definitions["Links"], + MarkdownContent: definitions["MarkdownContent"], + Metadata: definitions["Metadata"], + }, + }, + updateChannel: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { result: { $ref: "#/$defs/Channel" } }, + required: ["result"], + additionalProperties: false, + $defs: { + Channel: definitions["Channel"], + ChannelAbilities: definitions["ChannelAbilities"], + ChannelCollaborator: definitions["ChannelCollaborator"], + ChannelCounts: definitions["ChannelCounts"], + ChannelOwner: definitions["ChannelOwner"], + ChannelState: definitions["ChannelState"], + ChannelVisibility: definitions["ChannelVisibility"], + EmbeddedConnection: definitions["EmbeddedConnection"], + EmbeddedGroup: definitions["EmbeddedGroup"], + EmbeddedUser: definitions["EmbeddedUser"], + Link: definitions["Link"], + Links: definitions["Links"], + MarkdownContent: definitions["MarkdownContent"], + Metadata: definitions["Metadata"], + }, + }, + deleteChannel: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { result: { type: "null" } }, + required: ["result"], + additionalProperties: false, + $defs: {}, + }, + createConnection: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { + result: { + type: "object", + properties: { + data: { type: "array", items: { $ref: "#/$defs/Connection" } }, + }, + }, + }, + required: ["result"], + additionalProperties: false, + $defs: { + Connection: definitions["Connection"], + ConnectionAbilities: definitions["ConnectionAbilities"], + EmbeddedConnection: definitions["EmbeddedConnection"], + EmbeddedUser: definitions["EmbeddedUser"], + Link: definitions["Link"], + Links: definitions["Links"], + Metadata: definitions["Metadata"], + }, + }, + getConnection: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { result: { $ref: "#/$defs/Connection" } }, + required: ["result"], + additionalProperties: false, + $defs: { + Connection: definitions["Connection"], + ConnectionAbilities: definitions["ConnectionAbilities"], + EmbeddedConnection: definitions["EmbeddedConnection"], + EmbeddedUser: definitions["EmbeddedUser"], + Link: definitions["Link"], + Links: definitions["Links"], + Metadata: definitions["Metadata"], + }, + }, + updateConnection: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { result: { $ref: "#/$defs/Connection" } }, + required: ["result"], + additionalProperties: false, + $defs: { + Connection: definitions["Connection"], + ConnectionAbilities: definitions["ConnectionAbilities"], + EmbeddedConnection: definitions["EmbeddedConnection"], + EmbeddedUser: definitions["EmbeddedUser"], + Link: definitions["Link"], + Links: definitions["Links"], + Metadata: definitions["Metadata"], + }, + }, + deleteConnection: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { result: { type: "null" } }, + required: ["result"], + additionalProperties: false, + $defs: {}, + }, + moveConnection: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { result: { $ref: "#/$defs/Connection" } }, + required: ["result"], + additionalProperties: false, + $defs: { + Connection: definitions["Connection"], + ConnectionAbilities: definitions["ConnectionAbilities"], + EmbeddedConnection: definitions["EmbeddedConnection"], + EmbeddedUser: definitions["EmbeddedUser"], + Link: definitions["Link"], + Links: definitions["Links"], + Metadata: definitions["Metadata"], + }, + }, + getChannelContents: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { result: { $ref: "#/$defs/ConnectableListResponse" } }, + required: ["result"], + additionalProperties: false, + $defs: { + AttachmentBlock: definitions["AttachmentBlock"], + BaseBlockProperties: definitions["BaseBlockProperties"], + BlockAbilities: definitions["BlockAbilities"], + BlockAttachment: definitions["BlockAttachment"], + BlockEmbed: definitions["BlockEmbed"], + BlockImage: definitions["BlockImage"], + BlockProvider: definitions["BlockProvider"], + BlockSource: definitions["BlockSource"], + BlockState: definitions["BlockState"], + BlockVisibility: definitions["BlockVisibility"], + Channel: definitions["Channel"], + ChannelAbilities: definitions["ChannelAbilities"], + ChannelCollaborator: definitions["ChannelCollaborator"], + ChannelCounts: definitions["ChannelCounts"], + ChannelOwner: definitions["ChannelOwner"], + ChannelState: definitions["ChannelState"], + ChannelVisibility: definitions["ChannelVisibility"], + ConnectableList: definitions["ConnectableList"], + ConnectableListResponse: definitions["ConnectableListResponse"], + EmbedBlock: definitions["EmbedBlock"], + EmbeddedConnection: definitions["EmbeddedConnection"], + EmbeddedGroup: definitions["EmbeddedGroup"], + EmbeddedUser: definitions["EmbeddedUser"], + ImageBlock: definitions["ImageBlock"], + ImageVersion: definitions["ImageVersion"], + Link: definitions["Link"], + LinkBlock: definitions["LinkBlock"], + Links: definitions["Links"], + MarkdownContent: definitions["MarkdownContent"], + Metadata: definitions["Metadata"], + PaginatedResponse: definitions["PaginatedResponse"], + PaginationMeta: definitions["PaginationMeta"], + TextBlock: definitions["TextBlock"], + }, + }, + getChannelConnections: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { result: { $ref: "#/$defs/ChannelListResponse" } }, + required: ["result"], + additionalProperties: false, + $defs: { + Channel: definitions["Channel"], + ChannelAbilities: definitions["ChannelAbilities"], + ChannelCollaborator: definitions["ChannelCollaborator"], + ChannelCounts: definitions["ChannelCounts"], + ChannelList: definitions["ChannelList"], + ChannelListResponse: definitions["ChannelListResponse"], + ChannelOwner: definitions["ChannelOwner"], + ChannelState: definitions["ChannelState"], + ChannelVisibility: definitions["ChannelVisibility"], + EmbeddedConnection: definitions["EmbeddedConnection"], + EmbeddedGroup: definitions["EmbeddedGroup"], + EmbeddedUser: definitions["EmbeddedUser"], + Link: definitions["Link"], + Links: definitions["Links"], + MarkdownContent: definitions["MarkdownContent"], + Metadata: definitions["Metadata"], + PaginatedResponse: definitions["PaginatedResponse"], + PaginationMeta: definitions["PaginationMeta"], + }, + }, + getChannelFollowers: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { result: { $ref: "#/$defs/UserListResponse" } }, + required: ["result"], + additionalProperties: false, + $defs: { + EmbeddedUser: definitions["EmbeddedUser"], + Link: definitions["Link"], + Links: definitions["Links"], + MarkdownContent: definitions["MarkdownContent"], + PaginatedResponse: definitions["PaginatedResponse"], + PaginationMeta: definitions["PaginationMeta"], + User: definitions["User"], + UserBadge: definitions["UserBadge"], + UserCounts: definitions["UserCounts"], + UserList: definitions["UserList"], + UserListResponse: definitions["UserListResponse"], + UserTier: definitions["UserTier"], + }, + }, + getCurrentUser: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { result: { $ref: "#/$defs/Me" } }, + required: ["result"], + additionalProperties: false, + $defs: { + EmbeddedUser: definitions["EmbeddedUser"], + Link: definitions["Link"], + Links: definitions["Links"], + MarkdownContent: definitions["MarkdownContent"], + Me: definitions["Me"], + MeCounts: definitions["MeCounts"], + User: definitions["User"], + UserBadge: definitions["UserBadge"], + UserCounts: definitions["UserCounts"], + UserTier: definitions["UserTier"], + }, + }, + getMyFeed: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { result: { $ref: "#/$defs/FeedListResponse" } }, + required: ["result"], + additionalProperties: false, + $defs: { + Activity: definitions["Activity"], + ActivityActor: definitions["ActivityActor"], + ActivityBase: definitions["ActivityBase"], + ActivityKind: definitions["ActivityKind"], + ActivitySubject: definitions["ActivitySubject"], + AttachmentBlock: definitions["AttachmentBlock"], + BaseBlockProperties: definitions["BaseBlockProperties"], + BlockAbilities: definitions["BlockAbilities"], + BlockAttachment: definitions["BlockAttachment"], + BlockEmbed: definitions["BlockEmbed"], + BlockImage: definitions["BlockImage"], + BlockProvider: definitions["BlockProvider"], + BlockSource: definitions["BlockSource"], + BlockState: definitions["BlockState"], + BlockVisibility: definitions["BlockVisibility"], + Channel: definitions["Channel"], + ChannelAbilities: definitions["ChannelAbilities"], + ChannelCollaborator: definitions["ChannelCollaborator"], + ChannelCounts: definitions["ChannelCounts"], + ChannelOwner: definitions["ChannelOwner"], + ChannelState: definitions["ChannelState"], + ChannelVisibility: definitions["ChannelVisibility"], + Comment: definitions["Comment"], + CursorMeta: definitions["CursorMeta"], + EmbedBlock: definitions["EmbedBlock"], + EmbeddedConnection: definitions["EmbeddedConnection"], + EmbeddedGroup: definitions["EmbeddedGroup"], + EmbeddedUser: definitions["EmbeddedUser"], + FeedListResponse: definitions["FeedListResponse"], + ImageBlock: definitions["ImageBlock"], + ImageVersion: definitions["ImageVersion"], + Link: definitions["Link"], + LinkBlock: definitions["LinkBlock"], + Links: definitions["Links"], + MarkdownContent: definitions["MarkdownContent"], + Metadata: definitions["Metadata"], + TextBlock: definitions["TextBlock"], + }, + }, + getMyNotifications: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { result: { $ref: "#/$defs/NotificationListResponse" } }, + required: ["result"], + additionalProperties: false, + $defs: { + ActivityActor: definitions["ActivityActor"], + ActivityBase: definitions["ActivityBase"], + ActivityKind: definitions["ActivityKind"], + ActivitySubject: definitions["ActivitySubject"], + AttachmentBlock: definitions["AttachmentBlock"], + BaseBlockProperties: definitions["BaseBlockProperties"], + BlockAbilities: definitions["BlockAbilities"], + BlockAttachment: definitions["BlockAttachment"], + BlockEmbed: definitions["BlockEmbed"], + BlockImage: definitions["BlockImage"], + BlockProvider: definitions["BlockProvider"], + BlockSource: definitions["BlockSource"], + BlockState: definitions["BlockState"], + BlockVisibility: definitions["BlockVisibility"], + Channel: definitions["Channel"], + ChannelAbilities: definitions["ChannelAbilities"], + ChannelCollaborator: definitions["ChannelCollaborator"], + ChannelCounts: definitions["ChannelCounts"], + ChannelOwner: definitions["ChannelOwner"], + ChannelState: definitions["ChannelState"], + ChannelVisibility: definitions["ChannelVisibility"], + Comment: definitions["Comment"], + CursorMeta: definitions["CursorMeta"], + EmbedBlock: definitions["EmbedBlock"], + EmbeddedConnection: definitions["EmbeddedConnection"], + EmbeddedGroup: definitions["EmbeddedGroup"], + EmbeddedUser: definitions["EmbeddedUser"], + ImageBlock: definitions["ImageBlock"], + ImageVersion: definitions["ImageVersion"], + Link: definitions["Link"], + LinkBlock: definitions["LinkBlock"], + Links: definitions["Links"], + MarkdownContent: definitions["MarkdownContent"], + Metadata: definitions["Metadata"], + Notification: definitions["Notification"], + NotificationListResponse: definitions["NotificationListResponse"], + TextBlock: definitions["TextBlock"], + }, + }, + markAllMyNotificationsRead: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { result: { $ref: "#/$defs/NotificationsReadAllResponse" } }, + required: ["result"], + additionalProperties: false, + $defs: { + NotificationReadMeta: definitions["NotificationReadMeta"], + NotificationsReadAllResponse: definitions["NotificationsReadAllResponse"], + }, + }, + markMyNotificationRead: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { result: { $ref: "#/$defs/NotificationReadResponse" } }, + required: ["result"], + additionalProperties: false, + $defs: { + ActivityActor: definitions["ActivityActor"], + ActivityBase: definitions["ActivityBase"], + ActivityKind: definitions["ActivityKind"], + ActivitySubject: definitions["ActivitySubject"], + AttachmentBlock: definitions["AttachmentBlock"], + BaseBlockProperties: definitions["BaseBlockProperties"], + BlockAbilities: definitions["BlockAbilities"], + BlockAttachment: definitions["BlockAttachment"], + BlockEmbed: definitions["BlockEmbed"], + BlockImage: definitions["BlockImage"], + BlockProvider: definitions["BlockProvider"], + BlockSource: definitions["BlockSource"], + BlockState: definitions["BlockState"], + BlockVisibility: definitions["BlockVisibility"], + Channel: definitions["Channel"], + ChannelAbilities: definitions["ChannelAbilities"], + ChannelCollaborator: definitions["ChannelCollaborator"], + ChannelCounts: definitions["ChannelCounts"], + ChannelOwner: definitions["ChannelOwner"], + ChannelState: definitions["ChannelState"], + ChannelVisibility: definitions["ChannelVisibility"], + Comment: definitions["Comment"], + EmbedBlock: definitions["EmbedBlock"], + EmbeddedConnection: definitions["EmbeddedConnection"], + EmbeddedGroup: definitions["EmbeddedGroup"], + EmbeddedUser: definitions["EmbeddedUser"], + ImageBlock: definitions["ImageBlock"], + ImageVersion: definitions["ImageVersion"], + Link: definitions["Link"], + LinkBlock: definitions["LinkBlock"], + Links: definitions["Links"], + MarkdownContent: definitions["MarkdownContent"], + Metadata: definitions["Metadata"], + Notification: definitions["Notification"], + NotificationReadMeta: definitions["NotificationReadMeta"], + NotificationReadResponse: definitions["NotificationReadResponse"], + TextBlock: definitions["TextBlock"], + }, + }, + getUser: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { result: { $ref: "#/$defs/User" } }, + required: ["result"], + additionalProperties: false, + $defs: { + EmbeddedUser: definitions["EmbeddedUser"], + Link: definitions["Link"], + Links: definitions["Links"], + MarkdownContent: definitions["MarkdownContent"], + User: definitions["User"], + UserBadge: definitions["UserBadge"], + UserCounts: definitions["UserCounts"], + UserTier: definitions["UserTier"], + }, + }, + getUserContents: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { result: { $ref: "#/$defs/ConnectableListResponse" } }, + required: ["result"], + additionalProperties: false, + $defs: { + AttachmentBlock: definitions["AttachmentBlock"], + BaseBlockProperties: definitions["BaseBlockProperties"], + BlockAbilities: definitions["BlockAbilities"], + BlockAttachment: definitions["BlockAttachment"], + BlockEmbed: definitions["BlockEmbed"], + BlockImage: definitions["BlockImage"], + BlockProvider: definitions["BlockProvider"], + BlockSource: definitions["BlockSource"], + BlockState: definitions["BlockState"], + BlockVisibility: definitions["BlockVisibility"], + Channel: definitions["Channel"], + ChannelAbilities: definitions["ChannelAbilities"], + ChannelCollaborator: definitions["ChannelCollaborator"], + ChannelCounts: definitions["ChannelCounts"], + ChannelOwner: definitions["ChannelOwner"], + ChannelState: definitions["ChannelState"], + ChannelVisibility: definitions["ChannelVisibility"], + ConnectableList: definitions["ConnectableList"], + ConnectableListResponse: definitions["ConnectableListResponse"], + EmbedBlock: definitions["EmbedBlock"], + EmbeddedConnection: definitions["EmbeddedConnection"], + EmbeddedGroup: definitions["EmbeddedGroup"], + EmbeddedUser: definitions["EmbeddedUser"], + ImageBlock: definitions["ImageBlock"], + ImageVersion: definitions["ImageVersion"], + Link: definitions["Link"], + LinkBlock: definitions["LinkBlock"], + Links: definitions["Links"], + MarkdownContent: definitions["MarkdownContent"], + Metadata: definitions["Metadata"], + PaginatedResponse: definitions["PaginatedResponse"], + PaginationMeta: definitions["PaginationMeta"], + TextBlock: definitions["TextBlock"], + }, + }, + getUserFollowers: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { result: { $ref: "#/$defs/UserListResponse" } }, + required: ["result"], + additionalProperties: false, + $defs: { + EmbeddedUser: definitions["EmbeddedUser"], + Link: definitions["Link"], + Links: definitions["Links"], + MarkdownContent: definitions["MarkdownContent"], + PaginatedResponse: definitions["PaginatedResponse"], + PaginationMeta: definitions["PaginationMeta"], + User: definitions["User"], + UserBadge: definitions["UserBadge"], + UserCounts: definitions["UserCounts"], + UserList: definitions["UserList"], + UserListResponse: definitions["UserListResponse"], + UserTier: definitions["UserTier"], + }, + }, + getUserFollowing: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { result: { $ref: "#/$defs/FollowableListResponse" } }, + required: ["result"], + additionalProperties: false, + $defs: { + Channel: definitions["Channel"], + ChannelAbilities: definitions["ChannelAbilities"], + ChannelCollaborator: definitions["ChannelCollaborator"], + ChannelCounts: definitions["ChannelCounts"], + ChannelOwner: definitions["ChannelOwner"], + ChannelState: definitions["ChannelState"], + ChannelVisibility: definitions["ChannelVisibility"], + EmbeddedConnection: definitions["EmbeddedConnection"], + EmbeddedGroup: definitions["EmbeddedGroup"], + EmbeddedUser: definitions["EmbeddedUser"], + FollowableList: definitions["FollowableList"], + FollowableListResponse: definitions["FollowableListResponse"], + Group: definitions["Group"], + GroupAbilities: definitions["GroupAbilities"], + GroupCounts: definitions["GroupCounts"], + GroupMembershipRole: definitions["GroupMembershipRole"], + Link: definitions["Link"], + Links: definitions["Links"], + MarkdownContent: definitions["MarkdownContent"], + Metadata: definitions["Metadata"], + PaginatedResponse: definitions["PaginatedResponse"], + PaginationMeta: definitions["PaginationMeta"], + User: definitions["User"], + UserBadge: definitions["UserBadge"], + UserCounts: definitions["UserCounts"], + UserTier: definitions["UserTier"], + }, + }, + getUserGroups: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { result: { $ref: "#/$defs/GroupListResponse" } }, + required: ["result"], + additionalProperties: false, + $defs: { + EmbeddedGroup: definitions["EmbeddedGroup"], + EmbeddedUser: definitions["EmbeddedUser"], + Group: definitions["Group"], + GroupAbilities: definitions["GroupAbilities"], + GroupCounts: definitions["GroupCounts"], + GroupList: definitions["GroupList"], + GroupListResponse: definitions["GroupListResponse"], + GroupMembershipRole: definitions["GroupMembershipRole"], + Link: definitions["Link"], + Links: definitions["Links"], + MarkdownContent: definitions["MarkdownContent"], + PaginatedResponse: definitions["PaginatedResponse"], + PaginationMeta: definitions["PaginationMeta"], + }, + }, + createGroup: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { result: { $ref: "#/$defs/Group" } }, + required: ["result"], + additionalProperties: false, + $defs: { + EmbeddedGroup: definitions["EmbeddedGroup"], + EmbeddedUser: definitions["EmbeddedUser"], + Group: definitions["Group"], + GroupAbilities: definitions["GroupAbilities"], + GroupCounts: definitions["GroupCounts"], + GroupMembershipRole: definitions["GroupMembershipRole"], + Link: definitions["Link"], + Links: definitions["Links"], + MarkdownContent: definitions["MarkdownContent"], + }, + }, + getGroup: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { result: { $ref: "#/$defs/Group" } }, + required: ["result"], + additionalProperties: false, + $defs: { + EmbeddedGroup: definitions["EmbeddedGroup"], + EmbeddedUser: definitions["EmbeddedUser"], + Group: definitions["Group"], + GroupAbilities: definitions["GroupAbilities"], + GroupCounts: definitions["GroupCounts"], + GroupMembershipRole: definitions["GroupMembershipRole"], + Link: definitions["Link"], + Links: definitions["Links"], + MarkdownContent: definitions["MarkdownContent"], + }, + }, + updateGroup: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { result: { $ref: "#/$defs/Group" } }, + required: ["result"], + additionalProperties: false, + $defs: { + EmbeddedGroup: definitions["EmbeddedGroup"], + EmbeddedUser: definitions["EmbeddedUser"], + Group: definitions["Group"], + GroupAbilities: definitions["GroupAbilities"], + GroupCounts: definitions["GroupCounts"], + GroupMembershipRole: definitions["GroupMembershipRole"], + Link: definitions["Link"], + Links: definitions["Links"], + MarkdownContent: definitions["MarkdownContent"], + }, + }, + deleteGroup: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { result: { type: "null" } }, + required: ["result"], + additionalProperties: false, + $defs: {}, + }, + getGroupMembers: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { result: { $ref: "#/$defs/GroupMemberListResponse" } }, + required: ["result"], + additionalProperties: false, + $defs: { + EmbeddedUser: definitions["EmbeddedUser"], + GroupMember: definitions["GroupMember"], + GroupMemberList: definitions["GroupMemberList"], + GroupMemberListResponse: definitions["GroupMemberListResponse"], + PaginatedResponse: definitions["PaginatedResponse"], + PaginationMeta: definitions["PaginationMeta"], + }, + }, + joinGroup: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { result: { $ref: "#/$defs/GroupMember" } }, + required: ["result"], + additionalProperties: false, + $defs: { + EmbeddedUser: definitions["EmbeddedUser"], + GroupMember: definitions["GroupMember"], + }, + }, + leaveGroup: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { result: { type: "null" } }, + required: ["result"], + additionalProperties: false, + $defs: {}, + }, + removeGroupMember: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { result: { type: "null" } }, + required: ["result"], + additionalProperties: false, + $defs: {}, + }, + updateGroupMember: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { result: { $ref: "#/$defs/GroupMember" } }, + required: ["result"], + additionalProperties: false, + $defs: { + EmbeddedUser: definitions["EmbeddedUser"], + GroupMember: definitions["GroupMember"], + }, + }, + getGroupInvitations: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { + result: { $ref: "#/$defs/MembershipInvitationListResponse" }, + }, + required: ["result"], + additionalProperties: false, + $defs: { + EmbeddedGroup: definitions["EmbeddedGroup"], + EmbeddedUser: definitions["EmbeddedUser"], + MembershipInvitation: definitions["MembershipInvitation"], + MembershipInvitationList: definitions["MembershipInvitationList"], + MembershipInvitationListResponse: + definitions["MembershipInvitationListResponse"], + PaginatedResponse: definitions["PaginatedResponse"], + PaginationMeta: definitions["PaginationMeta"], + }, + }, + createGroupInvitation: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { result: { $ref: "#/$defs/GroupMemberInviteResponse" } }, + required: ["result"], + additionalProperties: false, + $defs: { + EmbeddedGroup: definitions["EmbeddedGroup"], + EmbeddedUser: definitions["EmbeddedUser"], + GroupMemberInviteOutcome: definitions["GroupMemberInviteOutcome"], + GroupMemberInviteResponse: definitions["GroupMemberInviteResponse"], + MembershipInvitation: definitions["MembershipInvitation"], + }, + }, + revokeGroupInvitation: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { result: { type: "null" } }, + required: ["result"], + additionalProperties: false, + $defs: {}, + }, + getGroupInvite: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { result: { $ref: "#/$defs/GroupInvite" } }, + required: ["result"], + additionalProperties: false, + $defs: { + GroupInvite: definitions["GroupInvite"], + Link: definitions["Link"], + Links: definitions["Links"], + }, + }, + createGroupInvite: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { result: { $ref: "#/$defs/GroupInvite" } }, + required: ["result"], + additionalProperties: false, + $defs: { + GroupInvite: definitions["GroupInvite"], + Link: definitions["Link"], + Links: definitions["Links"], + }, + }, + deleteGroupInvite: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { result: { type: "null" } }, + required: ["result"], + additionalProperties: false, + $defs: {}, + }, + getGroupContents: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { result: { $ref: "#/$defs/ConnectableListResponse" } }, + required: ["result"], + additionalProperties: false, + $defs: { + AttachmentBlock: definitions["AttachmentBlock"], + BaseBlockProperties: definitions["BaseBlockProperties"], + BlockAbilities: definitions["BlockAbilities"], + BlockAttachment: definitions["BlockAttachment"], + BlockEmbed: definitions["BlockEmbed"], + BlockImage: definitions["BlockImage"], + BlockProvider: definitions["BlockProvider"], + BlockSource: definitions["BlockSource"], + BlockState: definitions["BlockState"], + BlockVisibility: definitions["BlockVisibility"], + Channel: definitions["Channel"], + ChannelAbilities: definitions["ChannelAbilities"], + ChannelCollaborator: definitions["ChannelCollaborator"], + ChannelCounts: definitions["ChannelCounts"], + ChannelOwner: definitions["ChannelOwner"], + ChannelState: definitions["ChannelState"], + ChannelVisibility: definitions["ChannelVisibility"], + ConnectableList: definitions["ConnectableList"], + ConnectableListResponse: definitions["ConnectableListResponse"], + EmbedBlock: definitions["EmbedBlock"], + EmbeddedConnection: definitions["EmbeddedConnection"], + EmbeddedGroup: definitions["EmbeddedGroup"], + EmbeddedUser: definitions["EmbeddedUser"], + ImageBlock: definitions["ImageBlock"], + ImageVersion: definitions["ImageVersion"], + Link: definitions["Link"], + LinkBlock: definitions["LinkBlock"], + Links: definitions["Links"], + MarkdownContent: definitions["MarkdownContent"], + Metadata: definitions["Metadata"], + PaginatedResponse: definitions["PaginatedResponse"], + PaginationMeta: definitions["PaginationMeta"], + TextBlock: definitions["TextBlock"], + }, + }, + getGroupFollowers: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { result: { $ref: "#/$defs/UserListResponse" } }, + required: ["result"], + additionalProperties: false, + $defs: { + EmbeddedUser: definitions["EmbeddedUser"], + Link: definitions["Link"], + Links: definitions["Links"], + MarkdownContent: definitions["MarkdownContent"], + PaginatedResponse: definitions["PaginatedResponse"], + PaginationMeta: definitions["PaginationMeta"], + User: definitions["User"], + UserBadge: definitions["UserBadge"], + UserCounts: definitions["UserCounts"], + UserList: definitions["UserList"], + UserListResponse: definitions["UserListResponse"], + UserTier: definitions["UserTier"], + }, + }, + search: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { result: { $ref: "#/$defs/EverythingListResponse" } }, + required: ["result"], + additionalProperties: false, + $defs: { + AttachmentBlock: definitions["AttachmentBlock"], + BaseBlockProperties: definitions["BaseBlockProperties"], + BlockAbilities: definitions["BlockAbilities"], + BlockAttachment: definitions["BlockAttachment"], + BlockEmbed: definitions["BlockEmbed"], + BlockImage: definitions["BlockImage"], + BlockProvider: definitions["BlockProvider"], + BlockSource: definitions["BlockSource"], + BlockState: definitions["BlockState"], + BlockVisibility: definitions["BlockVisibility"], + Channel: definitions["Channel"], + ChannelAbilities: definitions["ChannelAbilities"], + ChannelCollaborator: definitions["ChannelCollaborator"], + ChannelCounts: definitions["ChannelCounts"], + ChannelOwner: definitions["ChannelOwner"], + ChannelState: definitions["ChannelState"], + ChannelVisibility: definitions["ChannelVisibility"], + EmbedBlock: definitions["EmbedBlock"], + EmbeddedConnection: definitions["EmbeddedConnection"], + EmbeddedGroup: definitions["EmbeddedGroup"], + EmbeddedUser: definitions["EmbeddedUser"], + EverythingList: definitions["EverythingList"], + EverythingListResponse: definitions["EverythingListResponse"], + Group: definitions["Group"], + GroupAbilities: definitions["GroupAbilities"], + GroupCounts: definitions["GroupCounts"], + GroupMembershipRole: definitions["GroupMembershipRole"], + ImageBlock: definitions["ImageBlock"], + ImageVersion: definitions["ImageVersion"], + Link: definitions["Link"], + LinkBlock: definitions["LinkBlock"], + Links: definitions["Links"], + MarkdownContent: definitions["MarkdownContent"], + Metadata: definitions["Metadata"], + PaginatedResponse: definitions["PaginatedResponse"], + PaginationMeta: definitions["PaginationMeta"], + TextBlock: definitions["TextBlock"], + User: definitions["User"], + UserBadge: definitions["UserBadge"], + UserCounts: definitions["UserCounts"], + UserTier: definitions["UserTier"], + }, + }, +} satisfies Record; diff --git a/src/generated/tools.ts b/src/generated/tools.ts index 3017427..78061db 100644 --- a/src/generated/tools.ts +++ b/src/generated/tools.ts @@ -1,9 +1,14 @@ // Auto-generated by scripts/generate-tools.ts — do not edit manually import { z } from "zod"; -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { withArenaClient, textResult, errorResult } from "../lib/tool-helpers"; +import type { ToolRegistry } from "../lib/tool-registry"; +import { + withArenaClient, + successResult, + errorResult, +} from "../lib/tool-helpers"; +import { outputSchemas } from "./output-schemas"; -export function registerGeneratedTools(server: McpServer): void { +export function registerGeneratedTools(server: ToolRegistry): void { server.registerTool( "presignUpload", { @@ -21,6 +26,7 @@ export function registerGeneratedTools(server: McpServer): void { .max(50) .describe("Array of files to generate presigned URLs for"), }, + outputSchema: outputSchemas["presignUpload"], }, async (args: Record, extra) => { return withArenaClient(extra, async (client) => { @@ -28,7 +34,7 @@ export function registerGeneratedTools(server: McpServer): void { body: { files: args["files"] }, } as any); if (error) return errorResult(error); - return textResult(data); + return successResult(data); }).catch((err) => errorResult(err.message)); }, ); @@ -111,6 +117,7 @@ export function registerGeneratedTools(server: McpServer): void { "Target channels with optional per-channel position and connection metadata.", ), }, + outputSchema: outputSchemas["createBlock"], }, async (args: Record, extra) => { return withArenaClient(extra, async (client) => { @@ -130,7 +137,7 @@ export function registerGeneratedTools(server: McpServer): void { }, } as any); if (error) return errorResult(error); - return textResult(data); + return successResult(data); }).catch((err) => errorResult(err.message)); }, ); @@ -191,6 +198,7 @@ export function registerGeneratedTools(server: McpServer): void { .max(50) .describe("Array of blocks to create"), }, + outputSchema: outputSchemas["batchCreateBlocks"], }, async (args: Record, extra) => { return withArenaClient(extra, async (client) => { @@ -198,7 +206,7 @@ export function registerGeneratedTools(server: McpServer): void { body: { channel_ids: args["channel_ids"], blocks: args["blocks"] }, } as any); if (error) return errorResult(error); - return textResult(data); + return successResult(data); }).catch((err) => errorResult(err.message)); }, ); @@ -213,6 +221,7 @@ export function registerGeneratedTools(server: McpServer): void { .string() .describe("The batch ID returned from the batch create endpoint"), }, + outputSchema: outputSchemas["getBatchStatus"], }, async (args: Record, extra) => { return withArenaClient(extra, async (client) => { @@ -225,7 +234,7 @@ export function registerGeneratedTools(server: McpServer): void { } as any, ); if (error) return errorResult(error); - return textResult(data); + return successResult(data); }).catch((err) => errorResult(err.message)); }, ); @@ -238,6 +247,7 @@ export function registerGeneratedTools(server: McpServer): void { inputSchema: { id: z.number().int().describe("Resource ID"), }, + outputSchema: outputSchemas["getBlock"], }, async (args: Record, extra) => { return withArenaClient(extra, async (client) => { @@ -247,7 +257,7 @@ export function registerGeneratedTools(server: McpServer): void { }, } as any); if (error) return errorResult(error); - return textResult(data); + return successResult(data); }).catch((err) => errorResult(err.message)); }, ); @@ -280,6 +290,7 @@ export function registerGeneratedTools(server: McpServer): void { "Custom key-value metadata. Uses merge semantics: new keys are added,\nexisting keys are updated, keys set to null are removed.\n", ), }, + outputSchema: outputSchemas["updateBlock"], }, async (args: Record, extra) => { return withArenaClient(extra, async (client) => { @@ -296,7 +307,7 @@ export function registerGeneratedTools(server: McpServer): void { }, } as any); if (error) return errorResult(error); - return textResult(data); + return successResult(data); }).catch((err) => errorResult(err.message)); }, ); @@ -330,6 +341,7 @@ export function registerGeneratedTools(server: McpServer): void { .optional() .describe("Filter connections by ownership."), }, + outputSchema: outputSchemas["getBlockConnections"], }, async (args: Record, extra) => { return withArenaClient(extra, async (client) => { @@ -348,7 +360,7 @@ export function registerGeneratedTools(server: McpServer): void { } as any, ); if (error) return errorResult(error); - return textResult(data); + return successResult(data); }).catch((err) => errorResult(err.message)); }, ); @@ -378,6 +390,7 @@ export function registerGeneratedTools(server: McpServer): void { .optional() .describe("Sort by the date the relationship was created."), }, + outputSchema: outputSchemas["getBlockComments"], }, async (args: Record, extra) => { return withArenaClient(extra, async (client) => { @@ -388,7 +401,7 @@ export function registerGeneratedTools(server: McpServer): void { }, } as any); if (error) return errorResult(error); - return textResult(data); + return successResult(data); }).catch((err) => errorResult(err.message)); }, ); @@ -401,6 +414,7 @@ export function registerGeneratedTools(server: McpServer): void { id: z.number().int().describe("Resource ID"), body: z.string().describe("Comment body (supports @mentions)"), }, + outputSchema: outputSchemas["createBlockComment"], }, async (args: Record, extra) => { return withArenaClient(extra, async (client) => { @@ -411,7 +425,7 @@ export function registerGeneratedTools(server: McpServer): void { body: { body: args["body"] }, } as any); if (error) return errorResult(error); - return textResult(data); + return successResult(data); }).catch((err) => errorResult(err.message)); }, ); @@ -423,6 +437,7 @@ export function registerGeneratedTools(server: McpServer): void { inputSchema: { id: z.number().int().describe("Resource ID"), }, + outputSchema: outputSchemas["deleteComment"], }, async (args: Record, extra) => { return withArenaClient(extra, async (client) => { @@ -432,7 +447,7 @@ export function registerGeneratedTools(server: McpServer): void { }, } as any); if (error) return errorResult(error); - return textResult(data); + return successResult(data); }).catch((err) => errorResult(err.message)); }, ); @@ -469,6 +484,7 @@ export function registerGeneratedTools(server: McpServer): void { .optional() .describe("Custom key-value metadata to set on the new channel."), }, + outputSchema: outputSchemas["createChannel"], }, async (args: Record, extra) => { return withArenaClient(extra, async (client) => { @@ -482,7 +498,7 @@ export function registerGeneratedTools(server: McpServer): void { }, } as any); if (error) return errorResult(error); - return textResult(data); + return successResult(data); }).catch((err) => errorResult(err.message)); }, ); @@ -495,6 +511,7 @@ export function registerGeneratedTools(server: McpServer): void { inputSchema: { id: z.string().describe("Resource ID or slug"), }, + outputSchema: outputSchemas["getChannel"], }, async (args: Record, extra) => { return withArenaClient(extra, async (client) => { @@ -504,7 +521,7 @@ export function registerGeneratedTools(server: McpServer): void { }, } as any); if (error) return errorResult(error); - return textResult(data); + return successResult(data); }).catch((err) => errorResult(err.message)); }, ); @@ -548,6 +565,7 @@ export function registerGeneratedTools(server: McpServer): void { "Custom key-value metadata. Uses merge semantics: new keys are added,\nexisting keys are updated, keys set to null are removed.\n", ), }, + outputSchema: outputSchemas["updateChannel"], }, async (args: Record, extra) => { return withArenaClient(extra, async (client) => { @@ -564,7 +582,7 @@ export function registerGeneratedTools(server: McpServer): void { }, } as any); if (error) return errorResult(error); - return textResult(data); + return successResult(data); }).catch((err) => errorResult(err.message)); }, ); @@ -576,6 +594,7 @@ export function registerGeneratedTools(server: McpServer): void { inputSchema: { id: z.string().describe("Resource ID or slug"), }, + outputSchema: outputSchemas["deleteChannel"], }, async (args: Record, extra) => { return withArenaClient(extra, async (client) => { @@ -585,7 +604,7 @@ export function registerGeneratedTools(server: McpServer): void { }, } as any); if (error) return errorResult(error); - return textResult(data); + return successResult(data); }).catch((err) => errorResult(err.message)); }, ); @@ -643,6 +662,7 @@ export function registerGeneratedTools(server: McpServer): void { "Target channels with optional per-channel position and connection metadata.", ), }, + outputSchema: outputSchemas["createConnection"], }, async (args: Record, extra) => { return withArenaClient(extra, async (client) => { @@ -656,7 +676,7 @@ export function registerGeneratedTools(server: McpServer): void { }, } as any); if (error) return errorResult(error); - return textResult(data); + return successResult(data); }).catch((err) => errorResult(err.message)); }, ); @@ -669,6 +689,7 @@ export function registerGeneratedTools(server: McpServer): void { inputSchema: { id: z.number().int().describe("Resource ID"), }, + outputSchema: outputSchemas["getConnection"], }, async (args: Record, extra) => { return withArenaClient(extra, async (client) => { @@ -678,7 +699,7 @@ export function registerGeneratedTools(server: McpServer): void { }, } as any); if (error) return errorResult(error); - return textResult(data); + return successResult(data); }).catch((err) => errorResult(err.message)); }, ); @@ -698,6 +719,7 @@ export function registerGeneratedTools(server: McpServer): void { "Custom key-value metadata. Uses merge semantics: new keys are added,\nexisting keys are updated, keys set to null are removed.\n", ), }, + outputSchema: outputSchemas["updateConnection"], }, async (args: Record, extra) => { return withArenaClient(extra, async (client) => { @@ -708,7 +730,7 @@ export function registerGeneratedTools(server: McpServer): void { body: { metadata: args["metadata"] }, } as any); if (error) return errorResult(error); - return textResult(data); + return successResult(data); }).catch((err) => errorResult(err.message)); }, ); @@ -721,6 +743,7 @@ export function registerGeneratedTools(server: McpServer): void { inputSchema: { id: z.number().int().describe("Resource ID"), }, + outputSchema: outputSchemas["deleteConnection"], }, async (args: Record, extra) => { return withArenaClient(extra, async (client) => { @@ -730,7 +753,7 @@ export function registerGeneratedTools(server: McpServer): void { }, } as any); if (error) return errorResult(error); - return textResult(data); + return successResult(data); }).catch((err) => errorResult(err.message)); }, ); @@ -759,6 +782,7 @@ export function registerGeneratedTools(server: McpServer): void { "Target position, 1-indexed (required when movement is insert_at)", ), }, + outputSchema: outputSchemas["moveConnection"], }, async (args: Record, extra) => { return withArenaClient(extra, async (client) => { @@ -769,7 +793,7 @@ export function registerGeneratedTools(server: McpServer): void { body: { movement: args["movement"], position: args["position"] }, } as any); if (error) return errorResult(error); - return textResult(data); + return successResult(data); }).catch((err) => errorResult(err.message)); }, ); @@ -813,6 +837,7 @@ export function registerGeneratedTools(server: McpServer): void { .optional() .describe("Filter by user who added the content"), }, + outputSchema: outputSchemas["getChannelContents"], }, async (args: Record, extra) => { return withArenaClient(extra, async (client) => { @@ -828,7 +853,7 @@ export function registerGeneratedTools(server: McpServer): void { }, } as any); if (error) return errorResult(error); - return textResult(data); + return successResult(data); }).catch((err) => errorResult(err.message)); }, ); @@ -858,6 +883,7 @@ export function registerGeneratedTools(server: McpServer): void { .optional() .describe("Sort by the date the relationship was created."), }, + outputSchema: outputSchemas["getChannelConnections"], }, async (args: Record, extra) => { return withArenaClient(extra, async (client) => { @@ -875,7 +901,7 @@ export function registerGeneratedTools(server: McpServer): void { } as any, ); if (error) return errorResult(error); - return textResult(data); + return successResult(data); }).catch((err) => errorResult(err.message)); }, ); @@ -905,6 +931,7 @@ export function registerGeneratedTools(server: McpServer): void { .optional() .describe("Sort by the date the relationship was created."), }, + outputSchema: outputSchemas["getChannelFollowers"], }, async (args: Record, extra) => { return withArenaClient(extra, async (client) => { @@ -922,7 +949,7 @@ export function registerGeneratedTools(server: McpServer): void { } as any, ); if (error) return errorResult(error); - return textResult(data); + return successResult(data); }).catch((err) => errorResult(err.message)); }, ); @@ -933,12 +960,13 @@ export function registerGeneratedTools(server: McpServer): void { description: "Get current user — Returns the currently authenticated user's profile, including the token holder's `email` address.", inputSchema: {}, + outputSchema: outputSchemas["getCurrentUser"], }, async (args: Record, extra) => { return withArenaClient(extra, async (client) => { const { data, error } = await client.GET("/v3/me"); if (error) return errorResult(error); - return textResult(data); + return successResult(data); }).catch((err) => errorResult(err.message)); }, ); @@ -969,6 +997,7 @@ export function registerGeneratedTools(server: McpServer): void { "Load the **previous** page (toward newer items). Pass back the\n`meta.prev_cursor` from a previous response.\n", ), }, + outputSchema: outputSchemas["getMyFeed"], }, async (args: Record, extra) => { return withArenaClient(extra, async (client) => { @@ -982,7 +1011,7 @@ export function registerGeneratedTools(server: McpServer): void { }, } as any); if (error) return errorResult(error); - return textResult(data); + return successResult(data); }).catch((err) => errorResult(err.message)); }, ); @@ -1017,6 +1046,7 @@ export function registerGeneratedTools(server: McpServer): void { .optional() .describe("When true, only unread notifications are returned."), }, + outputSchema: outputSchemas["getMyNotifications"], }, async (args: Record, extra) => { return withArenaClient(extra, async (client) => { @@ -1031,7 +1061,7 @@ export function registerGeneratedTools(server: McpServer): void { }, } as any); if (error) return errorResult(error); - return textResult(data); + return successResult(data); }).catch((err) => errorResult(err.message)); }, ); @@ -1042,12 +1072,13 @@ export function registerGeneratedTools(server: McpServer): void { description: "Mark all current user's notifications as read — Clears the authenticated user's unread notification state and returns the new unread count.", inputSchema: {}, + outputSchema: outputSchemas["markAllMyNotificationsRead"], }, async (args: Record, extra) => { return withArenaClient(extra, async (client) => { const { data, error } = await client.POST("/v3/me/notifications/read"); if (error) return errorResult(error); - return textResult(data); + return successResult(data); }).catch((err) => errorResult(err.message)); }, ); @@ -1063,6 +1094,7 @@ export function registerGeneratedTools(server: McpServer): void { .int() .describe("Activity id returned by the notifications feed."), }, + outputSchema: outputSchemas["markMyNotificationRead"], }, async (args: Record, extra) => { return withArenaClient(extra, async (client) => { @@ -1075,7 +1107,7 @@ export function registerGeneratedTools(server: McpServer): void { } as any, ); if (error) return errorResult(error); - return textResult(data); + return successResult(data); }).catch((err) => errorResult(err.message)); }, ); @@ -1088,6 +1120,7 @@ export function registerGeneratedTools(server: McpServer): void { inputSchema: { id: z.string().describe("Resource ID or slug"), }, + outputSchema: outputSchemas["getUser"], }, async (args: Record, extra) => { return withArenaClient(extra, async (client) => { @@ -1097,7 +1130,7 @@ export function registerGeneratedTools(server: McpServer): void { }, } as any); if (error) return errorResult(error); - return textResult(data); + return successResult(data); }).catch((err) => errorResult(err.message)); }, ); @@ -1144,6 +1177,7 @@ export function registerGeneratedTools(server: McpServer): void { .optional() .describe("Filter to a specific content type."), }, + outputSchema: outputSchemas["getUserContents"], }, async (args: Record, extra) => { return withArenaClient(extra, async (client) => { @@ -1159,7 +1193,7 @@ export function registerGeneratedTools(server: McpServer): void { }, } as any); if (error) return errorResult(error); - return textResult(data); + return successResult(data); }).catch((err) => errorResult(err.message)); }, ); @@ -1189,6 +1223,7 @@ export function registerGeneratedTools(server: McpServer): void { .optional() .describe("Sort by the date the relationship was created."), }, + outputSchema: outputSchemas["getUserFollowers"], }, async (args: Record, extra) => { return withArenaClient(extra, async (client) => { @@ -1199,7 +1234,7 @@ export function registerGeneratedTools(server: McpServer): void { }, } as any); if (error) return errorResult(error); - return textResult(data); + return successResult(data); }).catch((err) => errorResult(err.message)); }, ); @@ -1233,6 +1268,7 @@ export function registerGeneratedTools(server: McpServer): void { .optional() .describe("Filter by followable type"), }, + outputSchema: outputSchemas["getUserFollowing"], }, async (args: Record, extra) => { return withArenaClient(extra, async (client) => { @@ -1248,7 +1284,7 @@ export function registerGeneratedTools(server: McpServer): void { }, } as any); if (error) return errorResult(error); - return textResult(data); + return successResult(data); }).catch((err) => errorResult(err.message)); }, ); @@ -1285,6 +1321,7 @@ export function registerGeneratedTools(server: McpServer): void { .optional() .describe("Sort groups by name or date."), }, + outputSchema: outputSchemas["getUserGroups"], }, async (args: Record, extra) => { return withArenaClient(extra, async (client) => { @@ -1295,7 +1332,7 @@ export function registerGeneratedTools(server: McpServer): void { }, } as any); if (error) return errorResult(error); - return textResult(data); + return successResult(data); }).catch((err) => errorResult(err.message)); }, ); @@ -1322,6 +1359,7 @@ export function registerGeneratedTools(server: McpServer): void { "Role assigned to new members joining the group. Defaults to `member`.", ), }, + outputSchema: outputSchemas["createGroup"], }, async (args: Record, extra) => { return withArenaClient(extra, async (client) => { @@ -1334,7 +1372,7 @@ export function registerGeneratedTools(server: McpServer): void { }, } as any); if (error) return errorResult(error); - return textResult(data); + return successResult(data); }).catch((err) => errorResult(err.message)); }, ); @@ -1347,6 +1385,7 @@ export function registerGeneratedTools(server: McpServer): void { inputSchema: { id: z.string().describe("Resource ID or slug"), }, + outputSchema: outputSchemas["getGroup"], }, async (args: Record, extra) => { return withArenaClient(extra, async (client) => { @@ -1356,7 +1395,7 @@ export function registerGeneratedTools(server: McpServer): void { }, } as any); if (error) return errorResult(error); - return textResult(data); + return successResult(data); }).catch((err) => errorResult(err.message)); }, ); @@ -1384,6 +1423,7 @@ export function registerGeneratedTools(server: McpServer): void { .optional() .describe("Role assigned to new members joining the group"), }, + outputSchema: outputSchemas["updateGroup"], }, async (args: Record, extra) => { return withArenaClient(extra, async (client) => { @@ -1399,7 +1439,7 @@ export function registerGeneratedTools(server: McpServer): void { }, } as any); if (error) return errorResult(error); - return textResult(data); + return successResult(data); }).catch((err) => errorResult(err.message)); }, ); @@ -1411,6 +1451,7 @@ export function registerGeneratedTools(server: McpServer): void { inputSchema: { id: z.string().describe("Resource ID or slug"), }, + outputSchema: outputSchemas["deleteGroup"], }, async (args: Record, extra) => { return withArenaClient(extra, async (client) => { @@ -1420,7 +1461,7 @@ export function registerGeneratedTools(server: McpServer): void { }, } as any); if (error) return errorResult(error); - return textResult(data); + return successResult(data); }).catch((err) => errorResult(err.message)); }, ); @@ -1446,6 +1487,7 @@ export function registerGeneratedTools(server: McpServer): void { .optional() .describe("Number of items per page (max 100)"), }, + outputSchema: outputSchemas["getGroupMembers"], }, async (args: Record, extra) => { return withArenaClient(extra, async (client) => { @@ -1456,7 +1498,7 @@ export function registerGeneratedTools(server: McpServer): void { }, } as any); if (error) return errorResult(error); - return textResult(data); + return successResult(data); }).catch((err) => errorResult(err.message)); }, ); @@ -1475,6 +1517,7 @@ export function registerGeneratedTools(server: McpServer): void { "The group's invite code, authorizing the authenticated user to join", ), }, + outputSchema: outputSchemas["joinGroup"], }, async (args: Record, extra) => { return withArenaClient(extra, async (client) => { @@ -1485,7 +1528,7 @@ export function registerGeneratedTools(server: McpServer): void { body: { invite_token: args["invite_token"] }, } as any); if (error) return errorResult(error); - return textResult(data); + return successResult(data); }).catch((err) => errorResult(err.message)); }, ); @@ -1498,6 +1541,7 @@ export function registerGeneratedTools(server: McpServer): void { inputSchema: { id: z.string().describe("Resource ID or slug"), }, + outputSchema: outputSchemas["leaveGroup"], }, async (args: Record, extra) => { return withArenaClient(extra, async (client) => { @@ -1510,7 +1554,7 @@ export function registerGeneratedTools(server: McpServer): void { } as any, ); if (error) return errorResult(error); - return textResult(data); + return successResult(data); }).catch((err) => errorResult(err.message)); }, ); @@ -1526,6 +1570,7 @@ export function registerGeneratedTools(server: McpServer): void { .int() .describe("User ID of the group member to remove"), }, + outputSchema: outputSchemas["removeGroupMember"], }, async (args: Record, extra) => { return withArenaClient(extra, async (client) => { @@ -1538,7 +1583,7 @@ export function registerGeneratedTools(server: McpServer): void { } as any, ); if (error) return errorResult(error); - return textResult(data); + return successResult(data); }).catch((err) => errorResult(err.message)); }, ); @@ -1556,6 +1601,7 @@ export function registerGeneratedTools(server: McpServer): void { .describe("User ID of the group member to update"), role: z.enum(["viewer", "member", "admin"]), }, + outputSchema: outputSchemas["updateGroupMember"], }, async (args: Record, extra) => { return withArenaClient(extra, async (client) => { @@ -1569,7 +1615,7 @@ export function registerGeneratedTools(server: McpServer): void { } as any, ); if (error) return errorResult(error); - return textResult(data); + return successResult(data); }).catch((err) => errorResult(err.message)); }, ); @@ -1595,6 +1641,7 @@ export function registerGeneratedTools(server: McpServer): void { .optional() .describe("Number of items per page (max 100)"), }, + outputSchema: outputSchemas["getGroupInvitations"], }, async (args: Record, extra) => { return withArenaClient(extra, async (client) => { @@ -1608,7 +1655,7 @@ export function registerGeneratedTools(server: McpServer): void { } as any, ); if (error) return errorResult(error); - return textResult(data); + return successResult(data); }).catch((err) => errorResult(err.message)); }, ); @@ -1627,6 +1674,7 @@ export function registerGeneratedTools(server: McpServer): void { .describe("User ID to add or invite"), email: z.string().optional().describe("Email address to invite"), }, + outputSchema: outputSchemas["createGroupInvitation"], }, async (args: Record, extra) => { return withArenaClient(extra, async (client) => { @@ -1640,7 +1688,7 @@ export function registerGeneratedTools(server: McpServer): void { } as any, ); if (error) return errorResult(error); - return textResult(data); + return successResult(data); }).catch((err) => errorResult(err.message)); }, ); @@ -1657,6 +1705,7 @@ export function registerGeneratedTools(server: McpServer): void { .int() .describe("ID of the invitation to revoke"), }, + outputSchema: outputSchemas["revokeGroupInvitation"], }, async (args: Record, extra) => { return withArenaClient(extra, async (client) => { @@ -1669,7 +1718,7 @@ export function registerGeneratedTools(server: McpServer): void { } as any, ); if (error) return errorResult(error); - return textResult(data); + return successResult(data); }).catch((err) => errorResult(err.message)); }, ); @@ -1682,6 +1731,7 @@ export function registerGeneratedTools(server: McpServer): void { inputSchema: { id: z.string().describe("Resource ID or slug"), }, + outputSchema: outputSchemas["getGroupInvite"], }, async (args: Record, extra) => { return withArenaClient(extra, async (client) => { @@ -1691,7 +1741,7 @@ export function registerGeneratedTools(server: McpServer): void { }, } as any); if (error) return errorResult(error); - return textResult(data); + return successResult(data); }).catch((err) => errorResult(err.message)); }, ); @@ -1704,6 +1754,7 @@ export function registerGeneratedTools(server: McpServer): void { inputSchema: { id: z.string().describe("Resource ID or slug"), }, + outputSchema: outputSchemas["createGroupInvite"], }, async (args: Record, extra) => { return withArenaClient(extra, async (client) => { @@ -1713,7 +1764,7 @@ export function registerGeneratedTools(server: McpServer): void { }, } as any); if (error) return errorResult(error); - return textResult(data); + return successResult(data); }).catch((err) => errorResult(err.message)); }, ); @@ -1726,6 +1777,7 @@ export function registerGeneratedTools(server: McpServer): void { inputSchema: { id: z.string().describe("Resource ID or slug"), }, + outputSchema: outputSchemas["deleteGroupInvite"], }, async (args: Record, extra) => { return withArenaClient(extra, async (client) => { @@ -1735,7 +1787,7 @@ export function registerGeneratedTools(server: McpServer): void { }, } as any); if (error) return errorResult(error); - return textResult(data); + return successResult(data); }).catch((err) => errorResult(err.message)); }, ); @@ -1782,6 +1834,7 @@ export function registerGeneratedTools(server: McpServer): void { .optional() .describe("Filter to a specific content type."), }, + outputSchema: outputSchemas["getGroupContents"], }, async (args: Record, extra) => { return withArenaClient(extra, async (client) => { @@ -1797,7 +1850,7 @@ export function registerGeneratedTools(server: McpServer): void { }, } as any); if (error) return errorResult(error); - return textResult(data); + return successResult(data); }).catch((err) => errorResult(err.message)); }, ); @@ -1827,6 +1880,7 @@ export function registerGeneratedTools(server: McpServer): void { .optional() .describe("Sort by the date the relationship was created."), }, + outputSchema: outputSchemas["getGroupFollowers"], }, async (args: Record, extra) => { return withArenaClient(extra, async (client) => { @@ -1837,7 +1891,7 @@ export function registerGeneratedTools(server: McpServer): void { }, } as any); if (error) return errorResult(error); - return textResult(data); + return successResult(data); }).catch((err) => errorResult(err.message)); }, ); @@ -2012,6 +2066,7 @@ export function registerGeneratedTools(server: McpServer): void { .optional() .describe("Number of items per page (max 100)"), }, + outputSchema: outputSchemas["search"], }, async (args: Record, extra) => { return withArenaClient(extra, async (client) => { @@ -2034,7 +2089,7 @@ export function registerGeneratedTools(server: McpServer): void { }, } as any); if (error) return errorResult(error); - return textResult(data); + return successResult(data); }).catch((err) => errorResult(err.message)); }, ); diff --git a/src/lib/portable-json-schema.ts b/src/lib/portable-json-schema.ts new file mode 100644 index 0000000..6f1ce3c --- /dev/null +++ b/src/lib/portable-json-schema.ts @@ -0,0 +1,26 @@ +import type { ZodTypeAny } from "zod"; +import { zodToJsonSchema, type JsonSchema7Type } from "zod-to-json-schema"; + +// Equivalent to array-form type, but consumable by clients expecting one type +// per schema node. Other constraints continue to apply to every union branch. +export function expandTypeArray( + schema: Record, +): Record { + if (!Array.isArray(schema.type)) return schema; + const { type, ...result } = schema; + const union = { anyOf: type.map((type) => ({ type })) }; + if (result.anyOf) { + result.allOf = [...((result.allOf as unknown[]) ?? []), union]; + } else { + result.anyOf = union.anyOf; + } + return result; +} + +export function zodToToolJsonSchema(schema: ZodTypeAny) { + return zodToJsonSchema(schema, { + target: "jsonSchema7", + postProcess: (result) => + result ? (expandTypeArray(result) as JsonSchema7Type) : result, + }); +} diff --git a/src/lib/tool-helpers.ts b/src/lib/tool-helpers.ts index e64ee5e..1f9da09 100644 --- a/src/lib/tool-helpers.ts +++ b/src/lib/tool-helpers.ts @@ -1,9 +1,12 @@ import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import { createArenaClient, type ArenaClient } from "../client"; -export function textResult(data: unknown): CallToolResult { +export function successResult(data: unknown): CallToolResult { + const text = JSON.stringify({ result: data ?? null }, null, 2); + const structuredContent: Record = JSON.parse(text); return { - content: [{ type: "text", text: JSON.stringify(data, null, 2) ?? "null" }], + content: [{ type: "text", text }], + structuredContent, }; } diff --git a/src/lib/tool-registry.ts b/src/lib/tool-registry.ts new file mode 100644 index 0000000..2f011ca --- /dev/null +++ b/src/lib/tool-registry.ts @@ -0,0 +1,101 @@ +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import type { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js"; +import { + CallToolRequestSchema, + ListToolsRequestSchema, + ErrorCode, + McpError, + type CallToolResult, + type ServerRequest, + type ServerNotification, + type Tool, +} from "@modelcontextprotocol/sdk/types.js"; +import { CfWorkerJsonSchemaValidator } from "@modelcontextprotocol/sdk/validation/cfworker"; +import { z } from "zod"; +import { zodToToolJsonSchema } from "./portable-json-schema"; +import { errorResult } from "./tool-helpers"; + +export type OutputSchema = NonNullable; +type Extra = RequestHandlerExtra; + +// The low-level MCP API lets us publish the original JSON Schemas without +// lossy conversion through Zod. Input parsing remains the existing Zod contract. +export class ToolRegistry { + private tools = new Map< + string, + { + definition: Tool; + call: (args: unknown, extra: Extra) => Promise; + } + >(); + + registerTool( + name: string, + config: { + description: string; + inputSchema: Shape; + outputSchema: OutputSchema; + }, + handler: ( + args: z.infer>, + extra: Extra, + ) => Promise, + ): void { + if (this.tools.has(name)) throw new Error(`Duplicate tool: ${name}`); + const input = z.object(config.inputSchema); + const inputSchema = zodToToolJsonSchema(input); + // Compile lazily: each HTTP request creates a server, but usually calls only + // one tool. No eval/code generation is used in the Workers validator. + let validate: + | ReturnType + | undefined; + this.tools.set(name, { + definition: { + name, + description: config.description, + inputSchema: { ...inputSchema, type: "object" }, + outputSchema: config.outputSchema, + }, + call: async (args, extra) => { + const parsed = input.safeParse(args ?? {}); + if (!parsed.success) + return errorResult( + `Invalid arguments for ${name}: ${parsed.error.message}`, + ); + try { + const result = await handler(parsed.data, extra); + if (!result.isError) { + validate ??= new CfWorkerJsonSchemaValidator().getValidator( + config.outputSchema, + ); + const checked = validate(result.structuredContent); + if (!checked.valid) + return errorResult( + `Output validation failed for ${name}; the API operation may already have completed. Do not automatically retry writes. ${checked.errorMessage}`, + ); + } + return result; + } catch (error) { + return errorResult( + error instanceof Error ? error.message : String(error), + ); + } + }, + }); + } + + install(server: Server): void { + server.setRequestHandler(ListToolsRequestSchema, async () => ({ + tools: [...this.tools.values()].map(({ definition }) => definition), + })); + server.setRequestHandler(CallToolRequestSchema, async (request, extra) => { + const tool = this.tools.get(request.params.name); + if (!tool) + throw new McpError( + ErrorCode.InvalidParams, + `Unknown tool: ${request.params.name}`, + ); + return tool.call(request.params.arguments, extra); + }); + } +} diff --git a/src/server.ts b/src/server.ts index c6598ac..b0a7d5b 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,15 +1,25 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { CfWorkerJsonSchemaValidator } from "@modelcontextprotocol/sdk/validation/cfworker"; +import { ToolRegistry } from "./lib/tool-registry"; import { registerGeneratedTools } from "./generated/tools"; import { registerCustomTools } from "./tools/index"; -export function createMcpServer(): McpServer { - const server = new McpServer({ - name: "@aredotna/mcp", - version: "0.1.0", - }); +export function createMcpServer(): Server { + const server = new Server( + { + name: "@aredotna/mcp", + version: "0.1.0", + }, + { + capabilities: { tools: {} }, + jsonSchemaValidator: new CfWorkerJsonSchemaValidator(), + }, + ); - registerGeneratedTools(server); - registerCustomTools(server); + const tools = new ToolRegistry(); + registerGeneratedTools(tools); + registerCustomTools(tools); + tools.install(server); return server; } diff --git a/src/tools/custom/channel-digest.ts b/src/tools/custom/channel-digest.ts index 048cc37..04d7545 100644 --- a/src/tools/custom/channel-digest.ts +++ b/src/tools/custom/channel-digest.ts @@ -1,17 +1,19 @@ +import { channelDigestOutput } from "./output-schemas"; import { z } from "zod"; -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolRegistry } from "../../lib/tool-registry"; import { withArenaClient, - textResult, + successResult, errorResult, } from "../../lib/tool-helpers"; -export function registerChannelDigest(server: McpServer): void { +export function registerChannelDigest(server: ToolRegistry): void { server.registerTool( "channelDigest", { description: "Get a structured summary of an Are.na channel including metadata, type breakdown, collaborators, and recent activity. Samples content rather than fetching everything, so it works efficiently on large channels.", + outputSchema: channelDigestOutput, inputSchema: { channel_id: z.string().describe("Channel ID or slug"), sample_size: z @@ -74,7 +76,7 @@ export function registerChannelDigest(server: McpServer): void { const total = meta.counts.contents; const isSampled = total > sampled; - return textResult({ + return successResult({ channel: { id: meta.id, title: meta.title, diff --git a/src/tools/custom/output-schemas.ts b/src/tools/custom/output-schemas.ts new file mode 100644 index 0000000..b84bf6d --- /dev/null +++ b/src/tools/custom/output-schemas.ts @@ -0,0 +1,65 @@ +import { z } from "zod"; +import { zodToToolJsonSchema } from "../../lib/portable-json-schema"; +import type { OutputSchema } from "../../lib/tool-registry"; + +function resultSchema(result: z.ZodTypeAny): OutputSchema { + return { + ...zodToToolJsonSchema(z.object({ result })), + type: "object", + }; +} + +const identity = z.object({ + slug: z.string(), + name: z.string(), + type: z.enum(["User", "Group"]), +}); + +export const channelDigestOutput = resultSchema( + z.object({ + channel: z.object({ + id: z.number().int(), + title: z.string(), + slug: z.string(), + visibility: z.enum(["public", "closed", "private"]), + description: z.string().nullable(), + created_at: z.string(), + updated_at: z.string(), + }), + owner: identity, + counts: z.object({ + blocks: z.number().int(), + channels: z.number().int(), + contents: z.number().int(), + collaborators: z.number().int(), + }), + collaborators: z.array(identity).optional(), + type_breakdown: z.record(z.union([z.number().int(), z.string()])), + active_contributors: z.array(z.string()), + recent_items: z.array( + z.object({ + id: z.number().int(), + type: z.string(), + title: z.string().optional(), + added: z.string(), + added_by: z.string().optional(), + }), + ), + }), +); + +export const searchAndConnectOutput = resultSchema( + z.object({ + query: z.string(), + target_channel: z.string(), + found: z.number().int(), + connected: z.number().int(), + skipped: z + .array(z.object({ id: z.number().int(), type: z.string() })) + .optional(), + errors: z.array(z.string()).optional(), + items: z.array( + z.object({ id: z.number().int(), type: z.enum(["Block", "Channel"]) }), + ), + }), +); diff --git a/src/tools/custom/search-and-connect.ts b/src/tools/custom/search-and-connect.ts index ffbc5b2..bb2a9ca 100644 --- a/src/tools/custom/search-and-connect.ts +++ b/src/tools/custom/search-and-connect.ts @@ -1,8 +1,9 @@ +import { searchAndConnectOutput } from "./output-schemas"; import { z } from "zod"; -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolRegistry } from "../../lib/tool-registry"; import { withArenaClient, - textResult, + successResult, errorResult, } from "../../lib/tool-helpers"; @@ -15,12 +16,13 @@ const CONNECTABLE_TYPES = new Set([ "Channel", ]); -export function registerSearchAndConnect(server: McpServer): void { +export function registerSearchAndConnect(server: ToolRegistry): void { server.registerTool( "searchAndConnect", { description: "Search Are.na and connect matching results to a target channel", + outputSchema: searchAndConnectOutput, inputSchema: { query: z.string().describe("Search query"), target_channel_id: z @@ -98,7 +100,7 @@ export function registerSearchAndConnect(server: McpServer): void { } } - return textResult({ + return successResult({ query: args.query, target_channel: args.target_channel_id, found: items.length, diff --git a/src/tools/index.ts b/src/tools/index.ts index 2e051f3..09232e6 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -1,8 +1,8 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolRegistry } from "../lib/tool-registry"; import { registerSearchAndConnect } from "./custom/search-and-connect"; import { registerChannelDigest } from "./custom/channel-digest"; -export function registerCustomTools(server: McpServer): void { +export function registerCustomTools(server: ToolRegistry): void { registerSearchAndConnect(server); registerChannelDigest(server); } diff --git a/yarn.lock b/yarn.lock index 746bc81..66b455b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16,6 +16,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz#010b6938fab7cb7df74aa2bbc06aa503b8fe5fb4" integrity sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q== +"@cfworker/json-schema@^4.1.1": + version "4.1.1" + resolved "https://registry.yarnpkg.com/@cfworker/json-schema/-/json-schema-4.1.1.tgz#4a2a3947ee9fa7b7c24be981422831b8674c3be6" + integrity sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og== + "@cloudflare/kv-asset-handler@0.4.2": version "0.4.2" resolved "https://registry.yarnpkg.com/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.4.2.tgz#b6b8eab81f0f9d8378e219dd321df20280e3bbd2"