@edda-business/mcp 0.62.0 → 0.63.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@edda-business/mcp",
3
- "version": "0.62.0",
3
+ "version": "0.63.0",
4
4
  "description": "Edda — the company data layer for AI agents.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
package/src/client.js CHANGED
@@ -24,7 +24,10 @@ function resolvedEnv(name) {
24
24
  return v;
25
25
  }
26
26
 
27
- const API_URL = resolvedEnv("NOUS_API_URL") || "https://api.opennous.cloud";
27
+ // Prefer the EDDA_* env vars; fall back to the legacy NOUS_* names so existing configs keep working.
28
+ const envKey = () => resolvedEnv("EDDA_API_KEY") ?? resolvedEnv("NOUS_API_KEY");
29
+ const envUrl = () => resolvedEnv("EDDA_API_URL") ?? resolvedEnv("NOUS_API_URL");
30
+ const API_URL = envUrl() || "https://api.opennous.cloud";
28
31
 
29
32
  // Per-request key context for the hosted HTTP server. Empty in stdio mode.
30
33
  export const apiKeyStore = new AsyncLocalStorage();
@@ -35,17 +38,16 @@ export function runWithApiKey(apiKey, fn) {
35
38
  return apiKeyStore.run({ apiKey }, fn);
36
39
  }
37
40
 
