@opennous/mcp 0.46.0 → 0.47.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 +1 -1
- package/src/http.js +1 -1
- package/src/server.js +164 -43
package/package.json
CHANGED
package/src/http.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* Nous MCP Server — hosted, multi-tenant HTTP entrypoint (mcp.opennous.cloud).
|
|
5
5
|
*
|
|
6
|
-
* Serves the same
|
|
6
|
+
* Serves the same tool set (server.js) over the MCP Streamable HTTP transport,
|
|
7
7
|
* so cloud clients that cannot launch a local process — n8n cloud above all —
|
|
8
8
|
* can connect by pasting one URL plus their workspace API key.
|
|
9
9
|
*
|
package/src/server.js
CHANGED
|
@@ -10,34 +10,23 @@
|
|
|
10
10
|
* sees raw rows — it gets engineered, epistemics-tagged context. It never
|
|
11
11
|
* "updates" — it records observations; Nous derives.
|
|
12
12
|
*
|
|
13
|
-
* Tools:
|
|
14
|
-
* get_context
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* record
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
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_icp_model — build/rebuild the ICP scoring model from the recorded GTM context
|
|
27
|
-
* train_icp_model — build the ICP model from real closed-won/lost deals (contrastive lift)
|
|
28
|
-
* sync_icp — sync the user's EXISTING ICP/positioning files into Nous (file → graph)
|
|
29
|
-
* export_icp_model — get the learned ICP model as a block to write back into their ICP file (graph → file)
|
|
30
|
-
* connect_integration — connect a key-based integration (Apollo, Prospeo, HubSpot, …)
|
|
31
|
-
* configure_crm_sync — set CRM sync rules (auto-sync, create policy, hygiene cadence)
|
|
32
|
-
* sync_crm_now — run an immediate incremental/full CRM pull (don't wait for the daily cron)
|
|
33
|
-
* get_routing_preferences — Claude Code routing prefs to default GTM to Nous (write to CLAUDE.md)
|
|
13
|
+
* Tools, by group (the authoritative live catalog is `node scripts/list-tools.mjs`):
|
|
14
|
+
* READ get_context · get_account · verify · query · attention · get_action_items ·
|
|
15
|
+
* pipeline · pipeline_intelligence · campaign_performance · score ·
|
|
16
|
+
* search_notes · get_foundation · get_insights
|
|
17
|
+
* WRITE record · record_signal · save_note · propose_vault_file · merge_contacts ·
|
|
18
|
+
* sync_foundation
|
|
19
|
+
* ACT send_linkedin_message
|
|
20
|
+
* RUN get_workspace_status · set_workspace_profile · build_icp_model · train_icp_model ·
|
|
21
|
+
* sync_icp · export_icp_model · connect_integration · configure_crm_sync ·
|
|
22
|
+
* sync_crm_now · scrape_engagers · get_routing_preferences
|
|
34
23
|
*/
|
|
35
24
|
|
|
36
25
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
37
26
|
import { z } from "zod";
|
|
38
27
|
import { get, post } from "./client.js";
|
|
39
28
|
|
|
40
|
-
export const SERVER_VERSION = "0.
|
|
29
|
+
export const SERVER_VERSION = "0.47.0";
|
|
41
30
|
|
|
42
31
|
// ─── helpers ──────────────────────────────────────────────────────────────────
|
|
43
32
|
|
|
@@ -508,7 +497,7 @@ export function createServer() {
|
|
|
508
497
|
"Retrieve and summarise activity across many people. Three powers:\n" +
|
|
509
498
|
" 1. return:'entities' groups results by person/company (one row per entity, ranked by " +
|
|
510
499
|
"most-recent matching activity). Use for 'hottest leads', 'who replied this week', " +
|
|
511
|
-
"'who's in
|
|
500
|
+
"'who's in the negotiation stage'.\n" +
|
|
512
501
|
" 2. `without` subtracts entities — 'sent in 5d MINUS replied in 5d' = 'no-reply leads'. " +
|
|
513
502
|
"'activity in 30d MINUS activity in 5d' = 'cooled leads'.\n" +
|
|
514
503
|
" 3. rollups.by_value appears when scope.kind='state' — counts entities by current value " +
|
|
@@ -679,19 +668,31 @@ export function createServer() {
|
|
|
679
668
|
const camps = r.campaigns || [];
|
|
680
669
|
if (!camps.length) return { content: [{ type: "text", text: "No campaign replies logged yet." }] };
|
|
681
670
|
const t = r.totals || {};
|
|
682
|
-
const pct = (n) => `${Math.round(
|
|
671
|
+
const pct = (n) => n == null ? "—" : `${Math.round(n * 100)}%`;
|
|
683
672
|
const lines = camps.map(c => {
|
|
673
|
+
const cap = c.captured || {};
|
|
674
|
+
const pt = c.provider_totals, pr = c.provider_rates;
|
|
684
675
|
const tiers = c.tier_of_positive || {};
|
|
685
676
|
const good = (tiers.tier_1 || 0) + (tiers.tier_2 || 0);
|
|
686
|
-
const
|
|
687
|
-
|
|
677
|
+
const funnel = pt
|
|
678
|
+
? ` funnel: ${pt.sent ?? "?"} sent → ${pt.contacted ?? "?"} contacted → ${pt.replies ?? "?"} replies (${pct(pr?.reply_rate)}) → ${pt.opportunities ?? "?"} opportunities (${pct(pr?.opportunity_rate)})`
|
|
679
|
+
: ` funnel: (no provider analytics yet)`;
|
|
680
|
+
const conv = `${c.conversions || 0} client${c.conversions === 1 ? "" : "s"}${c.median_days_to_client != null ? ` (avg ${c.median_days_to_client}d)` : ""}${c.in_progress ? `, ${c.in_progress} in evaluation` : ""}`;
|
|
681
|
+
const steps = (c.steps || []).map(s => {
|
|
682
|
+
const subj = s.subject ? ` — "${s.subject}"` : "";
|
|
683
|
+
return ` step ${s.step ?? "—"}${subj}: ${s.positive}/${s.replies} positive (${pct(s.positive_rate)})`;
|
|
684
|
+
}).join("\n");
|
|
685
|
+
const obj = (c.top_objections || []).length
|
|
686
|
+
? `\n objections/pains: ${c.top_objections.slice(0, 4).map(o => `“${String(o).slice(0, 90)}”`).join("; ")}`
|
|
687
|
+
: "";
|
|
688
688
|
return ` ${c.campaign_name || c.campaign_id}\n` +
|
|
689
|
-
|
|
689
|
+
funnel + "\n" +
|
|
690
|
+
` captured sample: ${cap.replies} replies — ${cap.positive} positive (${pct(cap.positive_rate)}), ${cap.neutral} neutral, ${cap.negative} negative\n` +
|
|
690
691
|
` converted: ${conv}\n` +
|
|
691
|
-
` positive repliers: avg ICP ${c.avg_icp_of_positive ?? "?"}, ${good} tier-1/2 of ${
|
|
692
|
-
(steps ? `\n${steps}` : "");
|
|
692
|
+
` positive repliers: avg ICP ${c.avg_icp_of_positive ?? "?"}, ${good} tier-1/2 of ${cap.positive}` +
|
|
693
|
+
(steps ? `\n${steps}` : "") + obj;
|
|
693
694
|
});
|
|
694
|
-
const head = `Campaign performance (${t.conversions || 0} clients
|
|
695
|
+
const head = `Campaign performance (${t.conversions || 0} clients · ${t.sent || 0} sent · ${t.positive || 0}/${t.replies || 0} captured replies positive):`;
|
|
695
696
|
return { content: [{ type: "text", text: `${head}\n${lines.join("\n")}` }] };
|
|
696
697
|
}
|
|
697
698
|
);
|
|
@@ -712,11 +713,28 @@ export function createServer() {
|
|
|
712
713
|
const r = await get("/v2/pipeline/intelligence", {});
|
|
713
714
|
const pct = (n) => n == null ? "—" : `${Math.round(n * 100)}%`;
|
|
714
715
|
const cc = r.current_stage_counts || {};
|
|
715
|
-
|
|
716
|
-
|
|
716
|
+
// The funnel, lowest → highest, then the terminals. SOURCE OF TRUTH:
|
|
717
|
+
// PIPELINE_LADDER + TERMINAL_STAGES in packages/core/src/pipeline.ts. This
|
|
718
|
+
// package publishes standalone and has no @nous/core dependency, so the
|
|
719
|
+
// list is copied here. Keep it in step with core.
|
|
720
|
+
const order = [
|
|
721
|
+
"identified", "connected", "interested", "meeting_booked",
|
|
722
|
+
"discovery", "demo", "negotiation", "closed_won",
|
|
723
|
+
"closed_lost", "disqualified", "churned",
|
|
724
|
+
];
|
|
725
|
+
// Ladder order first, then any stage the API sent that this copy doesn't
|
|
726
|
+
// know about. The old list had no proposal, negotiation or demo in it and
|
|
727
|
+
// the filter silently dropped everything it missed, so late-funnel accounts
|
|
728
|
+
// disappeared from the distribution entirely. A stage must never vanish
|
|
729
|
+
// just because a copy of the list went stale.
|
|
730
|
+
const byStage = (obj, keep) => [
|
|
731
|
+
...order.filter(s => keep(obj[s])),
|
|
732
|
+
...Object.keys(obj).filter(s => !order.includes(s) && keep(obj[s])),
|
|
733
|
+
];
|
|
734
|
+
const dist = byStage(cc, v => !!v).map(s => `${s} ${cc[s]}`).join(" · ") || "no staged accounts";
|
|
717
735
|
const conv = (r.stage_conversion || []).map(c => ` ${c.from} → ${c.to}: ${pct(c.rate)} (${c.reached_to}/${c.reached_from})`).join("\n");
|
|
718
736
|
const dis = r.median_days_in_stage || {};
|
|
719
|
-
const inStage =
|
|
737
|
+
const inStage = byStage(dis, v => v != null).map(s => `${s} ${dis[s]}d`).join(" · ");
|
|
720
738
|
const text = `Pipeline (${r.accounts || 0} accounts, ${r.clients || 0} clients):\n` +
|
|
721
739
|
` now: ${dist}\n` +
|
|
722
740
|
` median days to client: ${r.median_days_to_client ?? "— (no wins yet)"}\n` +
|
|
@@ -726,6 +744,43 @@ export function createServer() {
|
|
|
726
744
|
}
|
|
727
745
|
);
|
|
728
746
|
|
|
747
|
+
// ===========================================================================
|
|
748
|
+
// TOOL: pipeline — GET /v2/pipeline/portfolio
|
|
749
|
+
// The whole account portfolio in one call: health/band, ICP fit, stage,
|
|
750
|
+
// days-quiet, open flags, open objections, live competitors, engagement trend,
|
|
751
|
+
// multi-threading. The "reason over my whole book" tool.
|
|
752
|
+
// ===========================================================================
|
|
753
|
+
server.tool(
|
|
754
|
+
"pipeline",
|
|
755
|
+
"The whole ACCOUNT PORTFOLIO in one call — every in-touch account with its deal health + band, " +
|
|
756
|
+
"ICP fit, pipeline stage, days-quiet, open re-engagement flags, open-objection count, live competitors, " +
|
|
757
|
+
"engagement trend (rising/steady/cooling/cold), and multi-threading, plus a stage-count breakdown. Use " +
|
|
758
|
+
"for any portfolio question: 'which accounts are cooling', 'who should I re-engage', 'where is competitive " +
|
|
759
|
+
"risk highest', 'what's slipping', 'great-fit deals gone cold', 'analyze my pipeline'. Reason over the list.",
|
|
760
|
+
{ limit: z.number().optional().describe("Cap on accounts returned (default: all, most-actionable first)") },
|
|
761
|
+
async (input) => {
|
|
762
|
+
const r = await get("/v2/pipeline/portfolio", input.limit ? { limit: input.limit } : {});
|
|
763
|
+
const stages = Object.entries(r.stage_counts || {}).map(([s, n]) => `${s} ${n}`).join(" · ");
|
|
764
|
+
const rows = (r.accounts || []).slice(0, input.limit || 60).map(a => {
|
|
765
|
+
const bits = [
|
|
766
|
+
a.name,
|
|
767
|
+
`health ${a.health ?? "—"} (${a.band})`,
|
|
768
|
+
a.icp != null ? `ICP ${a.icp}` : null,
|
|
769
|
+
`stage ${a.stage}`,
|
|
770
|
+
a.days_quiet != null ? `quiet ${a.days_quiet}d` : null,
|
|
771
|
+
`engagement ${a.engagement_trend}`,
|
|
772
|
+
a.single_threaded ? "single-threaded" : `${a.engaged_contacts}/${a.total_contacts} engaged`,
|
|
773
|
+
a.open_objections ? `${a.open_objections} open objection${a.open_objections === 1 ? "" : "s"}` : null,
|
|
774
|
+
a.competitors?.length ? `competitors: ${a.competitors.join(", ")}` : null,
|
|
775
|
+
a.flags?.length ? `flags: ${a.flags.join(", ")}` : null,
|
|
776
|
+
].filter(Boolean);
|
|
777
|
+
return ` • ${bits.join(" — ")}`;
|
|
778
|
+
}).join("\n");
|
|
779
|
+
const text = `Portfolio (${r.total || 0} in-touch accounts). By stage: ${stages || "—"}\n${rows || " (none)"}`;
|
|
780
|
+
return { content: [{ type: "text", text }] };
|
|
781
|
+
}
|
|
782
|
+
);
|
|
783
|
+
|
|
729
784
|
// ===========================================================================
|
|
730
785
|
// TOOL: get_action_items — GET /v2/action-items
|
|
731
786
|
// Commitments extracted from meetings/emails — what you owe each account.
|
|
@@ -798,10 +853,6 @@ export function createServer() {
|
|
|
798
853
|
}
|
|
799
854
|
);
|
|
800
855
|
|
|
801
|
-
// get_gtm_profile removed: the user's GTM lives in their files, mirrored into
|
|
802
|
-
// the graph as foundations (get_foundation) plus the learned ICP model. Read
|
|
803
|
-
// get_foundation for the user's own rules, ICP, and positioning.
|
|
804
|
-
|
|
805
856
|
// ===========================================================================
|
|
806
857
|
// TOOLS: get_foundation / sync_foundation — the POLICY layer (vs. facts).
|
|
807
858
|
// Foundations are versioned rule-docs that GOVERN agent behavior: voice, outreach,
|
|
@@ -889,12 +940,6 @@ export function createServer() {
|
|
|
889
940
|
"category to list all four.",
|
|
890
941
|
getInsightsSchema, getInsightsHandler);
|
|
891
942
|
|
|
892
|
-
// The GTM context is no longer written through a dedicated MCP tool. In the file
|
|
893
|
-
// symbiosis model the user's own files (context/icp.md, positioning.md, …) are
|
|
894
|
-
// the source of truth: the agent edits those with its own file tools and calls
|
|
895
|
-
// `sync_icp` to sync them into the graph (and `export_icp_model` to write the learned
|
|
896
|
-
// model back). The shared POST /v2/workspace/facts route still backs that import.
|
|
897
|
-
|
|
898
943
|
// ===========================================================================
|
|
899
944
|
// TOOL: save_note — POST /v2/notes
|
|
900
945
|
// Attach a long-form artifact to a CONTACT: a meeting brief you wrote, a
|
|
@@ -928,6 +973,36 @@ export function createServer() {
|
|
|
928
973
|
},
|
|
929
974
|
);
|
|
930
975
|
|
|
976
|
+
// ===========================================================================
|
|
977
|
+
// TOOL: propose_vault_file — POST /v2/personal/propose
|
|
978
|
+
// Propose a markdown file into the member's PERSONAL vault. It lands in their
|
|
979
|
+
// inbox as a proposal — the member approves it before it is filed into a folder
|
|
980
|
+
// or synced to their Git. This is how an agent contributes to a member's own
|
|
981
|
+
// notes (thoughts, decisions, briefs, content) without writing anything without
|
|
982
|
+
// consent. Distinct from `save_note` (which attaches a document to a CONTACT's
|
|
983
|
+
// record) — this is the member's private vault, not an account.
|
|
984
|
+
// ===========================================================================
|
|
985
|
+
server.tool(
|
|
986
|
+
"propose_vault_file",
|
|
987
|
+
"Propose a markdown file into the member's personal vault (their private notes, not an " +
|
|
988
|
+
"account). It lands in their INBOX as a proposal — the member approves it before it's filed " +
|
|
989
|
+
"into a folder and synced to their Git; nothing is written without their approval. Use this to " +
|
|
990
|
+
"draft a thought, a decision record, a brief, or content for the member to review and keep. " +
|
|
991
|
+
"Pick the destination folder. This is NOT for notes on a contact (use save_note) and NOT for the " +
|
|
992
|
+
"GTM profile (use sync_icp).",
|
|
993
|
+
{
|
|
994
|
+
folder: z.enum(["inbox", "thoughts", "decisions", "projects", "briefs", "content", "company"])
|
|
995
|
+
.describe("Where it files once approved. ROUTING: briefs = a one-off account or meeting brief, filed FLAT (no subfolder). projects = ongoing recurring analysis by area (with a subfolder): pipeline/funnel review -> projects, subfolder 'pipeline'; campaign analysis -> projects, subfolder 'campaigns'; an initiative -> projects, subfolder = its name. content = anything to publish/send (post, newsletter, outbound copy, market/research write-up). decisions = a decision. thoughts = a loose idea. company = the company's own context. Don't pick 'inbox' — that's where it lands to await approval."),
|
|
996
|
+
name: z.string().describe("The file name, ending in .md, for what it is plus the date, e.g. 'Pipeline Review — 2026-08-12.md'."),
|
|
997
|
+
content: z.string().describe("The full markdown content of the file."),
|
|
998
|
+
subfolder: z.string().optional().describe("REQUIRED for 'projects' — the area: 'pipeline', 'campaigns', or the initiative name. Leave empty for 'briefs' (flat) and other folders."),
|
|
999
|
+
},
|
|
1000
|
+
async ({ folder, name, content, subfolder }) => {
|
|
1001
|
+
await post("/v2/personal/propose", { folder, name, content, subfolder });
|
|
1002
|
+
return { content: [{ type: "text", text: `Proposed "${name}" into ${folder}. It's waiting in the member's inbox for approval.` }] };
|
|
1003
|
+
},
|
|
1004
|
+
);
|
|
1005
|
+
|
|
931
1006
|
// ===========================================================================
|
|
932
1007
|
// TOOL: search_notes — POST /v2/notes/search
|
|
933
1008
|
// Semantic search over saved notes & documents (briefs, transcripts, notes).
|
|
@@ -1474,6 +1549,52 @@ export function createServer() {
|
|
|
1474
1549
|
}
|
|
1475
1550
|
);
|
|
1476
1551
|
|
|
1552
|
+
// ===========================================================================
|
|
1553
|
+
// TOOL: send_linkedin_message — the ACT layer. An OUTWARD ACTION: it goes out
|
|
1554
|
+
// through Unipile immediately, it is not a draft. The agent should read the
|
|
1555
|
+
// 'voice'/'outreach' foundations first and `record` the touch after. Derives the
|
|
1556
|
+
// workspace from the API key and runs on the workspace's connected LinkedIn
|
|
1557
|
+
// account. POST /api/linkedin/send-message (verifyAuthEither).
|
|
1558
|
+
// ===========================================================================
|
|
1559
|
+
server.tool(
|
|
1560
|
+
"send_linkedin_message",
|
|
1561
|
+
"SEND a LinkedIn direct message for real, right now, from the workspace's connected LinkedIn " +
|
|
1562
|
+
"account. This is an OUTWARD ACTION, not a draft — it reaches the person the moment you call it, so " +
|
|
1563
|
+
"only send a message the user has approved. Address it one of three ways: `linkedin_url` (their " +
|
|
1564
|
+
"profile URL) or `linkedin_member_id` to start a new conversation, or `chat_id` to REPLY inside an " +
|
|
1565
|
+
"existing thread. It returns the thread's `chat_id` — keep it so the next reply stays in the same " +
|
|
1566
|
+
"thread. Best practice: read the 'voice' and 'outreach' foundations first so the message obeys the " +
|
|
1567
|
+
"user's rules, and `record` the touch afterward so the graph stays current. Requires a connected " +
|
|
1568
|
+
"LinkedIn account (Integrations); it says so if none is connected.",
|
|
1569
|
+
{
|
|
1570
|
+
text: z.string().min(1).describe("The message body to send."),
|
|
1571
|
+
linkedin_url: z.string().optional().describe("The recipient's LinkedIn profile URL. Provide this OR linkedin_member_id (to start a new chat), OR chat_id (to reply in an existing thread)."),
|
|
1572
|
+
linkedin_member_id: z.string().optional().describe("The recipient's LinkedIn member id, if you have it instead of a profile URL."),
|
|
1573
|
+
chat_id: z.string().optional().describe("An existing conversation's chat_id — pass it to reply in-thread instead of opening a new chat."),
|
|
1574
|
+
},
|
|
1575
|
+
async ({ text, linkedin_url, linkedin_member_id, chat_id }) => {
|
|
1576
|
+
if (!linkedin_url && !linkedin_member_id && !chat_id) {
|
|
1577
|
+
return { content: [{ type: "text", text:
|
|
1578
|
+
"Tell me who to message: pass linkedin_url or linkedin_member_id to start a new chat, or chat_id to reply in an existing thread." }] };
|
|
1579
|
+
}
|
|
1580
|
+
try {
|
|
1581
|
+
const r = await post("/api/linkedin/send-message", { text, linkedin_url, linkedin_member_id, chat_id });
|
|
1582
|
+
return { content: [{ type: "text", text:
|
|
1583
|
+
`Message sent over LinkedIn.${r.chat_id ? ` Thread chat_id: ${r.chat_id} (reuse it to reply in-thread).` : ""}` }] };
|
|
1584
|
+
} catch (e) {
|
|
1585
|
+
const msg =
|
|
1586
|
+
/linkedin_not_connected/.test(e.message)
|
|
1587
|
+
? "No LinkedIn account is connected. Tell the user to connect LinkedIn in Integrations first."
|
|
1588
|
+
: /auth_required/.test(e.message)
|
|
1589
|
+
? "This API key isn't scoped to a workspace, or LinkedIn isn't connected."
|
|
1590
|
+
: /missing_params/.test(e.message)
|
|
1591
|
+
? "Couldn't tell who to message — provide linkedin_url, linkedin_member_id, or chat_id."
|
|
1592
|
+
: `Couldn't send the message: ${e.message}`;
|
|
1593
|
+
return { content: [{ type: "text", text: msg }] };
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
);
|
|
1597
|
+
|
|
1477
1598
|
// ===========================================================================
|
|
1478
1599
|
// TOOL: get_routing_preferences
|
|
1479
1600
|
// The routing preferences that make THIS agent default to Nous for GTM. The
|