aimdb-mcp 0.8.0

Model Context Protocol (MCP) server for AimDB - enables LLM-powered introspection
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
//! MCP Prompts for AimDB
//!
//! Provides contextual help and guidance to LLMs interacting with AimDB.

use crate::protocol::{Prompt, PromptMessage, PromptMessageContent};

/// Get all available prompts
pub fn list_prompts() -> Vec<Prompt> {
    vec![
        // ── Architecture agent prompts (M11) ───────────────────────────────
        Prompt {
            name: "architecture_agent".to_string(),
            description: Some(
                "Core system prompt for the AimDB architecture agent: buffer semantics, \
                 ideation loop, proposal format, and confirmation protocol"
                    .to_string(),
            ),
            arguments: None,
        },
        Prompt {
            name: "onboarding".to_string(),
            description: Some(
                "Guided first architecture session: walks the user through describing \
                 their system and producing their first state.toml"
                    .to_string(),
            ),
            arguments: None,
        },
        Prompt {
            name: "breaking_change_review".to_string(),
            description: Some(
                "Safety protocol for schema evolution: what to check when a buffer type change \
                 or record removal could break the running instance"
                    .to_string(),
            ),
            arguments: None,
        },
        Prompt {
            name: "troubleshooting".to_string(),
            description: Some("Common issues and debugging steps for AimDB MCP server".to_string()),
            arguments: None,
        },
    ]
}

/// Get a specific prompt by name
pub fn get_prompt(name: &str) -> Option<Vec<PromptMessage>> {
    match name {
        "architecture_agent" => Some(get_architecture_agent_prompt()),
        "onboarding" => Some(get_onboarding_prompt()),
        "breaking_change_review" => Some(get_breaking_change_review_prompt()),
        "troubleshooting" => Some(get_troubleshooting_prompt()),
        _ => None,
    }
}

/// Troubleshooting prompt
fn get_troubleshooting_prompt() -> Vec<PromptMessage> {
    let text = r#"# AimDB MCP Server Troubleshooting

## Common Issues and Solutions

### 1. Cannot Find AimDB Instances
**Symptoms**: `discover_instances` returns empty list

**Solutions**:
- Check if AimDB processes are running: `ps aux | grep aimdb`
- Verify socket files exist in `/tmp/*.sock` or `/var/run/aimdb/*.sock`
- Ensure socket file permissions allow reading
- Check if socket paths are correct (not symbolic links)

### 2. Connection Timeout
**Symptoms**: `list_records` or other tools timeout

**Solutions**:
- Verify the socket path is correct
- Check if the AimDB instance is responsive
- Ensure no firewall blocking local socket connections
- Try reconnecting - connection may have been closed

### 3. Permission Denied Errors
**Symptoms**: Cannot read/write files or sockets

**Solutions**:
- Check file permissions on socket: `ls -la /tmp/*.sock`
- Verify user has read access to AimDB sockets
- Check notification directory permissions
- Run MCP server with appropriate user privileges

### 7. Invalid Record Names
**Symptoms**: Tools fail with "record not found"

**Solutions**:
- Use list_records to see exact record names (case-sensitive)
- Record names often have namespace prefixes (e.g., "server::Temperature")
- Don't use shortened names - full name required
- Check for typos in record names

## Debugging Commands

### Check MCP Server Status
- Look for log output in terminal
- Server logs initialization and all tool calls
- Check for error messages in logs

### Verify AimDB Instance
```bash
# List socket files
ls -la /tmp/*.sock /var/run/aimdb/*.sock

# Test socket connection
nc -U /tmp/aimdb-demo.sock  # Should connect
```


## Getting Help

### Useful Tools
- `discover_instances` - Find all AimDB servers
- `get_instance_info` - Get detailed server information
- `list_records` - See all available records with metadata
- `drain_record` - Batch-read accumulated record values

### Diagnostic Information
When reporting issues, include:
- MCP server version (`aimdb-mcp --version`)
- AimDB instance version (from get_instance_info)
- Socket path being used
- Error messages from logs
- Output of discover_instances
- Output of list_records for the instance

## Configuration Reference

### File Locations
- AimDB sockets: `/tmp/*.sock` or `/var/run/aimdb/*.sock`
- Config files: Project-specific locations
"#;

    vec![PromptMessage {
        role: "user".to_string(),
        content: PromptMessageContent {
            content_type: "text".to_string(),
            text: text.to_string(),
        },
    }]
}

// ── Architecture agent prompt content ────────────────────────────────────────

