mcpgraph 0.1.14 → 0.1.16

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.
Files changed (55) hide show
  1. package/README.md +30 -1
  2. package/dist/api.d.ts +13 -1
  3. package/dist/api.d.ts.map +1 -1
  4. package/dist/api.js +40 -0
  5. package/dist/api.js.map +1 -1
  6. package/dist/config/expression-validator.d.ts +12 -0
  7. package/dist/config/expression-validator.d.ts.map +1 -0
  8. package/dist/config/expression-validator.js +77 -0
  9. package/dist/config/expression-validator.js.map +1 -0
  10. package/dist/config/parser.d.ts.map +1 -1
  11. package/dist/config/parser.js +9 -0
  12. package/dist/config/parser.js.map +1 -1
  13. package/dist/execution/context.d.ts +27 -3
  14. package/dist/execution/context.d.ts.map +1 -1
  15. package/dist/execution/context.js +52 -12
  16. package/dist/execution/context.js.map +1 -1
  17. package/dist/execution/executor.d.ts.map +1 -1
  18. package/dist/execution/executor.js +7 -5
  19. package/dist/execution/executor.js.map +1 -1
  20. package/dist/execution/nodes/entry-executor.d.ts.map +1 -1
  21. package/dist/execution/nodes/entry-executor.js +1 -2
  22. package/dist/execution/nodes/entry-executor.js.map +1 -1
  23. package/dist/execution/nodes/exit-executor.d.ts.map +1 -1
  24. package/dist/execution/nodes/exit-executor.js +15 -5
  25. package/dist/execution/nodes/exit-executor.js.map +1 -1
  26. package/dist/execution/nodes/mcp-tool-executor.d.ts.map +1 -1
  27. package/dist/execution/nodes/mcp-tool-executor.js +4 -3
  28. package/dist/execution/nodes/mcp-tool-executor.js.map +1 -1
  29. package/dist/execution/nodes/switch-executor.d.ts.map +1 -1
  30. package/dist/execution/nodes/switch-executor.js +13 -8
  31. package/dist/execution/nodes/switch-executor.js.map +1 -1
  32. package/dist/execution/nodes/transform-executor.d.ts.map +1 -1
  33. package/dist/execution/nodes/transform-executor.js +4 -3
  34. package/dist/execution/nodes/transform-executor.js.map +1 -1
  35. package/dist/expressions/json-logic.d.ts +11 -1
  36. package/dist/expressions/json-logic.d.ts.map +1 -1
  37. package/dist/expressions/json-logic.js +133 -4
  38. package/dist/expressions/json-logic.js.map +1 -1
  39. package/dist/expressions/jsonata-extensions.d.ts +10 -0
  40. package/dist/expressions/jsonata-extensions.d.ts.map +1 -0
  41. package/dist/expressions/jsonata-extensions.js +69 -0
  42. package/dist/expressions/jsonata-extensions.js.map +1 -0
  43. package/dist/expressions/jsonata.d.ts +8 -1
  44. package/dist/expressions/jsonata.d.ts.map +1 -1
  45. package/dist/expressions/jsonata.js +72 -3
  46. package/dist/expressions/jsonata.js.map +1 -1
  47. package/dist/types/execution.d.ts +1 -1
  48. package/dist/types/execution.d.ts.map +1 -1
  49. package/docs/design.md +14 -2
  50. package/docs/execution-context-redesign.md +284 -0
  51. package/docs/implementation.md +2 -1
  52. package/docs/introspection-debugging.md +28 -1
  53. package/examples/api-usage.ts +54 -1
  54. package/examples/loop_example.yaml +84 -0
  55. package/package.json +1 -1
package/docs/design.md CHANGED
@@ -67,6 +67,7 @@ transform:
67
67
  - Allows complex rules (e.g., "if price > 100 and status == 'active'") as pure JSON objects
68
68
  - Declarative and observable
69
69
  - No embedded code execution
70
+ - **Note:** `var` operations in JSON Logic rules are evaluated using JSONata, allowing full JSONata expression support (including `$previousNode()` function)
70
71
 
