diff --git a/docs/current-status-card.md b/docs/current-status-card.md index d32c52b..b218e8d 100644 --- a/docs/current-status-card.md +++ b/docs/current-status-card.md @@ -14,20 +14,21 @@ Remote base before local roadmap commits: `b2d839e Merge branch 'codex/n15-repor ## Current completed state - Go MCP foundation exists: `cmd/isphere-mcp`, `internal/mcpserver`, `internal/helperclient`, and `internal/tools` build a stdio MCP server. -- The current MCP surface exposes five read-oriented tools: +- The current MCP surface exposes six read-oriented tools: - `win_helper_version` - `win_helper_self_check` - `win_helper_scan_windows` - `win_helper_dump_uia` - `isphere_receive_messages` + - `isphere_search_contacts` - C# `ISphereWinHelper` exists under `native/ISphereWinHelper` and uses the `isphere.helper.v1` stdin/stdout JSON contract. -- `scripts/verify-win-helper.ps1` and `scripts/verify-go-mcp.ps1` provide repeatable local checks for the helper, five-tool MCP surface, default empty-source `isphere_receive_messages`, and synthetic configured-source receive smoke. +- `scripts/verify-win-helper.ps1` and `scripts/verify-go-mcp.ps1` provide repeatable local checks for the helper, six-tool MCP surface, default empty-source receive/contact calls, and synthetic configured-source receive/contact smoke. - 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. ## What cannot be claimed externally -- The project has not yet implemented business MCP tools for contact search, group search, message sending, file receiving, or file sending; `isphere_receive_messages` has an operator-local encrypted log-file loader but not a production ingestion path. +- The project has not yet implemented business MCP tools for group search, message sending, file receiving, or file sending; `isphere_receive_messages` and `isphere_search_contacts` have operator-local encrypted log-file support but not a production ingestion path. - The project has not completed production iSphere integration. - The project has not proven a stable real message source for the full business workflow beyond decrypted PacketReader log evidence and synthetic/redacted tests. - The project has not implemented real message sending or real file upload. @@ -40,7 +41,7 @@ Remote base before local roadmap commits: `b2d839e Merge branch 'codex/n15-repor - The helper protocol boundary is explicit and keeps Windows/UIA work separate from the Go MCP service layer. - The current code can support future MCP business tools by routing low-level Windows/UIA fallback work through a bounded helper layer. - The project has repeatable verification scripts for the helper/MCP foundation and the current five-tool surface. -- The project now has a corrected business contract and plan for core MCP communication tools, plus a registered first read tool that can use `ISPHERE_PACKET_LOG_FILE` for operator-local encrypted PacketReader log lines. +- The project now has a corrected business contract and plan for core MCP communication tools, plus registered receive/contact read tools that can use `ISPHERE_PACKET_LOG_FILE` for operator-local encrypted PacketReader log lines. ## N13/N14/N15 positioning @@ -48,7 +49,7 @@ N13/N14/N15 are pre-business validation results. They can help identify UI eleme ## Current loop -Current loop: `Stage C / Loop C7 - log-backed contact candidate extraction`. +Current loop: `Stage C / Loop C9 - group source evidence precheck`. Plan file: `docs/superpowers/plans/2026-07-09-stage-c-log-backed-receive-messages-loop.md`. @@ -68,11 +69,11 @@ Current loop output so far: 5. C5: safe `ISPHERE_PACKET_LOG_FILE` loader for operator-local encrypted PacketReader log lines. 6. C6: fixture-backed operator smoke for configured receive source. 7. C7: log-backed contact candidate extraction from sender/receiver JIDs. +8. C8: `isphere_search_contacts` MCP registration and six-tool verification. ## Next business mainline -1. Implement Stage C Loop C7: extract searchable contact candidates from log-backed messages. -2. Register `isphere_search_contacts` after contact source tests pass. -3. Add group/file read tools after their source decisions are recorded. -4. Add `isphere_send_message` and `isphere_send_file` after source discovery and connector selection. -5. Keep UIA selector/report work as fallback support, not as the primary roadmap. +1. Run Stage C Loop C9: verify whether current decrypted log evidence contains group/groupchat identifiers before implementing `isphere_search_groups`. +2. Add group/file read tools after their source decisions are recorded. +3. Add `isphere_send_message` and `isphere_send_file` after source discovery and connector selection. +4. Keep UIA selector/report work as fallback support, not as the primary roadmap. diff --git a/docs/go-mcp-runbook.md b/docs/go-mcp-runbook.md index 42e3f87..0ee17ac 100644 --- a/docs/go-mcp-runbook.md +++ b/docs/go-mcp-runbook.md @@ -90,10 +90,11 @@ The verification confirms: - C# helper can be built. - Go MCP binary can be built. - MCP initialize/list/call flow works through the SDK harness. -- Exactly five tools are exposed: four WinHelper observation tools plus `isphere_receive_messages`. +- Exactly six tools are exposed: four WinHelper observation tools plus `isphere_receive_messages` and `isphere_search_contacts`. - `win_helper_version` returns `ISphereWinHelper`. - `isphere_receive_messages` is callable and returns an empty `messages` array from the default empty source. - A synthetic configured-source smoke check proves `ISPHERE_PACKET_LOG_FILE` can drive `isphere_receive_messages` end-to-end with one redacted fixture message. +- `isphere_search_contacts` is callable and returns JID-derived contact candidates from the same configured synthetic message fixture. - Verification uses synthetic/local helper checks and does not load raw N12-pre evidence. - Contact/group/file/search/send business tools remain later-stage work. @@ -152,7 +153,7 @@ E:\coding\codex\isphere-ai-bridge\isphere-mcp.exe ## 8. Allowed tools -These five tools are currently allowed: +These six tools are currently allowed: | Tool | Source/op | Purpose | | --- | --- | --- | @@ -161,6 +162,7 @@ These five tools are currently allowed: | `win_helper_scan_windows` | helper `scan_windows` | Read visible window metadata or likely iSphere candidates. | | `win_helper_dump_uia` | helper `dump_uia` | Read a UI Automation tree for a specified window handle. | | `isphere_receive_messages` | log-backed `EncryptedPacketLogSource` | Read normalized messages from the configured PacketReader log source. The default server source is empty until a loader is wired. | +| `isphere_search_contacts` | log-backed message JIDs | Search contact candidates extracted from sender/receiver bare JIDs in the configured message source. | Allowed parameters: @@ -169,16 +171,18 @@ Allowed parameters: - `win_helper_scan_windows`: `include_all_visible` only. - `win_helper_dump_uia`: `hwnd`, `max_depth`, `include_text`, `max_children` only. - `isphere_receive_messages`: `limit` only. +- `isphere_search_contacts`: `query`, `limit` only. ## 9. Current tool surface and next-stage route -The current runbook verifies five tools: +The current runbook verifies six tools: - `win_helper_version` - `win_helper_self_check` - `win_helper_scan_windows` - `win_helper_dump_uia` - `isphere_receive_messages` +- `isphere_search_contacts` `isphere_receive_messages` currently has parser/source/tool registration and an empty default server source. Raw N12-pre evidence remains under ignored `runs/` paths and is not committed. The current command entry point can read an operator-local encrypted PacketReader log-line file via `ISPHERE_PACKET_LOG_FILE`; this is still not a production ingestion claim because raw evidence remains local and uncommitted. diff --git a/docs/source-discovery/capability-source-matrix.md b/docs/source-discovery/capability-source-matrix.md index cc78911..ed46fff 100644 --- a/docs/source-discovery/capability-source-matrix.md +++ b/docs/source-discovery/capability-source-matrix.md @@ -18,7 +18,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 `` stanzas via `internal/isphere.EncryptedPacketLogSource` | decrypted `Smark.SendReceive` transport stanzas; decrypted `SaveToDB` `Chat_OnMessageArrived` traces; wrapped `MsgLib.db` after DB wrapper is solved | `docs/source-discovery/2026-07-09-n12-pre-zyl-schema-notes.md#field-mapping-evidence` | C5 env loader wired through ISPHERE_PACKET_LOG_FILE; default source remains empty when unset | fixture-backed configured-source operator smoke | -| `isphere_search_contacts` | bare sender/receiver JIDs extracted from normalized log-backed messages | decrypted roster/contact stanzas from `Smark.SendReceive`; `MsgLib.db` contact/conversation tables after DB wrapper is solved; UIA helper source if display names are missing | `docs/source-discovery/2026-07-09-n12-pre-zyl-schema-notes.md#log-decryption-result` | C7 source extraction unit-verified with JID-only display/account fallback | register MCP tool from C7 source | +| `isphere_search_contacts` | bare sender/receiver JIDs extracted from normalized log-backed messages | decrypted roster/contact stanzas from `Smark.SendReceive`; `MsgLib.db` contact/conversation tables after DB wrapper is solved; UIA helper source if display names are missing | `docs/source-discovery/2026-07-09-n12-pre-zyl-schema-notes.md#log-decryption-result` | C8 MCP tool registered and verify-go-mcp covers default/configured contact smoke; JID-only display/account fallback | enrich display names from roster/db/uia evidence | | `isphere_search_groups` | decrypted XMPP group/message stanzas if groupchat/group JIDs appear | `MsgLib.db` group/conversation tables after DB wrapper is solved; UIA helper source if group display names are missing | `docs/source-discovery/2026-07-09-n12-pre-zyl-schema-notes.md#field-mapping-evidence` | candidate pending group stanza evidence | third | | `isphere_receive_files` | decrypted `PacketReader.ProcessPacket` file-transfer message stanzas plus `zyl\importal\` cache mapping | decrypted `Smark.SendReceive` and `SaveToDB` traces for file-reference reconciliation | `docs/source-discovery/2026-07-09-n12-pre-zyl-schema-notes.md#field-mapping-evidence` | candidate pending cache-reference mapping | fourth | diff --git a/docs/superpowers/plans/2026-07-09-stage-c-log-backed-receive-messages-loop.md b/docs/superpowers/plans/2026-07-09-stage-c-log-backed-receive-messages-loop.md index bd4d510..06d5eaa 100644 --- a/docs/superpowers/plans/2026-07-09-stage-c-log-backed-receive-messages-loop.md +++ b/docs/superpowers/plans/2026-07-09-stage-c-log-backed-receive-messages-loop.md @@ -990,7 +990,7 @@ Implemented and verified in this loop: - Output shape: `ok`, `contacts`, `next_cursor`, `audit` minimal fields aligned with `docs/mcp-core-tools-contract.md`. - Default empty source returns `ok: true`, `contacts: []`. -- [ ] **Step 1: Write failing tool test** +- [x] **Step 1: Write failing tool test** Create `internal/tools/isphere_contacts_test.go` and run: @@ -1000,15 +1000,15 @@ go test ./internal/tools -run TestISphereSearchContactsToolReturnsJIDContacts -v Expected initial failure: contact tool registration does not exist. -- [ ] **Step 2: Implement contact tool registration** +- [x] **Step 2: Implement contact tool registration** Register `isphere_search_contacts` with source injection for tests. Use C7 source behavior and return a contract-shaped map with minimal audit metadata. -- [ ] **Step 3: Wire server and verification** +- [x] **Step 3: Wire server and verification** Register contact search in `mcpserver.NewServerWithReceiveSource` or an equivalent shared source path. Update server tool-count tests and `scripts/verify-go-mcp.ps1` to expect six tools and call `isphere_search_contacts` against both default empty source and synthetic configured fixture. -- [ ] **Step 4: Run full verification and commit** +- [x] **Step 4: Run full verification and commit** Run: @@ -1023,6 +1023,54 @@ Expected: all exit 0. Remove any generated root `isphere-mcp.exe` after build. C --- +## Loop C8 Result + +Implemented and verified in this loop: + +- `internal/tools.RegisterISphereContactTools(server, source)` registers `isphere_search_contacts` over the same receive-message source used by `isphere_receive_messages`. +- `internal/mcpserver.NewServerWithReceiveSource` now exposes six tools: four WinHelper tools, `isphere_receive_messages`, and `isphere_search_contacts`. +- `scripts/verify-go-mcp.ps1` now expects six tools, allows `search` as a legitimate read capability term, and verifies both default empty contact results and configured synthetic contact extraction. +- `isphere_search_contacts` currently returns JID-derived `local_readonly` contacts with minimal audit metadata; richer names remain deferred until roster/db/uia evidence is validated. +- Verification completed for C8 with affected package tests and `powershell -NoProfile -ExecutionPolicy Bypass -File scripts/verify-go-mcp.ps1`; full verification is run again before commit. + +--- + +## Loop C9: Group source evidence precheck + +**Goal:** Before implementing `isphere_search_groups`, verify whether the current decrypted log evidence contains group/groupchat identifiers that can support a real source decision instead of guessing. + +**Planned files:** +- Modify: `docs/source-discovery/capability-source-matrix.md` +- Modify: `docs/superpowers/plans/2026-07-09-stage-c-log-backed-receive-messages-loop.md` +- Optionally create a small ignored metadata summary under `runs/` if needed; do not commit raw evidence. + +**Planned behavior:** +- Inspect existing ignored N12-pre decrypted metadata or selected decrypted log samples under `runs/` for groupchat stanzas, room/group JIDs, or group display-name clues. +- If evidence exists, write a C10 implementation loop for group candidate extraction. +- If evidence does not exist, mark `isphere_search_groups` blocked on additional roster/group evidence and choose the next read slice (`isphere_receive_files` or contact enrichment) instead. + +- [ ] **Step 1: Inspect current source evidence for group identifiers** + +Use existing ignored metadata/log summaries under `runs/` and committed schema notes. Do not commit raw logs. + +- [ ] **Step 2: Update matrix and next loop** + +Record whether group extraction can proceed from current evidence. Rewrite C10 accordingly. + +- [ ] **Step 3: Verify docs-only change and commit** + +Run: + +```powershell +git diff --check +go test ./... +powershell -NoProfile -ExecutionPolicy Bypass -File scripts/verify-go-mcp.ps1 +``` + +Expected: all exit 0. Commit exact C9 paths only. + +--- + ## Self-Review - Spec coverage: the plan follows the user's cyclic requirement: plan first, implement one loop at a time, update the next loop after each result, and ask only on blockers. diff --git a/internal/mcpserver/server.go b/internal/mcpserver/server.go index cad5e4f..880f7a4 100644 --- a/internal/mcpserver/server.go +++ b/internal/mcpserver/server.go @@ -44,5 +44,6 @@ func NewServerWithReceiveSource(source tools.ReceiveMessagesSource) *mcp.Server }, nil) tools.RegisterWinHelperTools(server, helperclient.Client{}) tools.RegisterISphereReadTools(server, source) + tools.RegisterISphereContactTools(server, source) return server } diff --git a/internal/mcpserver/server_test.go b/internal/mcpserver/server_test.go index 82f355f..46508a0 100644 --- a/internal/mcpserver/server_test.go +++ b/internal/mcpserver/server_test.go @@ -57,7 +57,7 @@ func TestNewServerRegistersN8WinHelperToolsWithoutCallingHelper(t *testing.T) { gotNames = append(gotNames, tool.Name) } sort.Strings(gotNames) - wantNames := []string{"isphere_receive_messages", "win_helper_dump_uia", "win_helper_scan_windows", "win_helper_self_check", "win_helper_version"} + wantNames := []string{"isphere_receive_messages", "isphere_search_contacts", "win_helper_dump_uia", "win_helper_scan_windows", "win_helper_self_check", "win_helper_version"} if !reflect.DeepEqual(gotNames, wantNames) { t.Fatalf("server tool names = %#v, want %#v", gotNames, wantNames) } diff --git a/internal/tools/isphere_contacts.go b/internal/tools/isphere_contacts.go new file mode 100644 index 0000000..d96427a --- /dev/null +++ b/internal/tools/isphere_contacts.go @@ -0,0 +1,65 @@ +package tools + +import ( + "context" + "time" + + "github.com/modelcontextprotocol/go-sdk/mcp" + + "isphere-ai-bridge/internal/isphere" +) + +const ToolNameSearchContacts = "isphere_search_contacts" + +type SearchContactsArgs struct { + Query string `json:"query" jsonschema:"contact search query; currently matches bare JIDs from local readonly message logs"` + Limit int `json:"limit,omitempty" jsonschema:"maximum number of contacts to return; zero or negative returns all matches"` +} + +func RegisterISphereContactTools(server *mcp.Server, source ReceiveMessagesSource) { + if source == nil { + source = isphere.EncryptedPacketLogSource{} + } + + mcp.AddTool[SearchContactsArgs, map[string]any](server, &mcp.Tool{ + Name: ToolNameSearchContacts, + Description: "Search iSphere contact candidates from the configured read-only message source.", + }, func(ctx context.Context, _ *mcp.CallToolRequest, input SearchContactsArgs) (*mcp.CallToolResult, map[string]any, error) { + started := time.Now().UTC() + messages, err := source.ReceiveMessages(ctx, isphere.ReceiveMessagesQuery{Limit: 0}) + if err != nil { + return nil, nil, err + } + contacts := isphere.SearchContactsFromMessages(messages.Messages, isphere.SearchContactsQuery{Query: input.Query, Limit: input.Limit}) + return nil, searchContactsResultToMap(contacts, started, time.Now().UTC()), nil + }) +} + +func searchContactsResultToMap(result isphere.SearchContactsResult, started time.Time, finished time.Time) map[string]any { + contacts := make([]map[string]any, 0, len(result.Contacts)) + for _, contact := range result.Contacts { + contacts = append(contacts, map[string]any{ + "contact_id": contact.ContactID, + "display_name": contact.DisplayName, + "account": contact.Account, + "department": nil, + "title": nil, + "source": contact.Source, + "confidence": contact.Confidence, + "raw_ref": contact.RawRef, + }) + } + return map[string]any{ + "ok": true, + "contacts": contacts, + "next_cursor": nil, + "audit": map[string]any{ + "tool": ToolNameSearchContacts, + "source": "local_readonly", + "execution_mode": "read", + "started_at": started.Format(time.RFC3339Nano), + "finished_at": finished.Format(time.RFC3339Nano), + "result": "ok", + }, + } +} diff --git a/internal/tools/isphere_contacts_test.go b/internal/tools/isphere_contacts_test.go new file mode 100644 index 0000000..b9d7ea6 --- /dev/null +++ b/internal/tools/isphere_contacts_test.go @@ -0,0 +1,76 @@ +package tools + +import ( + "context" + "encoding/json" + "testing" + + "github.com/modelcontextprotocol/go-sdk/mcp" + + "isphere-ai-bridge/internal/isphere" +) + +func TestISphereSearchContactsToolReturnsJIDContacts(t *testing.T) { + fake := &fakeReceiveMessagesSource{ + result: isphere.ReceiveMessagesResult{Messages: []isphere.Message{{ + ID: "msg-1", + SenderID: "alice@imopenfire1-lanzhou", + ReceiverID: "bob@imopenfire1-lanzhou", + }}}, + } + session, cleanup := connectToolsTestSession(t, func(server *mcp.Server) { + RegisterISphereContactTools(server, fake) + }) + defer cleanup() + + callResult, err := session.CallTool(context.Background(), &mcp.CallToolParams{ + Name: ToolNameSearchContacts, + Arguments: map[string]any{"query": "alice", "limit": 10}, + }) + if err != nil { + t.Fatalf("call %s: %v", ToolNameSearchContacts, err) + } + if callResult.IsError { + t.Fatalf("call result is error: %+v", callResult) + } + + var decoded struct { + OK bool `json:"ok"` + 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"` + NextCursor any `json:"next_cursor"` + Audit map[string]any `json:"audit"` + } + payload, err := json.Marshal(callResult.StructuredContent) + if err != nil { + t.Fatalf("marshal structured content: %v", err) + } + if err := json.Unmarshal(payload, &decoded); err != nil { + t.Fatalf("decode structured content %s: %v", payload, err) + } + if !decoded.OK { + t.Fatalf("ok = false in %s", payload) + } + if len(decoded.Contacts) != 1 { + t.Fatalf("contacts = %+v, want one", decoded.Contacts) + } + contact := decoded.Contacts[0] + if contact.ContactID != "alice@imopenfire1-lanzhou" || contact.DisplayName != "alice@imopenfire1-lanzhou" || contact.Account != "alice@imopenfire1-lanzhou" { + t.Fatalf("unexpected contact identity: %+v", contact) + } + if contact.Source != "local_readonly" || contact.Confidence != 0.6 || contact.RawRef != "message_jid" { + t.Fatalf("unexpected contact metadata: %+v", contact) + } + if decoded.Audit["tool"] != ToolNameSearchContacts || decoded.Audit["result"] != "ok" { + t.Fatalf("audit = %+v", decoded.Audit) + } + if len(fake.queries) != 1 || fake.queries[0].Limit != 0 { + t.Fatalf("source queries = %+v, want one unbounded receive query", fake.queries) + } +} diff --git a/scripts/verify-go-mcp.ps1 b/scripts/verify-go-mcp.ps1 index 7ab6eb1..ba0e125 100644 --- a/scripts/verify-go-mcp.ps1 +++ b/scripts/verify-go-mcp.ps1 @@ -7,7 +7,8 @@ $expectedTools = @( "win_helper_self_check", "win_helper_scan_windows", "win_helper_dump_uia", - "isphere_receive_messages" + "isphere_receive_messages", + "isphere_search_contacts" ) $tempRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("isphere-go-mcp-verify-" + [guid]::NewGuid().ToString("N")) @@ -79,9 +80,10 @@ var expectedTools = []string{ "win_helper_scan_windows", "win_helper_dump_uia", "isphere_receive_messages", + "isphere_search_contacts", } -var forbiddenTerms = []string{"send", "search", "file", "login", "conversation", "upload", "download", "autologin"} +var forbiddenTerms = []string{"send", "file", "login", "conversation", "upload", "download", "autologin"} func main() { ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) @@ -134,7 +136,7 @@ func main() { sort.Strings(toolNames) want := append([]string(nil), expectedTools...) sort.Strings(want) - if len(toolNames) != 5 || !reflect.DeepEqual(toolNames, want) { + if len(toolNames) != 6 || !reflect.DeepEqual(toolNames, want) { fail("tools/list", fmt.Errorf("tool names = %#v, want %#v", toolNames, want)) } @@ -184,7 +186,8 @@ func main() { if len(messagesValue) != 0 { fail("tools/call isphere_receive_messages", fmt.Errorf("default empty source returned %d messages, want 0", len(messagesValue))) } - configuredReceiveCount := verifyConfiguredReceiveSource(ctx) + defaultContactCount := verifySearchContacts(ctx, session, "sender", "tools/call isphere_search_contacts default", 0) + configuredReceiveCount, configuredContactCount := verifyConfiguredReceiveSource(ctx) result := map[string]any{ "ok": true, @@ -194,7 +197,9 @@ func main() { "tools": toolNames, "helper_name": helperName, "receive_message_count": len(messagesValue), + "contact_count": defaultContactCount, "configured_receive_message_count": configuredReceiveCount, + "configured_contact_count": configuredContactCount, "packet_log_file_configured": os.Getenv("ISPHERE_PACKET_LOG_FILE") != "", "real_isphere_login_required": false, "python_runtime_required": false, @@ -205,7 +210,7 @@ func main() { } -func verifyConfiguredReceiveSource(ctx context.Context) int { +func verifyConfiguredReceiveSource(ctx context.Context) (int, int) { plaintext := "--------------------------------------------------------------------------------------------------------------------------------------------\n" + "2026/7/7 15:30:07\n" + "\n" + @@ -302,7 +307,43 @@ func verifyConfiguredReceiveSource(ctx context.Context) int { if message["conversation_id"] != "sender@imopenfire1-lanzhou|receiver@imopenfire1-lanzhou" { fail("fixture/assert", fmt.Errorf("conversation_id = %#v", message["conversation_id"])) } - return len(messagesValue) + contactCount := verifySearchContacts(ctx, session, "sender", "fixture/call isphere_search_contacts", 1) + return len(messagesValue), contactCount +} + +func verifySearchContacts(ctx context.Context, session *mcp.ClientSession, query string, step string, wantCount int) int { + callResult, err := session.CallTool(ctx, &mcp.CallToolParams{Name: "isphere_search_contacts", Arguments: map[string]any{"query": query, "limit": 5}}) + if err != nil { + fail(step, err) + } + if callResult.IsError { + fail(step, fmt.Errorf("tool returned isError=true: %#v", callResult.Content)) + } + var contactsPayload map[string]any + encoded, _ := json.Marshal(callResult.StructuredContent) + if err := json.Unmarshal(encoded, &contactsPayload); err != nil { + fail(step, fmt.Errorf("decode structuredContent %s: %w", encoded, err)) + } + if contactsPayload["ok"] != true { + fail(step, fmt.Errorf("ok = %#v, want true", contactsPayload["ok"])) + } + contactsValue, ok := contactsPayload["contacts"].([]any) + if !ok { + fail(step, fmt.Errorf("contacts missing or not array: %s", encoded)) + } + if len(contactsValue) != wantCount { + fail(step, fmt.Errorf("contacts count = %d, want %d: %s", len(contactsValue), wantCount, encoded)) + } + if wantCount > 0 { + contact, ok := contactsValue[0].(map[string]any) + if !ok { + fail(step, fmt.Errorf("contact was not object: %#v", contactsValue[0])) + } + if contact["contact_id"] != "sender@imopenfire1-lanzhou" || contact["source"] != "local_readonly" { + fail(step, fmt.Errorf("unexpected contact: %#v", contact)) + } + } + return len(contactsValue) } func encryptPacketLogLineForHarness(plaintext string) (string, error) { @@ -353,9 +394,11 @@ func fail(step string, err error) { Assert-True ($script:harnessJson.ok -eq $true) "SDK harness did not return ok=true" Assert-True ($script:harnessJson.helper_name -eq "ISphereWinHelper") "SDK harness helper_name mismatch: $($script:harnessJson.helper_name)" - Assert-True ($script:harnessJson.tool_count -eq 5) "SDK harness tool_count mismatch: $($script:harnessJson.tool_count)" + Assert-True ($script:harnessJson.tool_count -eq 6) "SDK harness tool_count mismatch: $($script:harnessJson.tool_count)" Assert-True ($script:harnessJson.receive_message_count -eq 0) "SDK harness receive_message_count mismatch: $($script:harnessJson.receive_message_count)" + Assert-True ($script:harnessJson.contact_count -eq 0) "SDK harness contact_count mismatch: $($script:harnessJson.contact_count)" Assert-True ($script:harnessJson.configured_receive_message_count -eq 1) "SDK harness configured_receive_message_count mismatch: $($script:harnessJson.configured_receive_message_count)" + Assert-True ($script:harnessJson.configured_contact_count -eq 1) "SDK harness configured_contact_count mismatch: $($script:harnessJson.configured_contact_count)" [ordered]@{ ok = $true @@ -365,7 +408,9 @@ func fail(step string, err error) { tool_count = $script:harnessJson.tool_count tools = $script:harnessJson.tools receive_message_count = $script:harnessJson.receive_message_count + contact_count = $script:harnessJson.contact_count configured_receive_message_count = $script:harnessJson.configured_receive_message_count + configured_contact_count = $script:harnessJson.configured_contact_count packet_log_file_configured = $script:harnessJson.packet_log_file_configured python_runtime_required = $false real_isphere_login_required = $false