@sonicbotman/lobster-press 3.2.5 → 3.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,207 @@
1
+ # LobsterPress MCP Server
2
+
3
+ > **推荐方式**:如果你在使用 OpenClaw,请优先使用
4
+ > [插件安装模式](../README.md#-openclaw-插件推荐),
5
+ > 更简单,无需手动管理进程。
6
+ >
7
+ > 本文档描述直接运行 MCP Server 的高级用法,适合非 OpenClaw 环境
8
+ > 或需要自定义集成的场景。
9
+
10
+ 基于 Model Context Protocol (MCP) 的 OpenClaw 会话压缩服务。
11
+
12
+ ## 安装
13
+
14
+ ```bash
15
+ cd lobster-press/mcp_server
16
+ pip install -r requirements.txt
17
+ ```
18
+
19
+ ## 使用方法
20
+
21
+ ### 1. 与 Claude Desktop 集成
22
+
23
+ 在 Claude Desktop 配置文件中添加:
24
+
25
+ **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
26
+ **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
27
+
28
+ ```json
29
+ {
30
+ "mcpServers": {
31
+ "lobster-press": {
32
+ "command": "python3",
33
+ "args": ["/path/to/lobster-press/mcp_server/lobster_mcp_server.py"]
34
+ }
35
+ }
36
+ }
37
+ ```
38
+
39
+ ### 2. 与 Cursor 集成
40
+
41
+ 在 Cursor 设置中添加 MCP 服务器配置。
42
+
43
+ ### 3. 测试模式
44
+
45
+ ```bash
46
+ python3 lobster_mcp_server.py --test
47
+ ```
48
+
49
+ ## 可用工具
50
+
51
+ ### 1. compress_session
52
+
53
+ 压缩 OpenClaw 会话历史。
54
+
55
+ ```json
56
+ {
57
+ "session_id": "abc123",
58
+ "strategy": "medium",
59
+ "dry_run": false
60
+ }
61
+ ```
62
+
63
+ **参数:**
64
+ - `session_id`: 会话 ID(文件名,不含扩展名)
65
+ - `strategy`: 压缩策略(light/medium/aggressive)
66
+ - `dry_run`: 是否仅预览(默认 false)
67
+
68
+ **返回:**
69
+ ```json
70
+ {
71
+ "status": "success",
72
+ "original_messages": 1000,
73
+ "compressed_messages": 500,
74
+ "tokens_saved": 50000,
75
+ "compression_ratio": "50%"
76
+ }
77
+ ```
78
+
79
+ ### 2. preview_compression
80
+
81
+ 预览压缩效果。
82
+
83
+ ```json
84
+ {
85
+ "session_id": "abc123",
86
+ "strategy": "medium"
87
+ }
88
+ ```
89
+
90
+ ### 3. get_compression_stats
91
+
92
+ 获取压缩统计数据。
93
+
94
+ ### 4. update_weights
95
+
96
+ 更新消息类型权重配置。
97
+
98
+ ```json
99
+ {
100
+ "weights": {
101
+ "decision": 0.3,
102
+ "error": 0.25,
103
+ "config": 0.2,
104
+ "preference": 0.15
105
+ }
106
+ }
107
+ ```
108
+
109
+ ### 5. list_sessions
110
+
111
+ 列出所有可压缩的会话。
112
+
113
+ ```json
114
+ {
115
+ "min_tokens": 10000
116
+ }
117
+ ```
118
+
119
+ ## 压缩策略
120
+
121
+ | 策略 | 保留比例 | 适用场景 |
122
+ |------|---------|---------|
123
+ | light | 70% | 保留大部分信息 |
124
+ | medium | 50% | 平衡压缩与保留 |
125
+ | aggressive | 30% | 最大压缩 |
126
+
127
+ ## 消息评分
128
+
129
+ 消息按以下权重评分:
130
+
131
+ | 类型 | 权重 | 示例 |
132
+ |------|------|------|
133
+ | decision | 0.3 | "决定采用方案A" |
134
+ | error | 0.25 | "发生错误:连接超时" |
135
+ | config | 0.2 | "API Key 已更新" |
136
+ | preference | 0.15 | "用户偏好使用 GLM-4" |
137
+ | context | 0.05 | "当前系统版本 2026.3.2" |
138
+ | chitchat | 0.02 | "哈哈,不错" |
139
+
140
+ ## 资源访问
141
+
142
+ MCP 客户端可以通过以下 URI 访问会话资源:
143
+
144
+ ```
145
+ lobster://sessions/{session_id}
146
+ ```
147
+
148
+ ## 示例
149
+
150
+ ### Python 客户端
151
+
152
+ ```python
153
+ import json
154
+ import subprocess
155
+
156
+ def call_mcp_tool(tool_name, arguments):
157
+ request = {
158
+ "method": "tools/call",
159
+ "params": {
160
+ "name": tool_name,
161
+ "arguments": arguments
162
+ }
163
+ }
164
+
165
+ proc = subprocess.Popen(
166
+ ["python3", "lobster_mcp_server.py"],
167
+ stdin=subprocess.PIPE,
168
+ stdout=subprocess.PIPE,
169
+ text=True
170
+ )
171
+
172
+ stdout, _ = proc.communicate(json.dumps(request))
173
+ return json.loads(stdout)
174
+
175
+ # 列出会话
176
+ result = call_mcp_tool("list_sessions", {"min_tokens": 10000})
177
+ print(result)
178
+
179
+ # 预览压缩
180
+ result = call_mcp_tool("preview_compression", {
181
+ "session_id": "abc123",
182
+ "strategy": "medium"
183
+ })
184
+ print(result)
185
+
186
+ # 执行压缩
187
+ result = call_mcp_tool("compress_session", {
188
+ "session_id": "abc123",
189
+ "strategy": "medium"
190
+ })
191
+ print(result)
192
+ ```
193
+
194
+ ## 安全性
195
+
196
+ - ✅ 自动备份原文件(带时间戳)
197
+ - ✅ 仅本地操作,不上传数据
198
+ - ✅ 支持预览模式(dry_run)
199
+
200
+ ## 许可证
201
+
202
+ MIT License
203
+
204
+ ## 相关
205
+
206
+ - Issue #42: v2.0 架构演进:向 MCP Server 演进
207
+ - Issue #28: MCP 协议支持
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "lobster-press",
3
+ "version": "1.2.8",
4
+ "description": "LobsterPress - 智能 OpenClaw 会话压缩服务 (MCP Server)",
5
+ "main": "lobster_mcp_server.py",
6
+ "type": "mcp-server",
7
+ "tools": [
8
+ {
9
+ "name": "compress_session",
10
+ "description": "压缩 OpenClaw 会话历史,保留重要信息"
11
+ },
12
+ {
13
+ "name": "preview_compression",
14
+ "description": "预览压缩效果,显示将要保留和删除的消息"
15
+ },
16
+ {
17
+ "name": "get_compression_stats",
18
+ "description": "获取压缩统计数据"
19
+ },
20
+ {
21
+ "name": "update_weights",
22
+ "description": "更新消息类型权重配置"
23
+ },
24
+ {
25
+ "name": "list_sessions",
26
+ "description": "列出所有可压缩的会话"
27
+ }
28
+ ],
29
+ "resources": [
30
+ {
31
+ "uri": "lobster://sessions/{session_id}",
32
+ "description": "访问 OpenClaw 会话数据"
33
+ }
34
+ ],
35
+ "config": {
36
+ "sessions_dir": "~/.openclaw/agents/main/sessions",
37
+ "default_strategy": "medium",
38
+ "strategies": {
39
+ "light": {
40
+ "retention_ratio": 0.7,
41
+ "description": "轻度压缩,保留 70% 消息"
42
+ },
43
+ "medium": {
44
+ "retention_ratio": 0.5,
45
+ "description": "中度压缩,保留 50% 消息"
46
+ },
47
+ "aggressive": {
48
+ "retention_ratio": 0.3,
49
+ "description": "激进压缩,保留 30% 消息"
50
+ }
51
+ }
52
+ },
53
+ "author": "LobsterPress Team",
54
+ "repository": "https://github.com/SonicBotMan/lobster-press",
55
+ "license": "MIT"
56
+ }
@@ -0,0 +1,658 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ LobsterPress MCP Server v1.0.0
5
+ 基于 Model Context Protocol (MCP) 的压缩服务
6
+
7
+ Issue: #42 - v2.0 架构演进:向 MCP Server 演进
8
+ Author: LobsterPress Team
9
+ Version: v1.3.0
10
+ """
11
+
12
+ import sys
13
+ import json
14
+ import asyncio
15
+ import os
16
+ import re
17
+ from pathlib import Path
18
+ from typing import Dict, List, Optional, Any
19
+ from dataclasses import dataclass, asdict
20
+ from datetime import datetime
21
+
22
+
23
+ @dataclass
24
+ class MCPTool:
25
+ """MCP 工具定义"""
26
+ name: str
27
+ description: str
28
+ input_schema: Dict[str, Any]
29
+
30
+
31
+ class LobsterPressMCPServer:
32
+ """LobsterPress MCP Server"""
33
+
34
+ def __init__(self, sessions_dir: str = None, db_path: str = None, llm_provider: str = None, llm_model: str = None):
35
+ """初始化 MCP Server
36
+
37
+ Args:
38
+ sessions_dir: 会话目录
39
+ db_path: LobsterPress 数据库路径
40
+ llm_provider: LLM 提供商
41
+ llm_model: LLM 模型名称
42
+ """
43
+ self.sessions_dir = Path(sessions_dir or os.path.expanduser("~/.openclaw/agents/main/sessions"))
44
+ self.sessions_dir.mkdir(parents=True, exist_ok=True)
45
+
46
+ # v3.2.2: OpenClaw 插件支持
47
+ self.db_path = db_path or os.path.expanduser("~/.openclaw/lobster.db")
48
+ self.llm_provider = llm_provider
49
+ self.llm_model = llm_model
50
+ self._db = None # 懒加载数据库连接
51
+
52
+ # 统计数据
53
+ self.stats = {
54
+ "total_compressions": 0,
55
+ "total_messages_processed": 0,
56
+ "total_tokens_saved": 0,
57
+ "last_compression": None
58
+ }
59
+
60
+ # 配置
61
+ self.config = {
62
+ "weights": {
63
+ "decision": 0.3,
64
+ "error": 0.25,
65
+ "config": 0.2,
66
+ "preference": 0.15,
67
+ "context": 0.05,
68
+ "chitchat": 0.02,
69
+ "other": 0.03
70
+ },
71
+ "strategy": "medium",
72
+ "max_tokens": 800000
73
+ }
74
+
75
+ # 注册工具
76
+ self.tools = self._register_tools()
77
+
78
+ def _register_tools(self) -> List[MCPTool]:
79
+ """注册 MCP 工具"""
80
+ return [
81
+ MCPTool(
82
+ name="compress_session",
83
+ description="压缩 OpenClaw 会话历史,保留重要信息",
84
+ input_schema={
85
+ "type": "object",
86
+ "properties": {
87
+ "session_id": {
88
+ "type": "string",
89
+ "description": "会话 ID(文件名,不含扩展名)"
90
+ },
91
+ "strategy": {
92
+ "type": "string",
93
+ "enum": ["light", "medium", "aggressive"],
94
+ "description": "压缩策略:light(轻度)、medium(中度)、aggressive(激进)",
95
+ "default": "medium"
96
+ },
97
+ "dry_run": {
98
+ "type": "boolean",
99
+ "description": "是否仅预览,不实际压缩",
100
+ "default": False
101
+ }
102
+ },
103
+ "required": ["session_id"]
104
+ }
105
+ ),
106
+ MCPTool(
107
+ name="preview_compression",
108
+ description="预览压缩效果,显示将要保留和删除的消息",
109
+ input_schema={
110
+ "type": "object",
111
+ "properties": {
112
+ "session_id": {
113
+ "type": "string",
114
+ "description": "会话 ID"
115
+ },
116
+ "strategy": {
117
+ "type": "string",
118
+ "enum": ["light", "medium", "aggressive"],
119
+ "default": "medium"
120
+ }
121
+ },
122
+ "required": ["session_id"]
123
+ }
124
+ ),
125
+ MCPTool(
126
+ name="get_compression_stats",
127
+ description="获取压缩统计数据",
128
+ input_schema={
129
+ "type": "object",
130
+ "properties": {},
131
+ "required": []
132
+ }
133
+ ),
134
+ MCPTool(
135
+ name="update_weights",
136
+ description="更新消息类型权重配置",
137
+ input_schema={
138
+ "type": "object",
139
+ "properties": {
140
+ "weights": {
141
+ "type": "object",
142
+ "description": "消息类型权重",
143
+ "properties": {
144
+ "decision": {"type": "number"},
145
+ "error": {"type": "number"},
146
+ "config": {"type": "number"},
147
+ "preference": {"type": "number"},
148
+ "context": {"type": "number"},
149
+ "chitchat": {"type": "number"}
150
+ }
151
+ }
152
+ },
153
+ "required": ["weights"]
154
+ }
155
+ ),
156
+ MCPTool(
157
+ name="list_sessions",
158
+ description="列出所有可压缩的会话",
159
+ input_schema={
160
+ "type": "object",
161
+ "properties": {
162
+ "min_tokens": {
163
+ "type": "integer",
164
+ "description": "最小 Token 数阈值",
165
+ "default": 10000
166
+ }
167
+ },
168
+ "required": []
169
+ }
170
+ ),
171
+ # v3.2.2: OpenClaw 插件工具
172
+ MCPTool(
173
+ name="lobster_grep",
174
+ description="在 LobsterPress 记忆库中全文搜索历史对话(FTS5 + TF-IDF 重排序)",
175
+ input_schema={
176
+ "type": "object",
177
+ "properties": {
178
+ "query": {"type": "string", "description": "搜索关键词或短语"},
179
+ "conversation_id": {"type": "string", "description": "限定搜索范围的会话 ID(可选)"},
180
+ "limit": {"type": "integer", "description": "最多返回条数,默认 5", "default": 5}
181
+ },
182
+ "required": ["query"]
183
+ }
184
+ ),
185
+ MCPTool(
186
+ name="lobster_describe",
187
+ description="查看 LobsterPress 的 DAG 摘要层级结构",
188
+ input_schema={
189
+ "type": "object",
190
+ "properties": {
191
+ "conversation_id": {"type": "string", "description": "会话 ID(可选,留空查全局)"}
192
+ },
193
+ "required": []
194
+ }
195
+ ),
196
+ MCPTool(
197
+ name="lobster_expand",
198
+ description="将 DAG 摘要节点展开,还原其对应的原始消息(无损检索)",
199
+ input_schema={
200
+ "type": "object",
201
+ "properties": {
202
+ "summary_id": {"type": "string", "description": "要展开的摘要节点 ID"},
203
+ "max_depth": {"type": "integer", "description": "最大展开层数,默认 2", "default": 2}
204
+ },
205
+ "required": ["summary_id"]
206
+ }
207
+ )
208
+ ]
209
+
210
+ async def handle_request(self, request: Dict[str, Any]) -> Dict[str, Any]:
211
+ """处理 MCP 请求
212
+
213
+ Args:
214
+ request: MCP 请求
215
+
216
+ Returns:
217
+ MCP 响应
218
+ """
219
+ method = request.get("method")
220
+ params = request.get("params", {})
221
+
222
+ try:
223
+ if method == "tools/list":
224
+ return {
225
+ "tools": [asdict(tool) for tool in self.tools]
226
+ }
227
+
228
+ elif method == "tools/call":
229
+ tool_name = params.get("name")
230
+ arguments = params.get("arguments", {})
231
+
232
+ result = await self._call_tool(tool_name, arguments)
233
+ return {
234
+ "content": [
235
+ {
236
+ "type": "text",
237
+ "text": json.dumps(result, ensure_ascii=False, indent=2)
238
+ }
239
+ ]
240
+ }
241
+
242
+ elif method == "resources/list":
243
+ return await self._list_resources()
244
+
245
+ elif method == "resources/read":
246
+ return await self._read_resource(params.get("uri"))
247
+
248
+ else:
249
+ return {"error": f"Unknown method: {method}"}
250
+
251
+ except Exception as e:
252
+ return {"error": str(e)}
253
+
254
+ async def _call_tool(self, tool_name: str, arguments: Dict[str, Any]) -> Dict[str, Any]:
255
+ """调用工具"""
256
+ if tool_name == "compress_session":
257
+ return await self._compress_session(
258
+ arguments.get("session_id"),
259
+ arguments.get("strategy", "medium"),
260
+ arguments.get("dry_run", False)
261
+ )
262
+
263
+ elif tool_name == "preview_compression":
264
+ return await self._preview_compression(
265
+ arguments.get("session_id"),
266
+ arguments.get("strategy", "medium")
267
+ )
268
+
269
+ elif tool_name == "get_compression_stats":
270
+ return self._get_stats()
271
+
272
+ elif tool_name == "update_weights":
273
+ return self._update_weights(arguments.get("weights", {}))
274
+
275
+ elif tool_name == "list_sessions":
276
+ return await self._list_sessions(arguments.get("min_tokens", 10000))
277
+
278
+ # v3.2.2: OpenClaw 插件工具
279
+ elif tool_name == "lobster_grep":
280
+ from agent_tools import lobster_grep
281
+ db = self._get_db()
282
+ results = lobster_grep(
283
+ db,
284
+ arguments["query"],
285
+ conversation_id=arguments.get("conversation_id"),
286
+ limit=arguments.get("limit", 5)
287
+ )
288
+ return {"results": results}
289
+
290
+ elif tool_name == "lobster_describe":
291
+ from agent_tools import lobster_describe
292
+ db = self._get_db()
293
+ return lobster_describe(db, conversation_id=arguments.get("conversation_id"))
294
+
295
+ elif tool_name == "lobster_expand":
296
+ from agent_tools import lobster_expand
297
+ db = self._get_db()
298
+ return lobster_expand(db, arguments["summary_id"])
299
+
300
+ else:
301
+ raise ValueError(f"Unknown tool: {tool_name}")
302
+
303
+ def _get_db(self):
304
+ """获取数据库连接(懒加载)"""
305
+ if self._db is None:
306
+ # 添加 src 目录到 path
307
+ src_dir = Path(__file__).parent.parent / "src"
308
+ if str(src_dir) not in sys.path:
309
+ sys.path.insert(0, str(src_dir))
310
+
311
+ from database import LobsterDatabase
312
+ self._db = LobsterDatabase(self.db_path)
313
+ return self._db
314
+
315
+ def _validate_session_id(self, session_id: str) -> str:
316
+ """验证 session_id 防止路径遍历攻击(修复 Issue #12)
317
+
318
+ Args:
319
+ session_id: 会话 ID
320
+
321
+ Returns:
322
+ 验证后的会话 ID
323
+
324
+ Raises:
325
+ ValueError: 无效的会话 ID
326
+ """
327
+ if not session_id:
328
+ raise ValueError("Session ID cannot be empty")
329
+
330
+ # 只允许字母、数字、下划线、连字符
331
+ if not re.match(r'^[a-zA-Z0-9_-]+$', session_id):
332
+ raise ValueError(f"Invalid session_id: {session_id} (only alphanumeric, underscore, hyphen allowed)")
333
+
334
+ # 防止路径遍历
335
+ if '..' in session_id or '/' in session_id or '\\' in session_id:
336
+ raise ValueError(f"Invalid session_id: {session_id} (path traversal detected)")
337
+
338
+ # 长度限制
339
+ if len(session_id) > 255:
340
+ raise ValueError(f"Session ID too long: {len(session_id)} > 255")
341
+
342
+ return session_id
343
+
344
+ async def _compress_session(self, session_id: str, strategy: str, dry_run: bool) -> Dict[str, Any]:
345
+ """压缩会话"""
346
+ # 验证 session_id(修复 Issue #12)
347
+ session_id = self._validate_session_id(session_id)
348
+ session_file = self.sessions_dir / f"{session_id}.jsonl"
349
+
350
+ if not session_file.exists():
351
+ raise FileNotFoundError(f"Session not found: {session_id}")
352
+
353
+ # 读取会话
354
+ messages = []
355
+ with open(session_file, 'r', encoding='utf-8') as f:
356
+ for line in f:
357
+ if line.strip():
358
+ messages.append(json.loads(line))
359
+
360
+ total_messages = len(messages)
361
+
362
+ # 估算 Token 数
363
+ estimated_tokens = self._estimate_tokens(messages)
364
+
365
+ # 根据策略决定保留比例
366
+ retention_ratios = {
367
+ "light": 0.7,
368
+ "medium": 0.5,
369
+ "aggressive": 0.3
370
+ }
371
+ retention_ratio = retention_ratios.get(strategy, 0.5)
372
+
373
+ # 计算要保留的消息数
374
+ keep_count = int(total_messages * retention_ratio)
375
+
376
+ # 评分并排序(简化版)
377
+ scored_messages = []
378
+ for i, msg in enumerate(messages):
379
+ score = self._score_message(msg)
380
+ scored_messages.append((i, score, msg))
381
+
382
+ # 按分数排序,保留高分消息
383
+ scored_messages.sort(key=lambda x: x[1], reverse=True)
384
+ keep_indices = set(x[0] for x in scored_messages[:keep_count])
385
+
386
+ # 按原始顺序保留
387
+ compressed_messages = [msg for i, msg in enumerate(messages) if i in keep_indices]
388
+
389
+ # 计算压缩后的 Token 数
390
+ compressed_tokens = self._estimate_tokens(compressed_messages)
391
+ tokens_saved = estimated_tokens - compressed_tokens
392
+
393
+ if dry_run:
394
+ return {
395
+ "status": "preview",
396
+ "session_id": session_id,
397
+ "original_messages": total_messages,
398
+ "compressed_messages": len(compressed_messages),
399
+ "original_tokens": estimated_tokens,
400
+ "compressed_tokens": compressed_tokens,
401
+ "tokens_saved": tokens_saved,
402
+ "compression_ratio": f"{(tokens_saved / estimated_tokens * 100):.1f}%" if estimated_tokens > 0 else "0%",
403
+ "strategy": strategy
404
+ }
405
+
406
+ # 实际写入(备份原文件)
407
+ backup_file = self.sessions_dir / f"{session_id}.backup.{datetime.now().strftime('%Y%m%d%H%M%S')}"
408
+ session_file.rename(backup_file)
409
+
410
+ with open(session_file, 'w', encoding='utf-8') as f:
411
+ for msg in compressed_messages:
412
+ f.write(json.dumps(msg, ensure_ascii=False) + '\n')
413
+
414
+ # 更新统计
415
+ self.stats["total_compressions"] += 1
416
+ self.stats["total_messages_processed"] += total_messages
417
+ self.stats["total_tokens_saved"] += tokens_saved
418
+ self.stats["last_compression"] = datetime.now().isoformat()
419
+
420
+ return {
421
+ "status": "success",
422
+ "session_id": session_id,
423
+ "original_messages": total_messages,
424
+ "compressed_messages": len(compressed_messages),
425
+ "original_tokens": estimated_tokens,
426
+ "compressed_tokens": compressed_tokens,
427
+ "tokens_saved": tokens_saved,
428
+ "compression_ratio": f"{(tokens_saved / estimated_tokens * 100):.1f}%" if estimated_tokens > 0 else "0%",
429
+ "strategy": strategy,
430
+ "backup_file": str(backup_file)
431
+ }
432
+
433
+ async def _preview_compression(self, session_id: str, strategy: str) -> Dict[str, Any]:
434
+ """预览压缩效果"""
435
+ return await self._compress_session(session_id, strategy, dry_run=True)
436
+
437
+ def _get_stats(self) -> Dict[str, Any]:
438
+ """获取统计数据"""
439
+ return {
440
+ "stats": self.stats,
441
+ "config": self.config
442
+ }
443
+
444
+ def _update_weights(self, weights: Dict[str, float]) -> Dict[str, Any]:
445
+ """更新权重配置"""
446
+ # 验证权重
447
+ for key, value in weights.items():
448
+ if key in self.config["weights"]:
449
+ self.config["weights"][key] = value
450
+
451
+ return {
452
+ "status": "success",
453
+ "updated_weights": self.config["weights"]
454
+ }
455
+
456
+ async def _list_sessions(self, min_tokens: int) -> Dict[str, Any]:
457
+ """列出会话"""
458
+ sessions = []
459
+
460
+ for session_file in self.sessions_dir.glob("*.jsonl"):
461
+ if session_file.name.endswith((".backup.", ".reset.", ".deleted.")):
462
+ continue
463
+
464
+ # 估算 Token 数
465
+ messages = []
466
+ with open(session_file, 'r', encoding='utf-8') as f:
467
+ for line in f:
468
+ if line.strip():
469
+ messages.append(json.loads(line))
470
+
471
+ estimated_tokens = self._estimate_tokens(messages)
472
+
473
+ if estimated_tokens >= min_tokens:
474
+ sessions.append({
475
+ "session_id": session_file.stem,
476
+ "messages": len(messages),
477
+ "estimated_tokens": estimated_tokens,
478
+ "file_size": session_file.stat().st_size,
479
+ "modified": datetime.fromtimestamp(session_file.stat().st_mtime).isoformat()
480
+ })
481
+
482
+ # 按 Token 数排序
483
+ sessions.sort(key=lambda x: x["estimated_tokens"], reverse=True)
484
+
485
+ return {
486
+ "sessions": sessions,
487
+ "total": len(sessions)
488
+ }
489
+
490
+ async def _list_resources(self) -> Dict[str, Any]:
491
+ """列出资源"""
492
+ resources = []
493
+
494
+ for session_file in self.sessions_dir.glob("*.jsonl"):
495
+ if not session_file.name.endswith((".backup.", ".reset.", ".deleted.")):
496
+ resources.append({
497
+ "uri": f"lobster://sessions/{session_file.stem}",
498
+ "name": session_file.stem,
499
+ "mimeType": "application/jsonl"
500
+ })
501
+
502
+ return {"resources": resources}
503
+
504
+ async def _read_resource(self, uri: str) -> Dict[str, Any]:
505
+ """读取资源"""
506
+ if not uri.startswith("lobster://sessions/"):
507
+ raise ValueError(f"Invalid resource URI: {uri}")
508
+
509
+ session_id = uri.replace("lobster://sessions/", "")
510
+ session_file = self.sessions_dir / f"{session_id}.jsonl"
511
+
512
+ if not session_file.exists():
513
+ raise FileNotFoundError(f"Session not found: {session_id}")
514
+
515
+ with open(session_file, 'r', encoding='utf-8') as f:
516
+ content = f.read()
517
+
518
+ return {
519
+ "contents": [
520
+ {
521
+ "uri": uri,
522
+ "mimeType": "application/jsonl",
523
+ "text": content
524
+ }
525
+ ]
526
+ }
527
+
528
+ def _estimate_tokens(self, messages: List[Dict]) -> int:
529
+ """估算 Token 数"""
530
+ total_chars = 0
531
+ for msg in messages:
532
+ content = msg.get("content", "")
533
+ if isinstance(content, str):
534
+ total_chars += len(content)
535
+ elif isinstance(content, list):
536
+ for part in content:
537
+ if isinstance(part, dict) and "text" in part:
538
+ total_chars += len(part["text"])
539
+
540
+ # 简单估算:3 字符 = 1 token
541
+ return total_chars // 3
542
+
543
+ def _score_message(self, msg: Dict) -> float:
544
+ """评分消息重要性"""
545
+ content = msg.get("content", "")
546
+ if isinstance(content, list):
547
+ content = " ".join(p.get("text", "") for p in content if isinstance(p, dict))
548
+
549
+ content_lower = content.lower()
550
+ score = 0.0
551
+
552
+ # 关键词评分
553
+ patterns = {
554
+ "decision": ["决定", "方案", "选择", "决定采用", "decide", "choose"],
555
+ "error": ["错误", "失败", "异常", "error", "fail", "exception"],
556
+ "config": ["配置", "设置", "更新", "config", "setting", "update"],
557
+ "preference": ["偏好", "喜欢", "希望", "prefer", "like", "want"],
558
+ }
559
+
560
+ for category, keywords in patterns.items():
561
+ for keyword in keywords:
562
+ if keyword in content_lower:
563
+ score += self.config["weights"].get(category, 0.1)
564
+
565
+ return score
566
+
567
+
568
+ def emit(obj: Dict[str, Any]) -> None:
569
+ """发送 JSON 响应到 stdout"""
570
+ sys.stdout.write(json.dumps(obj, ensure_ascii=False) + "\n")
571
+ sys.stdout.flush()
572
+
573
+
574
+ async def main():
575
+ """主入口"""
576
+ import argparse
577
+ import time
578
+
579
+ parser = argparse.ArgumentParser(description="LobsterPress MCP Server")
580
+ parser.add_argument("--sessions-dir", help="会话目录")
581
+ parser.add_argument("--db", dest="db_path", help="LobsterPress 数据库路径")
582
+ parser.add_argument("--provider", dest="llm_provider", help="LLM 提供商")
583
+ parser.add_argument("--model", dest="llm_model", help="LLM 模型名称")
584
+ parser.add_argument("--test", action="store_true", help="测试模式")
585
+
586
+ args = parser.parse_args()
587
+
588
+ server = LobsterPressMCPServer(
589
+ sessions_dir=args.sessions_dir,
590
+ db_path=args.db_path,
591
+ llm_provider=args.llm_provider,
592
+ llm_model=args.llm_model
593
+ )
594
+
595
+ if args.test:
596
+ # 测试模式
597
+ print("=== LobsterPress MCP Server 测试 ===")
598
+ print("\n可用工具:")
599
+ for tool in server.tools:
600
+ print(f" - {tool.name}: {tool.description}")
601
+
602
+ # 测试 list_sessions
603
+ result = await server._call_tool("list_sessions", {"min_tokens": 1000})
604
+ print(f"\n找到 {result['total']} 个可压缩会话")
605
+ for session in result["sessions"][:3]:
606
+ print(f" - {session['session_id']}: {session['estimated_tokens']} tokens")
607
+
608
+ print("\n✅ MCP Server 正常工作")
609
+ else:
610
+ # Phase 1 (Issue #115): 发送 ready handshake
611
+ emit({"type": "lobster-press/ready", "ts": time.time()})
612
+
613
+ # MCP 协议模式(读取 stdin)
614
+ for line in sys.stdin:
615
+ line = line.strip()
616
+ if not line:
617
+ continue
618
+
619
+ request_id = None
620
+ try:
621
+ req = json.loads(line)
622
+ request_id = req.get("requestId") or req.get("id")
623
+ method = req.get("method")
624
+
625
+ if method == "tools/call":
626
+ params = req.get("params", {})
627
+ tool_name = params.get("name")
628
+ arguments = params.get("arguments", {})
629
+ result = await server._call_tool(tool_name, arguments)
630
+ emit({
631
+ "requestId": request_id,
632
+ "status": "ok",
633
+ "result": result,
634
+ })
635
+ else:
636
+ # 其他方法使用原有处理逻辑
637
+ response = await server.handle_request(req)
638
+ emit({
639
+ "requestId": request_id,
640
+ "status": "ok",
641
+ "result": response,
642
+ })
643
+ except json.JSONDecodeError as e:
644
+ emit({
645
+ "requestId": request_id,
646
+ "status": "error",
647
+ "error": f"Invalid JSON: {e}",
648
+ })
649
+ except Exception as e:
650
+ emit({
651
+ "requestId": request_id,
652
+ "status": "error",
653
+ "error": str(e),
654
+ })
655
+
656
+
657
+ if __name__ == "__main__":
658
+ asyncio.run(main())
@@ -0,0 +1,11 @@
1
+ # LobsterPress MCP Server Dependencies
2
+
3
+ # 核心依赖
4
+ # Python 3.8+ required
5
+
6
+ # 可选依赖(用于精确 Token 计数)
7
+ tiktoken>=0.5.0
8
+
9
+ # 测试依赖
10
+ pytest>=7.0.0
11
+ pytest-asyncio>=0.21.0
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@sonicbotman/lobster-press",
3
- "version": "3.2.5",
3
+ "version": "3.2.6",
4
4
  "description": "Cognitive Memory System for AI Agents \u2014 OpenClaw Plugin",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
8
8
  "files": [
9
9
  "dist/",
10
+ "mcp_server/",
10
11
  "openclaw.plugin.json",
11
12
  "README.md"
12
13
  ],