@relvo/mcp 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +47 -154
  2. package/bin/relvo-mcp.mjs +1 -1
  3. package/dist/client/agent-run.js +681 -0
  4. package/dist/client/browser.js +652 -0
  5. package/dist/client/evm-wallet.js +214 -0
  6. package/dist/client/index.js +3 -360
  7. package/dist/client/local-input.js +18 -11
  8. package/dist/client/local-profile.js +22 -110
  9. package/dist/client/local-setup.js +0 -1
  10. package/dist/client/tool-name-hints.js +48 -0
  11. package/dist/client/wallet-access.js +207 -0
  12. package/dist/mcp/agent-server.js +237 -0
  13. package/dist/mcp/context.js +3 -2
  14. package/dist/mcp/host-schema.js +47 -0
  15. package/dist/mcp/instructions.js +12 -2
  16. package/dist/mcp/local-upload.js +12 -4
  17. package/dist/mcp/operations.js +30 -0
  18. package/dist/mcp/runtime.js +25 -5
  19. package/dist/mcp/server.js +191 -37
  20. package/dist/mcp/version.js +1 -0
  21. package/dist/schema/accountRequestMethods.js +1 -0
  22. package/dist/schema/cronValidator.js +150 -0
  23. package/dist/schema/platformApiKey.js +5 -5
  24. package/dist/schema/platformBillingConstants.js +3 -0
  25. package/dist/schema/platformCapabilityFamilies.js +269 -0
  26. package/dist/schema/platformConnectionCatalog.js +20 -3
  27. package/dist/schema/platformConnectionPolicy.js +26 -0
  28. package/dist/schema/platformCustomTools.js +113 -0
  29. package/dist/schema/platformFunding.js +9 -0
  30. package/dist/schema/platformHttpConstraints.js +7 -0
  31. package/dist/schema/platformMcpTools.js +7 -0
  32. package/dist/schema/platformMetaTools.js +8 -6
  33. package/dist/schema/platformMonitorSchedule.js +123 -0
  34. package/dist/schema/platformMonitors.js +159 -0
  35. package/dist/schema/platformOAuth.js +7 -0
  36. package/dist/schema/platformPublicExecution.js +53 -15
  37. package/dist/schema/platformRoutes.js +61 -1
  38. package/dist/schema/platformSkills.js +1 -1
  39. package/dist/schema/platformTeamSecretName.js +3 -0
  40. package/dist/schema/platformTeamSkills.js +49 -0
  41. package/dist/schema/platformToolAccess.js +18 -0
  42. package/dist/schema/platformTriggers.js +152 -0
  43. package/dist/schema/platformWorkflows.js +100 -0
  44. package/dist/schema/scheduleTimeZone.js +15 -0
  45. package/dist/schema/toolDiscovery.js +20 -3
  46. package/dist/schema/toolInputRepair.js +110 -0
  47. package/dist/schema/toolResultContext.js +1 -1
  48. package/package.json +1 -1
  49. package/dist/schema/amazonAds.js +0 -13
  50. package/dist/schema/oauth2.js +0 -64
@@ -1,26 +1,41 @@
1
1
  import { McpServer } from "@modelcontextprotocol/server";
2
- import { fitToolResultPage, executionReceiptContext, executionPageContext, hasRelvoEnvironment, relvoEnvironmentValue, platformToolInputSchema, platformCreateExecutionSchema, platformExecutionIdSchema, platformArtifactIdSchema, platformExecutionListQuerySchema, toolDiscoveryInputSchema, platformSkillSchema, toolResultSelectionSchema, platformConnectionCatalogQuerySchema } from "../client/index.js";
2
+ import { fitToolResultPage, executionReceiptContext, executionPageContext, hasRelvoEnvironment, platformQuoteRequestSchema, platformCreateExecutionSchema, platformExecutionIdSchema, platformArtifactIdSchema, platformExecutionListQuerySchema, toolDiscoveryInputSchema, platformSkillSchema, toolResultSelectionSchema, platformConnectionCatalogQuerySchema, platformMonitorCreateSchema, platformMonitorEventsQuerySchema, platformMonitorQuoteSchema, platformMonitorResumeSchema, PLATFORM_MONITOR_ID, platformCustomEndpointDefinitionSchema, platformCustomToolSlugSchema, platformCustomToolDefinitionSchema, platformCustomToolRestoreSchema, platformTeamSkillCreateSchema, platformTeamSkillDefinitionSchema, platformTeamSkillRestoreSchema, platformTeamSkillUpdateSchema } from "../client/index.js";
3
+ import { platformWorkflowDefinitionSchema, platformWorkflowRunCreateSchema, platformWorkflowRunIdSchema, platformWorkflowRunListQuerySchema, platformWorkflowSlugSchema, platformWorkflowStepSchema } from "../client/index.js";
4
+ import { platformTriggerCreateSchema, platformTriggerDestinationInputSchema, platformTriggerEventsQuerySchema, platformTriggerIdSchema, platformTriggerIntervalSchema, platformTriggerTypesQuerySchema, platformTriggerUpdateSchema } from "../client/index.js";
3
5
  import { z } from "zod";
4
6
  import { uploadAuthorizedFile } from "./local-upload.js";
5
7
  import { platformMetaSearchSchema, platformMetaSchemasSchema, platformMetaConnectionsSchema, platformMetaExecuteSchema, platformMetaWorkbenchSchema, platformMetaBashSchema } from "../client/index.js";
6
8
  import { boundedMcpResult, inspectJsonContext, mcpContextFits, MCP_RESULT_MAX_BYTES } from "./context.js";
9
+ import { forHosts } from "./host-schema.js";
7
10
  import { RELVO_MCP_INSTRUCTIONS } from "./instructions.js";
11
+ import { PLATFORM_MCP_VERSION } from "./version.js";
12
+ import { PlatformOperations } from "./operations.js";
8
13
  const projectionOptions = { maximumBytes: Math.floor(MCP_RESULT_MAX_BYTES / 4), readWith: "platform_result" };
9
14
  const jsonObject = z.record(z.string(), z.json());
10
15
  const inspectInput = { ...toolResultSelectionSchema.omit({ fields: true }).shape,
11
16
  path: toolResultSelectionSchema.shape.path.describe("Own JSON keys in the definition or guide, e.g. [\"inputSchema\",\"properties\"]. Follow entry paths for omitted subtrees.") };
12
17
  const summary = (tool) => {
13
18
  const { inputSchema: _schema, example: _example, models: _models, ...metadata } = tool;
14
- return metadata;
19
+ return { ...metadata, description: tool.description.split("\n")[0] };
15
20
  };
