feat: validate contact and group search args

This commit is contained in:
zhaoyilun
2026-07-10 01:38:48 +08:00
parent 95699e05b5
commit b1891dd6cd
10 changed files with 207 additions and 21 deletions

View File

@@ -27,6 +27,7 @@ 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.
- 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.
@@ -53,7 +54,7 @@ N13/N14/N15 are pre-business validation results. They can help identify UI eleme
## Current loop ## Current loop
Current loop: `Stage C / Loop C21 - contact/group search remaining contract argument validation`. Current loop: `Stage C / Loop C22 - file receive remaining contract argument validation`.
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`.
@@ -86,10 +87,11 @@ Current loop output so far:
18. C18: `isphere_receive_messages` gained `conversation_id` and `query` filters across source, MCP args, tests, and configured file/directory smoke verification. 18. C18: `isphere_receive_messages` gained `conversation_id` and `query` filters across source, MCP args, tests, and configured file/directory smoke verification.
19. C19: `isphere_receive_messages` gained RFC3339 `since` filtering against PacketReader epoch-millisecond timestamps plus invalid-since error handling. 19. C19: `isphere_receive_messages` gained RFC3339 `since` filtering against PacketReader epoch-millisecond timestamps plus invalid-since error handling.
20. C20: `isphere_receive_messages` now accepts and validates the remaining safe read-contract args, rejects unsupported source/cursor cases, and can suppress inline attachment metadata. 20. C20: `isphere_receive_messages` now accepts and validates the remaining safe read-contract args, rejects unsupported source/cursor cases, and can suppress inline attachment metadata.
21. C21: `isphere_search_contacts` and `isphere_search_groups` now accept and validate safe search-contract args, while rejecting unsupported source/cursor cases.
## Next business mainline ## Next business mainline
1. Run Stage C Loop C21: apply the same safe-argument validation pattern to `isphere_search_contacts` and `isphere_search_groups`. 1. Run Stage C Loop C22: apply the same safe-argument validation pattern to `isphere_receive_files` list mode while keeping download blocked.
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.

View File

@@ -185,8 +185,8 @@ Allowed parameters:
- `win_helper_scan_windows`: `include_all_visible` only. - `win_helper_scan_windows`: `include_all_visible` only.
- `win_helper_dump_uia`: `hwnd`, `max_depth`, `include_text`, `max_children` only. - `win_helper_dump_uia`: `hwnd`, `max_depth`, `include_text`, `max_children` only.
- `isphere_receive_messages`: `conversation_id`, `query`, `since`, `include_attachment_metadata`, `source_preference`, `preview`, `cursor`, `limit` only. `since` must be RFC3339/RFC3339Nano when supplied; `source_preference` currently supports empty/`auto`/`local_readonly`; `cursor` must be empty until pagination is implemented. - `isphere_receive_messages`: `conversation_id`, `query`, `since`, `include_attachment_metadata`, `source_preference`, `preview`, `cursor`, `limit` only. `since` must be RFC3339/RFC3339Nano when supplied; `source_preference` currently supports empty/`auto`/`local_readonly`; `cursor` must be empty until pagination is implemented.
- `isphere_search_contacts`: `query`, `limit` only. - `isphere_search_contacts`: `query`, `cursor`, `source_preference`, `include_inactive`, `limit` only. `source_preference` currently supports empty/`auto`/`local_readonly`; `cursor` must be empty until pagination is implemented.
- `isphere_search_groups`: `query`, `limit` only. - `isphere_search_groups`: `query`, `cursor`, `source_preference`, `include_archived`, `limit` only. `source_preference` currently supports empty/`auto`/`local_readonly`; `cursor` must be empty until pagination is implemented.
- `isphere_receive_files`: `conversation_id`, `message_id`, `file_id`, `name_contains`, `mode`, `limit` only. C14 supports `mode` empty or `list`; download is deferred. - `isphere_receive_files`: `conversation_id`, `message_id`, `file_id`, `name_contains`, `mode`, `limit` only. C14 supports `mode` empty or `list`; download is deferred.
## 9. Current tool surface and next-stage route ## 9. Current tool surface and next-stage route
@@ -202,7 +202,7 @@ The current runbook verifies eight tools:
- `isphere_search_groups` - `isphere_search_groups`
- `isphere_receive_files` - `isphere_receive_files`
The current log-backed business read tools have 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` or a directory of `.log`/`.txt` files via `ISPHERE_PACKET_LOG_DIR`; this is still not a production ingestion claim because raw evidence remains local and uncommitted. `isphere_receive_messages` now exposes contract-facing fields for digital-employee consumption while preserving older aliases for compatibility, supports `conversation_id`, keyword `query`, and RFC3339 `since` filtering, and validates the remaining safe contract args without pretending unsupported connectors/pagination exist. `isphere_receive_files` currently supports list mode only; download/cache mapping is deferred. The current log-backed business read tools have 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` or a directory of `.log`/`.txt` files via `ISPHERE_PACKET_LOG_DIR`; this is still not a production ingestion claim because raw evidence remains local and uncommitted. `isphere_receive_messages` now exposes contract-facing fields for digital-employee consumption while preserving older aliases for compatibility, supports `conversation_id`, keyword `query`, and RFC3339 `since` filtering, and validates the remaining safe contract args without pretending unsupported connectors/pagination exist. Contact and group search also accept the safe local-readonly contract args. `isphere_receive_files` currently supports list mode only; download/cache mapping is deferred.
Core business tools for contacts, groups, messages, and files are defined in `docs/mcp-core-tools-contract.md`. Send capabilities enter the roadmap through that contract after source discovery, connector selection, preview metadata, execution metadata, and audit records are in place. Core business tools for contacts, groups, messages, and files are defined in `docs/mcp-core-tools-contract.md`. Send capabilities enter the roadmap through that contract after source discovery, connector selection, preview metadata, execution metadata, and audit records are in place.

View File

@@ -18,8 +18,8 @@ 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; wrapped `MsgLib.db` after DB wrapper is solved | `docs/source-discovery/2026-07-09-n12-pre-zyl-schema-notes.md#field-mapping-evidence` | C20 supports and validates the current safe receive-message contract args; default source remains empty when unset | apply safe contract args to contact/group search | | `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; wrapped `MsgLib.db` after DB wrapper is solved | `docs/source-discovery/2026-07-09-n12-pre-zyl-schema-notes.md#field-mapping-evidence` | C20 supports and validates the current safe receive-message contract args; default source remains empty when unset | apply safe contract args to contact/group search |
| `isphere_search_contacts` | bare sender/receiver JIDs extracted from normalized log-backed messages loaded from configured PacketReader file or directory source | 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` | C16 verification covers default, configured-file, and configured-directory contact smoke; JID-only display/account fallback | enrich display names from roster/db/uia evidence | | `isphere_search_contacts` | bare sender/receiver JIDs extracted from normalized log-backed messages loaded from configured PacketReader file or directory source | 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` | C21 verifies safe search-contract args plus default/configured contact smokes; 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 loaded from configured PacketReader file or directory source | `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 | C16 verification covers default, configured-file, and configured-directory group smoke; JID-only display fallback | enrich group display/member data from roster/db/uia evidence | | `isphere_search_groups` | decrypted `PacketReader.ProcessPacket` `type="groupchat"` stanzas and conference/MUC JIDs loaded from configured PacketReader file or directory source | `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 | C21 verifies safe search-contract args plus default/configured group smokes; JID-only display fallback | enrich group display/member data from roster/db/uia 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` | C16 verification covers default, configured-file, and configured-directory file-list smoke; no log-to-cache/download mapping yet | defer download mapping; align read-tool contract 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` | C16 verification covers default, configured-file, and configured-directory file-list smoke; no log-to-cache/download mapping yet | defer download mapping; align read-tool contract output |
| `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 |

View File

@@ -1755,19 +1755,19 @@ Implemented and verified in this loop:
- Treat include flags as accepted no-op on local readonly JID-derived candidates because active/archive state is not available in current source. - Treat include flags as accepted no-op on local readonly JID-derived candidates because active/archive state is not available in current source.
- Do not add write/send/file-download behavior. - Do not add write/send/file-download behavior.
- [ ] **Step 1: Write failing contact/group MCP arg tests** - [x] **Step 1: Write failing contact/group MCP arg tests**
Extend contact/group tool tests with accepted safe args plus rejected unsupported source/cursor cases. Extend contact/group tool tests with accepted safe args plus rejected unsupported source/cursor cases.
- [ ] **Step 2: Implement validation helpers** - [x] **Step 2: Implement validation helpers**
Add minimal validation to contact and group tool handlers. Add minimal validation to contact and group tool handlers.
- [ ] **Step 3: Update verification and docs** - [x] **Step 3: Update verification and docs**
Update `scripts/verify-go-mcp.ps1`, runbook, status card, and this plan. Update `scripts/verify-go-mcp.ps1`, runbook, status card, and this plan.
- [ ] **Step 4: Run full verification and commit** - [x] **Step 4: Run full verification and commit**
Run: Run:
@@ -1782,6 +1782,67 @@ Expected: all exit 0. Remove any generated root `isphere-mcp.exe` after build. C
--- ---
## Loop C21 Result
Implemented and verified in this loop:
- `isphere_search_contacts` now accepts safe contract args `cursor`, `source_preference`, and `include_inactive`.
- `isphere_search_groups` now accepts safe contract args `cursor`, `source_preference`, and `include_archived`.
- Both tools accept empty/`auto`/`local_readonly` source preference and reject unsupported connector selections until those sources exist.
- Both tools accept empty cursor and reject non-empty cursor until pagination exists.
- Include flags are accepted no-ops for the current local-readonly JID-derived source because inactive/archive state is not available.
- `scripts/verify-go-mcp.ps1` now exercises these contact/group safe args in default and configured source smokes.
---
## Loop C22: File receive remaining contract argument validation
**Goal:** Apply the same safe-argument validation pattern to `isphere_receive_files` list mode, while continuing to reject download mode until cache/download mapping is validated.
**Planned files:**
- Modify: `internal/tools/isphere_files.go`
- Modify: `internal/tools/isphere_files_test.go`
- Modify: `scripts/verify-go-mcp.ps1`
- Modify: `docs/go-mcp-runbook.md`
- 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:**
- Add/validate safe args: `cursor`, `source_preference`, `preview`, and `output_dir`.
- Accept `source_preference` empty, `auto`, or `local_readonly`; reject bridge/uia/network until those connectors exist.
- Accept empty cursor only; reject non-empty cursor until pagination exists.
- Accept `preview` in list mode as a no-op because list mode is read-only.
- Accept empty `output_dir` in list mode; reject non-empty `output_dir` unless/until download mode is implemented to avoid implying files were written.
- Keep mode empty/`list` only; continue rejecting `download`.
- [ ] **Step 1: Write failing file MCP arg tests**
Extend `internal/tools/isphere_files_test.go` with accepted safe args and rejected unsupported source/cursor/output/download cases.
- [ ] **Step 2: Implement file arg validation**
Reuse local-readonly source/cursor validation and add output/download list-mode checks.
- [ ] **Step 3: Update verification and docs**
Update `scripts/verify-go-mcp.ps1`, runbook, status card, matrix, and this plan.
- [ ] **Step 4: Run full verification and commit**
Run:
```powershell
git diff --check
go test ./...
go build ./cmd/isphere-mcp
powershell -NoProfile -ExecutionPolicy Bypass -File scripts/verify-go-mcp.ps1
```
Expected: all exit 0. Remove any generated root `isphere-mcp.exe` after build. Commit exact C22 paths only and rewrite the next loop from actual results.
---
## Self-Review ## 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. - 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.

View File

@@ -14,6 +14,9 @@ const ToolNameSearchContacts = "isphere_search_contacts"
type SearchContactsArgs struct { type SearchContactsArgs struct {
Query string `json:"query" jsonschema:"contact search query; currently matches bare JIDs from local readonly message logs"` 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"` Limit int `json:"limit,omitempty" jsonschema:"maximum number of contacts to return; zero or negative returns all matches"`
Cursor string `json:"cursor,omitempty" jsonschema:"pagination cursor; currently only empty cursor is supported"`
SourcePreference string `json:"source_preference,omitempty" jsonschema:"source selector; currently supports auto or local_readonly only"`
IncludeInactive bool `json:"include_inactive,omitempty" jsonschema:"accepted for contract compatibility; local readonly message logs do not expose inactive state"`
} }
func RegisterISphereContactTools(server *mcp.Server, source ReceiveMessagesSource) { func RegisterISphereContactTools(server *mcp.Server, source ReceiveMessagesSource) {
@@ -25,6 +28,9 @@ func RegisterISphereContactTools(server *mcp.Server, source ReceiveMessagesSourc
Name: ToolNameSearchContacts, Name: ToolNameSearchContacts,
Description: "Search iSphere contact candidates from the configured read-only message source.", 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) { }, func(ctx context.Context, _ *mcp.CallToolRequest, input SearchContactsArgs) (*mcp.CallToolResult, map[string]any, error) {
if err := validateLocalReadonlySourceAndCursor(ToolNameSearchContacts, input.SourcePreference, input.Cursor); err != nil {
return nil, nil, err
}
started := time.Now().UTC() started := time.Now().UTC()
messages, err := source.ReceiveMessages(ctx, isphere.ReceiveMessagesQuery{Limit: 0}) messages, err := source.ReceiveMessages(ctx, isphere.ReceiveMessagesQuery{Limit: 0})
if err != nil { if err != nil {

View File

@@ -74,3 +74,50 @@ func TestISphereSearchContactsToolReturnsJIDContacts(t *testing.T) {
t.Fatalf("source queries = %+v, want one unbounded receive query", fake.queries) t.Fatalf("source queries = %+v, want one unbounded receive query", fake.queries)
} }
} }
func TestISphereSearchContactsToolValidatesContractArgs(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",
"source_preference": "local_readonly",
"include_inactive": true,
"cursor": "",
"limit": 10,
},
})
if err != nil {
t.Fatalf("call with safe contract args: %v", err)
}
if callResult.IsError {
t.Fatalf("call result is error: %+v", callResult)
}
bridgeResult, err := session.CallTool(context.Background(), &mcp.CallToolParams{
Name: ToolNameSearchContacts,
Arguments: map[string]any{"query": "alice", "source_preference": "bridge"},
})
if err == nil && !bridgeResult.IsError {
t.Fatalf("bridge source_preference was accepted before connector exists")
}
cursorResult, err := session.CallTool(context.Background(), &mcp.CallToolParams{
Name: ToolNameSearchContacts,
Arguments: map[string]any{"query": "alice", "cursor": "next-page"},
})
if err == nil && !cursorResult.IsError {
t.Fatalf("non-empty cursor was accepted before pagination exists")
}
}

