@nightowlsdev/mcp-server 0.1.0 → 1.0.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/README.md ADDED
@@ -0,0 +1,195 @@
1
+ # @nightowlsdev/mcp-server
2
+
3
+ > Expose a Night Owls swarm as `ask_<agent>` MCP tools — serving machine callers, human MCP-client UIs, and services uniformly.
4
+
5
+ This package turns any Night Owls `SwarmEngine` into an MCP server. Each agent you register becomes an `ask_<slug>` tool that handles both fresh conversations and multi-turn human-in-the-loop (HITL) resume. The engine-wall contract is enforced: only `@nightowlsdev/core` types and the raw `@modelcontextprotocol/sdk` are imported — no engine-vendor (`@mastra/*`) symbols leak into the public API.
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ pnpm add @nightowlsdev/mcp-server
11
+ ```
12
+
13
+ **Peer dependencies** (install separately):
14
+
15
+ ```sh
16
+ pnpm add @nightowlsdev/core
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ### stdio server (local / CLI integration)
22
+
23
+ ```ts
24
+ import { runSwarmMcpStdio } from "@nightowlsdev/mcp-server";
25
+ import { SwarmEngine, InMemoryStorage } from "@nightowlsdev/core";
26
+ import type { SwarmMcpContext } from "@nightowlsdev/mcp-server";
27
+
28
+ const storage = new InMemoryStorage();
29
+ const engine = new SwarmEngine({ storage, model, modelFactory, cost });
30
+
31
+ // All logs go to stderr; stdout is the JSON-RPC channel.
32
+ await runSwarmMcpStdio({
33
+ engine,
34
+ storage,
35
+ agents: [
36
+ { slug: "biller", name: "Billing agent", role: "handles refunds and invoices" },
37
+ { slug: "support" },
38
+ ],
39
+ // stdio/local: return a fixed context.
40
+ // HTTP: read extra.authInfo or request headers — never tool args.
41
+ resolveContext: (): SwarmMcpContext => ({ tenantId: "acme", userId: "u-123" }),
42
+ });
43
+ // Process stays alive; connect your MCP client to its stdin/stdout.
44
+ ```
45
+
46
+ ### HTTP / programmatic server
47
+
48
+ ```ts
49
+ import { createSwarmMcpServer } from "@nightowlsdev/mcp-server";
50
+
51
+ const server = createSwarmMcpServer({
52
+ engine,
53
+ storage,
54
+ agents: await engine.listAgents(ctx), // AgentSummary[] — pass directly
55
+ resolveContext: async (extra) => {
56
+ // extra = transport/auth info from the MCP SDK; shape depends on your transport.
57
+ // Return null to reject as unauthorized.
58
+ const token = (extra as { authInfo?: { token: string } }).authInfo?.token;
59
+ if (!token) return null;
60
+ return { tenantId: "acme", userId: await verifyToken(token) };
61
+ },
62
+ });
63
+
64
+ // Wire to your chosen transport (e.g. StreamableHTTPServerTransport).
65
+ await server.connect(transport);
66
+ ```
67
+
68
+ ## API
69
+
70
+ ### `createSwarmMcpServer(opts: SwarmMcpServerOpts): McpServer`
71
+
72
+ Builds and returns an MCP `McpServer` (from `@modelcontextprotocol/sdk`) pre-loaded with one `ask_<slug>` tool per agent in `opts.agents`. The server is not yet connected — call `server.connect(transport)` yourself.
73
+
74
+ ### `runSwarmMcpStdio(opts: SwarmMcpServerOpts): Promise<McpServer>`
75
+
76
+ Convenience wrapper: builds the server and immediately connects it to a `StdioServerTransport`. All diagnostic logs go to stderr; stdout carries the JSON-RPC channel. Resolves with the connected server; await it or attach a `close` handler to keep the process alive.
77
+
78
+ ### `SwarmMcpServerOpts`
79
+
80
+ ```ts
81
+ interface SwarmMcpServerOpts {
82
+ /** The running Night Owls engine. */
83
+ engine: SwarmEngine;
84
+ /** The storage adapter (used to look up suspended runs on resume). */
85
+ storage: StorageAdapter;
86
+ /**
87
+ * Agents to expose. Each slug becomes an ask_<slug> MCP tool.
88
+ * Pass `await engine.listAgents(ctx)` (AgentSummary[]) directly, or a hand-built list.
89
+ * name/role/description only shape the tool's model-facing description.
90
+ */
91
+ agents: SwarmMcpAgent[];
92
+ /**
93
+ * Resolve caller identity from the MCP request's per-handler `extra` (transport/auth info).
94
+ * Return null to reject the call as unauthorized.
95
+ * Identity is taken ONLY from here — never from tool arguments, which are attacker-controllable.
96
+ */
97
+ resolveContext: (extra: unknown) => Promise<SwarmMcpContext | null> | SwarmMcpContext | null;
98
+ /** Id generator for runId and threadId. Injectable for deterministic tests. Default: crypto.randomUUID. */
99
+ newId?: () => string;
100
+ /** MCP server name advertised in the handshake. Default: "nightowls-swarm". */
101
+ name?: string;
102
+ /** MCP server version advertised in the handshake. Default: "0.1.0". */
103
+ version?: string;
104
+ }
105
+ ```
106
+
107
+ ### `SwarmMcpAgent`
108
+
109
+ ```ts
110
+ interface SwarmMcpAgent {
111
+ slug: string; // Becomes the ask_<slug> tool name.
112
+ name?: string; // Human label used in the tool title.
113
+ role?: string; // Inserted into the auto-generated tool description.
114
+ description?: string; // Overrides the auto-generated tool description entirely.
115
+ }
116
+ ```
117
+
118
+ ### `SwarmMcpContext`
119
+
120
+ ```ts
121
+ interface SwarmMcpContext {
122
+ /** Tenant that owns this call — the ONLY source of tenancy; never read from tool args. */
123
+ tenantId: string;
124
+ /** The end-user making the request. */
125
+ userId: string;
126
+ }
127
+ ```
128
+
129
+ ## `ask_<slug>` tool — input/output contract
130
+
131
+ Every registered agent is exposed as an MCP tool named `ask_<slug>`. The tool handles both fresh asks and multi-turn HITL resume within the same signature.
132
+
133
+ ### Tool input
134
+
135
+ | Field | Type | Required | Purpose |
136
+ |---|---|---|---|
137
+ | `message` | `string` | Required for a **new** ask | What to ask the agent. |
138
+ | `threadId` | `string` | Optional | Continue an existing conversation. Omit to start a new thread. |
139
+ | `context` | `Record<string, unknown>` | Optional | Untrusted/advisory page context forwarded to the agent. |
140
+ | `followupId` | `string` | Required for **resume** | Taken from a prior `needs_input` result. |
141
+ | `answer` | `string` | Required for **resume** | Your answer to the agent's suspended question. Pass with `followupId`. |
142
+
143
+ For a fresh call, supply `message` (and optionally `threadId`/`context`). For a resume call, supply `followupId` + `answer` (message is ignored).
144
+
145
+ ### Tool output
146
+
147
+ All results are returned as a JSON object in `content[0].text`. The `status` field selects the variant:
148
+
149
+ **`done`** — the agent finished and returned its answer.
150
+
151
+ ```json
152
+ { "status": "done", "answer": "Refund of $42 processed.", "runId": "<uuid>" }
153
+ ```
154
+
155
+ **`needs_input`** — the agent asked a question and is durably suspended. The run remains open; the caller must answer by calling the same tool again with `followupId` + `answer`.
156
+
157
+ ```json
158
+ {
159
+ "status": "needs_input",
160
+ "question": "Which order number should I refund?",
161
+ "followupId": "<durable-token>",
162
+ "runId": "<uuid>",
163
+ "partial": "I can help with your refund."
164
+ }
165
+ ```
166
+
167
+ - `followupId` is the durable suspend token. **Do not discard it.** Without it the run cannot be resumed and will be orphaned.
168
+ - `partial` carries any assistant text streamed before the question was asked; may be absent.
169
+ - The `to` field on a swarm question is advisory metadata — the MCP caller (the host) is always the sole resumer, regardless of what `to` says.
170
+
171
+ **`failed`** — the engine reported a run failure.
172
+
173
+ ```json
174
+ { "status": "failed", "message": "Tool call limit exceeded.", "stage": "run", "runId": "<uuid>" }
175
+ ```
176
+
177
+ - `stage` echoes the engine's failure stage (e.g. `"run"`, `"resume"`).
178
+
179
+ **`error`** — a pre-run error (unauthorized, missing argument, unknown agent, storage failure). `isError: true` is set on the MCP result envelope.
180
+
181
+ ```json
182
+ { "status": "error", "message": "unauthorized: resolveContext returned null" }
183
+ ```
184
+
185
+ Every failure path — including engine throws that occur before the first event is yielded — returns this structured JSON shape rather than a raw exception, so callers can always parse `content[0].text` safely.
186
+
187
+ ## Configuration / Environment
188
+
189
+ This package reads **no environment variables directly**. Configuration is passed entirely through `SwarmMcpServerOpts` at construction time. The underlying `@nightowlsdev/core` engine reads its own env vars (model keys, cost limits, etc.) — see the `@nightowlsdev/core` README.
190
+
191
+ ## Engine-wall contract
192
+
193
+ `@nightowlsdev/mcp-server` imports only `@nightowlsdev/core` **types** and the raw `@modelcontextprotocol/sdk`. It never imports `@mastra/*` symbols. The wall is verified at build time: the compiled `dist/index.d.ts` must not reference `@mastra`. This means the package is safe to bundle in edge or non-Node runtimes that cannot load native Mastra dependencies.
194
+
195
+ The package exposes **only** `ask_<agent>` tools — never `run_<workflow>` — as specified in the engine-wall contract (CONTRACTS §1).
package/dist/index.cjs CHANGED
@@ -92,7 +92,7 @@ function createSwarmMcpServer(opts) {
92
92
  async function runSwarmMcpStdio(opts) {
93
93
  const server = createSwarmMcpServer(opts);
94
94
  await server.connect(new import_stdio.StdioServerTransport());
95
- console.error(`[nightowls] swarm MCP server ready on stdio (${opts.agents.length} ask_<agent> tools)`);
95
+ console.error(`[@nightowlsdev/mcp-server] swarm MCP server ready on stdio (${opts.agents.length} ask_<agent> tools)`);
96
96
  return server;
97
97
  }
98
98
  // Annotate the CommonJS export names for ESM import in node:
package/dist/index.js CHANGED
@@ -65,7 +65,7 @@ function createSwarmMcpServer(opts) {
65
65
  async function runSwarmMcpStdio(opts) {
66
66
  const server = createSwarmMcpServer(opts);
67
67
  await server.connect(new StdioServerTransport());
68
- console.error(`[nightowls] swarm MCP server ready on stdio (${opts.agents.length} ask_<agent> tools)`);
68
+ console.error(`[@nightowlsdev/mcp-server] swarm MCP server ready on stdio (${opts.agents.length} ask_<agent> tools)`);
69
69
  return server;
70
70
  }
71
71
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nightowlsdev/mcp-server",
3
- "version": "0.1.0",
3
+ "version": "1.0.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -31,7 +31,7 @@
31
31
  "zod": "^4.0.0"
32
32
  },
33
33
  "peerDependencies": {
34
- "@nightowlsdev/core": "0.3.0"
34
+ "@nightowlsdev/core": "0.4.0"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/node": "^24.12.4",
@@ -39,9 +39,9 @@
39
39
  "tsup": "8.5.1",
40
40
  "typescript": "6.0.3",
41
41
  "vitest": "^3.2.0",
42
- "@nightowlsdev/core": "0.3.0",
43
- "@nightowlsdev/eslint-config": "0.0.0",
44
- "@nightowlsdev/tsconfig": "0.0.0"
42
+ "@nightowlsdev/core": "0.4.0",
43
+ "@nightowlsdev/tsconfig": "0.0.0",
44
+ "@nightowlsdev/eslint-config": "0.0.0"
45
45
  },
46
46
  "scripts": {
47
47
  "build": "tsup",