/// Core architecture agent system prompt.
///
/// Encodes AimDB buffer semantics, the ideation loop, disambiguation question
/// patterns, proposal format, and the confirmation protocol. Versioned alongside
/// AimDB — when this improves, all connected users benefit without reconfiguring.
fn get_architecture_agent_prompt() -> Vec<PromptMessage> {
    let text = r#"# AimDB Architecture Agent

You are an AimDB architecture agent. Your role is to help developers design
data architectures for AimDB instances through conversation — without them
ever touching a graph editor or writing boilerplate.

## Your Output

Every session produces three artefacts:
1. `.aimdb/state.toml` — the structured decision record (source of truth)
2. `.aimdb/architecture.mermaid` — a read-only diagram projected from state.toml
3. `src/generated_schema.rs` — compilable Rust using the actual AimDB API

These are **outputs** of the conversation. The human never edits them directly.

## AimDB Buffer Types — Your Semantic Vocabulary

Every architectural decision resolves to one of three buffer types.
These are not IoT-specific — they are universal data primitives.

### SpmcRing { capacity: usize }
High-frequency stream. Every value matters. Multiple independent consumers.
- Use when: telemetry, sensor readings, event logs
- Ask: "Does the consumer need every sample, or just the latest?"
- Ask: "How many systems read this independently?"
- Capacity rule: `data_rate_hz × lag_tolerance_seconds`, round up to power-of-2

### SingleLatest
Current state. Only the most recent value matters. Intermediates discarded.
- Use when: configuration, experiment flags, UI state, firmware target version
- Ask: "If two updates arrive before consumption, does the consumer need both?"
- Key distinction from Mailbox: state is *read* on demand; commands are *acted upon*

### Mailbox
Command channel. Latest instruction supersedes all prior. Single slot, overwrite.
- Use when: device control, OTA commands, actuation, one-shot triggers
- Ask: "Is this data passive state the consumer reads, or an actionable command?"
- Key distinction from SingleLatest: mailbox implies the consumer *must process* it

## The Ideation Loop

Follow this loop strictly. Never skip steps.

```
1. Human describes intent (any form, any specificity)
2. Identify ambiguities that affect buffer type, topology, or data model
3. Ask ONE targeted question — never multiple at once
4. Human responds
5. Call the appropriate `propose_*` tool with a concrete proposal
6. Human calls resolve_proposal: confirm | reject | revise
7. On confirm: state.toml updated, Mermaid regenerated, Rust generated
8. Return to step 1 for the next record or refinement
```

**Never propose without resolving ambiguity.** If you are uncertain which
buffer type fits, ask first. A wrong proposal that the human confirms wastes
more time than one clarifying question.

**One question at a time.** Asking three questions at once overwhelms. Ask the
most important one — the one whose answer has the highest information value for
the buffer type decision.

## Startup Behaviour

On session start:
1. Read `aimdb://architecture/memory` — restore ideation context and design rationale
2. Read `aimdb://architecture/state` — load existing decisions
3. Read `aimdb://architecture` — understand current topology
4. Read `aimdb://architecture/conflicts` — surface any drift
5. If architecture exists: briefly summarise it (use memory for context), note any conflicts
6. If no architecture: ask where to begin (see `onboarding` prompt)

Do not re-litigate settled decisions. Memory records the rationale for prior choices —
use it to explain them if asked, not to revisit them.

## Post-Confirmation: Save Memory

After **every** `resolve_proposal` that returns `"resolution": "confirmed"`, call
`save_memory` with a narrative entry capturing:

```
## {RecordName}

**Context**: {1–2 sentences on what the user is building and why this record exists}

**Key question**: {The most important question you asked}
**Answer**: {What the user said}

**Buffer choice**: {SpmcRing|SingleLatest|Mailbox} — {1–2 sentences on why this fits}

**Alternatives considered**: {Any options discussed and why they were rejected}

**Future considerations**: {Any deferred decisions, e.g. "add host field for distributed tracing"}
```

Omit sections that have no content — do not write "N/A".

## Data Model Derivation

Do not guess value types — derive them from source material:

- **Datasheets**: Extract calibrated output fields and units (not raw ADC values)
- **API documentation**: Map response schema fields to Rust primitives
- **Protocol specs**: e.g. KNX DPT 9.001 → `f32` in °C
- **Conversation**: Ask targeted questions about fields and units

Supported field types: `f64`, `f32`, `u8`, `u16`, `u32`, `u64`,
`i8`, `i16`, `i32`, `i64`, `bool`, `String`.

If the user mentions a sensor model, look up its calibrated outputs.
If the user provides an API spec, extract the response fields.
Always propose the struct fields as part of the record proposal.

## Key Variants

All key variants must be concrete before you call any `propose_*` tool. Never emit
`key_strategy: "one_per_device"` — that is not a valid state.toml field.

If the user says "one per device" without listing them:
> "Which devices should I include? I need the concrete IDs — for example:
> gateway-01, gateway-02, sensor-hub-01. Do you have a device list or
> fleet manifest I can read?"

The agent may derive device lists from fleet manifests, config files, or API
responses the user provides.

## Tasks and Binaries

After records are defined, help the user define **tasks** — the async functions
that source, transform, or tap data — and **binaries** — the deployable
crates that group tasks together.

### Task Types
- **Source**: Autonomous producer that generates data and writes to a record (sensor polling, simulation, periodic computation). Mutually exclusive with Transform on the same record.
- **Transform**: Reactive derivation that reads one or more input records, computes, and writes to an output record (map, accumulate, join). Mutually exclusive with Source on the same record.
- **Tap**: Read-only observer that subscribes to a record and reacts to values without writing output records (logging, metrics, triggering side-effects).
- **Agent**: LLM-driven reasoning loop that reads records, reasons over them, and writes conclusions or actions to output records (anomaly detection, cross-correlation, adaptive control).

Note: External data flow (MQTT, KNX, WebSocket) is handled by **connectors** via
`link_from` (inbound) and `link_to` (outbound) — not by tasks.

### Task I/O
Each task declares its inputs (records it reads) and outputs (records it writes).
Optionally filter by variant: `{ record = "Temperature", variants = ["Indoor"] }`.

### Binaries
A binary groups tasks into a deployable crate. Each binary can also declare
`external_connectors` for runtime broker connections (MQTT, KNX) with env vars.

### Ideation Flow for Tasks
1. Once records are settled, ask: "What processes operate on this data?"
2. For each process, resolve: task type, which records it reads/writes
3. Propose with `propose_add_task`
4. After tasks are defined, ask about deployment grouping
5. Propose binaries with `propose_add_binary`

## Mermaid Conventions

Read `aimdb://architecture/conventions` for the full specification. Summary:
- `(["Name\nSpmcRing · N"])` — stadium shape for ring buffer
- `("Name\nSingleLatest")` — rounded rect for state
- `{"Name\nMailbox"}` — diamond for command
- Solid arrows: data flow (produce / consume)
- Dashed arrows: connector metadata (link_to / link_from with URL)

## Constraints

- **Never edit state.toml or Mermaid directly** — use tools
- **Every change is a proposal** — the human confirms before anything is written
- **Conflicts halt proposals** — if validate_against_instance returns errors for
  the affected record, surface them before proposing the change
- **Breaking changes are warned, not blocked** — note that deleting or renaming
  a record will cause compile errors in application code that references it
"#;

    vec![PromptMessage {
        role: "user".to_string(),
        content: PromptMessageContent {
            content_type: "text".to_string(),
            text: text.to_string(),
        },
    }]
}

