@sellable/mcp 0.1.754 → 0.1.755

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.
package/dist/server.js CHANGED
@@ -8,6 +8,7 @@ import { compileAgentToolPolicy, createAgentMcpAuthorizationHandlers, } from "./
8
8
  import { getConfig, getConfiguredCredentialIntent } from "./auth.js";
9
9
  import { MCP_RUNTIME_IDENTITY } from "./runtime-identity.js";
10
10
  import { getSkillByName, listSkills } from "./skills.js";
11
+ import { toMcpToolResult } from "./tool-result-envelope.js";
11
12
  import { getAuthStatus } from "./tools/auth.js";
12
13
  import { handleAddColumn, handleCommitBlueprint, } from "./tools/blueprint-commit.js";
13
14
  import { bootstrapCreateCampaign } from "./tools/bootstrap.js";
@@ -63,7 +64,6 @@ import { attachRecommendedSequence, attachSequence, createWorkflowTable, } from
63
64
  import { setupEvergreenCampaigns } from "./tools/setup-evergreen-campaigns.js";
64
65
  import { exportTableCsv, listTables } from "./tools/tables.js";
65
66
  import { handleVerifyTableRow } from "./tools/verify-row.js";
66
- import { sanitizeWatchUrlsForMcpResult } from "./tools/watch-url-security.js";
67
67
  import { getCampaignWaterfall, setCampaignWaterfallOrder, } from "./tools/waterfalls.js";
68
68
  import { exportWorkspaceCsv } from "./tools/workspace-export.js";
69
69
  import { addTeammate, createWorkspace, getActiveWorkspace, getWorkspace, listWorkspaces, setActiveWorkspace, } from "./tools/workspaces.js";
@@ -134,11 +134,6 @@ function formatSubskillPromptText(result) {
134
134
  : "";
135
135
  return `${header}${result.prompt}${footer}`;
136
136
  }
137
- function isStructuredToolResult(result) {
138
- return (!!result &&
139
- typeof result === "object" &&
140
- Array.isArray(result.content));
141
- }
142
137
  const agentApprovalPort = new HttpAgentApprovalEffectPort();
143
138
  const agentMcpAuthorization = createAgentMcpAuthorizationHandlers({
144
139
  allTools,
@@ -901,18 +896,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
901
896
  default:
902
897
  throw new Error(`Unknown tool: ${name}`);
903
898
  }
904
- const safeResult = sanitizeWatchUrlsForMcpResult(result);
905
- if (isStructuredToolResult(safeResult)) {
906
- return safeResult;
907
- }
908
- return {
909
- content: [
910
- {
911
- type: "text",
912
- text: JSON.stringify(safeResult),
913
- },
914
- ],
915
- };
899
+ // A tool that DECLARES an `outputSchema` must answer with `structuredContent`,
900
+ // or the client discards the whole response. `toMcpToolResult` owns that,
901
+ // keyed on the tool's own declaration. See tool-result-envelope.ts.
902
+ return toMcpToolResult(name, result);
916
903
  }
