@edda-business/mcp 0.52.0 → 0.53.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/server.js +15 -196
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@edda-business/mcp",
3
- "version": "0.52.0",
3
+ "version": "0.53.0",
4
4
  "description": "Edda — the company data layer for AI agents.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
package/src/server.js CHANGED
@@ -10,19 +10,19 @@
10
10
  * sees raw rows — it gets engineered, epistemics-tagged context. It never
11
11
  * "updates" — it records observations; Edda derives.
12
12
  *
13
- * Tools, by group (the authoritative live catalog is `node scripts/list-tools.mjs`):
13
+ * Tools, by group:
14
14
  * READ get_context · get_account · query · attention · verify ·
15
15
  * search_notes · search_company_knowledge · search_my_vault
16
- * WRITE record · save_note · propose_vault_file · propose_company_file · merge_contacts
17
- * CORRECT retract_observation · unmerge_contacts
18
- * RUN get_workspace_status · whoami · list_integrations · set_workspace_profile · connect_integration
16
+ * WRITE save_note · propose_vault_file · propose_company_file · merge_contacts
17
+ * CORRECT unmerge_contacts
18
+ * RUN list_integrations · connect_integration
19
19
  */
20
20
 
21
21
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
22
22
  import { z } from "zod";
23
23
  import { get, post, del } from "./client.js";
24
24
 
25
- export const SERVER_VERSION = "0.51.0";
25
+ export const SERVER_VERSION = "0.53.0";
26
26
 
27
27
  // ─── helpers ──────────────────────────────────────────────────────────────────
28
28
 
