@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.
- package/package.json +1 -1
- package/src/server.js +80 -63
package/package.json
CHANGED
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.
|
|
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
|
|
95
|
-
"
|
|
96
|
-
"
|
|
97
|
-
"
|
|
98
|
-
"
|
|
99
|
-
"
|
|
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
|
|
161
|
-
//
|
|
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 ? `
|
|
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
|
|
192
|
-
"
|
|
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
|
|
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 ? `
|
|
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.
|
|
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 '
|
|
427
|
-
"'
|
|
428
|
-
"
|
|
429
|
-
"'
|
|
430
|
-
"
|
|
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
|
|
437
|
-
"instead of activity — cross-
|
|
438
|
-
"
|
|
439
|
-
"
|
|
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
|
|
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
|
|
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', '
|
|
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: `
|
|
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
|
|
765
|
-
"
|
|
766
|
-
"
|
|
767
|
-
"
|
|
768
|
-
"
|
|
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
|
|
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
|
|
804
|
-
"(inbox, projects, decisions, companies, people,
|
|
805
|
-
"
|
|
806
|
-
"
|
|
807
|
-
"
|
|
808
|
-
"
|
|
809
|
-
"propose_company_file), NOT a note on a
|
|
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: `
|
|
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, …) " +
|