@edda-business/mcp 0.66.1 → 0.68.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 +79 -6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@edda-business/mcp",
3
- "version": "0.66.1",
3
+ "version": "0.68.0",
4
4
  "description": "Edda — the company data layer for AI agents.",
5
5
  "license": "Apache-2.0",
6
6
  "publishConfig": {
package/src/server.js CHANGED
@@ -197,6 +197,43 @@ export function createServer() {
197
197
 
198
198
  // EfB read-only seat mode stops here: tools/list is exactly search + read, and the write
199
199
  // tools below don't exist for the seat at all (unavailable, not merely rejected).
200
+ // ===========================================================================
201
+ // TOOL: orientation — GET /v2/company/orientation
202
+ // The served hot-cache: what is active right now, before you search.
203
+ // ===========================================================================
204
+ server.tool(
205
+ "orientation",
206
+ "START HERE at the beginning of a session: one cheap call that answers 'what is active in " +
207
+ "this company right now?' — each project with its status and its Current-state excerpt " +
208
+ "(from the project's index.md), the most recently updated pages, and how many items wait in " +
209
+ "the Inbox. Use it to orient BEFORE searching; then open the relevant project's index.md or " +
210
+ "search for specifics.",
211
+ {},
212
+ async () => {
213
+ try {
214
+ const r = await get("/v2/company/orientation");
215
+ const lines = [];
216
+ const projects = r?.projects ?? [];
217
+ lines.push(`Active projects (${projects.length}):`);
218
+ for (const p of projects) {
219
+ lines.push(`\n• ${p.name}${p.status ? ` [${p.status}]` : ""}`);
220
+ lines.push(p.current_state ? ` ${p.current_state.replace(/\n/g, "\n ")}` : " (no current-state entries yet)");
221
+ }
222
+ const recent = r?.recent_pages ?? [];
223
+ if (recent.length) {
224
+ lines.push(`\nRecently updated:`);
225
+ for (const pg of recent) lines.push(` · ${[pg.folder, pg.name].filter(Boolean).join("/")} (${String(pg.updated_at).slice(0, 10)})`);
226
+ }
227
+ lines.push(`\nInbox: ${r?.inbox_pending ?? 0} item(s) waiting.`);
228
+ return text(lines.join("\n"));
229
+ } catch (e) {
230
+ const msg = String(e?.message || e);
231
+ if (msg.includes("(404)")) return text("Orientation isn't available on this company's server yet — fall back to search.");
232
+ throw e;
233
+ }
234
+ },
235
+ );
236
+
200
237
  if (readOnly) return server;
201
238
 
202
239
  // ===========================================================================
@@ -241,6 +278,36 @@ export function createServer() {
241
278
  },
242
279
  );
243
280
 
281
+ // ===========================================================================
282
+ // TOOL: inbox — GET /v2/company/inbox
283
+ // The waiting room the agent-run ingest reads (Decision Log 2026-09-04: the agent is
284
+ // the analyst; the app is the filing clerk).
285
+ // ===========================================================================
286
+ server.tool(
287
+ "inbox",
288
+ "LIST what is waiting in the company Inbox — each staged item with its id, submitter, the " +
289
+ "submitter's suggested destination (if any), and a content preview. This is the start of the " +
290
+ "company ingest: read each item (use `read` for the full text if the preview is not enough), " +
291
+ "decide where it belongs among the real projects, then file it with " +
292
+ "reconcile { stagingId, path }. Admin/ambassador only.",
293
+ {},
294
+ async () => {
295
+ try {
296
+ const r = await get("/v2/company/inbox");
297
+ const pending = r?.pending ?? [];
298
+ if (!pending.length) return text("The company Inbox is empty — nothing waiting.");
299
+ const lines = pending.map((f) =>
300
+ `• ${f.name} (stagingId: ${f.id})\n staged ${String(f.created_at).slice(0, 10)}` +
301
+ `${f.suggested ? ` · suggested: ${f.suggested}` : ""}\n ${String(f.preview || "").replace(/\s+/g, " ").slice(0, 240)}`);
302
+ return text(`${pending.length} item(s) waiting in the company Inbox:\n\n${lines.join("\n\n")}`);
303
+ } catch (e) {
304
+ const msg = String(e?.message || e);
305
+ if (msg.includes("admins_only") || msg.includes("(403)")) return text("Only an admin or ambassador can read the company Inbox.");
306
+ throw e;
307
+ }
308
+ },
309
+ );
310
+
244
311
  // ===========================================================================
245
312
  // TOOL: reconcile — POST /v2/company/river/reconcile
246
313
  // Process the company Edda's Inbox: the River classifies + places each staged item into the
@@ -248,14 +315,20 @@ export function createServer() {
248
315
  // ===========================================================================
249
316
  server.tool(
250
317
  "reconcile",
251
- "PROCESS the company Inbox have the company file every item waiting in its Inbox into the " +
252
- "right folder (classify place make searchable). Run this after staging, or when the person " +
253
- "says 'process the company inbox'. Optionally pass a single `stagingId` to file just that item. " +
318
+ "PROCESS the company Inbox. Two modes. (1) Sweep: no arguments the company auto-files items " +
319
+ "with a clear destination and leaves ambiguous ones waiting. (2) ANALYZED FILING the ingest " +
320
+ "flow: list waiting items with `inbox`, read and analyze each one, then call this with that " +
321
+ "item's `stagingId` AND an explicit `path` (e.g. 'Projects/Whitehay AI/Working Documents') to " +
322
+ "file it exactly there. YOU are the analyst; the company only files what you tell it. " +
254
323
  "Returns where each item landed.",
255
- { stagingId: z.string().optional().describe("Optional id of one staged item to file (from a submit_company_candidate receipt). Omit to process the whole Inbox.") },
256
- async ({ stagingId }) => {
324
+ {
325
+ stagingId: z.string().optional().describe("Id of one staged item to file (from an `inbox` row or a submit receipt). Omit to sweep the whole Inbox."),
326
+ path: z.string().optional().describe("Explicit destination folder path for THAT item (requires stagingId). Missing folders are created."),
327
+ },
328
+ async ({ stagingId, path }) => {
257
329
  try {
258
- const r = await post("/v2/company/river/reconcile", { stagingId });
330
+ if (path && !stagingId) return text("An explicit `path` files ONE analyzed item — pass its stagingId too.");
331
+ const r = await post("/v2/company/river/reconcile", { stagingId, forcePath: path });
259
332
  const receipts = r?.receipts ?? [];
260
333
  if (!receipts.length) return text("Nothing waiting in the company Inbox.");
261
334
  const lines = receipts.map((x) => ` ${x.filed ? "✓ filed" : x.status} → ${x.location}${x.audience && x.audience !== "company" ? ` [${x.audience}]` : ""} (${x.reason})`);