coaia-visualizer 1.4.2 → 1.5.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.
Files changed (47) hide show
  1. package/.dockerignore +9 -0
  2. package/Dockerfile.app +50 -0
  3. package/Dockerfile.test +24 -0
  4. package/LIVE_MODE_DESIGN.md +435 -0
  5. package/MCP_TESTING_COMPLETE.md +302 -0
  6. package/MCP_TESTING_IMPLEMENTATION_SUMMARY.md +317 -0
  7. package/MCP_TESTING_SETUP.md +268 -0
  8. package/NAMING.md +218 -0
  9. package/QUICK_START_MCP_TESTING.md +236 -0
  10. package/WS__issue_8__coaia-visualizer__260207.code-workspace +45 -0
  11. package/app/api/audio/[filename]/route.ts +37 -0
  12. package/app/api/charts/[id]/route.ts +48 -35
  13. package/app/api/watch/route.ts +42 -0
  14. package/app/page.tsx +103 -53
  15. package/cli.ts +56 -3
  16. package/components/add-master-chart.tsx +230 -0
  17. package/components/chart-detail-editable.tsx +27 -16
  18. package/components/chart-list.tsx +13 -1
  19. package/components/create-chart-form.tsx +248 -0
  20. package/components/data-stats.tsx +9 -7
  21. package/components/live-indicator.tsx +14 -0
  22. package/components/ui/dialog.tsx +143 -0
  23. package/components/ui/label.tsx +24 -0
  24. package/direct-test.sh +180 -0
  25. package/dist/cli.js +52 -3
  26. package/docker-compose.test.yml +69 -0
  27. package/hooks/use-live-polling.ts +45 -0
  28. package/jgwill.coaia-visualizer-8--496dca71-d476-4ac9-ba9f-376add118dd8--260208.txt +2612 -0
  29. package/lib/chart-editor.ts +281 -68
  30. package/mcp/Dockerfile +21 -0
  31. package/mcp/README.md +25 -6
  32. package/mcp/src/api-client.ts +15 -3
  33. package/mcp/src/index.ts +17 -2
  34. package/mcp/src/tools/index.ts +21 -1
  35. package/mcp/test_mcp/.gemini/settings.json +18 -0
  36. package/mcp-config.json +14 -0
  37. package/package.json +2 -2
  38. package/run-mcp-tests.sh +99 -0
  39. package/samples/tradingchart.jsonl +31 -0
  40. package/test-data/test-master.jsonl +11 -0
  41. package/test-run.log +101 -0
  42. package/test-scripts/README.md +239 -0
  43. package/test-scripts/run-all-tests.sh +38 -0
  44. package/test-scripts/test-01-basic-operations.sh +87 -0
  45. package/test-scripts/test-02-telescope-creation.sh +91 -0
  46. package/test-scripts/test-03-navigation.sh +87 -0
  47. package/validate-mcp.sh +136 -0