16
- export function createPlatformMcp(client, version = "0.1.2", localFilesystem = false, managedProfile = false) {
21
+ export function compactToolDiscovery(value, fits) {
22
+ const tools = value.data.tools.map(summary);
23
+ const response = () => ({ data: { ...value.data, tools, discovery: { ...value.data.discovery, returnedCount: tools.length, truncated: true } } });
24
+ while (tools.length && !fits(response()))
25
+ tools.pop();
26
+ return response();
27
+ }
28
+ export function createPlatformMcp(client, version = PLATFORM_MCP_VERSION, localFilesystem = false, managedProfile = false) {
29
+ const server = new McpServer({ name: "relvo", version }, { instructions: RELVO_MCP_INSTRUCTIONS });
30
+ createPlatformOperations(client, localFilesystem, managedProfile).attach(server);
31
+ return server;
32
+ }
33
+ export function createPlatformOperations(client, localFilesystem = false, managedProfile = false) {
17
34
  const relvoCredentials = managedProfile || hasRelvoEnvironment(process.env);
18
- const uploadRoots = relvoEnvironmentValue(process.env, "RELVO_MCP_ALLOWED_UPLOAD_ROOTS") ?? (relvoCredentials ? undefined : process.env.NOIMOS_MCP_ALLOWED_UPLOAD_ROOTS);
19
- const server = new McpServer({ name: "relvo", version }, {
20
- instructions: RELVO_MCP_INSTRUCTIONS,
21
- });
22
- server.registerTool("platform_search_tools", { description: "Search up to four independent capabilities and open/continue a project tool session. Retain session.id for batches or remote processing. Free; omit private data from queries.",
23
- inputSchema: platformMetaSearchSchema, annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true, idempotentHint: false } }, input => respond(() => client.searchMetaTools(input), undefined, (value, fits) => {
35
+ const uploadRoots = process.env.RELVO_MCP_ALLOWED_UPLOAD_ROOTS ?? (relvoCredentials ? undefined : process.env.NOIMOS_MCP_ALLOWED_UPLOAD_ROOTS);
36
+ const server = new PlatformOperations();
37
+ server.registerTool("platform_search_tools", { description: "Search up to four independent capabilities and open/continue a project tool session. Retain session.id for batches or remote processing. Each query counts toward the team's monthly free calls, then is charged; omit private data from queries.",
38
+ inputSchema: forHosts(platformMetaSearchSchema), annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true, idempotentHint: false } }, input => respond(() => client.searchMetaTools(input), undefined, (value, fits) => {
24
39
  const results = value.data.results.map(result => ({ ...result, tools: result.tools.slice(0, 0) }));
25
40
  const response = () => ({ data: { ...value.data, results: results.map((result, index) => ({ ...result,
26
41
  discovery: { ...result.discovery, returnedCount: result.tools.length, truncated: result.discovery.truncated || result.tools.length < value.data.results[index].tools.length } })) } });
@@ -37,7 +52,7 @@ export function createPlatformMcp(client, version = "0.1.2", localFilesystem = f
37
52
  return response();
38
53
  }));
39
54
  server.registerTool("platform_get_tool_schemas", { description: "Read selected schemas, examples and prices in a session. Inspect remaining names with platform_tool; large definitions provide exact paths/pages. Free.",
40
- inputSchema: platformMetaSchemasSchema, annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false, idempotentHint: true } }, input => respond(() => client.getToolSchemas(input), undefined, (value, fits) => {
55
+ inputSchema: forHosts(platformMetaSchemasSchema), annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false, idempotentHint: true } }, input => respond(() => client.getToolSchemas(input), undefined, (value, fits) => {
41
56
  const data = { ...value.data, tools: [...value.data.tools], remaining: [...value.data.remaining] };
42
57
  while (data.tools.length && !fits({ data })) {
43
58
  const tool = data.tools.pop();
@@ -47,14 +62,14 @@ export function createPlatformMcp(client, version = "0.1.2", localFilesystem = f
47
62
  return { data };
48
63
  }));
49
64
  server.registerTool("platform_manage_connections", { description: "Check OAuth accounts required by a selected tool. Reuse the intended connected account; request console consent only for a missing/expired connection. Free read; no account changes.",
50
- inputSchema: platformMetaConnectionsSchema, annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true, idempotentHint: true } }, input => respond(() => client.manageConnections(input), undefined, (value, fits) => {
65
+ inputSchema: forHosts(platformMetaConnectionsSchema), annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true, idempotentHint: true } }, input => respond(() => client.manageConnections(input), undefined, (value, fits) => {
51
66
  const data = { ...value.data, connections: [...value.data.connections] };
52
67
  while (data.connections.length && !fits({ data }))
53
68
  data.connections.pop();
54
69
  return { data };
55
70
  }));
56
71
  server.registerTool("platform_multi_execute", { description: "Submit independent quoted calls within an accepted total cap. Save the complete request and per-item keys first. Read every admission even on error; wait for accepted IDs. Compact receipts use index to identify original calls. Dependencies require a later batch.",
57
- inputSchema: platformMetaExecuteSchema, annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true, idempotentHint: true } }, async (input) => {
72
+ inputSchema: forHosts(platformMetaExecuteSchema), annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true, idempotentHint: true } }, async (input) => {
58
73
  let allAccepted = false;
59
74
  const result = await respond(async () => { const value = await client.multiExecute(input); allAccepted = value.data.allAccepted; return value; }, undefined, (value, fits) => {
60
75
  const data = { ...value.data, results: value.data.results.map(({ tool: _tool, idempotencyKey: _key, ...receipt }) => receipt),
@@ -68,10 +83,10 @@ export function createPlatformMcp(client, version = "0.1.2", localFilesystem = f
68
83
  return allAccepted ? result : { ...result, isError: true };
69
84
  });
70
85
  server.registerTool("platform_remote_workbench", { description: "Submit a quoted Python cell to this session's remote sandbox. Stage full prior outputs with sourceExecutionIds. Wait for success and a ready session before the next cell. Files/checkpointable variables persist; no credentials or authenticated tool proxy.",
71
- inputSchema: platformMetaWorkbenchSchema, annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true, idempotentHint: true } }, input => respond(() => client.remoteWorkbench(input), "platform_remote_workbench", value => ({ data: { ...value.data,
86
+ inputSchema: forHosts(platformMetaWorkbenchSchema), annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true, idempotentHint: true } }, input => respond(() => client.remoteWorkbench(input), "platform_remote_workbench", value => ({ data: { ...value.data,
72
87
  execution: executionReceiptContext({ data: value.data.execution }, projectionOptions).data } })));
73
88
  server.registerTool("platform_remote_bash", { description: "Submit a quoted remote Bash cell. Shares files with Python and retains its own checkpointed shell state. Wait for success and a ready session before another cell. Deployment controls network; never runs on the user's machine.",
74
- inputSchema: platformMetaBashSchema, annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true, idempotentHint: true } }, input => respond(() => client.remoteBash(input), "platform_remote_bash", value => ({ data: { ...value.data,
89
+ inputSchema: forHosts(platformMetaBashSchema), annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true, idempotentHint: true } }, input => respond(() => client.remoteBash(input), "platform_remote_bash", value => ({ data: { ...value.data,
75
90
  execution: executionReceiptContext({ data: value.data.execution }, projectionOptions).data } })));
76
91
  const respond = async (operation, untrustedTool, compact) => {
77
92
  try {
@@ -84,20 +99,162 @@ export function createPlatformMcp(client, version = "0.1.2", localFilesystem = f
84
99
  if (localFilesystem)
85
100
  server.registerTool("platform_upload_file", {
86
101
  description: "Upload an authorized media file under configured RELVO_MCP_ALLOWED_UPLOAD_ROOTS, up to 12 MiB. A request to use that exact file authorizes upload. Never upload credentials/configs or broaden roots. Returns data.id for documented fileId inputs.",
87
- inputSchema: z.object({ path: z.string().min(1).max(4096), confirmedByUser: z.literal(true).describe("True only when the user has authorized uploading this exact file.") }).strict(),
102
+ inputSchema: forHosts(z.object({ path: z.string().min(1).max(4096), confirmedByUser: z.literal(true).describe("True only when the user has authorized uploading this exact file.") }).strict()),
88
103
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true, idempotentHint: true },
89
104
  }, ({ path }) => respond(() => uploadAuthorizedFile(client, path, uploadRoots)));
90
105
  for (const [name, description, operation] of [
91
106
  ["platform_project", "Read the developer organization and project bound to this API key.", () => client.project()],
92
107
  ["platform_usage", "Read this developer project's current monthly requests and spend in USD.", () => client.usage()],
93
108
  ]) {
94
- server.registerTool(name, { description, inputSchema: z.object({}), annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false, idempotentHint: true } }, async () => {
109
+ server.registerTool(name, { description, inputSchema: forHosts(z.object({})), annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false, idempotentHint: true } }, async () => {
95
110
  return respond(async () => operation());
96
111
  });
97
112
  }
113
+ const monitorId = z.string().regex(PLATFORM_MONITOR_ID);
114
+ server.registerTool("platform_monitors", { description: "List this project's monitors: saved tool calls Relvo re-runs on a schedule and diffs. Shows subscribers, price (chargeCeilingMicros per run, charged to every subscriber in full; members), status (suspended when a run could not be paid or its price rose above the accepted maximum), last check/change and last error. Free.",
115
+ inputSchema: forHosts(z.object({})), annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false, idempotentHint: true } }, () => respond(() => client.monitors()));
116
+ server.registerTool("platform_monitor_quote", { description: "Price a monitor before creating it. A run costs what one execution of the tool costs (at most chargeCeilingMicros) and every project watching the target pays that in full; they only share the fetch. Only tools whose execution is priced 0 cost 0. Free.",
117
+ inputSchema: forHosts(z.object({ ...platformMonitorQuoteSchema.shape, arguments: z.record(z.string(), z.unknown()).default({}).describe("The tool's arguments, exactly as for platform_execute.") }).strict()),
118
+ annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false, idempotentHint: true } }, input => respond(() => client.quoteMonitor(platformMonitorQuoteSchema.parse(input))));
119
+ server.registerTool("platform_monitor_create", { description: "Watch a tool for changes. platform_monitor_tools lists monitorable tools and the provider to pin. A monitor charges this project the full price of every run from the balance until deleted (0 only for tools priced 0): quote it, get the user's approval of that recurring price, and pass maximumChargeMicrosPerRun of at least the quote's chargeCeilingMicros. The first run is a baseline with no events; later runs report only new items (list results) or changed fields (single results). Identical monitors are shared and idempotent.",
120
+ inputSchema: forHosts(z.object({ ...platformMonitorCreateSchema.shape, arguments: z.record(z.string(), z.unknown()).default({}).describe("The tool's arguments, exactly as for platform_execute.") }).strict()),
121
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true, idempotentHint: true } }, input => respond(async () => client.createMonitor(platformMonitorCreateSchema.parse(input))));
122
+ server.registerTool("platform_monitor_resume", { description: "Resume a suspended monitor from now on; events detected while suspended stay hidden. Pass maximumChargeMicrosPerRun to accept a higher price, with the user's approval.",
123
+ inputSchema: forHosts(z.object({ id: monitorId, ...platformMonitorResumeSchema.shape }).strict()), annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true, idempotentHint: true } }, ({ id, ...input }) => respond(() => client.resumeMonitor(id, input)));
124
+ server.registerTool("platform_monitor_delete", { description: "Stop one monitor of this project. Its events and other projects' monitors are unaffected. Free.",
125
+ inputSchema: forHosts(z.object({ id: monitorId }).strict()), annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false, idempotentHint: true } }, ({ id }) => respond(() => client.deleteMonitor(id)));
126
+ server.registerTool("platform_monitor_events", { description: "Read what changed: events oldest first. Omit monitorId for every monitor. Pass the returned nextAfter as after to read only newer events. Without after, returns the latest events. Free.",
127
+ inputSchema: forHosts(platformMonitorEventsQuerySchema.extend({ monitorId: monitorId.optional() }).strict()), annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false, idempotentHint: true } }, input => respond(() => client.monitorEvents(input)));
128
+ server.registerTool("platform_monitors_popular", { description: "Monitors that at least 3 organizations already watch (tool, provider, arguments, interval, organizations, chargeCeilingMicros). Joining one shares its fetch but you pay the full price per run. Copy one to platform_monitor_quote. Free.",
129
+ inputSchema: forHosts(z.object({})), annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false, idempotentHint: true } }, () => respond(() => client.popularMonitors()));
130
+ server.registerTool("platform_monitor_tools", { description: "Tools that can be monitored, with the provider to pin (empty when none) and chargeCeilingMicros, the most one run of the tool's example costs in total (0 = free). Narrow with query or freeOnly and follow nextOffset. Free.",
131
+ inputSchema: forHosts(z.object({
132
+ query: z.string().trim().max(100).optional().describe("Part of a tool name to keep; empty means every tool."),
133
+ freeOnly: z.boolean().default(false).describe("Keep only tools a monitor runs for free (chargeCeilingMicros 0)."),
134
+ offset: z.number().int().min(0).default(0),
135
+ limit: z.number().int().min(1).max(200).default(100),
136
+ }).strict()), annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false, idempotentHint: true } }, ({ query, freeOnly, offset, limit }) => respond(async () => {
137
+ const needle = query?.toLowerCase() ?? "";
138
+ const matching = (await client.monitorableTools()).data.tools.filter(tool => tool.name.includes(needle) && (!freeOnly || tool.chargeCeilingMicros === 0));
139
+ const tools = matching.slice(offset, offset + limit);
140
+ return { data: { tools, total: matching.length, nextOffset: offset + tools.length < matching.length ? offset + tools.length : null } };
141
+ }, undefined, (value, fits) => {
142
+ const tools = [...value.data.tools];
143
+ const page = () => ({ data: { tools, total: value.data.total, nextOffset: offset + tools.length < value.data.total ? offset + tools.length : null } });
144
+ while (tools.length && !fits(page()))
145
+ tools.pop();
146
+ return page();
147
+ }));
148
+ const customToolName = z.string().max(64).describe("The tool's name, custom__<slug> (the slug alone works too).");
149
+ const customToolDefinition = z.object({ ...platformCustomToolDefinitionSchema.shape,
150
+ inputSchema: z.record(z.string(), z.unknown()).describe(platformCustomToolDefinitionSchema.shape.inputSchema.description ?? "JSON Schema of the input object.") });
151
+ const definitionOf = (input) => platformCustomToolDefinitionSchema.parse(input);
152
+ server.registerTool("platform_custom_tools", { description: "List this project's custom tools: the team's own Python tools, each listed in the catalog as custom__<slug> and run like any tool (quote and execute). Free.",
153
+ inputSchema: forHosts(z.object({})), annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false, idempotentHint: true } }, () => respond(() => client.customTools()));
154
+ server.registerTool("platform_custom_tool", { description: "Read one custom tool's input schema, Python code and version, before running or editing it. Free.",
155
+ inputSchema: forHosts(z.object({ name: customToolName }).strict()), annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false, idempotentHint: true } }, ({ name }) => respond(() => client.customTool(name)));
156
+ server.registerTool("platform_custom_tool_create", { description: "Add a tool for the whole team: Python that Relvo runs in its isolated sandbox with the validated input as the dict `input`; print the result. It is listed as custom__<slug> and billed like run_python. Save a step the team repeats as a tool. No credentials exist in the sandbox: to use connected data, run that tool first and pass its execution in sourceExecutionIds.",
157
+ inputSchema: forHosts(customToolDefinition.extend({ slug: platformCustomToolSlugSchema }).strict()),
158
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false, idempotentHint: false } }, ({ slug, ...input }) => respond(() => client.createCustomTool({ slug, ...definitionOf(input) })));
159
+ server.registerTool("platform_custom_tool_update", { description: "Replace a custom tool's whole definition. Read it first and pass its version as expectedVersion; an edit made since is refused instead of lost. Every version is kept and can be restored. Only its creator or an admin may change it.",
160
+ inputSchema: forHosts(customToolDefinition.extend({ name: customToolName, expectedVersion: z.number().int().min(1) }).strict()),
161
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false, idempotentHint: false } }, ({ name, expectedVersion, ...input }) => respond(() => client.updateCustomTool(name, { ...definitionOf(input), expectedVersion })));
162
+ const endpointDefinition = platformCustomEndpointDefinitionSchema.omit({ kind: true });
163
+ server.registerTool("platform_custom_endpoint_create", { description: "Register an API endpoint for the whole team as custom__<slug>: a base URL plus the credentials Relvo adds to every request (each binding names a header or query parameter, a template with {secret}, and the secret). Agents then call the endpoint with method, path under the base URL, query, json or form; Relvo adds the credentials and never shows them, so pass each secret only here. No Relvo usage fee; the API bills its own account. The response never repeats a secret.",
164
+ inputSchema: forHosts(endpointDefinition.extend({ slug: platformCustomToolSlugSchema }).strict()),
165
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false, idempotentHint: false } }, ({ slug, ...input }) => respond(() => client.createCustomTool({ slug, ...endpointDefinition.parse(input), kind: "http" })));
166
+ server.registerTool("platform_custom_endpoint_update", { description: "Replace an API endpoint's whole definition. Read it first and pass its version as expectedVersion. Keep a saved binding's secret by passing the binding's id without a secret; a secret sent replaces it. If the base URL moves to another origin every binding needs its secret again.",
167
+ inputSchema: forHosts(endpointDefinition.extend({ name: customToolName, expectedVersion: z.number().int().min(1) }).strict()),
168
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false, idempotentHint: false } }, ({ name, expectedVersion, ...input }) => respond(() => client.updateCustomTool(name, { ...endpointDefinition.parse(input), kind: "http", expectedVersion })));
169
+ server.registerTool("platform_custom_tool_versions", { description: "List a custom tool's kept versions, newest first. Free.",
170
+ inputSchema: forHosts(z.object({ name: customToolName }).strict()), annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false, idempotentHint: true } }, ({ name }) => respond(() => client.customToolVersions(name)));
171
+ server.registerTool("platform_custom_tool_restore", { description: "Make an earlier version of a custom tool current again; this adds a new version, so it can be undone. Pass the current version as expectedVersion.",
172
+ inputSchema: forHosts(platformCustomToolRestoreSchema.extend({ name: customToolName }).strict()), annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false, idempotentHint: false } }, ({ name, ...input }) => respond(() => client.restoreCustomTool(name, input)));
173
+ server.registerTool("platform_custom_tool_delete", { description: "Delete a custom tool and its versions for the whole team. Only its creator or an admin may delete it.",
174
+ inputSchema: forHosts(z.object({ name: customToolName }).strict()), annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false, idempotentHint: true } }, ({ name }) => respond(() => client.deleteCustomTool(name)));
175
+ const teamSkillSlug = platformTeamSkillCreateSchema.shape.slug;
176
+ server.registerTool("platform_team_skills", { description: "List this project's team skills: procedures the team's agents wrote for each other, each with when to use it. Before a Relvo task, read one whose description fits it and follow it within the user's request. Free.",
177
+ inputSchema: forHosts(z.object({})), annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false, idempotentHint: true } }, () => respond(() => client.teamSkills()));
178
+ server.registerTool("platform_team_skill", { description: "Read one team skill's steps and version. Free.",
179
+ inputSchema: forHosts(z.object({ slug: teamSkillSlug }).strict()), annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false, idempotentHint: true } }, ({ slug }) => respond(() => client.teamSkill(slug)));
180
+ server.registerTool("platform_team_skill_create", { description: "Write down, for the whole team, how a Relvo task was done once it worked: when to use it, the steps, the tools by name and what to check. Other members' agents follow it next time. Check the list first and improve an existing skill instead of adding a near copy. No secrets or private code or file contents. Free.",
181
+ inputSchema: forHosts(platformTeamSkillCreateSchema), annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false, idempotentHint: false } }, input => respond(() => client.createTeamSkill(input)));
182
+ server.registerTool("platform_team_skill_update", { description: "Improve a team skill when following it showed a missing step, a better tool or a mistake. Read it first, send the whole new text and its version as expectedVersion; an edit made since is refused instead of lost. Every version is kept. Free.",
183
+ inputSchema: forHosts(platformTeamSkillDefinitionSchema.extend({ slug: teamSkillSlug, expectedVersion: platformTeamSkillUpdateSchema.shape.expectedVersion }).strict()),
184
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false, idempotentHint: false } }, ({ slug, ...input }) => respond(() => client.updateTeamSkill(slug, input)));
185
+ server.registerTool("platform_team_skill_versions", { description: "List a team skill's kept versions, newest first. Free.",
186
+ inputSchema: forHosts(z.object({ slug: teamSkillSlug }).strict()), annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false, idempotentHint: true } }, ({ slug }) => respond(() => client.teamSkillVersions(slug)));
187
+ server.registerTool("platform_team_skill_restore", { description: "Make an earlier version of a team skill current again; this adds a new version, so it can be undone. Pass the current version as expectedVersion. Free.",
188
+ inputSchema: forHosts(platformTeamSkillRestoreSchema.extend({ slug: teamSkillSlug }).strict()), annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false, idempotentHint: false } }, ({ slug, ...input }) => respond(() => client.restoreTeamSkill(slug, input)));
189
+ server.registerTool("platform_team_skill_delete", { description: "Delete a team skill and its versions for the whole team. Only its creator or an admin may delete it.",
190
+ inputSchema: forHosts(z.object({ slug: teamSkillSlug }).strict()), annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false, idempotentHint: true } }, ({ slug }) => respond(() => client.deleteTeamSkill(slug)));
191
+ server.registerTool("platform_suggestions", { description: "Repeated work in the last 14 days: the same Python your keys ran again and again, with its code (save it with platform_custom_tool_create, taking the values that change as input), and the same tool sequence in several sessions of this project (write it down with platform_team_skill_create). Check it at the end of a Relvo task. Free.",
192
+ inputSchema: forHosts(z.object({})), annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false, idempotentHint: true } }, () => respond(() => client.suggestions()));
193
+ server.registerTool("platform_team_access", { description: "List the team's members and pending invitations with what each may use of the team's own connections, custom tools, skills and secrets: `toolAccess` null is everything, a list names `connection:<id>`, `connection:<id>/<tool>`, `connection:<id>/reads`, `custom__<slug>`, `skill:<projectId>/<slug>` and `secret:<projectId>/<NAME>`, and hides what it does not name. Needs a key of the team's owner or an admin made with access:write. Free.",
194
+ inputSchema: forHosts(z.object({}).strict()), annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false, idempotentHint: true } }, () => respond(() => client.access()));
195
+ server.registerTool("platform_member_access_update", { description: "Replace one member's list of the team's connections, connection operations, custom tools, skills and secrets they may use, with the whole new list in the grammar platform_team_access shows; \"all\" gives every one, those added later included. Read the current list first. An admin changes no admin's or owner's list and gives nothing they do not hold; the member's role is never changed. Needs a key of the team's owner or an admin made with access:write.",
196
+ inputSchema: forHosts(z.object({ memberId: z.string().regex(/^[A-Za-z0-9-]{1,128}$/), toolAccess: z.union([z.literal("all"), z.array(z.string().max(200)).max(20_000)]) }).strict()),
197
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false, idempotentHint: true } }, ({ memberId, toolAccess }) => respond(() => client.updateMemberAccess(memberId, { toolAccess: toolAccess === "all" ? null : toolAccess })));
198
+ const workflowDefinition = z.object({ ...platformWorkflowDefinitionSchema.shape,
199
+ inputSchema: z.record(z.string(), z.unknown()).describe(platformWorkflowDefinitionSchema.shape.inputSchema.description ?? "JSON Schema of the run inputs."),
200
+ steps: z.array(z.object({ ...platformWorkflowStepSchema.shape, arguments: z.record(z.string(), z.unknown()).describe(platformWorkflowStepSchema.shape.arguments.description ?? "The tool's arguments.") }).strict())
201
+ .min(1).describe(platformWorkflowDefinitionSchema.shape.steps.description ?? "Tool calls in order.") }).strict();
202
+ const workflowDefinitionOf = (input) => platformWorkflowDefinitionSchema.parse(input);
203
+ const workflowSlug = z.object({ slug: platformWorkflowSlugSchema }).strict();
204
+ const workflowRun = z.object({ id: platformWorkflowRunIdSchema }).strict();
205
+ server.registerTool("platform_workflows", { description: "List this project's saved workflows: tool calls in a fixed order that a run repeats with new inputs and a budget. Free.",
206
+ inputSchema: forHosts(z.object({})), annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false, idempotentHint: true } }, () => respond(() => client.workflows()));
207
+ server.registerTool("platform_workflow", { description: "Read one workflow's input schema, steps and version. Free.",
208
+ inputSchema: forHosts(workflowSlug), annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false, idempotentHint: true } }, ({ slug }) => respond(() => client.workflow(slug)));
209
+ server.registerTool("platform_workflow_create", { description: "Save, for the whole team, a sequence of tool calls that worked so it can be run again: an input schema and steps, each a catalog tool with its arguments. In arguments, {\"$input\":\"name\"} takes a run input and {\"$step\":\"id\",\"path\":\"/data/0/url\"} a value of an earlier step's output (a JSON Pointer). Saving buys nothing. Free.",
210
+ inputSchema: forHosts(workflowDefinition.extend({ slug: platformWorkflowSlugSchema }).strict()), annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false, idempotentHint: false } }, ({ slug, ...input }) => respond(() => client.createWorkflow({ slug, ...workflowDefinitionOf(input) })));
211
+ server.registerTool("platform_workflow_update", { description: "Replace a workflow's whole definition. Read it first and pass its version as expectedVersion; an edit made since is refused instead of lost. Runs keep the version they started with. Only its creator or an admin may change it. Free.",
212
+ inputSchema: forHosts(workflowDefinition.extend({ slug: platformWorkflowSlugSchema, expectedVersion: z.number().int().min(1) }).strict()),
213
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false, idempotentHint: false } }, ({ slug, expectedVersion, ...input }) => respond(() => client.updateWorkflow(slug, { ...workflowDefinitionOf(input), expectedVersion })));
214
+ server.registerTool("platform_workflow_versions", { description: "List a workflow's kept versions, newest first. Free.",
215
+ inputSchema: forHosts(workflowSlug), annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false, idempotentHint: true } }, ({ slug }) => respond(() => client.workflowVersions(slug)));
216
+ server.registerTool("platform_workflow_delete", { description: "Delete a workflow and its versions for the whole team; its runs stay readable. Only its creator or an admin may delete it.",
217
+ inputSchema: forHosts(workflowSlug), annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false, idempotentHint: true } }, ({ slug }) => respond(() => client.deleteWorkflow(slug)));
218
+ server.registerTool("platform_workflow_run", { description: "Run a saved workflow. Relvo buys each step in order as an ordinary execution under this key and stops before a step whose price no longer fits what is left of maximumChargeMicros. Keep idempotencyKey: repeating it returns the same run instead of buying again. Read progress with platform_workflow_run_status and the result with the execution in resultExecutionId.",
219
+ inputSchema: forHosts(platformWorkflowRunCreateSchema.extend({ slug: platformWorkflowSlugSchema, inputs: z.record(z.string(), z.unknown()).default({}).describe("Values for the workflow's input schema."),
220
+ idempotencyKey: z.string().regex(/^[A-Za-z0-9._:-]{8,128}$/) }).strict()),
221
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true, idempotentHint: true } }, ({ slug, idempotencyKey, ...input }) => respond(() => client.runWorkflow(slug, platformWorkflowRunCreateSchema.parse(input), idempotencyKey)));
222
+ server.registerTool("platform_workflow_runs", { description: "List a workflow's runs, newest first, with status and charge. Free.",
223
+ inputSchema: forHosts(platformWorkflowRunListQuerySchema.extend({ slug: platformWorkflowSlugSchema }).strict()), annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false, idempotentHint: true } }, ({ slug, ...query }) => respond(() => client.workflowRuns(slug, query)));
224
+ server.registerTool("platform_workflow_run_status", { description: "Read a workflow run: its version, each step's execution, status and charge, and resultExecutionId once it succeeded. Free.",
225
+ inputSchema: forHosts(workflowRun), annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false, idempotentHint: true } }, ({ id }) => respond(() => client.workflowRun(id)));
226
+ server.registerTool("platform_workflow_run_cancel", { description: "Cancel a workflow run: its running step is asked to stop and no later step is bought. Only the member who started it or an admin may cancel it.",
227
+ inputSchema: forHosts(workflowRun), annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false, idempotentHint: true } }, ({ id }) => respond(() => client.cancelWorkflowRun(id)));
228
+ const readTriggerOnly = { readOnlyHint: true, destructiveHint: false, openWorldHint: false, idempotentHint: true };
229
+ const triggerId = platformTriggerIdSchema;
230
+ server.registerTool("platform_triggers", { description: "List this project's triggers: events from a connected app, delivered as they happen. Shows kind (poll or webhook), app, status (failed when Relvo stopped it, see statusReason), last check/event, last error and destinations. Free.",
231
+ inputSchema: forHosts(z.object({})), annotations: readTriggerOnly }, () => respond(() => client.triggers()));
232
+ server.registerTool("platform_trigger_types", { description: "For one app, list the webhook trigger types (type, configSchema, available) and a page of its read operations a poll trigger can run (tool, inputSchema). Page with offset while offset + limit < total. Free.",
233
+ inputSchema: forHosts(platformTriggerTypesQuerySchema), annotations: readTriggerOnly }, input => respond(() => client.triggerTypes(input)));
234
+ server.registerTool("platform_trigger_create", { description: "Get events from a connected app. kind:poll runs a read operation (tool, arguments without connectionId) every interval and reports what is new; the first run is a baseline. Each poll is one execution that counts toward the plan's free runs and is charged beyond them, up to maximumChargeMicrosPerPoll. kind:webhook registers type (from platform_trigger_types) with the app and needs config valid against its configSchema. The trigger runs as this API key. Read events with platform_trigger_events. destinations: webhook (signing secret shown once), slack, discord or workflow {workflow, input, maximumChargeMicros}; email is console only. Get the user's approval before creating a poll trigger.",
235
+ inputSchema: forHosts(z.object({
236
+ kind: z.enum(["poll", "webhook"]),
237
+ connectionId: z.string().describe("The connection of the app, from platform_connections."),
238
+ name: z.string().optional().describe("Omit to name it after its type or operation."),
239
+ tool: z.string().optional().describe("poll only: a read operation of the app, for example github__issues_list_for_repo."),
240
+ arguments: z.record(z.string(), z.unknown()).optional().describe("poll only: the operation's arguments without connectionId."),
241
+ interval: platformTriggerIntervalSchema.optional().describe("poll only. Omit for 5m."),
242
+ itemsPath: z.string().optional().describe("poll only: JSON Pointer to the list in the response, for example /data/items. Omit to use the largest list."),
243
+ maximumChargeMicrosPerPoll: z.number().int().min(0).optional().describe("poll only: the most one poll may charge. Omit for the default."),
244
+ type: z.string().optional().describe("webhook only: a type from platform_trigger_types, for example github.push."),
245
+ config: z.record(z.string(), z.unknown()).optional().describe("webhook only: the type's settings."),
246
+ destinations: z.array(platformTriggerDestinationInputSchema).optional().describe("Where each event is sent. Omit for none; read events with platform_trigger_events."),
247
+ }).strict()), annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true, idempotentHint: false } }, input => respond(async () => client.createTrigger(platformTriggerCreateSchema.parse(Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined && value !== ""))))));
248
+ server.registerTool("platform_trigger_update", { description: "Enable or disable a trigger, or rename it. A failed trigger is checked again when enabled. Omitted fields stay as they are.",
249
+ inputSchema: forHosts(z.object({ id: triggerId, enabled: z.boolean().optional(), name: z.string().optional().describe("Empty or omitted keeps the name.") }).strict()),
250
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true, idempotentHint: true } }, ({ id, ...input }) => respond(() => client.updateTrigger(id, platformTriggerUpdateSchema.parse(Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined && value !== ""))))));
251
+ server.registerTool("platform_trigger_delete", { description: "Delete one trigger of this project; a webhook is removed from the app. Its kept events go with it. Free.",
252
+ inputSchema: forHosts(z.object({ id: triggerId }).strict()), annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true, idempotentHint: true } }, ({ id }) => respond(() => client.deleteTrigger(id)));
253
+ server.registerTool("platform_trigger_events", { description: "Read trigger events oldest first. Omit triggerId for every trigger. Pass the returned nextAfter as after to read only newer events. Free.",
254
+ inputSchema: forHosts(platformTriggerEventsQuerySchema.extend({ triggerId: triggerId.optional() }).strict()), annotations: readTriggerOnly }, input => respond(() => client.triggerEvents(input)));
98
255
  server.registerTool("platform_connections", {
99
256
  description: "Page project OAuth IDs and account labels for a tool's connectionId. Follow nextOffset. No tokens or account changes. Empty scopes do not establish lack of access; reauth_required needs console consent.",
100
- inputSchema: platformConnectionCatalogQuerySchema,
257
+ inputSchema: forHosts(platformConnectionCatalogQuerySchema),
101
258
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false, idempotentHint: true },
102
259
  }, ({ provider, offset, limit }) => respond(async () => {
103
260
  const catalog = (await client.connections({ provider, offset, limit })).data;
@@ -111,42 +268,39 @@ export function createPlatformMcp(client, version = "0.1.2", localFilesystem = f
111
268
  throw new Error("This connection's metadata exceeds the context budget. Inspect it in the Relvo console.");
112
269
  return page();
113
270
  }));
