@edda-business/mcp 0.72.0 → 0.74.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 +65 -15
package/package.json
CHANGED
package/src/server.js
CHANGED
|
@@ -66,22 +66,40 @@ async function resolvePathToId(path) {
|
|
|
66
66
|
const base = norm(segs[segs.length - 1] ?? "");
|
|
67
67
|
const folderSegs = segs.length > 1 ? segs.slice(0, -1).map(norm) : null;
|
|
68
68
|
let cands = pages.filter((p) => norm(p.name) === base);
|
|
69
|
-
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
//
|
|
69
|
+
|
|
70
|
+
// EDDA-91: a folder-qualified request MUST honour its folder. A basename-only match silently
|
|
71
|
+
// substituted an unrelated same-named page — an exact read returns THAT document or a truthful
|
|
72
|
+
// unavailable, never a masquerade.
|
|
73
|
+
//
|
|
74
|
+
// EDDA-151: it must honour the WHOLE folder path. The previous check asked only whether the
|
|
75
|
+
// page's leaf folder name appeared somewhere among the requested segments, so
|
|
76
|
+
// `AION/Context/_context` matched every project's `Context/_context.md` — six indistinguishable
|
|
77
|
+
// candidates, and the agent opening unrelated projects to find the one it asked for. Match
|
|
78
|
+
// against the page's full path (now returned by /pages) as a trailing sequence of segments, so
|
|
79
|
+
// `AION/Context/_context` matches `Projects/AION/Context/_context.md` and nothing else.
|
|
74
80
|
if (folderSegs) {
|
|
75
|
-
|
|
81
|
+
const wanted = segs.map(norm);
|
|
82
|
+
const scored = cands.map((p) => {
|
|
83
|
+
const full = String(p.path || [p.folder, p.name].filter(Boolean).join("/")).split("/").map(norm);
|
|
84
|
+
// strip the extension from the final segment for comparison
|
|
85
|
+
const tail = full.slice(-wanted.length);
|
|
86
|
+
const exact = tail.length === wanted.length && tail.every((seg, i) => seg === wanted[i]);
|
|
87
|
+
return { p, exact };
|
|
88
|
+
});
|
|
89
|
+
const exact = scored.filter((x) => x.exact).map((x) => x.p);
|
|
90
|
+
// Fall back to the old leaf-name behaviour only when nothing matches the full path, so a
|
|
91
|
+
// legacy flat row (no folder tree) still resolves.
|
|
92
|
+
cands = exact.length ? exact : cands.filter((p) => !p.path && p.folder && folderSegs.includes(norm(p.folder)));
|
|
76
93
|
}
|
|
94
|
+
|
|
77
95
|
if (!cands.length) {
|
|
78
96
|
return { error: folderSegs
|
|
79
97
|
? `"${String(path)}" is not available here — it either does not exist or is outside what this seat can read. Use search to see what you can access.`
|
|
80
98
|
: `No company page named "${String(path).split("/").pop()}". Use search to find the exact path first.` };
|
|
81
99
|
}
|
|
82
100
|
if (cands.length > 1) {
|
|
83
|
-
const opts = cands.map((p) => ` • ${[p.folder, p.name].filter(Boolean).join("/")} (id: ${p.id})`).join("\n");
|
|
84
|
-
return { error: `Several pages match "${
|
|
101
|
+
const opts = cands.map((p) => ` • ${p.path || [p.folder, p.name].filter(Boolean).join("/")} (id: ${p.id})`).join("\n");
|
|
102
|
+
return { error: `Several pages match "${String(path)}". Pass one of these ids as \`id\`:\n${opts}` };
|
|
85
103
|
}
|
|
86
104
|
return { id: cands[0].id };
|
|
87
105
|
}
|
|
@@ -110,6 +128,28 @@ async function uploadLocalFile(filePath, { sourceId } = {}) {
|
|
|
110
128
|
return { originalId: r?.original?.id ?? null, name, sha256, bytes: r?.original?.byte_size ?? bytes.length };
|
|
111
129
|
}
|
|
112
130
|
|
|
131
|
+
// A submit-only seat has no `inbox` tool, so the submit response is the ONLY place it can learn
|
|
132
|
+
// what happened to anything it sent. Render it there or the loop never closes.
|
|
133
|
+
function loopLines(r) {
|
|
134
|
+
const out = [];
|
|
135
|
+
const d = r?.recent_decisions;
|
|
136
|
+
if (d && d.total) {
|
|
137
|
+
const bits = [];
|
|
138
|
+
if (d.filed) bits.push(`${d.filed} filed`);
|
|
139
|
+
if (d.rejected) bits.push(`${d.rejected} rejected`);
|
|
140
|
+
out.push("", `Since ${String(d.since).slice(0, 10)}, of what you sent: ${bits.join(", ") || `${d.total} decided`}.`);
|
|
141
|
+
for (const e of d.examples ?? []) {
|
|
142
|
+
out.push(e.status === "rejected"
|
|
143
|
+
? ` ✗ ${e.name} — rejected${e.reason ? ` — ${e.reason}` : " (no reason given)"}`
|
|
144
|
+
: ` ✓ ${e.name} → ${e.location}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (typeof r?.pending_mine === "number" && r.pending_mine > 0) {
|
|
148
|
+
out.push(`${r.pending_mine} of your submissions ${r.pending_mine === 1 ? "is" : "are"} still waiting for review.`);
|
|
149
|
+
}
|
|
150
|
+
return out;
|
|
151
|
+
}
|
|
152
|
+
|
|
113
153
|
export function createServer() {
|
|
114
154
|
// EfB seat boundary: with EDDA_MCP_READONLY set (the flag Hermes EfB box profiles use),
|
|
115
155
|
// the personal agent gets EXACTLY search + read — the write tools (push/update/create) are
|
|
@@ -142,7 +182,9 @@ export function createServer() {
|
|
|
142
182
|
"connected sources (Slack, HubSpot, Stripe, calls). This is the default retrieval tool: reach " +
|
|
143
183
|
"for it before answering from generic knowledge about how the company works or what happened. " +
|
|
144
184
|
"Each hit is tagged [company] (a wiki page) or [company·distilled] (from activity), with a " +
|
|
145
|
-
"snippet
|
|
185
|
+
"snippet. A [company] hit carries an `id:` — pass THAT to read, it opens exactly the document " +
|
|
186
|
+
"you found. The `path:` is shown for context; prefer the id. Every hit returned is one you " +
|
|
187
|
+
"can actually open. " +
|
|
146
188
|
"Narrow with `kind`: 'documents' (wiki only), 'distilled' (activity only), or 'all' (default).",
|
|
147
189
|
{
|
|
148
190
|
question: z.string().describe("Natural-language query."),
|
|
@@ -185,8 +227,13 @@ export function createServer() {
|
|
|
185
227
|
const tag = declared ? "[company]" : "[company·distilled]";
|
|
186
228
|
const from = d.source_member ? ` · from ${d.source_member}` : "";
|
|
187
229
|
lines.push(`${tag} ${String(d.name ?? "").replace(/\.md$/, "")}${from} (${pct(d.similarity)})`);
|
|
188
|
-
|
|
189
|
-
|
|
230
|
+
// EDDA-151: the ID is what reliably opens the document. The API always returned it and
|
|
231
|
+
// this tool never printed it, so the agent had to go back through a path — and a path
|
|
232
|
+
// built from the leaf folder name could not tell one project's `Context/_context.md` from
|
|
233
|
+
// another's. Print the id first and say plainly that it is the thing to use.
|
|
234
|
+
if (declared && d.id) lines.push(` id: ${d.id} ← pass this to read`);
|
|
235
|
+
if (declared && d.ref) lines.push(` path: ${d.ref}`);
|
|
236
|
+
else if (!declared && d.ref) lines.push(` source: ${d.ref}`); // distilled: origin, read-only
|
|
190
237
|
if (d.snippet) lines.push(` ${d.snippet}`);
|
|
191
238
|
lines.push("");
|
|
192
239
|
}
|
|
@@ -201,8 +248,9 @@ export function createServer() {
|
|
|
201
248
|
server.tool(
|
|
202
249
|
"read",
|
|
203
250
|
"Read the FULL text of one company wiki page — search returns snippets; use this to " +
|
|
204
|
-
"get the whole document.
|
|
205
|
-
"
|
|
251
|
+
"get the whole document. PREFER the `id:` from a [company] search hit — it opens exactly that " +
|
|
252
|
+
"document with no ambiguity. A `path` also works, but pass the FULL project-qualified path as " +
|
|
253
|
+
"search printed it (e.g. 'Projects/AION/Context/_context.md'), not a fragment. Also returns the " +
|
|
206
254
|
"page's LINK GRAPH — outgoing [[links]] and backlinks — so you can TRAVERSE the wiki to " +
|
|
207
255
|
"complete the picture (read a linked page next, or see what references this one). Distilled " +
|
|
208
256
|
"hits ([company·distilled]) aren't pages — their content is in the search snippet.",
|
|
@@ -362,10 +410,11 @@ export function createServer() {
|
|
|
362
410
|
const n = Array.isArray(claims) ? claims.length : 0;
|
|
363
411
|
// Say what is waiting, including the belief change — the person needs to know a claim is
|
|
364
412
|
// pending a human decision, not quietly applied.
|
|
365
|
-
return text(
|
|
413
|
+
return text([
|
|
366
414
|
`Staged "${name}" into the company Inbox (${rc?.location ?? "Inbox"}). The company will route it to the right folder on the next reconcile.`
|
|
367
415
|
+ (n ? `\n${n} proposed knowledge update${n === 1 ? "" : "s"} rides with it — shown in the review gate as a belief change and applied only on approval.` : ""),
|
|
368
|
-
|
|
416
|
+
...loopLines(r),
|
|
417
|
+
].join("\n"));
|
|
369
418
|
} catch (e) {
|
|
370
419
|
const msg = String(e?.message || e);
|
|
371
420
|
if (msg.includes("read_only") || msg.includes("admins_only") || msg.includes("(403)")) return text("This seat is read-only on the company Edda, so it can search and read but not contribute. Ask an admin for a write role.");
|
|
@@ -443,6 +492,7 @@ export function createServer() {
|
|
|
443
492
|
else if (rec.suggestion_dropped) lines.push(` • ${rec.name} — staged; your suggested destination was dropped (not visible to you)`);
|
|
444
493
|
}
|
|
445
494
|
lines.push("", "They wait for a person to review and file them. Nothing has been placed in a project yet.");
|
|
495
|
+
lines.push(...loopLines(r));
|
|
446
496
|
return text(lines.join("\n"));
|
|
447
497
|
} catch (e) {
|
|
448
498
|
const msg = String(e?.message || e);
|