feat: load packet logs from configured source

This commit is contained in:
zhaoyilun
2026-07-09 23:23:06 +08:00
parent 220f3f68c1
commit 28bcb94b41
10 changed files with 337 additions and 28 deletions

View File

@@ -15,7 +15,10 @@ func main() {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop() defer stop()
server := mcpserver.NewServer() server, err := mcpserver.NewServerFromEnv()
if err != nil {
log.Fatalf("configure isphere mcp server: %v", err)
}
if err := server.Run(ctx, &mcp.StdioTransport{}); err != nil { if err := server.Run(ctx, &mcp.StdioTransport{}); err != nil {
log.Fatalf("isphere mcp server stopped: %v", err) log.Fatalf("isphere mcp server stopped: %v", err)
} }

View File

@@ -21,13 +21,13 @@ Remote base before local roadmap commits: `b2d839e Merge branch 'codex/n15-repor
- `win_helper_dump_uia` - `win_helper_dump_uia`
- `isphere_receive_messages` - `isphere_receive_messages`
- C# `ISphereWinHelper` exists under `native/ISphereWinHelper` and uses the `isphere.helper.v1` stdin/stdout JSON contract. - C# `ISphereWinHelper` exists under `native/ISphereWinHelper` and uses the `isphere.helper.v1` stdin/stdout JSON contract.
- `scripts/verify-win-helper.ps1` and `scripts/verify-go-mcp.ps1` provide repeatable local checks for the helper, five-tool MCP surface, and empty-source `isphere_receive_messages` smoke call. - `scripts/verify-win-helper.ps1` and `scripts/verify-go-mcp.ps1` provide repeatable local checks for the helper, five-tool MCP surface, and default empty-source `isphere_receive_messages` smoke call.
- 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.
## What cannot be claimed externally ## What cannot be claimed externally
- The project has not yet implemented business MCP tools for contact search, group search, message sending, file receiving, or file sending; `isphere_receive_messages` is registered but still lacks a production loader/configuration path. - The project has not yet implemented business MCP tools for contact search, group search, message sending, file receiving, or file sending; `isphere_receive_messages` has an operator-local encrypted log-file loader but not a production ingestion path.
- The project has not completed production iSphere integration. - 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 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. - The project has not implemented real message sending or real file upload.
@@ -40,7 +40,7 @@ Remote base before local roadmap commits: `b2d839e Merge branch 'codex/n15-repor
- The helper protocol boundary is explicit and keeps Windows/UIA work separate from the Go MCP service layer. - The 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 current code can support future MCP business tools by routing low-level Windows/UIA fallback work through a bounded helper layer.
- The project has repeatable verification scripts for the helper/MCP foundation and the current five-tool surface. - The project has repeatable verification scripts for the helper/MCP foundation and the current five-tool surface.
- The project now has a corrected business contract and plan for core MCP communication tools, plus a registered first read tool for receiving messages from a future configured source. - The project now has a corrected business contract and plan for core MCP communication tools, plus a registered first read tool that can use `ISPHERE_PACKET_LOG_FILE` for operator-local encrypted PacketReader log lines.
## N13/N14/N15 positioning ## N13/N14/N15 positioning
@@ -48,7 +48,7 @@ N13/N14/N15 are pre-business validation results. They can help identify UI eleme
## Current loop ## Current loop
Current loop: `Stage C / Loop C4 - operator verification and docs`. Current loop: `Stage C / Loop C6 - fixture-backed operator smoke for configured receive source`.
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`.
@@ -64,12 +64,14 @@ Current loop output so far:
1. C1: `internal/isphere/logcodec.DecryptLine` and `internal/isphere/packetlog.ParseMessageLog` for redacted PacketReader message logs. 1. C1: `internal/isphere/logcodec.DecryptLine` and `internal/isphere/packetlog.ParseMessageLog` for redacted PacketReader message logs.
2. C2: `internal/isphere.EncryptedPacketLogSource.ReceiveMessages` source abstraction with synthetic/redacted encrypted fixtures. 2. C2: `internal/isphere.EncryptedPacketLogSource.ReceiveMessages` source abstraction with synthetic/redacted encrypted fixtures.
3. C3: `isphere_receive_messages` MCP tool registration and server tool list update. 3. C3: `isphere_receive_messages` MCP tool registration and server tool list update.
4. C4: updating repeatable operator verification and docs for the five-tool surface. 4. C4: repeatable operator verification and docs for the five-tool surface.
5. C5: safe `ISPHERE_PACKET_LOG_FILE` loader for operator-local encrypted PacketReader log lines.
6. C6: fixture-backed operator smoke for configured receive source.
## Next business mainline ## Next business mainline
1. Finish Stage C Loop C4: verify and document the five-tool MCP surface. 1. Finish Stage C Loop C6: add fixture-backed configured-source operator smoke.
2. Implement the next loop: safe configurable source loader or fixture path for `isphere_receive_messages`, without committing raw evidence. 2. Rewrite the next loop around either production-safe source configuration hardening or the next read capability, based on C6 results.
3. Add contact/group/file read tools after their source decisions are recorded. 3. Add contact/group/file read tools after their source decisions are recorded.
4. Add `isphere_send_message` and `isphere_send_file` after source discovery and connector selection. 4. Add `isphere_send_message` and `isphere_send_file` after source discovery and connector selection.
5. Keep UIA selector/report work as fallback support, not as the primary roadmap. 5. 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. 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 until a safe evidence loader is wired. 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.
## 1. Prerequisites ## 1. Prerequisites
@@ -96,7 +96,25 @@ The verification confirms:
- Verification uses synthetic/local helper checks and does not load raw N12-pre evidence. - 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. - Contact/group/file/search/send business tools remain later-stage work.
## 6. Configure MCP client command ## 6. Configure optional message source
By default, `isphere_receive_messages` is registered but uses an empty source, so it returns:
```json
{"messages":[]}
```
To point the server at a local encrypted PacketReader log-line file, set:
```powershell
$env:ISPHERE_PACKET_LOG_FILE = "E:\coding\codex\isphere-ai-bridge\runs\offline-evidence-intake\<evidence-id>\packet-reader-lines.txt"
```
The file must contain one encrypted Base64 log line per line. Blank lines are ignored. Keep this file under ignored `runs/` paths or another operator-local location; do not commit raw logs or decrypted message content.
Then start the MCP server normally. The command entry point uses `ISPHERE_PACKET_LOG_FILE` if it is set; the repeatable verification script clears it so the default smoke test remains deterministic.
## 7. Configure MCP client command
After building a stable binary, configure your MCP client to run the Go MCP executable as a stdio server. After building a stable binary, configure your MCP client to run the Go MCP executable as a stdio server.
@@ -131,7 +149,7 @@ If you used `go build ./cmd/isphere-mcp` without `-o`, point the command to:
E:\coding\codex\isphere-ai-bridge\isphere-mcp.exe E:\coding\codex\isphere-ai-bridge\isphere-mcp.exe
``` ```
## 7. Allowed tools ## 8. Allowed tools
These five tools are currently allowed: These five tools are currently allowed:
@@ -151,7 +169,7 @@ Allowed parameters:
- `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`: `limit` only. - `isphere_receive_messages`: `limit` only.
## 8. Current tool surface and next-stage route ## 9. Current tool surface and next-stage route
The current runbook verifies five tools: The current runbook verifies five tools:
@@ -161,11 +179,11 @@ The current runbook verifies five tools:
- `win_helper_dump_uia` - `win_helper_dump_uia`
- `isphere_receive_messages` - `isphere_receive_messages`
`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 next implementation slice should wire a safe configurable loader or fixture path before claiming production message ingestion. `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.
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/file capabilities enter the roadmap through that contract after source discovery, connector selection, preview metadata, execution metadata, and audit records are in place.
## 9. Real-login UIA capture procedure ## 10. Real-login UIA capture procedure
This is a later real-login capture gate used to collect live UI structure for connector work. This is a later real-login capture gate used to collect live UI structure for connector work.
@@ -206,7 +224,7 @@ Procedure:
8. Review and redact any sensitive content before sharing reports. 8. Review and redact any sensitive content before sharing reports.
## 10. If this outer-network environment cannot log in ## 11. If this outer-network environment cannot log in
iSphere is internal-network only. If the current repository environment is outside that network, use two separate routes: iSphere is internal-network only. If the current repository environment is outside that network, use two separate routes:
@@ -237,7 +255,7 @@ powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\validate-n12r-retu
Keep the validation report outside `$captureRoot`; the returned package is evidence input and should remain unchanged. Keep the validation report outside `$captureRoot`; the returned package is evidence input and should remain unchanged.
## 11. Troubleshooting ## 12. Troubleshooting
- If C# helper build fails, run `scripts\build-win-helper.ps1` directly and check for missing .NET Framework reference assemblies. - If C# helper build fails, run `scripts\build-win-helper.ps1` directly and check for missing .NET Framework reference assemblies.
- If `win_helper_version` fails, rerun `powershell -NoProfile -ExecutionPolicy Bypass -File scripts\verify-win-helper.ps1` first. - If `win_helper_version` fails, rerun `powershell -NoProfile -ExecutionPolicy Bypass -File scripts\verify-win-helper.ps1` first.

View File

@@ -17,7 +17,7 @@ Schema notes: `docs/source-discovery/2026-07-09-n12-pre-zyl-schema-notes.md`
| MCP tool | Primary source | Fallback source | Evidence file | Decision status | Next implementation slice | | 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` | 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` | C3 registered and unit-verified; default server source is empty until operator verification/loader wiring | fixture-backed operator verification and docs | | `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` | decrypted packet/send-receive JIDs and later roster 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` | candidate pending parser coverage | second | | `isphere_search_contacts` | decrypted packet/send-receive JIDs and later roster 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` | candidate pending parser coverage | second |
| `isphere_search_groups` | decrypted XMPP group/message stanzas if groupchat/group JIDs appear | `MsgLib.db` group/conversation tables after DB wrapper is solved; UIA helper source if group display names are missing | `docs/source-discovery/2026-07-09-n12-pre-zyl-schema-notes.md#field-mapping-evidence` | candidate pending group stanza evidence | third | | `isphere_search_groups` | decrypted XMPP group/message stanzas if groupchat/group JIDs appear | `MsgLib.db` group/conversation tables after DB wrapper is solved; UIA helper source if group display names are missing | `docs/source-discovery/2026-07-09-n12-pre-zyl-schema-notes.md#field-mapping-evidence` | candidate pending group stanza evidence | third |
| `isphere_receive_files` | decrypted `PacketReader.ProcessPacket` file-transfer message stanzas plus `zyl\importal\<hash>` cache mapping | decrypted `Smark.SendReceive` and `SaveToDB` traces for file-reference reconciliation | `docs/source-discovery/2026-07-09-n12-pre-zyl-schema-notes.md#field-mapping-evidence` | candidate pending cache-reference mapping | fourth | | `isphere_receive_files` | decrypted `PacketReader.ProcessPacket` file-transfer message stanzas plus `zyl\importal\<hash>` cache mapping | decrypted `Smark.SendReceive` and `SaveToDB` traces for file-reference reconciliation | `docs/source-discovery/2026-07-09-n12-pre-zyl-schema-notes.md#field-mapping-evidence` | candidate pending cache-reference mapping | fourth |

View File

@@ -795,7 +795,7 @@ Implemented and verified in this loop:
- Configured behavior: env var set to a local text file means the server reads non-empty encrypted Base64 lines from that file into `isphere.EncryptedPacketLogSource{Lines: lines}`. - Configured behavior: env var set to a local text file means the server reads non-empty encrypted Base64 lines from that file into `isphere.EncryptedPacketLogSource{Lines: lines}`.
- Evidence rule: the configured file should live under ignored `runs/` or another operator-local path; the loader must not require or commit raw evidence. - Evidence rule: the configured file should live under ignored `runs/` or another operator-local path; the loader must not require or commit raw evidence.
- [ ] **Step 1: Write failing file loader test** - [x] **Step 1: Write failing file loader test**
Create `internal/isphere/file_source_test.go` first and run: Create `internal/isphere/file_source_test.go` first and run:
@@ -805,23 +805,23 @@ go test ./internal/isphere -run TestLoadEncryptedPacketLogSourceFromFileReadsNon
Expected initial failure: loader function/type does not exist. Expected initial failure: loader function/type does not exist.
- [ ] **Step 2: Implement file loader** - [x] **Step 2: Implement file loader**
Implement a small loader that reads a UTF-8 text file, trims whitespace, skips blank lines, and returns `EncryptedPacketLogSource{Lines: lines}`. It should return clear path/context errors for missing or unreadable files. Implement a small loader that reads a UTF-8 text file, trims whitespace, skips blank lines, and returns `EncryptedPacketLogSource{Lines: lines}`. It should return clear path/context errors for missing or unreadable files.
- [ ] **Step 3: Write failing env-wired server test** - [x] **Step 3: Write failing env-wired server test**
Update `internal/mcpserver/server_test.go` with a temp encrypted fixture file and a test that sets `ISPHERE_PACKET_LOG_FILE`, constructs the env-wired server, calls `isphere_receive_messages`, and expects one redacted message. Run the specific test before implementation. Update `internal/mcpserver/server_test.go` with a temp encrypted fixture file and a test that sets `ISPHERE_PACKET_LOG_FILE`, constructs the env-wired server, calls `isphere_receive_messages`, and expects one redacted message. Run the specific test before implementation.
- [ ] **Step 4: Implement env-wired server construction** - [x] **Step 4: Implement env-wired server construction**
Add `NewServerFromEnv()` or equivalent env-aware construction. Keep `NewServer()` as the empty default for existing tests. Update `cmd/isphere-mcp/main.go` to use the env-aware server. Add `NewServerFromEnv()` or equivalent env-aware construction. Keep `NewServer()` as the empty default for existing tests. Update `cmd/isphere-mcp/main.go` to use the env-aware server.
- [ ] **Step 5: Update operator verification/docs** - [x] **Step 5: Update operator verification/docs**
Extend `scripts/verify-go-mcp.ps1` so default verification still asserts zero messages when no env var is set. Document optional `ISPHERE_PACKET_LOG_FILE` usage in `docs/go-mcp-runbook.md` and update the status card. Extend `scripts/verify-go-mcp.ps1` so default verification still asserts zero messages when no env var is set. Document optional `ISPHERE_PACKET_LOG_FILE` usage in `docs/go-mcp-runbook.md` and update the status card.
- [ ] **Step 6: Run full verification and commit** - [x] **Step 6: Run full verification and commit**
Run: Run:
@@ -836,6 +836,67 @@ Expected: all exit 0. Remove any generated root `isphere-mcp.exe` after build. C
--- ---
## Loop C5 Result
Implemented and verified in this loop:
- `internal/isphere.LoadEncryptedPacketLogSourceFromFile(path)` reads non-empty encrypted Base64 PacketReader log lines from an operator-local text file and returns `EncryptedPacketLogSource{Lines: lines}`.
- `internal/mcpserver.NewServerFromEnv()` reads `ISPHERE_PACKET_LOG_FILE` when set, otherwise preserves the empty default receive source.
- `cmd/isphere-mcp/main.go` now uses the env-aware server constructor and fails fast on invalid configuration.
- `scripts/verify-go-mcp.ps1` now exercises `NewServerFromEnv()` with `ISPHERE_PACKET_LOG_FILE` cleared and confirms the deterministic empty-source behavior.
- Docs now explain optional `ISPHERE_PACKET_LOG_FILE` usage and reinforce that raw evidence must remain under ignored/operator-local paths.
- Verification completed for C5 with `git diff --check`, `go test ./...`, `go build ./cmd/isphere-mcp`, and `powershell -NoProfile -ExecutionPolicy Bypass -File scripts/verify-go-mcp.ps1`; the generated root `isphere-mcp.exe` build artifact was removed.
---
## Loop C6: Fixture-backed operator smoke for configured receive source
**Goal:** Extend repeatable operator verification so a synthetic encrypted PacketReader fixture file proves `ISPHERE_PACKET_LOG_FILE` can drive `isphere_receive_messages` end-to-end without committing raw evidence.
**Planned files:**
- Modify: `scripts/verify-go-mcp.ps1`
- Modify: `docs/go-mcp-runbook.md`
- Modify: `docs/current-status-card.md`
- Modify: `docs/superpowers/plans/2026-07-09-stage-c-log-backed-receive-messages-loop.md`
**Planned behavior:**
- Existing default smoke remains: unset `ISPHERE_PACKET_LOG_FILE` returns `messages: []`.
- New fixture smoke creates a temporary synthetic/redacted PacketReader plaintext, encrypts it with the known log policy inside the temporary Go harness, writes one encrypted line to a temp file, sets `ISPHERE_PACKET_LOG_FILE` to that temp file, starts `mcpserver.NewServerFromEnv()`, calls `isphere_receive_messages`, and asserts one normalized message.
- No raw N12-pre evidence or decrypted user content is written to the repo.
- [ ] **Step 1: Capture current fixture-smoke gap**
Run the current verification script and note that it only proves the default empty source, not configured fixture ingestion:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File scripts/verify-go-mcp.ps1
```
Expected: PASS with `receive_message_count: 0`, but no configured-source proof yet.
- [ ] **Step 2: Update the temporary Go harness**
Extend the generated harness with a second server session that sets `ISPHERE_PACKET_LOG_FILE` to a temp synthetic encrypted log-line file and asserts one returned message with redacted content.
- [ ] **Step 3: Update docs**
Update the runbook/status card to say verification covers both default empty source and synthetic configured-source smoke.
- [ ] **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 C6 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

@@ -0,0 +1,36 @@
package isphere
import (
"bufio"
"fmt"
"os"
"strings"
)
func LoadEncryptedPacketLogSourceFromFile(path string) (EncryptedPacketLogSource, error) {
trimmedPath := strings.TrimSpace(path)
if trimmedPath == "" {
return EncryptedPacketLogSource{}, fmt.Errorf("packet log file path is empty")
}
file, err := os.Open(trimmedPath)
if err != nil {
return EncryptedPacketLogSource{}, fmt.Errorf("open packet log file %q: %w", trimmedPath, err)
}
defer file.Close()
scanner := bufio.NewScanner(file)
scanner.Buffer(make([]byte, 64*1024), 16*1024*1024)
lines := []string{}
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" {
continue
}
lines = append(lines, line)
}
if err := scanner.Err(); err != nil {
return EncryptedPacketLogSource{}, fmt.Errorf("read packet log file %q: %w", trimmedPath, err)
}
return EncryptedPacketLogSource{Lines: lines}, nil
}

View File

@@ -0,0 +1,29 @@
package isphere
import (
"os"
"reflect"
"testing"
)
func TestLoadEncryptedPacketLogSourceFromFileReadsNonEmptyLines(t *testing.T) {
path := writePacketLogFileForTest(t, "\n encrypted-line-1 \r\n\r\n\tencrypted-line-2\n")
got, err := LoadEncryptedPacketLogSourceFromFile(path)
if err != nil {
t.Fatalf("LoadEncryptedPacketLogSourceFromFile returned error: %v", err)
}
want := EncryptedPacketLogSource{Lines: []string{"encrypted-line-1", "encrypted-line-2"}}
if !reflect.DeepEqual(got, want) {
t.Fatalf("source = %#v, want %#v", got, want)
}
}
func writePacketLogFileForTest(t *testing.T, content string) string {
t.Helper()
path := t.TempDir() + "\\packet.log"
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
t.Fatalf("write test packet log: %v", err)
}
return path
}

View File

@@ -1,6 +1,10 @@
package mcpserver package mcpserver
import ( import (
"fmt"
"os"
"strings"
"github.com/modelcontextprotocol/go-sdk/mcp" "github.com/modelcontextprotocol/go-sdk/mcp"
"isphere-ai-bridge/internal/helperclient" "isphere-ai-bridge/internal/helperclient"
@@ -12,15 +16,33 @@ const (
ServerName = "isphere-ai-bridge" ServerName = "isphere-ai-bridge"
ServerTitle = "iSphere AI Bridge" ServerTitle = "iSphere AI Bridge"
ServerVersion = "0.1.0" ServerVersion = "0.1.0"
EnvPacketLogFileKey = "ISPHERE_PACKET_LOG_FILE"
) )
func NewServer() *mcp.Server { func NewServer() *mcp.Server {
return NewServerWithReceiveSource(isphere.EncryptedPacketLogSource{})
}
func NewServerFromEnv() (*mcp.Server, error) {
var source tools.ReceiveMessagesSource = isphere.EncryptedPacketLogSource{}
packetLogFile := strings.TrimSpace(os.Getenv(EnvPacketLogFileKey))
if packetLogFile != "" {
loaded, err := isphere.LoadEncryptedPacketLogSourceFromFile(packetLogFile)
if err != nil {
return nil, fmt.Errorf("configure %s: %w", EnvPacketLogFileKey, err)
}
source = loaded
}
return NewServerWithReceiveSource(source), nil
}
func NewServerWithReceiveSource(source tools.ReceiveMessagesSource) *mcp.Server {
server := mcp.NewServer(&mcp.Implementation{ server := mcp.NewServer(&mcp.Implementation{
Name: ServerName, Name: ServerName,
Title: ServerTitle, Title: ServerTitle,
Version: ServerVersion, Version: ServerVersion,
}, nil) }, nil)
tools.RegisterWinHelperTools(server, helperclient.Client{}) tools.RegisterWinHelperTools(server, helperclient.Client{})
tools.RegisterISphereReadTools(server, isphere.EncryptedPacketLogSource{}) tools.RegisterISphereReadTools(server, source)
return server return server
} }

View File

@@ -2,6 +2,11 @@ package mcpserver
import ( import (
"context" "context"
"crypto/cipher"
"crypto/des"
"encoding/base64"
"encoding/json"
"os"
"reflect" "reflect"
"sort" "sort"
"testing" "testing"
@@ -57,3 +62,127 @@ func TestNewServerRegistersN8WinHelperToolsWithoutCallingHelper(t *testing.T) {
t.Fatalf("server tool names = %#v, want %#v", gotNames, wantNames) t.Fatalf("server tool names = %#v, want %#v", gotNames, wantNames)
} }
} }
func TestNewServerFromEnvUsesPacketLogFile(t *testing.T) {
plaintext := `--------------------------------------------------------------------------------------------------------------------------------------------
2026/7/7 15:30:07
<message id="msg-env-1" from="sender@imopenfire1-lanzhou/imp_pc_4.1.2.6842" to="receiver@imopenfire1-lanzhou" type="chat">
<body>redacted from env</body>
<received xmlns="urn:xmpp:receipts" id="receipt-env-1" type="1" stamp="" />
<subject>ENV_SOURCE</subject>
<isphere xmlns="isphere.im" type="1001" sendtime="1783423807000" version="1" />
<readed />
</message>`
filePath := writeEncryptedPacketLogFileForServerTest(t, plaintext)
t.Setenv("ISPHERE_PACKET_LOG_FILE", filePath)
server, err := NewServerFromEnv()
if err != nil {
t.Fatalf("NewServerFromEnv returned error: %v", err)
}
session, cleanup := connectServerTestSession(t, server)
defer cleanup()
callResult, err := session.CallTool(context.Background(), &mcp.CallToolParams{
Name: "isphere_receive_messages",
Arguments: map[string]any{"limit": 10},
})
if err != nil {
t.Fatalf("call isphere_receive_messages: %v", err)
}
if callResult.IsError {
t.Fatalf("call result is error: %+v", callResult)
}
var decoded struct {
Messages []struct {
ID string `json:"id"`
Text string `json:"text"`
Subject string `json:"subject"`
ConversationID string `json:"conversation_id"`
SenderID string `json:"sender_id"`
ReceiverID string `json:"receiver_id"`
Read bool `json:"read"`
} `json:"messages"`
}
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 len(decoded.Messages) != 1 {
t.Fatalf("message count = %d, want 1", len(decoded.Messages))
}
msg := decoded.Messages[0]
if msg.ID != "msg-env-1" || msg.Text != "redacted from env" || msg.Subject != "ENV_SOURCE" {
t.Fatalf("unexpected message: %+v", msg)
}
if msg.ConversationID != "sender@imopenfire1-lanzhou|receiver@imopenfire1-lanzhou" {
t.Fatalf("conversation id = %q", msg.ConversationID)
}
if msg.SenderID != "sender@imopenfire1-lanzhou" || msg.ReceiverID != "receiver@imopenfire1-lanzhou" || !msg.Read {
t.Fatalf("metadata = %+v", msg)
}
}
func connectServerTestSession(t *testing.T, server *mcp.Server) (*mcp.ClientSession, func()) {
t.Helper()
ctx, cancel := context.WithCancel(context.Background())
serverTransport, clientTransport := mcp.NewInMemoryTransports()
errCh := make(chan error, 1)
go func() {
errCh <- server.Run(ctx, serverTransport)
}()
client := mcp.NewClient(&mcp.Implementation{Name: "mcpserver-env-test-client", Version: "0.0.0"}, nil)
session, err := client.Connect(ctx, clientTransport, nil)
if err != nil {
cancel()
t.Fatalf("connect client: %v", err)
}
cleanup := func() {
_ = session.Close()
cancel()
select {
case <-errCh:
case <-time.After(2 * time.Second):
t.Fatalf("server did not stop")
}
}
return session, cleanup
}
func writeEncryptedPacketLogFileForServerTest(t *testing.T, plaintext string) string {
t.Helper()
path := t.TempDir() + "\\packet-reader.log"
line := encryptPacketLogLineForServerTest(t, plaintext)
if err := os.WriteFile(path, []byte("\n"+line+"\n"), 0o600); err != nil {
t.Fatalf("write encrypted packet log file: %v", err)
}
return path
}
func encryptPacketLogLineForServerTest(t *testing.T, plaintext string) string {
t.Helper()
block, err := des.NewCipher([]byte("hyhccdtm"))
if err != nil {
t.Fatalf("NewCipher: %v", err)
}
plain := padPacketLogLineForServerTest([]byte(plaintext), des.BlockSize)
ciphertext := make([]byte, len(plain))
iv := []byte{0x12, 0x34, 0x56, 0x78, 0x90, 0xAB, 0xCD, 0xEF}
cipher.NewCBCEncrypter(block, iv).CryptBlocks(ciphertext, plain)
return base64.StdEncoding.EncodeToString(ciphertext)
}
func padPacketLogLineForServerTest(data []byte, blockSize int) []byte {
pad := blockSize - len(data)%blockSize
out := append([]byte(nil), data...)
for i := 0; i < pad; i++ {
out = append(out, byte(pad))
}
return out
}

View File

@@ -84,8 +84,15 @@ func main() {
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel() defer cancel()
if err := os.Unsetenv("ISPHERE_PACKET_LOG_FILE"); err != nil {
fail("server/env", err)
}
serverTransport, clientTransport := mcp.NewInMemoryTransports() serverTransport, clientTransport := mcp.NewInMemoryTransports()
server := mcpserver.NewServer() server, err := mcpserver.NewServerFromEnv()
if err != nil {
fail("server/config", err)
}
serverErr := make(chan error, 1) serverErr := make(chan error, 1)
go func() { go func() {
serverErr <- server.Run(ctx, serverTransport) serverErr <- server.Run(ctx, serverTransport)
@@ -183,6 +190,7 @@ func main() {
"tools": toolNames, "tools": toolNames,
"helper_name": helperName, "helper_name": helperName,
"receive_message_count": len(messagesValue), "receive_message_count": len(messagesValue),
"packet_log_file_configured": os.Getenv("ISPHERE_PACKET_LOG_FILE") != "",
"real_isphere_login_required": false, "real_isphere_login_required": false,
"python_runtime_required": false, "python_runtime_required": false,
"action_tools_present": false, "action_tools_present": false,
@@ -229,6 +237,7 @@ func fail(step string, err error) {
tool_count = $script:harnessJson.tool_count tool_count = $script:harnessJson.tool_count
tools = $script:harnessJson.tools tools = $script:harnessJson.tools
receive_message_count = $script:harnessJson.receive_message_count receive_message_count = $script:harnessJson.receive_message_count
packet_log_file_configured = $script:harnessJson.packet_log_file_configured
python_runtime_required = $false python_runtime_required = $false
real_isphere_login_required = $false real_isphere_login_required = $false
action_tools_present = $false action_tools_present = $false