@@ -71,10 +71,10 @@ export function createServer() {
71
71
  name: "edda",
72
72
  version: SERVER_VERSION,
73
73
  description:
74
- "Edda — the Context Graph for AI Agents. Edda is operated by the agent, not by a human " +
75
- "clicking around: call get_workspace_status at the start of a session to see what's set up " +
76
- "and what to set up next. Call get_context before drafting outreach or preparing for a " +
77
- "meeting. Call record after every interaction, or whenever you learn something.",
74
+ "Edda — the company knowledge layer for AI agents. The agent reads engineered, " +
75
+ "epistemics-tagged context instead of raw rows. Call get_context before preparing for a " +
76
+ "meeting or a decision about a person; search_company_knowledge for how the company works; " +
77
+ "save_note to keep a brief or transcript on a contact.",
78
78
  icons: [
79
79
  { src: "https://opennous.cloud/newlogoP.png", mimeType: "image/png", sizes: ["64x64"] },
80
80
  ],
@@ -96,7 +96,7 @@ export function createServer() {
96
96
  "act on it, 'suspect'/'expired' verify first.",
97
97
  {
98
98
  focus: z.string().describe("Who to look up — an email, a LinkedIn URL, a domain, an entity UUID, or a name. A name may match several people; you'll get candidates to choose from."),
99
- intent: z.enum(["draft_email", "follow_up", "meeting_prep", "call_prep", "account_review"])
99
+ intent: z.enum(["follow_up", "meeting_prep", "call_prep", "account_review"])
100
100
  .optional()
101
101
  .describe("What you are about to do — shapes which context surfaces (default: account_review)"),
102
102
  budget_tokens: z.number().optional().describe("Approximate token budget for the context block"),
@@ -190,7 +190,7 @@ export function createServer() {
190
190
  {
191
191
  id: z.string().describe("Who to look up — an email, an entity UUID, or a name. A name may match several people; you'll get candidates to choose from."),
192
192
  intent: z
193
- .enum(["meeting_prep", "call_prep", "account_review", "follow_up", "draft_email"])
193
+ .enum(["meeting_prep", "call_prep", "account_review", "follow_up"])
194
194
  .optional()
195
195
  .describe(
196
196
  "What you're about to do. Shapes how much of their history comes back: a meeting brief wants " +
@@ -385,38 +385,6 @@ export function createServer() {
385
385
  }
386
386
  );
387
387
 
388
- // ===========================================================================
389
- // TOOL: record — POST /v2/observations
390
- // The single write verb. You observe — Edda derives the updated facts.
391
- // ===========================================================================
392
- server.tool(
393
- "record",
394
- "Record what happened or what you learned about a person or company. You never overwrite " +
395
- "anything — you observe, and Edda derives the updated facts. Use kind:'event' for an interaction " +
396
- "(property like 'interaction.email_sent', 'interaction.call_held', 'interaction.email_reply') and " +
397
- "kind:'state' for a fact (property like 'job_title', 'deal.proposal_amount'). Examples — sent an " +
398
- "email: {kind:'event',property:'interaction.email_sent',value:{description:'intro email'}}; " +
399
- "learned their title changed: {kind:'state',property:'job_title',value:'VP of Engineering'}; " +
400
- "a fact ended (they left): {kind:'state',property:'job_title',value:null}.",
401
- {
402
- focus: z.string().describe("Email address or entity UUID of the person or company"),
403
- observations: z.array(z.object({
404
- kind: z.enum(["event", "state"]).describe("event = an interaction; state = a fact"),
405
- property: z.string().describe("e.g. 'interaction.email_sent' or 'job_title'"),
406
- value: z.any().optional().describe("the event detail or the fact value; null = the fact ended"),
407
- source: z.string().optional().describe("where this came from (default: agent)"),
408
- })).describe("One or more observations to record"),
409
- },
410
- async ({ focus, observations }) => {
411
- const result = await post("/v2/observations", { focus, observations });
412
- const parts = [`Recorded ${result.recorded} observation${result.recorded !== 1 ? "s" : ""}.`];
413
- if (result.claims_recomputed?.length) {
414
- parts.push(`Facts updated: ${result.claims_recomputed.join(", ")}.`);
415
- }
416
- parts.push(`(entity_id: ${result.entity_id})`);
417
- return { content: [{ type: "text", text: parts.join("\n") }] };
418
- }
419
- );
420
388
 
421
389
 
422
390
  // ===========================================================================
@@ -566,8 +534,7 @@ export function createServer() {
566
534
  // TOOL: save_note — POST /v2/notes
567
535
  // Attach a long-form artifact to a CONTACT: a meeting brief you wrote, a
568
536
  // transcript, pre-meeting prep, or a plain note. Append-only and dated, so the
569
- // contact builds a record across meetings. Distinct from `record` (which logs
570
- // that an interaction happened) — this keeps the document itself.
537
+ // contact builds up a document trail across meetings.
571
538
  // ===========================================================================
572
539
  server.tool(
573
540
  "save_note",
@@ -575,11 +542,9 @@ export function createServer() {
575
542
  "brief you wrote, a transcript, pre-meeting prep, research, or a plain note. Use this whenever " +
576
543
  "you produce something durable about a specific contact that's worth keeping for next time (e.g. " +
577
544
  "after writing a meeting brief, save it to the contact so future meetings can reference it). " +
578
- "Notes are append-only and dated, so a contact builds a record across meetings — later you can " +
579
- "read the last few and see what changed. This is NOT for logging that an interaction happened " +
580
- "(use `record` with an interaction.* event for that), and NOT for the user's own GTM profile " +
581
- "(that lives in their context files — sync it with `sync_icp`). Put the full text in `content` — it's kept for agents to read; the " +
582
- "UI shows the title and date, not the whole body.",
545
+ "Notes are append-only and dated, so a contact builds a document trail across meetings — later " +
546
+ "you can read the last few and see what changed. Put the full text in `content` it's kept for " +
547
+ "agents to read; the UI shows the title and date, not the whole body.",
583
548
  {
584
549
  focus: z.string().describe("Who to attach it to — an email, LinkedIn URL, domain, or entity UUID (not a bare name)."),
585
550
  content: z.string().describe("The full note or document text (a short note or a complete brief/transcript)."),
@@ -772,104 +737,6 @@ export function createServer() {
772
737
  },
773
738
  );
774
739
 
775
- // ===========================================================================
776
- // TOOL: get_workspace_status — GET /v2/workspace/status
777
- // The "one main call." Edda is operated by the agent, so the agent needs to
778
- // know the state of the workspace: is it onboarded, is the GTM foundation built,
779
- // which integrations are connected, is CRM sync configured, are events live —
780
- // and what to set up next. Call this at the start of a session.
781
- // ===========================================================================
782
- server.tool(
783
- "get_workspace_status",
784
- "See the whole setup state of this workspace in one call, plus a ranked NEXT STEPS list (each step " +
785
- "carries its own why/how). Edda is operated by you, the agent — call this at the START of a session " +
786
- "and walk the user top-down through the steps it returns; the server sequences them by current " +
787
- "state, so trust that order. Two constraints when acting on them: (1) Gmail (Google OAuth) and " +
788
- "LinkedIn (no public API — Edda uses Unipile) CANNOT be connected by you — point the user to the " +
789
- "Integrations page; key-based tools (Prospeo, Apollo, Instantly, HubSpot token) you CAN connect via " +
790
- "connect_integration, and CSV import is a user action in the app. (2) Respect the plan — never push " +
791
- "a feature it doesn't include (e.g. CRM sync on free). Recommend the next 1-2 steps, don't dump the " +
792
- "whole list.",
793
- {},
794
- async () => {
795
- const s = await get("/v2/workspace/status");
796
- const setup = s.setup ?? {};
797
- const lines = [];
798
-
799
- const ws = s.workspace ?? {};
800
- lines.push(`WORKSPACE: ${ws.name || "(unnamed)"}${ws.website ? ` · ${ws.website}` : ""}${ws.business_type ? ` · ${ws.business_type}` : ""}`);
801
- const pl = s.plan ?? {};
802
- lines.push(`PLAN: ${pl.name || pl.id || "free"}${pl.crm_sync === false ? " (CRM sync not included — do not offer it)" : ""}`);
803
- if (s.self_hosted) {
804
- const e = s.env_integrations ?? {};
805
- const mk = (b) => (b ? "✓ set" : "✗ NOT set");
806
- lines.push("SELF-HOSTED — these channels are wired via edda.env (you can't set env vars; tell the operator to set + restart):");
807
- lines.push(` LinkedIn/Unipile: ${mk(e.linkedin_unipile)} Email/Resend: ${mk(e.email_resend)} Gmail OAuth: ${mk(e.gmail_oauth)}`);
808
- }
809
- lines.push("");
810
-
811
- const mark = (b) => (b ? "✓" : "✗");
812
- lines.push("SETUP:");
813
- lines.push(` ${mark(setup.onboarding?.done)} Profile${setup.onboarding?.done ? "" : ` — missing ${(setup.onboarding?.missing ?? []).join(", ") || "details"}`}`);
814
- const ints = setup.integrations?.connected ?? [];
815
- lines.push(` ${mark((setup.integrations?.count ?? 0) > 0)} Integrations (${setup.integrations?.count ?? 0})${ints.length ? `: ${ints.map((i) => i.name).join(", ")}` : ""}`);
816
- const crm = setup.crm_sync ?? {};
817
- if (crm.available === false) {
818
- lines.push(` – CRM sync (not on the ${pl.name || pl.id || "current"} plan)`);
819
- } else {
820
- 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` : ""}`);
821
- }
822
- lines.push(` ${mark(setup.enrichment?.connected)} Enrichment${setup.enrichment?.provider ? `: ${setup.enrichment.provider}` : ""}`);
823
- 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)`);
824
- const rec = setup.recommended ?? {};
825
- lines.push("");
826
- lines.push("RECOMMENDED CHANNELS (connect these first):");
827
- lines.push(` ${mark(rec.email)} Email / Gmail ${mark(rec.linkedin)} LinkedIn ${mark(rec.meeting_notetaker)} Meeting note-taker`);
828
- lines.push(` Records imported: ${setup.records?.count ?? 0}`);
829
-
830
- if (s.next_steps?.length) {
831
- lines.push("");
832
- lines.push("NEXT STEPS:");
833
- for (const step of s.next_steps) {
834
- lines.push(` • ${step.title}`);
835
- if (step.why) lines.push(` why: ${step.why}`);
836
- if (step.how) lines.push(` how: ${step.how}`);
837
- }
838
- } else {
839
- lines.push("");
840
- lines.push("Everything's set up. Nothing pending.");
841
- }
842
-
843
- return { content: [{ type: "text", text: lines.join("\n").trim() }] };
844
- }
845
- );
846
-
847
- // ===========================================================================
848
- // TOOL: set_workspace_profile — POST /v2/workspace/onboarding
849
- // Agent-driven onboarding. Instead of a human clicking through a wizard in the
850
- // app, you collect the basics from the user in conversation and write them
851
- // here. This is the first thing get_workspace_status asks for when a workspace
852
- // is new.
853
- // ===========================================================================
854
- server.tool(
855
- "set_workspace_profile",
856
- "Set or update the workspace's basic identity — its company name and website. Send only the fields " +
857
- "you're setting or changing.",
858
- {
859
- name: z.string().optional().describe("The company / workspace name."),
860
- website: z.string().optional().describe("The company website."),
861
- },
862
- async ({ name, website }) => {
863
- const r = await post("/v2/workspace/onboarding", { name, website });
864
- const w = r.workspace ?? {};
865
- const set = [
866
- w.name && `name=${w.name}`,
867
- w.website && `site=${w.website}`,
868
- ].filter(Boolean);
869
- return { content: [{ type: "text", text:
870
- `Workspace profile saved.${set.length ? ` ${set.join(" · ")}.` : ""}` }] };
871
- }
872
- );
873
740
 
874
741
  // ===========================================================================
875
742
  // TOOL: connect_integration — POST /v2/workspace/integrations
@@ -910,36 +777,6 @@ export function createServer() {
910
777
  }
911
778
  );
912
779
 
913
- // ===========================================================================
914
- // TOOLS: the CORRECTION layer — unsay something recorded by mistake. `record` and
915
- // `save_note` are how you write; these are how you take it back. Both heal the
916
- // derived layer: retracting an observation re-derives the claim from what remains,
917
- // deleting a note drops it from search/context. DELETE /v2/observations|notes/:id.
918
- // ===========================================================================
919
- server.tool(
920
- "retract_observation",
921
- "RETRACT an observation you recorded by mistake, and heal the record. Pass the observation's " +
922
- "`id` (returned by `record`). Edda deletes it and re-derives the affected fact from the " +
923
- "observations that remain — so a wrong value you observed is un-observed and the claim reverts as " +
924
- "if it had never happened; if it was the only observation for that fact, the fact is invalidated. " +
925
- "Use this when you recorded the wrong thing (wrong value, wrong person, a test), NOT to represent a " +
926
- "real change over time — a genuine change is a NEW `record`, which supersedes by recency.",
927
- {
928
- id: z.string().describe("The observation id to retract (from a prior `record` result)."),
929
- },
930
- async ({ id }) => {
931
- try {
932
- const r = await del(`/v2/observations/${encodeURIComponent(id)}`);
933
- return { content: [{ type: "text", text:
934
- `Observation retracted. The claim for ${r.property} was ${r.claim === "invalidated" ? "invalidated (no observations left)" : "re-derived from the remaining observations"}.` }] };
935
- } catch (e) {
936
- const msg = /observation_not_found/.test(e.message)
937
- ? "No observation with that id in this workspace — check the id from the record result."
938
- : `Couldn't retract the observation: ${e.message}`;
939
- return { content: [{ type: "text", text: msg }] };
940
- }
941
- }
942
- );
943
780
 
944
781
 
945
782
  server.tool(
@@ -956,24 +793,6 @@ export function createServer() {
956
793
  }
957
794
  );
958
795
 
959
- server.tool(
960
- "whoami",
961
- "Report who this API key acts AS and who else is on the workspace — the agent's own identity in " +
962
- "Edda. Returns your scope (a MEMBER key sees only that member's private content plus the shared " +
963
- "graph; an ADMIN key sees all raw content) and the team roster with names and roles. Use it to " +
964
- "understand whose view you have and to reference teammates. Emails show only for an admin key.",
965
- {},
966
- async () => {
967
- const r = await get("/v2/workspace/members");
968
- const scope = r.you?.scope === "admin"
969
- ? "an ADMIN key — you see all raw content on this workspace"
970
- : "a MEMBER key — you see only your own private content plus the shared graph";
971
- const roster = (r.members || [])
972
- .map(m => ` • ${m.name || "(unnamed)"} — ${m.role}${m.you ? " (you)" : ""}${m.email ? ` · ${m.email}` : ""}`)
973
- .join("\n");
974
- return { content: [{ type: "text", text: `You are ${scope}.\n\nWorkspace members (${r.count ?? 0}):\n${roster || " (none)"}` }] };
975
- }
976
- );
977
796
 
978
797
 
979
798
  return server;