feat: register isphere file receive list tool

This commit is contained in:
zhaoyilun
2026-07-10 00:53:04 +08:00
parent fe242145bb
commit bd03cf5d83
9 changed files with 327 additions and 34 deletions

View File

@@ -14,7 +14,7 @@ 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 seven read-oriented tools:
- The current MCP surface exposes eight read-oriented tools:
- `win_helper_version`
- `win_helper_self_check`
- `win_helper_scan_windows`
@@ -22,14 +22,15 @@ Remote base before local roadmap commits: `b2d839e Merge branch 'codex/n15-repor
- `isphere_receive_messages`
- `isphere_search_contacts`
- `isphere_search_groups`
- `isphere_receive_files`
- 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, seven-tool MCP surface, default empty-source receive/contact/group calls, and synthetic configured-source receive/contact/group 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, and synthetic configured-source receive/contact/group/file 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 message sending, file receiving/download, 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 yet implemented business MCP tools for message sending, file download, or file sending; `isphere_receive_messages`, `isphere_search_contacts`, `isphere_search_groups`, and `isphere_receive_files` list mode 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,8 +42,8 @@ Remote base before local roadmap commits: `b2d839e Merge branch 'codex/n15-repor
- The repository has a Go MCP + C# WinHelper foundation for controlled Windows-side read-only observation.
- 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 seven-tool surface.
- 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.
- The project has repeatable verification scripts for the helper/MCP foundation and the current eight-tool surface.
- The project now has a corrected business contract and plan for core MCP communication tools, plus registered receive/contact/group/file-list read tools that can use `ISPHERE_PACKET_LOG_FILE` for operator-local encrypted PacketReader log lines.
## N13/N14/N15 positioning
@@ -50,7 +51,7 @@ N13/N14/N15 are pre-business validation results. They can help identify UI eleme
## Current loop
Current loop: `Stage C / Loop C14 - register isphere_receive_files list mode`.
Current loop: `Stage C / Loop C15 - send message source evidence precheck`.
Plan file: `docs/superpowers/plans/2026-07-09-stage-c-log-backed-receive-messages-loop.md`.
@@ -75,11 +76,12 @@ Current loop output so far:
10. C10: log-backed group candidate extraction from groupchat/conference JIDs.
11. C11: `isphere_search_groups` MCP registration and seven-tool verification.
12. C12: file evidence precheck confirmed enough PacketReader/Smark evidence for file metadata listing, but not enough for real cache/download mapping.
13. C13: `internal/isphere.ListFilesFromMessages` extracts synthetic/redacted file metadata candidates from normalized messages; MCP registration is next.
13. C13: `internal/isphere.ListFilesFromMessages` extracts synthetic/redacted file metadata candidates from normalized messages.
14. C14: `isphere_receive_files` MCP list mode registered and verify-go-mcp now covers eight tools plus default/configured file-list smoke.
## Next business mainline
1. Run Stage C Loop C14: register `isphere_receive_files` list mode on top of `ListFilesFromMessages`.
2. Leave actual download/cache mapping as a later connector node until a message-to-cache hash or client download source is validated.
3. Add `isphere_send_message` and `isphere_send_file` after source discovery and connector selection.
1. Run Stage C Loop C15: verify available evidence for `isphere_send_message` connector selection before implementing any write-capable path.
2. Leave actual file download/cache mapping as a later connector node until a message-to-cache hash or client download source is validated.
3. Add `isphere_send_message` and `isphere_send_file` only after source discovery, preview semantics, execution metadata, and audit shape are defined.
4. Keep UIA selector/report work as fallback support, not as the primary roadmap.

View File

