@edda-business/mcp 0.64.0 → 0.65.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 +31 -31
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@edda-business/mcp",
3
- "version": "0.64.0",
3
+ "version": "0.65.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
@@ -20,11 +20,11 @@
20
20
  * + embeds them. You only RETRIEVE them (they surface in search tagged ·distilled).
21
21
  *
22
22
  * TOOLS:
23
- * search_company RETRIEVE across BOTH natures (documents + distilled), kind filter
24
- * read_company_page read one full wiki page (by path from a search result, or id)
25
- * push_company_file PUSH one or many files up into the company folders (shared wiki)
26
- * update_company_page edit an existing wiki page (replace or append)
27
- * create_company_folder organize the wiki tree
23
+ * search RETRIEVE across BOTH natures (documents + distilled), kind filter
24
+ * read read one full wiki page (by path from a search result, or id)
25
+ * push PUSH one or many files up into the company folders (shared wiki)
26
+ * update edit an existing wiki page (replace or append)
27
+ * create organize the wiki tree
28
28
  *
29
29
  * Sources (connecting Slack/HubSpot/… so distillation happens) are wired in the Edda web
30
30
  * app, not here — the agent only reads and writes knowledge.
@@ -34,7 +34,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
34
34
  import { z } from "zod";
35
35
  import { get, post, patch } from "./client.js";
36
36
 
37
- export const SERVER_VERSION = "0.64.0";
37
+ export const SERVER_VERSION = "0.65.0";
38
38
 
39
39
  // ─── helpers ──────────────────────────────────────────────────────────────────
40
40
 