71
72
  **Resources:** [JSON Logic Documentation](https://jsonlogic.com/)
72
73
 
@@ -74,8 +75,14 @@ transform:
74
75
  ```yaml
75
76
  condition:
76
77
  and:
77
- - ">": [{ var: "price" }, 100]
78
- - "==": [{ var: "status" }, "active"]
78
+ - ">": [{ var: "entry.price" }, 100]
79
+ - "==": [{ var: "entry.status" }, "active"]
80
+ ```
81
+
82
+ **Advanced Example with JSONata:**
83
+ ```yaml
84
+ condition:
85
+ ">": [{ var: "$previousNode().count" }, 10]
79
86
  ```
80
87
 
81
88
  ## High-Level Design: The Graph Runner
@@ -124,10 +131,15 @@ The YAML configuration centers around MCP server and tool definitions:
124
131
  - Note: Entry and exit nodes are defined in the nodes section with a `tool` field indicating which tool they belong to
125
132
  3. **Nodes**: The directed graph of nodes that execute when tools are called. Node types include:
126
133
  - **`entry`**: Entry point for a tool's graph execution. Receives tool arguments and initializes execution context.
134
+ - **Output**: The tool input arguments (passed through as-is)
127
135
  - **`mcp`**: Calls an MCP tool on an internal or external MCP server using `callTool`
136
+ - **Output**: The MCP tool's response (parsed from the tool's content)
128
137
  - **`transform`**: Applies JSONata expressions to transform data between nodes
138
+ - **Output**: The result of evaluating the JSONata expression
129
139
  - **`switch`**: Uses JSON Logic to conditionally route to different nodes based on data
140
+ - **Output**: The node ID of the target node that was routed to (string)
130
141
  - **`exit`**: Exit point for a tool's graph execution. Extracts and returns the final result to the MCP tool caller
142
+ - **Output**: The output from the previous node in the execution history
131
143
 
132
144
  ### Example YAML Structure: count_files Tool
133
145
 