@@ -2,7 +2,7 @@
This runbook is for the first Go MCP phase of `isphere-ai-bridge`. It lets an operator build and verify the Windows helper and the Go MCP server without reading the source code.
Current scope: expose four WinHelper observation operations plus the first log-backed business read tool, `isphere_receive_messages`, through Go MCP. The receive tool is registered with an empty default source unless `ISPHERE_PACKET_LOG_FILE` points to an operator-local encrypted PacketReader log-line file.
Current scope: expose four WinHelper observation operations plus four log-backed business read tools through Go MCP: `isphere_receive_messages`, `isphere_search_contacts`, `isphere_search_groups`, and `isphere_receive_files` list mode. These business read tools use an empty default source unless `ISPHERE_PACKET_LOG_FILE` points to an operator-local encrypted PacketReader log-line file.
## 1. Prerequisites
@@ -90,14 +90,15 @@ 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 seven tools are exposed: four WinHelper observation tools plus `isphere_receive_messages`, `isphere_search_contacts`, and `isphere_search_groups`.
- Exactly eight tools are exposed: four WinHelper observation tools plus `isphere_receive_messages`, `isphere_search_contacts`, `isphere_search_groups`, and `isphere_receive_files`.
- `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.
- `isphere_receive_files` is callable in list mode and returns one file metadata candidate from the configured synthetic file-transfer 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.
- File download/cache mapping and send business tools remain later-stage work.
## 6. Configure optional message source
@@ -154,7 +155,7 @@ E:\coding\codex\isphere-ai-bridge\isphere-mcp.exe
## 8. Allowed tools
These seven tools are currently allowed:
These eight tools are currently allowed:
| Tool | Source/op | Purpose |
| --- | --- | --- |
@@ -165,6 +166,7 @@ These seven tools are currently allowed:
| `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. |
| `isphere_receive_files` | log-backed file-transfer messages | List file metadata candidates extracted from file-transfer message bodies. Download/cache mapping is not implemented yet. |
Allowed parameters:
@@ -175,10 +177,11 @@ Allowed parameters:
- `isphere_receive_messages`: `limit` only.
- `isphere_search_contacts`: `query`, `limit` only.
- `isphere_search_groups`: `query`, `limit` only.
- `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
The current runbook verifies seven tools:
The current runbook verifies eight tools:
- `win_helper_version`
- `win_helper_self_check`
@@ -186,10 +189,12 @@ The current runbook verifies seven tools:
- `win_helper_dump_uia`
- `isphere_receive_messages`
- `isphere_search_contacts`
- `isphere_search_groups`
- `isphere_receive_files`
`isphere_receive_messages` currently has parser/source/tool registration and an empty default server source. Raw N12-pre evidence remains under ignored `runs/` paths and is not committed. The current command entry point can read an operator-local encrypted PacketReader log-line file via `ISPHERE_PACKET_LOG_FILE`; this is still not a production ingestion claim because raw evidence remains local and uncommitted.
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`; this is still not a production ingestion claim because raw evidence remains local and uncommitted. `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/file 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.
## 10. Real-login UIA capture procedure
@@ -248,7 +253,7 @@ win_helper_scan_windows
win_helper_dump_uia
```
The MCP server also registers `isphere_receive_messages`, but its default source is empty and it should not be used as N12R UIA evidence collection.
The MCP server also registers the log-backed read tools (`isphere_receive_messages`, `isphere_search_contacts`, `isphere_search_groups`, and `isphere_receive_files`), but their default source is empty and they should not be used as N12R UIA evidence collection.
After receiving an N12R package, validate it with `docs/n12r-return-package-validation-checklist.md` before using it as evidence for selector-design review. Prefer the scripted read-only validator:

View File

@@ -20,7 +20,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 | 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 for metadata listing via `internal/isphere.ListFilesFromMessages`; `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` | C13 source behavior implemented for metadata listing; no log-to-cache/download mapping yet | register `isphere_receive_files` MCP list mode |
| `isphere_receive_files` | decrypted `PacketReader.ProcessPacket` file-transfer message stanzas via `internal/isphere.ListFilesFromMessages` and `isphere_receive_files` list mode; `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` | C14 MCP list mode registered and verify-go-mcp covers default/configured file-list smoke; no log-to-cache/download mapping yet | defer download mapping; run send-message source evidence precheck |
## Stage C entry rule

View File

