feat: wire explicit msglib receive source

This commit is contained in:
zhaoyilun
2026-07-10 09:11:22 +08:00
parent 3f270b454f
commit 5a1b85906b
8 changed files with 250 additions and 25 deletions

View File

@@ -24,6 +24,15 @@ const (
EnvMsgLibDBKey = "ISPHERE_MSGLIB_DB"
)
type msgLibClient interface {
tools.DisplayEntitySource
isphere.MsgLibMessageLister
}
var newMsgLibClient = func(cfg msglib.Config) msgLibClient {
return msglib.NewClient(cfg)
}
func NewServer() *mcp.Server {
return NewServerWithReceiveSource(isphere.EncryptedPacketLogSource{})
}
@@ -48,14 +57,14 @@ func NewServerFromEnv() (*mcp.Server, error) {
}
source = loaded
}
displaySource, err := msgLibDisplayEntitySourceFromEnv()
displaySource, msglibReceiveSource, err := msgLibSourcesFromEnv()
if err != nil {
return nil, err
}
return NewServerWithSources(source, displaySource), nil
return NewServerWithSourcesAndMsgLibReceive(source, displaySource, msglibReceiveSource), nil
}
func msgLibDisplayEntitySourceFromEnv() (tools.DisplayEntitySource, error) {
func msgLibSourcesFromEnv() (tools.DisplayEntitySource, tools.ReceiveMessagesSource, error) {
sidecarExe := strings.TrimSpace(os.Getenv(EnvMsgLibSidecarExeKey))
sqliteDLL := strings.TrimSpace(os.Getenv(EnvMsgLibSQLiteDLLKey))
dbPath := strings.TrimSpace(os.Getenv(EnvMsgLibDBKey))
@@ -66,16 +75,21 @@ func msgLibDisplayEntitySourceFromEnv() (tools.DisplayEntitySource, error) {
}
}
if configured == 0 {
return nil, nil
return nil, nil, nil
}
if configured != 3 {
return nil, fmt.Errorf("configure MsgLib source: set all of %s, %s, and %s, or set none", EnvMsgLibSidecarExeKey, EnvMsgLibSQLiteDLLKey, EnvMsgLibDBKey)
return nil, nil, fmt.Errorf("configure MsgLib source: set all of %s, %s, and %s, or set none", EnvMsgLibSidecarExeKey, EnvMsgLibSQLiteDLLKey, EnvMsgLibDBKey)
}
cfg, err := msglib.ConfigFromEnv()
if err != nil {
return nil, fmt.Errorf("configure MsgLib source: %w", err)
return nil, nil, fmt.Errorf("configure MsgLib source: %w", err)
}
return msglib.NewClient(cfg), nil
client := newMsgLibClient(cfg)
return client, isphere.MsgLibMessageSource{
Lister: client,
IncludeBody: true,
IncludeAttachmentMetadata: true,
}, nil
}
func NewServerWithReceiveSource(source tools.ReceiveMessagesSource) *mcp.Server {
@@ -83,13 +97,17 @@ func NewServerWithReceiveSource(source tools.ReceiveMessagesSource) *mcp.Server
}
func NewServerWithSources(source tools.ReceiveMessagesSource, displaySource tools.DisplayEntitySource) *mcp.Server {
return NewServerWithSourcesAndMsgLibReceive(source, displaySource, nil)
}
func NewServerWithSourcesAndMsgLibReceive(source tools.ReceiveMessagesSource, displaySource tools.DisplayEntitySource, msglibReceiveSource tools.ReceiveMessagesSource) *mcp.Server {
server := mcp.NewServer(&mcp.Implementation{
Name: ServerName,
Title: ServerTitle,
Version: ServerVersion,
}, nil)
tools.RegisterWinHelperTools(server, helperclient.Client{})
tools.RegisterISphereReadToolsWithDisplayEntities(server, source, displaySource)
tools.RegisterISphereReadToolsWithReceiveSources(server, source, msglibReceiveSource, displaySource)
tools.RegisterISphereContactToolsWithDisplayEntities(server, source, displaySource)
tools.RegisterISphereGroupToolsWithDisplayEntities(server, source, displaySource)
tools.RegisterISphereFileTools(server, source)

View File

@@ -14,6 +14,8 @@ import (
"time"
"github.com/modelcontextprotocol/go-sdk/mcp"
"isphere-ai-bridge/internal/msglib"
)
func TestNewServerConstructsMCPServer(t *testing.T) {
@@ -77,6 +79,90 @@ func TestNewServerFromEnvRejectsPartialMsgLibConfig(t *testing.T) {
}
}
func TestNewServerFromEnvWiresExplicitMsgLibReceiveSource(t *testing.T) {
clearMsgLibEnvForServerTest(t)
fake := &fakeMsgLibClientForServerTest{
listResult: msglib.ListMessagesResult{
ReadOnly: true,
MessageBodyValuesReturned: true,
RawRowsReturned: false,
FilePathsReturned: false,
SourceTables: []string{"tblPersonMsg"},
Messages: []msglib.ListMessage{{
MessageID: "db-env-msg-1",
ID: "db-env-msg-1",
ConversationID: "db-env-conversation",
ConversationType: "direct",
SenderID: "db-env-sender",
ReceiverID: "db-env-receiver",
ContentText: "db env body",
Timestamp: "1783423809000",
Source: "local_readonly",
RawRef: "msglib:tblPersonMsg",
}},
},
}
oldFactory := newMsgLibClient
newMsgLibClient = func(msglib.Config) msgLibClient {
return fake
}
defer func() { newMsgLibClient = oldFactory }()
t.Setenv(EnvMsgLibSidecarExeKey, `C:\fixture\MsgLibReadSidecar.exe`)
t.Setenv(EnvMsgLibSQLiteDLLKey, `C:\fixture\System.Data.SQLite.dll`)
t.Setenv(EnvMsgLibDBKey, `C:\fixture\MsgLib.db`)
server, err := NewServerFromEnv()
if err != nil {
t.Fatalf("NewServerFromEnv returned error: %v", err)
}
session, cleanup := connectServerTestSession(t, server)
defer cleanup()
defaultResult, err := session.CallTool(context.Background(), &mcp.CallToolParams{
Name: "isphere_receive_messages",
Arguments: map[string]any{"limit": 5},
})
if err != nil {
t.Fatalf("default receive call: %v", err)
}
if defaultResult.IsError {
t.Fatalf("default receive returned error: %+v", defaultResult)
}
if len(fake.listCalls) != 0 {
t.Fatalf("default receive called MsgLib ListMessages: %+v", fake.listCalls)
}
dbResult, err := session.CallTool(context.Background(), &mcp.CallToolParams{
Name: "isphere_receive_messages",
Arguments: map[string]any{"source_preference": "msglib_readonly", "limit": 5},
})
if err != nil {
t.Fatalf("msglib receive call: %v", err)
}
if dbResult.IsError {
t.Fatalf("msglib receive returned error: %+v", dbResult)
}
payload, err := json.Marshal(dbResult.StructuredContent)
if err != nil {
t.Fatalf("marshal structured content: %v", err)
}
var decoded struct {
Messages []struct {
ID string `json:"id"`
RawRef string `json:"raw_ref"`
} `json:"messages"`
}
if err := json.Unmarshal(payload, &decoded); err != nil {
t.Fatalf("decode structured content %s: %v", payload, err)
}
if len(decoded.Messages) != 1 || decoded.Messages[0].ID != "db-env-msg-1" || decoded.Messages[0].RawRef != "msglib:tblPersonMsg" {
t.Fatalf("messages = %+v, want explicit MsgLib result", decoded.Messages)
}
if len(fake.listCalls) != 1 || !fake.listCalls[0].IncludeBody || !fake.listCalls[0].IncludeAttachmentMetadata {
t.Fatalf("ListMessages calls = %+v", fake.listCalls)
}
}
func TestNewServerFromEnvUsesPacketLogFile(t *testing.T) {
clearMsgLibEnvForServerTest(t)
plaintext := `--------------------------------------------------------------------------------------------------------------------------------------------
@@ -197,6 +283,22 @@ func TestNewServerFromEnvUsesPacketLogDirectory(t *testing.T) {
}
}
type fakeMsgLibClientForServerTest struct {
displayCalls []msglib.DisplayEntitiesOptions
listCalls []msglib.ListMessagesOptions
listResult msglib.ListMessagesResult
}
func (f *fakeMsgLibClientForServerTest) DisplayEntities(_ context.Context, opts msglib.DisplayEntitiesOptions) ([]msglib.DisplayEntity, error) {
f.displayCalls = append(f.displayCalls, opts)
return nil, nil
}
func (f *fakeMsgLibClientForServerTest) ListMessages(_ context.Context, opts msglib.ListMessagesOptions) (msglib.ListMessagesResult, error) {
f.listCalls = append(f.listCalls, opts)
return f.listResult, nil
}
func clearMsgLibEnvForServerTest(t *testing.T) {
t.Helper()
t.Setenv(EnvMsgLibSidecarExeKey, "")