@sellable/mcp 0.1.753 → 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.
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Trusted request-actor plumbing for the managed-integration tools.
3
+ *
4
+ * WHY THIS IS A SEPARATE MODULE. `server.ts` calls `main()` at import time and
5
+ * connects a stdio transport, so it cannot be imported by a unit test. The
6
+ * tool-name set and the fail-closed resolver therefore live here, `server.ts`
7
+ * imports them, and plan 141-06 imports the same constant when it registers the
8
+ * three `case` labels. One source of truth, testable in isolation.
9
+ *
10
+ * THE ACTOR IS NEVER A TOOL INPUT, and there is exactly one mechanism.
11
+ * `packages/sellable-install/lib/sellable-agent/mcp-context-proxy.mjs:352-402`
12
+ * REJECTS any model-supplied `_meta` with `model_metadata_rejected`, validates a
13
+ * signed single-use descriptor, burns a nonce, and only then injects
14
+ * `_meta.sellableAgent` itself. Upstream of that,
15
+ * `packages/sellable-install/lib/sellable-agent/hermes-bridge.mjs:646-665`
16
+ * patches Hermes at the SHARED generic `server.session.call_tool` dispatch site,
17
+ * so the injection is per-call and tool-agnostic and a newly registered tool
18
+ * inherits it with no further change. A model-supplied actor claim is worthless,
19
+ * so this module reads the actor ONLY from the already-verified credential
20
+ * boundary context and NEVER from tool arguments. Do not add a second mechanism.
21
+ *
22
+ * THE BACKEND-FACING CONSEQUENCE, recorded here because plan 141-06's routes
23
+ * depend on it: on the `Bearer sat_` middleware branch, `X-authuserid` and
24
+ * `X-workspaceid` are OVERWRITTEN from `AgentServicePrincipal.subjectUserId`
25
+ * and the five `x-sellable-*` principal headers are DELETED
26
+ * (`src/middleware.ts:80-86`, `:134-140`, `:806-809`), so no caller-supplied
27
+ * identity header can carry the Slack requester. The actor must therefore travel
28
+ * as an EXPLICIT REQUEST BODY FIELD that the route re-verifies against
29
+ * `WorkspaceSlackIdentity`. See
30
+ * `.planning/artifacts/uat/141/current/mcp/actor-propagation.json` for the
31
+ * measured trace and the exact residual gap.
32
+ */
33
+ /**
34
+ * The provider-neutral managed-integration tools. Plan 141-06 registers the
35
+ * `case` labels and the tool schemas; this set exists in WAVE 2 so the
36
+ * fail-closed guard is already in place when they arrive, and so the two plans
37
+ * do not both try to own the dispatch switch.
38
+ */
39
+ export declare const AGENT_INTEGRATION_CONTEXT_TOOL_NAMES: readonly ["integrations_list_tools", "integrations_describe_tool", "integrations_call_tool"];
40
+ export type AgentIntegrationContextToolName = (typeof AGENT_INTEGRATION_CONTEXT_TOOL_NAMES)[number];
41
+ /** The trusted actor triple a managed-integration call forwards to the backend. */
42
+ export type AgentIntegrationRequestContext = {
43
+ requesterId: string;
44
+ channelId: string;
45
+ providerRequestId: string;
46
+ };
47
+ /**
48
+ * The boundary context shape `createMcpCredentialBoundary` returns on success.
49
+ * `undefined` is accepted because the `agent_service` refusal branches at
50
+ * `agent-service-auth.ts:139-157` return no `context` at all.
51
+ */
52
+ type BoundaryContext = {
53
+ requesterId?: string | null;
54
+ channelId?: string | null;
55
+ providerRequestId?: string | null;
56
+ } | null | undefined;
57
+ export type AgentIntegrationRequestActor = {
58
+ /** True when the called tool is one that must have a trusted actor. */
59
+ required: boolean;
60
+ /** Present only when the actor is fully resolved. */
61
+ context: AgentIntegrationRequestContext | null;
62
+ };
63
+ export declare const AGENT_INTEGRATION_CONTEXT_REQUIRED_ERROR = "agent_request_context_required";
64
+ export declare function isAgentIntegrationContextTool(name: string): boolean;
65
+ /**
66
+ * Resolves the trusted actor for a managed-integration tool call.
67
+ *
68
+ * FAILS CLOSED. A tool in the set with no boundary context — a `human`
69
+ * credential, an `invalid` boundary, or an `agent_service` call whose
70
+ * `_meta.sellableAgent` never arrived — resolves to `context: null`, and the
71
+ * dispatch must refuse with `agent_request_context_required` rather than
72
+ * defaulting to a synthetic or absent actor. `agent-service-auth.ts:128-147`
73
+ * already refuses every `agent_service` call missing the triple with the same
74
+ * code; this guard is the belt for tools whose authorization decision is
75
+ * `AUTO`/read-only and for the non-`agent_service` modes that never reach it.
76
+ */
77
+ export declare function agentIntegrationRequestActor(name: string, boundaryContext: BoundaryContext): AgentIntegrationRequestActor;
78
+ export {};
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Trusted request-actor plumbing for the managed-integration tools.
3
+ *
4
+ * WHY THIS IS A SEPARATE MODULE. `server.ts` calls `main()` at import time and
5
+ * connects a stdio transport, so it cannot be imported by a unit test. The
6
+ * tool-name set and the fail-closed resolver therefore live here, `server.ts`
7
+ * imports them, and plan 141-06 imports the same constant when it registers the
8
+ * three `case` labels. One source of truth, testable in isolation.
9
+ *
10
+ * THE ACTOR IS NEVER A TOOL INPUT, and there is exactly one mechanism.
11
+ * `packages/sellable-install/lib/sellable-agent/mcp-context-proxy.mjs:352-402`
12
+ * REJECTS any model-supplied `_meta` with `model_metadata_rejected`, validates a
13
+ * signed single-use descriptor, burns a nonce, and only then injects
14
+ * `_meta.sellableAgent` itself. Upstream of that,
15
+ * `packages/sellable-install/lib/sellable-agent/hermes-bridge.mjs:646-665`
16
+ * patches Hermes at the SHARED generic `server.session.call_tool` dispatch site,
17
+ * so the injection is per-call and tool-agnostic and a newly registered tool
18
+ * inherits it with no further change. A model-supplied actor claim is worthless,
19
+ * so this module reads the actor ONLY from the already-verified credential
20
+ * boundary context and NEVER from tool arguments. Do not add a second mechanism.
21
+ *
22
+ * THE BACKEND-FACING CONSEQUENCE, recorded here because plan 141-06's routes
23
+ * depend on it: on the `Bearer sat_` middleware branch, `X-authuserid` and
24
+ * `X-workspaceid` are OVERWRITTEN from `AgentServicePrincipal.subjectUserId`
25
+ * and the five `x-sellable-*` principal headers are DELETED
26
+ * (`src/middleware.ts:80-86`, `:134-140`, `:806-809`), so no caller-supplied
27
+ * identity header can carry the Slack requester. The actor must therefore travel
28
+ * as an EXPLICIT REQUEST BODY FIELD that the route re-verifies against
29
+ * `WorkspaceSlackIdentity`. See
30
+ * `.planning/artifacts/uat/141/current/mcp/actor-propagation.json` for the
31
+ * measured trace and the exact residual gap.
32
+ */
33
+ /**
34
+ * The provider-neutral managed-integration tools. Plan 141-06 registers the
35
+ * `case` labels and the tool schemas; this set exists in WAVE 2 so the
36
+ * fail-closed guard is already in place when they arrive, and so the two plans
37
+ * do not both try to own the dispatch switch.
38
+ */
39
+ export const AGENT_INTEGRATION_CONTEXT_TOOL_NAMES = Object.freeze([
40
+ "integrations_list_tools",
41
+ "integrations_describe_tool",
42
+ "integrations_call_tool",
43
+ ]);
44
+ export const AGENT_INTEGRATION_CONTEXT_REQUIRED_ERROR = "agent_request_context_required";
45
+ export function isAgentIntegrationContextTool(name) {
46
+ return AGENT_INTEGRATION_CONTEXT_TOOL_NAMES.includes(name);
47
+ }
48
+ /**
49
+ * Resolves the trusted actor for a managed-integration tool call.
50
+ *
51
+ * FAILS CLOSED. A tool in the set with no boundary context — a `human`
52
+ * credential, an `invalid` boundary, or an `agent_service` call whose
53
+ * `_meta.sellableAgent` never arrived — resolves to `context: null`, and the
54
+ * dispatch must refuse with `agent_request_context_required` rather than
55
+ * defaulting to a synthetic or absent actor. `agent-service-auth.ts:128-147`
56
+ * already refuses every `agent_service` call missing the triple with the same
57
+ * code; this guard is the belt for tools whose authorization decision is
58
+ * `AUTO`/read-only and for the non-`agent_service` modes that never reach it.
59
+ */
60
+ export function agentIntegrationRequestActor(name, boundaryContext) {
61
+ if (!isAgentIntegrationContextTool(name)) {
62
+ return { required: false, context: null };
63
+ }
64
+ const requesterId = boundaryContext?.requesterId?.trim();
65
+ const channelId = boundaryContext?.channelId?.trim();
66
+ const providerRequestId = boundaryContext?.providerRequestId?.trim();
67
+ if (!requesterId || !channelId || !providerRequestId) {
68
+ return { required: true, context: null };
69
+ }
70
+ return {
71
+ required: true,
72
+ context: { requesterId, channelId, providerRequestId },
73
+ };
74
+ }
@@ -3,4 +3,4 @@
3
3
  * manifests. Keep this free of tool implementation imports so Next can bundle