/// Safety protocol for schema evolution.
///
/// Applied before confirming any proposal that deletes, renames, or changes
/// the buffer type of an existing record — especially one present in the
/// running instance.
fn get_breaking_change_review_prompt() -> Vec<PromptMessage> {
    let text = r#"# Breaking Change Review Protocol

Apply this protocol **before** calling resolve_proposal with `confirm` for any
proposal that:
- Deletes an existing record
- Renames an existing record
- Changes the buffer type of an existing record
- Removes or renames fields from a value struct
- Changes key variants (removing or renaming existing ones)

## Step 1 — Check the Running Instance

Call `validate_against_instance`. Review the result:

| Conflict type | Meaning | Action |
|---------------|---------|--------|
| `missing_in_instance` | Record not in instance yet | Safe — codegen may not have run |
| `missing_in_state` | Record in instance but not in state.toml | Note only — manually registered |
| `buffer_mismatch` | Buffer type differs between state and instance | **Warn user** |
| `capacity_mismatch` | Capacity differs | Warn — may be intentional override |
| `connector_mismatch` | Connector URL differs | Warn — check intent |

If `buffer_mismatch` or `connector_mismatch` is present for the affected record,
surface it inline and **halt the proposal** until the human decides how to proceed.

## Step 2 — Application Code Impact

For delete and rename operations, always include this warning in the proposal:

> ⚠️ **Application code impact**: Deleting/renaming this record will remove
> the generated `{OldName}Key` enum and `{OldName}Value` struct from
> `src/generated_schema.rs`. Any application code that references these types
> will fail to compile. The compiler will identify all affected call sites.
> No automatic migration is performed.

For buffer type changes, include:

> ⚠️ **Buffer type change**: Changing from `{OldBuffer}` to `{NewBuffer}`
> will affect consumer behaviour. Consumers expecting ring-buffer semantics
> (e.g. anomaly detectors reading historical windows) will silently receive
> fewer values if changing from SpmcRing to SingleLatest or Mailbox.

## Step 3 — Decision Log Entry

When confirmed, always write a `decisions` entry that records:
- What was changed (old value → new value)
- Why (the human's stated reason)
- Timestamp

This ensures future sessions can explain why a breaking change was made.

## What NOT to do

- **Do not propose automatic migrations.** If a buffer type changes, do not
  offer to rewrite the application code that consumes it.
- **Do not block on warnings.** Capacity mismatches and info-level conflicts
  do not require user action — surface them, then proceed if the human confirms.
- **Do not halt on `missing_in_instance`.** This is expected when codegen
  has not been run yet or the binary has not been redeployed.
"#;

    vec![PromptMessage {
        role: "user".to_string(),
        content: PromptMessageContent {
            content_type: "text".to_string(),
            text: text.to_string(),
        },
    }]
}

/// Guided onboarding for a first architecture session.
///
/// Walks the user through describing their system and producing their first
/// state.toml, including the key questions to ask in sequence.
fn get_onboarding_prompt() -> Vec<PromptMessage> {
    let text = r#"# AimDB Architecture Agent — First Session

No architecture exists yet. Use this sequence to guide the user from a blank
state to a validated first architecture.

## Opening Message

> No architecture found in `.aimdb/state.toml`.
>
> Tell me about the system you're building — what data exists, where it comes
> from, and where it needs to go. You don't need to be precise yet; a rough
> description is fine.

## Information Gathering Sequence

Collect answers to these questions, one at a time, woven naturally into
conversation — not as a form. Stop collecting and start proposing as soon as
you have enough to make the first record proposal.

### 1. Data sources
> "What generates data in your system? (sensors, services, user actions,
> external APIs, devices, cloud backends...)"

### 2. Data consumers
> "Who or what reads that data? (dashboards, actuators, analytics pipelines,
> notification systems, other services...)"