@@ -63,7 +63,7 @@ async function resolvePathToId(path) {
63
63
  const pages = list?.pages ?? [];
64
64
  const base = norm(String(path).split("/").pop());
65
65
  let cands = pages.filter((p) => norm(p.name) === base);
66
- if (!cands.length) return { error: `No company page named "${String(path).split("/").pop()}". Use search_company to find the exact path first.` };
66
+ if (!cands.length) return { error: `No company page named "${String(path).split("/").pop()}". Use search to find the exact path first.` };
67
67
  if (cands.length > 1) {
68
68
  // Prefer a candidate whose folder slug appears in the requested path.
69
69
  const pref = cands.filter((p) => p.folder && norm(path).includes(norm(p.folder)));
@@ -87,7 +87,7 @@ export function createServer() {
87
87
  "Edda — the company's shared knowledge for AI agents. Search the company wiki (declared " +
88
88
  "policies, playbooks, decisions) AND distilled knowledge from connected sources (Slack, " +
89
89
  "HubSpot, calls) in one call; read a full page; and contribute pages/folders back. Reach " +
90
- "for search_company before answering from generic knowledge. (Personal files live on the " +
90
+ "for search before answering from generic knowledge. (Personal files live on the " +
91
91
  "user's own disk; this is the shared company layer only.)",
92
92
  icons: [
93
93
  { src: "https://opennous.cloud/newlogoP.png", mimeType: "image/png", sizes: ["64x64"] },
@@ -95,18 +95,18 @@ export function createServer() {
95
95
  });
96
96
 
97
97
  // ===========================================================================
98
- // TOOL: search_company — POST /v2/company/search
98
+ // TOOL: search — POST /v2/company/search
99
99
  // The primary read surface. Hybrid retrieval (RRF + rerank) fused across the DECLARED wiki
100
100
  // and the DERIVED distilled layer, ACL-filtered. `kind` narrows to one nature.
101
101
  // ===========================================================================
102
102
  server.tool(
103
- "search_company",
103
+ "search",
104
104
  "Search the COMPANY'S SHARED KNOWLEDGE in one call — both natures at once: the DECLARED wiki " +
105
105
  "(policies, playbooks, decisions, SOPs, how-we-work) AND DERIVED knowledge distilled from " +
106
106
  "connected sources (Slack, HubSpot, Stripe, calls). This is the default retrieval tool: reach " +
107
107
  "for it before answering from generic knowledge about how the company works or what happened. " +
108
108
  "Each hit is tagged [company] (a wiki page) or [company·distilled] (from activity), with a " +
109
- "snippet; wiki hits include a `path:` you can pass to read_company_page for the full text. " +
109
+ "snippet; wiki hits include a `path:` you can pass to read for the full text. " +
110
110
  "Narrow with `kind`: 'documents' (wiki only), 'distilled' (activity only), or 'all' (default).",
111
111
  {
112
112
  question: z.string().describe("Natural-language query."),
@@ -132,7 +132,7 @@ export function createServer() {
132
132
  const tag = declared ? "[company]" : "[company·distilled]";
133
133
  const from = d.source_member ? ` · from ${d.source_member}` : "";
134
134
  lines.push(`${tag} ${String(d.name ?? "").replace(/\.md$/, "")}${from} (${pct(d.similarity)})`);
135
- if (declared && d.ref) lines.push(` path: ${d.ref}`); // pass to read_company_page
135
+ if (declared && d.ref) lines.push(` path: ${d.ref}`); // pass to read
136
136
  else if (d.ref) lines.push(` source: ${d.ref}`); // distilled: origin, read-only
137
137
  if (d.snippet) lines.push(` ${d.snippet}`);
138
138
  lines.push("");
@@ -142,12 +142,12 @@ export function createServer() {
142
142
  );
143
143
 
144
144
  // ===========================================================================
145
- // TOOL: read_company_page — GET /v2/company/pages/:id
146
- // Read one full wiki page. Take the `path:` from a search_company result, or an id.
145
+ // TOOL: read — GET /v2/company/pages/:id
146
+ // Read one full wiki page. Take the `path:` from a search result, or an id.
147
147
  // ===========================================================================
148
148
  server.tool(
149
- "read_company_page",
150
- "Read the FULL text of one company wiki page — search_company returns snippets; use this to " +
149
+ "read",
150
+ "Read the FULL text of one company wiki page — search returns snippets; use this to " +
151
151
  "get the whole document. Pass the `path:` shown under a [company] search hit (e.g. " +
152
152
  "'Company/Policies/Refund Policy.md'), or an `id` if you already have one. Also returns the " +
153
153
  "page's LINK GRAPH — outgoing [[links]] and backlinks — so you can TRAVERSE the wiki to " +
@@ -164,7 +164,7 @@ export function createServer() {
164
164
  if (res.error) return text(res.error);
165
165
  pageId = res.id;
166
166
  }
167
- if (!pageId) return text("Tell me which page to read — pass the `path` from a search_company result (its `path:` line), or an `id`.");
167
+ if (!pageId) return text("Tell me which page to read — pass the `path` from a search result (its `path:` line), or an `id`.");
168
168
  try {
169
169
  const r = await get(`/v2/company/pages/${encodeURIComponent(pageId)}`);
170
170
  const p = r?.page;
@@ -175,14 +175,14 @@ export function createServer() {
175
175
  const outgoing = p.ingest?.outgoing ?? [];
176
176
  const backlinks = p.ingest?.backlinks ?? [];
177
177
  if (outgoing.length || backlinks.length) {
178
- out.push("", "---", "Links (read_company_page on any of these to traverse):");
178
+ out.push("", "---", "Links (read on any of these to traverse):");
179
179
  if (outgoing.length) out.push(" → links to: " + outgoing.map((l) => `${l.target}${l.resolved ? "" : " (unresolved)"}`).join(", "));
180
180
  if (backlinks.length) out.push(" ← linked from: " + backlinks.map((b) => b.source).join(", "));
181
181
  }
182
182
  return text(out.join("\n"));
183
183
  } catch (e) {
184
184
  const msg = String(e?.message || e);
185
- if (msg.includes("not_found") || msg.includes("(404)")) return text("No page with that id — use search_company to find the path, then read it.");
185
+ if (msg.includes("not_found") || msg.includes("(404)")) return text("No page with that id — use search to find the path, then read it.");
186
186
  if (msg.includes("forbidden") || msg.includes("(403)")) return text("You don't have access to read that company page.");
187
187
  throw e;
188
188
  }
@@ -190,15 +190,15 @@ export function createServer() {
190
190
  );
191
191
 
192
192
  // ===========================================================================
193
- // TOOL: push_company_file — POST /v2/company/pages/batch
193
+ // TOOL: push — POST /v2/company/pages/batch
194
194
  // PUSH one or many files UP into the shared company folders (the personal→company promotion).
195
195
  // Hybrid write policy: a write role publishes live; a viewer sends the batch to the company
196
196
  // inbox to approve.
197
197
  // ===========================================================================
198
198
  server.tool(
199
- "push_company_file",
199
+ "push",
200
200
  "PUSH a file (or several) UP into the COMPANY WIKI — promote knowledge from your own work into " +
201
- "the shared company folders every member's agent can read via search_company. Typically you " +
201
+ "the shared company folders every member's agent can read via search. Typically you " +
202
202
  "push a personal/working file up so the company has it. All pushed files go into the SAME " +
203
203
  "folder (resolved or created once). Name the target with `folder` — a name or path like " +
204
204
  "'Projects/Data Centre' (top-level folders are fixed; a bare name becomes a project under " +
@@ -233,16 +233,16 @@ export function createServer() {
233
233
  );
234
234
 
235
235
  // ===========================================================================
236
- // TOOL: update_company_page — PATCH /v2/company/pages/:id
236
+ // TOOL: update — PATCH /v2/company/pages/:id
237
237
  // Edit an existing wiki page: replace its content, or append. Re-embeds on the server.
238
238
  // ===========================================================================
239
239
  server.tool(
240
- "update_company_page",
240
+ "update",
241
241
  "Edit an EXISTING company wiki page — revise it or append to it. Identify the page by `path` " +
242
- "(the `path:` from a search_company result) or `id`. By default the new `content` REPLACES the " +
242
+ "(the `path:` from a search result) or `id`. By default the new `content` REPLACES the " +
243
243
  "page; set append:true to add to the end instead (kept for a rolling log or a running doc). " +
244
244
  "You can also rename it with `name`. Re-embedded so search picks up the change. For a NEW page " +
245
- "use push_company_file. Distilled items (from sources) are read-only and can't be edited here.",
245
+ "use push. Distilled items (from sources) are read-only and can't be edited here.",
246
246
  {
247
247
  path: z.string().optional().describe("The page path from a search result's `path:` line. Provide this OR id."),
248
248
  id: z.string().optional().describe("The page id (advanced). Provide this OR path."),
@@ -257,7 +257,7 @@ export function createServer() {
257
257
  if (res.error) return text(res.error);
258
258
  pageId = res.id;
259
259
  }
260
- if (!pageId) return text("Tell me which page to edit — pass the `path` from a search_company result, or an `id`.");
260
+ if (!pageId) return text("Tell me which page to edit — pass the `path` from a search result, or an `id`.");
261
261
  try {
262
262
  const body = {};
263
263
  if (append) {
@@ -271,7 +271,7 @@ export function createServer() {
271
271
  return text(`Updated "${name ? name.replace(/\.md$/, "") : (path ? String(path).split("/").pop().replace(/\.md$/, "") : pageId)}"${append ? " (appended)" : ""} — re-embedded, searchable now.`);
272
272
  } catch (e) {
273
273
  const msg = String(e?.message || e);
274
- if (msg.includes("not_found") || msg.includes("(404)")) return text("No editable page with that id — distilled items are read-only. Use search_company to find a wiki page.");
274
+ if (msg.includes("not_found") || msg.includes("(404)")) return text("No editable page with that id — distilled items are read-only. Use search to find a wiki page.");
275
275
  if (msg.includes("forbidden") || msg.includes("(403)")) return text("You don't have access to edit that company page.");
276
276
  throw e;
277
277
  }
@@ -279,17 +279,17 @@ export function createServer() {
279
279
  );
280
280
 
281
281
  // ===========================================================================
282
- // TOOL: create_company_folder — POST /v2/company/folders
282
+ // TOOL: create — POST /v2/company/folders
283
283
  // Create a folder / nested path in the shared company tree so pages can be filed into it.
284
284
  // ===========================================================================
285
285
  server.tool(
286
- "create_company_folder",
286
+ "create",
287
287
  "Create a folder in the COMPANY WIKI tree so you can file pages into it. The TOP-LEVEL folders " +
288
288
  "are FIXED (Company · Projects · Decisions · Companies · People · Raw Documents · Archive) — you " +
289
289
  "cannot add new ones. A bare name (or a path not starting with a fixed folder) becomes a PROJECT " +
290
290
  "under Projects: 'Agency' → Projects/Agency. To nest under a specific top-level folder, start the " +
291
291
  "path with it, e.g. 'Company/Policies' or 'Projects/Data Centre/Specs'. A write role is required; " +
292
- "the folder appears immediately. Then push files into it with push_company_file using the same " +
292
+ "the folder appears immediately. Then push files into it with push using the same " +
293
293
  "path. Returns the folder id.",
294
294
  { path: z.string().describe("Folder name or path. A bare name → a project under Projects; 'Top-Level/Sub' nests under a fixed top-level folder.") },
295
295
  async ({ path }) => {