View File

@@ -14,6 +14,9 @@ const ToolNameSearchGroups = "isphere_search_groups"
type SearchGroupsArgs struct { type SearchGroupsArgs struct {
Query string `json:"query" jsonschema:"group search query; currently matches bare group JIDs from local readonly message logs"` 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"` Limit int `json:"limit,omitempty" jsonschema:"maximum number of groups to return; zero or negative returns all matches"`
Cursor string `json:"cursor,omitempty" jsonschema:"pagination cursor; currently only empty cursor is supported"`
SourcePreference string `json:"source_preference,omitempty" jsonschema:"source selector; currently supports auto or local_readonly only"`
IncludeArchived bool `json:"include_archived,omitempty" jsonschema:"accepted for contract compatibility; local readonly message logs do not expose archived state"`
} }
func RegisterISphereGroupTools(server *mcp.Server, source ReceiveMessagesSource) { func RegisterISphereGroupTools(server *mcp.Server, source ReceiveMessagesSource) {
@@ -25,6 +28,9 @@ func RegisterISphereGroupTools(server *mcp.Server, source ReceiveMessagesSource)
Name: ToolNameSearchGroups, Name: ToolNameSearchGroups,
Description: "Search iSphere group candidates from the configured read-only message source.", 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) { }, func(ctx context.Context, _ *mcp.CallToolRequest, input SearchGroupsArgs) (*mcp.CallToolResult, map[string]any, error) {
if err := validateLocalReadonlySourceAndCursor(ToolNameSearchGroups, input.SourcePreference, input.Cursor); err != nil {
return nil, nil, err
}
started := time.Now().UTC() started := time.Now().UTC()
messages, err := source.ReceiveMessages(ctx, isphere.ReceiveMessagesQuery{Limit: 0}) messages, err := source.ReceiveMessages(ctx, isphere.ReceiveMessagesQuery{Limit: 0})
if err != nil { if err != nil {

View File

@@ -79,3 +79,51 @@ func TestISphereSearchGroupsToolReturnsJIDGroups(t *testing.T) {
t.Fatalf("source queries = %+v, want one unbounded receive query", fake.queries) t.Fatalf("source queries = %+v, want one unbounded receive query", fake.queries)
} }
} }
func TestISphereSearchGroupsToolValidatesContractArgs(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",
"source_preference": "local_readonly",
"include_archived": true,
"cursor": "",
"limit": 10,
},
})
if err != nil {
t.Fatalf("call with safe contract args: %v", err)
}
if callResult.IsError {
t.Fatalf("call result is error: %+v", callResult)
}
bridgeResult, err := session.CallTool(context.Background(), &mcp.CallToolParams{
Name: ToolNameSearchGroups,
Arguments: map[string]any{"query": "project", "source_preference": "bridge"},
})
if err == nil && !bridgeResult.IsError {
t.Fatalf("bridge source_preference was accepted before connector exists")
}
cursorResult, err := session.CallTool(context.Background(), &mcp.CallToolParams{
Name: ToolNameSearchGroups,
Arguments: map[string]any{"query": "project", "cursor": "next-page"},
})
if err == nil && !cursorResult.IsError {
t.Fatalf("non-empty cursor was accepted before pagination exists")
}
}