114
- server.registerTool("platform_tools", { description: "Find a capability or exact tool/model name; filter quotes, funding, unit, category or provider. summary:false with limit:1 can include its schema; reuse it, inspecting omitted definitions with platform_tool. Example quotes need actual-input requoting. Free. No match does not imply missing OAuth; provider does not pin execution.",
115
- inputSchema: toolDiscoveryInputSchema, annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false, idempotentHint: true } }, input => respond(() => client.searchTools(input), undefined, (value, fits) => {
116
- const tools = value.data.tools.map(summary);
117
- const response = () => ({ data: { ...value.data, tools, discovery: { ...value.data.discovery, returnedCount: tools.length, truncated: true } } });
118
- while (tools.length && !fits(response()))
119
- tools.pop();
120
- return response();
121
- }));
271
+ server.registerTool("platform_tools", { description: "Find a capability or exact tool/model name; filter quotes, funding, unit, category or provider. summary:false with limit:1 can include its schema; reuse it, inspecting omitted definitions with platform_tool. Example quotes need actual-input requoting. Counts toward the team's monthly free calls, then is charged. No match does not imply missing OAuth; provider does not pin execution.",
272
+ inputSchema: forHosts(toolDiscoveryInputSchema), annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false, idempotentHint: true } }, input => respond(() => client.searchTools(input), undefined, compactToolDiscovery));
122
273
  server.registerTool("platform_quote", { description: "Quote exact tool arguments, optional provider and session without executing. Auto is default. Retain priceVersion, maximumChargeMicros and the unchanged selection for submission within the user's budget.",
123
- inputSchema: platformToolInputSchema, annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false, idempotentHint: true } }, (input) => respond(() => client.quote(input)));
274
+ inputSchema: forHosts(platformQuoteRequestSchema), annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false, idempotentHint: true } }, (input) => respond(() => client.quote(input)));
124
275
  server.registerTool("platform_execute", { description: "Enqueue an authorized paid call using its accepted quote and a saved idempotencyKey. Auto is default. Wait on the returned ID. Retry uncertain admission with identical input/key to recover the same purchase.",
125
- inputSchema: platformCreateExecutionSchema.extend({ idempotencyKey: z.string().regex(/^[A-Za-z0-9._:-]{8,128}$/) }),
276
+ inputSchema: forHosts(platformCreateExecutionSchema.extend({ idempotencyKey: z.string().regex(/^[A-Za-z0-9._:-]{8,128}$/) }), json => {
277
+ const { runRequestHash: _receiptMetadata, ...properties } = jsonObject.parse(json.properties);
278
+ return { ...json, properties };
279
+ }),
126
280
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true, idempotentHint: true } }, ({ idempotencyKey, ...input }) => respond(async () => executionReceiptContext(await client.createExecution(input, idempotencyKey)), "platform_execute", value => executionReceiptContext(value, projectionOptions)));
127
281
  server.registerTool("platform_execution", { description: "Read status, charges and a bounded result preview. Use platform_result for offloaded content. Pending zero charges are not final. needs_review requires operator reconciliation, not replacement work.",
128
- inputSchema: z.object({ id: platformExecutionIdSchema }).strict(),
282
+ inputSchema: forHosts(z.object({ id: platformExecutionIdSchema }).strict()),
129
283
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true, idempotentHint: true } }, ({ id }) => respond(() => client.executionContext(id), "platform_execution", value => executionReceiptContext(value, projectionOptions)));
130
284
  server.registerTool("platform_result", { description: "Read saved results without regenerating. Select a path, page and optional row fields. Pages fit the response budget automatically; follow nextOffset. An offloaded single value needs a narrower path. Process bulk data in an available remote sandbox.",
131
- inputSchema: z.object({ id: platformExecutionIdSchema, ...toolResultSelectionSchema.shape }).strict(),
285
+ inputSchema: forHosts(z.object({ id: platformExecutionIdSchema, ...toolResultSelectionSchema.shape }).strict()),
132
286
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true, idempotentHint: true } }, ({ id, ...selection }) => respond(() => client.readExecutionResult(id, selection), "platform_result", (value, fits) => {
133
287
  const fitted = { data: { ...value.data, page: fitToolResultPage(value.data.page, page => fits({ data: { ...value.data, page } })) } };
134
288
  return fits(fitted) ? fitted : executionPageContext(value, projectionOptions);
135
289
  }));
