feat: register isphere group search tool
This commit is contained in:
@@ -14,21 +14,22 @@ 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 six read-oriented tools:
|
||||
- The current MCP surface exposes seven read-oriented tools:
|
||||
- `win_helper_version`
|
||||
- `win_helper_self_check`
|
||||
- `win_helper_scan_windows`
|
||||
- `win_helper_dump_uia`
|
||||
- `isphere_receive_messages`
|
||||
- `isphere_search_contacts`
|
||||
- `isphere_search_groups`
|
||||
- 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, six-tool MCP surface, default empty-source receive/contact calls, and synthetic configured-source receive/contact smoke.
|
||||
- `scripts/verify-win-helper.ps1` and `scripts/verify-go-mcp.ps1` provide repeatable local checks for the helper, seven-tool MCP surface, default empty-source receive/contact/group calls, and synthetic configured-source receive/contact/group 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 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 yet implemented business MCP tools for message sending, file receiving, or file sending; `isphere_receive_messages`, `isphere_search_contacts`, and `isphere_search_groups` 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.
|
||||
@@ -41,7 +42,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 registered receive/contact read tools 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/group read tools that can use `ISPHERE_PACKET_LOG_FILE` for operator-local encrypted PacketReader log lines.
|
||||
|
||||
## N13/N14/N15 positioning
|
||||
|
||||
@@ -49,7 +50,7 @@ N13/N14/N15 are pre-business validation results. They can help identify UI eleme
|
||||
|
||||
## Current loop
|
||||
|
||||
Current loop: `Stage C / Loop C10 - log-backed group candidate extraction`.
|
||||
Current loop: `Stage C / Loop C12 - file receive source evidence precheck`.
|
||||
|
||||
Plan file: `docs/superpowers/plans/2026-07-09-stage-c-log-backed-receive-messages-loop.md`.
|
||||
|
||||
@@ -71,11 +72,12 @@ Current loop output so far:
|
||||
7. C7: log-backed contact candidate extraction from sender/receiver JIDs.
|
||||
8. C8: `isphere_search_contacts` MCP registration and six-tool verification.
|
||||
9. C9: group source evidence precheck confirmed groupchat/conference/MUC hits in ignored decrypted logs.
|
||||
10. C10: log-backed group candidate extraction from groupchat/conference JIDs.
|
||||
11. C11: `isphere_search_groups` MCP registration and seven-tool verification.
|
||||
|
||||
## Next business mainline
|
||||
|
||||
1. Implement Stage C Loop C10: extract group candidates from log-backed groupchat/conference JIDs.
|
||||
2. Register `isphere_search_groups` after group source tests pass.
|
||||
3. Add 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 C12: verify current evidence for `isphere_receive_files` before implementation.
|
||||
2. Implement file metadata extraction only after file-transfer/cache evidence is 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.
|
||||
|
||||
@@ -90,11 +90,12 @@ 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 six tools are exposed: four WinHelper observation tools plus `isphere_receive_messages` and `isphere_search_contacts`.
|
||||
- Exactly seven tools are exposed: four WinHelper observation tools plus `isphere_receive_messages`, `isphere_search_contacts`, and `isphere_search_groups`.
|
||||
- `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.
|
||||
- `isphere_search_groups` is callable and returns JID-derived group candidates from the configured synthetic groupchat 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.
|
||||
|
||||
@@ -153,7 +154,7 @@ E:\coding\codex\isphere-ai-bridge\isphere-mcp.exe
|
||||
|
||||
## 8. Allowed tools
|
||||
|
||||
These six tools are currently allowed:
|
||||
These seven tools are currently allowed:
|
||||
|
||||
| Tool | Source/op | Purpose |
|
||||
| --- | --- | --- |
|
||||
@@ -163,6 +164,7 @@ These six tools are currently allowed:
|
||||
| `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. |
|
||||
| `isphere_search_groups` | log-backed groupchat JIDs | Search group candidates extracted from groupchat/conference/MUC bare JIDs in the configured message source. |
|
||||
|
||||
Allowed parameters:
|
||||
|
||||
@@ -172,10 +174,11 @@ Allowed parameters:
|
||||
- `win_helper_dump_uia`: `hwnd`, `max_depth`, `include_text`, `max_children` only.
|
||||
- `isphere_receive_messages`: `limit` only.
|
||||
- `isphere_search_contacts`: `query`, `limit` only.
|
||||
- `isphere_search_groups`: `query`, `limit` only.
|
||||
|
||||
## 9. Current tool surface and next-stage route
|
||||
|
||||
The current runbook verifies six tools:
|
||||
The current runbook verifies seven tools:
|
||||
|
||||
- `win_helper_version`
|
||||
- `win_helper_self_check`
|
||||
|
||||
@@ -19,7 +19,7 @@ Schema notes: `docs/source-discovery/2026-07-09-n12-pre-zyl-schema-notes.md`
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| `isphere_receive_messages` | decrypted `PacketReader.ProcessPacket` XMPP `<message>` 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` | 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 `PacketReader.ProcessPacket` `type="groupchat"` stanzas and conference/MUC JIDs | `MsgLib.db` group/conversation tables after DB wrapper is solved; UIA helper source if group display names are missing | C9 ignored-log scan: PacketReader 86 groupchat/86 conference hits; Smark 24 groupchat/658 conference/631 muc hits | C10 source extraction unit-verified with JID-only display fallback | register MCP tool from C10 source |
|
||||
| `isphere_search_groups` | decrypted `PacketReader.ProcessPacket` `type="groupchat"` stanzas and conference/MUC JIDs | `MsgLib.db` group/conversation tables after DB wrapper is solved; UIA helper source if group display names are missing | C9 ignored-log scan: PacketReader 86 groupchat/86 conference hits; Smark 24 groupchat/658 conference/631 muc hits | C11 MCP tool registered and verify-go-mcp covers default/configured group smoke; JID-only display fallback | enrich group display/member data from roster/db/uia evidence |
|
||||
| `isphere_receive_files` | decrypted `PacketReader.ProcessPacket` file-transfer message stanzas plus `zyl\importal\<hash>` 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 |
|
||||
|
||||
## Stage C entry rule
|
||||
|
||||
@@ -1164,7 +1164,7 @@ Implemented and verified in this loop:
|
||||
- Output shape: `ok`, `groups`, `next_cursor`, `audit` minimal fields aligned with `docs/mcp-core-tools-contract.md`.
|
||||
- Default empty source returns `ok: true`, `groups: []`.
|
||||
|
||||
- [ ] **Step 1: Write failing tool test**
|
||||
- [x] **Step 1: Write failing tool test**
|
||||
|
||||
Create `internal/tools/isphere_groups_test.go` and run:
|
||||
|
||||
@@ -1174,15 +1174,15 @@ go test ./internal/tools -run TestISphereSearchGroupsToolReturnsJIDGroups -v
|
||||
|
||||
Expected initial failure: group tool registration does not exist.
|
||||
|
||||
- [ ] **Step 2: Implement group tool registration**
|
||||
- [x] **Step 2: Implement group tool registration**
|
||||
|
||||
Register `isphere_search_groups` with source injection for tests. Use C10 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 group search in server construction. Update server tool-count tests and `scripts/verify-go-mcp.ps1` to expect seven tools and call `isphere_search_groups` against default empty source and synthetic configured groupchat fixture.
|
||||
|
||||
- [ ] **Step 4: Run full verification and commit**
|
||||
- [x] **Step 4: Run full verification and commit**
|
||||
|
||||
Run:
|
||||
|
||||
@@ -1197,6 +1197,54 @@ Expected: all exit 0. Remove any generated root `isphere-mcp.exe` after build. C
|
||||
|
||||
---
|
||||
|
||||
## Loop C11 Result
|
||||
|
||||
Implemented and verified in this loop:
|
||||
|
||||
- `internal/tools.RegisterISphereGroupTools(server, source)` registers `isphere_search_groups` over the same receive-message source used by receive/contact tools.
|
||||
- `internal/mcpserver.NewServerWithReceiveSource` now exposes seven tools: four WinHelper tools, `isphere_receive_messages`, `isphere_search_contacts`, and `isphere_search_groups`.
|
||||
- `scripts/verify-go-mcp.ps1` now expects seven tools, verifies default empty group results, and verifies configured synthetic groupchat extraction.
|
||||
- `isphere_search_groups` currently returns JID-derived `local_readonly` groups with unknown member/owner fields and minimal audit metadata.
|
||||
- Verification completed for C11 with affected package tests and `powershell -NoProfile -ExecutionPolicy Bypass -File scripts/verify-go-mcp.ps1`; full verification is run again before commit.
|
||||
|
||||
---
|
||||
|
||||
## Loop C12: File receive source evidence precheck
|
||||
|
||||
**Goal:** Before implementing `isphere_receive_files`, verify whether current decrypted PacketReader/Smark/SaveToDB evidence and extracted cache paths contain enough file-transfer metadata to support a real read-only file listing/download-reference source.
|
||||
|
||||
**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 ignored metadata under `runs/`; do not commit raw evidence.
|
||||
|
||||
**Planned behavior:**
|
||||
- Inspect committed schema notes and ignored N12-pre metadata/log summaries for file-transfer subjects, file names, cache/hash paths, download refs, or attachment-like fields.
|
||||
- If evidence exists, write C13 around file metadata extraction from normalized messages/cache references.
|
||||
- If evidence is insufficient, mark `isphere_receive_files` blocked on additional file-transfer/cache mapping evidence and move to source hardening/docs instead.
|
||||
|
||||
- [ ] **Step 1: Inspect current source evidence for file-transfer identifiers**
|
||||
|
||||
Use committed schema notes and ignored summaries/log scans. Do not commit raw logs or decrypted content.
|
||||
|
||||
- [ ] **Step 2: Update matrix and next loop**
|
||||
|
||||
Record whether file receive implementation can proceed from current evidence. Rewrite C13 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 C12 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.
|
||||
|
||||
@@ -45,5 +45,6 @@ func NewServerWithReceiveSource(source tools.ReceiveMessagesSource) *mcp.Server
|
||||
tools.RegisterWinHelperTools(server, helperclient.Client{})
|
||||
tools.RegisterISphereReadTools(server, source)
|
||||
tools.RegisterISphereContactTools(server, source)
|
||||
tools.RegisterISphereGroupTools(server, source)
|
||||
return server
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ func TestNewServerRegistersN8WinHelperToolsWithoutCallingHelper(t *testing.T) {
|
||||
gotNames = append(gotNames, tool.Name)
|
||||
}
|
||||
sort.Strings(gotNames)
|
||||
wantNames := []string{"isphere_receive_messages", "isphere_search_contacts", "win_helper_dump_uia", "win_helper_scan_windows", "win_helper_self_check", "win_helper_version"}
|
||||
wantNames := []string{"isphere_receive_messages", "isphere_search_contacts", "isphere_search_groups", "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)
|
||||
}
|
||||
|
||||
64
internal/tools/isphere_groups.go
Normal file
64
internal/tools/isphere_groups.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
|
||||
"isphere-ai-bridge/internal/isphere"
|
||||
)
|
||||
|
||||
const ToolNameSearchGroups = "isphere_search_groups"
|
||||
|
||||
type SearchGroupsArgs struct {
|
||||
Query string `json:"query" jsonschema:"group search query; currently matches bare group JIDs from local readonly message logs"`
|
||||
Limit int `json:"limit,omitempty" jsonschema:"maximum number of groups to return; zero or negative returns all matches"`
|
||||
}
|
||||
|
||||
func RegisterISphereGroupTools(server *mcp.Server, source ReceiveMessagesSource) {
|
||||
if source == nil {
|
||||
source = isphere.EncryptedPacketLogSource{}
|
||||
}
|
||||
|
||||
mcp.AddTool[SearchGroupsArgs, map[string]any](server, &mcp.Tool{
|
||||
Name: ToolNameSearchGroups,
|
||||
Description: "Search iSphere group candidates from the configured read-only message source.",
|
||||
}, func(ctx context.Context, _ *mcp.CallToolRequest, input SearchGroupsArgs) (*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
|
||||
}
|
||||
groups := isphere.SearchGroupsFromMessages(messages.Messages, isphere.SearchGroupsQuery{Query: input.Query, Limit: input.Limit})
|
||||
return nil, searchGroupsResultToMap(groups, started, time.Now().UTC()), nil
|
||||
})
|
||||
}
|
||||
|
||||
func searchGroupsResultToMap(result isphere.SearchGroupsResult, started time.Time, finished time.Time) map[string]any {
|
||||
groups := make([]map[string]any, 0, len(result.Groups))
|
||||
for _, group := range result.Groups {
|
||||
groups = append(groups, map[string]any{
|
||||
"group_id": group.GroupID,
|
||||
"display_name": group.DisplayName,
|
||||
"member_count": nil,
|
||||
"owner_ref": nil,
|
||||
"source": group.Source,
|
||||
"confidence": group.Confidence,
|
||||
"raw_ref": group.RawRef,
|
||||
})
|
||||
}
|
||||
return map[string]any{
|
||||
"ok": true,
|
||||
"groups": groups,
|
||||
"next_cursor": nil,
|
||||
"audit": map[string]any{
|
||||
"tool": ToolNameSearchGroups,
|
||||
"source": "local_readonly",
|
||||
"execution_mode": "read",
|
||||
"started_at": started.Format(time.RFC3339Nano),
|
||||
"finished_at": finished.Format(time.RFC3339Nano),
|
||||
"result": "ok",
|
||||
},
|
||||
}
|
||||
}
|
||||
81
internal/tools/isphere_groups_test.go
Normal file
81
internal/tools/isphere_groups_test.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
|
||||
"isphere-ai-bridge/internal/isphere"
|
||||
)
|
||||
|
||||
func TestISphereSearchGroupsToolReturnsJIDGroups(t *testing.T) {
|
||||
fake := &fakeReceiveMessagesSource{
|
||||
result: isphere.ReceiveMessagesResult{Messages: []isphere.Message{{
|
||||
ID: "msg-group-1",
|
||||
ConversationType: "groupchat",
|
||||
SenderID: "project-room@conference.imopenfire1-lanzhou",
|
||||
ReceiverID: "user@imopenfire1-lanzhou",
|
||||
}}},
|
||||
}
|
||||
session, cleanup := connectToolsTestSession(t, func(server *mcp.Server) {
|
||||
RegisterISphereGroupTools(server, fake)
|
||||
})
|
||||
defer cleanup()
|
||||
|
||||
callResult, err := session.CallTool(context.Background(), &mcp.CallToolParams{
|
||||
Name: ToolNameSearchGroups,
|
||||
Arguments: map[string]any{"query": "project", "limit": 10},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("call %s: %v", ToolNameSearchGroups, err)
|
||||
}
|
||||
if callResult.IsError {
|
||||
t.Fatalf("call result is error: %+v", callResult)
|
||||
}
|
||||
|
||||
var decoded struct {
|
||||
OK bool `json:"ok"`
|
||||
Groups []struct {
|
||||
GroupID string `json:"group_id"`
|
||||
DisplayName string `json:"display_name"`
|
||||
MemberCount *int `json:"member_count"`
|
||||
OwnerRef *string `json:"owner_ref"`
|
||||
Source string `json:"source"`
|
||||
Confidence float64 `json:"confidence"`
|
||||
RawRef string `json:"raw_ref"`
|
||||
} `json:"groups"`
|
||||
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.Groups) != 1 {
|
||||
t.Fatalf("groups = %+v, want one", decoded.Groups)
|
||||
}
|
||||
group := decoded.Groups[0]
|
||||
if group.GroupID != "project-room@conference.imopenfire1-lanzhou" || group.DisplayName != "project-room@conference.imopenfire1-lanzhou" {
|
||||
t.Fatalf("unexpected group identity: %+v", group)
|
||||
}
|
||||
if group.MemberCount != nil || group.OwnerRef != nil {
|
||||
t.Fatalf("member/owner should be unknown: %+v", group)
|
||||
}
|
||||
if group.Source != "local_readonly" || group.Confidence != 0.6 || group.RawRef != "groupchat_jid" {
|
||||
t.Fatalf("unexpected group metadata: %+v", group)
|
||||
}
|
||||
if decoded.Audit["tool"] != ToolNameSearchGroups || 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)
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,8 @@ $expectedTools = @(
|
||||
"win_helper_scan_windows",
|
||||
"win_helper_dump_uia",
|
||||
"isphere_receive_messages",
|
||||
"isphere_search_contacts"
|
||||
"isphere_search_contacts",
|
||||
"isphere_search_groups"
|
||||
)
|
||||
|
||||
$tempRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("isphere-go-mcp-verify-" + [guid]::NewGuid().ToString("N"))
|
||||
@@ -81,6 +82,7 @@ var expectedTools = []string{
|
||||
"win_helper_dump_uia",
|
||||
"isphere_receive_messages",
|
||||
"isphere_search_contacts",
|
||||
"isphere_search_groups",
|
||||
}
|
||||
|
||||
var forbiddenTerms = []string{"send", "file", "login", "conversation", "upload", "download", "autologin"}
|
||||
@@ -136,7 +138,7 @@ func main() {
|
||||
sort.Strings(toolNames)
|
||||
want := append([]string(nil), expectedTools...)
|
||||
sort.Strings(want)
|
||||
if len(toolNames) != 6 || !reflect.DeepEqual(toolNames, want) {
|
||||
if len(toolNames) != 7 || !reflect.DeepEqual(toolNames, want) {
|
||||
fail("tools/list", fmt.Errorf("tool names = %#v, want %#v", toolNames, want))
|
||||
}
|
||||
|
||||
@@ -187,7 +189,8 @@ func main() {
|
||||
fail("tools/call isphere_receive_messages", fmt.Errorf("default empty source returned %d messages, want 0", len(messagesValue)))
|
||||
}
|
||||
defaultContactCount := verifySearchContacts(ctx, session, "sender", "tools/call isphere_search_contacts default", 0)
|
||||
configuredReceiveCount, configuredContactCount := verifyConfiguredReceiveSource(ctx)
|
||||
defaultGroupCount := verifySearchGroups(ctx, session, "project", "tools/call isphere_search_groups default", 0)
|
||||
configuredReceiveCount, configuredContactCount, configuredGroupCount := verifyConfiguredReceiveSource(ctx)
|
||||
|
||||
result := map[string]any{
|
||||
"ok": true,
|
||||
@@ -198,8 +201,10 @@ func main() {
|
||||
"helper_name": helperName,
|
||||
"receive_message_count": len(messagesValue),
|
||||
"contact_count": defaultContactCount,
|
||||
"group_count": defaultGroupCount,
|
||||
"configured_receive_message_count": configuredReceiveCount,
|
||||
"configured_contact_count": configuredContactCount,
|
||||
"configured_group_count": configuredGroupCount,
|
||||
"packet_log_file_configured": os.Getenv("ISPHERE_PACKET_LOG_FILE") != "",
|
||||
"real_isphere_login_required": false,
|
||||
"python_runtime_required": false,
|
||||
@@ -210,10 +215,10 @@ func main() {
|
||||
}
|
||||
|
||||
|
||||
func verifyConfiguredReceiveSource(ctx context.Context) (int, int) {
|
||||
func verifyConfiguredReceiveSource(ctx context.Context) (int, int, int) {
|
||||
plaintext := "--------------------------------------------------------------------------------------------------------------------------------------------\n" +
|
||||
"2026/7/7 15:30:07\n" +
|
||||
"<message id=\"msg-fixture-1\" from=\"sender@imopenfire1-lanzhou/imp_pc_4.1.2.6842\" to=\"receiver@imopenfire1-lanzhou\" type=\"chat\">\n" +
|
||||
"<message id=\"msg-fixture-1\" from=\"project-room@conference.imopenfire1-lanzhou/imp_pc_4.1.2.6842\" to=\"sender@imopenfire1-lanzhou\" type=\"groupchat\">\n" +
|
||||
" <body>redacted fixture</body>\n" +
|
||||
" <received xmlns=\"urn:xmpp:receipts\" id=\"receipt-fixture-1\" type=\"1\" stamp=\"\" />\n" +
|
||||
" <subject>VERIFY_FIXTURE</subject>\n" +
|
||||
@@ -304,11 +309,12 @@ func verifyConfiguredReceiveSource(ctx context.Context) (int, int) {
|
||||
if message["id"] != "msg-fixture-1" || message["text"] != "redacted fixture" || message["subject"] != "VERIFY_FIXTURE" {
|
||||
fail("fixture/assert", fmt.Errorf("unexpected message: %#v", message))
|
||||
}
|
||||
if message["conversation_id"] != "sender@imopenfire1-lanzhou|receiver@imopenfire1-lanzhou" {
|
||||
if message["conversation_id"] != "project-room@conference.imopenfire1-lanzhou|sender@imopenfire1-lanzhou" {
|
||||
fail("fixture/assert", fmt.Errorf("conversation_id = %#v", message["conversation_id"]))
|
||||
}
|
||||
contactCount := verifySearchContacts(ctx, session, "sender", "fixture/call isphere_search_contacts", 1)
|
||||
return len(messagesValue), contactCount
|
||||
groupCount := verifySearchGroups(ctx, session, "project", "fixture/call isphere_search_groups", 1)
|
||||
return len(messagesValue), contactCount, groupCount
|
||||
}
|
||||
|
||||
func verifySearchContacts(ctx context.Context, session *mcp.ClientSession, query string, step string, wantCount int) int {
|
||||
@@ -346,6 +352,42 @@ func verifySearchContacts(ctx context.Context, session *mcp.ClientSession, query
|
||||
return len(contactsValue)
|
||||
}
|
||||
|
||||
|
||||
func verifySearchGroups(ctx context.Context, session *mcp.ClientSession, query string, step string, wantCount int) int {
|
||||
callResult, err := session.CallTool(ctx, &mcp.CallToolParams{Name: "isphere_search_groups", 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 groupsPayload map[string]any
|
||||
encoded, _ := json.Marshal(callResult.StructuredContent)
|
||||
if err := json.Unmarshal(encoded, &groupsPayload); err != nil {
|
||||
fail(step, fmt.Errorf("decode structuredContent %s: %w", encoded, err))
|
||||
}
|
||||
if groupsPayload["ok"] != true {
|
||||
fail(step, fmt.Errorf("ok = %#v, want true", groupsPayload["ok"]))
|
||||
}
|
||||
groupsValue, ok := groupsPayload["groups"].([]any)
|
||||
if !ok {
|
||||
fail(step, fmt.Errorf("groups missing or not array: %s", encoded))
|
||||
}
|
||||
if len(groupsValue) != wantCount {
|
||||
fail(step, fmt.Errorf("groups count = %d, want %d: %s", len(groupsValue), wantCount, encoded))
|
||||
}
|
||||
if wantCount > 0 {
|
||||
group, ok := groupsValue[0].(map[string]any)
|
||||
if !ok {
|
||||
fail(step, fmt.Errorf("group was not object: %#v", groupsValue[0]))
|
||||
}
|
||||
if group["group_id"] != "project-room@conference.imopenfire1-lanzhou" || group["source"] != "local_readonly" {
|
||||
fail(step, fmt.Errorf("unexpected group: %#v", group))
|
||||
}
|
||||
}
|
||||
return len(groupsValue)
|
||||
}
|
||||
|
||||
func encryptPacketLogLineForHarness(plaintext string) (string, error) {
|
||||
block, err := des.NewCipher([]byte("hyhccdtm"))
|
||||
if err != nil {
|
||||
@@ -394,11 +436,13 @@ 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 6) "SDK harness tool_count mismatch: $($script:harnessJson.tool_count)"
|
||||
Assert-True ($script:harnessJson.tool_count -eq 7) "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.group_count -eq 0) "SDK harness group_count mismatch: $($script:harnessJson.group_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)"
|
||||
Assert-True ($script:harnessJson.configured_group_count -eq 1) "SDK harness configured_group_count mismatch: $($script:harnessJson.configured_group_count)"
|
||||
|
||||
[ordered]@{
|
||||
ok = $true
|
||||
@@ -409,8 +453,10 @@ func fail(step string, err error) {
|
||||
tools = $script:harnessJson.tools
|
||||
receive_message_count = $script:harnessJson.receive_message_count
|
||||
contact_count = $script:harnessJson.contact_count
|
||||
group_count = $script:harnessJson.group_count
|
||||
configured_receive_message_count = $script:harnessJson.configured_receive_message_count
|
||||
configured_contact_count = $script:harnessJson.configured_contact_count
|
||||
configured_group_count = $script:harnessJson.configured_group_count
|
||||
packet_log_file_configured = $script:harnessJson.packet_log_file_configured
|
||||
python_runtime_required = $false
|
||||
real_isphere_login_required = $false
|
||||
|
||||
Reference in New Issue
Block a user