View File

@@ -58,14 +58,18 @@ func RegisterISphereReadTools(server *mcp.Server, source ReceiveMessagesSource)
} }
func validateReceiveMessagesArgs(input ReceiveMessagesArgs) error { func validateReceiveMessagesArgs(input ReceiveMessagesArgs) error {
sourcePreference := strings.ToLower(strings.TrimSpace(input.SourcePreference)) return validateLocalReadonlySourceAndCursor(ToolNameReceiveMessages, input.SourcePreference, input.Cursor)
}
func validateLocalReadonlySourceAndCursor(toolName string, sourcePreferenceValue string, cursorValue string) error {
sourcePreference := strings.ToLower(strings.TrimSpace(sourcePreferenceValue))
switch sourcePreference { switch sourcePreference {
case "", "auto", "local_readonly": case "", "auto", "local_readonly":
default: default:
return fmt.Errorf("isphere_receive_messages source_preference %q is not available; only auto and local_readonly are supported", input.SourcePreference) return fmt.Errorf("%s source_preference %q is not available; only auto and local_readonly are supported", toolName, sourcePreferenceValue)
} }
if strings.TrimSpace(input.Cursor) != "" { if strings.TrimSpace(cursorValue) != "" {
return fmt.Errorf("isphere_receive_messages cursor pagination is not available yet") return fmt.Errorf("%s cursor pagination is not available yet", toolName)
} }
return nil return nil
} }

View File

@@ -483,7 +483,13 @@ func verifyConfiguredDirectorySource(ctx context.Context) (int, int, int, int) {
} }
func verifySearchContacts(ctx context.Context, session *mcp.ClientSession, query string, step string, wantCount int) int { 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}}) callResult, err := session.CallTool(ctx, &mcp.CallToolParams{Name: "isphere_search_contacts", Arguments: map[string]any{
"query": query,
"source_preference": "local_readonly",
"include_inactive": true,
"cursor": "",
"limit": 5,
}})
if err != nil { if err != nil {
fail(step, err) fail(step, err)
} }
@@ -519,7 +525,13 @@ func verifySearchContacts(ctx context.Context, session *mcp.ClientSession, query
func verifySearchGroups(ctx context.Context, session *mcp.ClientSession, query string, step string, wantCount int) int { 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}}) callResult, err := session.CallTool(ctx, &mcp.CallToolParams{Name: "isphere_search_groups", Arguments: map[string]any{
"query": query,
"source_preference": "local_readonly",
"include_archived": true,
"cursor": "",
"limit": 5,
}})
if err != nil { if err != nil {
fail(step, err) fail(step, err)
} }