4
4
  * it; registry parity is enforced in sellable-agent-tool-policy.test.ts.
5
5
  */
6
- export declare const SELLABLE_MCP_TOOL_NAMES: readonly ["add_column", "add_on_demand_leads", "add_rubric_item", "add_teammate", "add_to_comment_campaign", "add_to_connection_campaign", "add_to_inmail_campaign", "archive_campaign", "attach_recommended_sequence", "attach_sequence", "bootstrap_create_campaign", "bootstrap_engage", "bootstrap_engage_multi", "bootstrap_find_leads", "bulk_enrich_with_prospeo", "calculate_linkedin_hook_preview", "cancel_campaign_message_preparation", "cancel_find_leads", "cancel_lead_import", "capture_post_idea", "check_inbox_reply_eligibility", "check_rubric", "commit_blueprint", "complete_sender_research", "confirm_harvest_job_companies", "confirm_lead_list", "confirm_prospeo_company_accounts", "copy_sender_config", "create_campaign", "create_on_demand_campaign", "create_on_demand_table", "create_workflow_table", "create_workspace", "delete_column", "delete_rubric_item", "draft_rubrics", "duplicate_campaign", "enrich_with_prospeo", "export_table_csv", "export_workspace_csv", "fetch_company", "fetch_company_posts", "fetch_linkedin_posts", "fetch_linkedin_profile", "fetch_post_engagers", "fill_campaign_horizon", "get_active_workspace", "get_auth_status", "get_campaign", "get_campaign_context", "get_campaign_framework", "get_campaign_message_preparation_status", "get_campaign_messages_preview", "get_campaign_navigation_state", "get_campaign_refill_state", "get_campaign_table_schema", "get_campaign_waterfall", "get_campaigns", "get_column_schema", "get_company_info", "get_engage_memory", "get_engage_state", "get_engaged_posts", "get_find_leads_run", "get_inbox_thread", "get_linkedin_profile", "get_or_create_direct_campaign_table", "get_post_draft", "get_post_find_leads_scout_registry", "get_post_idea", "get_prospeo_credits", "get_provider_prompt", "get_published_post", "get_refill_plan_v2", "get_refill_target_plan", "get_rows", "get_rows_minimal", "get_runtime_identity", "get_scheduler_fill_capacity", "get_sender", "get_sender_routing", "get_source_scout_registry", "get_subskill_asset", "get_subskill_prompt", "get_table_rows", "get_user_posts", "get_workspace", "import_leads", "init_on_demand_sequence", "list_column_types", "list_dnc_entries", "list_post_draft_iterations", "list_post_drafts", "list_post_ideas", "list_published_posts", "list_senders", "list_subskill_prompts", "list_tables", "list_workspaces", "load_csv_dnc_entries", "load_csv_domains", "load_csv_linkedin_leads", "lookup_sales_nav_filter", "mark_post_published", "migrate_flat_configs", "pause_campaign", "pause_direct_campaign", "pause_on_demand_campaign", "preflight_find_leads_provider", "prepare_campaign_ab_test", "queue_campaign_cells", "queue_cells", "record_campaign_review_batch", "record_engage_proven_search", "refill_sends", "refill_sends_v2", "refresh_paid_inmail_credits", "refresh_sender_engagement", "reissue_find_leads_watch_link", "render_linkedin_post_preview", "reorder_columns", "resolve_campaign_fill_route", "revise_message_template_and_rerun", "run_scheduler_sweep", "save_domain_filters", "save_hook_research", "save_post_draft", "save_rubrics", "search_apollo", "search_engagement_posts", "search_harvest_jobs", "search_inbox_threads", "search_prospeo", "search_prospeo_companies", "search_sales_nav", "search_signals", "search_subskill_prompts", "select_campaign_cells", "select_necessary_rubrics", "select_promising_posts", "send_inbox_draft", "send_inbox_manual_reply", "set_active_workspace", "set_campaign_waterfall_order", "set_company_info", "set_engage_state", "set_engage_style_guide", "set_headline_icp_criteria", "set_sender_calendar_link", "set_sender_routing", "setup_evergreen_campaigns", "start_campaign", "start_campaign_message_preparation", "start_cli_login", "start_direct_campaign", "start_on_demand_campaign", "update_campaign", "update_campaign_brief", "update_cell", "update_column", "update_find_leads_run", "update_inbox_draft", "update_post_draft", "update_published_post_metrics", "update_rubric_item", "upsert_engage_tracked_person", "upsert_rubric", "validate_campaign1_kickoff_handoff", "verify_table_row", "wait_for_campaign_processing", "wait_for_campaign_table_ready", "wait_for_cli_login", "wait_for_find_leads_run", "wait_for_lead_list_ready", "wait_for_rubric_results"];
6
+ export declare const SELLABLE_MCP_TOOL_NAMES: readonly ["add_column", "add_on_demand_leads", "add_rubric_item", "add_teammate", "add_to_comment_campaign", "add_to_connection_campaign", "add_to_inmail_campaign", "archive_campaign", "attach_recommended_sequence", "attach_sequence", "bootstrap_create_campaign", "bootstrap_engage", "bootstrap_engage_multi", "bootstrap_find_leads", "bulk_enrich_with_prospeo", "calculate_linkedin_hook_preview", "cancel_campaign_message_preparation", "cancel_find_leads", "cancel_lead_import", "capture_post_idea", "check_inbox_reply_eligibility", "check_rubric", "commit_blueprint", "complete_sender_research", "confirm_harvest_job_companies", "confirm_lead_list", "confirm_prospeo_company_accounts", "copy_sender_config", "create_campaign", "create_on_demand_campaign", "create_on_demand_table", "create_workflow_table", "create_workspace", "delete_column", "delete_rubric_item", "draft_rubrics", "duplicate_campaign", "enrich_with_prospeo", "export_table_csv", "export_workspace_csv", "fetch_company", "fetch_company_posts", "fetch_linkedin_posts", "fetch_linkedin_profile", "fetch_post_engagers", "fill_campaign_horizon", "get_active_workspace", "get_auth_status", "get_campaign", "get_campaign_context", "get_campaign_framework", "get_campaign_message_preparation_status", "get_campaign_messages_preview", "get_campaign_navigation_state", "get_campaign_refill_state", "get_campaign_table_schema", "get_campaign_waterfall", "get_campaigns", "get_column_schema", "get_company_info", "get_engage_memory", "get_engage_state", "get_engaged_posts", "get_find_leads_run", "get_inbox_thread", "get_linkedin_profile", "get_or_create_direct_campaign_table", "get_post_draft", "get_post_find_leads_scout_registry", "get_post_idea", "get_prospeo_credits", "get_provider_prompt", "get_published_post", "get_refill_plan_v2", "get_refill_target_plan", "get_rows", "get_rows_minimal", "get_runtime_identity", "get_scheduler_fill_capacity", "get_sender", "get_sender_routing", "get_source_scout_registry", "get_subskill_asset", "get_subskill_prompt", "get_table_rows", "get_user_posts", "get_workspace", "import_leads", "init_on_demand_sequence", "integrations_call_tool", "integrations_describe_tool", "integrations_list_tools", "list_column_types", "list_dnc_entries", "list_post_draft_iterations", "list_post_drafts", "list_post_ideas", "list_published_posts", "list_senders", "list_subskill_prompts", "list_tables", "list_workspaces", "load_csv_dnc_entries", "load_csv_domains", "load_csv_linkedin_leads", "lookup_sales_nav_filter", "mark_post_published", "migrate_flat_configs", "pause_campaign", "pause_direct_campaign", "pause_on_demand_campaign", "preflight_find_leads_provider", "prepare_campaign_ab_test", "queue_campaign_cells", "queue_cells", "record_campaign_review_batch", "record_engage_proven_search", "refill_sends", "refill_sends_v2", "refresh_paid_inmail_credits", "refresh_sender_engagement", "reissue_find_leads_watch_link", "render_linkedin_post_preview", "reorder_columns", "resolve_campaign_fill_route", "revise_message_template_and_rerun", "run_scheduler_sweep", "save_domain_filters", "save_hook_research", "save_post_draft", "save_rubrics", "search_apollo", "search_engagement_posts", "search_harvest_jobs", "search_inbox_threads", "search_prospeo", "search_prospeo_companies", "search_sales_nav", "search_signals", "search_subskill_prompts", "select_campaign_cells", "select_necessary_rubrics", "select_promising_posts", "send_inbox_draft", "send_inbox_manual_reply", "set_active_workspace", "set_campaign_waterfall_order", "set_company_info", "set_engage_state", "set_engage_style_guide", "set_headline_icp_criteria", "set_sender_calendar_link", "set_sender_routing", "setup_evergreen_campaigns", "start_campaign", "start_campaign_message_preparation", "start_cli_login", "start_direct_campaign", "start_on_demand_campaign", "update_campaign", "update_campaign_brief", "update_cell", "update_column", "update_find_leads_run", "update_inbox_draft", "update_post_draft", "update_published_post_metrics", "update_rubric_item", "upsert_engage_tracked_person", "upsert_rubric", "validate_campaign1_kickoff_handoff", "verify_table_row", "wait_for_campaign_processing", "wait_for_campaign_table_ready", "wait_for_cli_login", "wait_for_find_leads_run", "wait_for_lead_list_ready", "wait_for_rubric_results"];
@@ -93,6 +93,9 @@ export const SELLABLE_MCP_TOOL_NAMES = [
93
93
  "get_workspace",
94
94
  "import_leads",
95
95
  "init_on_demand_sequence",
96
+ "integrations_call_tool",
97
+ "integrations_describe_tool",
98
+ "integrations_list_tools",
96
99
  "list_column_types",
97
100
  "list_dnc_entries",
98
101
  "list_post_draft_iterations",
package/dist/api.d.ts CHANGED
@@ -11,6 +11,21 @@ export interface DownloadToFileResult {
11
11
  export interface ApiRequestOptions {
12
12
  workspaceId?: string | null;
13
13
  agentEffectId?: string;
14
+ /**
15
+ * An OPT-IN client-side deadline, in milliseconds.
16
+ *
17
+ * Added by plan 141-06 because this client previously set no `timeout`, no
18
+ * `AbortSignal`, and no `signal` anywhere. The old deadline came from
19
+ * `timeout: 120` on a per-binding `mcp_servers` entry in the Hermes profile;
20
+ * that entry no longer exists, so a hung backend would hang the Slack turn
21
+ * forever with nothing on the client side to end it.
22
+ *
23
+ * It is OPTIONAL and defaults to undefined ON PURPOSE: the existing 175 tools
24
+ * keep their current unbounded behaviour byte-for-byte (some legitimately run
25
+ * against 240 s and 300 s backend routes), and only the callers that opt in get
26
+ * a bound. The three `integrations_*` tools opt in.
27
+ */
28
+ timeoutMs?: number;
14
29
  }
15
30
  export declare class SellableApiError extends Error {
16
31
  status: number;
package/dist/api.js CHANGED
@@ -48,18 +48,43 @@ export class SellableApi {
48
48
  const workspaceId = options && Object.prototype.hasOwnProperty.call(options, "workspaceId")
49
49
  ? resolveWorkspaceIdForRequest(requestedWorkspaceId, `${method} ${path}`)
50
50
  : requestedWorkspaceId;
51
- const response = await fetch(url, {
52
- method,
53
- headers: {
54
- "Content-Type": "application/json",
55
- Authorization: `Bearer ${config.token}`,
56
- ...(workspaceId ? { "x-workspaceid": workspaceId } : {}),
57
- ...(options?.agentEffectId
58
- ? { "x-sellable-agent-effect-id": options.agentEffectId }
59
- : {}),
60
- },
61
- ...(body && { body: JSON.stringify(body) }),
62
- });
51
+ // Opt-in only: no `signal` is attached unless the caller asked for a bound,
52
+ // so every existing tool's request is byte-identical to before.
53
+ const controller = typeof options?.timeoutMs === "number" && options.timeoutMs > 0
54
+ ? new AbortController()
55
+ : null;
56
+ const timer = controller
57
+ ? setTimeout(() => controller.abort(), options.timeoutMs)
58
+ : null;
59
+ let response;
60
+ try {
61
+ response = await fetch(url, {
62
+ method,
63
+ headers: {
64
+ "Content-Type": "application/json",
65
+ Authorization: `Bearer ${config.token}`,
66
+ ...(workspaceId ? { "x-workspaceid": workspaceId } : {}),
67
+ ...(options?.agentEffectId
68
+ ? { "x-sellable-agent-effect-id": options.agentEffectId }
69
+ : {}),
70
+ },
71
+ ...(controller ? { signal: controller.signal } : {}),
72
+ ...(body && { body: JSON.stringify(body) }),
73
+ });
74
+ }
75
+ catch (error) {
76
+ if (controller?.signal.aborted) {
77
+ throw new SellableApiError(504, JSON.stringify({
78
+ error: "integration_client_timeout",
79
+ timeoutMs: options?.timeoutMs ?? null,
80
+ }), "The Sellable API did not respond inside the client deadline. Retry, or report the tool name if it keeps happening.");
81
+ }
82
+ throw error;
83
+ }
84
+ finally {
85
+ if (timer)
86
+ clearTimeout(timer);
87
+ }
63
88
  if (!response.ok) {
64
89
  const errorText = await response.text();
65
90
  throw this.buildError(response.status, errorText);
package/dist/server.js CHANGED
@@ -1,12 +1,14 @@
1
1
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
3
  import { CallToolRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
4
+ import { HttpAgentApprovalEffectPort } from "./agent-approval-effect-port.js";
5
+ import { AGENT_INTEGRATION_CONTEXT_REQUIRED_ERROR, agentIntegrationRequestActor, } from "./agent-integration-request-context.js";
4
6
  import { createMcpCredentialBoundary, resolveMcpCredentialIntent, } from "./agent-service-auth.js";
5
- import { getConfig, getConfiguredCredentialIntent } from "./auth.js";
6
7
  import { compileAgentToolPolicy, createAgentMcpAuthorizationHandlers, } from "./agent-tool-policy.js";
7
- import { HttpAgentApprovalEffectPort } from "./agent-approval-effect-port.js";
8
+ import { getConfig, getConfiguredCredentialIntent } from "./auth.js";
8
9
  import { MCP_RUNTIME_IDENTITY } from "./runtime-identity.js";
9
10
  import { getSkillByName, listSkills } from "./skills.js";
11
+ import { toMcpToolResult } from "./tool-result-envelope.js";
10
12
  import { getAuthStatus } from "./tools/auth.js";
11
13
  import { handleAddColumn, handleCommitBlueprint, } from "./tools/blueprint-commit.js";
12
14
  import { bootstrapCreateCampaign } from "./tools/bootstrap.js";
@@ -37,6 +39,7 @@ import { bootstrapFindLeads, cancelFindLeads, getFindLeadsRun, reissueFindLeadsW
37
39
  import { getCampaignFramework } from "./tools/framework.js";
38
40
  import { confirmHarvestJobCompanies, searchHarvestJobs, } from "./tools/harvest-jobs.js";
39
41
  import { checkInboxReplyEligibility, getInboxThread, searchInboxThreads, sendInboxDraft, sendInboxManualReply, updateInboxDraft, } from "./tools/inbox.js";
42
+ import { integrationsCallTool, integrationsDescribeTool, integrationsListTools, } from "./tools/integrations.js";
40
43
  import { cancelLeadImport, confirmLeadList, confirmProspeoCompanyAccounts, getProviderPrompt, importLeads, listDncEntriesTool, loadCsvDncEntriesTool, loadCsvDomains, loadCsvLinkedinLeads, lookupSalesNavFilter, saveDomainFilters, searchApollo, searchProspeo, searchProspeoCompanies, searchSalesNav, searchSignals, selectPromisingPosts, setHeadlineICPCriteria, } from "./tools/leads.js";
41
44
  import { fetchCompany, fetchCompanyPosts, fetchLinkedInPosts, fetchLinkedInProfile, fetchPostEngagers, getLinkedInProfile, getUserPosts, } from "./tools/linkedin.js";
42
45
  import { getCampaignNavigationState } from "./tools/navigation.js";
@@ -61,7 +64,6 @@ import { attachRecommendedSequence, attachSequence, createWorkflowTable, } from
61
64
  import { setupEvergreenCampaigns } from "./tools/setup-evergreen-campaigns.js";
62
65
  import { exportTableCsv, listTables } from "./tools/tables.js";
63
66
  import { handleVerifyTableRow } from "./tools/verify-row.js";
64
- import { sanitizeWatchUrlsForMcpResult } from "./tools/watch-url-security.js";
65
67
  import { getCampaignWaterfall, setCampaignWaterfallOrder, } from "./tools/waterfalls.js";
66
68
  import { exportWorkspaceCsv } from "./tools/workspace-export.js";
67
69
  import { addTeammate, createWorkspace, getActiveWorkspace, getWorkspace, listWorkspaces, setActiveWorkspace, } from "./tools/workspaces.js";
@@ -132,11 +134,6 @@ function formatSubskillPromptText(result) {
132
134
  : "";
133
135
  return `${header}${result.prompt}${footer}`;
134
136
  }
135
- function isStructuredToolResult(result) {
136
- return (!!result &&
137
- typeof result === "object" &&
138
- Array.isArray(result.content));
139
- }
140
137
  const agentApprovalPort = new HttpAgentApprovalEffectPort();
141
138
  const agentMcpAuthorization = createAgentMcpAuthorizationHandlers({
142
139
  allTools,
@@ -208,7 +205,40 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
208
205
  ],
209
206
  };
210
207
  }
211
- agentEffectId = boundary.authorization.effectId;
208
+ agentEffectId = boundary.authorization
209
+ .effectId;
210
+ }
211
+ // TRUSTED ACTOR PLUMBING FOR THE MANAGED-INTEGRATION TOOLS, keyed on the
212
+ // TOOL-NAME SET rather than on a `case` label, so plan 141-06's registration
213
+ // inherits the guard instead of re-implementing it. Plan 141-06 owns the
214
+ // three `case` labels and consumes `integrationActor.context`; this wave owns
215
+ // the plumbing and its fail-closed refusal. The two plans must not both try
216
+ // to own the switch.
217
+ //
218
+ // The actor comes ONLY from `boundary.context`, which is derived from
219
+ // `_meta.sellableAgent` — metadata `mcp-context-proxy.mjs:352-402` injects
220
+ // itself after REJECTING any model-supplied `_meta`. It is never read from
221
+ // tool ARGUMENTS, because a model-supplied actor claim is worthless.
222
+ //
223
+ // FAIL CLOSED, never default: a `human` credential, an `invalid` boundary, or
224
+ // a missing actor triple all refuse. A managed-integration call without a
225
+ // resolved Slack requester has no way to be authorized against a PERSONAL
226
+ // connection, so proceeding would be strictly worse than refusing.
227
+ const integrationActor = agentIntegrationRequestActor(name, boundary.mode === "agent_service" ? boundary.context : null);
228
+ if (integrationActor.required && !integrationActor.context) {
229
+ return {
230
+ isError: true,
231
+ content: [
232
+ {
233
+ type: "text",
234
+ text: JSON.stringify({
235
+ ok: false,
236
+ error: AGENT_INTEGRATION_CONTEXT_REQUIRED_ERROR,
237
+ decision: "DENY",
238
+ }),
239
+ },
240
+ ],
241
+ };
212
242
  }
213
243
  let result;
214
244
  switch (name) {
@@ -636,6 +666,19 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
636
666
  case "set_sender_routing":
637
667
  result = await setSenderRoutingTool(args);
638
668
  break;
669
+ // Plan 141-06 owns these three `case` labels ONLY. The fail-closed actor
670
+ // guard above is plan 141-04 T2's and is keyed on the tool-name SET, so it
671
+ // already ran before this switch and `integrationActor.context` is verified.
672
+ // Phase 142-05 adds its connect-tool cases to this same switch additively.
673
+ case "integrations_list_tools":
674
+ result = await integrationsListTools(args, integrationActor.context, agentEffectId);
675
+ break;
676
+ case "integrations_describe_tool":
677
+ result = await integrationsDescribeTool(args, integrationActor.context, agentEffectId);
678
+ break;
679
+ case "integrations_call_tool":
680
+ result = await integrationsCallTool(args, integrationActor.context, agentEffectId);
681
+ break;
639
682
  case "get_company_info":
640
683
  result = await getCompanyInfoTool();
641
684
  if (boundary.mode === "agent_service" &&
@@ -853,18 +896,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
853
896
  default:
854
897
  throw new Error(`Unknown tool: ${name}`);
855
898
  }
856
- const safeResult = sanitizeWatchUrlsForMcpResult(result);
857
- if (isStructuredToolResult(safeResult)) {
858
- return safeResult;
859
- }
860
- return {
861
- content: [
862
- {
863
- type: "text",
864
- text: JSON.stringify(safeResult),
865
- },
866
- ],
867
- };
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);
868
903
  }
869
904
  catch (error) {
870
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
+ }