mcpgraph 0.1.6 → 0.1.7

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/docs/design.md ADDED
@@ -0,0 +1,220 @@
1
+ # mcpGraph Design Document
2
+
3
+ ## Overview
4
+
5
+ This document memorializes the high-level design and tooling recommendations for mcpGraph, a product similar to n8n that surfaces an MCP interface and internally implements a directed graph of MCP server calls. The system enables filtering and reformatting data between nodes, makes routing decisions based on node output, and maintains a declarative, observable configuration without embedding a full programming language.
6
+
7
+ ## Core Architecture: The Orchestrator
8
+
9
+ ### Recommended Platform: TypeScript (Node.js)
10
+
11
+ **Rationale:**
12
+ - The MCP SDK is most mature in TypeScript
13
+ - Frontend tools for visual graph editing (like React Flow) are industry standard
14
+ - Strong ecosystem for both backend orchestration and frontend visualization
15
+
16
+ ### Graph Execution Engine
17
+
18
+ The system implements a custom graph execution engine that orchestrates the directed graph of MCP server calls.
19
+
20
+ **Design Approach:**
21
+ - **Custom Execution Loop**: A lightweight, sequential execution loop that provides full control over execution flow
22
+ - **Simple Architecture**: Direct mapping from YAML node definitions to execution, avoiding abstraction layers
23
+ - **Data Flow**: Execution context maintains state and data flow between nodes
24
+ - **Control Flow**: Supports conditional routing via switch nodes; cycles are supported for future loop constructs
25
+ - **Observability**: Built-in execution history tracking for debugging and introspection
26
+
27
+ **Implementation:**
28
+ The YAML configuration is parsed into a graph structure (`Graph` class) and executed by a custom `GraphExecutor` that:
29
+ - Starts at the tool's entry node
30
+ - Executes nodes sequentially based on the graph structure
31
+ - Tracks execution state and history
32
+ - Routes conditionally via switch nodes
33
+ - Continues until the exit node is reached
34
+
35
+ ### MCP Integration
36
+
37
+ - Use `@modelcontextprotocol/sdk`
38
+ - The system exposes an MCP server interface
39
+ - The YAML configuration defines the MCP server metadata and the tools it exposes
40
+ - Each tool definition includes standard MCP tool metadata (name, description, input/output parameters)
41
+ - Each tool's graph has an explicit entry node that receives tool arguments and initializes execution
42
+ - Each tool's graph has an explicit exit node that returns the final result to the MCP tool caller
43
+ - Execution flows through the graph from entry node to exit node
44
+
45
+ ## Declarative Logic & Data Transformation
46
+
47
+ To avoid embedding a full programming language while maintaining declarative, observable configurations, the system uses standardized expression engines.
48
+
49
+ ### Data Reformatting: JSONata
50
+
51
+ **Why JSONata:**
52
+ - Declarative query and transformation language for JSON
53
+ - Single string expression enables clear observability
54
+ - Can log input, expression, and output to debug transformations
55
+
56
+ **Resources:** [JSONata Documentation](https://jsonata.org/)
57
+
58
+ **YAML Example:**
59
+ ```yaml
60
+ transform:
61
+ expr: "$merge([payload, {'timestamp': $now()}])"
62
+ ```
63
+
64
+ ### Routing Decisions: JSON Logic
65
+
66
+ **Why JSON Logic:**
67
+ - Allows complex rules (e.g., "if price > 100 and status == 'active'") as pure JSON objects
68
+ - Declarative and observable
69
+ - No embedded code execution
70
+
71
+ **Resources:** [JSON Logic Documentation](https://jsonlogic.com/)
72
+
73
+ **YAML Example:**
74
+ ```yaml
75
+ condition:
76
+ and:
77
+ - ">": [{ var: "price" }, 100]
78
+ - "==": [{ var: "status" }, "active"]
79
+ ```
80
+
81
+ ## High-Level Design: The Graph Runner
82
+
83
+ The system exposes an MCP server that reads a YAML configuration file. When a tool is called on this MCP server, it executes the corresponding graph starting at the tool's entry node and continuing until the exit node is reached.
84
+
85
+ ### The Workflow Lifecycle
86
+
87
+ 1. **Parse**: Read the YAML configuration into a structure containing MCP server metadata, tool definitions, and the directed graph of nodes
88
+ 2. **Initialize MCP Server**: Expose the MCP server with the tools defined in the configuration
89
+ 3. **Tool Invocation**: When a tool is called:
90
+ - Receive tool arguments from the MCP client
91
+ - Start graph execution at the tool's entry node
92
+ - Entry node receives tool arguments and initializes the execution context
93
+ 4. **Execute Node**:
94
+ - **Entry Node**: Receives tool arguments, initializes execution context, passes to next node
95
+ - **Pre-transform**: Apply JSONata to the incoming data to format the tool arguments (if node is an MCP tool call)
96
+ - **Call Tool**: Use the MCP SDK to `callTool` on the target server (if node is an MCP tool call)
97
+ - **Transform**: Apply JSONata expressions to transform data (if node is a transform node)
98
+ - **Route**: Evaluate JSON Logic against the current data state to decide which edge to follow next (if node is a switch/conditional)
99
+ - **Track History**: Record node execution with inputs and outputs for observability
100
+ 5. **Exit Node**: When execution reaches the exit node, extract the final result and return it to the MCP tool caller
101
+
102
+ ## Visual Tooling & Observability
103
+
104
+ ### Component Recommendations
105
+
106
+ | Component | Tooling Recommendation |
107
+ |-----------|----------------------|
108
+ | **Visual Editor** | **React Flow** - Industry standard for node-based UIs, used by companies like Stripe and Typeform. Provides customizable nodes, edges, zooming, panning, and built-in components like MiniMap and Controls. [React Flow Documentation](https://reactflow.dev/) |
109
+ | **Observability** | OpenTelemetry - wrap each node execution in a "Span" to see the "Trace" of data through the graph in tools like Jaeger |
110
+
111
+ ## Implementation Strategy: The YAML Standard
112
+
113
+ Graph definitions should feel like Kubernetes manifests or GitHub Actions - declarative and version-controlled.
114
+
115
+ ### Configuration Structure
116
+
117
+ The YAML configuration centers around MCP server and tool definitions:
118
+
119
+ 1. **MCP Server Metadata**: Defines the MCP server information (name, version, description)
120
+ 2. **Tools**: Array of tool definitions, each containing:
121
+ - Standard MCP tool metadata (name, description)
122
+ - Input parameters schema (MCP tool parameter definitions)
123
+ - Output schema (what the tool returns)
124
+ - Note: Entry and exit nodes are defined in the nodes section with a `tool` field indicating which tool they belong to
125
+ 3. **Nodes**: The directed graph of nodes that execute when tools are called. Node types include:
126
+ - **`entry`**: Entry point for a tool's graph execution. Receives tool arguments and initializes execution context.
127
+ - **`mcp`**: Calls an MCP tool on an internal or external MCP server using `callTool`
128
+ - **`transform`**: Applies JSONata expressions to transform data between nodes
129
+ - **`switch`**: Uses JSON Logic to conditionally route to different nodes based on data
130
+ - **`exit`**: Exit point for a tool's graph execution. Extracts and returns the final result to the MCP tool caller
131
+
132
+ ### Example YAML Structure: count_files Tool
133
+
134
+ This example defines a `count_files` tool that takes a directory, lists its contents using the filesystem MCP server, counts the files, and returns the count:
135
+
136
+ ```yaml
137
+ version: "1.0"
138
+
139
+ # MCP Server Metadata
140
+ server:
141
+ name: "fileUtils"
142
+ version: "1.0.0"
143
+ description: "File utilities"
144
+
145
+ # Tool Definitions
146
+ tools:
147
+ - name: "count_files"
148
+ description: "Counts the number of files in a directory"
149
+ inputSchema:
150
+ type: "object"
151
+ properties:
152
+ directory:
153
+ type: "string"
154
+ description: "The directory path to count files in"
155
+ required:
156
+ - directory
157
+ outputSchema:
158
+ type: "object"
159
+ properties:
160
+ count:
161
+ type: "number"
162
+ description: "The number of files in the directory"
163
+
164
+ # MCP Servers used by the graph
165
+ servers:
166
+ filesystem:
167
+ command: "npx"
168
+ args:
169
+ - "-y"
170
+ - "@modelcontextprotocol/server-filesystem"
171
+ - "./tests/files"
172
+
173
+ # Graph Nodes
174
+ nodes:
175
+ # Entry node: Receives tool arguments
176
+ - id: "entry_count_files"
177
+ type: "entry"
178
+ tool: "count_files"
179
+ next: "list_directory_node"
180
+
181
+ # List directory contents
182
+ - id: "list_directory_node"
183
+ type: "mcp"
184
+ server: "filesystem"
185
+ tool: "list_directory"
186
+ args:
187
+ path: "$.input.directory"
188
+ next: "count_files_node"
189
+
190
+ # Transform and count files
191
+ - id: "count_files_node"
192
+ type: "transform"
193
+ transform:
194
+ expr: |
195
+ { "count": $count($split(list_directory_node, "\n")) }
196
+ next: "exit_count_files"
197
+
198
+ # Exit node: Returns the count
199
+ - id: "exit_count_files"
200
+ type: "exit"
201
+ tool: "count_files"
202
+ ```
203
+
204
+ ## Key Design Principles
205
+
206
+ 1. **Declarative Configuration**: All logic expressed in YAML using standard expression languages (JSONata, JSON Logic)
207
+ 2. **Observability**: Every transformation and decision is traceable and loggable
208
+ 3. **No Embedded Code**: Avoid full programming languages to maintain clarity and safety
209
+ 4. **Standard-Based**: Favor existing standards (JSONata, JSON Logic) over custom solutions
210
+ 5. **Visual First**: Graph should be viewable and editable through a visual interface
211
+ 6. **Execution Transparency**: Ability to observe graph execution in real-time
212
+
213
+ ## System Components
214
+
215
+ 1. **Executable**: Exposes an MCP server to run the graph (`mcpgraph` CLI)
216
+ 2. **Programmatic API**: Exports `McpGraphApi` class for programmatic use (e.g., by visualizer applications)
217
+ 3. **Graph Executor**: Core orchestration engine that executes the directed graph sequentially
218
+ 4. **Execution Context**: Tracks execution state, data flow, and history
219
+ 5. **Visual Editor**: Tools to visually view and edit the graph (future)
220
+ 6. **Execution Observer**: Ability to observe and debug graph execution (future - see `docs/future-introspection-debugging.md`)
@@ -0,0 +1,377 @@
1
+ # mcpGraph Implementation
2
+
3
+ This document describes the implementation of the mcpGraph MCP server with graph execution capabilities. The visual editor/UX is deferred to a later phase.
4
+
5
+ ## Overview
6
+
7
+ The implementation creates a working MCP server that can:
8
+ 1. Parse YAML configuration files
9
+ 2. Expose MCP tools defined in the configuration
10
+ 3. Execute directed graphs of nodes when tools are called
11
+ 4. Handle data transformation and routing between nodes
12
+
13
+ ## Phase 1: Foundation & Configuration Parsing
14
+
15
+ ### 1.1 Project Setup & Dependencies
16
+
17
+ **Implemented:**
18
+ - TypeScript project initialized
19
+ - Logger (stderr only) implemented in `src/logger.ts`
20
+ - All dependencies added:
21
+ - `@modelcontextprotocol/sdk` - MCP SDK
22
+ - `jsonata` - Data transformation
23
+ - `json-logic-js` - Conditional routing
24
+ - `js-yaml` - YAML parsing
25
+ - `zod` - Schema validation
26
+
27
+ **Note:** A custom execution loop was implemented to provide full control over execution flow and enable future introspection/debugging capabilities.
28
+
29
+ ### 1.2 Type Definitions
30
+
31
+ **File: `src/types/config.ts`**
32
+
33
+ All TypeScript interfaces defined:
34
+ - `McpGraphConfig` - Root configuration structure
35
+ - `ServerMetadata` - MCP server metadata
36
+ - `ToolDefinition` - Tool definition with input/output schemas (entry/exit nodes are defined in nodes with `tool` field)
37
+ - `NodeDefinition` - Base node interface
38
+ - `EntryNode` - Entry point node that receives tool arguments
39
+ - `ExitNode` - Exit point node that returns final result
40
+ - `McpNode` - MCP tool call node
41
+ - `TransformNode` - JSONata transformation node
42
+ - `SwitchNode` - JSON Logic routing node
43
+
44
+ ### 1.3 YAML Schema & Validation
45
+
46
+ **File: `src/config/schema.ts`**
47
+
48
+ Zod schemas implemented to validate YAML structure on load with clear error messages for invalid configurations.
49
+
50
+ ### 1.4 YAML Parser
51
+
52
+ **File: `src/config/parser.ts`**
53
+
54
+ YAML file parsing into structured configuration with validation against schema. Handles file I/O errors gracefully.
55
+
56
+ ### 1.5 Configuration Loader
57
+
58
+ **File: `src/config/loader.ts`**
59
+
60
+ Loads configuration from file path and returns parsed and validated configuration object.
61
+
62
+ ## Phase 2: Graph Structure & Representation
63
+
64
+ ### 2.1 Graph Data Structure
65
+
66
+ **File: `src/graph/graph.ts`**
67
+
68
+ Directed graph implementation from node definitions:
69
+ - Node adjacency (edges) storage
70
+ - Graph traversal utilities
71
+ - Support for cycles
72
+
73
+ ### 2.2 Node Registry
74
+
75
+ **Note:** Not implemented as separate file - functionality integrated into `Graph` class and `validator.ts`:
76
+ - Node ID to node definition mapping
77
+ - Node reference validation (tool references in entry/exit nodes, next, switch targets)
78
+ - Orphaned node detection
79
+ - Graph connectivity validation
80
+
81
+ ### 2.3 Graph Validator
82
+
83
+ **File: `src/graph/validator.ts`**
84
+
85
+ Graph structure validation:
86
+ - All referenced nodes exist
87
+ - All tools have exactly one entry and one exit node
88
+ - Entry nodes are only referenced as tool entry points
89
+ - Exit nodes are only referenced as tool exit points
90
+ - Exit nodes are reachable from entry nodes
91
+ - Detailed validation error messages
92
+
93
+ ## Phase 3: MCP Server Foundation
94
+
95
+ ### 3.1 MCP Server Setup
96
+
97
+ **Note:** Not implemented as separate file - functionality integrated into `src/main.ts`:
98
+ - MCP server initialization using `@modelcontextprotocol/sdk`
99
+ - Stdio transport setup
100
+ - Server lifecycle management
101
+
102
+ ### 3.2 Tool Registration
103
+
104
+ **Note:** Not implemented as separate file - functionality integrated into `src/main.ts`:
105
+ - Tool definitions converted to MCP tool schemas
106
+ - Tools registered with MCP server
107
+ - Tool names mapped to execution handlers
108
+
109
+ ### 3.3 Tool Execution Handler
110
+
111
+ **Note:** Not implemented as separate file - functionality integrated into `src/main.ts`:
112
+ - Tool invocation request handling
113
+ - Tool argument extraction
114
+ - Graph execution initiation at tool's entry node
115
+ - Result return to MCP client
116
+
117
+ ## Phase 4: Expression Engines Integration
118
+
119
+ ### 4.1 JSONata Integration
120
+
121
+ **File: `src/expressions/jsonata.ts`**
122
+
123
+ JSONata library wrapper:
124
+ - Expression evaluation with context data
125
+ - Error handling
126
+ - Support for JSONata references (e.g., `$.input.directory`)
127
+
128
+ ### 4.2 JSON Logic Integration
129
+
130
+ **File: `src/expressions/json-logic.ts`**
131
+
132
+ JSON Logic library wrapper:
133
+ - Rule evaluation with context data
134
+ - Boolean results for routing decisions
135
+ - Error handling
136
+
137
+ ### 4.3 Expression Context
138
+
139
+ **File: `src/execution/context.ts`**
140
+
141
+ Expression evaluation context building:
142
+ - Tool input arguments
143
+ - Previous node outputs
144
+ - Execution state
145
+ - Data access for expressions
146
+
147
+ **Note:** `src/expressions/context.ts` exists but functionality is primarily in `src/execution/context.ts`.
148
+
149
+ ## Phase 5: Node Execution
150
+
151
+ ### 5.1 Node Executor Base
152
+
153
+ **Note:** Not implemented as separate base class - each executor is standalone with consistent execution pattern:
154
+ - Pre-execution validation
155
+ - Execute node logic
156
+ - Post-execution processing
157
+ - Determine next node(s)
158
+
159
+ ### 5.2 MCP Tool Node Executor
160
+
161
+ **File: `src/execution/nodes/mcp-tool-executor.ts`**
162
+
163
+ MCP tool node execution:
164
+ - Pre-transform: Apply JSONata to format tool arguments
165
+ - Call MCP tool using MCP client
166
+ - Handle MCP call errors
167
+ - Return output and next node
168
+
169
+ ### 5.3 Transform Node Executor
170
+
171
+ **File: `src/execution/nodes/transform-executor.ts`**
172
+
173
+ Transform node execution:
174
+ - Apply JSONata transformation expression
175
+ - Pass through data with transformation
176
+ - Return transformed output and next node
177
+
178
+ ### 5.4 Switch Node Executor
179
+
180
+ **File: `src/execution/nodes/switch-executor.ts`**
181
+
182
+ Switch node execution:
183
+ - Evaluate JSON Logic conditions in order
184
+ - Select target node based on first matching condition
185
+ - Handle default/fallback case (conditions without rules)
186
+ - Return next node based on condition result
187
+
188
+ ### 5.5 Entry Node Executor
189
+
190
+ **File: `src/execution/nodes/entry-executor.ts`**
191
+
192
+ Entry node execution:
193
+ - Receive tool arguments from MCP client
194
+ - Initialize execution context with tool arguments
195
+ - Make tool arguments available to subsequent nodes (e.g., `$.input.*`)
196
+ - Pass execution to next node
197
+
198
+ ### 5.6 Exit Node Executor
199
+
200
+ **File: `src/execution/nodes/exit-executor.ts`**
201
+
202
+ Exit node execution:
203
+ - Extract final result from execution context
204
+ - Signal execution completion
205
+ - Return final result to MCP tool caller
206
+
207
+ ## Phase 6: Graph Execution Engine
208
+
209
+ ### 6.1 Execution Context
210
+
211
+ **File: `src/execution/context.ts`**
212
+
213
+ Execution state management:
214
+ - Current node tracking
215
+ - Data context (tool inputs, node outputs)
216
+ - Execution history
217
+ - Error state
218
+ - Data access for expressions
219
+
220
+ ### 6.2 Graph Executor
221
+
222
+ **File: `src/execution/executor.ts`**
223
+
224
+ Main graph execution orchestrator:
225
+ - Custom sequential execution loop
226
+ - Start at tool's entry node
227
+ - Execute current node based on type (entry, mcp, transform, switch, exit)
228
+ - Move to next node based on node's `next` field or switch routing
229
+ - Continue until exit node is reached
230
+ - Track execution history (node inputs/outputs)
231
+ - Handle errors with context
232
+
233
+ **Note:** The execution loop supports cycles (directed graphs with cycles), but infinite loops are prevented by the exit node check. Future loop node types can leverage this cycle support.
234
+
235
+ ### 6.3 Execution Flow
236
+
237
+ **Note:** Not implemented as separate file - functionality integrated into `executor.ts`:
238
+ - Node execution sequence coordination
239
+ - Data flow between nodes
240
+ - Branching (switch nodes)
241
+ - Parallel execution support deferred
242
+
243
+ ## Phase 7: MCP Client for External Servers
244
+
245
+ ### 7.1 MCP Client Manager
246
+
247
+ **File: `src/mcp/client-manager.ts`**
248
+
249
+ MCP client connection management:
250
+ - Connections to external MCP servers
251
+ - Client connection caching
252
+ - Connection lifecycle handling
253
+ - Support for multiple concurrent servers
254
+
255
+ ### 7.2 MCP Client Factory
256
+
257
+ **Note:** Not implemented as separate file - client creation functionality is in `client-manager.ts`:
258
+ - MCP client creation for different server types
259
+ - Stdio transport support
260
+ - Client configuration
261
+
262
+ ### 7.3 Tool Call Handler
263
+
264
+ **Note:** Not implemented as separate file - tool calling functionality is in `mcp-tool-executor.ts`:
265
+ - `callTool` request execution to external MCP servers
266
+ - Tool argument handling
267
+ - Tool response processing
268
+ - Error and timeout handling
269
+
270
+ ## Phase 8: Error Handling & Observability
271
+
272
+ ### 8.1 Error Types
273
+
274
+ **Note:** Not implemented - using standard Error types:
275
+ - Basic error handling works with standard Error
276
+ - Custom error types (ConfigError, GraphError, ExecutionError, NodeError, McpError) would improve developer experience but are not required
277
+
278
+ ### 8.2 Error Handling
279
+
280
+ **Note:** Basic error handling implemented throughout - centralized handler not implemented:
281
+ - Error logging with context (via logger)
282
+ - Error propagation through execution
283
+ - Basic user-friendly error messages
284
+ - Centralized error handler would be a nice-to-have enhancement
285
+
286
+ ### 8.3 Execution Logging
287
+
288
+ **Note:** Basic logging implemented - structured execution logger not implemented:
289
+ - Node execution events logged via basic logger
290
+ - Structured logging would be valuable for debugging but basic logger works
291
+
292
+ ## Phase 9: Integration & Testing
293
+
294
+ ### 9.1 Main Entry Point
295
+
296
+ **File: `src/main.ts`**
297
+
298
+ Main entry point implementation:
299
+ - Command-line argument parsing (config file path)
300
+ - Configuration loading
301
+ - Graph validation
302
+ - MCP server startup
303
+ - Tool registration
304
+ - Tool execution handling
305
+ - Graceful shutdown handling
306
+
307
+ ### 9.2 Integration Tests
308
+
309
+ **Files: `tests/files.test.ts`, `tests/mcp-server.test.ts`, `tests/switch.test.ts`**
310
+
311
+ Integration tests implemented:
312
+ - Full execution flow with sample configs
313
+ - Different node types tested
314
+ - MCP client integration tested
315
+ - Switch node conditional routing tested
316
+
317
+ ### 9.3 Sample Configurations
318
+
319
+ **File: `examples/count_files.yaml`**
320
+
321
+ Sample configuration implemented demonstrating:
322
+ - Tool definition
323
+ - Entry/exit nodes
324
+ - MCP tool node
325
+ - Transform node with JSONata
326
+
327
+ ## Phase 10: Polish & Documentation
328
+
329
+ ### 10.1 Code Documentation
330
+
331
+ Basic JSDoc comments present on key functions. Comprehensive documentation would be a nice-to-have enhancement.
332
+
333
+ ### 10.2 Error Messages
334
+
335
+ Basic error messages implemented. More helpful suggestions and context would improve developer experience.
336
+
337
+ ### 10.3 README Updates
338
+
339
+ **File: `README.md`**
340
+
341
+ README updated with:
342
+ - Usage instructions
343
+ - Installation from npm
344
+ - MCP server configuration examples
345
+ - Examples and configuration format documentation
346
+
347
+ ## Implementation Decisions
348
+
349
+ 1. **Custom Execution Engine**: A custom execution loop was implemented to provide full control over execution flow, enable observability (execution history), and support future debugging/introspection features.
350
+
351
+ 2. **Expression Evaluation**: All expressions (JSONata, JSON Logic) are evaluated with a consistent context that includes tool inputs and previous node outputs.
352
+
353
+ 3. **Data Flow**: Data flows through nodes as a JSON object that accumulates results. Each node can read from and write to this context.
354
+
355
+ 4. **Error Handling**: Errors at any node are caught, logged, and propagated. Basic error handling works; custom error types would be an enhancement.
356
+
357
+ 5. **MCP Client Management**: External MCP servers are managed as separate clients. The system maintains a registry of available MCP servers and their tools.
358
+
359
+ 6. **Code Organization**: Some planned separate files were integrated into existing files (e.g., server setup in main.ts, tool registration in main.ts). This works well and keeps the codebase simpler.
360
+
361
+ ## Future Considerations
362
+
363
+ See `docs/future-introspection-debugging.md` for planned introspection and debugging features.
364
+
365
+ Other future enhancements:
366
+ - Visual editor/UX
367
+ - Hot-reload of configuration
368
+ - Loop node types (for, while, foreach)
369
+ - Parallel node execution
370
+ - Retry logic for failed nodes
371
+ - Execution history persistence
372
+ - Performance monitoring/metrics
373
+ - OpenTelemetry integration
374
+ - Custom error types
375
+ - Structured execution logging
376
+ - Centralized error handler
377
+
@@ -0,0 +1,651 @@
1
+ # Graph Introspection & Debugging
2
+
3
+ This document explains how to use mcpGraph's introspection and debugging features to build visualizer applications, debuggers, and observability tools.
4
+
5
+ ## Overview
6
+
7
+ mcpGraph provides comprehensive introspection and debugging capabilities that allow you to:
8
+
9
+ - **Observe execution in real-time** using execution hooks
10
+ - **Control execution flow** with pause, resume, and step operations
11
+ - **Set breakpoints** on specific nodes
12
+ - **Collect telemetry** including timing and performance metrics
13
+ - **Inspect execution state** at any point during execution
14
+ - **Access execution history** with detailed timing information
15
+
16
+ All debugging features are **non-intrusive** - they only activate when explicitly enabled, ensuring normal execution remains unaffected.
17
+
18
+ ## Quick Start
19
+
20
+ ```typescript
21
+ import { McpGraphApi, type ExecutionHooks } from 'mcpgraph';
22
+
23
+ const api = new McpGraphApi('config.yaml');
24
+
25
+ // Execute with debugging enabled
26
+ const result = await api.executeTool('count_files', {
27
+ directory: './tests/files'
28
+ }, {
29
+ hooks: {
30
+ onNodeStart: async (nodeId, node) => {
31
+ console.log(`Starting node: ${nodeId} (${node.type})`);
32
+ },
33
+ onNodeComplete: async (nodeId, node, input, output, duration) => {
34
+ console.log(`Node ${nodeId} completed in ${duration}ms`);
35
+ }
36
+ },
37
+ enableTelemetry: true
38
+ });
39
+
40
+ // Access execution history and telemetry
41
+ console.log('History:', result.executionHistory);
42
+ console.log('Telemetry:', result.telemetry);
43
+ ```
44
+
45
+ ## Execution Hooks
46
+
47
+ Execution hooks allow you to observe and control execution at key points. All hooks are optional and are async functions. If you don't need to perform async operations, you can simply not use `await` - the function will still work correctly.
48
+
49
+ ### Available Hooks
50
+
51
+ All hooks are async functions that return Promises:
52
+
53
+ ```typescript
54
+ interface ExecutionHooks {
55
+ /**
56
+ * Called before a node executes
57
+ * Return false to pause execution (acts as a breakpoint)
58
+ */
59
+ onNodeStart?: (
60
+ nodeId: string,
61
+ node: NodeDefinition,
62
+ context: ExecutionContext
63
+ ) => Promise<boolean>;
64
+
65
+ /**
66
+ * Called after a node completes successfully
67
+ */
68
+ onNodeComplete?: (
69
+ nodeId: string,
70
+ node: NodeDefinition,
71
+ input: unknown,
72
+ output: unknown,
73
+ duration: number
74
+ ) => Promise<void>;
75
+
76
+ /**
77
+ * Called when a node encounters an error
78
+ */
79
+ onNodeError?: (
80
+ nodeId: string,
81
+ node: NodeDefinition,
82
+ error: Error,
83
+ context: ExecutionContext
84
+ ) => Promise<void>;
85
+
86
+ /**
87
+ * Called when execution pauses (breakpoint hit or manual pause)
88
+ */
89
+ onPause?: (nodeId: string, context: ExecutionContext) => Promise<void>;
90
+
91
+ /**
92
+ * Called when execution resumes
93
+ */
94
+ onResume?: () => Promise<void>;
95
+ }
96
+ ```
97
+
98
+ ### Example: Real-time UI Updates
99
+
100
+ All hooks are async functions. If you don't need async operations, you can simply not use `await`:
101
+
102
+ ```typescript
103
+ const hooks: ExecutionHooks = {
104
+ // Async function - no await needed if you don't have async operations
105
+ onNodeStart: async (nodeId, node, context) => {
106
+ // Update UI synchronously
107
+ updateNodeStatus(nodeId, 'running');
108
+ highlightNode(nodeId);
109
+ return true; // Continue execution
110
+ },
111
+
112
+ // Async function - can use await if needed
113
+ onNodeComplete: async (nodeId, node, input, output, duration) => {
114
+ // Update UI synchronously
115
+ updateNodeResult(nodeId, {
116
+ input,
117
+ output,
118
+ duration,
119
+ status: 'completed'
120
+ });
121
+ updateNodeVisualization(nodeId, output);
122
+
123
+ // Or perform async operations if needed
124
+ // await logToServer(nodeId, output);
125
+ },
126
+
127
+ onNodeError: async (nodeId, node, error, context) => {
128
+ // Error handling
129
+ showError(nodeId, error);
130
+ updateNodeStatus(nodeId, 'error');
131
+ }
132
+ };
133
+
134
+ await api.executeTool('my_tool', {}, { hooks });
135
+ ```
136
+
137
+ ## Execution Controller
138
+
139
+ The execution controller provides programmatic control over execution flow. It's available during execution when hooks or breakpoints are enabled.
140
+
141
+ ### Getting the Controller
142
+
143
+ ```typescript
144
+ // Start execution with hooks/breakpoints
145
+ const executionPromise = api.executeTool('my_tool', {}, {
146
+ hooks: { /* ... */ },
147
+ breakpoints: ['node_1', 'node_2']
148
+ });
149
+
150
+ // Get controller (available during execution)
151
+ const controller = api.getController();
152
+ if (controller) {
153
+ // Use controller methods
154
+ controller.pause();
155
+ controller.resume();
156
+ await controller.step();
157
+ }
158
+ ```
159
+
160
+ ### Controller Methods
161
+
162
+ ```typescript
163
+ interface ExecutionController {
164
+ /**
165
+ * Pause execution at the next node boundary
166
+ * Only valid when status is "running"
167
+ */
168
+ pause(): void;
169
+
170
+ /**
171
+ * Resume execution
172
+ * Only valid when status is "paused"
173
+ */
174
+ resume(): void;
175
+
176
+ /**
177
+ * Step to the next node (step over)
178
+ * Only valid when status is "paused"
179
+ */
180
+ step(): Promise<void>;
181
+
182
+ /**
183
+ * Get current execution state
184
+ */
185
+ getState(): ExecutionState;
186
+
187
+ /**
188
+ * Set breakpoints
189
+ */
190
+ setBreakpoints(nodeIds: string[]): void;
191
+
192
+ /**
193
+ * Clear breakpoints
194
+ */
195
+ clearBreakpoints(): void;
196
+ }
197
+ ```
198
+
199
+ ### Example: Step-through Debugging
200
+
201
+ ```typescript
202
+ const hooks: ExecutionHooks = {
203
+ onPause: async (nodeId, context) => {
204
+ console.log(`Paused at node: ${nodeId}`);
205
+ // Show debugger UI
206
+ showDebuggerUI();
207
+ },
208
+ onResume: async () => {
209
+ console.log('Resuming execution');
210
+ hideDebuggerUI();
211
+ }
212
+ };
213
+
214
+ // Start execution
215
+ const executionPromise = api.executeTool('my_tool', {}, {
216
+ hooks,
217
+ breakpoints: ['entry_node'] // Start paused
218
+ });
219
+
220
+ // In your UI event handlers:
221
+ function handleStep() {
222
+ const controller = api.getController();
223
+ if (controller) {
224
+ await controller.step();
225
+ }
226
+ }
227
+
228
+ function handlePause() {
229
+ const controller = api.getController();
230
+ if (controller) {
231
+ controller.pause();
232
+ }
233
+ }
234
+
235
+ function handleResume() {
236
+ const controller = api.getController();
237
+ if (controller) {
238
+ controller.resume();
239
+ }
240
+ }
241
+ ```
242
+
243
+ ## Breakpoints
244
+
245
+ Breakpoints allow you to pause execution at specific nodes. You can set breakpoints in two ways:
246
+
247
+ ### 1. Via Execution Options
248
+
249
+ ```typescript
250
+ await api.executeTool('my_tool', {}, {
251
+ breakpoints: ['node_1', 'node_2', 'node_3']
252
+ });
253
+ ```
254
+
255
+ ### 2. Via Execution Controller
256
+
257
+ ```typescript
258
+ const controller = api.getController();
259
+ if (controller) {
260
+ // Set breakpoints dynamically
261
+ controller.setBreakpoints(['node_1', 'node_2']);
262
+
263
+ // Clear all breakpoints
264
+ controller.clearBreakpoints();
265
+ }
266
+ ```
267
+
268
+ ### 3. Via onNodeStart Hook
269
+
270
+ ```typescript
271
+ const hooks: ExecutionHooks = {
272
+ onNodeStart: async (nodeId, node, context) => {
273
+ // Conditional breakpoint logic
274
+ if (shouldBreak(nodeId, context)) {
275
+ return false; // Pause execution
276
+ }
277
+ return true; // Continue
278
+ }
279
+ };
280
+ ```
281
+
282
+ ## Execution State
283
+
284
+ The execution state provides a snapshot of the current execution status, including:
285
+
286
+ - **Status**: Current execution status (`not_started`, `running`, `paused`, `finished`, `error`)
287
+ - **Current Node**: The node currently executing (or null)
288
+ - **Execution History**: Complete history of all executed nodes
289
+ - **Context**: The current execution context with all data
290
+
291
+ ### Getting Execution State
292
+
293
+ ```typescript
294
+ // During execution
295
+ const state = api.getExecutionState();
296
+ if (state) {
297
+ console.log('Status:', state.status);
298
+ console.log('Current Node:', state.currentNodeId);
299
+ console.log('History:', state.executionHistory);
300
+ console.log('Context:', state.context.getData());
301
+ }
302
+
303
+ // Or via controller
304
+ const controller = api.getController();
305
+ if (controller) {
306
+ const state = controller.getState();
307
+ // Same state object
308
+ }
309
+ ```
310
+
311
+ ### Execution Status
312
+
313
+ ```typescript
314
+ type ExecutionStatus =
315
+ | "not_started" // Execution hasn't begun
316
+ | "running" // Execution is actively running
317
+ | "paused" // Execution is paused (can resume/step)
318
+ | "finished" // Execution completed successfully
319
+ | "error"; // Execution failed with an error
320
+ ```
321
+
322
+ ### Execution State Interface
323
+
324
+ ```typescript
325
+ interface ExecutionState {
326
+ status: ExecutionStatus;
327
+ currentNodeId: string | null;
328
+ executionHistory: NodeExecutionRecord[];
329
+ context: ExecutionContext;
330
+ error?: Error; // Present when status is "error"
331
+ }
332
+ ```
333
+
334
+ ## Execution History
335
+
336
+ Execution history provides a complete record of all node executions with detailed timing information.
337
+
338
+ ### History Record Structure
339
+
340
+ ```typescript
341
+ interface NodeExecutionRecord {
342
+ nodeId: string; // ID of the executed node
343
+ nodeType: string; // Type of node (entry, exit, transform, mcp, switch)
344
+ startTime: number; // Timestamp when node started (milliseconds)
345
+ endTime: number; // Timestamp when node ended (milliseconds)
346
+ duration: number; // Execution duration (milliseconds)
347
+ input: unknown; // Input data for the node
348
+ output: unknown; // Output data from the node
349
+ error?: Error; // Error object if node failed
350
+ }
351
+ ```
352
+
353
+ ### Accessing History
354
+
355
+ ```typescript
356
+ // From execution result
357
+ const result = await api.executeTool('my_tool', {}, {
358
+ enableTelemetry: true
359
+ });
360
+
361
+ for (const record of result.executionHistory || []) {
362
+ console.log(`${record.nodeId}: ${record.duration}ms`);
363
+ if (record.error) {
364
+ console.error(`Error: ${record.error.message}`);
365
+ }
366
+ }
367
+
368
+ // From execution state (during execution)
369
+ const state = api.getExecutionState();
370
+ if (state) {
371
+ const history = state.executionHistory;
372
+ // Access history during execution
373
+ }
374
+ ```
375
+
376
+ ## Telemetry
377
+
378
+ Telemetry provides aggregated performance metrics and execution statistics.
379
+
380
+ ### Telemetry Structure
381
+
382
+ ```typescript
383
+ interface ExecutionTelemetry {
384
+ totalDuration: number; // Total execution time (milliseconds)
385
+ nodeDurations: Map<string, number>; // Total duration per node type
386
+ nodeCounts: Map<string, number>; // Execution count per node type
387
+ errorCount: number; // Total number of errors
388
+ }
389
+ ```
390
+
391
+ ### Collecting Telemetry
392
+
393
+ ```typescript
394
+ const result = await api.executeTool('my_tool', {}, {
395
+ enableTelemetry: true // Enable telemetry collection
396
+ });
397
+
398
+ if (result.telemetry) {
399
+ console.log(`Total duration: ${result.telemetry.totalDuration}ms`);
400
+ console.log(`Errors: ${result.telemetry.errorCount}`);
401
+
402
+ // Node type statistics
403
+ for (const [nodeType, duration] of result.telemetry.nodeDurations) {
404
+ const count = result.telemetry.nodeCounts.get(nodeType) || 0;
405
+ console.log(`${nodeType}: ${count} executions, ${duration}ms total`);
406
+ }
407
+ }
408
+ ```
409
+
410
+ ### Example: Performance Analysis
411
+
412
+ ```typescript
413
+ function analyzePerformance(telemetry: ExecutionTelemetry) {
414
+ const avgDurations = new Map<string, number>();
415
+
416
+ for (const [nodeType, totalDuration] of telemetry.nodeDurations) {
417
+ const count = telemetry.nodeCounts.get(nodeType) || 1;
418
+ const avgDuration = totalDuration / count;
419
+ avgDurations.set(nodeType, avgDuration);
420
+ }
421
+
422
+ // Find slowest node types
423
+ const sorted = Array.from(avgDurations.entries())
424
+ .sort((a, b) => b[1] - a[1]);
425
+
426
+ console.log('Slowest node types:');
427
+ for (const [nodeType, avgDuration] of sorted) {
428
+ console.log(` ${nodeType}: ${avgDuration.toFixed(2)}ms average`);
429
+ }
430
+ }
431
+ ```
432
+
433
+ ## Complete Example: Visualizer Application
434
+
435
+ Here's a complete example of how to build a visualizer application:
436
+
437
+ ```typescript
438
+ import { McpGraphApi, type ExecutionHooks, type ExecutionState } from 'mcpgraph';
439
+
440
+ class GraphVisualizer {
441
+ private api: McpGraphApi;
442
+ private executionPromise: Promise<any> | null = null;
443
+
444
+ constructor(configPath: string) {
445
+ this.api = new McpGraphApi(configPath);
446
+ }
447
+
448
+ async executeWithVisualization(toolName: string, args: Record<string, unknown>) {
449
+ const hooks: ExecutionHooks = {
450
+ onNodeStart: async (nodeId, node, context) => {
451
+ this.updateNodeStatus(nodeId, 'running');
452
+ this.highlightNode(nodeId);
453
+ return true;
454
+ },
455
+
456
+ onNodeComplete: async (nodeId, node, input, output, duration) => {
457
+ this.updateNodeStatus(nodeId, 'completed');
458
+ this.updateNodeData(nodeId, { input, output, duration });
459
+ this.unhighlightNode(nodeId);
460
+ },
461
+
462
+ onNodeError: async (nodeId, node, error, context) => {
463
+ this.updateNodeStatus(nodeId, 'error');
464
+ this.showError(nodeId, error);
465
+ },
466
+
467
+ onPause: async (nodeId, context) => {
468
+ this.showDebuggerControls();
469
+ this.updateExecutionStatus('paused');
470
+ },
471
+
472
+ onResume: async () => {
473
+ this.hideDebuggerControls();
474
+ this.updateExecutionStatus('running');
475
+ }
476
+ };
477
+
478
+ this.executionPromise = this.api.executeTool(toolName, args, {
479
+ hooks,
480
+ enableTelemetry: true
481
+ });
482
+
483
+ const result = await this.executionPromise;
484
+ this.executionPromise = null;
485
+
486
+ // Display results
487
+ this.displayHistory(result.executionHistory || []);
488
+ this.displayTelemetry(result.telemetry);
489
+
490
+ return result;
491
+ }
492
+
493
+ pause() {
494
+ const controller = this.api.getController();
495
+ if (controller) {
496
+ controller.pause();
497
+ }
498
+ }
499
+
500
+ resume() {
501
+ const controller = this.api.getController();
502
+ if (controller) {
503
+ controller.resume();
504
+ }
505
+ }
506
+
507
+ async step() {
508
+ const controller = this.api.getController();
509
+ if (controller) {
510
+ await controller.step();
511
+ }
512
+ }
513
+
514
+ setBreakpoints(nodeIds: string[]) {
515
+ const controller = this.api.getController();
516
+ if (controller) {
517
+ controller.setBreakpoints(nodeIds);
518
+ }
519
+ }
520
+
521
+ getCurrentState(): ExecutionState | null {
522
+ return this.api.getExecutionState();
523
+ }
524
+
525
+ // UI update methods (implement based on your UI framework)
526
+ private updateNodeStatus(nodeId: string, status: string) { /* ... */ }
527
+ private highlightNode(nodeId: string) { /* ... */ }
528
+ private unhighlightNode(nodeId: string) { /* ... */ }
529
+ private updateNodeData(nodeId: string, data: any) { /* ... */ }
530
+ private showError(nodeId: string, error: Error) { /* ... */ }
531
+ private showDebuggerControls() { /* ... */ }
532
+ private hideDebuggerControls() { /* ... */ }
533
+ private updateExecutionStatus(status: string) { /* ... */ }
534
+ private displayHistory(history: any[]) { /* ... */ }
535
+ private displayTelemetry(telemetry: any) { /* ... */ }
536
+ }
537
+ ```
538
+
539
+ ## API Reference
540
+
541
+ ### McpGraphApi Methods
542
+
543
+ ```typescript
544
+ class McpGraphApi {
545
+ /**
546
+ * Execute a tool with optional debugging options
547
+ */
548
+ async executeTool(
549
+ toolName: string,
550
+ toolArguments: Record<string, unknown>,
551
+ options?: ExecutionOptions
552
+ ): Promise<ExecutionResult>;
553
+
554
+ /**
555
+ * Get the execution controller (available during execution)
556
+ */
557
+ getController(): ExecutionController | null;
558
+
559
+ /**
560
+ * Get the current execution state (if execution is in progress)
561
+ */
562
+ getExecutionState(): ExecutionState | null;
563
+
564
+ /**
565
+ * Get the graph structure
566
+ */
567
+ getGraph(): Graph;
568
+
569
+ /**
570
+ * Get the full configuration
571
+ */
572
+ getConfig(): McpGraphConfig;
573
+ }
574
+ ```
575
+
576
+ ### ExecutionOptions
577
+
578
+ ```typescript
579
+ interface ExecutionOptions {
580
+ hooks?: ExecutionHooks; // Execution hooks for observation/control
581
+ breakpoints?: string[]; // Node IDs to pause at
582
+ enableTelemetry?: boolean; // Enable telemetry collection
583
+ }
584
+ ```
585
+
586
+ ### ExecutionResult
587
+
588
+ ```typescript
589
+ interface ExecutionResult {
590
+ result: unknown; // The tool's result
591
+ structuredContent?: Record<string, unknown>; // Structured result (for MCP)
592
+ executionHistory?: NodeExecutionRecord[]; // Execution history
593
+ telemetry?: ExecutionTelemetry; // Performance telemetry
594
+ }
595
+ ```
596
+
597
+ ## Type Exports
598
+
599
+ All types are exported from the main package:
600
+
601
+ ```typescript
602
+ import {
603
+ McpGraphApi,
604
+ type ExecutionHooks,
605
+ type ExecutionController,
606
+ type ExecutionState,
607
+ type ExecutionStatus,
608
+ type ExecutionOptions,
609
+ type ExecutionResult,
610
+ type NodeExecutionRecord,
611
+ type ExecutionTelemetry,
612
+ type NodeDefinition,
613
+ type McpGraphConfig
614
+ } from 'mcpgraph';
615
+ ```
616
+
617
+ ## Best Practices
618
+
619
+ 1. **Enable telemetry only when needed**: Telemetry collection adds overhead, so only enable it when you need performance metrics.
620
+
621
+ 2. **Use hooks for real-time updates**: Hooks are perfect for updating UI in real-time as execution progresses.
622
+
623
+ 3. **All hooks are async**: All hooks are async functions. If you don't need to perform async operations, you can simply not use `await` - the function will still work correctly. This provides consistency and allows you to add async operations later without changing the function signature.
624
+
625
+ 4. **Clean up after execution**: The controller is automatically cleaned up after execution completes, but you should check for `null` when accessing it.
626
+
627
+ 5. **Check execution state before operations**: Always verify that execution is in the correct state before calling controller methods (e.g., don't call `resume()` when status is `"running"`).
628
+
629
+ 6. **Use breakpoints for debugging**: Breakpoints are more efficient than conditional logic in hooks for simple pause-on-node scenarios.
630
+
631
+ ## Limitations
632
+
633
+ - **Controller availability**: The controller is only available during execution when hooks or breakpoints are enabled. After execution completes, it's cleaned up.
634
+
635
+
636
+ - **State modification**: You cannot modify execution context during paused execution (this may be added in future versions).
637
+
638
+ - **Concurrent executions**: Each `McpGraphApi` instance supports one execution at a time. For concurrent executions, create multiple instances.
639
+
640
+ ## Future Enhancements
641
+
642
+ Planned enhancements include:
643
+
644
+ - Conditional breakpoints (break when expression evaluates to true)
645
+ - Watch expressions (monitor specific values during execution)
646
+ - State modification during paused execution
647
+ - Execution replay from history
648
+ - Execution comparison (compare multiple runs)
649
+ - OpenTelemetry integration for distributed tracing
650
+
651
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcpgraph",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "MCP server that executes directed graphs of MCP server calls",
5
5
  "main": "dist/main.js",
6
6
  "type": "module",
@@ -16,6 +16,7 @@
16
16
  "files": [
17
17
  "dist",
18
18
  "examples",
19
+ "docs",
19
20
  "README.md",
20
21
  "LICENSE"
21
22
  ],