@edda-business/mcp 0.59.0 → 0.61.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 -10
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@edda-business/mcp",
3
- "version": "0.59.0",
3
+ "version": "0.61.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,8 @@
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 · update_vault_file · propose_company_file
16
+ * WRITE save_note · save_to_vault · update_vault_file · propose_company_file ·
17
+ * propose_company_files (bulk) · create_folder
17
18
  * FIX merge_contacts (action=merge|split)
18
19
  * RUN list_integrations · connect_integration
19
20
  *
@@ -26,7 +27,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
26
27
  import { z } from "zod";
27
28
  import { get, post, del } from "./client.js";
28
29
 
29
- export const SERVER_VERSION = "0.58.0";
30
+ export const SERVER_VERSION = "0.61.0";
30
31
 
31
32
  // ─── helpers ──────────────────────────────────────────────────────────────────
32
33
 
@@ -701,19 +702,23 @@ export function createServer() {
701
702
  "Add a markdown page to the SHARED COMPANY vault — the workspace's declared knowledge " +
702
703
  "(policies, playbooks, decisions, SOPs, how-we-work) that every member's agent can read via " +
703
704
  "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).",
705
+ "it live immediately; a viewer's page lands in the company inbox for an admin to approve. File " +
706
+ "it with `folder` a name or path like 'Projects/Data Centre' (created if missing) plus an " +
707
+ "optional `subfolder`. Set `visibility` to scope who sees it: 'department' (the team it's filed " +
708
+ "in the default), 'company' (everyone), or 'owner' (just you). A page in a project inherits the " +
709
+ "project's Shared-with setting for OTHER departments. To add many pages at once, use " +
710
+ "propose_company_files. NOT the member's private vault (use save_to_vault), NOT a contact note (save_note).",
708
711
  {
709
712
  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
713
  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."),
714
+ visibility: z.enum(["owner", "department", "company"]).optional().describe("Who can see the page: 'department' (the team it's filed in — default), 'company' (everyone), or 'owner' (just you)."),
715
+ folder: z.string().optional().describe("Folder to file under, by NAME or path. Top-level folders are fixed a bare name becomes a project under Projects (e.g. 'Agency' → Projects/Agency); 'Top-Level/Sub' nests under a fixed folder. Created if missing (write role). Use this OR folder_id."),
716
+ subfolder: z.string().optional().describe("Optional subfolder within `folder`, e.g. 'Specs'."),
717
+ folder_id: z.string().optional().describe("Id of an existing company folder to file under (alternative to `folder`). Omit both to use the default 'wiki' folder."),
713
718
  },
714
- async ({ name, content, visibility, folder_id }) => {
719
+ async ({ name, content, visibility, folder, subfolder, folder_id }) => {
715
720
  try {
716
- const r = await post("/v2/company/pages", { name, content, visibility: visibility ?? "company", folder_id });
721
+ const r = await post("/v2/company/pages", { name, content, visibility, folder, subfolder, folder_id });
717
722
  const live = r?.page?.status === "live";
718
723
  const where = live
719
724
  ? `published live to the company vault — searchable now`
@@ -733,6 +738,71 @@ export function createServer() {
733
738
  );
734
739
 
735
740
 
741
+ // ===========================================================================
742
+ // TOOL: create_folder — POST /v2/company/folders { path }
743
+ // Create a folder / nested path in the shared company tree so pages can be filed into it.
744
+ // ===========================================================================
745
+ server.tool(
746
+ "create_folder",
747
+ "Create a folder in the SHARED COMPANY knowledge tree so you can file pages into it. The TOP-LEVEL " +
748
+ "folders are FIXED (Company · Projects · Decisions · Companies · People · Raw Documents · Archive) — you " +
749
+ "cannot add new ones. A bare name (or a path that doesn't start with one of those) becomes a PROJECT under " +
750
+ "Projects: 'Agency' → Projects/Agency. To nest under a specific top-level folder, start the path with it, " +
751
+ "e.g. 'Company/Policies' or 'Projects/Data Centre/Specs'. A project gets a 'Shared with' control the " +
752
+ "Ambassador uses to grant departments. A write role is required; the folder appears immediately. Then add " +
753
+ "pages with propose_company_file / propose_company_files using the same path. Returns the folder id.",
754
+ { 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.") },
755
+ async ({ path }) => {
756
+ try {
757
+ const r = await post("/v2/company/folders", { path });
758
+ const made = r?.created?.length ? ` (created ${r.created.join(" / ")})` : " (already existed)";
759
+ return { content: [{ type: "text", text: `Folder "${r?.folder?.name ?? path}" ready${made}. folder_id: ${r?.folder?.id ?? "?"}` }] };
760
+ } catch (e) {
761
+ const msg = String(e?.message || e);
762
+ if (msg.includes("read_only") || msg.includes("(403)")) return { content: [{ type: "text", text: "You don't have a write role in this company vault, so you can't create folders here." }] };
763
+ if (msg.includes("bad_path")) return { content: [{ type: "text", text: "Couldn't create that path — give a folder name or 'Parent/Child' path." }] };
764
+ throw e;
765
+ }
766
+ },
767
+ );
768
+
769
+ // ===========================================================================
770
+ // TOOL: propose_company_files — POST /v2/company/pages/batch
771
+ // Bulk-add many pages into ONE company folder (resolved/created once).
772
+ // ===========================================================================
773
+ server.tool(
774
+ "propose_company_files",
775
+ "Add MANY markdown pages to the shared company vault in ONE call — all into the SAME folder (resolved or " +
776
+ "created once). Use it to bulk-file: 'put these 10 docs in Data Centre'. Same rules as propose_company_file: " +
777
+ "a write role publishes them live; a viewer sends the whole batch to the company inbox. Name the target with " +
778
+ "`folder` (a path, created if missing) + optional `subfolder`, or an existing `folder_id`. Max 50 pages.",
779
+ {
780
+ folder: z.string().optional().describe("Folder to file all pages under. Top-level is fixed — a bare name becomes a project under Projects (e.g. 'Agency' → Projects/Agency); 'Top-Level/Sub' nests under a fixed folder. Created if missing."),
781
+ subfolder: z.string().optional().describe("Optional subfolder within `folder`."),
782
+ folder_id: z.string().optional().describe("Id of an existing folder (alternative to `folder`)."),
783
+ visibility: z.enum(["owner", "department", "company"]).optional().describe("Default visibility for the pages (a page can override its own). Default 'department'."),
784
+ pages: z.array(z.object({
785
+ name: z.string().describe("Page name."),
786
+ content: z.string().optional().describe("Markdown content."),
787
+ visibility: z.enum(["owner", "department", "company"]).optional().describe("Optional per-page visibility."),
788
+ })).describe("The pages to add: each { name, content, visibility? }."),
789
+ },
790
+ async ({ folder, subfolder, folder_id, visibility, pages }) => {
791
+ try {
792
+ const r = await post("/v2/company/pages/batch", { folder, subfolder, folder_id, visibility, pages });
793
+ const where = r?.status === "live" ? "published live — searchable now" : "sent to the company inbox for an admin to approve";
794
+ return { content: [{ type: "text", text: `${r?.count ?? 0} page(s) ${where} in "${r?.folder ?? folder ?? "wiki"}".` }] };
795
+ } catch (e) {
796
+ const msg = String(e?.message || e);
797
+ if (msg.includes("forbidden") || msg.includes("(403)")) return { content: [{ type: "text", text: "You don't have access to write to this company vault." }] };
798
+ if (msg.includes("bad_folder")) return { content: [{ type: "text", text: "That folder path couldn't be resolved or created — check the name, or pass folder_id." }] };
799
+ if (msg.includes("too_many")) return { content: [{ type: "text", text: "Too many pages — max 50 per call. Split into batches." }] };
800
+ if (msg.includes("pages_required")) return { content: [{ type: "text", text: "Give at least one page ({ name, content })." }] };
801
+ throw e;
802
+ }
803
+ },
804
+ );
805
+
736
806
  // ===========================================================================
737
807
  // TOOL: connect_integration — POST /v2/workspace/integrations
738
808
  // The agent connects a KEY-BASED integration for the user (no clicking through