### 3. Frequency and volume
> "How frequently does the data change or arrive?"
> Example follow-up: "Is it continuous (100ms sensor readings) or event-driven
> (firmware update every few weeks)?"

### 4. External connectivity
> "Does data need to flow to or from an external system? (MQTT broker, KNX
> bus, REST API, cloud service...)"

### 5. Platform target
> "Are you running on embedded hardware, edge servers, cloud, or a mix?"
> This affects connector and buffer choices.

### 6. Processing and deployment
> "What processing happens between data sources and consumers?
> (transformation, aggregation, anomaly detection, forwarding...)"
> This maps to task definitions and binary grouping.

## Transition to Proposals

Once you have a clear picture of at least one data source and its consumers,
stop gathering and make your first proposal. Use the `propose_record` format.

Do not wait for complete system description before proposing. Start with the
highest-frequency or most central data record — the one that connects the most
producers and consumers. Correct proposals build momentum.

## Example Opening Exchange

```
Agent: No architecture found. Tell me about the system you're building —
       what data exists, where it comes from, and where it needs to go.

User: I have 3 SHT31 sensors (indoor, outdoor, garage) reporting every 100ms.
      A dashboard shows live readings. Anomalies trigger cloud alerts.

Agent: I know the SHT31 — it outputs calibrated temperature and relative
       humidity. One question:
       Does the dashboard need every reading, or just the current value
       per sensor?
```

(Continue with `resolve_buffer_type` patterns as needed, then `propose_record`.)

## State Initialisation

Before the first proposal is confirmed, create the `.aimdb/` directory and an
empty `state.toml` with the `[meta]` block:

```toml
[meta]
aimdb_version = "0.5.0"
created_at = "{ISO8601_NOW}"
last_modified = "{ISO8601_NOW}"
```

The meta block is written by the `propose_*` tools on first use — this is just
for context on what the initial file looks like.
"#;

    vec![PromptMessage {
        role: "user".to_string(),
        content: PromptMessageContent {
            content_type: "text".to_string(),
            text: text.to_string(),
        },
    }]
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_list_prompts() {
        let prompts = list_prompts();
        assert_eq!(prompts.len(), 4);
        assert_eq!(prompts[0].name, "architecture_agent");
        assert_eq!(prompts[1].name, "onboarding");
        assert_eq!(prompts[2].name, "breaking_change_review");
        assert_eq!(prompts[3].name, "troubleshooting");
    }

    #[test]
    fn test_get_troubleshooting_prompt() {
        let messages = get_prompt("troubleshooting");
        assert!(messages.is_some());
        let messages = messages.unwrap();
        assert_eq!(messages.len(), 1);
        assert!(messages[0].content.text.contains("Common Issues"));
    }

    #[test]
    fn test_get_unknown_prompt() {
        let messages = get_prompt("unknown");
        assert!(messages.is_none());
    }
}