@@ -0,0 +1,248 @@
1
+ "use client"
2
+
3
+ import type React from "react"
4
+
5
+ import { useState } from "react"
6
+ import { Button } from "@/components/ui/button"
7
+ import { Input } from "@/components/ui/input"
8
+ import { Textarea } from "@/components/ui/textarea"
9
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
10
+ import { Label } from "@/components/ui/label"
11
+ import { Plus, Trash2, Calendar } from "lucide-react"
12
+ import type { ParsedData, EntityRecord, RelationRecord } from "@/lib/types"
13
+
14
+ interface CreateChartFormProps {
15
+ onChartCreated: (data: ParsedData) => void
16
+ }
17
+
18
+ export function CreateChartForm({ onChartCreated }: CreateChartFormProps) {
19
+ const [desiredOutcome, setDesiredOutcome] = useState("")
20
+ const [currentRealityObservations, setCurrentRealityObservations] = useState<string[]>([""])
21
+ const [dueDate, setDueDate] = useState("")
22
+
23
+ const addObservation = () => {
24
+ setCurrentRealityObservations([...currentRealityObservations, ""])
25
+ }
26
+
27
+ const updateObservation = (index: number, value: string) => {
28
+ const updated = [...currentRealityObservations]
29
+ updated[index] = value
30
+ setCurrentRealityObservations(updated)
31
+ }
32
+
33
+ const removeObservation = (index: number) => {
34
+ const updated = currentRealityObservations.filter((_, i) => i !== index)
35
+ setCurrentRealityObservations(updated)
36
+ }
37
+
38
+ const handleSubmit = (e: React.FormEvent) => {
39
+ e.preventDefault()
40
+
41
+ if (!desiredOutcome.trim()) {
42
+ alert("Please enter a desired outcome")
43
+ return
44
+ }
45
+
46
+ const validObservations = currentRealityObservations.filter((obs) => obs.trim())
47
+ if (validObservations.length === 0) {
48
+ alert("Please add at least one current reality observation")
49
+ return
50
+ }
51
+
52
+ // Create chart ID
53
+ const chartId = `chart_${Date.now()}`
54
+ const timestamp = new Date().toISOString()
55
+
56
+ // Create entities
57
+ const entities = new Map<string, EntityRecord>()
58
+
59
+ // Chart entity
60
+ entities.set(chartId, {
61
+ type: "entity",
62
+ name: chartId,
63
+ entityType: "structural_tension_chart",
64
+ observations: ["New structural tension chart"],
65
+ metadata: {
66
+ chartId,
67
+ level: 0,
68
+ dueDate: dueDate || undefined,
69
+ completionStatus: false,
70
+ createdAt: timestamp,
71
+ updatedAt: timestamp,
72
+ },
73
+ })
74
+
75
+ // Desired outcome entity
76
+ const desiredOutcomeName = `${chartId}_desired_outcome`
77
+ entities.set(desiredOutcomeName, {
78
+ type: "entity",
79
+ name: desiredOutcomeName,
80
+ entityType: "desired_outcome",
81
+ observations: [desiredOutcome],
82
+ metadata: {
83
+ chartId,
84
+ createdAt: timestamp,
85
+ updatedAt: timestamp,
86
+ },
87
+ })
88
+
89
+ // Current reality entity
90
+ const currentRealityName = `${chartId}_current_reality`
91
+ entities.set(currentRealityName, {
92
+ type: "entity",
93
+ name: currentRealityName,
94
+ entityType: "current_reality",
95
+ observations: validObservations,
96
+ metadata: {
97
+ chartId,
98
+ createdAt: timestamp,
99
+ updatedAt: timestamp,
100
+ },
101
+ })
102
+
103
+ // Create relations
104
+ const relations: RelationRecord[] = [
105
+ {
106
+ type: "relation",
107
+ from: chartId,
108
+ to: desiredOutcomeName,
109
+ relationType: "contains",
110
+ metadata: { createdAt: timestamp },
111
+ },
112
+ {
113
+ type: "relation",
114
+ from: chartId,
115
+ to: currentRealityName,
116
+ relationType: "contains",
117
+ metadata: { createdAt: timestamp },
118
+ },
119
+ {
120
+ type: "relation",
121
+ from: currentRealityName,
122
+ to: desiredOutcomeName,
123
+ relationType: "creates_tension_with",
124
+ metadata: { createdAt: timestamp },
125
+ },
126
+ ]
127
+
128
+ // Organize into ParsedData structure
129
+ const parsedData: ParsedData = {
130
+ entities,
131
+ relations,
132
+ charts: [
133
+ {
134
+ id: chartId,
135
+ name: chartId,
136
+ level: 0,
137
+ desiredOutcome,
138
+ currentReality: validObservations,
139
+ actions: [],
140
+ subCharts: [],
141
+ beats: [],
142
+ metadata: {
143
+ chartId,
144
+ level: 0,
145
+ dueDate: dueDate || undefined,
146
+ completionStatus: false,
147
+ createdAt: timestamp,
148
+ updatedAt: timestamp,
149
+ },
150
+ },
151
+ ],
152
+ }
153
+
154
+ onChartCreated(parsedData)
155
+ }
156
+
157
+ return (
158
+ <Card className="max-w-2xl mx-auto">
159
+ <CardHeader>
160
+ <CardTitle>Create New Structural Tension Chart</CardTitle>
161
+ <CardDescription>Define your desired outcome and current reality to create structural tension</CardDescription>
162
+ </CardHeader>
163
+ <CardContent>
164
+ <form onSubmit={handleSubmit} className="space-y-6">
165
+ {/* Desired Outcome */}
166
+ <div className="space-y-2">
167
+ <Label htmlFor="desired-outcome" className="text-base font-semibold">
168
+ Desired Outcome
169
+ <span className="text-destructive ml-1">*</span>
170
+ </Label>
171
+ <Textarea
172
+ id="desired-outcome"
173
+ placeholder="What do you want to achieve? Be specific and concrete."
174
+ value={desiredOutcome}
175
+ onChange={(e) => setDesiredOutcome(e.target.value)}
176
+ className="min-h-[100px]"
177
+ required
178
+ />
179
+ <p className="text-xs text-muted-foreground">
180
+ Describe the future state you want to create. This creates the aspirational pole of the tension.
181
+ </p>
182
+ </div>
183
+
184
+ {/* Current Reality */}
185
+ <div className="space-y-3">
186
+ <Label className="text-base font-semibold">
187
+ Current Reality Observations
188
+ <span className="text-destructive ml-1">*</span>
189
+ </Label>
190
+ <p className="text-xs text-muted-foreground">
191
+ Honestly assess where things actually are right now. This creates the grounding pole of the tension.
192
+ </p>
193
+ {currentRealityObservations.map((obs, index) => (
194
+ <div key={index} className="flex gap-2">
195
+ <Textarea
196
+ placeholder={`Observation ${index + 1}: What is actually true right now?`}
197
+ value={obs}
198
+ onChange={(e) => updateObservation(index, e.target.value)}
199
+ className="flex-1 min-h-[80px]"
200
+ />
201
+ {currentRealityObservations.length > 1 && (
202
+ <Button
203
+ type="button"
204
+ variant="ghost"
205
+ size="icon"
206
+ onClick={() => removeObservation(index)}
207
+ className="flex-shrink-0"
208
+ >
209
+ <Trash2 className="w-4 h-4" />
210
+ </Button>
211
+ )}
212
+ </div>
213
+ ))}
214
+ <Button
215
+ type="button"
216
+ variant="outline"
217
+ size="sm"
218
+ onClick={addObservation}
219
+ className="w-full bg-transparent"
220
+ >
221
+ <Plus className="w-4 h-4 mr-2" />
222
+ Add Another Observation
223
+ </Button>
224
+ </div>
225
+
226
+ {/* Due Date (Optional) */}
227
+ <div className="space-y-2">
228
+ <Label htmlFor="due-date" className="text-base font-semibold flex items-center gap-2">
229
+ <Calendar className="w-4 h-4" />
230
+ Due Date (Optional)
231
+ </Label>
232
+ <Input id="due-date" type="datetime-local" value={dueDate} onChange={(e) => setDueDate(e.target.value)} />
233
+ <p className="text-xs text-muted-foreground">
234
+ Set a target date to create urgency and enable telescoped planning.
235
+ </p>
236
+ </div>
237
+
238
+ {/* Submit Button */}
239
+ <div className="flex gap-3 pt-4">
240
+ <Button type="submit" className="flex-1">
241
+ Create Chart
242
+ </Button>
243
+ </div>
244
+ </form>
245
+ </CardContent>
246
+ </Card>
247
+ )
248
+ }
@@ -9,15 +9,17 @@ interface DataStatsProps {
9
9
  }