136
290
  server.registerTool("platform_cancel", { description: "Request cancellation of this execution's unstarted work. Already incurred provider costs may still be charged. Requires ownership of the execution or project ADMIN access.",
137
- inputSchema: z.object({ id: platformExecutionIdSchema }).strict(),
291
+ inputSchema: forHosts(z.object({ id: platformExecutionIdSchema }).strict()),
138
292
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true, idempotentHint: true } }, ({ id }) => respond(async () => executionReceiptContext(await client.cancelExecution(id)), "platform_cancel", value => executionReceiptContext(value, projectionOptions)));
139
293
  server.registerTool("platform_artifact_url", { description: "Get a completed execution's private output download URL, including generated media and downloaded social videos, valid for at most 5 minutes. Use execution and artifact IDs from platform_execution. Treat the URL as temporary access to private data; share it only within the caller's authorized scope.",
140
- inputSchema: z.object({ id: platformExecutionIdSchema, artifactId: platformArtifactIdSchema }).strict(),
294
+ inputSchema: forHosts(z.object({ id: platformExecutionIdSchema, artifactId: platformArtifactIdSchema }).strict()),
141
295
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false, idempotentHint: true } }, ({ id, artifactId }) => respond(() => client.artifactDownload(id, artifactId)));
142
296
  server.registerTool("platform_tool", { description: "Inspect one discovered tool's schema, example and price. Small definitions are complete; large ones return page.entries with exact paths. Read entries without value at their path; follow nextOffset for siblings. Free.",
143
- inputSchema: z.object({ name: z.string().regex(/^[a-z][a-z0-9_]{1,63}$/).describe("Exact discovered tool name."), ...inspectInput }).strict(),
297
+ inputSchema: forHosts(z.object({ name: z.string().regex(/^[a-z][a-z0-9_]{1,63}$/).describe("Exact discovered tool name."), ...inspectInput }).strict()),
144
298
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false, idempotentHint: true } }, ({ name, ...selection }) => respond(async () => {
145
299
  const value = await client.inspectTool(name);
146
300
  return inspectJsonContext(name, jsonObject.parse(value.data), selection, mcpContextFits);
147
301
  }));
