lore-framework-mcp 1.2.2__py3-none-any.whl
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.
- lore_framework_mcp/__init__.py +22 -0
- lore_framework_mcp/cli.py +303 -0
- lore_framework_mcp/server.py +572 -0
- lore_framework_mcp-1.2.2.dist-info/METADATA +115 -0
- lore_framework_mcp-1.2.2.dist-info/RECORD +7 -0
- lore_framework_mcp-1.2.2.dist-info/WHEEL +4 -0
- lore_framework_mcp-1.2.2.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Lore Framework MCP Server
|
|
3
|
+
|
|
4
|
+
MCP server and CLI for managing lore/ directory structure.
|
|
5
|
+
Provides tools for session management, task tracking, and index generation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
from .server import mcp, run_server
|
|
10
|
+
from .cli import run_cli
|
|
11
|
+
|
|
12
|
+
__version__ = "1.2.2"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def main():
|
|
16
|
+
"""Entry point - detect CLI mode or start MCP server."""
|
|
17
|
+
if len(sys.argv) > 1:
|
|
18
|
+
# CLI mode
|
|
19
|
+
sys.exit(run_cli(sys.argv))
|
|
20
|
+
else:
|
|
21
|
+
# MCP server mode
|
|
22
|
+
run_server()
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Lore Framework CLI
|
|
3
|
+
|
|
4
|
+
CLI interface for lore-framework-mcp.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
lore-framework-mcp set-user <user_id>
|
|
8
|
+
lore-framework-mcp set-user --env
|
|
9
|
+
lore-framework-mcp set-task <task_id>
|
|
10
|
+
lore-framework-mcp show-session
|
|
11
|
+
lore-framework-mcp list-users
|
|
12
|
+
lore-framework-mcp clear-task
|
|
13
|
+
lore-framework-mcp generate-index [--next-only] [--quiet]
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
import sys
|
|
18
|
+
import json
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
import yaml
|
|
22
|
+
import frontmatter
|
|
23
|
+
|
|
24
|
+
from .server import (
|
|
25
|
+
get_project_dir,
|
|
26
|
+
get_lore_dir,
|
|
27
|
+
get_session_dir,
|
|
28
|
+
load_team,
|
|
29
|
+
generate_current_user_md,
|
|
30
|
+
find_task,
|
|
31
|
+
parse_tasks,
|
|
32
|
+
parse_adrs,
|
|
33
|
+
compute_blocks,
|
|
34
|
+
generate_readme,
|
|
35
|
+
generate_next,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def parse_args(argv: list[str]) -> tuple[str, list[str], dict]:
|
|
40
|
+
"""Parse command line arguments."""
|
|
41
|
+
args = argv[1:] # Remove script name
|
|
42
|
+
command = args[0] if args else "help"
|
|
43
|
+
positional = []
|
|
44
|
+
flags = {"env": False, "next_only": False, "quiet": False}
|
|
45
|
+
|
|
46
|
+
for i, arg in enumerate(args[1:], 1):
|
|
47
|
+
if arg == "--env":
|
|
48
|
+
flags["env"] = True
|
|
49
|
+
elif arg == "--next-only":
|
|
50
|
+
flags["next_only"] = True
|
|
51
|
+
elif arg in ("--quiet", "-q"):
|
|
52
|
+
flags["quiet"] = True
|
|
53
|
+
elif not arg.startswith("-"):
|
|
54
|
+
positional.append(arg)
|
|
55
|
+
|
|
56
|
+
return command, positional, flags
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def cmd_set_user(args: list[str], flags: dict) -> int:
|
|
60
|
+
"""Set current user from team.yaml."""
|
|
61
|
+
session_dir = get_session_dir()
|
|
62
|
+
|
|
63
|
+
if not session_dir.exists():
|
|
64
|
+
print("Error: 0-session/ directory not found", file=sys.stderr)
|
|
65
|
+
return 1
|
|
66
|
+
|
|
67
|
+
if flags["env"]:
|
|
68
|
+
user_id = os.environ.get("LORE_SESSION_CURRENT_USER")
|
|
69
|
+
if not user_id:
|
|
70
|
+
print("Error: LORE_SESSION_CURRENT_USER not set", file=sys.stderr)
|
|
71
|
+
return 1
|
|
72
|
+
else:
|
|
73
|
+
if not args:
|
|
74
|
+
print("Usage: lore-framework-mcp set-user <user_id> | --env", file=sys.stderr)
|
|
75
|
+
return 1
|
|
76
|
+
user_id = args[0]
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
team = load_team(session_dir)
|
|
80
|
+
except FileNotFoundError as e:
|
|
81
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
82
|
+
return 1
|
|
83
|
+
|
|
84
|
+
if user_id not in team:
|
|
85
|
+
print(f"Error: User '{user_id}' not found", file=sys.stderr)
|
|
86
|
+
print(f"Available: {', '.join(team.keys())}", file=sys.stderr)
|
|
87
|
+
return 1
|
|
88
|
+
|
|
89
|
+
user_data = team[user_id]
|
|
90
|
+
content = generate_current_user_md(user_id, user_data, team)
|
|
91
|
+
(session_dir / "current-user.md").write_text(content)
|
|
92
|
+
|
|
93
|
+
if not flags["quiet"]:
|
|
94
|
+
print(f"User: {user_id}")
|
|
95
|
+
return 0
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def cmd_set_task(args: list[str], flags: dict) -> int:
|
|
99
|
+
"""Set current task by ID."""
|
|
100
|
+
lore_dir = get_lore_dir()
|
|
101
|
+
session_dir = get_session_dir()
|
|
102
|
+
|
|
103
|
+
if not session_dir.exists():
|
|
104
|
+
print("Error: 0-session/ directory not found", file=sys.stderr)
|
|
105
|
+
return 1
|
|
106
|
+
|
|
107
|
+
if not args:
|
|
108
|
+
print("Usage: lore-framework-mcp set-task <task_id>", file=sys.stderr)
|
|
109
|
+
return 1
|
|
110
|
+
|
|
111
|
+
task_id = args[0]
|
|
112
|
+
task_path = find_task(lore_dir, task_id)
|
|
113
|
+
|
|
114
|
+
if not task_path:
|
|
115
|
+
print(f"Error: Task {task_id} not found", file=sys.stderr)
|
|
116
|
+
return 1
|
|
117
|
+
|
|
118
|
+
current_task_md = session_dir / "current-task.md"
|
|
119
|
+
current_task_json = session_dir / "current-task.json"
|
|
120
|
+
|
|
121
|
+
# Remove existing symlink
|
|
122
|
+
if current_task_md.exists() or current_task_md.is_symlink():
|
|
123
|
+
current_task_md.unlink()
|
|
124
|
+
|
|
125
|
+
# Create relative symlink
|
|
126
|
+
relative_path = os.path.relpath(task_path, session_dir)
|
|
127
|
+
current_task_md.symlink_to(relative_path)
|
|
128
|
+
|
|
129
|
+
# Write task metadata
|
|
130
|
+
task_dir = os.path.relpath(task_path.parent, lore_dir)
|
|
131
|
+
current_task_json.write_text(json.dumps({"id": task_id, "path": task_dir}, indent=2))
|
|
132
|
+
|
|
133
|
+
if not flags["quiet"]:
|
|
134
|
+
print(f"Task: {task_id} -> {relative_path}")
|
|
135
|
+
return 0
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def cmd_show_session() -> int:
|
|
139
|
+
"""Show current session state."""
|
|
140
|
+
session_dir = get_session_dir()
|
|
141
|
+
|
|
142
|
+
if not session_dir.exists():
|
|
143
|
+
print("Error: 0-session/ directory not found", file=sys.stderr)
|
|
144
|
+
return 1
|
|
145
|
+
|
|
146
|
+
# User
|
|
147
|
+
current_user_md = session_dir / "current-user.md"
|
|
148
|
+
if current_user_md.exists():
|
|
149
|
+
content = current_user_md.read_text()
|
|
150
|
+
for line in content.split("\n"):
|
|
151
|
+
if line.startswith("name:"):
|
|
152
|
+
print(f"User: {line.split(':', 1)[1].strip()}")
|
|
153
|
+
break
|
|
154
|
+
else:
|
|
155
|
+
env_user = os.environ.get("LORE_SESSION_CURRENT_USER")
|
|
156
|
+
if env_user:
|
|
157
|
+
print(f"User: not set (LORE_SESSION_CURRENT_USER={env_user} available)")
|
|
158
|
+
else:
|
|
159
|
+
print("User: not set")
|
|
160
|
+
|
|
161
|
+
# Task
|
|
162
|
+
current_task_md = session_dir / "current-task.md"
|
|
163
|
+
if current_task_md.is_symlink():
|
|
164
|
+
target = os.readlink(current_task_md)
|
|
165
|
+
parts = target.split("/")
|
|
166
|
+
for part in parts:
|
|
167
|
+
if part and part[0].isdigit() and "_" in part:
|
|
168
|
+
print(f"Task: {part.split('_')[0]} -> {target}")
|
|
169
|
+
break
|
|
170
|
+
else:
|
|
171
|
+
print("Task: not set")
|
|
172
|
+
|
|
173
|
+
return 0
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def cmd_list_users() -> int:
|
|
177
|
+
"""List available users from team.yaml."""
|
|
178
|
+
session_dir = get_session_dir()
|
|
179
|
+
|
|
180
|
+
if not session_dir.exists():
|
|
181
|
+
print("Error: 0-session/ directory not found", file=sys.stderr)
|
|
182
|
+
return 1
|
|
183
|
+
|
|
184
|
+
try:
|
|
185
|
+
team = load_team(session_dir)
|
|
186
|
+
except FileNotFoundError as e:
|
|
187
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
188
|
+
return 1
|
|
189
|
+
|
|
190
|
+
print("Available users:")
|
|
191
|
+
for user_id, user_data in team.items():
|
|
192
|
+
name = user_data.get("name", user_id)
|
|
193
|
+
role = f" ({user_data['role']})" if user_data.get("role") else ""
|
|
194
|
+
print(f" {user_id}: {name}{role}")
|
|
195
|
+
|
|
196
|
+
return 0
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def cmd_clear_task(flags: dict) -> int:
|
|
200
|
+
"""Clear current task symlink."""
|
|
201
|
+
session_dir = get_session_dir()
|
|
202
|
+
|
|
203
|
+
if not session_dir.exists():
|
|
204
|
+
print("Error: 0-session/ directory not found", file=sys.stderr)
|
|
205
|
+
return 1
|
|
206
|
+
|
|
207
|
+
current_task_md = session_dir / "current-task.md"
|
|
208
|
+
current_task_json = session_dir / "current-task.json"
|
|
209
|
+
|
|
210
|
+
cleared = False
|
|
211
|
+
if current_task_md.exists() or current_task_md.is_symlink():
|
|
212
|
+
current_task_md.unlink()
|
|
213
|
+
cleared = True
|
|
214
|
+
if current_task_json.exists():
|
|
215
|
+
current_task_json.unlink()
|
|
216
|
+
cleared = True
|
|
217
|
+
|
|
218
|
+
if not flags["quiet"]:
|
|
219
|
+
print("Task: cleared" if cleared else "Task: none set")
|
|
220
|
+
return 0
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def cmd_generate_index(flags: dict) -> int:
|
|
224
|
+
"""Regenerate lore index files."""
|
|
225
|
+
lore_dir = get_lore_dir()
|
|
226
|
+
|
|
227
|
+
if not lore_dir.exists():
|
|
228
|
+
print(f"Error: lore/ directory not found at {lore_dir}", file=sys.stderr)
|
|
229
|
+
return 1
|
|
230
|
+
|
|
231
|
+
tasks = parse_tasks(lore_dir)
|
|
232
|
+
adrs = parse_adrs(lore_dir)
|
|
233
|
+
blocks = compute_blocks(tasks)
|
|
234
|
+
|
|
235
|
+
# Generate next-tasks.md
|
|
236
|
+
next_content = generate_next(tasks, blocks)
|
|
237
|
+
next_path = lore_dir / "0-session" / "next-tasks.md"
|
|
238
|
+
if next_path.parent.exists():
|
|
239
|
+
next_path.write_text(next_content)
|
|
240
|
+
if not flags["quiet"]:
|
|
241
|
+
print(f"Generated {next_path}")
|
|
242
|
+
|
|
243
|
+
# Generate README.md (unless --next-only)
|
|
244
|
+
if not flags["next_only"]:
|
|
245
|
+
readme_content = generate_readme(tasks, adrs, blocks)
|
|
246
|
+
readme_path = lore_dir / "README.md"
|
|
247
|
+
readme_path.write_text(readme_content)
|
|
248
|
+
if not flags["quiet"]:
|
|
249
|
+
print(f"Generated {readme_path}")
|
|
250
|
+
|
|
251
|
+
return 0
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def cmd_help() -> int:
|
|
255
|
+
"""Show help message."""
|
|
256
|
+
print("""lore-framework-mcp - CLI and MCP server for Lore Framework
|
|
257
|
+
|
|
258
|
+
Usage:
|
|
259
|
+
lore-framework-mcp [command] [options]
|
|
260
|
+
|
|
261
|
+
Commands:
|
|
262
|
+
set-user <id> Set current user from team.yaml
|
|
263
|
+
set-user --env Set user from LORE_SESSION_CURRENT_USER env var
|
|
264
|
+
set-task <id> Set current task by ID
|
|
265
|
+
show-session Show current session state
|
|
266
|
+
list-users List available users from team.yaml
|
|
267
|
+
clear-task Clear current task
|
|
268
|
+
generate-index Regenerate lore/README.md and next-tasks.md
|
|
269
|
+
help Show this help message
|
|
270
|
+
|
|
271
|
+
Options:
|
|
272
|
+
--env Use LORE_SESSION_CURRENT_USER for set-user
|
|
273
|
+
--next-only Only generate next-tasks.md (skip README.md)
|
|
274
|
+
--quiet, -q Suppress output
|
|
275
|
+
|
|
276
|
+
MCP Server:
|
|
277
|
+
Run without arguments to start the MCP server (stdio transport).
|
|
278
|
+
""")
|
|
279
|
+
return 0
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def run_cli(argv: list[str]) -> int:
|
|
283
|
+
"""Run CLI command."""
|
|
284
|
+
command, args, flags = parse_args(argv)
|
|
285
|
+
|
|
286
|
+
commands = {
|
|
287
|
+
"set-user": lambda: cmd_set_user(args, flags),
|
|
288
|
+
"set-task": lambda: cmd_set_task(args, flags),
|
|
289
|
+
"show-session": cmd_show_session,
|
|
290
|
+
"list-users": cmd_list_users,
|
|
291
|
+
"clear-task": lambda: cmd_clear_task(flags),
|
|
292
|
+
"generate-index": lambda: cmd_generate_index(flags),
|
|
293
|
+
"help": cmd_help,
|
|
294
|
+
"--help": cmd_help,
|
|
295
|
+
"-h": cmd_help,
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if command in commands:
|
|
299
|
+
return commands[command]()
|
|
300
|
+
else:
|
|
301
|
+
print(f"Unknown command: {command}", file=sys.stderr)
|
|
302
|
+
print('Run with "help" for usage information.', file=sys.stderr)
|
|
303
|
+
return 1
|
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Lore Framework MCP Server
|
|
3
|
+
|
|
4
|
+
Provides MCP tools for session and index management.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import json
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
|
|
12
|
+
import yaml
|
|
13
|
+
import frontmatter
|
|
14
|
+
from mcp.server.fastmcp import FastMCP
|
|
15
|
+
|
|
16
|
+
# Create MCP server
|
|
17
|
+
mcp = FastMCP("lore-framework")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_project_dir() -> Path:
|
|
21
|
+
"""Get project directory from env or cwd."""
|
|
22
|
+
return Path(os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd()))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_lore_dir() -> Path:
|
|
26
|
+
"""Get lore directory path."""
|
|
27
|
+
return get_project_dir() / "lore"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_session_dir() -> Path:
|
|
31
|
+
"""Get session directory path."""
|
|
32
|
+
return get_lore_dir() / "0-session"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def load_team(session_dir: Path) -> dict:
|
|
36
|
+
"""Load team.yaml file."""
|
|
37
|
+
team_file = session_dir / "team.yaml"
|
|
38
|
+
if not team_file.exists():
|
|
39
|
+
raise FileNotFoundError(f"team.yaml not found at {team_file}")
|
|
40
|
+
with open(team_file, "r") as f:
|
|
41
|
+
return yaml.safe_load(f) or {}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def generate_current_user_md(user_id: str, user_data: dict, team: dict) -> str:
|
|
45
|
+
"""Generate current-user.md content."""
|
|
46
|
+
lines = []
|
|
47
|
+
|
|
48
|
+
# Frontmatter
|
|
49
|
+
lines.append("---")
|
|
50
|
+
lines.append(f"name: {user_id}")
|
|
51
|
+
if user_data.get("github"):
|
|
52
|
+
lines.append(f"github: {user_data['github']}")
|
|
53
|
+
if user_data.get("role"):
|
|
54
|
+
lines.append(f"role: {user_data['role']}")
|
|
55
|
+
lines.append("---")
|
|
56
|
+
lines.append("")
|
|
57
|
+
|
|
58
|
+
# Content
|
|
59
|
+
name = user_data.get("name", user_id)
|
|
60
|
+
lines.append(f"# Current User: {name}")
|
|
61
|
+
lines.append("")
|
|
62
|
+
|
|
63
|
+
if user_data.get("focus"):
|
|
64
|
+
lines.append(f"**Focus:** {user_data['focus'].strip()}")
|
|
65
|
+
lines.append("")
|
|
66
|
+
|
|
67
|
+
if user_data.get("prompting"):
|
|
68
|
+
lines.append("## Communication Preferences")
|
|
69
|
+
lines.append("")
|
|
70
|
+
lines.append(user_data["prompting"].strip())
|
|
71
|
+
lines.append("")
|
|
72
|
+
|
|
73
|
+
if user_data.get("note"):
|
|
74
|
+
lines.append(f"> {user_data['note']}")
|
|
75
|
+
lines.append("")
|
|
76
|
+
|
|
77
|
+
# Other team members
|
|
78
|
+
other_members = [(k, v) for k, v in team.items() if k != user_id]
|
|
79
|
+
if other_members:
|
|
80
|
+
lines.append("---")
|
|
81
|
+
lines.append("")
|
|
82
|
+
lines.append("## Rest of Team")
|
|
83
|
+
lines.append("")
|
|
84
|
+
lines.append("| Name | Role |")
|
|
85
|
+
lines.append("|------|------|")
|
|
86
|
+
for member_id, member_data in other_members:
|
|
87
|
+
member_name = member_data.get("name", member_id)
|
|
88
|
+
role = member_data.get("role", "—")
|
|
89
|
+
lines.append(f"| {member_name} | {role} |")
|
|
90
|
+
lines.append("")
|
|
91
|
+
|
|
92
|
+
return "\n".join(lines)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def find_task(lore_dir: Path, task_id: str) -> Path | None:
|
|
96
|
+
"""Find task file by ID."""
|
|
97
|
+
tasks_dir = lore_dir / "1-tasks"
|
|
98
|
+
task_num = task_id.lstrip("0") or "0"
|
|
99
|
+
|
|
100
|
+
for status_dir in ["active", "blocked", "archive", "backlog"]:
|
|
101
|
+
status_path = tasks_dir / status_dir
|
|
102
|
+
if not status_path.exists():
|
|
103
|
+
continue
|
|
104
|
+
|
|
105
|
+
for item in status_path.iterdir():
|
|
106
|
+
if item.name.startswith("_"):
|
|
107
|
+
continue
|
|
108
|
+
|
|
109
|
+
item_id = item.name.split("_")[0].lstrip("0") or "0"
|
|
110
|
+
if item_id == task_num:
|
|
111
|
+
if item.is_dir():
|
|
112
|
+
readme = item / "README.md"
|
|
113
|
+
if readme.exists():
|
|
114
|
+
return readme
|
|
115
|
+
elif item.suffix == ".md":
|
|
116
|
+
return item
|
|
117
|
+
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# ============================================================================
|
|
122
|
+
# MCP Tools
|
|
123
|
+
# ============================================================================
|
|
124
|
+
|
|
125
|
+
@mcp.tool()
|
|
126
|
+
def lore_framework_set_user(user_id: str) -> str:
|
|
127
|
+
"""Set current user from team.yaml.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
user_id: The user ID from team.yaml
|
|
131
|
+
"""
|
|
132
|
+
session_dir = get_session_dir()
|
|
133
|
+
|
|
134
|
+
if not session_dir.exists():
|
|
135
|
+
return "Error: 0-session/ directory not found. Run lore framework bootstrap first."
|
|
136
|
+
|
|
137
|
+
try:
|
|
138
|
+
team = load_team(session_dir)
|
|
139
|
+
except FileNotFoundError as e:
|
|
140
|
+
return f"Error: {e}"
|
|
141
|
+
|
|
142
|
+
if user_id not in team:
|
|
143
|
+
available = ", ".join(team.keys())
|
|
144
|
+
return f"Error: User '{user_id}' not found. Available: {available}"
|
|
145
|
+
|
|
146
|
+
user_data = team[user_id]
|
|
147
|
+
content = generate_current_user_md(user_id, user_data, team)
|
|
148
|
+
|
|
149
|
+
current_user_md = session_dir / "current-user.md"
|
|
150
|
+
current_user_md.write_text(content)
|
|
151
|
+
|
|
152
|
+
name = user_data.get("name", user_id)
|
|
153
|
+
return f"User set: {user_id} ({name})"
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@mcp.tool()
|
|
157
|
+
def lore_framework_set_task(task_id: str) -> str:
|
|
158
|
+
"""Set current task by ID (creates symlink to task file).
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
task_id: The task ID (e.g., "1", "01", "123")
|
|
162
|
+
"""
|
|
163
|
+
lore_dir = get_lore_dir()
|
|
164
|
+
session_dir = get_session_dir()
|
|
165
|
+
|
|
166
|
+
if not session_dir.exists():
|
|
167
|
+
return "Error: 0-session/ directory not found. Run lore framework bootstrap first."
|
|
168
|
+
|
|
169
|
+
task_path = find_task(lore_dir, task_id)
|
|
170
|
+
if not task_path:
|
|
171
|
+
return f"Error: Task {task_id} not found. Check 1-tasks/{{active,blocked,archive,backlog}}/"
|
|
172
|
+
|
|
173
|
+
current_task_md = session_dir / "current-task.md"
|
|
174
|
+
current_task_json = session_dir / "current-task.json"
|
|
175
|
+
|
|
176
|
+
# Remove existing symlink
|
|
177
|
+
if current_task_md.exists() or current_task_md.is_symlink():
|
|
178
|
+
current_task_md.unlink()
|
|
179
|
+
|
|
180
|
+
# Create relative symlink
|
|
181
|
+
relative_path = os.path.relpath(task_path, session_dir)
|
|
182
|
+
current_task_md.symlink_to(relative_path)
|
|
183
|
+
|
|
184
|
+
# Write task metadata
|
|
185
|
+
task_dir = os.path.relpath(task_path.parent, lore_dir)
|
|
186
|
+
current_task_json.write_text(json.dumps({"id": task_id, "path": task_dir}, indent=2))
|
|
187
|
+
|
|
188
|
+
return f"Task set: {task_id} -> {relative_path}"
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@mcp.tool()
|
|
192
|
+
def lore_framework_show_session() -> str:
|
|
193
|
+
"""Show current session state (user and task)."""
|
|
194
|
+
session_dir = get_session_dir()
|
|
195
|
+
|
|
196
|
+
if not session_dir.exists():
|
|
197
|
+
return "Error: 0-session/ directory not found. Run lore framework bootstrap first."
|
|
198
|
+
|
|
199
|
+
lines = []
|
|
200
|
+
|
|
201
|
+
# User
|
|
202
|
+
current_user_md = session_dir / "current-user.md"
|
|
203
|
+
if current_user_md.exists():
|
|
204
|
+
content = current_user_md.read_text()
|
|
205
|
+
for line in content.split("\n"):
|
|
206
|
+
if line.startswith("name:"):
|
|
207
|
+
lines.append(f"User: {line.split(':', 1)[1].strip()}")
|
|
208
|
+
break
|
|
209
|
+
else:
|
|
210
|
+
env_user = os.environ.get("LORE_SESSION_CURRENT_USER")
|
|
211
|
+
if env_user:
|
|
212
|
+
lines.append(f"User: not set (LORE_SESSION_CURRENT_USER={env_user} available)")
|
|
213
|
+
else:
|
|
214
|
+
lines.append("User: not set")
|
|
215
|
+
|
|
216
|
+
# Task
|
|
217
|
+
current_task_md = session_dir / "current-task.md"
|
|
218
|
+
if current_task_md.is_symlink():
|
|
219
|
+
target = os.readlink(current_task_md)
|
|
220
|
+
parts = target.split("/")
|
|
221
|
+
for part in parts:
|
|
222
|
+
if part and part[0].isdigit() and "_" in part:
|
|
223
|
+
lines.append(f"Task: {part.split('_')[0]} -> {target}")
|
|
224
|
+
break
|
|
225
|
+
else:
|
|
226
|
+
lines.append("Task: not set")
|
|
227
|
+
|
|
228
|
+
return "\n".join(lines)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
@mcp.tool()
|
|
232
|
+
def lore_framework_list_users() -> str:
|
|
233
|
+
"""List available users from team.yaml."""
|
|
234
|
+
session_dir = get_session_dir()
|
|
235
|
+
|
|
236
|
+
if not session_dir.exists():
|
|
237
|
+
return "Error: 0-session/ directory not found. Run lore framework bootstrap first."
|
|
238
|
+
|
|
239
|
+
try:
|
|
240
|
+
team = load_team(session_dir)
|
|
241
|
+
except FileNotFoundError as e:
|
|
242
|
+
return f"Error: {e}"
|
|
243
|
+
|
|
244
|
+
lines = ["Available users:", ""]
|
|
245
|
+
for user_id, user_data in team.items():
|
|
246
|
+
name = user_data.get("name", user_id)
|
|
247
|
+
role = f" ({user_data['role']})" if user_data.get("role") else ""
|
|
248
|
+
lines.append(f"- {user_id}: {name}{role}")
|
|
249
|
+
|
|
250
|
+
return "\n".join(lines)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
@mcp.tool()
|
|
254
|
+
def lore_framework_clear_task() -> str:
|
|
255
|
+
"""Clear current task symlink."""
|
|
256
|
+
session_dir = get_session_dir()
|
|
257
|
+
|
|
258
|
+
if not session_dir.exists():
|
|
259
|
+
return "Error: 0-session/ directory not found. Run lore framework bootstrap first."
|
|
260
|
+
|
|
261
|
+
current_task_md = session_dir / "current-task.md"
|
|
262
|
+
current_task_json = session_dir / "current-task.json"
|
|
263
|
+
|
|
264
|
+
cleared = False
|
|
265
|
+
if current_task_md.exists() or current_task_md.is_symlink():
|
|
266
|
+
current_task_md.unlink()
|
|
267
|
+
cleared = True
|
|
268
|
+
if current_task_json.exists():
|
|
269
|
+
current_task_json.unlink()
|
|
270
|
+
cleared = True
|
|
271
|
+
|
|
272
|
+
return "Task cleared" if cleared else "No task was set"
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
@mcp.tool()
|
|
276
|
+
def lore_framework_generate_index() -> str:
|
|
277
|
+
"""Regenerate lore/README.md and 0-session/next-tasks.md from task and ADR frontmatter."""
|
|
278
|
+
lore_dir = get_lore_dir()
|
|
279
|
+
|
|
280
|
+
if not lore_dir.exists():
|
|
281
|
+
return f"Error: lore/ directory not found at {lore_dir}"
|
|
282
|
+
|
|
283
|
+
tasks = parse_tasks(lore_dir)
|
|
284
|
+
adrs = parse_adrs(lore_dir)
|
|
285
|
+
blocks = compute_blocks(tasks)
|
|
286
|
+
|
|
287
|
+
# Generate README.md
|
|
288
|
+
readme_content = generate_readme(tasks, adrs, blocks)
|
|
289
|
+
readme_path = lore_dir / "README.md"
|
|
290
|
+
readme_path.write_text(readme_content)
|
|
291
|
+
|
|
292
|
+
# Generate next-tasks.md
|
|
293
|
+
next_content = generate_next(tasks, blocks)
|
|
294
|
+
next_path = lore_dir / "0-session" / "next-tasks.md"
|
|
295
|
+
if next_path.parent.exists():
|
|
296
|
+
next_path.write_text(next_content)
|
|
297
|
+
|
|
298
|
+
stats = {
|
|
299
|
+
"active": len([t for t in tasks.values() if t["status"] == "active"]),
|
|
300
|
+
"blocked": len([t for t in tasks.values() if t["status"] == "blocked"]),
|
|
301
|
+
"backlog": len([t for t in tasks.values() if t["status"] == "backlog"]),
|
|
302
|
+
"completed": len([t for t in tasks.values() if t["status"] == "completed"]),
|
|
303
|
+
"adrs": len(adrs),
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return f"""Generated:
|
|
307
|
+
- {readme_path}
|
|
308
|
+
- {next_path}
|
|
309
|
+
|
|
310
|
+
Stats: {stats['active']} active, {stats['blocked']} blocked, {stats['backlog']} backlog, {stats['completed']} completed, {stats['adrs']} ADRs"""
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
# ============================================================================
|
|
314
|
+
# Index Generation Helpers
|
|
315
|
+
# ============================================================================
|
|
316
|
+
|
|
317
|
+
def parse_tasks(lore_dir: Path) -> dict:
|
|
318
|
+
"""Parse all tasks from 1-tasks/."""
|
|
319
|
+
tasks = {}
|
|
320
|
+
tasks_base = lore_dir / "1-tasks"
|
|
321
|
+
|
|
322
|
+
for subdir in ["active", "blocked", "archive", "backlog"]:
|
|
323
|
+
subdir_path = tasks_base / subdir
|
|
324
|
+
if not subdir_path.exists():
|
|
325
|
+
continue
|
|
326
|
+
|
|
327
|
+
for item in subdir_path.iterdir():
|
|
328
|
+
if item.name.startswith("_"):
|
|
329
|
+
continue
|
|
330
|
+
|
|
331
|
+
task_path = None
|
|
332
|
+
if item.is_file() and item.suffix == ".md":
|
|
333
|
+
task_path = item
|
|
334
|
+
elif item.is_dir():
|
|
335
|
+
readme = item / "README.md"
|
|
336
|
+
if readme.exists():
|
|
337
|
+
task_path = readme
|
|
338
|
+
|
|
339
|
+
if not task_path:
|
|
340
|
+
continue
|
|
341
|
+
|
|
342
|
+
# Extract task ID
|
|
343
|
+
if task_path.name == "README.md":
|
|
344
|
+
task_id = task_path.parent.name.split("_")[0]
|
|
345
|
+
else:
|
|
346
|
+
task_id = item.stem.split("_")[0]
|
|
347
|
+
|
|
348
|
+
if not task_id.isdigit():
|
|
349
|
+
continue
|
|
350
|
+
|
|
351
|
+
try:
|
|
352
|
+
post = frontmatter.load(task_path)
|
|
353
|
+
meta = post.metadata
|
|
354
|
+
|
|
355
|
+
if not meta:
|
|
356
|
+
continue
|
|
357
|
+
|
|
358
|
+
status = meta.get("status", "active")
|
|
359
|
+
if subdir == "archive":
|
|
360
|
+
status = "completed"
|
|
361
|
+
elif subdir == "blocked":
|
|
362
|
+
status = "blocked"
|
|
363
|
+
elif subdir == "backlog":
|
|
364
|
+
status = "backlog"
|
|
365
|
+
|
|
366
|
+
# Extract blocked_by from history
|
|
367
|
+
blocked_by = []
|
|
368
|
+
history = meta.get("history", [])
|
|
369
|
+
if history and isinstance(history, list) and len(history) > 0:
|
|
370
|
+
latest = history[-1]
|
|
371
|
+
if latest.get("status") == "blocked":
|
|
372
|
+
by = latest.get("by", [])
|
|
373
|
+
if isinstance(by, list):
|
|
374
|
+
blocked_by = [str(b) for b in by]
|
|
375
|
+
elif by:
|
|
376
|
+
blocked_by = [str(by)]
|
|
377
|
+
|
|
378
|
+
tasks[str(meta.get("id", task_id))] = {
|
|
379
|
+
"id": str(meta.get("id", task_id)),
|
|
380
|
+
"title": meta.get("title") or extract_title(post.content),
|
|
381
|
+
"type": meta.get("type", "FEATURE"),
|
|
382
|
+
"status": status,
|
|
383
|
+
"path": str(task_path.relative_to(lore_dir.parent)),
|
|
384
|
+
"blocked_by": blocked_by,
|
|
385
|
+
"related_adr": meta.get("related_adr", []) or [],
|
|
386
|
+
}
|
|
387
|
+
except Exception:
|
|
388
|
+
pass
|
|
389
|
+
|
|
390
|
+
return tasks
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def parse_adrs(lore_dir: Path) -> dict:
|
|
394
|
+
"""Parse all ADRs from 2-adrs/."""
|
|
395
|
+
adrs = {}
|
|
396
|
+
adr_dir = lore_dir / "2-adrs"
|
|
397
|
+
|
|
398
|
+
if not adr_dir.exists():
|
|
399
|
+
return adrs
|
|
400
|
+
|
|
401
|
+
for item in adr_dir.glob("*.md"):
|
|
402
|
+
if item.name.startswith("_"):
|
|
403
|
+
continue
|
|
404
|
+
|
|
405
|
+
adr_id = item.stem.split("_")[0]
|
|
406
|
+
|
|
407
|
+
try:
|
|
408
|
+
post = frontmatter.load(item)
|
|
409
|
+
meta = post.metadata
|
|
410
|
+
|
|
411
|
+
if not meta:
|
|
412
|
+
continue
|
|
413
|
+
|
|
414
|
+
adrs[str(meta.get("id", adr_id))] = {
|
|
415
|
+
"id": str(meta.get("id", adr_id)),
|
|
416
|
+
"title": meta.get("title") or extract_title(post.content),
|
|
417
|
+
"status": meta.get("status", "proposed"),
|
|
418
|
+
"path": str(item.relative_to(lore_dir.parent)),
|
|
419
|
+
"related_tasks": meta.get("related_tasks", []) or [],
|
|
420
|
+
}
|
|
421
|
+
except Exception:
|
|
422
|
+
pass
|
|
423
|
+
|
|
424
|
+
return adrs
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def extract_title(content: str) -> str:
|
|
428
|
+
"""Extract title from markdown content."""
|
|
429
|
+
for line in content.split("\n"):
|
|
430
|
+
if line.startswith("# "):
|
|
431
|
+
return line[2:].strip()
|
|
432
|
+
return "Untitled"
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def compute_blocks(tasks: dict) -> dict:
|
|
436
|
+
"""Compute which tasks block other tasks."""
|
|
437
|
+
blocks = {tid: [] for tid in tasks}
|
|
438
|
+
|
|
439
|
+
for task in tasks.values():
|
|
440
|
+
for blocker_id in task.get("blocked_by", []):
|
|
441
|
+
if blocker_id in blocks:
|
|
442
|
+
blocks[blocker_id].append(task["id"])
|
|
443
|
+
|
|
444
|
+
return blocks
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def generate_readme(tasks: dict, adrs: dict, blocks: dict) -> str:
|
|
448
|
+
"""Generate lore/README.md content."""
|
|
449
|
+
now = datetime.now()
|
|
450
|
+
date_str = now.strftime("%Y-%m-%d %H:%M")
|
|
451
|
+
|
|
452
|
+
active_count = len([t for t in tasks.values() if t["status"] == "active"])
|
|
453
|
+
blocked_count = len([t for t in tasks.values() if t["status"] == "blocked"])
|
|
454
|
+
backlog_count = len([t for t in tasks.values() if t["status"] == "backlog"])
|
|
455
|
+
completed_count = len([t for t in tasks.values() if t["status"] == "completed"])
|
|
456
|
+
adr_count = len(adrs)
|
|
457
|
+
|
|
458
|
+
sections = [f"""# Lore Index
|
|
459
|
+
|
|
460
|
+
> Auto-generated on {date_str}. Do not edit manually.
|
|
461
|
+
> Use `lore-framework_generate-index` tool to regenerate.
|
|
462
|
+
|
|
463
|
+
Quick reference for task dependencies, status, and ADR relationships.
|
|
464
|
+
|
|
465
|
+
## Quick Stats
|
|
466
|
+
|
|
467
|
+
| Active | Blocked | Backlog | Completed | ADRs |
|
|
468
|
+
|:------:|:-------:|:-------:|:---------:|:----:|
|
|
469
|
+
| {active_count} | {blocked_count} | {backlog_count} | {completed_count} | {adr_count} |"""]
|
|
470
|
+
|
|
471
|
+
# Ready to start
|
|
472
|
+
completed_ids = {t["id"] for t in tasks.values() if t["status"] == "completed"}
|
|
473
|
+
ready = []
|
|
474
|
+
for task in tasks.values():
|
|
475
|
+
if task["status"] not in ["active", "blocked"]:
|
|
476
|
+
continue
|
|
477
|
+
blocked_by = task.get("blocked_by", [])
|
|
478
|
+
if not blocked_by or all(b in completed_ids for b in blocked_by):
|
|
479
|
+
ready.append(task)
|
|
480
|
+
|
|
481
|
+
if ready:
|
|
482
|
+
sections.append("\n## Ready to Start\n\nThese tasks have no blockers (or all blockers completed):\n")
|
|
483
|
+
for task in sorted(ready, key=lambda t: t["id"]):
|
|
484
|
+
block_count = len(blocks.get(task["id"], []))
|
|
485
|
+
priority = "**HIGH**" if block_count >= 3 else "medium" if block_count >= 1 else "low"
|
|
486
|
+
sections.append(f"- **Task {task['id']}**: [{task['title']}]({task['path']}) — blocks {block_count} tasks ({priority})")
|
|
487
|
+
|
|
488
|
+
# Task status table
|
|
489
|
+
sections.append("\n## Task Status\n")
|
|
490
|
+
sections.append("| ID | Title | Type | Status | Blocked By | Blocks | ADRs |")
|
|
491
|
+
sections.append("|:---|:------|:-----|:-------|:-----------|:-------|:-----|")
|
|
492
|
+
|
|
493
|
+
status_order = {"active": 0, "blocked": 1, "backlog": 2, "completed": 3}
|
|
494
|
+
sorted_tasks = sorted(tasks.values(), key=lambda t: (status_order.get(t["status"], 4), t["id"]))
|
|
495
|
+
|
|
496
|
+
for task in sorted_tasks:
|
|
497
|
+
blocked_by = ", ".join(task.get("blocked_by", [])) or "—"
|
|
498
|
+
task_blocks = ", ".join(sorted(blocks.get(task["id"], []))) or "—"
|
|
499
|
+
related_adr = ", ".join(str(a) for a in task.get("related_adr", [])) or "—"
|
|
500
|
+
status_display = f"**{task['status']}**" if task["status"] == "active" else task["status"]
|
|
501
|
+
title = task["title"][:35] + "..." if len(task["title"]) > 35 else task["title"]
|
|
502
|
+
sections.append(f"| {task['id']} | [{title}]({task['path']}) | {task['type']} | {status_display} | {blocked_by} | {task_blocks} | {related_adr} |")
|
|
503
|
+
|
|
504
|
+
# ADR table
|
|
505
|
+
if adrs:
|
|
506
|
+
sections.append("\n## Architecture Decision Records\n")
|
|
507
|
+
sections.append("| ID | Title | Status | Related Tasks |")
|
|
508
|
+
sections.append("|:---|:------|:-------|:--------------|")
|
|
509
|
+
for adr in sorted(adrs.values(), key=lambda a: a["id"]):
|
|
510
|
+
related = ", ".join(str(t) for t in adr.get("related_tasks", [])) or "—"
|
|
511
|
+
sections.append(f"| {adr['id']} | [{adr['title']}]({adr['path']}) | {adr['status']} | {related} |")
|
|
512
|
+
|
|
513
|
+
return "\n".join(sections)
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def generate_next(tasks: dict, blocks: dict) -> str:
|
|
517
|
+
"""Generate 0-session/next-tasks.md content."""
|
|
518
|
+
completed_ids = {t["id"] for t in tasks.values() if t["status"] == "completed"}
|
|
519
|
+
ready = []
|
|
520
|
+
for task in tasks.values():
|
|
521
|
+
if task["status"] not in ["active", "blocked"]:
|
|
522
|
+
continue
|
|
523
|
+
blocked_by = task.get("blocked_by", [])
|
|
524
|
+
if not blocked_by or all(b in completed_ids for b in blocked_by):
|
|
525
|
+
ready.append(task)
|
|
526
|
+
|
|
527
|
+
active_count = len([t for t in tasks.values() if t["status"] == "active"])
|
|
528
|
+
blocked_count = len([t for t in tasks.values() if t["status"] == "blocked"])
|
|
529
|
+
backlog_count = len([t for t in tasks.values() if t["status"] == "backlog"])
|
|
530
|
+
completed_count = len([t for t in tasks.values() if t["status"] == "completed"])
|
|
531
|
+
|
|
532
|
+
lines = [
|
|
533
|
+
"# Next Tasks",
|
|
534
|
+
"",
|
|
535
|
+
"> Auto-generated. Use `lore-framework_generate-index` tool to regenerate.",
|
|
536
|
+
"> Full index: [README.md](../README.md)",
|
|
537
|
+
"",
|
|
538
|
+
f"**Active:** {active_count} | **Blocked:** {blocked_count} | **Backlog:** {backlog_count} | **Completed:** {completed_count}",
|
|
539
|
+
"",
|
|
540
|
+
]
|
|
541
|
+
|
|
542
|
+
if ready:
|
|
543
|
+
lines.append("## Ready to Start")
|
|
544
|
+
lines.append("")
|
|
545
|
+
|
|
546
|
+
sorted_ready = sorted(ready, key=lambda t: -len(blocks.get(t["id"], [])))
|
|
547
|
+
for task in sorted_ready[:10]:
|
|
548
|
+
block_count = len(blocks.get(task["id"], []))
|
|
549
|
+
priority = " [HIGH]" if block_count >= 3 else ""
|
|
550
|
+
unblocks = f"unblocks {block_count}" if block_count > 0 else "no blockers"
|
|
551
|
+
lines.append(f"- **{task['id']}** [{task['title']}]({task['path']}) — {unblocks}{priority}")
|
|
552
|
+
lines.append("")
|
|
553
|
+
|
|
554
|
+
blocked_tasks = [t for t in tasks.values() if t["status"] == "blocked"]
|
|
555
|
+
if blocked_tasks:
|
|
556
|
+
blocked_ids = ", ".join(sorted(t["id"] for t in blocked_tasks))
|
|
557
|
+
lines.append(f"## Blocked ({len(blocked_tasks)})")
|
|
558
|
+
lines.append("")
|
|
559
|
+
lines.append(blocked_ids)
|
|
560
|
+
lines.append("")
|
|
561
|
+
|
|
562
|
+
lines.append("---")
|
|
563
|
+
lines.append("")
|
|
564
|
+
lines.append("Set current task: use `lore-framework_set-task` tool with task ID")
|
|
565
|
+
lines.append("")
|
|
566
|
+
|
|
567
|
+
return "\n".join(lines)
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
def run_server():
|
|
571
|
+
"""Run the MCP server."""
|
|
572
|
+
mcp.run()
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lore-framework-mcp
|
|
3
|
+
Version: 1.2.2
|
|
4
|
+
Summary: MCP server for Lore Framework - AI-readable project memory with task/ADR/wiki management
|
|
5
|
+
Project-URL: Homepage, https://github.com/maledorak/maledorak-marketplace/tree/main/packages/lore-framework-mcp-py
|
|
6
|
+
Project-URL: Repository, https://github.com/maledorak/maledorak-marketplace
|
|
7
|
+
Author-email: "Mariusz (Maledorak) Korzekwa" <mariusz@korzekwa.dev>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Keywords: adr,ai-memory,claude,claude-code,lore,mcp,project-management,task-tracking
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Requires-Dist: mcp>=1.0.0
|
|
20
|
+
Requires-Dist: python-frontmatter>=1.0.0
|
|
21
|
+
Requires-Dist: pyyaml>=6.0
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# Lore Framework MCP (Python)
|
|
25
|
+
|
|
26
|
+
MCP server for Lore Framework - AI-readable project memory with task/ADR/wiki management.
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# Using uvx (recommended)
|
|
32
|
+
uvx lore-framework-mcp
|
|
33
|
+
|
|
34
|
+
# Using pip
|
|
35
|
+
pip install lore-framework-mcp
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
|
|
40
|
+
### As MCP Server
|
|
41
|
+
|
|
42
|
+
Add to your `.mcp.json`:
|
|
43
|
+
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"mcpServers": {
|
|
47
|
+
"lore-framework": {
|
|
48
|
+
"command": "uvx",
|
|
49
|
+
"args": ["lore-framework-mcp"]
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### As CLI
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
# Set current user
|
|
59
|
+
lore-framework-mcp set-user <user_id>
|
|
60
|
+
lore-framework-mcp set-user --env # from LORE_SESSION_CURRENT_USER
|
|
61
|
+
|
|
62
|
+
# Set current task
|
|
63
|
+
lore-framework-mcp set-task <task_id>
|
|
64
|
+
|
|
65
|
+
# Show session state
|
|
66
|
+
lore-framework-mcp show-session
|
|
67
|
+
|
|
68
|
+
# List available users
|
|
69
|
+
lore-framework-mcp list-users
|
|
70
|
+
|
|
71
|
+
# Clear current task
|
|
72
|
+
lore-framework-mcp clear-task
|
|
73
|
+
|
|
74
|
+
# Regenerate indexes
|
|
75
|
+
lore-framework-mcp generate-index
|
|
76
|
+
lore-framework-mcp generate-index --next-only --quiet
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## MCP Tools
|
|
80
|
+
|
|
81
|
+
| Tool | Description |
|
|
82
|
+
|------|-------------|
|
|
83
|
+
| `lore_framework_set_user` | Set current user from team.yaml |
|
|
84
|
+
| `lore_framework_set_task` | Set current task by ID (creates symlink) |
|
|
85
|
+
| `lore_framework_show_session` | Show current session state (user and task) |
|
|
86
|
+
| `lore_framework_list_users` | List available users from team.yaml |
|
|
87
|
+
| `lore_framework_clear_task` | Clear current task symlink |
|
|
88
|
+
| `lore_framework_generate_index` | Regenerate lore/README.md and next-tasks.md |
|
|
89
|
+
|
|
90
|
+
## Lore Directory Structure
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
lore/
|
|
94
|
+
├── 0-session/ # Session state (gitignored)
|
|
95
|
+
│ ├── team.yaml # Team members definition
|
|
96
|
+
│ ├── current-user.md # Active user (generated)
|
|
97
|
+
│ ├── current-task.md # Symlink to active task
|
|
98
|
+
│ └── next-tasks.md # Auto-generated task queue
|
|
99
|
+
├── 1-tasks/ # Task management
|
|
100
|
+
│ ├── active/ # In-progress tasks
|
|
101
|
+
│ ├── blocked/ # Blocked tasks
|
|
102
|
+
│ ├── backlog/ # Planned tasks
|
|
103
|
+
│ └── archive/ # Completed tasks
|
|
104
|
+
├── 2-adrs/ # Architecture Decision Records
|
|
105
|
+
├── 3-wiki/ # Project documentation
|
|
106
|
+
└── README.md # Auto-generated index
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Documentation
|
|
110
|
+
|
|
111
|
+
See full documentation: [Lore Framework Plugin](https://github.com/maledorak/maledorak-marketplace/tree/main/plugins/lore-framework)
|
|
112
|
+
|
|
113
|
+
## License
|
|
114
|
+
|
|
115
|
+
MIT
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
lore_framework_mcp/__init__.py,sha256=VUDi7g79QYVWyGIy1cr-qNPI5VJA3N4Qi2FU1WlOgIo,479
|
|
2
|
+
lore_framework_mcp/cli.py,sha256=UPAS0HgP9aIYfvZo7CuQpPhfK6egfpHbwpXMLAJbPRw,8840
|
|
3
|
+
lore_framework_mcp/server.py,sha256=XRCruIfGIyly12P_jQs7h6viXRTZ3V19ZU7NfBrosf4,19313
|
|
4
|
+
lore_framework_mcp-1.2.2.dist-info/METADATA,sha256=Ui_UEp27QVGsvjDUniMIhjmdqn3fax6i-bGUzF7W3Cg,3407
|
|
5
|
+
lore_framework_mcp-1.2.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
6
|
+
lore_framework_mcp-1.2.2.dist-info/entry_points.txt,sha256=N60Mad4vqpVo9_rZa9fW1H9BVGIWzZu-P3EHi1a8rzI,63
|
|
7
|
+
lore_framework_mcp-1.2.2.dist-info/RECORD,,
|