917
904
  catch (error) {
918
905
  const message = error instanceof Error ? error.message : "Unknown error";
@@ -0,0 +1,34 @@
1
+ /**
2
+ * THE MCP WIRE RESULT. One place shapes it, because the CONTRACT that governs it is
3
+ * declared per tool and enforced by the CLIENT, not by us.
4
+ *
5
+ * A tool that declares an `outputSchema` and answers without `structuredContent` has
6
+ * its ENTIRE response thrown away by the client, whatever the server actually said:
7
+ *
8
+ * - python `mcp/client/session.py`: `if not result.isError:
9
+ * _validate_tool_result(...)`, which raises
10
+ * `Tool <name> has an output schema but did not return structured content`;
11
+ * - typescript `@modelcontextprotocol/sdk` `client/index.js:499-520`: the same
12
+ * rule, then ajv validation of the payload against that schema.
13
+ *
14
+ * That is not a theoretical hazard. It took the three `integrations_*` tools down on
15
+ * the live customer runtime for every Slack turn — success and refusal alike, since
16
+ * neither sets `isError` — and three consecutive failures then tripped Hermes' MCP
17
+ * circuit breaker, which reported the whole server as "unreachable".
18
+ *
19
+ * The declaration and the payload therefore cannot be maintained in two places by
20
+ * two conventions. `inbox.ts`, `senders.ts`, and `runtime-identity.ts` each hand-built
21
+ * `{ content, structuredContent }` correctly and `integrations.ts` returned a bare
22
+ * envelope; keying off the tool's own declaration removes the chance to get it wrong.
23
+ */
24
+ export type McpToolTextContent = {
25
+ type: "text";
26
+ text: string;
27
+ };
28
+ export type McpToolResult = {
29
+ content: McpToolTextContent[];
30
+ structuredContent?: unknown;
31
+ isError?: boolean;
32
+ };
33
+ export declare function isStructuredToolResult(result: unknown): result is McpToolResult;
34
+ export declare function toMcpToolResult(name: string, result: unknown): McpToolResult;
@@ -0,0 +1,37 @@
1
+ import { allTools } from "./tools/registry.js";
2
+ import { sanitizeWatchUrlsForMcpResult } from "./tools/watch-url-security.js";
3
+ /**
4
+ * Read off the REAL tool table, so a tool that adds an `outputSchema` is covered the
5
+ * moment it is registered and cannot be forgotten here.
6
+ */
7
+ const OUTPUT_SCHEMA_TOOL_NAMES = new Set(allTools
8
+ .filter((tool) => tool.outputSchema)
9
+ .map((tool) => tool.name));
10
+ export function isStructuredToolResult(result) {
11
+ return (!!result &&
12
+ typeof result === "object" &&
13
+ Array.isArray(result.content));
14
+ }
15
+ function isPlainObject(value) {
16
+ return !!value && typeof value === "object" && !Array.isArray(value);
17
+ }
18
+ export function toMcpToolResult(name, result) {
19
+ const safeResult = sanitizeWatchUrlsForMcpResult(result);
20
+ if (isStructuredToolResult(safeResult)) {
21
+ return safeResult;
22
+ }
23
+ const content = [
24
+ { type: "text", text: JSON.stringify(safeResult) },
25
+ ];
26
+ if (!OUTPUT_SCHEMA_TOOL_NAMES.has(name)) {
27
+ return { content };
28
+ }
29
+ if (!isPlainObject(safeResult)) {
30
+ // `structuredContent` must be an OBJECT, so a non-object can never satisfy an
31
+ // object schema. FAIL CLOSED as an error the client will still deliver, instead
32
+ // of a well-formed response the client discards wholesale — the operator gets
33
+ // the payload and a visible error rather than a bare protocol complaint.
34
+ return { content, isError: true };
35
+ }
36
+ return { content, structuredContent: safeResult };
37
+ }
@@ -243,11 +243,36 @@ export const integrationsToolDefinitions = [
243
243
  },
244
244
  },
245
245
  ];
246
+ /**
247
+ * The two CLOSED enums, read off the declared `outputSchema` so there is one source.
248
+ *
249
+ * `outcome` and `attribution` are validated by the MCP CLIENT against
250
+ * `ENVELOPE_OUTPUT_SCHEMA` (`@modelcontextprotocol/sdk` runs ajv over
251
+ * `structuredContent`; the python SDK does the same via `jsonschema`). An off-enum
252
+ * value therefore does not degrade the envelope — it makes the client reject the
253
+ * WHOLE response, so the operator loses the refusal AND its reason.
254
+ *
255
+ * `toolError` forwards the route's fields VERBATIM, and off-enum values exist
256
+ * server-side already (`attribution: "app"` in
257
+ * `src/lib/sellable-agent/integration-connect-copy.ts`). So an unrecognized value is
258
+ * normalized FAIL-CLOSED to a refusal by `sellable`, while the route's `error` code —
259
+ * the field that actually carries the reason — is still forwarded verbatim.
260
+ */
261
+ const ENVELOPE_OUTCOMES = ENVELOPE_OUTPUT_SCHEMA.properties.outcome.enum;
262
+ const ENVELOPE_ATTRIBUTIONS = ENVELOPE_OUTPUT_SCHEMA.properties.attribution.enum;
246
263
  function envelope(value) {
264
+ const outcome = value.outcome ?? (value.ok ? "ok" : "refused");
265
+ const attribution = value.attribution ?? (value.ok ? "policy" : "sellable");
247
266
  return {
248
267
  ok: value.ok,
249
- outcome: value.outcome ?? (value.ok ? "ok" : "refused"),
250
- attribution: value.attribution ?? (value.ok ? "policy" : "sellable"),
268
+ outcome: ENVELOPE_OUTCOMES.includes(outcome)
269
+ ? outcome
270
+ : value.ok
271
+ ? "ok"
272
+ : "refused",
273
+ attribution: ENVELOPE_ATTRIBUTIONS.includes(attribution)
274
+ ? attribution
275
+ : "sellable",
251
276
  error: value.error ?? null,
252
277
  guidance: value.guidance ?? null,
253
278
  result: value.result ?? null,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/mcp",
3
- "version": "0.1.754",
3
+ "version": "0.1.755",
4
4
  "type": "module",
5
5
  "description": "Sellable MCP server for Claude Code, Codex, and Hermes campaign workflows",
6
6
  "main": "dist/index.js",