148
302
  server.registerTool("platform_executions", { description: "Recover prior project jobs, newest first. Follow nextBefore; summaries omit output. Inspect a known execution ID instead of purchasing replacement work. Free.",
149
- inputSchema: platformExecutionListQuerySchema,
303
+ inputSchema: forHosts(platformExecutionListQuerySchema),
150
304
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false, idempotentHint: true } }, input => respond(() => client.executions(input), undefined, (value, fits) => {
151
305
  const executions = [...value.data.executions];
152
306
  const page = () => ({ data: { executions, nextBefore: executions.at(-1)?.id ?? value.data.nextBefore } });
@@ -157,8 +311,8 @@ export function createPlatformMcp(client, version = "0.1.2", localFilesystem = f
157
311
  return page();
158
312
  }));
159
313
  server.registerTool("platform_wait", { description: "Wait for an existing job, up to 25 seconds per call. timedOut leaves it running: wait again on the same ID. Terminal failures include status/charges and isError. Free; never submits or cancels.",
160
- inputSchema: z.object({ id: platformExecutionIdSchema.describe("Execution ID from platform_execute or platform_executions."),
161
- timeoutSeconds: z.number().int().min(1).max(25).default(20).describe("Wait limit, 1–25 seconds; accepted work continues afterwards.") }).strict(),
314
+ inputSchema: forHosts(z.object({ id: platformExecutionIdSchema.describe("Execution ID from platform_execute or platform_executions."),
315
+ timeoutSeconds: z.number().int().min(1).max(25).default(20).describe("Wait limit, 1–25 seconds; accepted work continues afterwards.") }).strict()),
162
316
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true, idempotentHint: true } }, async ({ id, timeoutSeconds }, { mcpReq }) => {
163
317
  let failed = false;
164
318
  const result = await respond(async () => {
@@ -169,9 +323,9 @@ export function createPlatformMcp(client, version = "0.1.2", localFilesystem = f
169
323
  return failed ? { ...result, isError: true } : result;
170
324
  });
171
325
  server.registerTool("platform_skills", { description: "List current first-party guide summaries. Inspect only a needed guide with platform_skill. Guides do not grant permissions or budget. Oversized full lists return summaries.",
172
- inputSchema: z.object({ summary: z.boolean().default(true) }).strict(), annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false, idempotentHint: true } }, ({ summary }) => respond(async () => { const { data } = await client.skills(); return { data: { version: data.version, skills: summary ? data.skills.map(({ content, ...skill }) => skill) : data.skills } }; }, undefined, value => ({ data: { ...value.data, skills: value.data.skills.map(skill => ({ name: skill.name, description: skill.description, sha256: skill.sha256 })), offloaded: true, readWith: "platform_skill" } })));
326
+ inputSchema: forHosts(z.object({ summary: z.boolean().default(true) }).strict()), annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false, idempotentHint: true } }, ({ summary }) => respond(async () => { const { data } = await client.skills(); return { data: { version: data.version, skills: summary ? data.skills.map(({ content, ...skill }) => skill) : data.skills } }; }, undefined, value => ({ data: { ...value.data, skills: value.data.skills.map(skill => ({ name: skill.name, description: skill.description, sha256: skill.sha256 })), offloaded: true, readWith: "platform_skill" } })));
173
327
  server.registerTool("platform_skill", { description: "Read one needed first-party guide. Large guides return paths/pages like platform_tool. Current endpoint names come from discovery; guides do not authorize paid work.",
174
- inputSchema: z.object({ name: platformSkillSchema.shape.name, ...inspectInput }).strict(), annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false, idempotentHint: true } }, ({ name, ...selection }) => respond(async () => {
328
+ inputSchema: forHosts(z.object({ name: platformSkillSchema.shape.name, ...inspectInput }).strict()), annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false, idempotentHint: true } }, ({ name, ...selection }) => respond(async () => {
175
329
  const value = await client.skill(name);
176
330
  return inspectJsonContext(name, jsonObject.parse(value.data), selection, mcpContextFits);
177
331
  }));