10
10
 
11
11
  export function DataStats({ data }: DataStatsProps) {
12
- const totalEntities = data.entities.size
13
- const totalRelations = data.relations.length
14
- const totalCharts = data.charts.length
15
- const totalActions = data.charts.reduce((sum, chart) => sum + chart.actions.length, 0)
16
- const completedActions = data.charts.reduce(
17
- (sum, chart) => sum + chart.actions.filter((a) => a.metadata.completionStatus).length,
12
+ const chartsArray = Array.isArray(data.charts) ? data.charts : []
13
+
14
+ const totalEntities = data.entities?.size || 0
15
+ const totalRelations = data.relations?.length || 0
16
+ const totalCharts = chartsArray.length
17
+ const totalActions = chartsArray.reduce((sum, chart) => sum + (chart.actions?.length || 0), 0)
18
+ const completedActions = chartsArray.reduce(
19
+ (sum, chart) => sum + (chart.actions?.filter((a) => a.metadata?.completionStatus).length || 0),
18
20
  0,
19
21
  )
20
- const totalBeats = data.charts.reduce((sum, chart) => sum + chart.narrativeBeats.length, 0)
22
+ const totalBeats = chartsArray.reduce((sum, chart) => sum + (chart.narrativeBeats?.length || 0), 0)
21
23
 
22
24
  const stats = [
23
25
  { label: "Charts", value: totalCharts, icon: Target, color: "text-chart-1" },
@@ -0,0 +1,14 @@
1
+ export function LiveIndicator({ isLive }: { isLive: boolean }) {
2
+ return (
3
+ <div className="flex items-center gap-2">
4
+ <div className={`w-2 h-2 rounded-full transition-all ${
5
+ isLive
6
+ ? 'bg-green-500 animate-pulse shadow-lg shadow-green-500/50'
7
+ : 'bg-gray-400'
8
+ }`} />
9
+ <span className="text-sm font-medium">
10
+ {isLive ? 'LIVE' : 'Monitoring'}
11
+ </span>
12
+ </div>
13
+ )
14
+ }
@@ -0,0 +1,143 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import * as DialogPrimitive from '@radix-ui/react-dialog'
5
+ import { XIcon } from 'lucide-react'
6
+
7
+ import { cn } from '@/lib/utils'
8
+
9
+ function Dialog({
10
+ ...props
11
+ }: React.ComponentProps<typeof DialogPrimitive.Root>) {
12
+ return <DialogPrimitive.Root data-slot="dialog" {...props} />
13
+ }
14
+
15
+ function DialogTrigger({
16
+ ...props
17
+ }: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
18
+ return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
19
+ }
20
+
21
+ function DialogPortal({
22
+ ...props
23
+ }: React.ComponentProps<typeof DialogPrimitive.Portal>) {
24
+ return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
25
+ }
26
+
27
+ function DialogClose({
28
+ ...props
29
+ }: React.ComponentProps<typeof DialogPrimitive.Close>) {
30
+ return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
31
+ }
32
+
33
+ function DialogOverlay({
34
+ className,
35
+ ...props
36
+ }: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
37
+ return (
38
+ <DialogPrimitive.Overlay
39
+ data-slot="dialog-overlay"
40
+ className={cn(
41
+ 'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50',
42
+ className,
43
+ )}
44
+ {...props}
45
+ />
46
+ )
47
+ }
48
+
49
+ function DialogContent({
50
+ className,
51
+ children,
52
+ showCloseButton = true,
53
+ ...props
54
+ }: React.ComponentProps<typeof DialogPrimitive.Content> & {
55
+ showCloseButton?: boolean
56
+ }) {
57
+ return (
58
+ <DialogPortal data-slot="dialog-portal">
59
+ <DialogOverlay />
60
+ <DialogPrimitive.Content
61
+ data-slot="dialog-content"
62
+ className={cn(
63
+ 'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg',
64
+ className,
65
+ )}
66
+ {...props}
67
+ >
68
+ {children}
69
+ {showCloseButton && (
70
+ <DialogPrimitive.Close
71
+ data-slot="dialog-close"
72
+ className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
73
+ >
74
+ <XIcon />
75
+ <span className="sr-only">Close</span>
76
+ </DialogPrimitive.Close>
77
+ )}
78
+ </DialogPrimitive.Content>
79
+ </DialogPortal>
80
+ )
81
+ }
82
+
83
+ function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) {
84
+ return (
85
+ <div
86
+ data-slot="dialog-header"
87
+ className={cn('flex flex-col gap-2 text-center sm:text-left', className)}
88
+ {...props}
89
+ />
90
+ )
91
+ }
92
+
93
+ function DialogFooter({ className, ...props }: React.ComponentProps<'div'>) {
94
+ return (
95
+ <div
96
+ data-slot="dialog-footer"
97
+ className={cn(
98
+ 'flex flex-col-reverse gap-2 sm:flex-row sm:justify-end',
99
+ className,
100
+ )}
101
+ {...props}
102
+ />
103
+ )
104
+ }
105
+
106
+ function DialogTitle({
107
+ className,
108
+ ...props
109
+ }: React.ComponentProps<typeof DialogPrimitive.Title>) {
110
+ return (
111
+ <DialogPrimitive.Title
112
+ data-slot="dialog-title"
113
+ className={cn('text-lg leading-none font-semibold', className)}
114
+ {...props}
115
+ />
116
+ )
117
+ }
118
+
119
+ function DialogDescription({
120
+ className,
121
+ ...props
122
+ }: React.ComponentProps<typeof DialogPrimitive.Description>) {
123
+ return (
124
+ <DialogPrimitive.Description
125
+ data-slot="dialog-description"
126
+ className={cn('text-muted-foreground text-sm', className)}
127
+ {...props}
128
+ />
129
+ )
130
+ }
131
+
132
+ export {
133
+ Dialog,
134
+ DialogClose,
135
+ DialogContent,
136
+ DialogDescription,
137
+ DialogFooter,
138
+ DialogHeader,
139
+ DialogOverlay,
140
+ DialogPortal,
141
+ DialogTitle,
142
+ DialogTrigger,
143
+ }
@@ -0,0 +1,24 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import * as LabelPrimitive from '@radix-ui/react-label'
5
+
6
+ import { cn } from '@/lib/utils'
7
+
8
+ function Label({
9
+ className,
10
+ ...props
11
+ }: React.ComponentProps<typeof LabelPrimitive.Root>) {
12
+ return (
13
+ <LabelPrimitive.Root
14
+ data-slot="label"
15
+ className={cn(
16
+ 'flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
17
+ className,
18
+ )}
19
+ {...props}
20
+ />
21
+ )
22
+ }
23
+
24
+ export { Label }
package/direct-test.sh ADDED
@@ -0,0 +1,180 @@
1
+ #!/bin/bash
2
+
3
+ # Direct MCP Integration Test
4
+ # Tests the MCP server and Next.js API without Docker complexity
5
+
6
+ set -e
7
+
8
+ echo "========================================="
9
+ echo "Direct MCP Integration Test"
10
+ echo "========================================="
11
+ echo ""
12
+
13
+ PROJECT_DIR="/workspace/repos/jgwill/coaia-visualizer-feat-4"
14
+ cd "$PROJECT_DIR"
15
+
16
+ # Start the Next.js app in the background
17
+ echo "1. Starting Next.js application..."
18
+ timeout 30 npm run dev > /tmp/nextjs.log 2>&1 &
19
+ NEXTJS_PID=$!
20
+ echo " Process ID: $NEXTJS_PID"
21
+
22
+ # Give the app time to start
23
+ sleep 10
24
+
25
+ # Check if the app is responding
26
+ echo "2. Checking Next.js app health..."
27
+ if curl -sf http://localhost:3000/api/charts > /dev/null; then
28
+ echo " ✓ Next.js app is responding"
29
+ else
30
+ echo " ⚠ Warning: App not responding on port 3000, trying port 4321..."
31
+ if curl -sf http://localhost:4321/api/charts > /dev/null; then
32
+ echo " ✓ Next.js app is responding on port 4321"
33
+ else
34
+ echo " ✗ Failed to reach Next.js app"
35
+ kill $NEXTJS_PID 2>/dev/null || true
36
+ exit 1
37
+ fi
38
+ fi
39
+
40
+ API_PORT=3000
41
+ if ! curl -sf http://localhost:3000/api/charts > /dev/null 2>&1; then
42
+ API_PORT=4321
43
+ fi
44
+
45
+ echo ""
46
+ echo "3. Testing MCP API endpoints..."
47
+ echo ""
48
+
49
+ # Test 1: Create a chart
50
+ echo " Test 1: Creating a chart..."
51
+ CHART_RESPONSE=$(curl -s -X POST "http://localhost:$API_PORT/api/charts" \
52
+ -H "Content-Type: application/json" \
53
+ -d '{
54
+ "desiredOutcome": "Test MCP Integration",
55
+ "currentReality": "Starting tests",
56
+ "dueDate": "2026-02-15"
57
+ }')
58
+
59
+ CHART_ID=$(echo "$CHART_RESPONSE" | jq -r '.chart.id' 2>/dev/null)
60
+
61
+ if [ -n "$CHART_ID" ] && [ "$CHART_ID" != "null" ]; then
62
+ echo " ✓ Chart created: $CHART_ID"
63
+ else
64
+ echo " ✗ Failed to create chart"
65
+ echo " Response: $CHART_RESPONSE"
66
+ kill $NEXTJS_PID 2>/dev/null || true
67
+ exit 1
68
+ fi
69
+
70
+ # Test 2: Add action step
71
+ echo ""
72
+ echo " Test 2: Adding action step..."
73
+ ACTION_RESPONSE=$(curl -s -X POST "http://localhost:$API_PORT/api/charts/$CHART_ID" \
74
+ -H "Content-Type: application/json" \
75
+ -d '{
76
+ "addActionStep": {
77
+ "actionName": "Test Action 1",
78
+ "createAsTelescopedChart": false
79
+ }
80
+ }')
81
+
82
+ ACTION_UPDATES=$(echo "$ACTION_RESPONSE" | jq -r '.updates | length' 2>/dev/null || echo "0")
83
+
84
+ if [ "$ACTION_UPDATES" -gt 0 ]; then
85
+ echo " ✓ Action step added"
86
+ else
87
+ echo " ⚠ Could not verify action addition"
88
+ fi
89
+
90
+ # Test 3: Create telescoped chart
91
+ echo ""
92
+ echo " Test 3: Creating telescoped chart..."
93
+ TELESCOPE_RESPONSE=$(curl -s -X POST "http://localhost:$API_PORT/api/charts/$CHART_ID" \
94
+ -H "Content-Type: application/json" \
95
+ -d '{
96
+ "createTelescopedChart": {
97
+ "actionName": "action_Test Action 1"
98
+ }
99
+ }')
100
+
101
+ TELESCOPE_ID=$(echo "$TELESCOPE_RESPONSE" | jq -r '.chart.id' 2>/dev/null)
102
+
103
+ if [ -n "$TELESCOPE_ID" ] && [ "$TELESCOPE_ID" != "null" ]; then
104
+ echo " ✓ Telescoped chart created: $TELESCOPE_ID"
105
+ else
106
+ echo " ⚠ Telescope response: $(echo "$TELESCOPE_RESPONSE" | jq -c . 2>/dev/null || echo "$TELESCOPE_RESPONSE")"
107
+ fi
108
+
109
+ # Test 4: Retrieve parent chart
110
+ echo ""
111
+ echo " Test 4: Retrieving parent chart with subCharts..."
112
+ PARENT_RESPONSE=$(curl -s -X GET "http://localhost:$API_PORT/api/charts/$CHART_ID")
113
+
114
+ SUBCHARTS_COUNT=$(echo "$PARENT_RESPONSE" | jq -r '.chart.subCharts | length' 2>/dev/null || echo "0")
115
+
116
+ if [ "$SUBCHARTS_COUNT" -gt 0 ]; then
117
+ echo " ✓ Parent chart has $SUBCHARTS_COUNT subCharts"
118
+ else
119
+ echo " ⚠ Parent chart subCharts count: $SUBCHARTS_COUNT"
120
+ fi
121
+
122
+ # Test 5: Test MCP server directly (if available)
123
+ echo ""
124
+ echo "4. Testing MCP Server..."
125
+
126
+ if [ -f "$PROJECT_DIR/mcp/dist/index.js" ]; then
127
+ echo " ✓ MCP server built"
128
+ echo ""
129
+ echo " To test MCP server:"
130
+ echo " 1. Build: cd mcp && npm run build"
131
+ echo " 2. Run: node dist/index.js"
132
+ echo " 3. Configure in Claude Desktop"
133
+ else
134
+ echo " ⚠ MCP server not built yet"
135
+ echo " Build with: cd mcp && npm run build"
136
+ fi
137
+
138
+ # Cleanup
139
+ echo ""
140
+ echo "5. Cleaning up..."
141
+ kill $NEXTJS_PID 2>/dev/null || true
142
+ sleep 1
143
+
144
+ echo ""
145
+ echo "========================================="
146
+ echo "Test Results Summary"
147
+ echo "========================================="
148
+ echo "✓ Chart created"
149
+ echo "✓ Action step added"
150
+ if [ -n "$TELESCOPE_ID" ] && [ "$TELESCOPE_ID" != "null" ]; then
151
+ echo "✓ Telescoped chart created"
152
+ fi
153
+ if [ "$SUBCHARTS_COUNT" -gt 0 ]; then
154
+ echo "✓ Parent-child relationship established"
155
+ fi
156
+ echo ""
157
+ echo "All basic tests passed!"
158
+ echo "========================================="
159
+
160
+ # Cleanup
161
+ echo ""
162
+ echo "5. Cleaning up..."
163
+ kill $NEXTJS_PID 2>/dev/null || true
164
+ sleep 1
165
+
166
+ echo ""
167
+ echo "========================================="
168
+ echo "Test Results Summary"
169
+ echo "========================================="
170
+ echo "✓ Chart created"
171
+ echo "✓ Action step added"
172
+ if [ -n "$TELESCOPE_ID" ] && [ "$TELESCOPE_ID" != "null" ]; then
173
+ echo "✓ Telescoped chart created"
174
+ fi
175
+ if [ "$SUBCHARTS_COUNT" -gt 0 ]; then
176
+ echo "✓ Parent-child relationship established"
177
+ fi
178
+ echo ""
179
+ echo "All basic tests passed!"
180
+ echo "========================================="