@nightowlsdev/mcp-server 1.0.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/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);
@@ -95,8 +97,109 @@ async function runSwarmMcpStdio(opts) {
95
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
@@ -68,7 +68,108 @@ async function runSwarmMcpStdio(opts) {
68
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": "1.0.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.4.0"
34
+ "@nightowlsdev/core": "0.5.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.4.0",
43
- "@nightowlsdev/tsconfig": "0.0.0",
44
- "@nightowlsdev/eslint-config": "0.0.0"
42
+ "@nightowlsdev/eslint-config": "0.0.0",
43
+ "@nightowlsdev/core": "0.5.0",
44
+ "@nightowlsdev/tsconfig": "0.0.0"
45
45
  },
46
46
  "scripts": {
47
47
  "build": "tsup",