@edda-business/mcp 0.58.0 → 0.60.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 +94 -29
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@edda-business/mcp",
3
- "version": "0.58.0",
3
+ "version": "0.60.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.60.0";
30
31
 
31
32
  // ─── helpers ──────────────────────────────────────────────────────────────────
32
33
 
@@ -691,44 +692,45 @@ export function createServer() {
691
692
  );
692
693
 
693
694
  // ===========================================================================
694
- // TOOL: propose_company_file — POST /v2/company/propose
695
+ // TOOL: propose_company_file — POST /v2/company/pages
695
696
  // The WRITE side of the company vault (search_company_knowledge is the read side).
696
- // Admin-only: only an owner/admin may add to the shared knowledge; a member's agent
697
- // can read the company vault but gets a permission message here.
697
+ // Writes an in-app company page directly (no GitHub). Members with a write role
698
+ // publish it live; viewers land it in the company inbox for an admin to approve.
698
699
  // ===========================================================================
699
700
  server.tool(
700
701
  "propose_company_file",
701
- "Propose a markdown file for the SHARED COMPANY vault — the workspace's declared knowledge " +
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
- "search_company_knowledge. This OPENS A PULL REQUEST against the connected company repo for a " +
704
- "human to review and merge in GitHub nothing goes live until it's merged (company knowledge " +
705
- "is shared + institutional, so review-before-publish is the default; on merge the normal git " +
706
- "sync ingests it). Use it to contribute company-wide knowledge. ADMIN-ONLY: only an owner/admin " +
707
- "may propose; a regular member's agent can READ the vault but gets a permission message here. " +
708
- "NOT the member's private notes (use propose_vault_file), NOT a note on a contact (use save_note).",
704
+ "search_company_knowledge. This writes the page IN-APP: a member with a write role publishes " +
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).",
709
711
  {
710
- 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."),
711
- name: z.string().describe("The file name, ending in .md, e.g. 'Refund Policy.md' or 'Q3 Planning — 2026-08-22.md'."),
712
- content: z.string().describe("The full markdown content of the file."),
713
- subfolder: z.string().optional().describe("Optional subfolder within the folder."),
714
- message: z.string().optional().describe("Optional PR title / commit message describing the change."),
715
- 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)."),
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."),
713
+ content: z.string().describe("The full markdown content of the page."),
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, e.g. 'Projects/Data Centre'. Created if it doesn't exist (needs a 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."),
716
718
  },
