@opennous/mcp 0.46.0 → 0.48.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 +222 -44
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,24 @@
|
|
|
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
|
-
*
|
|
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
|
+
* CORRECT retract_observation · delete_note
|
|
21
|
+
* RUN get_workspace_status · set_workspace_profile · build_icp_model · train_icp_model ·
|
|
22
|
+
* sync_icp · export_icp_model · connect_integration · configure_crm_sync ·
|
|
23
|
+
* sync_crm_now · scrape_engagers · get_routing_preferences
|
|
34
24
|
*/
|
|
35
25
|
|
|
36
26
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
37
27
|
import { z } from "zod";
|
|
38
|
-
import { get, post } from "./client.js";
|
|
28
|
+
import { get, post, del } from "./client.js";
|
|
39
29
|
|
|
40
|
-
export const SERVER_VERSION = "0.
|
|
30
|
+
export const SERVER_VERSION = "0.48.0";
|
|
41
31
|
|
|
42
32
|
// ─── helpers ──────────────────────────────────────────────────────────────────
|
|
43
33
|
|
|
@@ -508,7 +498,7 @@ export function createServer() {
|
|
|
508
498
|
"Retrieve and summarise activity across many people. Three powers:\n" +
|
|
509
499
|
" 1. return:'entities' groups results by person/company (one row per entity, ranked by " +
|
|
510
500
|
"most-recent matching activity). Use for 'hottest leads', 'who replied this week', " +
|
|
511
|
-
"'who's in
|
|
501
|
+
"'who's in the negotiation stage'.\n" +
|
|
512
502
|
" 2. `without` subtracts entities — 'sent in 5d MINUS replied in 5d' = 'no-reply leads'. " +
|
|
513
503
|
"'activity in 30d MINUS activity in 5d' = 'cooled leads'.\n" +
|
|
514
504
|
" 3. rollups.by_value appears when scope.kind='state' — counts entities by current value " +
|
|
@@ -679,19 +669,31 @@ export function createServer() {
|
|
|
679
669
|
const camps = r.campaigns || [];
|
|
680
670
|
if (!camps.length) return { content: [{ type: "text", text: "No campaign replies logged yet." }] };
|
|
681
671
|
const t = r.totals || {};
|
|
682
|
-
const pct = (n) => `${Math.round(
|
|
672
|
+
const pct = (n) => n == null ? "—" : `${Math.round(n * 100)}%`;
|
|
683
673
|
const lines = camps.map(c => {
|
|
674
|
+
const cap = c.captured || {};
|
|
675
|
+
const pt = c.provider_totals, pr = c.provider_rates;
|
|
684
676
|
const tiers = c.tier_of_positive || {};
|
|
685
677
|
const good = (tiers.tier_1 || 0) + (tiers.tier_2 || 0);
|
|
686
|
-
const
|
|
687
|
-
|
|
678
|
+
const funnel = pt
|
|
679
|
+
? ` funnel: ${pt.sent ?? "?"} sent → ${pt.contacted ?? "?"} contacted → ${pt.replies ?? "?"} replies (${pct(pr?.reply_rate)}) → ${pt.opportunities ?? "?"} opportunities (${pct(pr?.opportunity_rate)})`
|
|
680
|
+
: ` funnel: (no provider analytics yet)`;
|
|
681
|
+
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` : ""}`;
|
|
682
|
+
const steps = (c.steps || []).map(s => {
|
|
683
|
+
const subj = s.subject ? ` — "${s.subject}"` : "";
|
|
684
|
+
return ` step ${s.step ?? "—"}${subj}: ${s.positive}/${s.replies} positive (${pct(s.positive_rate)})`;
|
|
685
|
+
}).join("\n");
|
|
686
|
+
const obj = (c.top_objections || []).length
|
|
687
|
+
? `\n objections/pains: ${c.top_objections.slice(0, 4).map(o => `“${String(o).slice(0, 90)}”`).join("; ")}`
|
|
688
|
+
: "";
|
|
688
689
|
return ` ${c.campaign_name || c.campaign_id}\n` +
|
|
689
|
-
|
|
690
|
+
funnel + "\n" +
|
|
691
|
+
` captured sample: ${cap.replies} replies — ${cap.positive} positive (${pct(cap.positive_rate)}), ${cap.neutral} neutral, ${cap.negative} negative\n` +
|
|
690
692
|
` converted: ${conv}\n` +
|
|
691
|
-
` positive repliers: avg ICP ${c.avg_icp_of_positive ?? "?"}, ${good} tier-1/2 of ${
|
|
692
|
-
(steps ? `\n${steps}` : "");
|
|
693
|
+
` positive repliers: avg ICP ${c.avg_icp_of_positive ?? "?"}, ${good} tier-1/2 of ${cap.positive}` +
|
|
694
|
+
(steps ? `\n${steps}` : "") + obj;
|
|
693
695
|
});
|
|
694
|
-
const head = `Campaign performance (${t.conversions || 0} clients
|
|
696
|
+
const head = `Campaign performance (${t.conversions || 0} clients · ${t.sent || 0} sent · ${t.positive || 0}/${t.replies || 0} captured replies positive):`;
|
|
695
697
|
return { content: [{ type: "text", text: `${head}\n${lines.join("\n")}` }] };
|
|
696
698
|
}
|
|
697
699
|
);
|
|
@@ -712,11 +714,28 @@ export function createServer() {
|
|
|
712
714
|
const r = await get("/v2/pipeline/intelligence", {});
|
|
713
715
|
const pct = (n) => n == null ? "—" : `${Math.round(n * 100)}%`;
|
|
714
716
|
const cc = r.current_stage_counts || {};
|
|
715
|
-
|
|
716
|
-
|
|
717
|
+
// The funnel, lowest → highest, then the terminals. SOURCE OF TRUTH:
|
|
718
|
+
// PIPELINE_LADDER + TERMINAL_STAGES in packages/core/src/pipeline.ts. This
|
|
719
|
+
// package publishes standalone and has no @nous/core dependency, so the
|
|
720
|
+
// list is copied here. Keep it in step with core.
|
|
721
|
+
const order = [
|
|
722
|
+
"identified", "connected", "interested", "meeting_booked",
|
|
723
|
+
"discovery", "demo", "negotiation", "closed_won",
|
|
724
|
+
"closed_lost", "disqualified", "churned",
|
|
725
|
+
];
|
|
726
|
+
// Ladder order first, then any stage the API sent that this copy doesn't
|
|
727
|
+
// know about. The old list had no proposal, negotiation or demo in it and
|
|
728
|
+
// the filter silently dropped everything it missed, so late-funnel accounts
|
|
729
|
+
// disappeared from the distribution entirely. A stage must never vanish
|
|
730
|
+
// just because a copy of the list went stale.
|
|
731
|
+
const byStage = (obj, keep) => [
|
|
732
|
+
...order.filter(s => keep(obj[s])),
|
|
733
|
+
...Object.keys(obj).filter(s => !order.includes(s) && keep(obj[s])),
|
|
734
|
+
];
|
|
735
|
+
const dist = byStage(cc, v => !!v).map(s => `${s} ${cc[s]}`).join(" · ") || "no staged accounts";
|
|
717
736
|
const conv = (r.stage_conversion || []).map(c => ` ${c.from} → ${c.to}: ${pct(c.rate)} (${c.reached_to}/${c.reached_from})`).join("\n");
|
|
718
737
|
const dis = r.median_days_in_stage || {};
|
|
719
|
-
const inStage =
|
|
738
|
+
const inStage = byStage(dis, v => v != null).map(s => `${s} ${dis[s]}d`).join(" · ");
|
|
720
739
|
const text = `Pipeline (${r.accounts || 0} accounts, ${r.clients || 0} clients):\n` +
|
|
721
740
|
` now: ${dist}\n` +
|
|
722
741
|
` median days to client: ${r.median_days_to_client ?? "— (no wins yet)"}\n` +
|
|
@@ -726,6 +745,43 @@ export function createServer() {
|
|
|
726
745
|
}
|
|
727
746
|
);
|
|
728
747
|
|
|
748
|
+
// ===========================================================================
|
|
749
|
+
// TOOL: pipeline — GET /v2/pipeline/portfolio
|
|
750
|
+
// The whole account portfolio in one call: health/band, ICP fit, stage,
|
|
751
|
+
// days-quiet, open flags, open objections, live competitors, engagement trend,
|
|
752
|
+
// multi-threading. The "reason over my whole book" tool.
|
|
753
|
+
// ===========================================================================
|
|
754
|
+
server.tool(
|
|
755
|
+
"pipeline",
|
|
756
|
+
"The whole ACCOUNT PORTFOLIO in one call — every in-touch account with its deal health + band, " +
|
|
757
|
+
"ICP fit, pipeline stage, days-quiet, open re-engagement flags, open-objection count, live competitors, " +
|
|
758
|
+
"engagement trend (rising/steady/cooling/cold), and multi-threading, plus a stage-count breakdown. Use " +
|
|
759
|
+
"for any portfolio question: 'which accounts are cooling', 'who should I re-engage', 'where is competitive " +
|
|
760
|
+
"risk highest', 'what's slipping', 'great-fit deals gone cold', 'analyze my pipeline'. Reason over the list.",
|
|
761
|
+
{ limit: z.number().optional().describe("Cap on accounts returned (default: all, most-actionable first)") },
|
|
762
|
+
async (input) => {
|
|
763
|
+
const r = await get("/v2/pipeline/portfolio", input.limit ? { limit: input.limit } : {});
|
|
764
|
+
const stages = Object.entries(r.stage_counts || {}).map(([s, n]) => `${s} ${n}`).join(" · ");
|
|
765
|
+
const rows = (r.accounts || []).slice(0, input.limit || 60).map(a => {
|
|
766
|
+
const bits = [
|
|
767
|
+
a.name,
|
|
768
|
+
`health ${a.health ?? "—"} (${a.band})`,
|
|
769
|
+
a.icp != null ? `ICP ${a.icp}` : null,
|
|
770
|
+
`stage ${a.stage}`,
|
|
771
|
+
a.days_quiet != null ? `quiet ${a.days_quiet}d` : null,
|
|
772
|
+
`engagement ${a.engagement_trend}`,
|
|
773
|
+
a.single_threaded ? "single-threaded" : `${a.engaged_contacts}/${a.total_contacts} engaged`,
|
|
774
|
+
a.open_objections ? `${a.open_objections} open objection${a.open_objections === 1 ? "" : "s"}` : null,
|
|
775
|
+
a.competitors?.length ? `competitors: ${a.competitors.join(", ")}` : null,
|
|
776
|
+
a.flags?.length ? `flags: ${a.flags.join(", ")}` : null,
|
|
777
|
+
].filter(Boolean);
|
|
778
|
+
return ` • ${bits.join(" — ")}`;
|
|
779
|
+
}).join("\n");
|
|
780
|
+
const text = `Portfolio (${r.total || 0} in-touch accounts). By stage: ${stages || "—"}\n${rows || " (none)"}`;
|
|
781
|
+
return { content: [{ type: "text", text }] };
|
|
782
|
+
}
|
|
783
|
+
);
|
|
784
|
+
|
|
729
785
|
// ===========================================================================
|
|
730
786
|
// TOOL: get_action_items — GET /v2/action-items
|
|
731
787
|
// Commitments extracted from meetings/emails — what you owe each account.
|
|
@@ -798,10 +854,6 @@ export function createServer() {
|
|
|
798
854
|
}
|
|
799
855
|
);
|
|
800
856
|
|
|
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
857
|
// ===========================================================================
|
|
806
858
|
// TOOLS: get_foundation / sync_foundation — the POLICY layer (vs. facts).
|
|
807
859
|
// Foundations are versioned rule-docs that GOVERN agent behavior: voice, outreach,
|
|
@@ -889,12 +941,6 @@ export function createServer() {
|
|
|
889
941
|
"category to list all four.",
|
|
890
942
|
getInsightsSchema, getInsightsHandler);
|
|
891
943
|
|
|
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
944
|
// ===========================================================================
|
|
899
945
|
// TOOL: save_note — POST /v2/notes
|
|
900
946
|
// Attach a long-form artifact to a CONTACT: a meeting brief you wrote, a
|
|
@@ -928,6 +974,36 @@ export function createServer() {
|
|
|
928
974
|
},
|
|
929
975
|
);
|
|
930
976
|
|
|
977
|
+
// ===========================================================================
|
|
978
|
+
// TOOL: propose_vault_file — POST /v2/personal/propose
|
|
979
|
+
// Propose a markdown file into the member's PERSONAL vault. It lands in their
|
|
980
|
+
// inbox as a proposal — the member approves it before it is filed into a folder
|
|
981
|
+
// or synced to their Git. This is how an agent contributes to a member's own
|
|
982
|
+
// notes (thoughts, decisions, briefs, content) without writing anything without
|
|
983
|
+
// consent. Distinct from `save_note` (which attaches a document to a CONTACT's
|
|
984
|
+
// record) — this is the member's private vault, not an account.
|
|
985
|
+
// ===========================================================================
|
|
986
|
+
server.tool(
|
|
987
|
+
"propose_vault_file",
|
|
988
|
+
"Propose a markdown file into the member's personal vault (their private notes, not an " +
|
|
989
|
+
"account). It lands in their INBOX as a proposal — the member approves it before it's filed " +
|
|
990
|
+
"into a folder and synced to their Git; nothing is written without their approval. Use this to " +
|
|
991
|
+
"draft a thought, a decision record, a brief, or content for the member to review and keep. " +
|
|
992
|
+
"Pick the destination folder. This is NOT for notes on a contact (use save_note) and NOT for the " +
|
|
993
|
+
"GTM profile (use sync_icp).",
|
|
994
|
+
{
|
|
995
|
+
folder: z.enum(["inbox", "thoughts", "decisions", "projects", "briefs", "content", "company"])
|
|
996
|
+
.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."),
|
|
997
|
+
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'."),
|
|
998
|
+
content: z.string().describe("The full markdown content of the file."),
|
|
999
|
+
subfolder: z.string().optional().describe("REQUIRED for 'projects' — the area: 'pipeline', 'campaigns', or the initiative name. Leave empty for 'briefs' (flat) and other folders."),
|
|
1000
|
+
},
|
|
1001
|
+
async ({ folder, name, content, subfolder }) => {
|
|
1002
|
+
await post("/v2/personal/propose", { folder, name, content, subfolder });
|
|
1003
|
+
return { content: [{ type: "text", text: `Proposed "${name}" into ${folder}. It's waiting in the member's inbox for approval.` }] };
|
|
1004
|
+
},
|
|
1005
|
+
);
|
|
1006
|
+
|
|
931
1007
|
// ===========================================================================
|
|
932
1008
|
// TOOL: search_notes — POST /v2/notes/search
|
|
933
1009
|
// Semantic search over saved notes & documents (briefs, transcripts, notes).
|
|
@@ -1474,6 +1550,108 @@ export function createServer() {
|
|
|
1474
1550
|
}
|
|
1475
1551
|
);
|
|
1476
1552
|
|
|
1553
|
+
// ===========================================================================
|
|
1554
|
+
// TOOL: send_linkedin_message — the ACT layer. An OUTWARD ACTION: it goes out
|
|
1555
|
+
// through Unipile immediately, it is not a draft. The agent should read the
|
|
1556
|
+
// 'voice'/'outreach' foundations first and `record` the touch after. Derives the
|
|
1557
|
+
// workspace from the API key and runs on the workspace's connected LinkedIn
|
|
1558
|
+
// account. POST /api/linkedin/send-message (verifyAuthEither).
|
|
1559
|
+
// ===========================================================================
|
|
1560
|
+
server.tool(
|
|
1561
|
+
"send_linkedin_message",
|
|
1562
|
+
"SEND a LinkedIn direct message for real, right now, from the workspace's connected LinkedIn " +
|
|
1563
|
+
"account. This is an OUTWARD ACTION, not a draft — it reaches the person the moment you call it, so " +
|
|
1564
|
+
"only send a message the user has approved. Address it one of three ways: `linkedin_url` (their " +
|
|
1565
|
+
"profile URL) or `linkedin_member_id` to start a new conversation, or `chat_id` to REPLY inside an " +
|
|
1566
|
+
"existing thread. It returns the thread's `chat_id` — keep it so the next reply stays in the same " +
|
|
1567
|
+
"thread. Best practice: read the 'voice' and 'outreach' foundations first so the message obeys the " +
|
|
1568
|
+
"user's rules, and `record` the touch afterward so the graph stays current. Requires a connected " +
|
|
1569
|
+
"LinkedIn account (Integrations); it says so if none is connected.",
|
|
1570
|
+
{
|
|
1571
|
+
text: z.string().min(1).describe("The message body to send."),
|
|
1572
|
+
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)."),
|
|
1573
|
+
linkedin_member_id: z.string().optional().describe("The recipient's LinkedIn member id, if you have it instead of a profile URL."),
|
|
1574
|
+
chat_id: z.string().optional().describe("An existing conversation's chat_id — pass it to reply in-thread instead of opening a new chat."),
|
|
1575
|
+
},
|
|
1576
|
+
async ({ text, linkedin_url, linkedin_member_id, chat_id }) => {
|
|
1577
|
+
if (!linkedin_url && !linkedin_member_id && !chat_id) {
|
|
1578
|
+
return { content: [{ type: "text", text:
|
|
1579
|
+
"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." }] };
|
|
1580
|
+
}
|
|
1581
|
+
try {
|
|
1582
|
+
const r = await post("/api/linkedin/send-message", { text, linkedin_url, linkedin_member_id, chat_id });
|
|
1583
|
+
return { content: [{ type: "text", text:
|
|
1584
|
+
`Message sent over LinkedIn.${r.chat_id ? ` Thread chat_id: ${r.chat_id} (reuse it to reply in-thread).` : ""}` }] };
|
|
1585
|
+
} catch (e) {
|
|
1586
|
+
const msg =
|
|
1587
|
+
/linkedin_not_connected/.test(e.message)
|
|
1588
|
+
? "No LinkedIn account is connected. Tell the user to connect LinkedIn in Integrations first."
|
|
1589
|
+
: /auth_required/.test(e.message)
|
|
1590
|
+
? "This API key isn't scoped to a workspace, or LinkedIn isn't connected."
|
|
1591
|
+
: /missing_params/.test(e.message)
|
|
1592
|
+
? "Couldn't tell who to message — provide linkedin_url, linkedin_member_id, or chat_id."
|
|
1593
|
+
: `Couldn't send the message: ${e.message}`;
|
|
1594
|
+
return { content: [{ type: "text", text: msg }] };
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
);
|
|
1598
|
+
|
|
1599
|
+
// ===========================================================================
|
|
1600
|
+
// TOOLS: the CORRECTION layer — unsay something recorded by mistake. `record` and
|
|
1601
|
+
// `save_note` are how you write; these are how you take it back. Both heal the
|
|
1602
|
+
// derived layer: retracting an observation re-derives the claim from what remains,
|
|
1603
|
+
// deleting a note drops it from search/context. DELETE /v2/observations|notes/:id.
|
|
1604
|
+
// ===========================================================================
|
|
1605
|
+
server.tool(
|
|
1606
|
+
"retract_observation",
|
|
1607
|
+
"RETRACT an observation you recorded by mistake, and heal the record. Pass the observation's " +
|
|
1608
|
+
"`id` (returned by `record`). Nous deletes it and re-derives the affected fact from the " +
|
|
1609
|
+
"observations that remain — so a wrong value you observed is un-observed and the claim reverts as " +
|
|
1610
|
+
"if it had never happened; if it was the only observation for that fact, the fact is invalidated. " +
|
|
1611
|
+
"Use this when you recorded the wrong thing (wrong value, wrong person, a test), NOT to represent a " +
|
|
1612
|
+
"real change over time — a genuine change is a NEW `record`, which supersedes by recency.",
|
|
1613
|
+
{
|
|
1614
|
+
id: z.string().describe("The observation id to retract (from a prior `record` result)."),
|
|
1615
|
+
},
|
|
1616
|
+
async ({ id }) => {
|
|
1617
|
+
try {
|
|
1618
|
+
const r = await del(`/v2/observations/${encodeURIComponent(id)}`);
|
|
1619
|
+
return { content: [{ type: "text", text:
|
|
1620
|
+
`Observation retracted. The claim for ${r.property} was ${r.claim === "invalidated" ? "invalidated (no observations left)" : "re-derived from the remaining observations"}.` }] };
|
|
1621
|
+
} catch (e) {
|
|
1622
|
+
const msg = /observation_not_found/.test(e.message)
|
|
1623
|
+
? "No observation with that id in this workspace — check the id from the record result."
|
|
1624
|
+
: `Couldn't retract the observation: ${e.message}`;
|
|
1625
|
+
return { content: [{ type: "text", text: msg }] };
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
);
|
|
1629
|
+
|
|
1630
|
+
server.tool(
|
|
1631
|
+
"delete_note",
|
|
1632
|
+
"DELETE a note saved by mistake. Pass the note's `id` (returned by `save_note`). It's removed from " +
|
|
1633
|
+
"search and context immediately; the timeline stays reconstructable. Only touches notes — it will " +
|
|
1634
|
+
"refuse an id that isn't a note. Use this for a note saved in error; to CHANGE a note, save a new " +
|
|
1635
|
+
"one instead.",
|
|
1636
|
+
{
|
|
1637
|
+
id: z.string().describe("The note id to delete (from a prior `save_note` result)."),
|
|
1638
|
+
},
|
|
1639
|
+
async ({ id }) => {
|
|
1640
|
+
try {
|
|
1641
|
+
const r = await del(`/v2/notes/${encodeURIComponent(id)}`);
|
|
1642
|
+
return { content: [{ type: "text", text:
|
|
1643
|
+
r.status === "already_deleted" ? "That note was already deleted." : "Note deleted — it's out of search and context now." }] };
|
|
1644
|
+
} catch (e) {
|
|
1645
|
+
const msg = /note_not_found/.test(e.message)
|
|
1646
|
+
? "No note with that id in this workspace — check the id from the save_note result."
|
|
1647
|
+
: /not_a_note/.test(e.message)
|
|
1648
|
+
? "That id isn't a note, so it can't be deleted here. Only save_note notes can be deleted this way."
|
|
1649
|
+
: `Couldn't delete the note: ${e.message}`;
|
|
1650
|
+
return { content: [{ type: "text", text: msg }] };
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
);
|
|
1654
|
+
|
|
1477
1655
|
// ===========================================================================
|
|
1478
1656
|
// TOOL: get_routing_preferences
|
|
1479
1657
|
// The routing preferences that make THIS agent default to Nous for GTM. The
|