38
- // Credential written by `nous login` (the browser device-auth flow). The CLI and
39
- // the MCP server share ~/.nous/config.json, so a user who runs the login command
40
- // gets a key — and, for self-host, the API URL — the MCP picks up on the next
41
- // call, with no paste and no env var.
41
+ // Credential written by the CLI login (the browser device-auth flow). The CLI and the MCP server
42
+ // share a config.json, so a user who runs the login command gets a key — and, for self-host, the
43
+ // API URL — the MCP picks up on the next call, with no paste and no env var. Checks ~/.edda first,
44
+ // then legacy ~/.nous.
42
45
  function readFileConfig() {
43
- try {
44
- const dir = resolvedEnv("NOUS_CONFIG_DIR") || path.join(os.homedir(), ".nous");
45
- return JSON.parse(fs.readFileSync(path.join(dir, "config.json"), "utf8"));
46
- } catch {
47
- return null;
46
+ const dirs = [resolvedEnv("EDDA_CONFIG_DIR"), resolvedEnv("NOUS_CONFIG_DIR"), path.join(os.homedir(), ".edda"), path.join(os.homedir(), ".nous")].filter(Boolean);
47
+ for (const dir of dirs) {
48
+ try { return JSON.parse(fs.readFileSync(path.join(dir, "config.json"), "utf8")); } catch { /* try next */ }
48
49
  }
50
+ return null;
49
51
  }
50
52
  function clean(v) {
51
53
  return v && !String(v).includes("${") ? v : undefined; // drop unresolved ${...} markers
@@ -54,25 +56,22 @@ function fileApiKey() { return clean(readFileConfig()?.apiKey); }
54
56
  function fileApiUrl() { return clean(readFileConfig()?.apiUrl); }
55
57
 
56
58
  function currentApiKey() {
57
- return apiKeyStore.getStore()?.apiKey ?? resolvedEnv("NOUS_API_KEY") ?? fileApiKey();
59
+ return apiKeyStore.getStore()?.apiKey ?? envKey() ?? fileApiKey();
58
60
  }
59
61
 
60
- // Resolve the API base per call: env → ~/.nous/config.json (set by `nous login
61
- // --url` on self-host) → cloud default. So a self-hoster who logs in via the CLI
62
- // gets the MCP pointed at their own instance automatically.
62
+ // Resolve the API base per call: env → config.json (set by the CLI login on self-host) → cloud
63
+ // default. So a self-hoster who logs in via the CLI gets the MCP pointed at their own instance.
63
64
  function currentApiUrl() {
64
- return resolvedEnv("NOUS_API_URL") ?? fileApiUrl() ?? "https://api.opennous.cloud";
65
+ return envUrl() ?? fileApiUrl() ?? "https://api.opennous.cloud";
65
66
  }
66
67
 
67
- // stdio-only preflight. A key may come from the env OR from `nous login`'s
68
- // credential file. This is advisory the server still starts without one so
69
- // the user can run the login command after installing the plugin, and the key
70
- // is resolved per-call.
68
+ // stdio-only preflight. A key may come from the env OR the CLI login credential file. Advisory —
69
+ // the server still starts without one so the user can log in after installing, key resolved per-call.
71
70
  export function validateConfig() {
72
- if (!resolvedEnv("NOUS_API_KEY") && !fileApiKey()) {
71
+ if (!envKey() && !fileApiKey()) {
73
72
  throw new Error(
74
73
  "No Edda API key found. Run `npx @edda-business/cli login` to sign in (or `npx @edda-business/cli init` " +
75
- "to set up from scratch), or set NOUS_API_KEY."
74
+ "to set up from scratch), or set EDDA_API_KEY."
76
75
  );
77
76
  }
78
77
  }
package/src/index.js CHANGED
@@ -8,9 +8,10 @@
8
8
  * variant see http.js; the tools themselves live in server.js.
9
9
  *
10
10
  * Required env:
11
- * NOUS_API_KEY — workspace API key (Settings -> API Keys)
11
+ * EDDA_API_KEY — workspace API key (Sources -> API Keys). Encodes the workspace + scope.
12
12
  * Optional:
13
- * NOUS_API_URL — API base URL (default: https://api.opennous.cloud)
13
+ * EDDA_API_URL — API base URL for your instance, e.g. https://api.whitehayai.com
14
+ * (Legacy NOUS_API_KEY / NOUS_API_URL are still accepted as a fallback.)
14
15
  */
15
16
 
16
17
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
package/src/server.js CHANGED
@@ -29,7 +29,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
29
29
  import { z } from "zod";
30
30
  import { get, post, del } from "./client.js";
31
31
 
32
- export const SERVER_VERSION = "0.62.0";
32
+ export const SERVER_VERSION = "0.63.0";
33
33
 
34
34
  // ─── helpers ──────────────────────────────────────────────────────────────────
35
35
 
@@ -574,6 +574,80 @@ export function createServer() {
574
574
  },
575
575
  );
576
576
 
577
+ // ===========================================================================
578
+ // TOOL: list_my_files — GET /v2/personal/tree
579
+ // Navigate the member's personal PARA vault like a filesystem: Inbox · Projects ·
580
+ // Areas · Resources · Archive · People · Companies. Returns every file as a path.
581
+ // ===========================================================================
582
+ server.tool(
583
+ "list_my_files",
584
+ "List the files in your personal workspace — a PARA knowledge tree (Inbox, Projects, Areas, " +
585
+ "Resources, Archive, People, Companies) of markdown files you own. Use this to SEE what's there " +
586
+ "and navigate it like a filesystem before reading or writing. Returns each file's path " +
587
+ "(e.g. 'projects/acme-rollout/notes.md'); read one with read_my_file, write with save_personal_file.",
588
+ {
589
+ folder: z.string().optional().describe("Optional: only list files under this top-level folder (e.g. 'projects')."),
590
+ },
591
+ async ({ folder }) => {
592
+ const d = await get("/v2/personal/tree");
593
+ let files = d.files ?? [];
594
+ if (folder) files = files.filter((f) => f.folder === String(folder).toLowerCase());
595
+ if (!files.length) return { content: [{ type: "text", text: folder ? `No files in ${folder}/ yet.` : "Your personal workspace is empty." }] };
596
+ const lines = files.map((f) => ` ${f.path}${f.status === "inbox_pending" ? " (pending approval)" : ""}`).join("\n");
597
+ return { content: [{ type: "text", text: `${files.length} file(s):\n${lines}` }] };
598
+ },
599
+ );
600
+
601
+ // ===========================================================================
602
+ // TOOL: read_my_file — GET /v2/personal/file?path=…
603
+ // Read one personal-vault file by its path.
604
+ // ===========================================================================
605
+ server.tool(
606
+ "read_my_file",
607
+ "Read one file from your personal workspace by its path (e.g. 'companies/acme.md' or " +
608
+ "'projects/acme-rollout/notes.md'). Use list_my_files first to find the path. Returns the " +
609
+ "markdown content so you can work with it, then save changes with save_personal_file.",
610
+ {
611
+ path: z.string().describe("The file path: 'folder/[subfolder/]name.md' (folder is one of inbox/projects/areas/resources/archive/people/companies)."),
612
+ },
613
+ async ({ path }) => {
614
+ try {
615
+ const d = await get("/v2/personal/file", { path });
616
+ let text = `# ${d.path}\n\n${d.content || "(empty)"}`;
617
+ const out = (d.outgoing ?? []).filter((l) => l.resolved);
618
+ const back = d.backlinks ?? [];
619
+ if (out.length || back.length) {
620
+ text += `\n\n---\n`;
621
+ if (out.length) text += `Links to: ${out.map((l) => `${l.target}${l.kind.startsWith("relation:") ? ` (${l.kind.slice(9)})` : ""}`).join(", ")}\n`;
622
+ if (back.length) text += `Linked from: ${back.map((l) => l.source).join(", ")}\n`;
623
+ }
624
+ return { content: [{ type: "text", text }] };
625
+ } catch (e) {
626
+ const msg = String(e?.message || "");
627
+ return { content: [{ type: "text", text: msg.includes("404") ? `No file at ${path}. Use list_my_files to see what's there.` : `Couldn't read ${path}.` }] };
628
+ }
629
+ },
630
+ );
631
+
632
+ // ===========================================================================
633
+ // TOOL: create_project — POST /v2/personal/project
634
+ // Scaffold a project folder with the standard sub-structure.
635
+ // ===========================================================================
636
+ server.tool(
637
+ "create_project",
638
+ "Create a new project in your workspace, scaffolded with the standard structure: Context, " +
639
+ "Working Documents, Decisions, People & Companies, Deliverables, Raw Documents, Archive. " +
640
+ "Use this when you start a real piece of work so everything about it has a home. Then write " +
641
+ "files into projects/<name>/<subfolder>/… (e.g. the current status goes in Context).",
642
+ {
643
+ name: z.string().describe("The project name, e.g. 'Acme rollout'."),
644
+ },
645
+ async ({ name }) => {
646
+ const d = await post("/v2/personal/project", { name });
647
+ return { content: [{ type: "text", text: `Created project "${d.project}" at ${d.path}/ with: ${(d.folders || []).join(", ")}.` }] };
648
+ },
649
+ );
650
+
577
651
  // ===========================================================================
578
652
  // TOOL: propose_vault_file — POST /v2/personal/propose
579
653
  // Propose a markdown file into the member's PERSONAL vault. It lands in their