717
- async ({ folder, name, content, subfolder, message, pr_group }) => {
719
+ async ({ name, content, visibility, folder, subfolder, folder_id }) => {
718
720
  try {
719
- const r = await post("/v2/company/propose", { folder, name, content, subfolder, message, pr_group });
720
- const how = r?.added_to_existing ? "added to the open PR" : "opened a pull request";
721
- 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.` }] };
721
+ const r = await post("/v2/company/pages", { name, content, visibility, folder, subfolder, folder_id });
722
+ const live = r?.page?.status === "live";
723
+ const where = live
724
+ ? `published live to the company vault — searchable now`
725
+ : `sent to the company inbox for an admin to approve`;
726
+ return { content: [{ type: "text", text: `"${r?.page?.name ?? name}" ${where}.` }] };
722
727
  } catch (e) {
723
728
  const msg = String(e?.message || e);
724
- if (msg.includes("admins_only") || msg.includes("(403)")) {
725
- 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.` }] };
726
- }
727
- if (msg.includes("no_company_repo")) {
728
- return { content: [{ type: "text", text: `Not proposed — no company GitHub repo is connected. An admin connects one under Company vault first.` }] };
729
+ if (msg.includes("forbidden") || msg.includes("(403)")) {
730
+ return { content: [{ type: "text", text: `Not savedyou don't have access to write to this company vault.` }] };
729
731
  }
730
- if (msg.includes("repo_write_denied")) {
731
- return { content: [{ type: "text", text: `Not proposedthe connected repo token is read-only. An admin must reconnect it with Contents:write + Pull requests:write.` }] };
732
+ if (msg.includes("bad_folder")) {
733
+ 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.` }] };
732
734
  }
733
735
  throw e;
734
736
  }
@@ -736,6 +738,69 @@ export function createServer() {
736
738
  );
737
739
 
738
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 (or a nested path) in the SHARED COMPANY knowledge tree so you can file pages into it. " +
748
+ "Pass `path` as a name or slash-path, e.g. 'Projects/Data Centre' — every missing level is created. A " +
749
+ "folder directly under 'Projects' becomes a project (it gets a 'Shared with' control the Ambassador uses " +
750
+ "to grant departments). A write role is required and the folder appears immediately. Then add pages with " +
751
+ "propose_company_file / propose_company_files using the same `folder` path. Returns the folder id.",
752
+ { path: z.string().describe("Folder name or path, e.g. 'Projects/Data Centre' or 'Playbooks'.") },
753
+ async ({ path }) => {
754
+ try {
755
+ const r = await post("/v2/company/folders", { path });
756
+ const made = r?.created?.length ? ` (created ${r.created.join(" / ")})` : " (already existed)";
757
+ return { content: [{ type: "text", text: `Folder "${r?.folder?.name ?? path}" ready${made}. folder_id: ${r?.folder?.id ?? "?"}` }] };
758
+ } catch (e) {
759
+ const msg = String(e?.message || e);
760
+ 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." }] };
761
+ if (msg.includes("bad_path")) return { content: [{ type: "text", text: "Couldn't create that path — give a folder name or 'Parent/Child' path." }] };
762
+ throw e;
763
+ }
764
+ },
765
+ );
766
+
767
+ // ===========================================================================
768
+ // TOOL: propose_company_files — POST /v2/company/pages/batch
769
+ // Bulk-add many pages into ONE company folder (resolved/created once).
770
+ // ===========================================================================
771
+ server.tool(
772
+ "propose_company_files",
773
+ "Add MANY markdown pages to the shared company vault in ONE call — all into the SAME folder (resolved or " +
774
+ "created once). Use it to bulk-file: 'put these 10 docs in Data Centre'. Same rules as propose_company_file: " +
775
+ "a write role publishes them live; a viewer sends the whole batch to the company inbox. Name the target with " +
776
+ "`folder` (a path, created if missing) + optional `subfolder`, or an existing `folder_id`. Max 50 pages.",
777
+ {
778
+ folder: z.string().optional().describe("Folder path to file all pages under, e.g. 'Projects/Data Centre'. Created if missing."),
779
+ subfolder: z.string().optional().describe("Optional subfolder within `folder`."),
780
+ folder_id: z.string().optional().describe("Id of an existing folder (alternative to `folder`)."),
781
+ visibility: z.enum(["owner", "department", "company"]).optional().describe("Default visibility for the pages (a page can override its own). Default 'department'."),
782
+ pages: z.array(z.object({
783
+ name: z.string().describe("Page name."),
784
+ content: z.string().optional().describe("Markdown content."),
785
+ visibility: z.enum(["owner", "department", "company"]).optional().describe("Optional per-page visibility."),
786
+ })).describe("The pages to add: each { name, content, visibility? }."),
787
+ },
788
+ async ({ folder, subfolder, folder_id, visibility, pages }) => {
789
+ try {
790
+ const r = await post("/v2/company/pages/batch", { folder, subfolder, folder_id, visibility, pages });
791
+ const where = r?.status === "live" ? "published live — searchable now" : "sent to the company inbox for an admin to approve";
792
+ return { content: [{ type: "text", text: `${r?.count ?? 0} page(s) ${where} in "${r?.folder ?? folder ?? "wiki"}".` }] };
793
+ } catch (e) {
794
+ const msg = String(e?.message || e);
795
+ if (msg.includes("forbidden") || msg.includes("(403)")) return { content: [{ type: "text", text: "You don't have access to write to this company vault." }] };
796
+ 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." }] };
797
+ if (msg.includes("too_many")) return { content: [{ type: "text", text: "Too many pages — max 50 per call. Split into batches." }] };
798
+ if (msg.includes("pages_required")) return { content: [{ type: "text", text: "Give at least one page ({ name, content })." }] };
799
+ throw e;
800
+ }
801
+ },
802
+ );
803
+
739
804
  // ===========================================================================
740
805
  // TOOL: connect_integration — POST /v2/workspace/integrations
741
806
  // The agent connects a KEY-BASED integration for the user (no clicking through