@opennous/mcp 0.16.0 → 0.18.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,7 +1,13 @@
1
1
  {
2
2
  "name": "@opennous/mcp",
3
- "version": "0.16.0",
3
+ "version": "0.18.0",
4
4
  "description": "Nous MCP Server — Customer graph for GTM agents.",
5
+ "license": "AGPL-3.0-only",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/NousC/nous.git",
9
+ "directory": "apps/mcp"
10
+ },
5
11
  "type": "module",
6
12
  "bin": {
7
13
  "nous-mcp": "src/index.js"
package/src/client.js CHANGED
@@ -9,6 +9,9 @@
9
9
  */
10
10
 
11
11
  import { AsyncLocalStorage } from "node:async_hooks";
12
+ import fs from "node:fs";
13
+ import os from "node:os";
14
+ import path from "node:path";
12
15
 
13
16
  // Resolve an env var defensively — Claude Code plugins use ${user_config.X}
14
17
  // substitution; when an optional userConfig field is left blank, the literal
@@ -32,15 +35,33 @@ export function runWithApiKey(apiKey, fn) {
32
35
  return apiKeyStore.run({ apiKey }, fn);
33
36
  }
34
37
 
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 the MCP picks up on the next call — no paste, no env var.
41
+ function fileApiKey() {
42
+ try {
43
+ const dir = resolvedEnv("NOUS_CONFIG_DIR") || path.join(os.homedir(), ".nous");
44
+ const cfg = JSON.parse(fs.readFileSync(path.join(dir, "config.json"), "utf8"));
45
+ const k = cfg?.apiKey;
46
+ return k && !String(k).includes("${") ? k : undefined;
47
+ } catch {
48
+ return undefined;
49
+ }
50
+ }
51
+
35
52
  function currentApiKey() {
36
- return apiKeyStore.getStore()?.apiKey ?? resolvedEnv("NOUS_API_KEY");
53
+ return apiKeyStore.getStore()?.apiKey ?? resolvedEnv("NOUS_API_KEY") ?? fileApiKey();
37
54
  }
38
55
 
39
- // stdio-only preflight: the env key must be present at startup.
56
+ // stdio-only preflight. A key may come from the env OR from `nous login`'s
57
+ // credential file. This is advisory — the server still starts without one so
58
+ // the user can run the login command after installing the plugin, and the key
59
+ // is resolved per-call.
40
60
  export function validateConfig() {
41
- if (!resolvedEnv("NOUS_API_KEY")) {
61
+ if (!resolvedEnv("NOUS_API_KEY") && !fileApiKey()) {
42
62
  throw new Error(
43
- "NOUS_API_KEY is required. Get yours at opennous.cloud Settings API Keys"
63
+ "No Nous API key found. Run the /nous-login command (or `npx @opennous/cli login`) to sign in, " +
64
+ "or set NOUS_API_KEY."
44
65
  );
45
66
  }
46
67
  }
package/src/index.js CHANGED
@@ -17,7 +17,15 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
17
17
  import { validateConfig } from "./client.js";
18
18
  import { createServer } from "./server.js";
19
19
 
20
- validateConfig();
20
+ // Advisory only — don't hard-exit if there's no key yet. The user may install
21
+ // the plugin and then run /nous-login; the server must already be running so the
22
+ // key (resolved per-call from env or ~/.nous/config.json) is picked up without a
23
+ // restart.
24
+ try {
25
+ validateConfig();
26
+ } catch (err) {
27
+ console.error(`[nous] ${err.message}`);
28
+ }
21
29
 
22
30
  const server = createServer();
23
31
  const transport = new StdioServerTransport();
package/src/server.js CHANGED
@@ -21,13 +21,16 @@
21
21
  * update_gtm_profile — write back a change to a GTM context section (evolve, keep history)
22
22
  * save_note — attach a note/document (meeting brief, transcript, prep) to a contact
23
23
  * search_notes — semantic search over saved notes & documents
24
+ * get_workspace_status — what's set up in this workspace + a ranked next_steps list (call first)
25
+ * set_workspace_profile— agent-driven onboarding: set the workspace's name, site, type, ICP
26
+ * build_scoring_model — build/rebuild the ICP scoring model from the recorded GTM context
24
27
  */
25
28
 
26
29
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
27
30
  import { z } from "zod";
28
31
  import { get, post } from "./client.js";
29
32
 
30
- export const SERVER_VERSION = "0.16.0";
33
+ export const SERVER_VERSION = "0.18.0";
31
34
 
32
35
  // ─── helpers ──────────────────────────────────────────────────────────────────
33
36
 
@@ -53,8 +56,10 @@ export function createServer() {
53
56
  name: "nous",
54
57
  version: SERVER_VERSION,
55
58
  description:
56
- "Nous — the context layer for GTM agents. Call get_context before drafting outreach or " +
57
- "preparing for a meeting. Call record after every interaction, or whenever you learn something.",
59
+ "Nous — the context layer for GTM agents. Nous is operated by the agent, not by a human " +
60
+ "clicking around: call get_workspace_status at the start of a session to see what's set up " +
61
+ "and what to set up next. Call get_context before drafting outreach or preparing for a " +
62
+ "meeting. Call record after every interaction, or whenever you learn something.",
58
63
  icons: [
59
64
  { src: "https://opennous.cloud/newlogoP.png", mimeType: "image/png", sizes: ["64x64"] },
60
65
  ],
@@ -485,5 +490,145 @@ export function createServer() {
485
490
  },
486
491
  );
487
492
 
493
+ // ===========================================================================
494
+ // TOOL: get_workspace_status — GET /v2/workspace/status
495
+ // The "one main call." Nous is operated by the agent, so the agent needs to
496
+ // know the state of the workspace: is it onboarded, is the GTM playbook built,
497
+ // which integrations are connected, is CRM sync configured, are events live —
498
+ // and what to set up next. Call this at the start of a session.
499
+ // ===========================================================================
500
+ server.tool(
501
+ "get_workspace_status",
502
+ "See the whole setup state of this workspace in one call — and what to set up next. Nous is " +
503
+ "operated by you, the agent, not by a human clicking through the app: call this at the start of " +
504
+ "a session to learn whether the workspace is onboarded, whether the GTM playbook (ICP model) is " +
505
+ "built, which integrations are connected, whether CRM sync is configured, and whether webhooks/" +
506
+ "triggers are live. Returns a ranked NEXT STEPS list — walk the user through whatever is missing " +
507
+ "(onboard them with set_workspace_profile, build the playbook with update_gtm_profile, connect " +
508
+ "their tools). Use this before offering to set anything up.",
509
+ {},
510
+ async () => {
511
+ const s = await get("/v2/workspace/status");
512
+ const setup = s.setup ?? {};
513
+ const lines = [];
514
+
515
+ const ws = s.workspace ?? {};
516
+ lines.push(`WORKSPACE: ${ws.name || "(unnamed)"}${ws.website ? ` · ${ws.website}` : ""}${ws.business_type ? ` · ${ws.business_type}` : ""}`);
517
+ lines.push("");
518
+
519
+ const mark = (b) => (b ? "✓" : "✗");
520
+ lines.push("SETUP:");
521
+ lines.push(` ${mark(setup.onboarding?.done)} Onboarding${setup.onboarding?.done ? "" : ` — missing ${(setup.onboarding?.missing ?? []).join(", ") || "details"}`}`);
522
+ lines.push(` ${mark(setup.gtm_playbook?.done)} GTM playbook${setup.gtm_playbook?.model ? " (scoring model live)" : ""}${setup.gtm_playbook?.stale_facts ? ` · ${setup.gtm_playbook.stale_facts} stale fact(s)` : ""}`);
523
+ const ints = setup.integrations?.connected ?? [];
524
+ lines.push(` ${mark((setup.integrations?.count ?? 0) > 0)} Integrations (${setup.integrations?.count ?? 0})${ints.length ? `: ${ints.map((i) => i.name).join(", ")}` : ""}`);
525
+ const crm = setup.crm_sync ?? {};
526
+ lines.push(` ${mark(crm.configured)} CRM sync${crm.configured ? `: ${(crm.providers ?? []).map((p) => p.provider).join(", ")}` : ""}${crm.pending_hygiene_proposals ? ` · ${crm.pending_hygiene_proposals} hygiene proposal(s) to review` : ""}`);
527
+ lines.push(` ${mark(setup.enrichment?.connected)} Enrichment${setup.enrichment?.provider ? `: ${setup.enrichment.provider}` : ""}`);
528
+ lines.push(` ${mark((setup.webhooks?.count ?? 0) > 0 || (setup.triggers?.count ?? 0) > 0)} Events — ${setup.webhooks?.count ?? 0} webhook(s), ${setup.triggers?.count ?? 0} trigger(s)`);
529
+
530
+ if (s.next_steps?.length) {
531
+ lines.push("");
532
+ lines.push("NEXT STEPS:");
533
+ for (const step of s.next_steps) {
534
+ lines.push(` • ${step.title}`);
535
+ if (step.why) lines.push(` why: ${step.why}`);
536
+ if (step.how) lines.push(` how: ${step.how}`);
537
+ }
538
+ } else {
539
+ lines.push("");
540
+ lines.push("Everything's set up. Nothing pending.");
541
+ }
542
+
543
+ return { content: [{ type: "text", text: lines.join("\n").trim() }] };
544
+ }
545
+ );
546
+
547
+ // ===========================================================================
548
+ // TOOL: set_workspace_profile — POST /v2/workspace/onboarding
549
+ // Agent-driven onboarding. Instead of a human clicking through a wizard in the
550
+ // app, you collect the basics from the user in conversation and write them
551
+ // here. This is the first thing get_workspace_status asks for when a workspace
552
+ // is new.
553
+ // ===========================================================================
554
+ server.tool(
555
+ "set_workspace_profile",
556
+ "Onboard the workspace, or update its basic profile. Nous is set up by you, the agent, in " +
557
+ "conversation — not by the user clicking through a wizard. Ask the user for their company name, " +
558
+ "their website, whether they sell a SERVICE or SOFTWARE, and a sentence describing their ideal " +
559
+ "customer, then write them here. This seeds the GTM context and the ICP scoring model. Call " +
560
+ "get_workspace_status first to see what's already set; send only the fields you're setting or " +
561
+ "changing. After this, the next step is usually the GTM playbook (update_gtm_profile).",
562
+ {
563
+ name: z.string().optional().describe("The user's company / workspace name."),
564
+ website: z.string().optional().describe("The company website (used to seed the GTM context)."),
565
+ business_type: z.enum(["service", "software"]).optional()
566
+ .describe("Whether they sell a service or software — sets the CRM's buyer terminology and default signup stage."),
567
+ plan_model: z.enum(["free_plan", "free_trial", "both", "paid_only"]).optional()
568
+ .describe("For software only: how they package (free plan, free trial, both, or paid only)."),
569
+ default_signup_stage: z.string().optional()
570
+ .describe("The pipeline stage a brand-new signup lands in (e.g. 'Lead', 'Free User'). Defaults sensibly from business_type."),
571
+ icp: z.string().optional()
572
+ .describe("A sentence or two describing their ideal customer — seeds the ICP scoring model."),
573
+ },
574
+ async ({ name, website, business_type, plan_model, default_signup_stage, icp }) => {
575
+ const r = await post("/v2/workspace/onboarding", { name, website, business_type, plan_model, default_signup_stage, icp });
576
+ const w = r.workspace ?? {};
577
+ const set = [
578
+ w.name && `name=${w.name}`,
579
+ w.website && `site=${w.website}`,
580
+ w.business_type && `type=${w.business_type}`,
581
+ icp && "ICP recorded",
582
+ ].filter(Boolean);
583
+ return { content: [{ type: "text", text:
584
+ `Workspace profile saved.${set.length ? ` ${set.join(" · ")}.` : ""}\n` +
585
+ `Next: call get_workspace_status to see what to set up next (usually the GTM playbook).` }] };
586
+ }
587
+ );
588
+
589
+ // ===========================================================================
590
+ // TOOL: build_scoring_model — POST /v2/workspace/scoring-model
591
+ // The second half of building the GTM playbook. The agent records the GTM
592
+ // context with update_gtm_profile, then calls this to turn it into a weighted
593
+ // ICP scoring model. After this, accounts get scored for fit and
594
+ // get_workspace_status shows the playbook as done.
595
+ // ===========================================================================
596
+ server.tool(
597
+ "build_scoring_model",
598
+ "Build (or rebuild) the user's ICP scoring model from the GTM context they've recorded. This is " +
599
+ "the second half of setting up the GTM playbook: first record the ICP and how they sell with " +
600
+ "update_gtm_profile, then call this to translate that context into a weighted set of scoring " +
601
+ "signals so accounts get scored for fit. If a model already exists it is left alone unless you " +
602
+ "pass force:true (use that when the GTM context has changed and the model should be rebuilt). If " +
603
+ "it reports no GTM context yet, record some with update_gtm_profile first, then call this again.",
604
+ {
605
+ force: z.boolean().optional()
606
+ .describe("Rebuild the model even if one already exists — use when the GTM context has changed."),
607
+ },
608
+ async ({ force }) => {
609
+ try {
610
+ const r = await post("/v2/workspace/scoring-model", { force: force === true });
611
+ const signals = r.signals ?? [];
612
+ const lines = [`Built the ICP scoring model — ${signals.length} signal${signals.length === 1 ? "" : "s"}:`];
613
+ for (const s of signals) lines.push(` • ${s.label ?? s.key} (weight ${s.weight})`);
614
+ lines.push("", "Accounts will now be scored for fit. Check it on the GTM Context page.");
615
+ return { content: [{ type: "text", text: lines.join("\n").trim() }] };
616
+ } catch (e) {
617
+ // Surface the actionable cases (no context yet / model already exists) as
618
+ // guidance rather than a raw error, so the agent knows what to do next.
619
+ const msg = String(e?.message ?? e);
620
+ if (msg.includes("no_gtm_context")) {
621
+ return { content: [{ type: "text", text:
622
+ "No GTM context recorded yet. Record the ICP and how they sell with update_gtm_profile first, then build the model." }] };
623
+ }
624
+ if (msg.includes("model_exists")) {
625
+ return { content: [{ type: "text", text:
626
+ "A scoring model already exists. Call build_scoring_model again with force:true to rebuild it from the current GTM context." }] };
627
+ }
628
+ throw e;
629
+ }
630
+ }
631
+ );
632
+
488
633
  return server;
489
634
  }