@@ -1341,7 +1341,7 @@ Implemented and verified in this loop:
- Output shape: `ok`, `mode`, `files`, `next_cursor`, and `audit`, aligned with `docs/mcp-core-tools-contract.md`.
- Default empty source returns `ok: true`, `mode: "list"`, `files: []`.
- [ ] **Step 1: Write failing MCP tool test**
- [x] **Step 1: Write failing MCP tool test**
Create `internal/tools/isphere_files_test.go` and run:
@@ -1351,15 +1351,15 @@ go test ./internal/tools -run TestISphereReceiveFilesToolReturnsLogBackedFiles -
Expected initial failure: file receive tool registration does not exist.
- [ ] **Step 2: Implement file tool registration**
- [x] **Step 2: Implement file tool registration**
Register `isphere_receive_files` with source injection for tests. Use `ListFilesFromMessages` and return a contract-shaped map with minimal audit metadata.
- [ ] **Step 3: Wire server and verification**
- [x] **Step 3: Wire server and verification**
Register file receive in server construction. Update server tool-count tests and `scripts/verify-go-mcp.ps1` to expect eight tools and call `isphere_receive_files` against default empty source and synthetic configured file-transfer fixture.
- [ ] **Step 4: Run full verification and commit**
- [x] **Step 4: Run full verification and commit**
Run:
@@ -1374,6 +1374,56 @@ Expected: all exit 0. Remove any generated root `isphere-mcp.exe` after build. C
---
## Loop C14 Result
Implemented and verified in this loop:
- `internal/tools.RegisterISphereFileTools(server, source)` registers `isphere_receive_files` over the same receive-message source used by receive/contact/group tools.
- `internal/mcpserver.NewServerWithReceiveSource` now exposes eight tools: four WinHelper tools plus `isphere_receive_messages`, `isphere_search_contacts`, `isphere_search_groups`, and `isphere_receive_files`.
- `scripts/verify-go-mcp.ps1` now expects eight tools, allows the file-list read tool, and verifies default empty file results plus configured synthetic file-transfer extraction.
- `isphere_receive_files` currently supports list mode only; download/cache mapping remains deferred because C12 found no message-to-cache hash link.
- Verification completed for C14 with affected package tests and `powershell -NoProfile -ExecutionPolicy Bypass -File scripts/verify-go-mcp.ps1`; full verification is run again before commit.
---
## Loop C15: Send message source evidence precheck
**Goal:** Before implementing `isphere_send_message`, identify the lowest-risk connector path and preview/audit shape from existing bridge/API, local data, UIA, or network evidence; do not implement write-capable behavior without a validated connector decision.
**Planned files:**
- Modify: `docs/source-discovery/capability-source-matrix.md`
- Modify: `docs/current-status-card.md`
- Modify: `docs/superpowers/plans/2026-07-09-stage-c-log-backed-receive-messages-loop.md`
- Optionally create a focused source-discovery note under `docs/source-discovery/` if evidence is non-trivial.
**Planned behavior:**
- Inspect existing docs/contracts and committed helper/UIA/source-discovery artifacts for a send-message source candidate.
- Rank candidate connector paths: existing bridge/API/local service, UIA draft/send path, network/protocol path, or blocked pending internal sandbox evidence.
- If a safe connector exists, write C16 around preview-first `isphere_send_message` design or implementation.
- If evidence is insufficient, mark send-message blocked on live/internal-sandbox connector evidence and choose the next read hardening node.
- [ ] **Step 1: Inspect current source evidence for send-message connector candidates**
Use committed docs and generated selector/source-discovery reports only. Do not infer a write path from unrelated read evidence.
- [ ] **Step 2: Update matrix and next loop**
Record whether send-message implementation can proceed, and rewrite C16 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 C15 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.

View File

@@ -46,5 +46,6 @@ func NewServerWithReceiveSource(source tools.ReceiveMessagesSource) *mcp.Server
tools.RegisterISphereReadTools(server, source)
tools.RegisterISphereContactTools(server, source)
tools.RegisterISphereGroupTools(server, source)
tools.RegisterISphereFileTools(server, source)
return server
}

View File

@@ -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", "isphere_search_groups", "win_helper_dump_uia", "win_helper_scan_windows", "win_helper_self_check", "win_helper_version"}
wantNames := []string{"isphere_receive_files", "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)
}

View File

