@opennous/mcp 0.16.0 → 0.19.0
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/package.json +7 -1
- package/src/client.js +25 -4
- package/src/index.js +9 -1
- package/src/server.js +278 -3
package/package.json
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opennous/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"description": "Nous MCP Server — Customer graph for GTM agents.",
|
|
5
|
+
"license": "AGPL-3.0-only",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/NousC/nous.git",
|
|
9
|
+
"directory": "apps/mcp"
|
|
10
|
+
},
|
|
5
11
|
"type": "module",
|
|
6
12
|
"bin": {
|
|
7
13
|
"nous-mcp": "src/index.js"
|
package/src/client.js
CHANGED
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
12
|
+
import fs from "node:fs";
|
|
13
|
+
import os from "node:os";
|
|
14
|
+
import path from "node:path";
|
|
12
15
|
|
|
13
16
|
// Resolve an env var defensively — Claude Code plugins use ${user_config.X}
|
|
14
17
|
// substitution; when an optional userConfig field is left blank, the literal
|
|
@@ -32,15 +35,33 @@ export function runWithApiKey(apiKey, fn) {
|
|
|
32
35
|
return apiKeyStore.run({ apiKey }, fn);
|
|
33
36
|
}
|
|
34
37
|
|
|
38
|
+
// Credential written by `nous login` (the browser device-auth flow). The CLI and
|
|
39
|
+
// the MCP server share ~/.nous/config.json, so a user who runs the login command
|
|
40
|
+
// gets a key the MCP picks up on the next call — no paste, no env var.
|
|
41
|
+
function fileApiKey() {
|
|
42
|
+
try {
|
|
43
|
+
const dir = resolvedEnv("NOUS_CONFIG_DIR") || path.join(os.homedir(), ".nous");
|
|
44
|
+
const cfg = JSON.parse(fs.readFileSync(path.join(dir, "config.json"), "utf8"));
|
|
45
|
+
const k = cfg?.apiKey;
|
|
46
|
+
return k && !String(k).includes("${") ? k : undefined;
|
|
47
|
+
} catch {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
35
52
|
function currentApiKey() {
|
|
36
|
-
return apiKeyStore.getStore()?.apiKey ?? resolvedEnv("NOUS_API_KEY");
|
|
53
|
+
return apiKeyStore.getStore()?.apiKey ?? resolvedEnv("NOUS_API_KEY") ?? fileApiKey();
|
|
37
54
|
}
|
|
38
55
|
|
|
39
|
-
// stdio-only preflight
|
|
56
|
+
// stdio-only preflight. A key may come from the env OR from `nous login`'s
|
|
57
|
+
// credential file. This is advisory — the server still starts without one so
|
|
58
|
+
// the user can run the login command after installing the plugin, and the key
|
|
59
|
+
// is resolved per-call.
|
|
40
60
|
export function validateConfig() {
|
|
41
|
-
if (!resolvedEnv("NOUS_API_KEY")) {
|
|
61
|
+
if (!resolvedEnv("NOUS_API_KEY") && !fileApiKey()) {
|
|
42
62
|
throw new Error(
|
|
43
|
-
"
|
|
63
|
+
"No Nous API key found. Run the /nous-login command (or `npx @opennous/cli login`) to sign in, " +
|
|
64
|
+
"or set NOUS_API_KEY."
|
|
44
65
|
);
|
|
45
66
|
}
|
|
46
67
|
}
|
package/src/index.js
CHANGED
|
@@ -17,7 +17,15 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
17
17
|
import { validateConfig } from "./client.js";
|
|
18
18
|
import { createServer } from "./server.js";
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
// Advisory only — don't hard-exit if there's no key yet. The user may install
|
|
21
|
+
// the plugin and then run /nous-login; the server must already be running so the
|
|
22
|
+
// key (resolved per-call from env or ~/.nous/config.json) is picked up without a
|
|
23
|
+
// restart.
|
|
24
|
+
try {
|
|
25
|
+
validateConfig();
|
|
26
|
+
} catch (err) {
|
|
27
|
+
console.error(`[nous] ${err.message}`);
|
|
28
|
+
}
|
|
21
29
|
|
|
22
30
|
const server = createServer();
|
|
23
31
|
const transport = new StdioServerTransport();
|
package/src/server.js
CHANGED
|
@@ -21,13 +21,20 @@
|
|
|
21
21
|
* update_gtm_profile — write back a change to a GTM context section (evolve, keep history)
|
|
22
22
|
* save_note — attach a note/document (meeting brief, transcript, prep) to a contact
|
|
23
23
|
* search_notes — semantic search over saved notes & documents
|
|
24
|
+
* get_workspace_status — what's set up in this workspace + a ranked next_steps list (call first)
|
|
25
|
+
* set_workspace_profile— agent-driven onboarding: set the workspace's name, site, type, ICP
|
|
26
|
+
* build_scoring_model — build/rebuild the ICP scoring model from the recorded GTM context
|
|
27
|
+
* connect_integration — connect a key-based integration (Apollo, Prospeo, HubSpot, …)
|
|
28
|
+
* configure_crm_sync — set CRM sync rules (auto-sync, create policy, hygiene cadence)
|
|
29
|
+
* set_trigger — create an outbound event trigger (webhook); list_triggers reads them
|
|
30
|
+
* list_triggers — list the workspace's event triggers + available events
|
|
24
31
|
*/
|
|
25
32
|
|
|
26
33
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
27
34
|
import { z } from "zod";
|
|
28
35
|
import { get, post } from "./client.js";
|
|
29
36
|
|
|
30
|
-
export const SERVER_VERSION = "0.
|
|
37
|
+
export const SERVER_VERSION = "0.19.0";
|
|
31
38
|
|
|
32
39
|
// ─── helpers ──────────────────────────────────────────────────────────────────
|
|
33
40
|
|
|
@@ -53,8 +60,10 @@ export function createServer() {
|
|
|
53
60
|
name: "nous",
|
|
54
61
|
version: SERVER_VERSION,
|
|
55
62
|
description:
|
|
56
|
-
"Nous — the context layer for GTM agents.
|
|
57
|
-
"
|
|
63
|
+
"Nous — the context layer for GTM agents. Nous is operated by the agent, not by a human " +
|
|
64
|
+
"clicking around: call get_workspace_status at the start of a session to see what's set up " +
|
|
65
|
+
"and what to set up next. Call get_context before drafting outreach or preparing for a " +
|
|
66
|
+
"meeting. Call record after every interaction, or whenever you learn something.",
|
|
58
67
|
icons: [
|
|
59
68
|
{ src: "https://opennous.cloud/newlogoP.png", mimeType: "image/png", sizes: ["64x64"] },
|
|
60
69
|
],
|
|
@@ -485,5 +494,271 @@ export function createServer() {
|
|
|
485
494
|
},
|
|
486
495
|
);
|
|
487
496
|
|
|
497
|
+
// ===========================================================================
|
|
498
|
+
// TOOL: get_workspace_status — GET /v2/workspace/status
|
|
499
|
+
// The "one main call." Nous is operated by the agent, so the agent needs to
|
|
500
|
+
// know the state of the workspace: is it onboarded, is the GTM playbook built,
|
|
501
|
+
// which integrations are connected, is CRM sync configured, are events live —
|
|
502
|
+
// and what to set up next. Call this at the start of a session.
|
|
503
|
+
// ===========================================================================
|
|
504
|
+
server.tool(
|
|
505
|
+
"get_workspace_status",
|
|
506
|
+
"See the whole setup state of this workspace in one call — and what to set up next. Nous is " +
|
|
507
|
+
"operated by you, the agent, not by a human clicking through the app: call this at the start of " +
|
|
508
|
+
"a session to learn whether the workspace is onboarded, whether the GTM playbook (ICP model) is " +
|
|
509
|
+
"built, which integrations are connected, whether CRM sync is configured, and whether webhooks/" +
|
|
510
|
+
"triggers are live. Returns a ranked NEXT STEPS list — walk the user through whatever is missing " +
|
|
511
|
+
"(onboard them with set_workspace_profile, build the playbook with update_gtm_profile, connect " +
|
|
512
|
+
"their tools). Use this before offering to set anything up.",
|
|
513
|
+
{},
|
|
514
|
+
async () => {
|
|
515
|
+
const s = await get("/v2/workspace/status");
|
|
516
|
+
const setup = s.setup ?? {};
|
|
517
|
+
const lines = [];
|
|
518
|
+
|
|
519
|
+
const ws = s.workspace ?? {};
|
|
520
|
+
lines.push(`WORKSPACE: ${ws.name || "(unnamed)"}${ws.website ? ` · ${ws.website}` : ""}${ws.business_type ? ` · ${ws.business_type}` : ""}`);
|
|
521
|
+
lines.push("");
|
|
522
|
+
|
|
523
|
+
const mark = (b) => (b ? "✓" : "✗");
|
|
524
|
+
lines.push("SETUP:");
|
|
525
|
+
lines.push(` ${mark(setup.onboarding?.done)} Onboarding${setup.onboarding?.done ? "" : ` — missing ${(setup.onboarding?.missing ?? []).join(", ") || "details"}`}`);
|
|
526
|
+
lines.push(` ${mark(setup.gtm_playbook?.done)} GTM playbook${setup.gtm_playbook?.model ? " (scoring model live)" : ""}${setup.gtm_playbook?.stale_facts ? ` · ${setup.gtm_playbook.stale_facts} stale fact(s)` : ""}`);
|
|
527
|
+
const ints = setup.integrations?.connected ?? [];
|
|
528
|
+
lines.push(` ${mark((setup.integrations?.count ?? 0) > 0)} Integrations (${setup.integrations?.count ?? 0})${ints.length ? `: ${ints.map((i) => i.name).join(", ")}` : ""}`);
|
|
529
|
+
const crm = setup.crm_sync ?? {};
|
|
530
|
+
lines.push(` ${mark(crm.configured)} CRM sync${crm.configured ? `: ${(crm.providers ?? []).map((p) => p.provider).join(", ")}` : ""}${crm.pending_hygiene_proposals ? ` · ${crm.pending_hygiene_proposals} hygiene proposal(s) to review` : ""}`);
|
|
531
|
+
lines.push(` ${mark(setup.enrichment?.connected)} Enrichment${setup.enrichment?.provider ? `: ${setup.enrichment.provider}` : ""}`);
|
|
532
|
+
lines.push(` ${mark((setup.webhooks?.count ?? 0) > 0 || (setup.triggers?.count ?? 0) > 0)} Events — ${setup.webhooks?.count ?? 0} webhook(s), ${setup.triggers?.count ?? 0} trigger(s)`);
|
|
533
|
+
|
|
534
|
+
if (s.next_steps?.length) {
|
|
535
|
+
lines.push("");
|
|
536
|
+
lines.push("NEXT STEPS:");
|
|
537
|
+
for (const step of s.next_steps) {
|
|
538
|
+
lines.push(` • ${step.title}`);
|
|
539
|
+
if (step.why) lines.push(` why: ${step.why}`);
|
|
540
|
+
if (step.how) lines.push(` how: ${step.how}`);
|
|
541
|
+
}
|
|
542
|
+
} else {
|
|
543
|
+
lines.push("");
|
|
544
|
+
lines.push("Everything's set up. Nothing pending.");
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
return { content: [{ type: "text", text: lines.join("\n").trim() }] };
|
|
548
|
+
}
|
|
549
|
+
);
|
|
550
|
+
|
|
551
|
+
// ===========================================================================
|
|
552
|
+
// TOOL: set_workspace_profile — POST /v2/workspace/onboarding
|
|
553
|
+
// Agent-driven onboarding. Instead of a human clicking through a wizard in the
|
|
554
|
+
// app, you collect the basics from the user in conversation and write them
|
|
555
|
+
// here. This is the first thing get_workspace_status asks for when a workspace
|
|
556
|
+
// is new.
|
|
557
|
+
// ===========================================================================
|
|
558
|
+
server.tool(
|
|
559
|
+
"set_workspace_profile",
|
|
560
|
+
"Onboard the workspace, or update its basic profile. Nous is set up by you, the agent, in " +
|
|
561
|
+
"conversation — not by the user clicking through a wizard. Ask the user for their company name, " +
|
|
562
|
+
"their website, whether they sell a SERVICE or SOFTWARE, and a sentence describing their ideal " +
|
|
563
|
+
"customer, then write them here. This seeds the GTM context and the ICP scoring model. Call " +
|
|
564
|
+
"get_workspace_status first to see what's already set; send only the fields you're setting or " +
|
|
565
|
+
"changing. After this, the next step is usually the GTM playbook (update_gtm_profile).",
|
|
566
|
+
{
|
|
567
|
+
name: z.string().optional().describe("The user's company / workspace name."),
|
|
568
|
+
website: z.string().optional().describe("The company website (used to seed the GTM context)."),
|
|
569
|
+
business_type: z.enum(["service", "software"]).optional()
|
|
570
|
+
.describe("Whether they sell a service or software — sets the CRM's buyer terminology and default signup stage."),
|
|
571
|
+
plan_model: z.enum(["free_plan", "free_trial", "both", "paid_only"]).optional()
|
|
572
|
+
.describe("For software only: how they package (free plan, free trial, both, or paid only)."),
|
|
573
|
+
default_signup_stage: z.string().optional()
|
|
574
|
+
.describe("The pipeline stage a brand-new signup lands in (e.g. 'Lead', 'Free User'). Defaults sensibly from business_type."),
|
|
575
|
+
icp: z.string().optional()
|
|
576
|
+
.describe("A sentence or two describing their ideal customer — seeds the ICP scoring model."),
|
|
577
|
+
},
|
|
578
|
+
async ({ name, website, business_type, plan_model, default_signup_stage, icp }) => {
|
|
579
|
+
const r = await post("/v2/workspace/onboarding", { name, website, business_type, plan_model, default_signup_stage, icp });
|
|
580
|
+
const w = r.workspace ?? {};
|
|
581
|
+
const set = [
|
|
582
|
+
w.name && `name=${w.name}`,
|
|
583
|
+
w.website && `site=${w.website}`,
|
|
584
|
+
w.business_type && `type=${w.business_type}`,
|
|
585
|
+
icp && "ICP recorded",
|
|
586
|
+
].filter(Boolean);
|
|
587
|
+
return { content: [{ type: "text", text:
|
|
588
|
+
`Workspace profile saved.${set.length ? ` ${set.join(" · ")}.` : ""}\n` +
|
|
589
|
+
`Next: call get_workspace_status to see what to set up next (usually the GTM playbook).` }] };
|
|
590
|
+
}
|
|
591
|
+
);
|
|
592
|
+
|
|
593
|
+
// ===========================================================================
|
|
594
|
+
// TOOL: build_scoring_model — POST /v2/workspace/scoring-model
|
|
595
|
+
// The second half of building the GTM playbook. The agent records the GTM
|
|
596
|
+
// context with update_gtm_profile, then calls this to turn it into a weighted
|
|
597
|
+
// ICP scoring model. After this, accounts get scored for fit and
|
|
598
|
+
// get_workspace_status shows the playbook as done.
|
|
599
|
+
// ===========================================================================
|
|
600
|
+
server.tool(
|
|
601
|
+
"build_scoring_model",
|
|
602
|
+
"Build (or rebuild) the user's ICP scoring model from the GTM context they've recorded. This is " +
|
|
603
|
+
"the second half of setting up the GTM playbook: first record the ICP and how they sell with " +
|
|
604
|
+
"update_gtm_profile, then call this to translate that context into a weighted set of scoring " +
|
|
605
|
+
"signals so accounts get scored for fit. If a model already exists it is left alone unless you " +
|
|
606
|
+
"pass force:true (use that when the GTM context has changed and the model should be rebuilt). If " +
|
|
607
|
+
"it reports no GTM context yet, record some with update_gtm_profile first, then call this again.",
|
|
608
|
+
{
|
|
609
|
+
force: z.boolean().optional()
|
|
610
|
+
.describe("Rebuild the model even if one already exists — use when the GTM context has changed."),
|
|
611
|
+
},
|
|
612
|
+
async ({ force }) => {
|
|
613
|
+
try {
|
|
614
|
+
const r = await post("/v2/workspace/scoring-model", { force: force === true });
|
|
615
|
+
const signals = r.signals ?? [];
|
|
616
|
+
const lines = [`Built the ICP scoring model — ${signals.length} signal${signals.length === 1 ? "" : "s"}:`];
|
|
617
|
+
for (const s of signals) lines.push(` • ${s.label ?? s.key} (weight ${s.weight})`);
|
|
618
|
+
lines.push("", "Accounts will now be scored for fit. Check it on the GTM Context page.");
|
|
619
|
+
return { content: [{ type: "text", text: lines.join("\n").trim() }] };
|
|
620
|
+
} catch (e) {
|
|
621
|
+
// Surface the actionable cases (no context yet / model already exists) as
|
|
622
|
+
// guidance rather than a raw error, so the agent knows what to do next.
|
|
623
|
+
const msg = String(e?.message ?? e);
|
|
624
|
+
if (msg.includes("no_gtm_context")) {
|
|
625
|
+
return { content: [{ type: "text", text:
|
|
626
|
+
"No GTM context recorded yet. Record the ICP and how they sell with update_gtm_profile first, then build the model." }] };
|
|
627
|
+
}
|
|
628
|
+
if (msg.includes("model_exists")) {
|
|
629
|
+
return { content: [{ type: "text", text:
|
|
630
|
+
"A scoring model already exists. Call build_scoring_model again with force:true to rebuild it from the current GTM context." }] };
|
|
631
|
+
}
|
|
632
|
+
throw e;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
);
|
|
636
|
+
|
|
637
|
+
// ===========================================================================
|
|
638
|
+
// TOOL: connect_integration — POST /v2/workspace/integrations
|
|
639
|
+
// The agent connects a KEY-BASED integration for the user (no clicking through
|
|
640
|
+
// the Integrations page). OAuth providers still need a browser, so this is
|
|
641
|
+
// limited to providers that authenticate with an API key/token.
|
|
642
|
+
// ===========================================================================
|
|
643
|
+
server.tool(
|
|
644
|
+
"connect_integration",
|
|
645
|
+
"Connect a key-based integration for the user — an enrichment, CRM, or sequencer provider that " +
|
|
646
|
+
"authenticates with an API key or token (e.g. Apollo, Prospeo, Instantly, HubSpot private-app " +
|
|
647
|
+
"token, Pipedrive, Attio, Smartlead, HeyReach). Ask the user for the provider's API key, then " +
|
|
648
|
+
"call this; it verifies the credentials before saving. Providers that use a browser sign-in " +
|
|
649
|
+
"(OAuth, e.g. Gmail) can't be connected this way — for those, point the user to the Integrations " +
|
|
650
|
+
"page. After connecting an enrichment provider, the account record starts filling in.",
|
|
651
|
+
{
|
|
652
|
+
provider: z.string().describe("Provider name, lowercase — e.g. 'apollo', 'prospeo', 'instantly', 'hubspot', 'pipedrive', 'attio'."),
|
|
653
|
+
credentials: z.record(z.string()).describe("The provider's credentials as key/value, e.g. { api_key: '...' } or { access_token: '...' }."),
|
|
654
|
+
name: z.string().optional().describe("Optional label for the connection."),
|
|
655
|
+
},
|
|
656
|
+
async ({ provider, credentials, name }) => {
|
|
657
|
+
try {
|
|
658
|
+
const r = await post("/v2/workspace/integrations", { provider, credentials, name });
|
|
659
|
+
return { content: [{ type: "text", text: `Connected ${r.connection?.provider ?? provider}.${r.message ? ` ${r.message}` : ""}` }] };
|
|
660
|
+
} catch (e) {
|
|
661
|
+
const msg = String(e?.message ?? e);
|
|
662
|
+
if (msg.includes("oauth_provider")) {
|
|
663
|
+
return { content: [{ type: "text", text: `${provider} uses a browser sign-in, so it can't be connected with a key. Tell the user to connect it on the Integrations page.` }] };
|
|
664
|
+
}
|
|
665
|
+
if (msg.includes("invalid_credentials")) {
|
|
666
|
+
return { content: [{ type: "text", text: `Those credentials didn't verify for ${provider}. Ask the user to double-check the key and try again.` }] };
|
|
667
|
+
}
|
|
668
|
+
if (msg.includes("unknown_provider")) {
|
|
669
|
+
return { content: [{ type: "text", text: `No provider named "${provider}". Ask the user which tool they mean.` }] };
|
|
670
|
+
}
|
|
671
|
+
throw e;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
);
|
|
675
|
+
|
|
676
|
+
// ===========================================================================
|
|
677
|
+
// TOOL: configure_crm_sync — POST /v2/workspace/crm-sync
|
|
678
|
+
// The agent sets the CRM sync rules — the same options as the CRM Sync page.
|
|
679
|
+
// The CRM must already be connected (OAuth connect stays a human step).
|
|
680
|
+
// ===========================================================================
|
|
681
|
+
server.tool(
|
|
682
|
+
"configure_crm_sync",
|
|
683
|
+
"Configure how Nous keeps a connected CRM in sync — the same settings as the CRM Sync page. The " +
|
|
684
|
+
"CRM must already be connected (HubSpot/Pipedrive/Attio). Set any of: auto-sync (daily pull), " +
|
|
685
|
+
"push of touchpoints, the create policy (when a new record is auto-created and the ICP-fit " +
|
|
686
|
+
"threshold), and the hygiene cadence. Only send the fields you want to change. If it reports the " +
|
|
687
|
+
"CRM isn't connected, tell the user to connect it on the Integrations page first.",
|
|
688
|
+
{
|
|
689
|
+
provider: z.enum(["hubspot", "pipedrive", "attio"]).describe("Which connected CRM to configure."),
|
|
690
|
+
autoSync: z.boolean().optional().describe("Pull contacts/companies/deals daily."),
|
|
691
|
+
pushActivities: z.boolean().optional().describe("Push touchpoints (meetings, replies, proposals) back to the CRM."),
|
|
692
|
+
createInCrm: z.boolean().optional().describe("Auto-create new records in the CRM when they earn it."),
|
|
693
|
+
createTrigger: z.enum(["any_reply_or_meeting", "positive_reply_or_meeting", "meeting_only", "interested_stage"]).optional()
|
|
694
|
+
.describe("What earns a new record."),
|
|
695
|
+
createRequireIcpFit: z.boolean().optional().describe("Require an ICP-fit score before creating a record."),
|
|
696
|
+
createIcpThreshold: z.number().optional().describe("Minimum ICP-fit score to create (0-100)."),
|
|
697
|
+
hygieneEnabled: z.boolean().optional().describe("Run scheduled hygiene reconciliation."),
|
|
698
|
+
hygieneCadence: z.enum(["weekly", "monthly"]).optional().describe("How often hygiene runs."),
|
|
699
|
+
},
|
|
700
|
+
async (args) => {
|
|
701
|
+
try {
|
|
702
|
+
const r = await post("/v2/workspace/crm-sync", args);
|
|
703
|
+
const c = r.config ?? {};
|
|
704
|
+
return { content: [{ type: "text", text:
|
|
705
|
+
`CRM sync configured for ${args.provider}. auto-sync ${c.auto_sync ? "on" : "off"}, ` +
|
|
706
|
+
`create ${c.create_in_crm ? `on (${c.create_trigger}${c.create_require_icp_fit ? `, ICP ≥ ${c.create_icp_threshold}` : ""})` : "off"}, ` +
|
|
707
|
+
`hygiene ${c.hygiene_enabled ? c.hygiene_cadence : "off"}.` }] };
|
|
708
|
+
} catch (e) {
|
|
709
|
+
const msg = String(e?.message ?? e);
|
|
710
|
+
if (msg.includes("crm_not_connected")) {
|
|
711
|
+
return { content: [{ type: "text", text: `${args.provider} isn't connected yet. Tell the user to connect it on the Integrations page, then configure sync.` }] };
|
|
712
|
+
}
|
|
713
|
+
throw e;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
);
|
|
717
|
+
|
|
718
|
+
// ===========================================================================
|
|
719
|
+
// TOOL: set_trigger / list_triggers — /v2/workspace/triggers
|
|
720
|
+
// Outbound event triggers (webhooks) — wire the user's stack to fire when the
|
|
721
|
+
// record changes.
|
|
722
|
+
// ===========================================================================
|
|
723
|
+
server.tool(
|
|
724
|
+
"set_trigger",
|
|
725
|
+
"Create an outbound event trigger (a webhook) so an external tool is notified when something " +
|
|
726
|
+
"happens in the workspace — e.g. a new contact, a reply, a meeting booked. Pass the destination " +
|
|
727
|
+
"URL and which events to fire on. Call list_triggers first to see the available event names.",
|
|
728
|
+
{
|
|
729
|
+
url: z.string().describe("The destination URL the event is POSTed to."),
|
|
730
|
+
events: z.array(z.string()).describe("Event names to fire on (see list_triggers for the catalog)."),
|
|
731
|
+
name: z.string().optional().describe("Optional label for the trigger."),
|
|
732
|
+
},
|
|
733
|
+
async ({ url, events, name }) => {
|
|
734
|
+
try {
|
|
735
|
+
const r = await post("/v2/workspace/triggers", { url, events, name });
|
|
736
|
+
return { content: [{ type: "text", text: `Trigger created for ${events.join(", ")} → ${url}.` }] };
|
|
737
|
+
} catch (e) {
|
|
738
|
+
const msg = String(e?.message ?? e);
|
|
739
|
+
return { content: [{ type: "text", text: `Couldn't create the trigger: ${msg}. Call list_triggers to see valid event names.` }] };
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
);
|
|
743
|
+
server.tool(
|
|
744
|
+
"list_triggers",
|
|
745
|
+
"List the workspace's outbound event triggers (webhooks) and the catalog of available event names.",
|
|
746
|
+
{},
|
|
747
|
+
async () => {
|
|
748
|
+
const r = await get("/v2/workspace/triggers");
|
|
749
|
+
const lines = [];
|
|
750
|
+
if (r.triggers?.length) {
|
|
751
|
+
lines.push(`TRIGGERS (${r.triggers.length}):`);
|
|
752
|
+
for (const t of r.triggers) lines.push(` ${t.name || "(unnamed)"} → ${t.url} [${(t.events || []).join(", ")}]`);
|
|
753
|
+
} else {
|
|
754
|
+
lines.push("No triggers set up yet.");
|
|
755
|
+
}
|
|
756
|
+
if (r.available_events?.length) {
|
|
757
|
+
lines.push("", `AVAILABLE EVENTS: ${r.available_events.join(", ")}`);
|
|
758
|
+
}
|
|
759
|
+
return { content: [{ type: "text", text: lines.join("\n").trim() }] };
|
|
760
|
+
}
|
|
761
|
+
);
|
|
762
|
+
|
|
488
763
|
return server;
|
|
489
764
|
}
|