@@ -0,0 +1 @@
1
+ export const PLATFORM_MCP_VERSION = "0.1.3";
@@ -0,0 +1 @@
1
+ export const ACCOUNT_REQUEST_HTTP_METHODS = ["GET", "HEAD", "OPTIONS", "POST", "PUT", "PATCH", "DELETE"];
@@ -0,0 +1,150 @@
1
+ const STRICT_CRON_RE = /^([\*0-9A-Za-z,\-\/]+)\s+([\*0-9A-Za-z,\-\/]+)\s+([\*0-9A-Za-z,\-\/]+)\s+([\*0-9A-Za-z,\-\/]+)\s+([\*0-9A-Za-z,\-\/]+)$/;
2
+ export function isStrictCron5Field(cron) {
3
+ if (typeof cron !== "string")
4
+ return false;
5
+ return STRICT_CRON_RE.test(cron.trim());
6
+ }
7
+ const RANGES = {
8
+ minute: { min: 0, max: 59 },
9
+ hour: { min: 0, max: 23 },
10
+ dayOfMonth: { min: 1, max: 31 },
11
+ month: { min: 1, max: 12 },
12
+ dayOfWeek: { min: 0, max: 6 },
13
+ };
14
+ const MONTH_NAMES = {
15
+ JAN: 1, FEB: 2, MAR: 3, APR: 4, MAY: 5, JUN: 6,
16
+ JUL: 7, AUG: 8, SEP: 9, OCT: 10, NOV: 11, DEC: 12,
17
+ };
18
+ const DOW_NAMES = {
19
+ SUN: 0, MON: 1, TUE: 2, WED: 3, THU: 4, FRI: 5, SAT: 6,
20
+ };
21
+ function normalizeToken(token, names) {
22
+ if (!names)
23
+ return token;
24
+ const upper = token.trim().toUpperCase();
25
+ return upper in names ? String(names[upper]) : token;
26
+ }
27
+ function expandField(field, range, names) {
28
+ const out = new Set();
29
+ for (const part of field.split(",")) {
30
+ const stepMatch = part.match(/^(.+)\/(\d+)$/);
31
+ let base = part;
32
+ let step = 1;
33
+ if (stepMatch) {
34
+ base = stepMatch[1];
35
+ step = Number.parseInt(stepMatch[2], 10);
36
+ if (!Number.isFinite(step) || step <= 0)
37
+ return null;
38
+ }
39
+ let lo;
40
+ let hi;
41
+ if (base === "*") {
42
+ lo = range.min;
43
+ hi = range.openMax ?? range.max;
44
+ }
45
+ else if (base.includes("-")) {
46
+ const [a, b] = base.split("-");
47
+ lo = Number.parseInt(normalizeToken(a ?? "", names), 10);
48
+ hi = Number.parseInt(normalizeToken(b ?? "", names), 10);
49
+ if (!Number.isFinite(lo) || !Number.isFinite(hi))
50
+ return null;
51
+ }
52
+ else {
53
+ const v = Number.parseInt(normalizeToken(base, names), 10);
54
+ if (!Number.isFinite(v))
55
+ return null;
56
+ lo = v;
57
+ hi = stepMatch ? (range.openMax ?? range.max) : v;
58
+ }
59
+ if (lo < range.min || hi > range.max || lo > hi)
60
+ return null;
61
+ for (let i = lo; i <= hi; i += step)
62
+ out.add(i);
63
+ }
64
+ return out.size > 0 ? out : null;
65
+ }
66
+ export function parseCron(cron) {
67
+ if (!isStrictCron5Field(cron))
68
+ return null;
69
+ const [minute, hour, dom, month, dow] = cron.trim().split(/\s+/);
70
+ const minutes = expandField(minute, RANGES.minute);
71
+ const hours = expandField(hour, RANGES.hour);
72
+ const daysOfMonth = expandField(dom, RANGES.dayOfMonth);
73
+ const months = expandField(month, RANGES.month, MONTH_NAMES);
74
+ const dowRaw = expandField(dow, { min: RANGES.dayOfWeek.min, max: 7, openMax: RANGES.dayOfWeek.max }, DOW_NAMES);
75
+ const dowExpanded = dowRaw && new Set([...dowRaw].map((d) => (d === 7 ? 0 : d)));
76
+ if (!minutes || !hours || !daysOfMonth || !months || !dowExpanded)
77
+ return null;
78
+ return {
79
+ minutes,
80
+ hours,
81
+ daysOfMonth,
82
+ months,
83
+ daysOfWeek: dowExpanded,
84
+ dayRestrictionOr: dom !== "*" && dow !== "*",
85
+ };
86
+ }
87
+ export function isValidCron(cron) {
88
+ if (typeof cron !== "string")
89
+ return false;
90
+ return parseCron(cron) !== null;
91
+ }
92
+ export function isCronTooFrequent(cron, minIntervalMinutes) {
93
+ const fields = cron.trim().split(/\s+/);
94
+ if (fields.length !== 5)
95
+ return true;
96
+ const minutes = expandField(fields[0], RANGES.minute);
97
+ const hours = expandField(fields[1], RANGES.hour);
98
+ if (!minutes || !hours)
99
+ return true;
100
+ const slots = [];
101
+ for (const h of hours) {
102
+ for (const m of minutes)
103
+ slots.push(h * 60 + m);
104
+ }
105
+ slots.sort((a, b) => a - b);
106
+ for (let i = 1; i < slots.length; i++) {
107
+ if (slots[i] - slots[i - 1] < minIntervalMinutes)
108
+ return true;
109
+ }
110
+ return slots[0] + 1440 - slots[slots.length - 1] < minIntervalMinutes;
111
+ }
112
+ function matches(parsed, d) {
113
+ if (!parsed.minutes.has(d.getMinutes()))
114
+ return false;
115
+ if (!parsed.hours.has(d.getHours()))
116
+ return false;
117
+ if (!parsed.months.has(d.getMonth() + 1))
118
+ return false;
119
+ const domMatch = parsed.daysOfMonth.has(d.getDate());
120
+ const dowMatch = parsed.daysOfWeek.has(d.getDay());
121
+ if (parsed.dayRestrictionOr)
122
+ return domMatch || dowMatch;
123
+ return domMatch && dowMatch;
124
+ }
125
+ export function findNextFires(cron, from, deadline, max = 30) {
126
+ const parsed = parseCron(cron);
127
+ if (!parsed)
128
+ return [];
129
+ if (!(deadline.getTime() > from.getTime()))
130
+ return [];
131
+ const fires = [];
132
+ const cursor = new Date(from.getTime());
133
+ cursor.setSeconds(0, 0);
134
+ cursor.setTime(cursor.getTime() + 60_000);
135
+ const HARD_CAP_MINUTES = 370 * 24 * 60;
136
+ let ticks = 0;
137
+ while (cursor.getTime() <= deadline.getTime() && ticks < HARD_CAP_MINUTES) {
138
+ if (matches(parsed, cursor)) {
139
+ fires.push(new Date(cursor.getTime()));
140
+ if (fires.length >= max)
141
+ break;
142
+ }
143
+ cursor.setTime(cursor.getTime() + 60_000);
144
+ ticks++;
145
+ }
146
+ return fires;
147
+ }
148
+ export function cronFiresBeforeDeadline(cron, deadline, from = new Date()) {
149
+ return findNextFires(cron, from, deadline, 1).length > 0;
150
+ }
@@ -1,13 +1,13 @@
1
- export const PLATFORM_KEY_PREFIXES = { noimos: "nmp_", relvo: "rlv_", toolhub: "thk_" };
2
- export const PLATFORM_API_KEY_PATTERN = /^(?:nmp|rlv|thk)_[A-Za-z0-9_-]{43}$/;
3
- export const PLATFORM_CLIENT_HEADERS = { noimos: "x-noimos-client", relvo: "x-relvo-client", toolhub: "x-toolhub-client" };
1
+ export const PLATFORM_KEY_PREFIXES = { noimos: "nmp_", relvo: "rlv_" };
2
+ export const PLATFORM_API_KEY_PATTERN = /^(?:nmp|rlv)_[A-Za-z0-9_-]{43}$/;
3
+ export const PLATFORM_CLIENT_HEADERS = { noimos: "x-noimos-client", relvo: "x-relvo-client" };
4
4
  export const PLATFORM_PRODUCT_NAMES = { noimos: "NoimosAI Platform", relvo: "Relvo" };
5
5
  export function isRelvoProduct(product) {
6
- return product === "relvo" || product === "toolhub";
6
+ return product === "relvo";
7
7
  }
8
8
  export function platformKeyProduct(key) {
9
9
  for (const product of Object.keys(PLATFORM_KEY_PREFIXES))
10
10
  if (key.startsWith(PLATFORM_KEY_PREFIXES[product]))
11
- return product === "toolhub" ? "relvo" : product;
11
+ return product;
12
12
  return null;
13
13
  }
@@ -0,0 +1,3 @@
1
+ export const PLATFORM_CHECKOUT_MIN_CENTS = 500;
2
+ export const PLATFORM_SETTLEMENT_MIN_CENTS = 100;
3
+ export const PLATFORM_FREE_TOOL_OVERAGE_MICROS = 300;