@@ -0,0 +1,96 @@
package tools
import (
"context"
"fmt"
"strings"
"time"
"github.com/modelcontextprotocol/go-sdk/mcp"
"isphere-ai-bridge/internal/isphere"
)
const ToolNameReceiveFiles = "isphere_receive_files"
type ReceiveFilesArgs struct {
ConversationID string `json:"conversation_id,omitempty" jsonschema:"conversation id filter for file listing"`
MessageID string `json:"message_id,omitempty" jsonschema:"message id filter for file listing"`
FileID string `json:"file_id,omitempty" jsonschema:"file id filter for file listing"`
NameContains string `json:"name_contains,omitempty" jsonschema:"case-insensitive filename substring filter"`
Mode string `json:"mode,omitempty" jsonschema:"file receive mode; C14 supports list only"`
Limit int `json:"limit,omitempty" jsonschema:"maximum number of files to return; zero or negative returns all matches"`
}
func RegisterISphereFileTools(server *mcp.Server, source ReceiveMessagesSource) {
if source == nil {
source = isphere.EncryptedPacketLogSource{}
}
mcp.AddTool[ReceiveFilesArgs, map[string]any](server, &mcp.Tool{
Name: ToolNameReceiveFiles,
Description: "List iSphere file metadata candidates from the configured read-only message source; download is deferred until cache mapping is validated.",
}, func(ctx context.Context, _ *mcp.CallToolRequest, input ReceiveFilesArgs) (*mcp.CallToolResult, map[string]any, error) {
started := time.Now().UTC()
mode := strings.ToLower(strings.TrimSpace(input.Mode))
if mode == "" {
mode = "list"
}
if mode != "list" {
return nil, nil, fmt.Errorf("isphere_receive_files mode %q is not available; only list is supported", input.Mode)
}
messages, err := source.ReceiveMessages(ctx, isphere.ReceiveMessagesQuery{Limit: 0})
if err != nil {
return nil, nil, err
}
files := isphere.ListFilesFromMessages(messages.Messages, isphere.ListFilesQuery{
ConversationID: input.ConversationID,
MessageID: input.MessageID,
FileID: input.FileID,
NameContains: input.NameContains,
Limit: input.Limit,
})
return nil, receiveFilesResultToMap(files, started, time.Now().UTC()), nil
})
}
func receiveFilesResultToMap(result isphere.ListFilesResult, started time.Time, finished time.Time) map[string]any {
files := make([]map[string]any, 0, len(result.Files))
for _, file := range result.Files {
files = append(files, map[string]any{
"file_id": file.FileID,
"message_id": nullableString(file.MessageID),
"conversation_id": nullableString(file.ConversationID),
"file_name": file.FileName,
"size_bytes": file.SizeBytes,
"mime_type": file.MimeType,
"created_at": nullableString(file.CreatedAt),
"download_ref": nullableString(file.DownloadRef),
"saved_path": nullableString(file.SavedPath),
"sha256": nullableString(file.SHA256),
"source": file.Source,
"raw_ref": file.RawRef,
})
}
return map[string]any{
"ok": true,
"mode": "list",
"files": files,
"next_cursor": nil,
"audit": map[string]any{
"tool": ToolNameReceiveFiles,
"source": "local_readonly",
"execution_mode": "read",
"started_at": started.Format(time.RFC3339Nano),
"finished_at": finished.Format(time.RFC3339Nano),
"result": "ok",
},
}
}
func nullableString(value string) any {
if strings.TrimSpace(value) == "" {
return nil
}
return value
}

View File

@@ -0,0 +1,90 @@
package tools
import (
"context"
"encoding/json"
"testing"
"github.com/modelcontextprotocol/go-sdk/mcp"
"isphere-ai-bridge/internal/isphere"
)
func TestISphereReceiveFilesToolReturnsLogBackedFiles(t *testing.T) {
fake := &fakeReceiveMessagesSource{
result: isphere.ReceiveMessagesResult{Messages: []isphere.Message{{
ID: "msg-file-1",
ConversationID: "alice@imopenfire1-lanzhou|bob@imopenfire1-lanzhou",
Text: "redacted-report.docx",
Timestamp: "1783423807000",
Subject: "FILE_TRANSFER_CANCEL",
}}},
}
session, cleanup := connectToolsTestSession(t, func(server *mcp.Server) {
RegisterISphereFileTools(server, fake)
})
defer cleanup()
callResult, err := session.CallTool(context.Background(), &mcp.CallToolParams{
Name: ToolNameReceiveFiles,
Arguments: map[string]any{"mode": "list", "name_contains": "report", "limit": 5},
})
if err != nil {
t.Fatalf("call %s: %v", ToolNameReceiveFiles, err)
}
if callResult.IsError {
t.Fatalf("call result is error: %+v", callResult)
}
var decoded struct {
OK bool `json:"ok"`
Mode string `json:"mode"`
Files []struct {
FileID string `json:"file_id"`
MessageID string `json:"message_id"`
ConversationID string `json:"conversation_id"`
FileName string `json:"file_name"`
SizeBytes *int64 `json:"size_bytes"`
MimeType string `json:"mime_type"`
CreatedAt string `json:"created_at"`
DownloadRef *string `json:"download_ref"`
SavedPath *string `json:"saved_path"`
SHA256 *string `json:"sha256"`
Source string `json:"source"`
} `json:"files"`
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 || decoded.Mode != "list" {
t.Fatalf("ok/mode = %v/%q in %s", decoded.OK, decoded.Mode, payload)
}
if len(decoded.Files) != 1 {
t.Fatalf("files = %+v, want one", decoded.Files)
}
file := decoded.Files[0]
if file.FileID != "msg-file-1:redacted-report.docx" || file.MessageID != "msg-file-1" || file.ConversationID != "alice@imopenfire1-lanzhou|bob@imopenfire1-lanzhou" {
t.Fatalf("unexpected file identity: %+v", file)
}
if file.FileName != "redacted-report.docx" || file.MimeType != "application/vnd.openxmlformats-officedocument.wordprocessingml.document" || file.CreatedAt != "1783423807000" {
t.Fatalf("unexpected file metadata: %+v", file)
}
if file.SizeBytes != nil || file.DownloadRef != nil || file.SavedPath != nil || file.SHA256 != nil {
t.Fatalf("download/cache fields should be unknown: %+v", file)
}
if file.Source != "local_readonly" {
t.Fatalf("source = %q", file.Source)
}
if decoded.Audit["tool"] != ToolNameReceiveFiles || decoded.Audit["result"] != "ok" || decoded.Audit["execution_mode"] != "read" {
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)
}
}

