@alt-t4b/pm-mcp 0.1.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 ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@alt-t4b/pm-mcp",
3
+ "version": "0.1.0",
4
+ "exports": {
5
+ ".": "./src/index.ts"
6
+ },
7
+ "bin": {
8
+ "tab-pm-mcp": "./src/standalone.ts"
9
+ },
10
+ "files": ["src"],
11
+ "scripts": {
12
+ "dev": "bun run --hot src/standalone.ts",
13
+ "start": "bun run src/standalone.ts"
14
+ },
15
+ "dependencies": {
16
+ "@alt-t4b/pm-domain": "^0.1.0",
17
+ "@modelcontextprotocol/sdk": "^1.27.1",
18
+ "hono": "^4",
19
+ "zod": "^4.3.6"
20
+ },
21
+ "devDependencies": {
22
+ "@types/bun": "^1.2",
23
+ "typescript": "^5"
24
+ }
25
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { createMcpServer, handleMcpHttp } from "./server";
2
+ export type { McpServiceContext } from "./server";
package/src/server.ts ADDED
@@ -0,0 +1,140 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
3
+ import { z } from "zod";
4
+ import {
5
+ ServiceError,
6
+ PROJECT_STATUSES,
7
+ TASK_STATUSES,
8
+ type IProjectService,
9
+ type ITaskService,
10
+ } from "@alt-t4b/pm-domain";
11
+
12
+ export interface McpServiceContext {
13
+ projectService: IProjectService;
14
+ taskService: ITaskService;
15
+ }
16
+
17
+ function handle<T>(fn: () => T) {
18
+ try {
19
+ const result = fn();
20
+ return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
21
+ } catch (err) {
22
+ if (err instanceof ServiceError) {
23
+ return {
24
+ content: [{ type: "text" as const, text: err.message }],
25
+ isError: true,
26
+ };
27
+ }
28
+ throw err;
29
+ }
30
+ }
31
+
32
+ /** Create an McpServer with all tools registered. */
33
+ export function createMcpServer(ctx: McpServiceContext): McpServer {
34
+ const { projectService, taskService } = ctx;
35
+
36
+ const server = new McpServer({
37
+ name: "tab-pm",
38
+ version: "0.1.0",
39
+ });
40
+
41
+ // ── Projects ──────────────────────────────────────────────
42
+
43
+ server.registerTool("list_projects", { description: "List all projects" }, () =>
44
+ handle(() => projectService.findAll())
45
+ );
46
+
47
+ server.registerTool(
48
+ "get_project",
49
+ { description: "Get a project by slug", inputSchema: { slug: z.string() } },
50
+ ({ slug }) => handle(() => projectService.findBySlug(slug))
51
+ );
52
+
53
+ server.registerTool(
54
+ "create_project",
55
+ {
56
+ description: "Create a new project",
57
+ inputSchema: {
58
+ name: z.string(),
59
+ slug: z.string(),
60
+ description: z.string().optional(),
61
+ status: z.enum(PROJECT_STATUSES).optional(),
62
+ },
63
+ },
64
+ (input) => handle(() => projectService.create(input))
65
+ );
66
+
67
+ server.registerTool(
68
+ "update_project",
69
+ {
70
+ description: "Update an existing project",
71
+ inputSchema: {
72
+ slug: z.string(),
73
+ name: z.string().optional(),
74
+ description: z.string().optional(),
75
+ status: z.enum(PROJECT_STATUSES).optional(),
76
+ },
77
+ },
78
+ ({ slug, ...updates }) => handle(() => projectService.update(slug, updates))
79
+ );
80
+
81
+ server.registerTool(
82
+ "delete_project",
83
+ { description: "Delete a project by slug", inputSchema: { slug: z.string() } },
84
+ ({ slug }) => handle(() => projectService.delete(slug))
85
+ );
86
+
87
+ // ── Tasks ─────────────────────────────────────────────────
88
+
89
+ server.registerTool(
90
+ "list_tasks",
91
+ { description: "List tasks for a project", inputSchema: { project_slug: z.string() } },
92
+ ({ project_slug }) => handle(() => taskService.findByProjectSlug(project_slug))
93
+ );
94
+
95
+ server.registerTool(
96
+ "create_task",
97
+ {
98
+ description: "Create a task in a project",
99
+ inputSchema: {
100
+ project_slug: z.string(),
101
+ title: z.string(),
102
+ description: z.string().optional(),
103
+ status: z.enum(TASK_STATUSES).optional(),
104
+ },
105
+ },
106
+ ({ project_slug, ...input }) => handle(() => taskService.create(project_slug, input))
107
+ );
108
+
109
+ server.registerTool(
110
+ "update_task",
111
+ {
112
+ description: "Update a task by ID",
113
+ inputSchema: {
114
+ id: z.string(),
115
+ title: z.string().optional(),
116
+ description: z.string().optional(),
117
+ status: z.enum(TASK_STATUSES).optional(),
118
+ },
119
+ },
120
+ ({ id, ...updates }) => handle(() => taskService.update(id, updates))
121
+ );
122
+
123
+ server.registerTool(
124
+ "delete_task",
125
+ { description: "Delete a task by ID", inputSchema: { id: z.string() } },
126
+ ({ id }) => handle(() => taskService.delete(id))
127
+ );
128
+
129
+ return server;
130
+ }
131
+
132
+ /** Handle a single MCP-over-HTTP request (stateless, one server per request). */
133
+ export async function handleMcpHttp(ctx: McpServiceContext, req: Request): Promise<Response> {
134
+ const server = createMcpServer(ctx);
135
+ const transport = new WebStandardStreamableHTTPServerTransport();
136
+ await server.connect(transport);
137
+ const response = await transport.handleRequest(req);
138
+ await server.close();
139
+ return response;
140
+ }
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env bun
2
+ import { Hono } from "hono";
3
+ import { cors } from "hono/cors";
4
+ import { bootstrap } from "@alt-t4b/pm-domain";
5
+ import { handleMcpHttp } from "./server";
6
+ import { networkInterfaces } from "os";
7
+
8
+ function parseArgs(): {
9
+ port: number;
10
+ host: string;
11
+ dbPath?: string;
12
+ } {
13
+ const args = process.argv.slice(2);
14
+ let port = Number(process.env.PM_MCP_PORT) || 3001;
15
+ let host = process.env.PM_HOST ?? "127.0.0.1";
16
+ let dbPath: string | undefined = process.env.SQLITE_PATH;
17
+
18
+ for (let i = 0; i < args.length; i++) {
19
+ if (args[i] === "--port" && args[i + 1]) port = Number(args[++i]);
20
+ if (args[i] === "--host" && args[i + 1]) host = args[++i];
21
+ if (args[i] === "--sqlite-path" && args[i + 1]) dbPath = args[++i];
22
+ }
23
+
24
+ return { port, host, dbPath };
25
+ }
26
+
27
+ const { port, host, dbPath } = parseArgs();
28
+ const ctx = bootstrap(dbPath);
29
+
30
+ const app = new Hono();
31
+ app.use(
32
+ "*",
33
+ cors({
34
+ origin: (origin) =>
35
+ origin?.startsWith("http://localhost") || origin?.startsWith("http://127.0.0.1")
36
+ ? origin
37
+ : null,
38
+ allowMethods: ["GET", "POST", "DELETE", "OPTIONS"],
39
+ allowHeaders: ["Content-Type", "mcp-session-id", "Last-Event-ID", "mcp-protocol-version"],
40
+ exposeHeaders: ["mcp-session-id", "mcp-protocol-version"],
41
+ })
42
+ );
43
+ app.all("/*", (c) => handleMcpHttp(ctx, c.req.raw));
44
+
45
+ function getNetworkAddress(): string | undefined {
46
+ for (const addrs of Object.values(networkInterfaces())) {
47
+ for (const addr of addrs ?? []) {
48
+ if (addr.family === "IPv4" && !addr.internal) return addr.address;
49
+ }
50
+ }
51
+ }
52
+
53
+ const displayHost = host === "0.0.0.0" ? "localhost" : host;
54
+ console.error(`tab-pm mcp listening on http://${displayHost}:${port}`);
55
+ if (host === "0.0.0.0") {
56
+ const networkAddr = getNetworkAddress();
57
+ if (networkAddr) {
58
+ console.error(`tab-pm mcp network: http://${networkAddr}:${port}`);
59
+ }
60
+ }
61
+
62
+ export default {
63
+ port,
64
+ hostname: host,
65
+ fetch: app.fetch,
66
+ };