feat: enrich search with msglib display entities
This commit is contained in:
@@ -27,9 +27,9 @@ Remote base before local roadmap commits: `b2d839e Merge branch 'codex/n15-repor
|
|||||||
- `scripts/verify-win-helper.ps1` and `scripts/verify-go-mcp.ps1` provide repeatable local checks for the helper, eight-tool MCP surface, default empty-source receive/contact/group/file calls, synthetic configured-file receive/contact/group/file smoke, and synthetic configured-directory receive/contact/group/file smoke.
|
- `scripts/verify-win-helper.ps1` and `scripts/verify-go-mcp.ps1` provide repeatable local checks for the helper, eight-tool MCP surface, default empty-source receive/contact/group/file calls, synthetic configured-file receive/contact/group/file smoke, and synthetic configured-directory receive/contact/group/file smoke.
|
||||||
- `isphere_receive_messages` now returns the contract-facing `ok`, `conversation`, `messages`, `next_cursor`, and `audit` envelope while retaining legacy parser-native message fields for compatibility.
|
- `isphere_receive_messages` now returns the contract-facing `ok`, `conversation`, `messages`, `next_cursor`, and `audit` envelope while retaining legacy parser-native message fields for compatibility.
|
||||||
- `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_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, but display names/member data are not yet enriched from `MsgLib.db` until C30 wires the C29 bounded display-entity reader into the MCP search tools.
|
- `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.
|
- `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 and bounded contact/group display-entity reads; `internal/msglib` wraps it from Go, but MCP tools are not wired to it yet.
|
- `native/MsgLibReadSidecar` provides a bounded x86 .NET read-only boundary for `MsgLib.db` schema/display-source checks and bounded contact/group display-entity reads; `internal/msglib` wraps it from Go, and C30 wires it as an optional MCP contact/group enrichment source.
|
||||||
- N12-pre and N12R documents define safe offline evidence intake and internal-sandbox live UIA capture procedures.
|
- 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.
|
- 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
|
||||||
|
|
||||||
Current loop: `Stage C / Loop C30 - integrate MsgLib display entities into contact/group search`.
|
Current loop: `Stage C / Loop C31 - optional real MsgLib MCP enrichment smoke`.
|
||||||
|
|
||||||
Plan file: `docs/superpowers/plans/2026-07-09-stage-c-log-backed-receive-messages-loop.md`.
|
Plan file: `docs/superpowers/plans/2026-07-09-stage-c-log-backed-receive-messages-loop.md`.
|
||||||
|
|
||||||
@@ -98,10 +98,11 @@ Current loop output so far:
|
|||||||
27. C27: added `native/MsgLibReadSidecar` with protocol `isphere.msglib.v1`, x86 build/verify scripts, and safe `self_check`, `schema_summary`, and `display_sources` operations; evidence-backed smoke returned 8 display-source candidates without row/message values.
|
27. C27: added `native/MsgLibReadSidecar` with protocol `isphere.msglib.v1`, x86 build/verify scripts, and safe `self_check`, `schema_summary`, and `display_sources` operations; evidence-backed smoke returned 8 display-source candidates without row/message values.
|
||||||
28. C28: added `internal/msglib` Go client wrapper for the sidecar protocol with env config, fake-sidecar unit tests, timeout handling, structured sidecar errors, `SelfCheck`, and `DisplaySources`; no MCP tool wiring or message/display row reads yet.
|
28. C28: added `internal/msglib` Go client wrapper for the sidecar protocol with env config, fake-sidecar unit tests, timeout handling, structured sidecar errors, `SelfCheck`, and `DisplaySources`; no MCP tool wiring or message/display row reads yet.
|
||||||
29. C29: added bounded `display_entities` support across the x86 sidecar, Go client, and verification script; evidence-backed verification returned sanitized contact/group counts and source-table names only, with no message bodies, file paths, raw rows, sends, writes, hooks, or injection.
|
29. C29: added bounded `display_entities` support across the x86 sidecar, Go client, and verification script; evidence-backed verification returned sanitized contact/group counts and source-table names only, with no message bodies, file paths, raw rows, sends, writes, hooks, or injection.
|
||||||
|
30. C30: wired `DisplayEntities` into `isphere_search_contacts` and `isphere_search_groups` as an optional env-configured enrichment source with fake-source tests; default MCP verification now clears MsgLib env to keep the standard smoke deterministic.
|
||||||
|
|
||||||
## Next business mainline
|
## Next business mainline
|
||||||
|
|
||||||
1. Run Stage C Loop C30: wire C29 `DisplayEntities` into `isphere_search_contacts` and `isphere_search_groups` as an env-configured optional enrichment source, with fake-sidecar tests first; keep message bodies, file paths, downloads, sends, writes, hooks, injection, and raw row dumps out of scope.
|
1. Run Stage C Loop C31: add an optional copied-DB MCP enrichment smoke/hardening path that proves contact/group enrichment through the real sidecar while printing only sanitized counts/source metadata by default.
|
||||||
2. Leave `isphere_send_message` blocked until bridge/API, UIA action-chain, or protocol connector evidence is validated.
|
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.
|
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.
|
4. Keep UIA selector/report work as fallback support, not as the primary roadmap.
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ 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 |
|
| 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` | decrypted `Smark.SendReceive` transport stanzas; decrypted `SaveToDB` `Chat_OnMessageArrived` traces; copied `MsgLib.db` through a future x86 read-only sidecar/helper | `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` | C20 supports and validates the current safe receive-message contract args; C29 adds bounded contact/group display-entity extraction; DB-backed message listing is still not implemented | Defer DB-backed message listing; first run C30 contact/group search enrichment |
|
| `isphere_receive_messages` | decrypted `PacketReader.ProcessPacket` XMPP `<message>` stanzas via `internal/isphere.EncryptedPacketLogSource`; configured by `ISPHERE_PACKET_LOG_FILE` or `ISPHERE_PACKET_LOG_DIR` | decrypted `Smark.SendReceive` transport stanzas; decrypted `SaveToDB` `Chat_OnMessageArrived` traces; copied `MsgLib.db` through a future x86 read-only sidecar/helper | `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` | C20 supports and validates the current safe receive-message contract args; C30 wires bounded contact/group display entities into search enrichment; DB-backed message listing is still not implemented | Defer DB-backed message listing; run optional real MsgLib MCP enrichment smoke |
|
||||||
| `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` | C29 verifies bounded contact display-entity extraction through the x86 sidecar and Go client; MCP search is not enriched yet | C30 wire optional MsgLib enrichment into `isphere_search_contacts` |
|
| `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` | C30 wires optional MsgLib display-entity enrichment into `isphere_search_contacts`; standard verification keeps MsgLib env cleared by default | C31 optional copied-DB MCP enrichment smoke with sanitized output |
|
||||||
| `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 | C29 verifies bounded group display-entity extraction through the x86 sidecar and Go client; MCP search is not enriched yet | C30 wire optional MsgLib enrichment into `isphere_search_groups` |
|
| `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 | C30 wires optional MsgLib display-entity enrichment into `isphere_search_groups`; standard verification keeps MsgLib env cleared by default | C31 optional copied-DB MCP enrichment smoke with sanitized output |
|
||||||
| `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 |
|
| `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 |
|
||||||
| `isphere_send_message` | none validated yet | existing bridge/API/local service preferred; UIA write connector only after internal-sandbox action evidence; network/protocol connector only after protocol discovery | `docs/source-discovery/2026-07-10-send-message-source-precheck.md` | C15 blocked: contract exists, but no committed bridge/API/local service, helper write op, UIA action chain, or protocol connector is validated | do not implement write behavior; harden existing read source configuration |
|
| `isphere_send_message` | none validated yet | existing bridge/API/local service preferred; UIA write connector only after internal-sandbox action evidence; network/protocol connector only after protocol discovery | `docs/source-discovery/2026-07-10-send-message-source-precheck.md` | C15 blocked: contract exists, but no committed bridge/API/local service, helper write op, UIA action chain, or protocol connector is validated | do not implement write behavior; harden existing read source configuration |
|
||||||
| `isphere_send_file` | none validated yet | depends on send-message connector plus outbound file/upload evidence | `docs/source-discovery/2026-07-10-send-message-source-precheck.md` | blocked behind `isphere_send_message` connector selection and file upload/source policy | defer until send-message connector is validated |
|
| `isphere_send_file` | none validated yet | depends on send-message connector plus outbound file/upload evidence | `docs/source-discovery/2026-07-10-send-message-source-precheck.md` | blocked behind `isphere_send_message` connector selection and file upload/source policy | defer until send-message connector is validated |
|
||||||
@@ -39,4 +39,4 @@ Start Stage C with a narrow log-backed `isphere_receive_messages` source abstrac
|
|||||||
|
|
||||||
## Deferred source work
|
## 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, and C29 adds bounded `display_entities` extraction. The next step is optional contact/group MCP enrichment from that source.
|
`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, and C30 wires it as optional contact/group MCP enrichment. The next step is an optional copied-DB MCP smoke with sanitized output.
|
||||||
|
|||||||
@@ -2302,7 +2302,8 @@ Bounded display entity extraction result:
|
|||||||
|
|
||||||
**Goal:** Wire the C29 bounded `DisplayEntities` source into `isphere_search_contacts` and `isphere_search_groups` as an optional env-configured enrichment path, while preserving the current log-backed fallback and keeping message bodies, file paths, downloads, sends, writes, hooks, injection, DB mutation, and raw row dumps out of scope.
|
**Goal:** Wire the C29 bounded `DisplayEntities` source into `isphere_search_contacts` and `isphere_search_groups` as an optional env-configured enrichment path, while preserving the current log-backed fallback and keeping message bodies, file paths, downloads, sends, writes, hooks, injection, DB mutation, and raw row dumps out of scope.
|
||||||
|
|
||||||
**Planned files:**
|
**Files:**
|
||||||
|
- Create: `internal/tools/display_entities.go`
|
||||||
- Modify: `internal/tools/isphere_contacts.go`
|
- Modify: `internal/tools/isphere_contacts.go`
|
||||||
- Modify: `internal/tools/isphere_contacts_test.go`
|
- Modify: `internal/tools/isphere_contacts_test.go`
|
||||||
- Modify: `internal/tools/isphere_groups.go`
|
- Modify: `internal/tools/isphere_groups.go`
|
||||||
@@ -2318,24 +2319,24 @@ Bounded display entity extraction result:
|
|||||||
- Add an injectable display-entity source interface around `internal/msglib.Client`.
|
- Add an injectable display-entity source interface around `internal/msglib.Client`.
|
||||||
- Keep default MCP behavior unchanged when `ISPHERE_MSGLIB_SIDECAR_EXE`, `ISPHERE_MSGLIB_SQLITE_DLL`, or `ISPHERE_MSGLIB_DB` are absent.
|
- Keep default MCP behavior unchanged when `ISPHERE_MSGLIB_SIDECAR_EXE`, `ISPHERE_MSGLIB_SQLITE_DLL`, or `ISPHERE_MSGLIB_DB` are absent.
|
||||||
- When all env vars are present, enrich contact/group search results from `DisplayEntities` with bounded `query` and `limit`.
|
- When all env vars are present, enrich contact/group search results from `DisplayEntities` with bounded `query` and `limit`.
|
||||||
- Normal tests use fake display-entity source data; verification script should include a synthetic/fake enrichment smoke before any real copied DB smoke.
|
- Normal tests use fake display-entity source data; `scripts/verify-go-mcp.ps1` clears MsgLib env by default to keep standard smoke deterministic.
|
||||||
- Do not expose a new MCP tool in this loop.
|
- Do not expose a new MCP tool in this loop.
|
||||||
|
|
||||||
- [ ] **Step 1: Write failing contact/group enrichment tests**
|
- [x] **Step 1: Write failing contact/group enrichment tests**
|
||||||
|
|
||||||
Add tests proving contacts/groups can consume injected display entities and still pass existing log-backed/default behavior.
|
Added fake-source tests proving `isphere_search_contacts` and `isphere_search_groups` can consume injected MsgLib display entities, pass query/limit/entity type, and return `msglib_readonly` metadata. Verified RED with missing `RegisterISphereContactToolsWithDisplayEntities` and `RegisterISphereGroupToolsWithDisplayEntities`.
|
||||||
|
|
||||||
- [ ] **Step 2: Implement optional display-entity source wiring**
|
- [x] **Step 2: Implement optional display-entity source wiring**
|
||||||
|
|
||||||
Implement source interface and env-configured `internal/msglib` client construction without changing default empty-source behavior.
|
Added `DisplayEntitySource`, mapping/merge helpers, contact/group registration functions with optional display entity source, and `NewServerFromEnv` MsgLib env wiring. Partial MsgLib env config now fails fast; default nil source remains unchanged.
|
||||||
|
|
||||||
- [ ] **Step 3: Update verification/docs**
|
- [x] **Step 3: Update verification/docs**
|
||||||
|
|
||||||
Extend `scripts/verify-go-mcp.ps1` with synthetic/fake enrichment smoke and update status/matrix/plan.
|
Updated `scripts/verify-go-mcp.ps1` to clear MsgLib env for deterministic default verification and added `msglib_configured=false` to the verification result. Updated status/matrix/plan.
|
||||||
|
|
||||||
- [ ] **Step 4: Full verification and commit**
|
- [x] **Step 4: Full verification and commit**
|
||||||
|
|
||||||
Run:
|
Run before commit:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
git diff --check
|
git diff --check
|
||||||
@@ -2346,6 +2347,48 @@ powershell -NoProfile -ExecutionPolicy Bypass -File scripts\verify-msglib-sideca
|
|||||||
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\verify-go-mcp.ps1
|
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\verify-go-mcp.ps1
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Final verification passed: `git diff --check`, `go test ./...`, explicit `go build ./cmd/isphere-mcp`, default sidecar smoke, evidence-backed sidecar smoke with sanitized `display_entity_summaries`, and `scripts/verify-go-mcp.ps1` with `msglib_configured=false`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Loop C30 Result
|
||||||
|
|
||||||
|
Optional MsgLib MCP enrichment result:
|
||||||
|
|
||||||
|
- `isphere_search_contacts` can now use injected/Env MsgLib display entities and return `msglib_readonly` contact metadata.
|
||||||
|
- `isphere_search_groups` can now use injected/Env MsgLib display entities and return `msglib_readonly` group metadata.
|
||||||
|
- Default behavior stays log/JID-backed when MsgLib env is absent.
|
||||||
|
- Standard `verify-go-mcp.ps1` stays deterministic by clearing MsgLib env; real copied-DB MCP smoke is deferred to C31 so it can be sanitized separately.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Loop C31: Optional real MsgLib MCP enrichment smoke
|
||||||
|
|
||||||
|
**Goal:** Add an optional verification path that proves `isphere_search_contacts` and `isphere_search_groups` can call the real copied-DB MsgLib sidecar through MCP, while printing only sanitized counts/source metadata by default.
|
||||||
|
|
||||||
|
**Planned files:**
|
||||||
|
- Modify: `scripts/verify-go-mcp.ps1` or add a focused optional verification script.
|
||||||
|
- Modify: `docs/current-status-card.md`
|
||||||
|
- Modify: `docs/source-discovery/capability-source-matrix.md`
|
||||||
|
- Modify: `docs/superpowers/plans/2026-07-09-stage-c-log-backed-receive-messages-loop.md`
|
||||||
|
|
||||||
|
**Planned behavior:**
|
||||||
|
- Default verification remains deterministic and does not use real MsgLib env.
|
||||||
|
- Optional verification is gated by explicit env variables and copied DB paths.
|
||||||
|
- Output only counts, source names, and booleans; do not print display names, JIDs, message bodies, file paths, or raw rows.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add optional real-MsgLib MCP smoke**
|
||||||
|
|
||||||
|
Implement opt-in smoke with sanitized summary only.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run default and optional verification**
|
||||||
|
|
||||||
|
Run default full verification and optional copied-DB smoke if paths are available.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Update docs and commit**
|
||||||
|
|
||||||
|
Record sanitized evidence and next loop.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Self-Review
|
## Self-Review
|
||||||
|
|||||||
@@ -9,15 +9,19 @@ import (
|
|||||||
|
|
||||||
"isphere-ai-bridge/internal/helperclient"
|
"isphere-ai-bridge/internal/helperclient"
|
||||||
"isphere-ai-bridge/internal/isphere"
|
"isphere-ai-bridge/internal/isphere"
|
||||||
|
"isphere-ai-bridge/internal/msglib"
|
||||||
"isphere-ai-bridge/internal/tools"
|
"isphere-ai-bridge/internal/tools"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ServerName = "isphere-ai-bridge"
|
ServerName = "isphere-ai-bridge"
|
||||||
ServerTitle = "iSphere AI Bridge"
|
ServerTitle = "iSphere AI Bridge"
|
||||||
ServerVersion = "0.1.0"
|
ServerVersion = "0.1.0"
|
||||||
EnvPacketLogFileKey = "ISPHERE_PACKET_LOG_FILE"
|
EnvPacketLogFileKey = "ISPHERE_PACKET_LOG_FILE"
|
||||||
EnvPacketLogDirKey = "ISPHERE_PACKET_LOG_DIR"
|
EnvPacketLogDirKey = "ISPHERE_PACKET_LOG_DIR"
|
||||||
|
EnvMsgLibSidecarExeKey = "ISPHERE_MSGLIB_SIDECAR_EXE"
|
||||||
|
EnvMsgLibSQLiteDLLKey = "ISPHERE_MSGLIB_SQLITE_DLL"
|
||||||
|
EnvMsgLibDBKey = "ISPHERE_MSGLIB_DB"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewServer() *mcp.Server {
|
func NewServer() *mcp.Server {
|
||||||
@@ -44,10 +48,41 @@ func NewServerFromEnv() (*mcp.Server, error) {
|
|||||||
}
|
}
|
||||||
source = loaded
|
source = loaded
|
||||||
}
|
}
|
||||||
return NewServerWithReceiveSource(source), nil
|
displaySource, err := msgLibDisplayEntitySourceFromEnv()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return NewServerWithSources(source, displaySource), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func msgLibDisplayEntitySourceFromEnv() (tools.DisplayEntitySource, error) {
|
||||||
|
sidecarExe := strings.TrimSpace(os.Getenv(EnvMsgLibSidecarExeKey))
|
||||||
|
sqliteDLL := strings.TrimSpace(os.Getenv(EnvMsgLibSQLiteDLLKey))
|
||||||
|
dbPath := strings.TrimSpace(os.Getenv(EnvMsgLibDBKey))
|
||||||
|
configured := 0
|
||||||
|
for _, value := range []string{sidecarExe, sqliteDLL, dbPath} {
|
||||||
|
if value != "" {
|
||||||
|
configured++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if configured == 0 {
|
||||||
|
return 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)
|
||||||
|
}
|
||||||
|
cfg, err := msglib.ConfigFromEnv()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("configure MsgLib source: %w", err)
|
||||||
|
}
|
||||||
|
return msglib.NewClient(cfg), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewServerWithReceiveSource(source tools.ReceiveMessagesSource) *mcp.Server {
|
func NewServerWithReceiveSource(source tools.ReceiveMessagesSource) *mcp.Server {
|
||||||
|
return NewServerWithSources(source, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewServerWithSources(source tools.ReceiveMessagesSource, displaySource tools.DisplayEntitySource) *mcp.Server {
|
||||||
server := mcp.NewServer(&mcp.Implementation{
|
server := mcp.NewServer(&mcp.Implementation{
|
||||||
Name: ServerName,
|
Name: ServerName,
|
||||||
Title: ServerTitle,
|
Title: ServerTitle,
|
||||||
@@ -55,8 +90,8 @@ func NewServerWithReceiveSource(source tools.ReceiveMessagesSource) *mcp.Server
|
|||||||
}, nil)
|
}, nil)
|
||||||
tools.RegisterWinHelperTools(server, helperclient.Client{})
|
tools.RegisterWinHelperTools(server, helperclient.Client{})
|
||||||
tools.RegisterISphereReadTools(server, source)
|
tools.RegisterISphereReadTools(server, source)
|
||||||
tools.RegisterISphereContactTools(server, source)
|
tools.RegisterISphereContactToolsWithDisplayEntities(server, source, displaySource)
|
||||||
tools.RegisterISphereGroupTools(server, source)
|
tools.RegisterISphereGroupToolsWithDisplayEntities(server, source, displaySource)
|
||||||
tools.RegisterISphereFileTools(server, source)
|
tools.RegisterISphereFileTools(server, source)
|
||||||
return server
|
return server
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -63,7 +64,21 @@ func TestNewServerRegistersN8WinHelperToolsWithoutCallingHelper(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNewServerFromEnvRejectsPartialMsgLibConfig(t *testing.T) {
|
||||||
|
clearMsgLibEnvForServerTest(t)
|
||||||
|
t.Setenv(EnvMsgLibSidecarExeKey, `C:\tools\MsgLibReadSidecar.exe`)
|
||||||
|
|
||||||
|
_, err := NewServerFromEnv()
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("NewServerFromEnv returned nil error for partial MsgLib config")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), EnvMsgLibSQLiteDLLKey) || !strings.Contains(err.Error(), EnvMsgLibDBKey) {
|
||||||
|
t.Fatalf("error = %v, want missing MsgLib env context", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestNewServerFromEnvUsesPacketLogFile(t *testing.T) {
|
func TestNewServerFromEnvUsesPacketLogFile(t *testing.T) {
|
||||||
|
clearMsgLibEnvForServerTest(t)
|
||||||
plaintext := `--------------------------------------------------------------------------------------------------------------------------------------------
|
plaintext := `--------------------------------------------------------------------------------------------------------------------------------------------
|
||||||
2026/7/7 15:30:07
|
2026/7/7 15:30:07
|
||||||
<message id="msg-env-1" from="sender@imopenfire1-lanzhou/imp_pc_4.1.2.6842" to="receiver@imopenfire1-lanzhou" type="chat">
|
<message id="msg-env-1" from="sender@imopenfire1-lanzhou/imp_pc_4.1.2.6842" to="receiver@imopenfire1-lanzhou" type="chat">
|
||||||
@@ -128,6 +143,7 @@ func TestNewServerFromEnvUsesPacketLogFile(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestNewServerFromEnvUsesPacketLogDirectory(t *testing.T) {
|
func TestNewServerFromEnvUsesPacketLogDirectory(t *testing.T) {
|
||||||
|
clearMsgLibEnvForServerTest(t)
|
||||||
plaintext := `--------------------------------------------------------------------------------------------------------------------------------------------
|
plaintext := `--------------------------------------------------------------------------------------------------------------------------------------------
|
||||||
2026/7/7 15:30:07
|
2026/7/7 15:30:07
|
||||||
<message id="msg-dir-1" from="sender@imopenfire1-lanzhou/imp_pc_4.1.2.6842" to="receiver@imopenfire1-lanzhou" type="chat">
|
<message id="msg-dir-1" from="sender@imopenfire1-lanzhou/imp_pc_4.1.2.6842" to="receiver@imopenfire1-lanzhou" type="chat">
|
||||||
@@ -181,6 +197,14 @@ func TestNewServerFromEnvUsesPacketLogDirectory(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func clearMsgLibEnvForServerTest(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
t.Setenv(EnvMsgLibSidecarExeKey, "")
|
||||||
|
t.Setenv(EnvMsgLibSQLiteDLLKey, "")
|
||||||
|
t.Setenv(EnvMsgLibDBKey, "")
|
||||||
|
t.Setenv("ISPHERE_MSGLIB_PASSWORD", "")
|
||||||
|
}
|
||||||
|
|
||||||
func writeEncryptedPacketLogDirectoryForServerTest(t *testing.T, plaintext string) string {
|
func writeEncryptedPacketLogDirectoryForServerTest(t *testing.T, plaintext string) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
|
|||||||
108
internal/tools/display_entities.go
Normal file
108
internal/tools/display_entities.go
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
package tools
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"isphere-ai-bridge/internal/isphere"
|
||||||
|
"isphere-ai-bridge/internal/msglib"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DisplayEntitySource interface {
|
||||||
|
DisplayEntities(ctx context.Context, opts msglib.DisplayEntitiesOptions) ([]msglib.DisplayEntity, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func displayEntityQueryLimit(limit int) int {
|
||||||
|
if limit <= 0 {
|
||||||
|
return msglib.DefaultDisplayEntityLimit
|
||||||
|
}
|
||||||
|
if limit > msglib.MaxDisplayEntityLimit {
|
||||||
|
return msglib.MaxDisplayEntityLimit
|
||||||
|
}
|
||||||
|
return limit
|
||||||
|
}
|
||||||
|
|
||||||
|
func contactsFromDisplayEntities(entities []msglib.DisplayEntity) []isphere.Contact {
|
||||||
|
contacts := make([]isphere.Contact, 0, len(entities))
|
||||||
|
for _, entity := range entities {
|
||||||
|
id := strings.TrimSpace(entity.JID)
|
||||||
|
name := strings.TrimSpace(entity.DisplayName)
|
||||||
|
if id == "" {
|
||||||
|
id = name
|
||||||
|
}
|
||||||
|
if name == "" {
|
||||||
|
name = id
|
||||||
|
}
|
||||||
|
if id == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
contacts = append(contacts, isphere.Contact{
|
||||||
|
ContactID: id,
|
||||||
|
DisplayName: name,
|
||||||
|
Account: strings.TrimSpace(entity.JID),
|
||||||
|
Source: "msglib_readonly",
|
||||||
|
Confidence: entity.Confidence,
|
||||||
|
RawRef: "msglib:" + entity.SourceTable,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return contacts
|
||||||
|
}
|
||||||
|
|
||||||
|
func groupsFromDisplayEntities(entities []msglib.DisplayEntity) []isphere.Group {
|
||||||
|
groups := make([]isphere.Group, 0, len(entities))
|
||||||
|
for _, entity := range entities {
|
||||||
|
id := strings.TrimSpace(entity.JID)
|
||||||
|
name := strings.TrimSpace(entity.DisplayName)
|
||||||
|
if id == "" {
|
||||||
|
id = name
|
||||||
|
}
|
||||||
|
if name == "" {
|
||||||
|
name = id
|
||||||
|
}
|
||||||
|
if id == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
groups = append(groups, isphere.Group{
|
||||||
|
GroupID: id,
|
||||||
|
DisplayName: name,
|
||||||
|
Source: "msglib_readonly",
|
||||||
|
Confidence: entity.Confidence,
|
||||||
|
RawRef: "msglib:" + entity.SourceTable,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return groups
|
||||||
|
}
|
||||||
|
|
||||||
|
func mergeContacts(primary []isphere.Contact, fallback []isphere.Contact, limit int) isphere.SearchContactsResult {
|
||||||
|
seen := map[string]bool{}
|
||||||
|
contacts := make([]isphere.Contact, 0, len(primary)+len(fallback))
|
||||||
|
for _, contact := range append(primary, fallback...) {
|
||||||
|
id := strings.TrimSpace(contact.ContactID)
|
||||||
|
if id == "" || seen[strings.ToLower(id)] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[strings.ToLower(id)] = true
|
||||||
|
contacts = append(contacts, contact)
|
||||||
|
if limit > 0 && len(contacts) >= limit {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return isphere.SearchContactsResult{Contacts: contacts}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mergeGroups(primary []isphere.Group, fallback []isphere.Group, limit int) isphere.SearchGroupsResult {
|
||||||
|
seen := map[string]bool{}
|
||||||
|
groups := make([]isphere.Group, 0, len(primary)+len(fallback))
|
||||||
|
for _, group := range append(primary, fallback...) {
|
||||||
|
id := strings.TrimSpace(group.GroupID)
|
||||||
|
if id == "" || seen[strings.ToLower(id)] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[strings.ToLower(id)] = true
|
||||||
|
groups = append(groups, group)
|
||||||
|
if limit > 0 && len(groups) >= limit {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return isphere.SearchGroupsResult{Groups: groups}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||||
|
|
||||||
"isphere-ai-bridge/internal/isphere"
|
"isphere-ai-bridge/internal/isphere"
|
||||||
|
"isphere-ai-bridge/internal/msglib"
|
||||||
)
|
)
|
||||||
|
|
||||||
const ToolNameSearchContacts = "isphere_search_contacts"
|
const ToolNameSearchContacts = "isphere_search_contacts"
|
||||||
@@ -20,6 +21,10 @@ type SearchContactsArgs struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func RegisterISphereContactTools(server *mcp.Server, source ReceiveMessagesSource) {
|
func RegisterISphereContactTools(server *mcp.Server, source ReceiveMessagesSource) {
|
||||||
|
RegisterISphereContactToolsWithDisplayEntities(server, source, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func RegisterISphereContactToolsWithDisplayEntities(server *mcp.Server, source ReceiveMessagesSource, displaySource DisplayEntitySource) {
|
||||||
if source == nil {
|
if source == nil {
|
||||||
source = isphere.EncryptedPacketLogSource{}
|
source = isphere.EncryptedPacketLogSource{}
|
||||||
}
|
}
|
||||||
@@ -36,7 +41,19 @@ func RegisterISphereContactTools(server *mcp.Server, source ReceiveMessagesSourc
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
contacts := isphere.SearchContactsFromMessages(messages.Messages, isphere.SearchContactsQuery{Query: input.Query, Limit: input.Limit})
|
logContacts := isphere.SearchContactsFromMessages(messages.Messages, isphere.SearchContactsQuery{Query: input.Query, Limit: input.Limit})
|
||||||
|
contacts := logContacts
|
||||||
|
if displaySource != nil {
|
||||||
|
entities, err := displaySource.DisplayEntities(ctx, msglib.DisplayEntitiesOptions{
|
||||||
|
EntityType: msglib.EntityTypeContacts,
|
||||||
|
Query: input.Query,
|
||||||
|
Limit: displayEntityQueryLimit(input.Limit),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
contacts = mergeContacts(contactsFromDisplayEntities(entities), logContacts.Contacts, input.Limit)
|
||||||
|
}
|
||||||
return nil, searchContactsResultToMap(contacts, started, time.Now().UTC()), nil
|
return nil, searchContactsResultToMap(contacts, started, time.Now().UTC()), nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||||
|
|
||||||
"isphere-ai-bridge/internal/isphere"
|
"isphere-ai-bridge/internal/isphere"
|
||||||
|
"isphere-ai-bridge/internal/msglib"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestISphereSearchContactsToolReturnsJIDContacts(t *testing.T) {
|
func TestISphereSearchContactsToolReturnsJIDContacts(t *testing.T) {
|
||||||
@@ -75,6 +76,61 @@ func TestISphereSearchContactsToolReturnsJIDContacts(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestISphereSearchContactsToolUsesInjectedDisplayEntities(t *testing.T) {
|
||||||
|
fakeMessages := &fakeReceiveMessagesSource{}
|
||||||
|
fakeDisplay := &fakeDisplayEntitySource{entities: []msglib.DisplayEntity{{
|
||||||
|
EntityType: msglib.EntityTypeContacts,
|
||||||
|
SourceTable: "TD_CustomEffigy",
|
||||||
|
JID: "alice@example",
|
||||||
|
DisplayName: "Alice Zhang",
|
||||||
|
Confidence: 0.9,
|
||||||
|
MatchedColumns: []string{"PersonJid", "PersonName"},
|
||||||
|
}}}
|
||||||
|
session, cleanup := connectToolsTestSession(t, func(server *mcp.Server) {
|
||||||
|
RegisterISphereContactToolsWithDisplayEntities(server, fakeMessages, fakeDisplay)
|
||||||
|
})
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
callResult, err := session.CallTool(context.Background(), &mcp.CallToolParams{
|
||||||
|
Name: ToolNameSearchContacts,
|
||||||
|
Arguments: map[string]any{"query": "alice", "limit": 5},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("call %s: %v", ToolNameSearchContacts, err)
|
||||||
|
}
|
||||||
|
if callResult.IsError {
|
||||||
|
t.Fatalf("call result is error: %+v", callResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
var decoded struct {
|
||||||
|
Contacts []struct {
|
||||||
|
ContactID string `json:"contact_id"`
|
||||||
|
DisplayName string `json:"display_name"`
|
||||||
|
Account string `json:"account"`
|
||||||
|
Source string `json:"source"`
|
||||||
|
Confidence float64 `json:"confidence"`
|
||||||
|
RawRef string `json:"raw_ref"`
|
||||||
|
} `json:"contacts"`
|
||||||
|
}
|
||||||
|
payload, _ := json.Marshal(callResult.StructuredContent)
|
||||||
|
if err := json.Unmarshal(payload, &decoded); err != nil {
|
||||||
|
t.Fatalf("decode structured content %s: %v", payload, err)
|
||||||
|
}
|
||||||
|
if len(decoded.Contacts) != 1 {
|
||||||
|
t.Fatalf("contacts = %+v, want one", decoded.Contacts)
|
||||||
|
}
|
||||||
|
contact := decoded.Contacts[0]
|
||||||
|
if contact.ContactID != "alice@example" || contact.DisplayName != "Alice Zhang" || contact.Account != "alice@example" {
|
||||||
|
t.Fatalf("unexpected MsgLib contact identity: %+v", contact)
|
||||||
|
}
|
||||||
|
if contact.Source != "msglib_readonly" || contact.Confidence != 0.9 || contact.RawRef != "msglib:TD_CustomEffigy" {
|
||||||
|
t.Fatalf("unexpected MsgLib contact metadata: %+v", contact)
|
||||||
|
}
|
||||||
|
if len(fakeDisplay.queries) != 1 || fakeDisplay.queries[0].EntityType != msglib.EntityTypeContacts || fakeDisplay.queries[0].Query != "alice" || fakeDisplay.queries[0].Limit != 5 {
|
||||||
|
t.Fatalf("display queries = %+v", fakeDisplay.queries)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestISphereSearchContactsToolValidatesContractArgs(t *testing.T) {
|
func TestISphereSearchContactsToolValidatesContractArgs(t *testing.T) {
|
||||||
fake := &fakeReceiveMessagesSource{
|
fake := &fakeReceiveMessagesSource{
|
||||||
result: isphere.ReceiveMessagesResult{Messages: []isphere.Message{{
|
result: isphere.ReceiveMessagesResult{Messages: []isphere.Message{{
|
||||||
@@ -121,3 +177,13 @@ func TestISphereSearchContactsToolValidatesContractArgs(t *testing.T) {
|
|||||||
t.Fatalf("non-empty cursor was accepted before pagination exists")
|
t.Fatalf("non-empty cursor was accepted before pagination exists")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type fakeDisplayEntitySource struct {
|
||||||
|
queries []msglib.DisplayEntitiesOptions
|
||||||
|
entities []msglib.DisplayEntity
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeDisplayEntitySource) DisplayEntities(_ context.Context, opts msglib.DisplayEntitiesOptions) ([]msglib.DisplayEntity, error) {
|
||||||
|
f.queries = append(f.queries, opts)
|
||||||
|
return f.entities, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||||
|
|
||||||
"isphere-ai-bridge/internal/isphere"
|
"isphere-ai-bridge/internal/isphere"
|
||||||
|
"isphere-ai-bridge/internal/msglib"
|
||||||
)
|
)
|
||||||
|
|
||||||
const ToolNameSearchGroups = "isphere_search_groups"
|
const ToolNameSearchGroups = "isphere_search_groups"
|
||||||
@@ -20,6 +21,10 @@ type SearchGroupsArgs struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func RegisterISphereGroupTools(server *mcp.Server, source ReceiveMessagesSource) {
|
func RegisterISphereGroupTools(server *mcp.Server, source ReceiveMessagesSource) {
|
||||||
|
RegisterISphereGroupToolsWithDisplayEntities(server, source, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func RegisterISphereGroupToolsWithDisplayEntities(server *mcp.Server, source ReceiveMessagesSource, displaySource DisplayEntitySource) {
|
||||||
if source == nil {
|
if source == nil {
|
||||||
source = isphere.EncryptedPacketLogSource{}
|
source = isphere.EncryptedPacketLogSource{}
|
||||||
}
|
}
|
||||||
@@ -36,7 +41,19 @@ func RegisterISphereGroupTools(server *mcp.Server, source ReceiveMessagesSource)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
groups := isphere.SearchGroupsFromMessages(messages.Messages, isphere.SearchGroupsQuery{Query: input.Query, Limit: input.Limit})
|
logGroups := isphere.SearchGroupsFromMessages(messages.Messages, isphere.SearchGroupsQuery{Query: input.Query, Limit: input.Limit})
|
||||||
|
groups := logGroups
|
||||||
|
if displaySource != nil {
|
||||||
|
entities, err := displaySource.DisplayEntities(ctx, msglib.DisplayEntitiesOptions{
|
||||||
|
EntityType: msglib.EntityTypeGroups,
|
||||||
|
Query: input.Query,
|
||||||
|
Limit: displayEntityQueryLimit(input.Limit),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
groups = mergeGroups(groupsFromDisplayEntities(entities), logGroups.Groups, input.Limit)
|
||||||
|
}
|
||||||
return nil, searchGroupsResultToMap(groups, started, time.Now().UTC()), nil
|
return nil, searchGroupsResultToMap(groups, started, time.Now().UTC()), nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||||
|
|
||||||
"isphere-ai-bridge/internal/isphere"
|
"isphere-ai-bridge/internal/isphere"
|
||||||
|
"isphere-ai-bridge/internal/msglib"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestISphereSearchGroupsToolReturnsJIDGroups(t *testing.T) {
|
func TestISphereSearchGroupsToolReturnsJIDGroups(t *testing.T) {
|
||||||
@@ -80,6 +81,60 @@ func TestISphereSearchGroupsToolReturnsJIDGroups(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestISphereSearchGroupsToolUsesInjectedDisplayEntities(t *testing.T) {
|
||||||
|
fakeMessages := &fakeReceiveMessagesSource{}
|
||||||
|
fakeDisplay := &fakeDisplayEntitySource{entities: []msglib.DisplayEntity{{
|
||||||
|
EntityType: msglib.EntityTypeGroups,
|
||||||
|
SourceTable: "TD_WorkGroupAuth",
|
||||||
|
JID: "project-room@conference",
|
||||||
|
DisplayName: "Project Room",
|
||||||
|
Confidence: 0.9,
|
||||||
|
MatchedColumns: []string{"GroupJID", "GroupName"},
|
||||||
|
}}}
|
||||||
|
session, cleanup := connectToolsTestSession(t, func(server *mcp.Server) {
|
||||||
|
RegisterISphereGroupToolsWithDisplayEntities(server, fakeMessages, fakeDisplay)
|
||||||
|
})
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
callResult, err := session.CallTool(context.Background(), &mcp.CallToolParams{
|
||||||
|
Name: ToolNameSearchGroups,
|
||||||
|
Arguments: map[string]any{"query": "project", "limit": 5},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("call %s: %v", ToolNameSearchGroups, err)
|
||||||
|
}
|
||||||
|
if callResult.IsError {
|
||||||
|
t.Fatalf("call result is error: %+v", callResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
var decoded struct {
|
||||||
|
Groups []struct {
|
||||||
|
GroupID string `json:"group_id"`
|
||||||
|
DisplayName string `json:"display_name"`
|
||||||
|
Source string `json:"source"`
|
||||||
|
Confidence float64 `json:"confidence"`
|
||||||
|
RawRef string `json:"raw_ref"`
|
||||||
|
} `json:"groups"`
|
||||||
|
}
|
||||||
|
payload, _ := json.Marshal(callResult.StructuredContent)
|
||||||
|
if err := json.Unmarshal(payload, &decoded); err != nil {
|
||||||
|
t.Fatalf("decode structured content %s: %v", payload, err)
|
||||||
|
}
|
||||||
|
if len(decoded.Groups) != 1 {
|
||||||
|
t.Fatalf("groups = %+v, want one", decoded.Groups)
|
||||||
|
}
|
||||||
|
group := decoded.Groups[0]
|
||||||
|
if group.GroupID != "project-room@conference" || group.DisplayName != "Project Room" {
|
||||||
|
t.Fatalf("unexpected MsgLib group identity: %+v", group)
|
||||||
|
}
|
||||||
|
if group.Source != "msglib_readonly" || group.Confidence != 0.9 || group.RawRef != "msglib:TD_WorkGroupAuth" {
|
||||||
|
t.Fatalf("unexpected MsgLib group metadata: %+v", group)
|
||||||
|
}
|
||||||
|
if len(fakeDisplay.queries) != 1 || fakeDisplay.queries[0].EntityType != msglib.EntityTypeGroups || fakeDisplay.queries[0].Query != "project" || fakeDisplay.queries[0].Limit != 5 {
|
||||||
|
t.Fatalf("display queries = %+v", fakeDisplay.queries)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestISphereSearchGroupsToolValidatesContractArgs(t *testing.T) {
|
func TestISphereSearchGroupsToolValidatesContractArgs(t *testing.T) {
|
||||||
fake := &fakeReceiveMessagesSource{
|
fake := &fakeReceiveMessagesSource{
|
||||||
result: isphere.ReceiveMessagesResult{Messages: []isphere.Message{{
|
result: isphere.ReceiveMessagesResult{Messages: []isphere.Message{{
|
||||||
|
|||||||
@@ -94,8 +94,10 @@ func main() {
|
|||||||
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
if err := os.Unsetenv("ISPHERE_PACKET_LOG_FILE"); err != nil {
|
for _, key := range []string{"ISPHERE_PACKET_LOG_FILE", "ISPHERE_PACKET_LOG_DIR", "ISPHERE_MSGLIB_SIDECAR_EXE", "ISPHERE_MSGLIB_SQLITE_DLL", "ISPHERE_MSGLIB_DB", "ISPHERE_MSGLIB_PASSWORD"} {
|
||||||
fail("server/env", err)
|
if err := os.Unsetenv(key); err != nil {
|
||||||
|
fail("server/env", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
serverTransport, clientTransport := mcp.NewInMemoryTransports()
|
serverTransport, clientTransport := mcp.NewInMemoryTransports()
|
||||||
@@ -230,6 +232,7 @@ func main() {
|
|||||||
"real_isphere_login_required": false,
|
"real_isphere_login_required": false,
|
||||||
"python_runtime_required": false,
|
"python_runtime_required": false,
|
||||||
"action_tools_present": false,
|
"action_tools_present": false,
|
||||||
|
"msglib_configured": false,
|
||||||
}
|
}
|
||||||
encoded, _ := json.Marshal(result)
|
encoded, _ := json.Marshal(result)
|
||||||
fmt.Println(string(encoded))
|
fmt.Println(string(encoded))
|
||||||
@@ -698,6 +701,7 @@ func fail(step string, err error) {
|
|||||||
python_runtime_required = $false
|
python_runtime_required = $false
|
||||||
real_isphere_login_required = $false
|
real_isphere_login_required = $false
|
||||||
action_tools_present = $false
|
action_tools_present = $false
|
||||||
|
msglib_configured = $script:harnessJson.msglib_configured
|
||||||
} | ConvertTo-Json -Depth 8 -Compress
|
} | ConvertTo-Json -Depth 8 -Compress
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
|
|||||||
Reference in New Issue
Block a user