@@ -0,0 +1,284 @@
1
+ # Execution Context & History Redesign
2
+
3
+ ## Rationale
4
+
5
+ The current execution context design has several limitations that become apparent when considering loops, debugging, and observability:
6
+
7
+ ### Current Issues
8
+
9
+ 1. **No execution history**: Only current context is maintained; no record of past executions
10
+ 2. **Previous node tracking is a hack**: Tracked separately (`previousNodeId`) instead of being derived from history
11
+ 3. **Loops overwrite data**: Same node executing multiple times overwrites previous outputs in context
12
+ 4. **Context structure doesn't distinguish iterations**: Using node ID as key doesn't handle multiple executions of the same node
13
+ 5. **Can't reconstruct historical context**: No way to see what context was available to a node at the time it executed (for debugging)
14
+
15
+ ### Goals
16
+
17
+ 1. **Single source of truth**: Execution history should be the authoritative record
18
+ 2. **Handle loops gracefully**: Multiple executions of the same node should be accessible
19
+ 3. **Derive previous node from history**: No separate tracking needed
20
+ 4. **Time-travel debugging**: Reconstruct context at any point in execution
21
+ 5. **Powerful JSONata access**: Access all executions, not just latest
22
+ 6. **Backward compatibility**: Existing expressions should continue to work
23
+
24
+ ## Current Implementation
25
+
26
+ ### What We Have Now
27
+
28
+ - **Execution History**: `NodeExecutionRecord[]` stored in `ExecutionContext`
29
+ - Each record: `nodeId`, `nodeType`, `startTime`, `endTime`, `duration`, `input`, `output`, `error`
30
+ - Used for telemetry and debugging hooks
31
+
32
+ - **Current Context**: Separate `data` object keyed by node ID
33
+ - `this.data[nodeId] = output` (overwrites on loops)
34
+ - Used for JSONata/JSON Logic evaluation
35
+
36
+ - **Previous Node Tracking**: Separate `previousNodeId` variable passed around
37
+
38
+ ### Problems
39
+
40
+ 1. **Redundancy**: Both history and context store node outputs
41
+ 2. **Input storage**: Storing `input` (full context) is redundant - can be derived from history
42
+ 3. **Loop handling**: Context overwrites, history has multiple records but can't distinguish them
43
+ 4. **Previous node**: Tracked separately instead of derived from history
44
+
45
+ ## Proposed Design
46
+
47
+ ### Core Insight
48
+
49
+ **If execution is sequential (no parallelism), the execution history is just an ordered array of node executions where:**
50
+ - Each execution's input is the context built from all previous executions
51
+ - Previous node is just `history[index - 1]`
52
+ - Context is built from history once per node execution (when node starts)
53
+ - History is the single source of truth
54
+
55
+ ### Execution History Structure
56
+
57
+ **Structure:**
58
+ ```typescript
59
+ interface NodeExecutionRecord {
60
+ executionIndex: number; // Position in overall execution history (0, 1, 2, ...) - unique identifier
61
+ nodeId: string;
62
+ nodeType: string;
63
+ startTime: number;
64
+ endTime: number;
65
+ duration: number;
66
+ output: unknown; // Only store output, derive input from history
67
+ error?: Error;
68
+ }
69
+
70
+ class ExecutionContext {
71
+ private history: NodeExecutionRecord[] = [];
72
+
73
+ getData(): Record<string, unknown> {
74
+ // Build context from history - called once per node execution
75
+ // The context is built when the node starts and used throughout its execution
76
+ return this.buildContextFromHistory();
77
+ }
78
+
79
+ getPreviousNode(currentIndex: number): NodeExecutionRecord | null {
80
+ return currentIndex > 0 ? this.history[currentIndex - 1] : null;
81
+ }
82
+ }
83
+ ```
84
+
85
+ **Key Points:**
86
+ - History array is the single source of truth
87
+ - No separate `data` object
88
+ - No `input` field in records (derivable from history)
89
+ - Context built fresh from history once per node execution
90
+ - Previous node derived from history index
91
+
92
+ ### Context Structure for JSONata Access
93
+
94
+ We use a **flat context structure** with **history access functions**:
95
+
96
+ **Context Building:**
97
+ ```typescript
98
+ private buildContextFromHistory(): Record<string, unknown> {
99
+ const context: Record<string, unknown> = {};
100
+ // Walk backwards, most recent execution of each node wins
101
+ for (let i = this.history.length - 1; i >= 0; i--) {
102
+ const record = this.history[i];
103
+ if (!(record.nodeId in context)) {
104
+ context[record.nodeId] = record.output;
105
+ }
106
+ }
107
+ return context;
108
+ }
109
+ ```
110
+
111
+ **Example Context Object:**
112
+ ```json
113
+ {
114
+ "entry_count_files": { "directory": "/path/to/dir" },
115
+ "list_directory_node": "[FILE] file1.txt\n[FILE] file2.txt",
116
+ "count_files_node": { "count": 2 }
117
+ }
118
+ ```
119
+
120
+ **If same node executes multiple times (loop):**
121
+ ```json
122
+ {
123
+ "entry_loop": { "value": 0 },
124
+ "increment_node": { "value": 3 } // Only latest execution
125
+ }
126
+ ```
127
+
128
+ **JSONata Access:**
129
+ - `$.node_name` → latest output (backward compatible, simple notation)
130
+ - `$.node_name.foo` → property access
131
+
132
+ **Custom JSONata Functions (access history directly):**
133
+ - `$previousNode()` → previous node's output (from history)
134
+ - `$previousNode(index)` → node that executed N steps before current
135
+ - `$executionCount(nodeName)` → count of executions for a node
136
+ - `$nodeExecution(nodeName, index)` → specific execution by index (0 = first, -1 = last)
137
+ - `$nodeExecutions(nodeName)` → array of all executions for a node
138
+
139
+ **Example Usage:**
140
+ ```jsonata
141
+ // Get previous node's output
142
+ $previousNode()
143
+
144
+ // Get execution count
145
+ $executionCount("increment_node") // Returns 3 if executed 3 times
146
+
147
+ // Get first execution
148
+ $nodeExecution("increment_node", 0) // Returns { "value": 1 }
149
+
150
+ // Get all executions
151
+ $nodeExecutions("increment_node") // Returns [{ "value": 1 }, { "value": 2 }, { "value": 3 }]
152
+
153
+ // Get second-to-last execution
154
+ $nodeExecution("increment_node", -2) // Returns { "value": 2 }
155
+ ```
156
+
157
+ **Implementation Note:**
158
+ Functions receive the execution history array and current execution index as parameters, allowing them to query history directly without exposing it in the context structure.
159
+
160
+ **Benefits:**
161
+ - Simple, flat context structure (backward compatible)
162
+ - Fast to build
163
+ - Clean notation: `$.node_name` for latest
164
+ - History access is explicit and clear
165
+ - No namespace conflicts (no special keys in context)
166
+ - Functions can provide powerful queries beyond simple data access
167
+ - History queries are separate from context structure
168
+
169
+ ## Design Decisions
170
+
171
+ ### Input Storage
172
+
173
+ **Decision**: Don't store `input` in `NodeExecutionRecord` - derive from history when needed.
174
+
175
+ **Rationale**: No redundancy, can always derive input by building context from history up to that point.
176
+
177
+ ### Previous Node Resolution
178
+
179
+ **Decision**: `$previousNode()` is a custom JSONata function that queries the history array.
180
+
181
+ **Rationale**: Cleaner, more flexible, and keeps history access explicit.
182
+
183
+ ### Entry Node Handling
184
+
185
+ **Decision**: Entry node's input is the tool input. When building context for the entry node, tool input is available as the entry node's output (stored in history).
186
+
187
+ **Rationale**: Consistent with other nodes - tool input is the input to the entry node.
188
+
189
+ ### Execution Index
190
+
191
+ **Decision**: Add `executionIndex` field to `NodeExecutionRecord` to uniquely identify each execution.
192
+
193
+ **Rationale**:
194
+ - Provides a unique identifier for each execution (even when same node executes multiple times)
195
+ - Enables API endpoints to reference specific executions (e.g., "get context for execution at index 5")
196
+ - Makes it easy for debuggers to reference and query specific executions
197
+ - The index represents the position in the overall execution history array (0, 1, 2, ...)
198
+
199
+ ## Implementation Plan
200
+
201
+ ### Phase 1: Refactor ExecutionContext
202
+
203
+ 1. Remove `data` object, keep only `history`
204
+ 2. Remove `input` from `NodeExecutionRecord` (derive from history)
205
+ 3. Add `executionIndex` to `NodeExecutionRecord` (set when adding to history)
206
+ 4. Implement `buildContextFromHistory(upToIndex?: number)` method:
207
+ - If `upToIndex` is provided, build context from history up to that index (for debugging)
208
+ - If not provided, build context from entire history (for current execution)
209
+ 5. Update `getData()` to build context from history once per node execution
210
+
211
+ ### Phase 2: Update Node Executors
212
+
213
+ 1. Remove `previousNodeId` parameter from all node executors
214
+ 2. Update `addHistory()` to include `executionIndex` (current history length)
215
+ 3. Update `addHistory()` calls to not pass `input` (or derive it)
216
+ 4. Update exit node to get previous node from history instead of `previousNodeId`
217
+
218
+ ### Phase 3: Update Expression Evaluation
219
+
220
+ 1. Context stays flat - no changes needed to context structure
221
+ 2. Add custom JSONata functions that receive history and current index:
222
+ - `$previousNode()` - returns previous node's output (from history)
223
+ - `$previousNode(index)` - returns node that executed N steps before current
224
+ - `$executionCount(nodeName)` - count of executions for a node
225
+ - `$nodeExecution(nodeName, index)` - specific execution by index (0 = first, -1 = last)
226
+ - `$nodeExecutions(nodeName)` - array of all executions for a node
227
+ 3. Update `evaluateJsonLogic()` to work with flat context and new functions
228
+ 4. Functions need access to:
229
+ - Execution history array
230
+ - Current execution index (to determine "previous")
231
+
232
+ ### Phase 4: Update Hooks and Telemetry
233
+
234
+ 1. Update hooks to derive input from history when needed
235
+ 2. Verify telemetry still works correctly (it already uses history structure, but verify after removing `input` field)
236
+ 3. Update introspection/debugging docs
237
+
238
+ ### Phase 5: Add Debugging API Endpoint
239
+
240
+ 1. Add `getContextForExecution(executionIndex: number)` method to API:
241
+ - Takes an `executionIndex` to identify a specific execution
242
+ - Builds context from history up to that execution index
243
+ - Returns the context that was available to that node when it executed
244
+ - Useful for time-travel debugging - "what context did this node see?"
245
+ 2. Add helper method `getExecutionByIndex(executionIndex: number)` to easily access a specific record
246
+
247
+ ### Phase 6: Testing & Documentation
248
+
249
+ 1. Add tests for loop scenarios
250
+ 2. Add tests for `$previousNode()` and other custom functions
251
+ 3. Add tests for `getContextForExecution()` API endpoint
252
+ 4. Update examples to show new capabilities
253
+ 5. Update documentation
254
+
255
+ ## Open Questions
256
+
257
+ 1. **Performance**: Is building context from history too slow? (Answer: No - we build it once per node execution when the node starts, and it's a simple loop through the history array)
258
+
259
+ 2. **Backward Compatibility**: Do we need to support old flat context structure? (Answer: Yes - the flat context structure maintains full backward compatibility - `$.node_name` works exactly as before)
260
+
261
+ 3. **History Persistence**: Should history be persisted across executions? (Answer: Not in scope for this redesign, but structure supports it)
262
+
263
+ 4. **Parallel Execution**: If we add parallel execution later, how does this design handle it? (Answer: Would need execution IDs or iteration numbers, but structure can accommodate)
264
+
265
+ ## Next Steps
266
+
267
+ 1. **Implementation**: Phase 1 (refactor ExecutionContext)
268
+ - Remove `data` object
269
+ - Remove `input` from `NodeExecutionRecord`
270
+ - Add `executionIndex` to `NodeExecutionRecord`
271
+ - Implement `buildContextFromHistory(upToIndex?)` method
272
+ 2. **Implementation**: Phase 2 (update node executors)
273
+ - Remove `previousNodeId` parameter from all node executors
274
+ - Update `addHistory()` to include `executionIndex` (current history length)
275
+ - Update exit node to get previous node from history instead of `previousNodeId`
276
+ 3. **Implementation**: Phase 3 (add history functions)
277
+ - Design function signatures (how to pass history/index to functions)
278
+ - Implement `$previousNode()`, `$executionCount()`, `$nodeExecution()`, `$nodeExecutions()`
279
+ 4. **Implementation**: Phase 5 (add debugging API)
280
+ - Add `getContextForExecution(executionIndex: number)` to API
281
+ - Add `getExecutionByIndex(executionIndex: number)` helper
282
+ 5. **Testing**: Ensure all existing tests pass
283
+ 6. **Testing**: Add loop tests, new function tests, and API endpoint tests
284
+
@@ -133,6 +133,7 @@ JSON Logic library wrapper:
133
133
  - Rule evaluation with context data
134
134
  - Boolean results for routing decisions
135
135
  - Error handling
136
+ - **Note:** `var` operations in JSON Logic rules are pre-processed and evaluated using JSONata, providing full JSONata expression support (including `$previousNode()` function) within JSON Logic rules
136
137
 
137
138
  ### 4.3 Expression Context
138
139
 
@@ -348,7 +349,7 @@ README updated with:
348
349
 
349
350
  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
 
351
- 2. **Expression Evaluation**: All expressions (JSONata, JSON Logic) are evaluated with a consistent context where all data is referenced by node ID. Tool input is stored as the entry node's output (e.g., `$.entry_count_files.directory`), and each node's output is stored by its node ID (e.g., `$.list_directory_node`).
352
+ 2. **Expression Evaluation**: All expressions (JSONata, JSON Logic) are evaluated with a consistent context where all data is referenced by node ID. Tool input is stored as the entry node's output (e.g., `$.entry_count_files.directory`), and each node's output is stored by its node ID (e.g., `$.list_directory_node`). The `$previousNode()` function is available in all JSONata expressions (including those used in JSON Logic `var` operations) to access the output of the node that executed immediately before the current node.
352
353
 
353
354
  3. **Data Flow**: Data flows through nodes as a JSON object where each node's output is stored by its node ID. Each node can read from previous nodes (by their node IDs) and write its own output (stored by its node ID).
354
355
 
@@ -364,17 +364,19 @@ Execution history provides a complete record of all node executions with detaile
364
364
 
365
365
  ```typescript
366
366
  interface NodeExecutionRecord {
367
+ executionIndex: number; // Position in overall execution history (0, 1, 2, ...) - unique identifier
367
368
  nodeId: string; // ID of the executed node
368
369
  nodeType: string; // Type of node (entry, exit, transform, mcp, switch)
369
370
  startTime: number; // Timestamp when node started (milliseconds)
370
371
  endTime: number; // Timestamp when node ended (milliseconds)
371
372
  duration: number; // Execution duration (milliseconds)
372
- input: unknown; // Input data for the node
373
373
  output: unknown; // Output data from the node
374
374
  error?: Error; // Error object if node failed
375
375
  }
376
376
  ```
377
377
 
378
+ **Note**: The `input` field has been removed. Input context can be derived by building context from history up to the execution index using `getContextForExecution(executionIndex)`.
379
+
378
380
  ### Accessing History
379
381
 
380
382
  ```typescript
@@ -398,6 +400,31 @@ if (state) {
398
400
  }
399
401
  ```
400
402
 
403
+ ### Time-Travel Debugging
404
+
405
+ You can get the context that was available to a specific execution using `getContextForExecution()`:
406
+
407
+ ```typescript
408
+ // Get context for a specific execution
409
+ const context = api.getContextForExecution(5);
410
+ if (context) {
411
+ console.log('Context available to execution #5:', context);
412
+ }
413
+
414
+ // Get a specific execution record
415
+ const record = api.getExecutionByIndex(5);
416
+ if (record) {
417
+ console.log(`Execution #5: ${record.nodeId} executed at ${record.startTime}`);
418
+ console.log(`Output:`, record.output);
419
+
420
+ // Get the context that was available to this execution
421
+ const inputContext = api.getContextForExecution(record.executionIndex);
422
+ console.log('Input context:', inputContext);
423
+ }
424
+ ```
425
+
426
+ **Note**: Both methods require an active execution with a controller (hooks/breakpoints enabled). They return `null` if no execution is in progress or the index is invalid.
427
+
401
428
  ## Telemetry
402
429
 
403
430
  Telemetry provides aggregated performance metrics and execution statistics.
@@ -70,7 +70,7 @@ async function introspectionExample() {
70
70
  if (result.executionHistory) {
71
71
  console.log('\nExecution History:');
72
72
  for (const record of result.executionHistory) {
73
- console.log(` ${record.nodeId} (${record.nodeType}): ${record.duration}ms`);
73
+ console.log(` [${record.executionIndex}] ${record.nodeId} (${record.nodeType}): ${record.duration}ms`);
74
74
  }
75
75
  }
76
76
 
@@ -88,6 +88,57 @@ async function introspectionExample() {
88
88
  await api.close();
89
89
  }
90
90
 
91
+ // Example: Time-travel debugging with getContextForExecution
92
+ async function timeTravelDebuggingExample() {
93
+ const api = new McpGraphApi('examples/count_files.yaml');
94
+
95
+ let executionIndexToInspect: number | null = null;
96
+
97
+ const { promise, controller } = api.executeTool('count_files', {
98
+ directory: './tests/files',
99
+ }, {
100
+ hooks: {
101
+ onNodeComplete: async (nodeId, node, input, output, duration) => {
102
+ // When count_files_node completes, inspect the context that was available to list_directory_node
103
+ if (nodeId === 'count_files_node') {
104
+ // Find the execution index of list_directory_node
105
+ const state = api.getExecutionState();
106
+ if (state) {
107
+ const listDirRecord = state.executionHistory.find(r => r.nodeId === 'list_directory_node');
108
+ if (listDirRecord) {
109
+ executionIndexToInspect = listDirRecord.executionIndex;
110
+ }
111
+ }
112
+ }
113
+ },
114
+ },
115
+ });
116
+
117
+ await promise;
118
+
119
+ if (executionIndexToInspect !== null && controller) {
120
+ // Get the context that was available to list_directory_node when it executed
121
+ const context = api.getContextForExecution(executionIndexToInspect);
122
+ if (context) {
123
+ console.log('\nTime-Travel Debugging:');
124
+ console.log(`Context available to execution #${executionIndexToInspect} (list_directory_node):`);
125
+ console.log(JSON.stringify(context, null, 2));
126
+ }
127
+
128
+ // Get the execution record itself
129
+ const record = api.getExecutionByIndex(executionIndexToInspect);
130
+ if (record) {
131
+ console.log(`\nExecution Record #${executionIndexToInspect}:`);
132
+ console.log(` Node: ${record.nodeId}`);
133
+ console.log(` Type: ${record.nodeType}`);
134
+ console.log(` Duration: ${record.duration}ms`);
135
+ console.log(` Output: ${JSON.stringify(record.output).substring(0, 100)}...`);
136
+ }
137
+ }
138
+
139
+ await api.close();
140
+ }
141
+
91
142
  // Example: Validate config without creating an API instance
