feat: wire explicit msglib receive source
This commit is contained in:
@@ -29,7 +29,7 @@ Remote base before local roadmap commits: `b2d839e Merge branch 'codex/n15-repor
|
||||
- `isphere_receive_messages` accepts the read-contract argument set for the current local-readonly path: `conversation_id`, `query`, `since`, `limit`, `include_attachment_metadata`, `source_preference`, `preview`, and empty `cursor`.
|
||||
- `isphere_search_contacts` and `isphere_search_groups` accept and validate the safe search-contract args for the current local-readonly path and can optionally enrich results from the env-configured C29 `MsgLib.db` display-entity reader; default behavior remains log/JID-backed when MsgLib env vars are absent.
|
||||
- `isphere_receive_files` list mode accepts and validates the safe file-list contract args for the current local-readonly path while keeping download blocked.
|
||||
- `native/MsgLibReadSidecar` provides a bounded x86 .NET read-only boundary for `MsgLib.db` schema/display-source checks, bounded contact/group display-entity reads, message-source metadata, and sidecar-level bounded message listing; `internal/msglib` wraps it from Go, C30 wires display metadata as an optional MCP contact/group enrichment source, C39 adds a Go receive-source adapter around `ListMessages`, and C40 adds tool-level explicit `msglib_readonly` source selection without changing the MCP default source.
|
||||
- `native/MsgLibReadSidecar` provides a bounded x86 .NET read-only boundary for `MsgLib.db` schema/display-source checks, bounded contact/group display-entity reads, message-source metadata, and sidecar-level bounded message listing; `internal/msglib` wraps it from Go, C30 wires display metadata as an optional MCP contact/group enrichment source, C39 adds a Go receive-source adapter around `ListMessages`, C40 adds tool-level explicit `msglib_readonly` source selection, and C41 wires it through env configuration without changing the MCP default source.
|
||||
- N12-pre and N12R documents define safe offline evidence intake and internal-sandbox live UIA capture procedures.
|
||||
- N13/N14/N15 produced selector catalog/report validation infrastructure, but those are supporting validation assets only.
|
||||
|
||||
@@ -56,7 +56,7 @@ N13/N14/N15 are pre-business validation results. They can help identify UI eleme
|
||||
|
||||
## Current loop
|
||||
|
||||
Current loop: `Stage C / Loop C41 - MsgLib receive env wiring and optional smoke`.
|
||||
Current loop: `Stage C / Loop C42 - MsgLib receive operator hardening and reconciliation precheck`.
|
||||
|
||||
Plan file: `docs/superpowers/plans/2026-07-09-stage-c-log-backed-receive-messages-loop.md`.
|
||||
|
||||
@@ -109,10 +109,11 @@ Current loop output so far:
|
||||
38. C38: added sidecar/Go-wrapper `list_messages`, clamped to `1..50`, cursor rejected, optional attachment metadata without path/download refs, and copied-DB verification with `include_body=false`; printed evidence is only count/source-table/safety booleans.
|
||||
39. C39: added `internal/isphere.MsgLibMessageSource`, an injected-provider adapter that maps `msglib.ListMessages` results into the existing receive-message domain model, preserving explicit source refs and attachment metadata while leaving MCP default PacketReader behavior unchanged.
|
||||
40. C40: added tool-level explicit receive source selection: empty/`auto`/`local_readonly` still use the primary PacketReader/log-backed source, while `source_preference="msglib_readonly"` uses a configured MsgLib receive source and is rejected when not configured.
|
||||
41. C41: wired env-configured MsgLib client into both display enrichment and explicit receive selection; optional copied-DB MCP smoke now proves `source_preference="msglib_readonly"` returns sanitized count/source evidence while standard `verify-go-mcp.ps1` remains deterministic.
|
||||
|
||||
## Next business mainline
|
||||
|
||||
1. Run Stage C Loop C41: wire the MsgLib receive adapter into server env configuration behind explicit `source_preference="msglib_readonly"` and add an optional sanitized smoke; keep standard `verify-go-mcp.ps1` deterministic.
|
||||
1. Run Stage C Loop C42: harden operator-facing MsgLib receive behavior and start reconciliation precheck between PacketReader and MsgLib rows without changing the default route.
|
||||
2. Leave `isphere_send_message` blocked until bridge/API, UIA action-chain, or protocol connector evidence is validated.
|
||||
3. Leave actual file download/cache mapping as a later connector node until a message-to-cache hash or client download source is validated.
|
||||
4. Keep UIA selector/report work as fallback support, not as the primary roadmap.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
This runbook is for the first Go MCP phase of `isphere-ai-bridge`. It lets an operator build and verify the Windows helper and the Go MCP server without reading the source code.
|
||||
|
||||
Current scope: expose four WinHelper observation operations plus four business read tools through Go MCP: `isphere_receive_messages`, `isphere_search_contacts`, `isphere_search_groups`, and `isphere_receive_files` list mode. These business read tools use an empty default message source unless `ISPHERE_PACKET_LOG_FILE` points to an operator-local encrypted PacketReader log-line file or `ISPHERE_PACKET_LOG_DIR` points to an operator-local directory of encrypted PacketReader `.log`/`.txt` files. Contact/group display names can also be optionally enriched from an operator-local copied `MsgLib.db` through the bounded read-only `MsgLibReadSidecar`.
|
||||
Current scope: expose four WinHelper observation operations plus four business read tools through Go MCP: `isphere_receive_messages`, `isphere_search_contacts`, `isphere_search_groups`, and `isphere_receive_files` list mode. These business read tools use an empty default message source unless `ISPHERE_PACKET_LOG_FILE` points to an operator-local encrypted PacketReader log-line file or `ISPHERE_PACKET_LOG_DIR` points to an operator-local directory of encrypted PacketReader `.log`/`.txt` files. Contact/group display names can also be optionally enriched from an operator-local copied `MsgLib.db` through the bounded read-only `MsgLibReadSidecar`; receive messages can use copied DB rows only when the caller explicitly passes `source_preference="msglib_readonly"`.
|
||||
|
||||
## 1. Prerequisites
|
||||
|
||||
@@ -129,9 +129,9 @@ The directory loader recursively reads `.log` and `.txt` files, sorts paths dete
|
||||
|
||||
Then start the MCP server normally. The command entry point uses `ISPHERE_PACKET_LOG_FILE` when the file variable is set, otherwise `ISPHERE_PACKET_LOG_DIR` when the directory variable is set. The repeatable verification script checks three paths: default empty source, a temporary synthetic encrypted fixture file, and a temporary synthetic encrypted fixture directory.
|
||||
|
||||
## 7. Configure optional MsgLib display-name enrichment
|
||||
## 7. Configure optional MsgLib display-name enrichment and explicit DB receive
|
||||
|
||||
This is optional. It enriches contact/group display metadata; it does not turn `MsgLib.db` into the primary message source. `isphere_receive_messages` still needs a PacketReader source to return messages, and MsgLib only fills safe display fields such as `sender_name` and `conversation.display_name`.
|
||||
This is optional. It enriches contact/group display metadata and can also serve `isphere_receive_messages` from copied `MsgLib.db` rows when the caller explicitly chooses `source_preference="msglib_readonly"`. It does not turn `MsgLib.db` into the default message source: empty or `auto` source preference still uses the PacketReader/log-backed source.
|
||||
|
||||
Build the bounded x86 sidecar first:
|
||||
|
||||
@@ -157,7 +157,8 @@ Rules:
|
||||
|
||||
- Set all of `ISPHERE_MSGLIB_SIDECAR_EXE`, `ISPHERE_MSGLIB_SQLITE_DLL`, and `ISPHERE_MSGLIB_DB`, or set none of them. Partial MsgLib config fails server startup.
|
||||
- Keep copied DB/provider files under ignored `runs/` paths or another operator-local location. Do not commit them.
|
||||
- `MsgLibReadSidecar` is read-only and bounded to schema/display metadata operations. It does not send messages, download files, mutate DB rows, log in, hook processes, or return raw rows.
|
||||
- `MsgLibReadSidecar` is read-only and bounded to schema/display/message-list operations. It does not send messages, download files, mutate DB rows, log in, hook processes, or return raw rows.
|
||||
- `source_preference="msglib_readonly"` is required to read messages from copied `MsgLib.db`; `auto` and empty source preference remain PacketReader/log-backed.
|
||||
- Standard `scripts\verify-go-mcp.ps1` intentionally clears MsgLib variables to keep the default smoke deterministic.
|
||||
|
||||
Verify the optional provider path:
|
||||
@@ -170,7 +171,7 @@ powershell -NoProfile -ExecutionPolicy Bypass -File scripts\verify-msglib-sideca
|
||||
|
||||
Expected provider evidence contains `provider_checked=true`, a `display_source_count`, and sanitized `display_entity_summaries` with counts/source table names only.
|
||||
|
||||
Verify MCP-level optional enrichment, including receive-message display fields:
|
||||
Verify MCP-level optional enrichment, including receive-message display fields and explicit DB receive:
|
||||
|
||||
```powershell
|
||||
$env:ISPHERE_MSGLIB_SQLITE_DLL = "<path-to-copied-System.Data.SQLite.dll>"
|
||||
@@ -183,17 +184,21 @@ Expected MCP evidence is sanitized and should include:
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"db_receive_smoke": true,
|
||||
"db_receive_message_count": 5,
|
||||
"db_receive_sources": ["msglib:tblPersonMsg"],
|
||||
"receive_display_smoke": true,
|
||||
"receive_sender_name_populated": true,
|
||||
"receive_conversation_display_populated": true,
|
||||
"printed_entity_values": false,
|
||||
"message_body_values_printed": false,
|
||||
"message_bodies_returned": false,
|
||||
"file_paths_returned": false,
|
||||
"raw_rows_returned": false
|
||||
}
|
||||
```
|
||||
|
||||
The optional MCP smoke internally uses copied-DB display metadata to build a synthetic encrypted PacketReader fixture, but the printed output must stay limited to counts, booleans, and `msglib:<source_table>` refs. It must not print entity values, JIDs, message bodies, local DB paths, attachment paths, or raw rows.
|
||||
The optional MCP smoke internally uses copied-DB display metadata to build a synthetic encrypted PacketReader fixture and explicitly calls `isphere_receive_messages` with `source_preference="msglib_readonly"`. The printed output must stay limited to counts, booleans, and `msglib:<source_table>` refs. It must not print entity values, JIDs, message body values, local DB paths, attachment paths, or raw rows.
|
||||
|
||||
## 8. Configure MCP client command
|
||||
|
||||
|
||||
@@ -307,6 +307,13 @@ Committed C40 implementation:
|
||||
- `source_preference="msglib_readonly"` can select an explicitly configured MsgLib receive source and is rejected when that source is absent.
|
||||
- Server env wiring is deferred to C41, so standard MCP startup and verification remain deterministic.
|
||||
|
||||
Committed C41 implementation:
|
||||
|
||||
- Server env wiring now constructs one MsgLib sidecar client for both display enrichment and explicit receive selection when all MsgLib env vars are present.
|
||||
- `isphere_receive_messages` with `source_preference="msglib_readonly"` uses `internal/isphere.MsgLibMessageSource`; default `auto` still uses PacketReader/log-backed source.
|
||||
- `scripts/verify-msglib-mcp-enrichment.ps1` includes an optional copied-DB explicit DB receive smoke and prints only counts, `msglib:<source_table>` refs, and booleans.
|
||||
- Standard `scripts/verify-go-mcp.ps1` continues to clear MsgLib env and remains deterministic.
|
||||
|
||||
## Verification
|
||||
|
||||
Default verification builds and checks `self_check` only:
|
||||
|
||||
@@ -17,7 +17,7 @@ Schema notes: `docs/source-discovery/2026-07-09-n12-pre-zyl-schema-notes.md`
|
||||
|
||||
| MCP tool | Primary source | Fallback source | Evidence file | Decision status | Next implementation slice |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| `isphere_receive_messages` | decrypted `PacketReader.ProcessPacket` XMPP `<message>` stanzas via `internal/isphere.EncryptedPacketLogSource`; configured by `ISPHERE_PACKET_LOG_FILE` or `ISPHERE_PACKET_LOG_DIR` | copied `MsgLib.db` message tables through x86 read-only sidecar `list_messages`; decrypted `Smark.SendReceive` transport stanzas; decrypted `SaveToDB` `Chat_OnMessageArrived` traces | `docs/source-discovery/2026-07-09-n12-pre-zyl-schema-notes.md#field-mapping-evidence`; `docs/source-discovery/2026-07-10-dotnet-wrapper-static-analysis.md`; `docs/source-discovery/2026-07-10-db-backed-receive-source-design.md` | C20 supports and validates the current safe receive-message contract args; C32 wires optional MsgLib display metadata into receive-message `sender_name` and `conversation.display_name`; C33 proves this path through a real copied-DB MCP smoke; C35 maps MsgLib message tables; C36 adds metadata-only `message_sources`; C37 defines bounded `list_messages` design; C38 implements sidecar/Go-wrapper `list_messages` with sanitized copied-DB verification; C39 adds a Go adapter from `ListMessages` to the receive-message domain model; C40 adds tool-level explicit `msglib_readonly` source selection while `auto` remains PacketReader/log-backed | C41 MsgLib receive env wiring and optional sanitized smoke |
|
||||
| `isphere_receive_messages` | decrypted `PacketReader.ProcessPacket` XMPP `<message>` stanzas via `internal/isphere.EncryptedPacketLogSource`; configured by `ISPHERE_PACKET_LOG_FILE` or `ISPHERE_PACKET_LOG_DIR` | copied `MsgLib.db` message tables through x86 read-only sidecar `list_messages` selected explicitly by `source_preference="msglib_readonly"`; decrypted `Smark.SendReceive` transport stanzas; decrypted `SaveToDB` `Chat_OnMessageArrived` traces | `docs/source-discovery/2026-07-09-n12-pre-zyl-schema-notes.md#field-mapping-evidence`; `docs/source-discovery/2026-07-10-dotnet-wrapper-static-analysis.md`; `docs/source-discovery/2026-07-10-db-backed-receive-source-design.md` | C20 supports and validates the current safe receive-message contract args; C32 wires optional MsgLib display metadata into receive-message `sender_name` and `conversation.display_name`; C33 proves this path through a real copied-DB MCP smoke; C35 maps MsgLib message tables; C36 adds metadata-only `message_sources`; C37 defines bounded `list_messages` design; C38 implements sidecar/Go-wrapper `list_messages` with sanitized copied-DB verification; C39 adds a Go adapter from `ListMessages` to the receive-message domain model; C40 adds tool-level explicit `msglib_readonly` source selection; C41 wires env-configured MsgLib receive source and optional copied-DB smoke while `auto` remains PacketReader/log-backed | C42 operator hardening and PacketReader-vs-MsgLib reconciliation precheck |
|
||||
| `isphere_search_contacts` | bare sender/receiver JIDs extracted from normalized log-backed messages loaded from configured PacketReader file or directory source | validated copied `MsgLib.db` tables: `TD_Roster`, `TD_CustomEffigy`, `tblRecent`, `tblChatLevel`, `tblPersonMsg`; decrypted roster/contact stanzas from `Smark.SendReceive`; UIA helper source if display names are still missing | `docs/source-discovery/2026-07-10-msglib-readonly-schema-extraction.md` | C34 documents how to enable optional MsgLib contact display enrichment and verify sanitized MCP output; search enrichment remains available | C37 bounded DB-backed receive-source design |
|
||||
| `isphere_search_groups` | decrypted `PacketReader.ProcessPacket` `type="groupchat"` stanzas and conference/MUC JIDs loaded from configured PacketReader file or directory source | validated copied `MsgLib.db` tables: `tblMsgGroupPersonMsg`, `TD_WorkGroupAuth`, `tblRecent`; UIA helper source if group display names are still missing | `docs/source-discovery/2026-07-10-msglib-readonly-schema-extraction.md`; C9 ignored-log scan: PacketReader 86 groupchat/86 conference hits; Smark 24 groupchat/658 conference/631 muc hits | C34 documents how to enable optional MsgLib group display enrichment and verify sanitized MCP output; search enrichment remains available | C37 bounded DB-backed receive-source design |
|
||||
| `isphere_receive_files` | decrypted `PacketReader.ProcessPacket` file-transfer message stanzas via `internal/isphere.ListFilesFromMessages` and `isphere_receive_files` list mode; configured PacketReader file or directory source; `zyl\importal\<hash>` cache entries remain unlinked | decrypted `Smark.SendReceive` and `SaveToDB` traces for file-reference reconciliation; future UIA/client connector for download | `docs/source-discovery/2026-07-09-n12-pre-zyl-schema-notes.md#c12-file-transfer-evidence-precheck` | C22 verifies safe file-list contract args; no log-to-cache/download mapping yet | defer download mapping; precheck contact/group display-name evidence |
|
||||
@@ -39,4 +39,4 @@ Start Stage C with a narrow log-backed `isphere_receive_messages` source abstrac
|
||||
|
||||
## Deferred source work
|
||||
|
||||
`MsgLib.db` has a validated copied read-only open path through the bundled 32-bit `System.Data.SQLite.dll` and password `123`. C27 adds `MsgLibReadSidecar` as the bounded x86 .NET reader boundary, C28 adds the Go `internal/msglib` process client, C29 adds bounded `display_entities` extraction, C30 wires it as optional contact/group MCP enrichment, C31 proves real copied-DB MCP enrichment with sanitized output, C32 reuses it for receive-message display fields, C33 proves that receive display path through a real copied-DB MCP smoke without printing entity values, C34 documents the operator setup/verification path, C35 maps MsgLib message tables to receive-message contract fields without reading row values, C36 adds metadata-only `message_sources` readiness, C37 defines the bounded DB-backed receive-source design, C38 implements sidecar/Go-wrapper `list_messages` with sanitized verification, C39 adds a Go adapter from MsgLib list results to the receive-message domain model, and C40 adds explicit tool-level `msglib_readonly` source selection. The next step is env wiring and optional smoke; do not silently merge MsgLib rows into the MCP default.
|
||||
`MsgLib.db` has a validated copied read-only open path through the bundled 32-bit `System.Data.SQLite.dll` and password `123`. C27 adds `MsgLibReadSidecar` as the bounded x86 .NET reader boundary, C28 adds the Go `internal/msglib` process client, C29 adds bounded `display_entities` extraction, C30 wires it as optional contact/group MCP enrichment, C31 proves real copied-DB MCP enrichment with sanitized output, C32 reuses it for receive-message display fields, C33 proves that receive display path through a real copied-DB MCP smoke without printing entity values, C34 documents the operator setup/verification path, C35 maps MsgLib message tables to receive-message contract fields without reading row values, C36 adds metadata-only `message_sources` readiness, C37 defines the bounded DB-backed receive-source design, C38 implements sidecar/Go-wrapper `list_messages` with sanitized verification, C39 adds a Go adapter from MsgLib list results to the receive-message domain model, C40 adds explicit tool-level `msglib_readonly` source selection, and C41 wires env-configured explicit MsgLib receive with optional sanitized smoke. The next step is operator hardening and reconciliation precheck; do not silently merge MsgLib rows into the MCP default.
|
||||
|
||||
@@ -2804,17 +2804,57 @@ Explicit MsgLib receive source selection result:
|
||||
- Standard `scripts/verify-go-mcp.ps1` remains deterministic and should still report `msglib_configured=false`.
|
||||
- Optional copied-DB smoke must print only counts/source refs/booleans and must not print message body values, file paths, raw rows, entity values, sends, downloads, hooks, injection, or DB writes.
|
||||
|
||||
- [ ] **Step 1: Write server wiring RED test**
|
||||
- [x] **Step 1: Write server wiring RED test**
|
||||
|
||||
Use injected/fake sources or env-shaped construction to prove the server can register both primary and MsgLib receive sources while default calls stay on primary.
|
||||
Added `TestNewServerFromEnvWiresExplicitMsgLibReceiveSource` with a fake MsgLib client factory. RED failed because `newMsgLibClient`/`msgLibClient` did not exist.
|
||||
|
||||
- [ ] **Step 2: Implement env wiring**
|
||||
- [x] **Step 2: Implement env wiring**
|
||||
|
||||
Reuse `msglib.ConfigFromEnv`/client construction and pass `isphere.MsgLibMessageSource` into the C40 selector. Keep partial-env failure behavior.
|
||||
Added `msgLibSourcesFromEnv`, a replaceable `newMsgLibClient` factory, and `NewServerWithSourcesAndMsgLibReceive`. Full MsgLib env now creates one sidecar client used for both display enrichment and explicit MsgLib receive source.
|
||||
|
||||
- [ ] **Step 3: Add optional sanitized smoke and docs**
|
||||
- [x] **Step 3: Add optional sanitized smoke and docs**
|
||||
|
||||
Extend optional verification to call explicit `msglib_readonly` only when copied DB env is present, printing sanitized result only.
|
||||
Extended `scripts/verify-msglib-mcp-enrichment.ps1` to call `isphere_receive_messages` with `source_preference="msglib_readonly"`. Output includes only DB receive count/source refs and booleans; message body values are not printed.
|
||||
|
||||
---
|
||||
|
||||
## Loop C41 Result
|
||||
|
||||
MsgLib receive env wiring result:
|
||||
|
||||
- `NewServerFromEnv` now wires env-configured MsgLib into both optional display enrichment and explicit DB receive source.
|
||||
- Default `auto` remains PacketReader/log-backed; standard `verify-go-mcp.ps1` still clears MsgLib env and reports `msglib_configured=false`.
|
||||
- Explicit `source_preference="msglib_readonly"` reaches `internal/isphere.MsgLibMessageSource`.
|
||||
- Optional copied-DB MCP smoke now proves explicit DB receive with sanitized output: count, `msglib:<source_table>` refs, and booleans only.
|
||||
|
||||
---
|
||||
|
||||
## Loop C42: MsgLib receive operator hardening and reconciliation precheck
|
||||
|
||||
**Goal:** Harden the operator-facing explicit DB receive path and prepare reconciliation between PacketReader and MsgLib rows without changing default routing.
|
||||
|
||||
**Planned files:**
|
||||
- Likely modify docs/runbook/verification only unless a test exposes a behavior gap.
|
||||
- Optionally add a focused reconciliation precheck doc under `docs/source-discovery/`.
|
||||
- Modify status/matrix/plan after implementation.
|
||||
|
||||
**Planned behavior:**
|
||||
- Keep default `auto` unchanged.
|
||||
- Make operator instructions clear: when to use PacketReader, when to use `msglib_readonly`, and what evidence is safe to print.
|
||||
- Identify the next reconciliation key candidates: message id, timestamp, sender/receiver/group ids, subject/body presence, and attachment refs.
|
||||
- Do not merge sources automatically and do not add sends/downloads.
|
||||
|
||||
- [ ] **Step 1: Audit explicit DB receive operator UX**
|
||||
|
||||
Review current tool args, runbook, and optional smoke output for confusing or unsafe wording.
|
||||
|
||||
- [ ] **Step 2: Write reconciliation precheck note**
|
||||
|
||||
Document what can and cannot be compared between PacketReader and MsgLib rows using only sanitized evidence.
|
||||
|
||||
- [ ] **Step 3: Verify docs and decide next implementation node**
|
||||
|
||||
Run full verification and choose whether C43 should add a reconciliation helper, pagination, or attachment/file mapping precheck.
|
||||
|
||||
---
|
||||
## Self-Review
|
||||
|
||||
@@ -24,6 +24,15 @@ const (
|
||||
EnvMsgLibDBKey = "ISPHERE_MSGLIB_DB"
|
||||
)
|
||||
|
||||
type msgLibClient interface {
|
||||
tools.DisplayEntitySource
|
||||
isphere.MsgLibMessageLister
|
||||
}
|
||||
|
||||
var newMsgLibClient = func(cfg msglib.Config) msgLibClient {
|
||||
return msglib.NewClient(cfg)
|
||||
}
|
||||
|
||||
func NewServer() *mcp.Server {
|
||||
return NewServerWithReceiveSource(isphere.EncryptedPacketLogSource{})
|
||||
}
|
||||
@@ -48,14 +57,14 @@ func NewServerFromEnv() (*mcp.Server, error) {
|
||||
}
|
||||
source = loaded
|
||||
}
|
||||
displaySource, err := msgLibDisplayEntitySourceFromEnv()
|
||||
displaySource, msglibReceiveSource, err := msgLibSourcesFromEnv()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewServerWithSources(source, displaySource), nil
|
||||
return NewServerWithSourcesAndMsgLibReceive(source, displaySource, msglibReceiveSource), nil
|
||||
}
|
||||
|
||||
func msgLibDisplayEntitySourceFromEnv() (tools.DisplayEntitySource, error) {
|
||||
func msgLibSourcesFromEnv() (tools.DisplayEntitySource, tools.ReceiveMessagesSource, error) {
|
||||
sidecarExe := strings.TrimSpace(os.Getenv(EnvMsgLibSidecarExeKey))
|
||||
sqliteDLL := strings.TrimSpace(os.Getenv(EnvMsgLibSQLiteDLLKey))
|
||||
dbPath := strings.TrimSpace(os.Getenv(EnvMsgLibDBKey))
|
||||
@@ -66,16 +75,21 @@ func msgLibDisplayEntitySourceFromEnv() (tools.DisplayEntitySource, error) {
|
||||
}
|
||||
}
|
||||
if configured == 0 {
|
||||
return nil, nil
|
||||
return nil, nil, nil
|
||||
}
|
||||
if configured != 3 {
|
||||
return nil, fmt.Errorf("configure MsgLib source: set all of %s, %s, and %s, or set none", EnvMsgLibSidecarExeKey, EnvMsgLibSQLiteDLLKey, EnvMsgLibDBKey)
|
||||
return nil, nil, fmt.Errorf("configure MsgLib source: set all of %s, %s, and %s, or set none", EnvMsgLibSidecarExeKey, EnvMsgLibSQLiteDLLKey, EnvMsgLibDBKey)
|
||||
}
|
||||
cfg, err := msglib.ConfigFromEnv()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("configure MsgLib source: %w", err)
|
||||
return nil, nil, fmt.Errorf("configure MsgLib source: %w", err)
|
||||
}
|
||||
return msglib.NewClient(cfg), nil
|
||||
client := newMsgLibClient(cfg)
|
||||
return client, isphere.MsgLibMessageSource{
|
||||
Lister: client,
|
||||
IncludeBody: true,
|
||||
IncludeAttachmentMetadata: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewServerWithReceiveSource(source tools.ReceiveMessagesSource) *mcp.Server {
|
||||
@@ -83,13 +97,17 @@ func NewServerWithReceiveSource(source tools.ReceiveMessagesSource) *mcp.Server
|
||||
}
|
||||
|
||||
func NewServerWithSources(source tools.ReceiveMessagesSource, displaySource tools.DisplayEntitySource) *mcp.Server {
|
||||
return NewServerWithSourcesAndMsgLibReceive(source, displaySource, nil)
|
||||
}
|
||||
|
||||
func NewServerWithSourcesAndMsgLibReceive(source tools.ReceiveMessagesSource, displaySource tools.DisplayEntitySource, msglibReceiveSource tools.ReceiveMessagesSource) *mcp.Server {
|
||||
server := mcp.NewServer(&mcp.Implementation{
|
||||
Name: ServerName,
|
||||
Title: ServerTitle,
|
||||
Version: ServerVersion,
|
||||
}, nil)
|
||||
tools.RegisterWinHelperTools(server, helperclient.Client{})
|
||||
tools.RegisterISphereReadToolsWithDisplayEntities(server, source, displaySource)
|
||||
tools.RegisterISphereReadToolsWithReceiveSources(server, source, msglibReceiveSource, displaySource)
|
||||
tools.RegisterISphereContactToolsWithDisplayEntities(server, source, displaySource)
|
||||
tools.RegisterISphereGroupToolsWithDisplayEntities(server, source, displaySource)
|
||||
tools.RegisterISphereFileTools(server, source)
|
||||
|
||||
@@ -14,6 +14,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
|
||||
"isphere-ai-bridge/internal/msglib"
|
||||
)
|
||||
|
||||
func TestNewServerConstructsMCPServer(t *testing.T) {
|
||||
@@ -77,6 +79,90 @@ func TestNewServerFromEnvRejectsPartialMsgLibConfig(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewServerFromEnvWiresExplicitMsgLibReceiveSource(t *testing.T) {
|
||||
clearMsgLibEnvForServerTest(t)
|
||||
fake := &fakeMsgLibClientForServerTest{
|
||||
listResult: msglib.ListMessagesResult{
|
||||
ReadOnly: true,
|
||||
MessageBodyValuesReturned: true,
|
||||
RawRowsReturned: false,
|
||||
FilePathsReturned: false,
|
||||
SourceTables: []string{"tblPersonMsg"},
|
||||
Messages: []msglib.ListMessage{{
|
||||
MessageID: "db-env-msg-1",
|
||||
ID: "db-env-msg-1",
|
||||
ConversationID: "db-env-conversation",
|
||||
ConversationType: "direct",
|
||||
SenderID: "db-env-sender",
|
||||
ReceiverID: "db-env-receiver",
|
||||
ContentText: "db env body",
|
||||
Timestamp: "1783423809000",
|
||||
Source: "local_readonly",
|
||||
RawRef: "msglib:tblPersonMsg",
|
||||
}},
|
||||
},
|
||||
}
|
||||
oldFactory := newMsgLibClient
|
||||
newMsgLibClient = func(msglib.Config) msgLibClient {
|
||||
return fake
|
||||
}
|
||||
defer func() { newMsgLibClient = oldFactory }()
|
||||
t.Setenv(EnvMsgLibSidecarExeKey, `C:\fixture\MsgLibReadSidecar.exe`)
|
||||
t.Setenv(EnvMsgLibSQLiteDLLKey, `C:\fixture\System.Data.SQLite.dll`)
|
||||
t.Setenv(EnvMsgLibDBKey, `C:\fixture\MsgLib.db`)
|
||||
|
||||
server, err := NewServerFromEnv()
|
||||
if err != nil {
|
||||
t.Fatalf("NewServerFromEnv returned error: %v", err)
|
||||
}
|
||||
session, cleanup := connectServerTestSession(t, server)
|
||||
defer cleanup()
|
||||
|
||||
defaultResult, err := session.CallTool(context.Background(), &mcp.CallToolParams{
|
||||
Name: "isphere_receive_messages",
|
||||
Arguments: map[string]any{"limit": 5},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("default receive call: %v", err)
|
||||
}
|
||||
if defaultResult.IsError {
|
||||
t.Fatalf("default receive returned error: %+v", defaultResult)
|
||||
}
|
||||
if len(fake.listCalls) != 0 {
|
||||
t.Fatalf("default receive called MsgLib ListMessages: %+v", fake.listCalls)
|
||||
}
|
||||
|
||||
dbResult, err := session.CallTool(context.Background(), &mcp.CallToolParams{
|
||||
Name: "isphere_receive_messages",
|
||||
Arguments: map[string]any{"source_preference": "msglib_readonly", "limit": 5},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("msglib receive call: %v", err)
|
||||
}
|
||||
if dbResult.IsError {
|
||||
t.Fatalf("msglib receive returned error: %+v", dbResult)
|
||||
}
|
||||
payload, err := json.Marshal(dbResult.StructuredContent)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal structured content: %v", err)
|
||||
}
|
||||
var decoded struct {
|
||||
Messages []struct {
|
||||
ID string `json:"id"`
|
||||
RawRef string `json:"raw_ref"`
|
||||
} `json:"messages"`
|
||||
}
|
||||
if err := json.Unmarshal(payload, &decoded); err != nil {
|
||||
t.Fatalf("decode structured content %s: %v", payload, err)
|
||||
}
|
||||
if len(decoded.Messages) != 1 || decoded.Messages[0].ID != "db-env-msg-1" || decoded.Messages[0].RawRef != "msglib:tblPersonMsg" {
|
||||
t.Fatalf("messages = %+v, want explicit MsgLib result", decoded.Messages)
|
||||
}
|
||||
if len(fake.listCalls) != 1 || !fake.listCalls[0].IncludeBody || !fake.listCalls[0].IncludeAttachmentMetadata {
|
||||
t.Fatalf("ListMessages calls = %+v", fake.listCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewServerFromEnvUsesPacketLogFile(t *testing.T) {
|
||||
clearMsgLibEnvForServerTest(t)
|
||||
plaintext := `--------------------------------------------------------------------------------------------------------------------------------------------
|
||||
@@ -197,6 +283,22 @@ func TestNewServerFromEnvUsesPacketLogDirectory(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
type fakeMsgLibClientForServerTest struct {
|
||||
displayCalls []msglib.DisplayEntitiesOptions
|
||||
listCalls []msglib.ListMessagesOptions
|
||||
listResult msglib.ListMessagesResult
|
||||
}
|
||||
|
||||
func (f *fakeMsgLibClientForServerTest) DisplayEntities(_ context.Context, opts msglib.DisplayEntitiesOptions) ([]msglib.DisplayEntity, error) {
|
||||
f.displayCalls = append(f.displayCalls, opts)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (f *fakeMsgLibClientForServerTest) ListMessages(_ context.Context, opts msglib.ListMessagesOptions) (msglib.ListMessagesResult, error) {
|
||||
f.listCalls = append(f.listCalls, opts)
|
||||
return f.listResult, nil
|
||||
}
|
||||
|
||||
func clearMsgLibEnvForServerTest(t *testing.T) {
|
||||
t.Helper()
|
||||
t.Setenv(EnvMsgLibSidecarExeKey, "")
|
||||
|
||||
@@ -67,6 +67,12 @@ type receiveDisplaySummary struct {
|
||||
ConversationDisplayPopulated bool
|
||||
}
|
||||
|
||||
type msgLibReceiveSummary struct {
|
||||
MessageCount int
|
||||
Sources []string
|
||||
BodyValuesPresent bool
|
||||
}
|
||||
|
||||
func main() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
@@ -97,6 +103,9 @@ func main() {
|
||||
if contacts.FirstID == "" || contacts.FirstDisplayName == "" { fail("contacts", fmt.Errorf("contact display fixture unavailable")) }
|
||||
if groups.FirstID == "" || groups.FirstDisplayName == "" { fail("groups", fmt.Errorf("group display fixture unavailable")) }
|
||||
|
||||
dbReceive := verifyMsgLibReceive(ctx, session)
|
||||
if dbReceive.MessageCount == 0 { fail("msglib_receive", fmt.Errorf("explicit msglib_readonly receive returned no messages")) }
|
||||
|
||||
receiveDisplay := verifyReceiveDisplay(ctx, contacts, groups)
|
||||
if receiveDisplay.MessageCount == 0 { fail("receive_display", fmt.Errorf("receive message fixture returned no messages")) }
|
||||
if !receiveDisplay.SenderNamePopulated { fail("receive_display", fmt.Errorf("sender display was not populated")) }
|
||||
@@ -109,11 +118,16 @@ func main() {
|
||||
"contact_sources": contacts.Sources,
|
||||
"group_count": groups.Count,
|
||||
"group_sources": groups.Sources,
|
||||
"db_receive_smoke": true,
|
||||
"db_receive_message_count": dbReceive.MessageCount,
|
||||
"db_receive_sources": dbReceive.Sources,
|
||||
"db_receive_body_values_present": dbReceive.BodyValuesPresent,
|
||||
"receive_display_smoke": true,
|
||||
"receive_message_count": receiveDisplay.MessageCount,
|
||||
"receive_sender_name_populated": receiveDisplay.SenderNamePopulated,
|
||||
"receive_conversation_display_populated": receiveDisplay.ConversationDisplayPopulated,
|
||||
"printed_entity_values": false,
|
||||
"message_body_values_printed": false,
|
||||
"message_bodies_returned": false,
|
||||
"file_paths_returned": false,
|
||||
"raw_rows_returned": false,
|
||||
@@ -164,6 +178,41 @@ func callSearch(ctx context.Context, session *mcp.ClientSession, tool string, fi
|
||||
return out
|
||||
}
|
||||
|
||||
func verifyMsgLibReceive(ctx context.Context, session *mcp.ClientSession) msgLibReceiveSummary {
|
||||
result, err := session.CallTool(ctx, &mcp.CallToolParams{Name: "isphere_receive_messages", Arguments: map[string]any{
|
||||
"source_preference": "msglib_readonly",
|
||||
"preview": true,
|
||||
"cursor": "",
|
||||
"include_attachment_metadata": false,
|
||||
"limit": 5,
|
||||
}})
|
||||
if err != nil { fail("msglib_receive_call", err) }
|
||||
if result.IsError { fail("msglib_receive_call", fmt.Errorf("tool returned isError=true")) }
|
||||
payload, _ := json.Marshal(result.StructuredContent)
|
||||
var decoded map[string]any
|
||||
if err := json.Unmarshal(payload, &decoded); err != nil { fail("msglib_receive_decode", err) }
|
||||
messages, ok := decoded["messages"].([]any)
|
||||
if !ok { fail("msglib_receive_shape", fmt.Errorf("messages missing or not array")) }
|
||||
sourceSet := map[string]bool{}
|
||||
out := msgLibReceiveSummary{MessageCount: len(messages)}
|
||||
for _, value := range messages {
|
||||
message, ok := value.(map[string]any)
|
||||
if !ok { continue }
|
||||
rawRef, _ := message["raw_ref"].(string)
|
||||
if strings.HasPrefix(rawRef, "msglib:") { sourceSet[rawRef] = true }
|
||||
text, _ := message["text"].(string)
|
||||
contentText, _ := message["content_text"].(string)
|
||||
if strings.TrimSpace(text) != "" || strings.TrimSpace(contentText) != "" {
|
||||
out.BodyValuesPresent = true
|
||||
}
|
||||
}
|
||||
sources := make([]string, 0, len(sourceSet))
|
||||
for source := range sourceSet { sources = append(sources, source) }
|
||||
sort.Strings(sources)
|
||||
out.Sources = sources
|
||||
return out
|
||||
}
|
||||
|
||||
func verifyReceiveDisplay(ctx context.Context, contact summary, group summary) receiveDisplaySummary {
|
||||
groupPlaintext := packetFixturePlaintext("c33-group-display-fixture", group.FirstID+"/imp_pc_4.1.2.6842", contact.FirstID, "groupchat", "redacted-group-body")
|
||||
directPlaintext := packetFixturePlaintext("c33-contact-display-fixture", contact.FirstID+"/imp_pc_4.1.2.6842", "receiver-redacted@imopenfire1-lanzhou", "chat", "redacted-contact-body")
|
||||
@@ -318,6 +367,9 @@ func fail(step string, err error) {
|
||||
Assert-True ($json.printed_entity_values -eq $false) "harness must not print entity values"
|
||||
Assert-True ($json.contact_count -gt 0) "contact_count should be > 0"
|
||||
Assert-True ($json.group_count -gt 0) "group_count should be > 0"
|
||||
Assert-True (($json.PSObject.Properties.Name -contains "db_receive_smoke") -and $json.db_receive_smoke -eq $true) "db receive smoke should be true"
|
||||
Assert-True (($json.PSObject.Properties.Name -contains "db_receive_message_count") -and $json.db_receive_message_count -gt 0) "db_receive_message_count should be > 0"
|
||||
Assert-True (($json.PSObject.Properties.Name -contains "message_body_values_printed") -and $json.message_body_values_printed -eq $false) "message body values must not be printed"
|
||||
Assert-True (($json.PSObject.Properties.Name -contains "receive_display_smoke") -and $json.receive_display_smoke -eq $true) "receive display smoke should be true"
|
||||
Assert-True (($json.PSObject.Properties.Name -contains "receive_sender_name_populated") -and $json.receive_sender_name_populated -eq $true) "receive sender_name should be populated"
|
||||
Assert-True (($json.PSObject.Properties.Name -contains "receive_conversation_display_populated") -and $json.receive_conversation_display_populated -eq $true) "receive conversation.display_name should be populated"
|
||||
|
||||
Reference in New Issue
Block a user