@lumenflow/mcp 2.11.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.
@@ -0,0 +1,128 @@
1
+ /**
2
+ * @file resources.ts
3
+ * @description MCP resource implementations for LumenFlow
4
+ *
5
+ * WU-1412: Resources available: context, wu/{id}, backlog
6
+ *
7
+ * Resources provide read access to LumenFlow data via URI patterns.
8
+ */
9
+ import * as fs from 'node:fs/promises';
10
+ import * as path from 'node:path';
11
+ import { runCliCommand } from './cli-runner.js';
12
+ // Lazy load core module for context
13
+ let coreModule = null;
14
+ async function getCore() {
15
+ if (!coreModule) {
16
+ coreModule = await import('@lumenflow/core');
17
+ }
18
+ return coreModule;
19
+ }
20
+ /**
21
+ * context - Current LumenFlow context
22
+ */
23
+ export const contextResource = {
24
+ uri: 'lumenflow://context',
25
+ name: 'LumenFlow Context',
26
+ description: 'Current LumenFlow context including location, git state, and active WU',
27
+ mimeType: 'application/json',
28
+ async fetch(_uri, options) {
29
+ try {
30
+ const core = await getCore();
31
+ const context = await core.computeWuContext({
32
+ cwd: options?.projectRoot,
33
+ });
34
+ return {
35
+ success: true,
36
+ content: JSON.stringify(context, null, 2),
37
+ };
38
+ }
39
+ catch (err) {
40
+ return {
41
+ success: false,
42
+ error: err instanceof Error ? err.message : String(err),
43
+ };
44
+ }
45
+ },
46
+ };
47
+ /**
48
+ * wu/{id} - WU specification by ID
49
+ */
50
+ export const wuResource = {
51
+ uriTemplate: 'lumenflow://wu/{id}',
52
+ name: 'Work Unit',
53
+ description: 'Work Unit specification by ID',
54
+ mimeType: 'application/json',
55
+ async fetch(uri, options) {
56
+ try {
57
+ // Extract ID from URI: lumenflow://wu/WU-1412 -> WU-1412
58
+ const match = /^lumenflow:\/\/wu\/(.+)$/.exec(uri);
59
+ if (!match) {
60
+ return { success: false, error: 'Invalid WU URI format' };
61
+ }
62
+ const id = match[1];
63
+ // Use CLI to get WU status (includes full WU context)
64
+ const result = await runCliCommand('wu:status', ['--id', id, '--json'], {
65
+ projectRoot: options?.projectRoot,
66
+ });
67
+ if (result.success) {
68
+ return {
69
+ success: true,
70
+ content: result.stdout,
71
+ };
72
+ }
73
+ else {
74
+ return {
75
+ success: false,
76
+ error: result.stderr || result.error?.message || 'Failed to get WU',
77
+ };
78
+ }
79
+ }
80
+ catch (err) {
81
+ return {
82
+ success: false,
83
+ error: err instanceof Error ? err.message : String(err),
84
+ };
85
+ }
86
+ },
87
+ };
88
+ /**
89
+ * backlog - Current backlog state
90
+ * Reads the backlog.md file directly (generated by LumenFlow CLI)
91
+ */
92
+ export const backlogResource = {
93
+ uri: 'lumenflow://backlog',
94
+ name: 'Backlog',
95
+ description: 'Current LumenFlow backlog with all WUs grouped by status',
96
+ mimeType: 'text/markdown',
97
+ async fetch(_uri, options) {
98
+ try {
99
+ const projectRoot = options?.projectRoot || process.cwd();
100
+ // Security: path is constructed from known static segments, not user input
101
+ const backlogPath = path.join(projectRoot, 'docs', '04-operations', 'tasks', 'backlog.md');
102
+ // eslint-disable-next-line security/detect-non-literal-fs-filename
103
+ const content = await fs.readFile(backlogPath, 'utf-8');
104
+ return {
105
+ success: true,
106
+ content,
107
+ };
108
+ }
109
+ catch (err) {
110
+ return {
111
+ success: false,
112
+ error: err instanceof Error ? err.message : String(err),
113
+ };
114
+ }
115
+ },
116
+ };
117
+ /**
118
+ * All static resources (fixed URIs)
119
+ */
120
+ export const staticResources = [contextResource, backlogResource];
121
+ /**
122
+ * All resource templates (parameterized URIs)
123
+ */
124
+ export const resourceTemplates = [wuResource];
125
+ /**
126
+ * All resources
127
+ */
128
+ export const allResources = [...staticResources, ...resourceTemplates];
@@ -0,0 +1,68 @@
1
+ /**
2
+ * @file server.ts
3
+ * @description MCP server factory and configuration
4
+ *
5
+ * WU-1412: MCP server runs via npx @lumenflow/mcp over stdio
6
+ *
7
+ * Creates an MCP server that exposes LumenFlow tools and resources.
8
+ * Supports configuration via environment variables:
9
+ * - LUMENFLOW_PROJECT_ROOT: Project root directory
10
+ * - LUMENFLOW_MCP_LOG_LEVEL: Log level (debug, info, warn, error)
11
+ */
12
+ /**
13
+ * Log levels supported by the MCP server
14
+ */
15
+ export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
16
+ /**
17
+ * MCP server configuration
18
+ */
19
+ export interface McpServerConfig {
20
+ /** Project root directory (default: process.cwd()) */
21
+ projectRoot?: string;
22
+ /** Log level (default: 'info') */
23
+ logLevel?: LogLevel;
24
+ }
25
+ /**
26
+ * MCP server instance with LumenFlow tools and resources
27
+ */
28
+ export interface McpServer {
29
+ /** Server name */
30
+ name: string;
31
+ /** Server configuration */
32
+ config: Required<McpServerConfig>;
33
+ /** List available tools */
34
+ listTools: () => Array<{
35
+ name: string;
36
+ description: string;
37
+ }>;
38
+ /** List available static resources */
39
+ listResources: () => Array<{
40
+ uri: string;
41
+ name: string;
42
+ description: string;
43
+ }>;
44
+ /** List available resource templates */
45
+ listResourceTemplates: () => Array<{
46
+ uriTemplate: string;
47
+ name: string;
48
+ description: string;
49
+ }>;
50
+ /** Start the server (connects stdio transport) */
51
+ start: () => Promise<void>;
52
+ /** Stop the server */
53
+ stop: () => Promise<void>;
54
+ }
55
+ /**
56
+ * Create an MCP server with LumenFlow tools and resources
57
+ *
58
+ * @param config - Server configuration
59
+ * @returns MCP server instance
60
+ *
61
+ * @example
62
+ * const server = createMcpServer({
63
+ * projectRoot: process.env.LUMENFLOW_PROJECT_ROOT,
64
+ * logLevel: process.env.LUMENFLOW_MCP_LOG_LEVEL as LogLevel,
65
+ * });
66
+ * await server.start();
67
+ */
68
+ export declare function createMcpServer(config?: McpServerConfig): McpServer;
package/dist/server.js ADDED
@@ -0,0 +1,190 @@
1
+ /**
2
+ * @file server.ts
3
+ * @description MCP server factory and configuration
4
+ *
5
+ * WU-1412: MCP server runs via npx @lumenflow/mcp over stdio
6
+ *
7
+ * Creates an MCP server that exposes LumenFlow tools and resources.
8
+ * Supports configuration via environment variables:
9
+ * - LUMENFLOW_PROJECT_ROOT: Project root directory
10
+ * - LUMENFLOW_MCP_LOG_LEVEL: Log level (debug, info, warn, error)
11
+ */
12
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
13
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
14
+ import { ListToolsRequestSchema, CallToolRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, ListResourceTemplatesRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
15
+ import { z } from 'zod';
16
+ import { allTools } from './tools.js';
17
+ import { staticResources, resourceTemplates } from './resources.js';
18
+ /**
19
+ * Convert a Zod schema to JSON Schema format for MCP
20
+ */
21
+ function zodToJsonSchema(schema) {
22
+ return z.toJSONSchema(schema);
23
+ }
24
+ /**
25
+ * Create an MCP server with LumenFlow tools and resources
26
+ *
27
+ * @param config - Server configuration
28
+ * @returns MCP server instance
29
+ *
30
+ * @example
31
+ * const server = createMcpServer({
32
+ * projectRoot: process.env.LUMENFLOW_PROJECT_ROOT,
33
+ * logLevel: process.env.LUMENFLOW_MCP_LOG_LEVEL as LogLevel,
34
+ * });
35
+ * await server.start();
36
+ */
37
+ export function createMcpServer(config = {}) {
38
+ const resolvedConfig = {
39
+ projectRoot: config.projectRoot || process.env.LUMENFLOW_PROJECT_ROOT || process.cwd(),
40
+ logLevel: config.logLevel || process.env.LUMENFLOW_MCP_LOG_LEVEL || 'info',
41
+ };
42
+ // Create the MCP SDK server
43
+ const server = new Server({
44
+ name: '@lumenflow/mcp',
45
+ version: '2.10.0',
46
+ }, {
47
+ capabilities: {
48
+ tools: {},
49
+ resources: {},
50
+ },
51
+ });
52
+ // Register tool handlers
53
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
54
+ return {
55
+ tools: allTools.map((tool) => ({
56
+ name: tool.name,
57
+ description: tool.description,
58
+ inputSchema: zodToJsonSchema(tool.inputSchema),
59
+ })),
60
+ };
61
+ });
62
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
63
+ const { name, arguments: args } = request.params;
64
+ const tool = allTools.find((t) => t.name === name);
65
+ if (!tool) {
66
+ return {
67
+ content: [{ type: 'text', text: JSON.stringify({ error: `Unknown tool: ${name}` }) }],
68
+ isError: true,
69
+ };
70
+ }
71
+ try {
72
+ const result = await tool.execute(args || {}, { projectRoot: resolvedConfig.projectRoot });
73
+ return {
74
+ content: [{ type: 'text', text: JSON.stringify(result) }],
75
+ isError: !result.success,
76
+ };
77
+ }
78
+ catch (err) {
79
+ return {
80
+ content: [
81
+ {
82
+ type: 'text',
83
+ text: JSON.stringify({ error: err instanceof Error ? err.message : String(err) }),
84
+ },
85
+ ],
86
+ isError: true,
87
+ };
88
+ }
89
+ });
90
+ // Register resource handlers
91
+ server.setRequestHandler(ListResourcesRequestSchema, async () => {
92
+ return {
93
+ resources: staticResources
94
+ .filter((r) => r.uri !== undefined)
95
+ .map((r) => ({
96
+ uri: r.uri,
97
+ name: r.name,
98
+ description: r.description,
99
+ mimeType: r.mimeType,
100
+ })),
101
+ };
102
+ });
103
+ server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
104
+ return {
105
+ resourceTemplates: resourceTemplates
106
+ .filter((r) => r.uriTemplate !== undefined)
107
+ .map((r) => ({
108
+ uriTemplate: r.uriTemplate,
109
+ name: r.name,
110
+ description: r.description,
111
+ mimeType: r.mimeType,
112
+ })),
113
+ };
114
+ });
115
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
116
+ const { uri } = request.params;
117
+ // Find matching resource
118
+ let resource;
119
+ // Check static resources first
120
+ resource = staticResources.find((r) => r.uri === uri);
121
+ // Check resource templates
122
+ if (!resource) {
123
+ for (const template of resourceTemplates) {
124
+ if (template.uriTemplate) {
125
+ // Simple template matching for lumenflow://wu/{id} pattern
126
+ // Security: pattern is derived from our own static uriTemplate, not user input
127
+ const pattern = template.uriTemplate.replace(/\{[^}]+\}/g, '([^/]+)');
128
+ // eslint-disable-next-line security/detect-non-literal-regexp
129
+ const regex = new RegExp(`^${pattern}$`);
130
+ if (regex.test(uri)) {
131
+ resource = template;
132
+ break;
133
+ }
134
+ }
135
+ }
136
+ }
137
+ if (!resource) {
138
+ return {
139
+ contents: [{ uri, text: `Resource not found: ${uri}`, mimeType: 'text/plain' }],
140
+ };
141
+ }
142
+ const result = await resource.fetch(uri, { projectRoot: resolvedConfig.projectRoot });
143
+ return {
144
+ contents: [
145
+ {
146
+ uri,
147
+ text: result.success ? (result.content ?? '') : `Error: ${result.error}`,
148
+ mimeType: resource.mimeType,
149
+ },
150
+ ],
151
+ };
152
+ });
153
+ // Build the McpServer wrapper
154
+ let transport = null;
155
+ return {
156
+ name: '@lumenflow/mcp',
157
+ config: resolvedConfig,
158
+ listTools() {
159
+ return allTools.map((t) => ({ name: t.name, description: t.description }));
160
+ },
161
+ listResources() {
162
+ return staticResources
163
+ .filter((r) => r.uri !== undefined)
164
+ .map((r) => ({
165
+ uri: r.uri,
166
+ name: r.name,
167
+ description: r.description,
168
+ }));
169
+ },
170
+ listResourceTemplates() {
171
+ return resourceTemplates
172
+ .filter((r) => r.uriTemplate !== undefined)
173
+ .map((r) => ({
174
+ uriTemplate: r.uriTemplate,
175
+ name: r.name,
176
+ description: r.description,
177
+ }));
178
+ },
179
+ async start() {
180
+ transport = new StdioServerTransport();
181
+ await server.connect(transport);
182
+ },
183
+ async stop() {
184
+ if (transport) {
185
+ await server.close();
186
+ transport = null;
187
+ }
188
+ },
189
+ };
190
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * @file tools.ts
3
+ * @description MCP tool implementations for LumenFlow operations
4
+ *
5
+ * WU-1412: Tools available: context_get, wu_list, wu_status, wu_create, wu_claim, wu_done, gates_run
6
+ *
7
+ * Architecture:
8
+ * - Read operations (context_get) use @lumenflow/core directly for context
9
+ * - All other operations shell out to CLI for consistency and safety
10
+ */
11
+ import { z } from 'zod';
12
+ /**
13
+ * Tool result structure matching MCP SDK expectations
14
+ */
15
+ export interface ToolResult {
16
+ success: boolean;
17
+ data?: unknown;
18
+ error?: {
19
+ message: string;
20
+ code?: string;
21
+ };
22
+ }
23
+ /**
24
+ * Base tool definition
25
+ */
26
+ export interface ToolDefinition {
27
+ name: string;
28
+ description: string;
29
+ inputSchema: z.ZodType;
30
+ execute: (input: Record<string, unknown>, options?: {
31
+ projectRoot?: string;
32
+ }) => Promise<ToolResult>;
33
+ }
34
+ /**
35
+ * context_get - Get current WU context (location, git state, WU state)
36
+ */
37
+ export declare const contextGetTool: ToolDefinition;
38
+ /**
39
+ * wu_list - List all WUs with optional status filter
40
+ * Uses CLI shell-out for consistency with other tools
41
+ */
42
+ export declare const wuListTool: ToolDefinition;
43
+ /**
44
+ * wu_status - Get status of a specific WU
45
+ * Uses CLI shell-out for consistency
46
+ */
47
+ export declare const wuStatusTool: ToolDefinition;
48
+ /**
49
+ * wu_create - Create a new WU
50
+ */
51
+ export declare const wuCreateTool: ToolDefinition;
52
+ /**
53
+ * wu_claim - Claim a WU and create worktree
54
+ */
55
+ export declare const wuClaimTool: ToolDefinition;
56
+ /**
57
+ * wu_done - Complete a WU (must be run from main checkout)
58
+ */
59
+ export declare const wuDoneTool: ToolDefinition;
60
+ /**
61
+ * gates_run - Run quality gates
62
+ */
63
+ export declare const gatesRunTool: ToolDefinition;
64
+ /**
65
+ * All available tools
66
+ */
67
+ export declare const allTools: ToolDefinition[];