View File

@@ -9,7 +9,8 @@ $expectedTools = @(
"win_helper_dump_uia",
"isphere_receive_messages",
"isphere_search_contacts",
"isphere_search_groups"
"isphere_search_groups",
"isphere_receive_files"
)
$tempRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("isphere-go-mcp-verify-" + [guid]::NewGuid().ToString("N"))
@@ -83,9 +84,10 @@ var expectedTools = []string{
"isphere_receive_messages",
"isphere_search_contacts",
"isphere_search_groups",
"isphere_receive_files",
}
var forbiddenTerms = []string{"send", "file", "login", "conversation", "upload", "download", "autologin"}
var forbiddenTerms = []string{"send", "login", "conversation", "upload", "download", "autologin"}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
@@ -138,7 +140,7 @@ func main() {
sort.Strings(toolNames)
want := append([]string(nil), expectedTools...)
sort.Strings(want)
if len(toolNames) != 7 || !reflect.DeepEqual(toolNames, want) {
if len(toolNames) != 8 || !reflect.DeepEqual(toolNames, want) {
fail("tools/list", fmt.Errorf("tool names = %#v, want %#v", toolNames, want))
}
@@ -190,7 +192,8 @@ func main() {
}
defaultContactCount := verifySearchContacts(ctx, session, "sender", "tools/call isphere_search_contacts default", 0)
defaultGroupCount := verifySearchGroups(ctx, session, "project", "tools/call isphere_search_groups default", 0)
configuredReceiveCount, configuredContactCount, configuredGroupCount := verifyConfiguredReceiveSource(ctx)
defaultFileCount := verifyReceiveFiles(ctx, session, "report", "tools/call isphere_receive_files default", 0)
configuredReceiveCount, configuredContactCount, configuredGroupCount, configuredFileCount := verifyConfiguredReceiveSource(ctx)
result := map[string]any{
"ok": true,
@@ -202,9 +205,11 @@ func main() {
"receive_message_count": len(messagesValue),
"contact_count": defaultContactCount,
"group_count": defaultGroupCount,
"file_count": defaultFileCount,
"configured_receive_message_count": configuredReceiveCount,
"configured_contact_count": configuredContactCount,
"configured_group_count": configuredGroupCount,
"configured_file_count": configuredFileCount,
"packet_log_file_configured": os.Getenv("ISPHERE_PACKET_LOG_FILE") != "",
"real_isphere_login_required": false,
"python_runtime_required": false,
@@ -215,13 +220,13 @@ func main() {
}
func verifyConfiguredReceiveSource(ctx context.Context) (int, int, int) {
func verifyConfiguredReceiveSource(ctx context.Context) (int, int, int, int) {
plaintext := "--------------------------------------------------------------------------------------------------------------------------------------------\n" +
"2026/7/7 15:30:07\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" +
" <body>redacted-report.docx</body>\n" +
" <received xmlns=\"urn:xmpp:receipts\" id=\"receipt-fixture-1\" type=\"1\" stamp=\"\" />\n" +
" <subject>VERIFY_FIXTURE</subject>\n" +
" <subject>FILE_TRANSFER_CANCEL</subject>\n" +
" <isphere xmlns=\"isphere.im\" type=\"1001\" sendtime=\"1783423807000\" version=\"1\" />\n" +
" <readed />\n" +
"</message>"
@@ -306,7 +311,7 @@ func verifyConfiguredReceiveSource(ctx context.Context) (int, int, int) {
if !ok {
fail("fixture/assert", fmt.Errorf("message was not object: %#v", messagesValue[0]))
}
if message["id"] != "msg-fixture-1" || message["text"] != "redacted fixture" || message["subject"] != "VERIFY_FIXTURE" {
if message["id"] != "msg-fixture-1" || message["text"] != "redacted-report.docx" || message["subject"] != "FILE_TRANSFER_CANCEL" {
fail("fixture/assert", fmt.Errorf("unexpected message: %#v", message))
}
if message["conversation_id"] != "project-room@conference.imopenfire1-lanzhou|sender@imopenfire1-lanzhou" {
@@ -314,7 +319,8 @@ func verifyConfiguredReceiveSource(ctx context.Context) (int, int, int) {
}
contactCount := verifySearchContacts(ctx, session, "sender", "fixture/call isphere_search_contacts", 1)
groupCount := verifySearchGroups(ctx, session, "project", "fixture/call isphere_search_groups", 1)
return len(messagesValue), contactCount, groupCount
fileCount := verifyReceiveFiles(ctx, session, "report", "fixture/call isphere_receive_files", 1)
return len(messagesValue), contactCount, groupCount, fileCount
}
func verifySearchContacts(ctx context.Context, session *mcp.ClientSession, query string, step string, wantCount int) int {
@@ -388,6 +394,45 @@ func verifySearchGroups(ctx context.Context, session *mcp.ClientSession, query s
return len(groupsValue)
}
func verifyReceiveFiles(ctx context.Context, session *mcp.ClientSession, nameContains string, step string, wantCount int) int {
callResult, err := session.CallTool(ctx, &mcp.CallToolParams{Name: "isphere_receive_files", Arguments: map[string]any{"mode": "list", "name_contains": nameContains, "limit": 5}})
if err != nil {
fail(step, err)
}
if callResult.IsError {
fail(step, fmt.Errorf("tool returned isError=true: %#v", callResult.Content))
}
var filesPayload map[string]any
encoded, _ := json.Marshal(callResult.StructuredContent)
if err := json.Unmarshal(encoded, &filesPayload); err != nil {
fail(step, fmt.Errorf("decode structuredContent %s: %w", encoded, err))
}
if filesPayload["ok"] != true || filesPayload["mode"] != "list" {
fail(step, fmt.Errorf("ok/mode mismatch: %s", encoded))
}
filesValue, ok := filesPayload["files"].([]any)
if !ok {
fail(step, fmt.Errorf("files missing or not array: %s", encoded))
}
if len(filesValue) != wantCount {
fail(step, fmt.Errorf("files count = %d, want %d: %s", len(filesValue), wantCount, encoded))
}
if wantCount > 0 {
file, ok := filesValue[0].(map[string]any)
if !ok {
fail(step, fmt.Errorf("file was not object: %#v", filesValue[0]))
}
if file["file_id"] != "msg-fixture-1:redacted-report.docx" || file["file_name"] != "redacted-report.docx" || file["source"] != "local_readonly" {
fail(step, fmt.Errorf("unexpected file: %#v", file))
}
if file["download_ref"] != nil || file["saved_path"] != nil || file["sha256"] != nil {
fail(step, fmt.Errorf("download/cache fields should be nil: %#v", file))
}
}
return len(filesValue)
}
func encryptPacketLogLineForHarness(plaintext string) (string, error) {
block, err := des.NewCipher([]byte("hyhccdtm"))
if err != nil {
@@ -436,13 +481,15 @@ 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 7) "SDK harness tool_count mismatch: $($script:harnessJson.tool_count)"
Assert-True ($script:harnessJson.tool_count -eq 8) "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.file_count -eq 0) "SDK harness file_count mismatch: $($script:harnessJson.file_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)"
Assert-True ($script:harnessJson.configured_file_count -eq 1) "SDK harness configured_file_count mismatch: $($script:harnessJson.configured_file_count)"
[ordered]@{
ok = $true
@@ -454,9 +501,11 @@ func fail(step string, err error) {
receive_message_count = $script:harnessJson.receive_message_count
contact_count = $script:harnessJson.contact_count
group_count = $script:harnessJson.group_count
file_count = $script:harnessJson.file_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
configured_file_count = $script:harnessJson.configured_file_count
packet_log_file_configured = $script:harnessJson.packet_log_file_configured
python_runtime_required = $false
real_isphere_login_required = $false