@edda-business/mcp 0.56.0 → 0.58.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 +80 -63
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@edda-business/mcp",
3
- "version": "0.56.0",
3
+ "version": "0.58.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
@@ -13,7 +13,7 @@
13
13
  * Tools, by group (v0.56 — consolidated, Cerebras-style):
14
14
  * RETRIEVE search (unified, scope=all|company|personal|notes) · get_context ·
15
15
  * get_account · who_knows · query · attention · verify
16
- * WRITE save_note · save_to_vault · propose_company_file
16
+ * WRITE save_note · save_to_vault · update_vault_file · propose_company_file
17
17
  * FIX merge_contacts (action=merge|split)
18
18
  * RUN list_integrations · connect_integration
19
19
  *
@@ -26,7 +26,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
26
26
  import { z } from "zod";
27
27
  import { get, post, del } from "./client.js";
28
28
 
29
- export const SERVER_VERSION = "0.56.0";
29
+ export const SERVER_VERSION = "0.58.0";
30
30
 
31
31
  // ─── helpers ──────────────────────────────────────────────────────────────────
32
32
 
@@ -91,13 +91,12 @@ export function createServer() {
91
91
  server.tool(
92
92
  "get_context",
93
93
  "Get engineered context for a specific task about a person or company. Pass their email (or " +
94
- "entity id) and the intent. Returns a focused, ranked context block: the facts that matter for " +
95
- "that task each with a confidence and a freshness the durable FACTS we've learned about them " +
96
- "(their atomic memory: budget, authority, pain, stack, plans), the recent timeline, the buying-group " +
97
- "stakeholders, open predictions, and the account's ICP fit score (0-100 + why). Call this before " +
98
- "drafting outreach, preparing for a meeting, " +
99
- "or making any decision about a person. A fact's freshness tells you whether to trust it: 'fresh' " +
100
- "act on it, 'suspect'/'expired' verify first.",
94
+ "entity id) and the intent. Returns a focused, ranked context block: the durable FACTS we've " +
95
+ "learned about them (each with a confidence and a freshness), the recent timeline, the other " +
96
+ "people at their company and how they relate, and the relevant notes and records. Call this before " +
97
+ "a meeting or any decision about a person or company, so you act on what we actually know rather " +
98
+ "than generic guesses. A fact's freshness tells you whether to trust it: 'fresh' act on it, " +
99
+ "'suspect'/'expired' verify first.",
101
100
  {
102
101
  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."),
103
102
  intent: z.enum(["follow_up", "meeting_prep", "call_prep", "account_review"])
@@ -131,11 +130,6 @@ export function createServer() {
131
130
  }
132
131
  lines.push("");
133
132
  }
134
- if (ctx.workspace?.length) {
135
- lines.push("YOUR CONTEXT (ICP / product / positioning):");
136
- for (const w of ctx.workspace) lines.push(` ${w.property}: ${fmtVal(w.value)}`);
137
- lines.push("");
138
- }
139
133
  if (ctx.timeline?.length) {
140
134
  lines.push("TIMELINE:");
141
135
  for (const t of ctx.timeline) {
@@ -157,23 +151,18 @@ export function createServer() {
157
151
  lines.push("");
158
152
  }
159
153
  if (ctx.stakeholders?.length) {
160
- // The buying committee as a STRUCTURE: who's at the account, their role,
161
- // whether we've engaged them, and how they relate so the agent works the
162
- // whole committee, not one person.
154
+ // The people connected to this company and how they relate — the local map, so
155
+ // the agent sees the whole picture, not one person in isolation.
163
156
  const c = ctx.committee;
164
- lines.push(c?.company ? `BUYING COMMITTEE ${c.company}:` : "STAKEHOLDERS:");
157
+ lines.push(c?.company ? `PEOPLE AT ${c.company}:` : "RELATED PEOPLE:");
165
158
  for (const s of ctx.stakeholders) {
166
159
  if (s.role === "company") continue; // the company is the header
167
160
  const bits = [];
168
- if (s.committee_role && s.committee_role !== "contact") bits.push(s.committee_role.replace(/_/g, " "));
169
161
  if (s.role) bits.push(s.role);
170
- bits.push(s.engaged ? "engaged" : "not yet engaged");
171
162
  if (s.confirmed === false) bits.push("mentioned, unconfirmed");
172
163
  const rel = s.relationships?.length ? ` — ${s.relationships.join("; ")}` : "";
173
- lines.push(` ${s.name ?? "—"} (${bits.join(", ")})${rel}`);
164
+ lines.push(` ${s.name ?? "—"}${bits.length ? ` (${bits.join(", ")})` : ""}${rel}`);
174
165
  }
175
- if (c?.champion) lines.push(` champion: ${c.champion}`);
176
- if (c?.gaps?.length) for (const g of c.gaps) lines.push(` ⚠ ${g}`);
177
166
  lines.push("");
178
167
  }
179
168
  return {
@@ -188,8 +177,8 @@ export function createServer() {
188
177
  // ===========================================================================
189
178
  server.tool(
190
179
  "get_account",
191
- "Get the full account record for a person or company — the durable FACTS we've learned about them " +
192
- "(their atomic memory: budget, authority, pain, stack, plans), every attribute (claim) with its " +
180
+ "Get the full record for a person or company — the durable FACTS we've learned about them (their " +
181
+ "role, company, history, and anything else worth remembering), every attribute (claim) with its " +
193
182
  "confidence and freshness, plus what they actually SAID and did, ranked by how much it tells you. " +
194
183
  "Pass an email or entity UUID, and the intent you're working toward so the record is shaped for it.",
195
184
  {
@@ -243,23 +232,18 @@ export function createServer() {
243
232
  }
244
233
  lines.push("");
245
234
  }
246
- // The buying committee who else is at the account, their role, whether we've
247
- // engaged them, and how they relate. Same structure get_context surfaces.
235
+ // The people connected to this company and how they relate. Same structure get_context surfaces.
248
236
  if (rec.stakeholders?.length) {
249
237
  const c = rec.committee;
250
- lines.push(c?.company ? `BUYING COMMITTEE ${c.company}:` : "STAKEHOLDERS:");
238
+ lines.push(c?.company ? `PEOPLE AT ${c.company}:` : "RELATED PEOPLE:");
251
239
  for (const s of rec.stakeholders) {
252
240
  if (s.role === "company") continue;
253
241
  const bits = [];
254
- if (s.committee_role && s.committee_role !== "contact") bits.push(s.committee_role.replace(/_/g, " "));
255
242
  if (s.role) bits.push(s.role);
256
- bits.push(s.engaged ? "engaged" : "not yet engaged");
257
243
  if (s.confirmed === false) bits.push("mentioned, unconfirmed");
258
244
  const rel = s.relationships?.length ? ` — ${s.relationships.join("; ")}` : "";
259
- lines.push(` ${s.name ?? "—"} (${bits.join(", ")})${rel}`);
245
+ lines.push(` ${s.name ?? "—"}${bits.length ? ` (${bits.join(", ")})` : ""}${rel}`);
260
246
  }
261
- if (c?.champion) lines.push(` champion: ${c.champion}`);
262
- if (c?.gaps?.length) for (const g of c.gaps) lines.push(` ⚠ ${g}`);
263
247
  lines.push("");
264
248
  }
265
249
  const claims = Object.values(rec.claims ?? {});
@@ -421,22 +405,21 @@ export function createServer() {
421
405
  // ===========================================================================
422
406
  server.tool(
423
407
  "query",
424
- "Retrieve and summarise activity across many people. Three powers:\n" +
408
+ "Retrieve and summarise activity across many people and companies. Powers:\n" +
425
409
  " 1. return:'entities' groups results by person/company (one row per entity, ranked by " +
426
- "most-recent matching activity). Use for 'hottest leads', 'who replied this week', " +
427
- "'who's in the negotiation stage'.\n" +
428
- " 2. `without` subtracts entities — 'sent in 5d MINUS replied in 5d' = 'no-reply leads'. " +
429
- "'activity in 30d MINUS activity in 5d' = 'cooled leads'.\n" +
430
- " 3. rollups.by_value appears when scope.kind='state' — counts entities by current value " +
431
- "(use scope.property='stage' for funnel reports).\n" +
410
+ "most-recent matching activity). Use for 'who we talked to this week', 'who replied recently'.\n" +
411
+ " 2. `without` subtracts entities — 'sent in 5d MINUS replied in 5d' = 'wrote to, no reply back'. " +
412
+ "'activity in 30d MINUS activity in 5d' = 'people who've gone quiet'.\n" +
413
+ " 3. rollups.by_value appears when scope.kind='state' counts entities by a current value " +
414
+ "(scope.property='<state property>').\n" +
432
415
  " 4. Scheduled meetings/calls are events with property 'interaction.meeting_scheduled' and a " +
433
416
  "future-dated `when`. For 'what's booked today/this week', set property:'interaction.meeting_scheduled' " +
434
417
  "with from/to bounding the day or week (since_days only looks backward and can't reach them), and " +
435
418
  "order:'asc' to list soonest-first. Meeting rows render the absolute date and time.\n" +
436
- " 5. scope.facts:true + question searches the FACTS corpus (durable atomic facts about accounts) " +
437
- "instead of activity — cross-account semantic fact search like 'which accounts want off Clay' or " +
438
- "'who is hiring'. return:'entities' gives the single best-matching fact per account. (A single " +
439
- "account's facts already come back inline with get_account.)",
419
+ " 5. scope.facts:true + question searches the FACTS corpus (durable facts about people/companies) " +
420
+ "instead of activity — cross-record semantic fact search like 'who is hiring' or 'who mentioned " +
421
+ "switching tools'. return:'entities' gives the single best-matching fact per record. (A single " +
422
+ "record's facts already come back inline with get_account.)",
440
423
  {
441
424
  scope: z.object({
442
425
  kind: z.enum(["event", "state"]).optional(),
@@ -448,7 +431,7 @@ export function createServer() {
448
431
  to: z.string().optional().describe("ISO timestamp — only activity at/before this (absolute upper bound). Combine from+to for a window; future-dated for upcoming meetings"),
449
432
  order: z.enum(["asc", "desc"]).optional().describe("observed_at order (default desc, newest first). Use 'asc' for an upcoming-meeting schedule (soonest first)"),
450
433
  limit: z.number().optional().describe("max items (default 50, cap 200)"),
451
- facts: z.boolean().optional().describe("search the FACTS corpus (durable atomic facts about accounts) instead of activity. Needs `question` — a cross-account semantic fact search, e.g. 'which accounts want off Clay'. return:'entities' = the best matching fact per account."),
434
+ facts: z.boolean().optional().describe("search the FACTS corpus (durable facts about people/companies) instead of activity. Needs `question` — a cross-record semantic fact search, e.g. 'who is hiring'. return:'entities' = the best matching fact per record."),
452
435
  }).describe("Corpus filter"),
453
436
  without: z.object({
454
437
  kind: z.enum(["event", "state"]).optional(),
@@ -537,12 +520,12 @@ export function createServer() {
537
520
  // ===========================================================================
538
521
  server.tool(
539
522
  "verify",
540
- "Re-check a specific fact before you act on it — e.g. an email or a deal stage that looks stale " +
523
+ "Re-check a specific fact before you act on it — e.g. an email or a job title that looks stale " +
541
524
  "in get_context. Pass the person/company and the property name. Returns the fact re-derived from " +
542
525
  "current evidence, and tells you whether it is still unverified.",
543
526
  {
544
527
  focus: z.string().describe("Email, LinkedIn URL, entity UUID, or name"),
545
- property: z.string().describe("The fact to re-check — e.g. 'email', 'job_title', 'pipeline_stage'"),
528
+ property: z.string().describe("The fact to re-check — e.g. 'email', 'job_title', 'company'"),
546
529
  },
547
530
  async ({ focus, property }) => {
548
531
  const r = await post("/v2/verify", { focus, property });
@@ -610,7 +593,7 @@ export function createServer() {
610
593
  },
611
594
  async ({ folder, name, content, subfolder }) => {
612
595
  const r = await post("/v2/personal/files", { folder, name, content, subfolder });
613
- return { content: [{ type: "text", text: `Saved "${name}" to your ${r?.folder || folder} vault — it's searchable now.` }] };
596
+ return { content: [{ type: "text", text: `"${name}" is waiting in your Inbox to approve — once you do, it files into ${r?.folder || folder}.` }] };
614
597
  },
615
598
  );
616
599
 
@@ -761,14 +744,13 @@ export function createServer() {
761
744
  // ===========================================================================
762
745
  server.tool(
763
746
  "connect_integration",
764
- "Connect a key-based integration for the useran enrichment, CRM, or sequencer provider that " +
765
- "authenticates with an API key or token (e.g. Apollo, Prospeo, Instantly, HubSpot private-app " +
766
- "token, Pipedrive, Attio, Smartlead, HeyReach). Ask the user for the provider's API key, then " +
767
- "call this; it verifies the credentials before saving. Providers that use a browser sign-in " +
768
- "(OAuth, e.g. Gmail) can't be connected this way — for those, point the user to the Integrations " +
769
- "page. After connecting an enrichment provider, the account record starts filling in.",
747
+ "Connect a data source that feeds the knowledge base a provider that authenticates with an API " +
748
+ "key or token. Ask the user for the provider's key, then call this; it verifies the credentials " +
749
+ "before saving. Providers that use a browser sign-in (OAuth, e.g. Gmail or calendar) can't be " +
750
+ "connected this way for those, point the user to the Integrations page. Once connected, the " +
751
+ "source starts feeding the knowledge base.",
770
752
  {
771
- provider: z.string().describe("Provider name, lowercase — e.g. 'apollo', 'prospeo', 'instantly', 'hubspot', 'pipedrive', 'attio'."),
753
+ provider: z.string().describe("Provider name, lowercase."),
772
754
  credentials: z.record(z.string()).describe("The provider's credentials as key/value, e.g. { api_key: '...' } or { access_token: '...' }."),
773
755
  name: z.string().optional().describe("Optional label for the connection."),
774
756
  },
@@ -800,13 +782,14 @@ export function createServer() {
800
782
  // ===========================================================================
801
783
  server.tool(
802
784
  "save_to_vault",
803
- "Save a file into the MEMBER'S OWN private knowledge vault, in a folder they name " +
804
- "(inbox, projects, decisions, companies, people, resources, archive, thoughts). Use it when the " +
805
- "member asks you to keep or file something for them 'save this to projects', 'put this PDF in " +
806
- "resources'. Pass markdown/text as `content`, OR a PDF/DOCX as base64 in `file` (it's extracted to " +
807
- "text automatically and stored so they can search it later). PRIVATE to the member nothing is " +
808
- "shared until they promote it to their department. NOT the shared company vault (use " +
809
- "propose_company_file), NOT a note on a contact (use save_note).",
785
+ "Save a file into the MEMBER'S OWN private knowledge vault. It lands in their INBOX for review " +
786
+ "with the target folder you name remembered (inbox, projects, decisions, companies, people, " +
787
+ "resources, archive, thoughts); the member approves it in the app, which files it into that folder " +
788
+ "and makes it searchable. Nothing enters their knowledge without their approval. Use it when the " +
789
+ "member asks you to keep or file something 'save this to resources', 'put this PDF in projects'. " +
790
+ "Pass markdown/text as `content`, OR a PDF/DOCX as base64 in `file` (extracted to text automatically). " +
791
+ "PRIVATE to the member. NOT the shared company vault (use propose_company_file), NOT a note on a " +
792
+ "contact (use save_note).",
810
793
  {
811
794
  folder: z.string().describe("The vault folder: inbox | projects | decisions | companies | people | resources | archive | thoughts."),
812
795
  name: z.string().describe("The file name, e.g. 'Q3 Plan.md' or 'vendor-contract.pdf'."),
@@ -818,7 +801,7 @@ export function createServer() {
818
801
  async ({ folder, name, content, file, mime, subfolder }) => {
819
802
  try {
820
803
  const r = await post("/v2/personal/files", { folder, name, content, file_base64: file, mime, subfolder });
821
- return { content: [{ type: "text", text: `Saved "${name}" to your ${r?.folder || folder} vault it's searchable now.` }] };
804
+ return { content: [{ type: "text", text: `"${name}" is waiting in your Inbox to approve — once you do, it files into ${r?.folder || folder} and becomes searchable.` }] };
822
805
  } catch (e) {
823
806
  const msg = String(e?.message || e);
824
807
  if (msg.includes("no_text_extracted")) return { content: [{ type: "text", text: `Couldn't read any text from that file — a scanned/image-only PDF has no extractable text. Text, markdown, or a text-based PDF/DOCX works.` }] };
@@ -828,6 +811,40 @@ export function createServer() {
828
811
  }
829
812
  );
830
813
 
814
+ // ===========================================================================
815
+ // TOOL: update_vault_file — POST /v2/personal/files/update
816
+ // Edit an existing personal file. Snapshots the old content to version history,
817
+ // then replaces/appends + re-embeds. save_to_vault is for NEW files (→ Inbox);
818
+ // this edits one that's already live.
819
+ // ===========================================================================
820
+ server.tool(
821
+ "update_vault_file",
822
+ "Edit an EXISTING file in the member's own vault — revise it or append to it. Identify the file by " +
823
+ "`file_id` (from a search or save result) or by `path` ('folder/[subfolder/]name.md'). The previous " +
824
+ "content is saved to version history FIRST (never lost), then your new content replaces it — or is added " +
825
+ "to the end with append:true — and the file is re-embedded; the member sees a fresh 'updated' time. Use " +
826
+ "this to keep a living document current: a rolling brief, a spec, a running log. For a NEW file use " +
827
+ "save_to_vault (which lands in the Inbox for approval); this edits one that already exists and is live.",
828
+ {
829
+ file_id: z.string().optional().describe("The file's id (from a search/save result). Provide this OR path."),
830
+ path: z.string().optional().describe("The file's vault path, e.g. 'resources/Edda Architecture.md'. Provide this OR file_id."),
831
+ content: z.string().describe("The new markdown content. Replaces the file's content unless append is true."),
832
+ append: z.boolean().optional().describe("If true, append this to the end of the file instead of replacing it."),
833
+ },
834
+ async ({ file_id, path, content, append }) => {
835
+ try {
836
+ const r = await post("/v2/personal/files/update", { file_id, path, content, append });
837
+ return { content: [{ type: "text", text: r?.note || "Updated the file." }] };
838
+ } catch (e) {
839
+ const msg = String(e?.message || e);
840
+ if (msg.includes("not_found")) return { content: [{ type: "text", text: "Couldn't find that file — check the file_id or path (folder/name.md)." }] };
841
+ if (msg.includes("file_id_or_path_required")) return { content: [{ type: "text", text: "Tell me which file to edit — pass file_id or path." }] };
842
+ if (msg.includes("content_required")) return { content: [{ type: "text", text: "Give me the new content to write." }] };
843
+ throw e;
844
+ }
845
+ }
846
+ );
847
+
831
848
  server.tool(
832
849
  "list_integrations",
833
850
  "List the integrations connected to this workspace (Gmail, HubSpot, Apollo, Instantly, LinkedIn, …) " +