@nightowlsdev/mcp-server 0.1.0 → 2.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
@@ -20,7 +20,9 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ createMcpServer: () => createMcpServer,
23
24
  createSwarmMcpServer: () => createSwarmMcpServer,
25
+ defineMcpTool: () => defineMcpTool,
24
26
  runSwarmMcpStdio: () => runSwarmMcpStdio
25
27
  });
26
28
  module.exports = __toCommonJS(index_exports);
@@ -92,11 +94,112 @@ function createSwarmMcpServer(opts) {
92
94
  async function runSwarmMcpStdio(opts) {
93
95
  const server = createSwarmMcpServer(opts);
94
96
  await server.connect(new import_stdio.StdioServerTransport());
95
- console.error(`[nightowls] swarm MCP server ready on stdio (${opts.agents.length} ask_<agent> tools)`);
97
+ console.error(`[@nightowlsdev/mcp-server] swarm MCP server ready on stdio (${opts.agents.length} ask_<agent> tools)`);
96
98
  return server;
97
99
  }
100
+
101
+ // src/host-tools.ts
102
+ var import_mcp2 = require("@modelcontextprotocol/sdk/server/mcp.js");
103
+ var import_stdio2 = require("@modelcontextprotocol/sdk/server/stdio.js");
104
+ var import_zod2 = require("zod");
105
+ var PROTOCOL_VERSION = "2025-06-18";
106
+ function bearerOf(req) {
107
+ return req.headers.get("authorization")?.replace(/^Bearer\s+/i, "") || void 0;
108
+ }
109
+ var jsonRpc = (id, result) => ({ jsonrpc: "2.0", id, result });
110
+ var jsonRpcError = (id, code, message) => ({ jsonrpc: "2.0", id, error: { code, message } });
111
+ function createMcpServer(opts) {
112
+ const byName = new Map(opts.tools.map((t) => [t.name, t]));
113
+ const server = new import_mcp2.McpServer({ name: opts.name ?? "nightowls-host", version: opts.version ?? "0.1.0" });
114
+ for (const t of opts.tools) {
115
+ server.registerTool(
116
+ t.name,
117
+ { title: t.name, description: t.description ?? t.name, inputSchema: t.inputSchema.shape },
118
+ async (args) => {
119
+ const out = await t.handler(args, opts.localContext ?? {});
120
+ return { content: [{ type: "text", text: JSON.stringify(out) }] };
121
+ }
122
+ );
123
+ }
124
+ const authorize = async (req) => {
125
+ if (!opts.auth?.verifyBearer) return { ctx: {} };
126
+ const resolved = await opts.auth.verifyBearer(bearerOf(req), { req });
127
+ return resolved ? { ctx: resolved } : { unauthorized: true };
128
+ };
129
+ const handleRpc = async (req, msg) => {
130
+ const { id, method } = msg;
131
+ switch (method) {
132
+ case "initialize":
133
+ return jsonRpc(id, { protocolVersion: PROTOCOL_VERSION, capabilities: { tools: {} }, serverInfo: { name: opts.name ?? "nightowls-host", version: opts.version ?? "0.1.0" } });
134
+ case "notifications/initialized":
135
+ return null;
136
+ // a notification — no response
137
+ case "ping":
138
+ return jsonRpc(id, {});
139
+ case "tools/list": {
140
+ if ((await authorize(req)).unauthorized) return jsonRpcError(id, -32001, "unauthorized");
141
+ const tools = opts.tools.map((t) => ({ name: t.name, description: t.description ?? t.name, inputSchema: import_zod2.z.toJSONSchema(t.inputSchema) }));
142
+ return jsonRpc(id, { tools });
143
+ }
144
+ case "tools/call": {
145
+ const authed = await authorize(req);
146
+ if (authed.unauthorized) return jsonRpcError(id, -32001, "unauthorized");
147
+ const ctx = authed.ctx;
148
+ const name = msg.params?.name;
149
+ const tool = name ? byName.get(name) : void 0;
150
+ if (!tool) return jsonRpcError(id, -32602, `unknown tool: ${name ?? "(none)"}`);
151
+ const parsed = tool.inputSchema.safeParse(msg.params?.arguments ?? {});
152
+ if (!parsed.success) return jsonRpc(id, { content: [{ type: "text", text: JSON.stringify({ error: "invalid arguments", issues: parsed.error.issues }) }], isError: true });
153
+ try {
154
+ const out = await tool.handler(parsed.data, ctx);
155
+ return jsonRpc(id, { content: [{ type: "text", text: JSON.stringify(out) }] });
156
+ } catch (e) {
157
+ return jsonRpc(id, { content: [{ type: "text", text: JSON.stringify({ error: e instanceof Error ? e.message : String(e) }) }], isError: true });
158
+ }
159
+ }
160
+ default:
161
+ return jsonRpcError(id, -32601, `method not found: ${method ?? "(none)"}`);
162
+ }
163
+ };
164
+ const POST = async (req) => {
165
+ let body;
166
+ try {
167
+ body = await req.json();
168
+ } catch {
169
+ return Response.json(jsonRpcError(null, -32700, "parse error"), { status: 400 });
170
+ }
171
+ const reqs = Array.isArray(body) ? body : [body];
172
+ const out = [];
173
+ for (const m of reqs) {
174
+ const res = await handleRpc(req, m ?? {});
175
+ if (res !== null) out.push(res);
176
+ }
177
+ if (out.length === 0) return new Response(null, { status: 202 });
178
+ return Response.json(Array.isArray(body) ? out : out[0], { headers: { "cache-control": "no-store" } });
179
+ };
180
+ const GET = async () => new Response(JSON.stringify({ error: "GET not supported (stateless server \u2014 POST JSON-RPC)" }), { status: 405, headers: { "content-type": "application/json", allow: "POST" } });
181
+ const wellKnownGET = async () => opts.auth?.metadata ? Response.json(opts.auth.metadata, { headers: { "cache-control": "no-store" } }) : new Response(JSON.stringify({ error: "no discovery metadata configured" }), { status: 404, headers: { "content-type": "application/json" } });
182
+ return {
183
+ server,
184
+ httpRoute: () => {
185
+ if (!opts.auth?.verifyBearer) console.warn("[@nightowlsdev/mcp-server] createMcpServer.httpRoute() has no auth.verifyBearer \u2014 the HTTP endpoint is UNAUTHENTICATED (all tools open). Configure `auth` for any non-trusted deployment.");
186
+ return { GET, POST };
187
+ },
188
+ wellKnownRoute: () => ({ GET: wellKnownGET }),
189
+ stdio: async () => {
190
+ await server.connect(new import_stdio2.StdioServerTransport());
191
+ console.error(`[@nightowlsdev/mcp-server] host MCP server ready on stdio (${opts.tools.length} tools)`);
192
+ return server;
193
+ }
194
+ };
195
+ }
196
+ function defineMcpTool(def) {
197
+ return def;
198
+ }
98
199
  // Annotate the CommonJS export names for ESM import in node:
99
200
  0 && (module.exports = {
201
+ createMcpServer,
100
202
  createSwarmMcpServer,
203
+ defineMcpTool,
101
204
  runSwarmMcpStdio
102
205
  });
package/dist/index.d.cts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { SwarmEngine, StorageAdapter } from '@nightowlsdev/core';
3
+ import { z } from 'zod';
3
4
 
4
5
  /** The caller identity resolved from an MCP request — the ONLY source of tenancy (never tool args). */
5
6
  interface SwarmMcpContext {
@@ -43,4 +44,51 @@ declare function createSwarmMcpServer(opts: SwarmMcpServerOpts): McpServer;
43
44
  * channel). Resolves with the connected server (await its transport close to keep the process alive). */
44
45
  declare function runSwarmMcpStdio(opts: SwarmMcpServerOpts): Promise<McpServer>;
45
46
 
46
- export { type SwarmMcpAgent, type SwarmMcpContext, type SwarmMcpServerOpts, createSwarmMcpServer, runSwarmMcpStdio };
47
+ /** The per-call identity a host tool sees whatever `verifyBearer` returns (e.g. `{ userId }`). For stdio (local)
48
+ * it is `{}` unless a `localContext` is supplied. Tools take identity ONLY from here, never from their args. */
49
+ type McpToolContext = Record<string, unknown>;
50
+ /** One host tool: a zod object input + a handler that receives the validated args and the resolved identity ctx. */
51
+ interface McpToolDef<I extends z.ZodObject<z.ZodRawShape> = z.ZodObject<z.ZodRawShape>> {
52
+ name: string;
53
+ description?: string;
54
+ inputSchema: I;
55
+ handler: (args: z.infer<I>, ctx: McpToolContext) => Promise<unknown> | unknown;
56
+ }
57
+ /** Pluggable auth seam. `verifyBearer` validates the request's bearer token and returns the per-call identity ctx
58
+ * (or null to reject 401). Omit for an unauthenticated server (local/stdio, or a host gating elsewhere).
59
+ * `metadata` (optional) is served verbatim at `/.well-known/oauth-protected-resource` for discovery — a host that
60
+ * runs its own OAuth AS points clients at it here; this package does not implement the AS. */
61
+ interface McpAuth {
62
+ verifyBearer?: (token: string | undefined, extra: {
63
+ req: Request;
64
+ }) => Promise<McpToolContext | null> | McpToolContext | null;
65
+ metadata?: Record<string, unknown>;
66
+ }
67
+ interface CreateMcpServerOpts {
68
+ tools: McpToolDef[];
69
+ auth?: McpAuth;
70
+ name?: string;
71
+ version?: string;
72
+ /** Identity ctx for the stdio transport (no HTTP bearer there). Default `{}`. */
73
+ localContext?: McpToolContext;
74
+ }
75
+ type RouteHandler = (req: Request) => Promise<Response>;
76
+ /** The generic host-tool MCP server. `httpRoute()` → a stateless `{ GET, POST }` for a Next.js App Router route;
77
+ * `stdio()` serves the same tools over stdio; `wellKnownRoute()` serves the optional discovery metadata. */
78
+ interface HostMcpServer {
79
+ /** The underlying SDK server (host tools registered) — for stdio + advanced wiring. */
80
+ readonly server: McpServer;
81
+ httpRoute(): {
82
+ GET: RouteHandler;
83
+ POST: RouteHandler;
84
+ };
85
+ wellKnownRoute(): {
86
+ GET: RouteHandler;
87
+ };
88
+ stdio(): Promise<McpServer>;
89
+ }
90
+ declare function createMcpServer(opts: CreateMcpServerOpts): HostMcpServer;
91
+ /** Define a host tool with inferred arg typing — a small ergonomic wrapper over the `McpToolDef` shape. */
92
+ declare function defineMcpTool<I extends z.ZodObject<z.ZodRawShape>>(def: McpToolDef<I>): McpToolDef<I>;
93
+
94
+ export { type CreateMcpServerOpts, type HostMcpServer, type McpAuth, type McpToolContext, type McpToolDef, type SwarmMcpAgent, type SwarmMcpContext, type SwarmMcpServerOpts, createMcpServer, createSwarmMcpServer, defineMcpTool, runSwarmMcpStdio };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { SwarmEngine, StorageAdapter } from '@nightowlsdev/core';
3
+ import { z } from 'zod';
3
4
 
4
5
  /** The caller identity resolved from an MCP request — the ONLY source of tenancy (never tool args). */
5
6
  interface SwarmMcpContext {
@@ -43,4 +44,51 @@ declare function createSwarmMcpServer(opts: SwarmMcpServerOpts): McpServer;
43
44
  * channel). Resolves with the connected server (await its transport close to keep the process alive). */
44
45
  declare function runSwarmMcpStdio(opts: SwarmMcpServerOpts): Promise<McpServer>;
45
46
 
46
- export { type SwarmMcpAgent, type SwarmMcpContext, type SwarmMcpServerOpts, createSwarmMcpServer, runSwarmMcpStdio };
47
+ /** The per-call identity a host tool sees whatever `verifyBearer` returns (e.g. `{ userId }`). For stdio (local)
48
+ * it is `{}` unless a `localContext` is supplied. Tools take identity ONLY from here, never from their args. */
49
+ type McpToolContext = Record<string, unknown>;
50
+ /** One host tool: a zod object input + a handler that receives the validated args and the resolved identity ctx. */
51
+ interface McpToolDef<I extends z.ZodObject<z.ZodRawShape> = z.ZodObject<z.ZodRawShape>> {
52
+ name: string;
53
+ description?: string;
54
+ inputSchema: I;
55
+ handler: (args: z.infer<I>, ctx: McpToolContext) => Promise<unknown> | unknown;
56
+ }
57
+ /** Pluggable auth seam. `verifyBearer` validates the request's bearer token and returns the per-call identity ctx
58
+ * (or null to reject 401). Omit for an unauthenticated server (local/stdio, or a host gating elsewhere).
59
+ * `metadata` (optional) is served verbatim at `/.well-known/oauth-protected-resource` for discovery — a host that
60
+ * runs its own OAuth AS points clients at it here; this package does not implement the AS. */
61
+ interface McpAuth {
62
+ verifyBearer?: (token: string | undefined, extra: {
63
+ req: Request;
64
+ }) => Promise<McpToolContext | null> | McpToolContext | null;
65
+ metadata?: Record<string, unknown>;
66
+ }
67
+ interface CreateMcpServerOpts {
68
+ tools: McpToolDef[];
69
+ auth?: McpAuth;
70
+ name?: string;
71
+ version?: string;
72
+ /** Identity ctx for the stdio transport (no HTTP bearer there). Default `{}`. */
73
+ localContext?: McpToolContext;
74
+ }
75
+ type RouteHandler = (req: Request) => Promise<Response>;
76
+ /** The generic host-tool MCP server. `httpRoute()` → a stateless `{ GET, POST }` for a Next.js App Router route;
77
+ * `stdio()` serves the same tools over stdio; `wellKnownRoute()` serves the optional discovery metadata. */
78
+ interface HostMcpServer {
79
+ /** The underlying SDK server (host tools registered) — for stdio + advanced wiring. */
80
+ readonly server: McpServer;
81
+ httpRoute(): {
82
+ GET: RouteHandler;
83
+ POST: RouteHandler;
84
+ };
85
+ wellKnownRoute(): {
86
+ GET: RouteHandler;
87
+ };
88
+ stdio(): Promise<McpServer>;
89
+ }
90
+ declare function createMcpServer(opts: CreateMcpServerOpts): HostMcpServer;
91
+ /** Define a host tool with inferred arg typing — a small ergonomic wrapper over the `McpToolDef` shape. */
92
+ declare function defineMcpTool<I extends z.ZodObject<z.ZodRawShape>>(def: McpToolDef<I>): McpToolDef<I>;
93
+
94
+ export { type CreateMcpServerOpts, type HostMcpServer, type McpAuth, type McpToolContext, type McpToolDef, type SwarmMcpAgent, type SwarmMcpContext, type SwarmMcpServerOpts, createMcpServer, createSwarmMcpServer, defineMcpTool, runSwarmMcpStdio };
package/dist/index.js CHANGED
@@ -65,10 +65,111 @@ 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
+
72
+ // src/host-tools.ts
73
+ import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js";
74
+ import { StdioServerTransport as StdioServerTransport2 } from "@modelcontextprotocol/sdk/server/stdio.js";
75
+ import { z as z2 } from "zod";
76
+ var PROTOCOL_VERSION = "2025-06-18";
77
+ function bearerOf(req) {
78
+ return req.headers.get("authorization")?.replace(/^Bearer\s+/i, "") || void 0;
79
+ }
80
+ var jsonRpc = (id, result) => ({ jsonrpc: "2.0", id, result });
81
+ var jsonRpcError = (id, code, message) => ({ jsonrpc: "2.0", id, error: { code, message } });
82
+ function createMcpServer(opts) {
83
+ const byName = new Map(opts.tools.map((t) => [t.name, t]));
84
+ const server = new McpServer2({ name: opts.name ?? "nightowls-host", version: opts.version ?? "0.1.0" });
85
+ for (const t of opts.tools) {
86
+ server.registerTool(
87
+ t.name,
88
+ { title: t.name, description: t.description ?? t.name, inputSchema: t.inputSchema.shape },
89
+ async (args) => {
90
+ const out = await t.handler(args, opts.localContext ?? {});
91
+ return { content: [{ type: "text", text: JSON.stringify(out) }] };
92
+ }
93
+ );
94
+ }
95
+ const authorize = async (req) => {
96
+ if (!opts.auth?.verifyBearer) return { ctx: {} };
97
+ const resolved = await opts.auth.verifyBearer(bearerOf(req), { req });
98
+ return resolved ? { ctx: resolved } : { unauthorized: true };
99
+ };
100
+ const handleRpc = async (req, msg) => {
101
+ const { id, method } = msg;
102
+ switch (method) {
103
+ case "initialize":
104
+ return jsonRpc(id, { protocolVersion: PROTOCOL_VERSION, capabilities: { tools: {} }, serverInfo: { name: opts.name ?? "nightowls-host", version: opts.version ?? "0.1.0" } });
105
+ case "notifications/initialized":
106
+ return null;
107
+ // a notification — no response
108
+ case "ping":
109
+ return jsonRpc(id, {});
110
+ case "tools/list": {
111
+ if ((await authorize(req)).unauthorized) return jsonRpcError(id, -32001, "unauthorized");
112
+ const tools = opts.tools.map((t) => ({ name: t.name, description: t.description ?? t.name, inputSchema: z2.toJSONSchema(t.inputSchema) }));
113
+ return jsonRpc(id, { tools });
114
+ }
115
+ case "tools/call": {
116
+ const authed = await authorize(req);
117
+ if (authed.unauthorized) return jsonRpcError(id, -32001, "unauthorized");
118
+ const ctx = authed.ctx;
119
+ const name = msg.params?.name;
120
+ const tool = name ? byName.get(name) : void 0;
121
+ if (!tool) return jsonRpcError(id, -32602, `unknown tool: ${name ?? "(none)"}`);
122
+ const parsed = tool.inputSchema.safeParse(msg.params?.arguments ?? {});
123
+ if (!parsed.success) return jsonRpc(id, { content: [{ type: "text", text: JSON.stringify({ error: "invalid arguments", issues: parsed.error.issues }) }], isError: true });
124
+ try {
125
+ const out = await tool.handler(parsed.data, ctx);
126
+ return jsonRpc(id, { content: [{ type: "text", text: JSON.stringify(out) }] });
127
+ } catch (e) {
128
+ return jsonRpc(id, { content: [{ type: "text", text: JSON.stringify({ error: e instanceof Error ? e.message : String(e) }) }], isError: true });
129
+ }
130
+ }
131
+ default:
132
+ return jsonRpcError(id, -32601, `method not found: ${method ?? "(none)"}`);
133
+ }
134
+ };
135
+ const POST = async (req) => {
136
+ let body;
137
+ try {
138
+ body = await req.json();
139
+ } catch {
140
+ return Response.json(jsonRpcError(null, -32700, "parse error"), { status: 400 });
141
+ }
142
+ const reqs = Array.isArray(body) ? body : [body];
143
+ const out = [];
144
+ for (const m of reqs) {
145
+ const res = await handleRpc(req, m ?? {});
146
+ if (res !== null) out.push(res);
147
+ }
148
+ if (out.length === 0) return new Response(null, { status: 202 });
149
+ return Response.json(Array.isArray(body) ? out : out[0], { headers: { "cache-control": "no-store" } });
150
+ };
151
+ const GET = async () => new Response(JSON.stringify({ error: "GET not supported (stateless server \u2014 POST JSON-RPC)" }), { status: 405, headers: { "content-type": "application/json", allow: "POST" } });
152
+ const wellKnownGET = async () => opts.auth?.metadata ? Response.json(opts.auth.metadata, { headers: { "cache-control": "no-store" } }) : new Response(JSON.stringify({ error: "no discovery metadata configured" }), { status: 404, headers: { "content-type": "application/json" } });
153
+ return {
154
+ server,
155
+ httpRoute: () => {
156
+ if (!opts.auth?.verifyBearer) console.warn("[@nightowlsdev/mcp-server] createMcpServer.httpRoute() has no auth.verifyBearer \u2014 the HTTP endpoint is UNAUTHENTICATED (all tools open). Configure `auth` for any non-trusted deployment.");
157
+ return { GET, POST };
158
+ },
159
+ wellKnownRoute: () => ({ GET: wellKnownGET }),
160
+ stdio: async () => {
161
+ await server.connect(new StdioServerTransport2());
162
+ console.error(`[@nightowlsdev/mcp-server] host MCP server ready on stdio (${opts.tools.length} tools)`);
163
+ return server;
164
+ }
165
+ };
166
+ }
167
+ function defineMcpTool(def) {
168
+ return def;
169
+ }
71
170
  export {
171
+ createMcpServer,
72
172
  createSwarmMcpServer,
173
+ defineMcpTool,
73
174
  runSwarmMcpStdio
74
175
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nightowlsdev/mcp-server",
3
- "version": "0.1.0",
3
+ "version": "2.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.5.0"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/node": "^24.12.4",
@@ -39,8 +39,8 @@
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
42
  "@nightowlsdev/eslint-config": "0.0.0",
43
+ "@nightowlsdev/core": "0.5.0",
44
44
  "@nightowlsdev/tsconfig": "0.0.0"
45
45
  },
46
46
  "scripts": {