feat: validate contact and group search args
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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 |
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -12,8 +12,11 @@ import (
|
|||||||
const ToolNameSearchContacts = "isphere_search_contacts"
|
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 {
|
||||||
|
|||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,8 +12,11 @@ import (
|
|||||||
const ToolNameSearchGroups = "isphere_search_groups"
|
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 {
|
||||||
|
|||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user