@edda-business/mcp 0.57.0 → 0.59.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 +59 -80
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@edda-business/mcp",
3
- "version": "0.57.0",
3
+ "version": "0.59.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
@@ -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.57.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 });
@@ -708,44 +691,41 @@ export function createServer() {
708
691
  );
709
692
 
710
693
  // ===========================================================================
711
- // TOOL: propose_company_file — POST /v2/company/propose
694
+ // TOOL: propose_company_file — POST /v2/company/pages
712
695
  // The WRITE side of the company vault (search_company_knowledge is the read side).
713
- // Admin-only: only an owner/admin may add to the shared knowledge; a member's agent
714
- // can read the company vault but gets a permission message here.
696
+ // Writes an in-app company page directly (no GitHub). Members with a write role
697
+ // publish it live; viewers land it in the company inbox for an admin to approve.
715
698
  // ===========================================================================
716
699
  server.tool(
717
700
  "propose_company_file",
718
- "Propose a markdown file for the SHARED COMPANY vault — the workspace's declared knowledge " +
701
+ "Add a markdown page to the SHARED COMPANY vault — the workspace's declared knowledge " +
719
702
  "(policies, playbooks, decisions, SOPs, how-we-work) that every member's agent can read via " +
720
- "search_company_knowledge. This OPENS A PULL REQUEST against the connected company repo for a " +
721
- "human to review and merge in GitHub nothing goes live until it's merged (company knowledge " +
722
- "is shared + institutional, so review-before-publish is the default; on merge the normal git " +
723
- "sync ingests it). Use it to contribute company-wide knowledge. ADMIN-ONLY: only an owner/admin " +
724
- "may propose; a regular member's agent can READ the vault but gets a permission message here. " +
725
- "NOT the member's private notes (use propose_vault_file), NOT a note on a contact (use save_note).",
703
+ "search_company_knowledge. This writes the page IN-APP: a member with a write role publishes " +
704
+ "it live immediately; a viewer's page lands in the company inbox for an admin to approve. Set " +
705
+ "`visibility` to scope who sees it 'company' (everyone, the default), 'department' (the team), " +
706
+ "or 'owner' (just you). Use it to contribute company-wide knowledge. " +
707
+ "NOT the member's private vault (use save_to_vault), NOT a note on a contact (use save_note).",
726
708
  {
727
- folder: z.string().describe("The company-vault folder to file under (e.g. 'company-wiki', 'decisions', 'projects', 'clients'). Must be an existing company folder; no slashes."),
728
- name: z.string().describe("The file name, ending in .md, e.g. 'Refund Policy.md' or 'Q3 Planning — 2026-08-22.md'."),
729
- content: z.string().describe("The full markdown content of the file."),
730
- subfolder: z.string().optional().describe("Optional subfolder within the folder."),
731
- message: z.string().optional().describe("Optional PR title / commit message describing the change."),
732
- pr_group: z.string().optional().describe("Optional key to batch several proposals from one work session into a SINGLE pull request (same key → same PR)."),
709
+ name: z.string().describe("The page name, e.g. 'Refund Policy' or 'Q3 Planning — 2026-08-22'. A '.md' suffix is added if missing."),
710
+ content: z.string().describe("The full markdown content of the page."),
711
+ visibility: z.enum(["owner", "department", "company"]).optional().describe("Who can see the page: 'company' (everyone — default), 'department' (the team), or 'owner' (just you)."),
712
+ folder_id: z.string().optional().describe("Optional id of an existing company folder to file under. Omit to land it in the default 'wiki' folder."),
733
713
  },
734
- async ({ folder, name, content, subfolder, message, pr_group }) => {
714
+ async ({ name, content, visibility, folder_id }) => {
735
715
  try {
736
- const r = await post("/v2/company/propose", { folder, name, content, subfolder, message, pr_group });
737
- const how = r?.added_to_existing ? "added to the open PR" : "opened a pull request";
738
- return { content: [{ type: "text", text: `Proposed "${name}" — ${how}${r?.pr_url ? `: ${r.pr_url}` : ""}. Review the diff and merge it in GitHub to publish into company knowledge.` }] };
716
+ const r = await post("/v2/company/pages", { name, content, visibility: visibility ?? "company", folder_id });
717
+ const live = r?.page?.status === "live";
718
+ const where = live
719
+ ? `published live to the company vault — searchable now`
720
+ : `sent to the company inbox for an admin to approve`;
721
+ return { content: [{ type: "text", text: `"${r?.page?.name ?? name}" ${where}.` }] };
739
722
  } catch (e) {
740
723
  const msg = String(e?.message || e);
741
- if (msg.includes("admins_only") || msg.includes("(403)")) {
742
- return { content: [{ type: "text", text: `Not proposedwriting to the company vault is admin-only. You can READ it with search_company_knowledge, but only an owner/admin can propose changes.` }] };
743
- }
744
- if (msg.includes("no_company_repo")) {
745
- return { content: [{ type: "text", text: `Not proposed — no company GitHub repo is connected. An admin connects one under Company vault first.` }] };
724
+ if (msg.includes("forbidden") || msg.includes("(403)")) {
725
+ return { content: [{ type: "text", text: `Not savedyou don't have access to write to this company vault.` }] };
746
726
  }
747
- if (msg.includes("repo_write_denied")) {
748
- return { content: [{ type: "text", text: `Not proposedthe connected repo token is read-only. An admin must reconnect it with Contents:write + Pull requests:write.` }] };
727
+ if (msg.includes("bad_folder")) {
728
+ return { content: [{ type: "text", text: `Not savedthat folder_id isn't a company folder in this workspace. Omit it to use the default 'wiki' folder.` }] };
749
729
  }
750
730
  throw e;
751
731
  }
@@ -761,14 +741,13 @@ export function createServer() {
761
741
  // ===========================================================================
762
742
  server.tool(
763
743
  "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.",
744
+ "Connect a data source that feeds the knowledge base a provider that authenticates with an API " +
745
+ "key or token. Ask the user for the provider's key, then call this; it verifies the credentials " +
746
+ "before saving. Providers that use a browser sign-in (OAuth, e.g. Gmail or calendar) can't be " +
747
+ "connected this way for those, point the user to the Integrations page. Once connected, the " +
748
+ "source starts feeding the knowledge base.",
770
749
  {
771
- provider: z.string().describe("Provider name, lowercase — e.g. 'apollo', 'prospeo', 'instantly', 'hubspot', 'pipedrive', 'attio'."),
750
+ provider: z.string().describe("Provider name, lowercase."),
772
751
  credentials: z.record(z.string()).describe("The provider's credentials as key/value, e.g. { api_key: '...' } or { access_token: '...' }."),
773
752
  name: z.string().optional().describe("Optional label for the connection."),
774
753
  },