92
143
  function validateConfigExample() {
93
144
  const errors = McpGraphApi.validateConfig('examples/count_files.yaml');
@@ -118,6 +169,8 @@ if (import.meta.url === `file://${process.argv[1]}`) {
118
169
 
119
170
  if (exampleToRun === 'introspection') {
120
171
  introspectionExample().catch(console.error);
172
+ } else if (exampleToRun === 'debugging') {
173
+ timeTravelDebuggingExample().catch(console.error);
121
174
  } else {
122
175
  example().catch(console.error);
123
176
  }
@@ -0,0 +1,84 @@
1
+ version: "1.0"
2
+
3
+ # MCP Server Metadata
4
+ server:
5
+ name: "loopExample"
6
+ version: "1.0.0"
7
+ description: "Example with a loop using history functions"
8
+
9
+ # Tool Definitions
10
+ tools:
11
+ - name: "sum_to_n"
12
+ description: "Sums numbers from 1 to n using a loop"
13
+ inputSchema:
14
+ type: "object"
15
+ properties:
16
+ n:
17
+ type: "number"
18
+ description: "The number to sum to"
19
+ required:
20
+ - n
21
+ outputSchema:
22
+ type: "object"
23
+ properties:
24
+ sum:
25
+ type: "number"
26
+ description: "The sum from 1 to n"
27
+
28
+ # Graph Nodes
29
+ nodes:
30
+ # Entry node: Receives tool arguments
31
+ - id: "entry_sum"
32
+ type: "entry"
33
+ tool: "sum_to_n"
34
+ next: "increment_node"
35
+
36
+ # Increment counter and add to sum
37
+ # Uses $nodeExecution() to get the previous iteration of this node
38
+ # First iteration: $executionCount("increment_node") = 0, so we initialize
39
+ # Subsequent iterations: $nodeExecution("increment_node", -1) gets the last execution
40
+ - id: "increment_node"
41
+ type: "transform"
42
+ transform:
43
+ expr: |
44
+ $executionCount("increment_node") = 0
45
+ ? {
46
+ "counter": 1,
47
+ "sum": 1,
48
+ "target": $.entry_sum.n
49
+ }
50
+ : {
51
+ "counter": $nodeExecution("increment_node", -1).counter + 1,
52
+ "sum": $nodeExecution("increment_node", -1).sum + $nodeExecution("increment_node", -1).counter + 1,
53
+ "target": $.entry_sum.n
54
+ }
55
+ next: "check_condition"
56
+
57
+ # Check if we should continue looping
58
+ - id: "check_condition"
59
+ type: "switch"
60
+ conditions:
61
+ - rule:
62
+ "<": [{"var": "$.increment_node.counter"}, {"var": "$.increment_node.target"}]
63
+ target: "increment_node"
64
+ - target: "exit_sum"
65
+
66
+ # Exit node: Extracts the sum from increment_node
67
+ # Uses $previousNode() to verify it returns the switch node's output (target node ID)
68
+ # The output matches the tool's outputSchema (just the sum)
69
+ - id: "exit_sum"
70
+ type: "transform"
71
+ transform:
72
+ expr: |
73
+ $previousNode() = "exit_sum" ? {
74
+ "sum": $.increment_node.sum
75
+ } : {
76
+ "sum": 0,
77
+ "error": "previousNode check failed"
78
+ }
79
+ next: "exit_sum_final"
80
+
81
+ # Final exit node
82
+ - id: "exit_sum_final"
83
+ type: "exit"
84
+ tool: "sum_to_n"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcpgraph",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
4
4
  "description": "MCP server that executes directed graphs of MCP server calls",
5
5
  "main": "dist/main.js",
6
6
  "type": "module",