handoff-mcp-server 0.1.0
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.
- package/Cargo.lock +781 -0
- package/Cargo.toml +24 -0
- package/LICENSE +21 -0
- package/README.md +223 -0
- package/bin/handoff-mcp.js +22 -0
- package/package.json +43 -0
- package/scripts/postinstall.js +44 -0
- package/src/lib.rs +2 -0
- package/src/main.rs +29 -0
- package/src/mcp/handlers/config.rs +115 -0
- package/src/mcp/handlers/dashboard.rs +109 -0
- package/src/mcp/handlers/init.rs +28 -0
- package/src/mcp/handlers/list_tasks.rs +61 -0
- package/src/mcp/handlers/load_context.rs +94 -0
- package/src/mcp/handlers/mod.rs +58 -0
- package/src/mcp/handlers/save_context.rs +96 -0
- package/src/mcp/handlers/update_task.rs +207 -0
- package/src/mcp/mod.rs +6 -0
- package/src/mcp/protocol.rs +41 -0
- package/src/mcp/resources.rs +55 -0
- package/src/mcp/router.rs +150 -0
- package/src/mcp/tools.rs +284 -0
- package/src/mcp/types.rs +108 -0
- package/src/storage/config.rs +112 -0
- package/src/storage/git.rs +47 -0
- package/src/storage/mod.rs +41 -0
- package/src/storage/sessions.rs +167 -0
- package/src/storage/tasks.rs +365 -0
- package/tests/mcp_protocol.rs +204 -0
- package/tests/storage_config.rs +107 -0
- package/tests/storage_sessions.rs +195 -0
- package/tests/storage_tasks.rs +302 -0
- package/tests/tool_dashboard.rs +165 -0
- package/tests/tool_init.rs +176 -0
- package/tests/tool_sessions.rs +318 -0
- package/tests/tool_tasks.rs +408 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
use serde_json::{json, Value};
|
|
2
|
+
|
|
3
|
+
use super::handlers::handle_tool_call;
|
|
4
|
+
use super::tools::{all_resource_definitions, all_tool_definitions};
|
|
5
|
+
use super::types::{
|
|
6
|
+
InitializeResult, JsonRpcResponse, ResourcesCapability, ServerCapabilities, ServerInfo,
|
|
7
|
+
ToolsCapability, ToolsListResult, INTERNAL_ERROR, METHOD_NOT_FOUND, PROTOCOL_VERSION,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
pub fn handle_request(method: &str, params: Option<&Value>) -> JsonRpcResponse {
|
|
11
|
+
match method {
|
|
12
|
+
"initialize" => handle_initialize(),
|
|
13
|
+
"notifications/initialized" => JsonRpcResponse {
|
|
14
|
+
jsonrpc: "2.0".to_string(),
|
|
15
|
+
id: None,
|
|
16
|
+
result: None,
|
|
17
|
+
error: None,
|
|
18
|
+
},
|
|
19
|
+
"tools/list" => handle_tools_list(),
|
|
20
|
+
"tools/call" => handle_tools_call(params),
|
|
21
|
+
"resources/list" => handle_resources_list(),
|
|
22
|
+
"resources/read" => handle_resources_read(params),
|
|
23
|
+
_ => JsonRpcResponse::error(
|
|
24
|
+
None,
|
|
25
|
+
METHOD_NOT_FOUND,
|
|
26
|
+
format!("Method not found: {method}"),
|
|
27
|
+
),
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
fn handle_initialize() -> JsonRpcResponse {
|
|
32
|
+
let result = InitializeResult {
|
|
33
|
+
protocol_version: PROTOCOL_VERSION.to_string(),
|
|
34
|
+
capabilities: ServerCapabilities {
|
|
35
|
+
tools: Some(ToolsCapability {
|
|
36
|
+
list_changed: false,
|
|
37
|
+
}),
|
|
38
|
+
resources: Some(ResourcesCapability {}),
|
|
39
|
+
},
|
|
40
|
+
server_info: ServerInfo {
|
|
41
|
+
name: "handoff-mcp".to_string(),
|
|
42
|
+
version: env!("CARGO_PKG_VERSION").to_string(),
|
|
43
|
+
},
|
|
44
|
+
instructions: Some(
|
|
45
|
+
"Handoff MCP server for AI session context persistence. \
|
|
46
|
+
Call handoff_load_context at session start, \
|
|
47
|
+
handoff_save_context at session end.\n\n\
|
|
48
|
+
## Session Start\n\
|
|
49
|
+
1. Call handoff_load_context (no args needed — uses cwd)\n\
|
|
50
|
+
2. If it returns \"not initialized\", call handoff_init with the project name\n\
|
|
51
|
+
3. Review returned tasks, decisions, blockers, and git state before proceeding\n\n\
|
|
52
|
+
## Session End\n\
|
|
53
|
+
1. Call handoff_save_context with:\n\
|
|
54
|
+
- summary: one-line description of what was accomplished\n\
|
|
55
|
+
- decisions: key decisions made (with reason and confidence)\n\
|
|
56
|
+
- blockers: anything preventing progress\n\
|
|
57
|
+
- handoff_notes: caution/context/suggestion for the next session\n\
|
|
58
|
+
- context_pointers: files and line ranges the next session should look at\n\n\
|
|
59
|
+
## During Work\n\
|
|
60
|
+
- Use handoff_update_task to create/update tasks as work progresses\n\
|
|
61
|
+
- Mark tasks in_progress when starting, done when complete\n\
|
|
62
|
+
- Record decisions as they are made, not just at session end"
|
|
63
|
+
.to_string(),
|
|
64
|
+
),
|
|
65
|
+
};
|
|
66
|
+
match serde_json::to_value(result) {
|
|
67
|
+
Ok(value) => JsonRpcResponse::success(None, value),
|
|
68
|
+
Err(e) => JsonRpcResponse::error(None, INTERNAL_ERROR, format!("Serialization error: {e}")),
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
fn handle_tools_list() -> JsonRpcResponse {
|
|
73
|
+
let result = ToolsListResult {
|
|
74
|
+
tools: all_tool_definitions(),
|
|
75
|
+
};
|
|
76
|
+
match serde_json::to_value(result) {
|
|
77
|
+
Ok(value) => JsonRpcResponse::success(None, value),
|
|
78
|
+
Err(e) => JsonRpcResponse::error(None, INTERNAL_ERROR, format!("Serialization error: {e}")),
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
fn handle_tools_call(params: Option<&Value>) -> JsonRpcResponse {
|
|
83
|
+
let params = match params {
|
|
84
|
+
Some(p) => p,
|
|
85
|
+
None => {
|
|
86
|
+
return JsonRpcResponse::error(
|
|
87
|
+
None,
|
|
88
|
+
super::types::INVALID_REQUEST,
|
|
89
|
+
"tools/call requires params",
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
let name = match params.get("name").and_then(|v| v.as_str()) {
|
|
95
|
+
Some(n) => n,
|
|
96
|
+
None => {
|
|
97
|
+
return JsonRpcResponse::error(
|
|
98
|
+
None,
|
|
99
|
+
super::types::INVALID_REQUEST,
|
|
100
|
+
"tools/call requires 'name' parameter",
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
let arguments = params
|
|
106
|
+
.get("arguments")
|
|
107
|
+
.cloned()
|
|
108
|
+
.unwrap_or_else(|| json!({}));
|
|
109
|
+
|
|
110
|
+
handle_tool_call(name, &arguments)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
fn handle_resources_list() -> JsonRpcResponse {
|
|
114
|
+
let resources = all_resource_definitions();
|
|
115
|
+
let result = json!({ "resources": resources });
|
|
116
|
+
JsonRpcResponse::success(None, result)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
fn handle_resources_read(params: Option<&Value>) -> JsonRpcResponse {
|
|
120
|
+
let params = match params {
|
|
121
|
+
Some(p) => p,
|
|
122
|
+
None => {
|
|
123
|
+
return JsonRpcResponse::error(
|
|
124
|
+
None,
|
|
125
|
+
super::types::INVALID_REQUEST,
|
|
126
|
+
"resources/read requires params",
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
let uri = match params.get("uri").and_then(|v| v.as_str()) {
|
|
132
|
+
Some(u) => u,
|
|
133
|
+
None => {
|
|
134
|
+
return JsonRpcResponse::error(
|
|
135
|
+
None,
|
|
136
|
+
super::types::INVALID_REQUEST,
|
|
137
|
+
"resources/read requires 'uri' parameter",
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
match super::resources::handle_resource_read(uri) {
|
|
143
|
+
Ok(result) => JsonRpcResponse::success(None, result),
|
|
144
|
+
Err(e) => JsonRpcResponse::error(
|
|
145
|
+
None,
|
|
146
|
+
super::types::INVALID_REQUEST,
|
|
147
|
+
format!("Resource error: {e}"),
|
|
148
|
+
),
|
|
149
|
+
}
|
|
150
|
+
}
|
package/src/mcp/tools.rs
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
use serde_json::{json, Value};
|
|
2
|
+
|
|
3
|
+
use super::types::ToolDefinition;
|
|
4
|
+
|
|
5
|
+
pub fn all_tool_definitions() -> Vec<ToolDefinition> {
|
|
6
|
+
vec![
|
|
7
|
+
ToolDefinition {
|
|
8
|
+
name: "handoff_init".to_string(),
|
|
9
|
+
description: "Initialize handoff tracking for a new project. Creates .handoff/ directory structure.".to_string(),
|
|
10
|
+
input_schema: json!({
|
|
11
|
+
"type": "object",
|
|
12
|
+
"properties": {
|
|
13
|
+
"project_dir": {
|
|
14
|
+
"type": "string",
|
|
15
|
+
"description": "Project directory path. Defaults to current working directory."
|
|
16
|
+
},
|
|
17
|
+
"project_name": {
|
|
18
|
+
"type": "string",
|
|
19
|
+
"description": "Project name"
|
|
20
|
+
},
|
|
21
|
+
"description": {
|
|
22
|
+
"type": "string",
|
|
23
|
+
"description": "Project description"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"required": ["project_name"]
|
|
27
|
+
}),
|
|
28
|
+
},
|
|
29
|
+
ToolDefinition {
|
|
30
|
+
name: "handoff_load_context".to_string(),
|
|
31
|
+
description: "Load handoff context for the current project. Call at session start to resume work.".to_string(),
|
|
32
|
+
input_schema: json!({
|
|
33
|
+
"type": "object",
|
|
34
|
+
"properties": {
|
|
35
|
+
"project_dir": {
|
|
36
|
+
"type": "string",
|
|
37
|
+
"description": "Project directory path. Defaults to current working directory."
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}),
|
|
41
|
+
},
|
|
42
|
+
ToolDefinition {
|
|
43
|
+
name: "handoff_save_context".to_string(),
|
|
44
|
+
description: "Save current session state for the next session. Call at session end.".to_string(),
|
|
45
|
+
input_schema: json!({
|
|
46
|
+
"type": "object",
|
|
47
|
+
"properties": {
|
|
48
|
+
"project_dir": {
|
|
49
|
+
"type": "string",
|
|
50
|
+
"description": "Project directory path. Defaults to current working directory."
|
|
51
|
+
},
|
|
52
|
+
"summary": {
|
|
53
|
+
"type": "string",
|
|
54
|
+
"description": "One-line summary of this session"
|
|
55
|
+
},
|
|
56
|
+
"decisions": {
|
|
57
|
+
"type": "array",
|
|
58
|
+
"description": "Decisions made during this session",
|
|
59
|
+
"items": {
|
|
60
|
+
"type": "object",
|
|
61
|
+
"properties": {
|
|
62
|
+
"decision": { "type": "string" },
|
|
63
|
+
"reason": { "type": "string" },
|
|
64
|
+
"confidence": {
|
|
65
|
+
"type": "string",
|
|
66
|
+
"enum": ["confirmed", "estimated", "unverified"]
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"required": ["decision"]
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
"blockers": {
|
|
73
|
+
"type": "array",
|
|
74
|
+
"items": { "type": "string" }
|
|
75
|
+
},
|
|
76
|
+
"checklist": {
|
|
77
|
+
"type": "array",
|
|
78
|
+
"items": {
|
|
79
|
+
"type": "object",
|
|
80
|
+
"properties": {
|
|
81
|
+
"item": { "type": "string" },
|
|
82
|
+
"checked": { "type": "boolean" },
|
|
83
|
+
"owner": {
|
|
84
|
+
"type": "string",
|
|
85
|
+
"enum": ["user", "ai"]
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
"required": ["item"]
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
"handoff_notes": {
|
|
92
|
+
"type": "array",
|
|
93
|
+
"items": {
|
|
94
|
+
"type": "object",
|
|
95
|
+
"properties": {
|
|
96
|
+
"note": { "type": "string" },
|
|
97
|
+
"category": {
|
|
98
|
+
"type": "string",
|
|
99
|
+
"enum": ["caution", "context", "suggestion"]
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
"required": ["note"]
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
"references": {
|
|
106
|
+
"type": "array",
|
|
107
|
+
"items": {
|
|
108
|
+
"type": "object",
|
|
109
|
+
"properties": {
|
|
110
|
+
"label": { "type": "string" },
|
|
111
|
+
"uri": { "type": "string" },
|
|
112
|
+
"type": {
|
|
113
|
+
"type": "string",
|
|
114
|
+
"enum": ["file", "issue", "mr", "wiki", "doc", "url"]
|
|
115
|
+
},
|
|
116
|
+
"notes": { "type": "string" }
|
|
117
|
+
},
|
|
118
|
+
"required": ["label", "uri"]
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
"context_pointers": {
|
|
122
|
+
"type": "array",
|
|
123
|
+
"items": {
|
|
124
|
+
"type": "object",
|
|
125
|
+
"properties": {
|
|
126
|
+
"path": { "type": "string" },
|
|
127
|
+
"reason": { "type": "string" },
|
|
128
|
+
"lines": { "type": "string" }
|
|
129
|
+
},
|
|
130
|
+
"required": ["path"]
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
"environment": {
|
|
134
|
+
"type": "object",
|
|
135
|
+
"description": "Free-form environment state"
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
"required": ["summary"]
|
|
139
|
+
}),
|
|
140
|
+
},
|
|
141
|
+
ToolDefinition {
|
|
142
|
+
name: "handoff_list_tasks".to_string(),
|
|
143
|
+
description: "List all tasks for the current project with optional status filter.".to_string(),
|
|
144
|
+
input_schema: json!({
|
|
145
|
+
"type": "object",
|
|
146
|
+
"properties": {
|
|
147
|
+
"project_dir": {
|
|
148
|
+
"type": "string",
|
|
149
|
+
"description": "Project directory path. Defaults to current working directory."
|
|
150
|
+
},
|
|
151
|
+
"status_filter": {
|
|
152
|
+
"type": "string",
|
|
153
|
+
"description": "Filter by status",
|
|
154
|
+
"enum": ["todo", "in_progress", "review", "done", "blocked", "skipped"]
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}),
|
|
158
|
+
},
|
|
159
|
+
ToolDefinition {
|
|
160
|
+
name: "handoff_update_task".to_string(),
|
|
161
|
+
description: "Add, update, or move a task. Manages the tasks/ directory structure.".to_string(),
|
|
162
|
+
input_schema: json!({
|
|
163
|
+
"type": "object",
|
|
164
|
+
"properties": {
|
|
165
|
+
"project_dir": {
|
|
166
|
+
"type": "string",
|
|
167
|
+
"description": "Project directory path. Defaults to current working directory."
|
|
168
|
+
},
|
|
169
|
+
"task": {
|
|
170
|
+
"type": "object",
|
|
171
|
+
"properties": {
|
|
172
|
+
"id": { "type": "string", "description": "Task ID. Omit for new task (auto-generated)." },
|
|
173
|
+
"title": { "type": "string" },
|
|
174
|
+
"status": {
|
|
175
|
+
"type": "string",
|
|
176
|
+
"enum": ["todo", "in_progress", "review", "done", "blocked", "skipped"]
|
|
177
|
+
},
|
|
178
|
+
"notes": { "type": "string" },
|
|
179
|
+
"priority": {
|
|
180
|
+
"type": "string",
|
|
181
|
+
"enum": ["low", "medium", "high"]
|
|
182
|
+
},
|
|
183
|
+
"labels": {
|
|
184
|
+
"type": "array",
|
|
185
|
+
"items": { "type": "string" }
|
|
186
|
+
},
|
|
187
|
+
"links": {
|
|
188
|
+
"type": "array",
|
|
189
|
+
"items": { "type": "string" }
|
|
190
|
+
},
|
|
191
|
+
"done_criteria": {
|
|
192
|
+
"type": "array",
|
|
193
|
+
"items": {
|
|
194
|
+
"type": "object",
|
|
195
|
+
"properties": {
|
|
196
|
+
"item": { "type": "string" },
|
|
197
|
+
"checked": { "type": "boolean" }
|
|
198
|
+
},
|
|
199
|
+
"required": ["item"]
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
"required": ["title"]
|
|
204
|
+
},
|
|
205
|
+
"parent_id": {
|
|
206
|
+
"type": "string",
|
|
207
|
+
"description": "Parent task ID for placement. Omit for auto-placement."
|
|
208
|
+
},
|
|
209
|
+
"move_to": {
|
|
210
|
+
"type": "string",
|
|
211
|
+
"description": "Move existing task subtree to a new parent."
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
"required": ["task"]
|
|
215
|
+
}),
|
|
216
|
+
},
|
|
217
|
+
ToolDefinition {
|
|
218
|
+
name: "handoff_get_config".to_string(),
|
|
219
|
+
description: "Read the project's handoff configuration.".to_string(),
|
|
220
|
+
input_schema: json!({
|
|
221
|
+
"type": "object",
|
|
222
|
+
"properties": {
|
|
223
|
+
"project_dir": {
|
|
224
|
+
"type": "string",
|
|
225
|
+
"description": "Project directory path. Defaults to current working directory."
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}),
|
|
229
|
+
},
|
|
230
|
+
ToolDefinition {
|
|
231
|
+
name: "handoff_update_config".to_string(),
|
|
232
|
+
description: "Update the project's handoff configuration.".to_string(),
|
|
233
|
+
input_schema: json!({
|
|
234
|
+
"type": "object",
|
|
235
|
+
"properties": {
|
|
236
|
+
"project_dir": {
|
|
237
|
+
"type": "string",
|
|
238
|
+
"description": "Project directory path. Defaults to current working directory."
|
|
239
|
+
},
|
|
240
|
+
"updates": {
|
|
241
|
+
"type": "object",
|
|
242
|
+
"description": "Key-value pairs to update (dot-notation keys like 'settings.history_limit')"
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
"required": ["updates"]
|
|
246
|
+
}),
|
|
247
|
+
},
|
|
248
|
+
ToolDefinition {
|
|
249
|
+
name: "handoff_dashboard".to_string(),
|
|
250
|
+
description: "Show handoff status across all projects in configured scan directories.".to_string(),
|
|
251
|
+
input_schema: json!({
|
|
252
|
+
"type": "object",
|
|
253
|
+
"properties": {
|
|
254
|
+
"scan_dirs": {
|
|
255
|
+
"type": "array",
|
|
256
|
+
"items": { "type": "string" },
|
|
257
|
+
"description": "Directories to scan. Defaults to config's dashboard.scan_dirs."
|
|
258
|
+
},
|
|
259
|
+
"include_completed": {
|
|
260
|
+
"type": "boolean",
|
|
261
|
+
"description": "Include completed tasks in summary"
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}),
|
|
265
|
+
},
|
|
266
|
+
]
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
pub fn all_resource_definitions() -> Vec<Value> {
|
|
270
|
+
vec![
|
|
271
|
+
json!({
|
|
272
|
+
"uri": "handoff://sessions",
|
|
273
|
+
"name": "Active Sessions",
|
|
274
|
+
"description": "All active session files for the current project",
|
|
275
|
+
"mimeType": "application/json"
|
|
276
|
+
}),
|
|
277
|
+
json!({
|
|
278
|
+
"uri": "handoff://config",
|
|
279
|
+
"name": "Project Configuration",
|
|
280
|
+
"description": "Current project's config.toml content",
|
|
281
|
+
"mimeType": "application/toml"
|
|
282
|
+
}),
|
|
283
|
+
]
|
|
284
|
+
}
|
package/src/mcp/types.rs
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
use serde::{Deserialize, Serialize};
|
|
2
|
+
use serde_json::Value;
|
|
3
|
+
|
|
4
|
+
#[derive(Debug, Deserialize)]
|
|
5
|
+
pub struct JsonRpcRequest {
|
|
6
|
+
pub jsonrpc: String,
|
|
7
|
+
pub id: Option<Value>,
|
|
8
|
+
pub method: String,
|
|
9
|
+
#[serde(default)]
|
|
10
|
+
pub params: Option<Value>,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
#[derive(Debug, Serialize)]
|
|
14
|
+
pub struct JsonRpcResponse {
|
|
15
|
+
pub jsonrpc: String,
|
|
16
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
17
|
+
pub id: Option<Value>,
|
|
18
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
19
|
+
pub result: Option<Value>,
|
|
20
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
21
|
+
pub error: Option<JsonRpcError>,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
#[derive(Debug, Serialize)]
|
|
25
|
+
pub struct JsonRpcError {
|
|
26
|
+
pub code: i32,
|
|
27
|
+
pub message: String,
|
|
28
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
29
|
+
pub data: Option<Value>,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
pub const PARSE_ERROR: i32 = -32700;
|
|
33
|
+
pub const INVALID_REQUEST: i32 = -32600;
|
|
34
|
+
pub const METHOD_NOT_FOUND: i32 = -32601;
|
|
35
|
+
pub const INTERNAL_ERROR: i32 = -32603;
|
|
36
|
+
|
|
37
|
+
impl JsonRpcResponse {
|
|
38
|
+
pub fn success(id: Option<Value>, result: Value) -> Self {
|
|
39
|
+
Self {
|
|
40
|
+
jsonrpc: "2.0".to_string(),
|
|
41
|
+
id,
|
|
42
|
+
result: Some(result),
|
|
43
|
+
error: None,
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
pub fn error(id: Option<Value>, code: i32, message: impl Into<String>) -> Self {
|
|
48
|
+
Self {
|
|
49
|
+
jsonrpc: "2.0".to_string(),
|
|
50
|
+
id,
|
|
51
|
+
result: None,
|
|
52
|
+
error: Some(JsonRpcError {
|
|
53
|
+
code,
|
|
54
|
+
message: message.into(),
|
|
55
|
+
data: None,
|
|
56
|
+
}),
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
#[derive(Debug, Serialize)]
|
|
62
|
+
pub struct ServerInfo {
|
|
63
|
+
pub name: String,
|
|
64
|
+
pub version: String,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
#[derive(Debug, Serialize)]
|
|
68
|
+
pub struct ServerCapabilities {
|
|
69
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
70
|
+
pub tools: Option<ToolsCapability>,
|
|
71
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
72
|
+
pub resources: Option<ResourcesCapability>,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
#[derive(Debug, Serialize)]
|
|
76
|
+
pub struct ToolsCapability {
|
|
77
|
+
#[serde(rename = "listChanged")]
|
|
78
|
+
pub list_changed: bool,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
#[derive(Debug, Serialize)]
|
|
82
|
+
pub struct ResourcesCapability {}
|
|
83
|
+
|
|
84
|
+
#[derive(Debug, Serialize)]
|
|
85
|
+
pub struct InitializeResult {
|
|
86
|
+
#[serde(rename = "protocolVersion")]
|
|
87
|
+
pub protocol_version: String,
|
|
88
|
+
pub capabilities: ServerCapabilities,
|
|
89
|
+
#[serde(rename = "serverInfo")]
|
|
90
|
+
pub server_info: ServerInfo,
|
|
91
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
92
|
+
pub instructions: Option<String>,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
#[derive(Debug, Serialize)]
|
|
96
|
+
pub struct ToolDefinition {
|
|
97
|
+
pub name: String,
|
|
98
|
+
pub description: String,
|
|
99
|
+
#[serde(rename = "inputSchema")]
|
|
100
|
+
pub input_schema: Value,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
#[derive(Debug, Serialize)]
|
|
104
|
+
pub struct ToolsListResult {
|
|
105
|
+
pub tools: Vec<ToolDefinition>,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
pub const PROTOCOL_VERSION: &str = "2025-03-26";
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
use std::collections::HashMap;
|
|
2
|
+
use std::path::Path;
|
|
3
|
+
|
|
4
|
+
use anyhow::{Context, Result};
|
|
5
|
+
use serde::{Deserialize, Serialize};
|
|
6
|
+
|
|
7
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
8
|
+
pub struct Config {
|
|
9
|
+
pub project: ProjectConfig,
|
|
10
|
+
#[serde(default)]
|
|
11
|
+
pub settings: SettingsConfig,
|
|
12
|
+
#[serde(default)]
|
|
13
|
+
pub dashboard: DashboardConfig,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
17
|
+
pub struct ProjectConfig {
|
|
18
|
+
pub name: String,
|
|
19
|
+
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
20
|
+
pub description: Option<String>,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
24
|
+
pub struct SettingsConfig {
|
|
25
|
+
#[serde(default = "default_history_limit")]
|
|
26
|
+
pub history_limit: u32,
|
|
27
|
+
#[serde(default = "default_done_task_limit")]
|
|
28
|
+
pub done_task_limit: u32,
|
|
29
|
+
#[serde(default = "default_auto_git_summary")]
|
|
30
|
+
pub auto_git_summary: bool,
|
|
31
|
+
#[serde(default)]
|
|
32
|
+
pub context_files: Vec<String>,
|
|
33
|
+
#[serde(default)]
|
|
34
|
+
pub custom_fields: HashMap<String, toml::Value>,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
38
|
+
pub struct DashboardConfig {
|
|
39
|
+
#[serde(default = "default_scan_dirs")]
|
|
40
|
+
pub scan_dirs: Vec<String>,
|
|
41
|
+
#[serde(default)]
|
|
42
|
+
pub exclude_patterns: Vec<String>,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
fn default_history_limit() -> u32 {
|
|
46
|
+
20
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
fn default_done_task_limit() -> u32 {
|
|
50
|
+
10
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
fn default_auto_git_summary() -> bool {
|
|
54
|
+
true
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
fn default_scan_dirs() -> Vec<String> {
|
|
58
|
+
vec!["~/pro/".to_string()]
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
impl Default for SettingsConfig {
|
|
62
|
+
fn default() -> Self {
|
|
63
|
+
Self {
|
|
64
|
+
history_limit: default_history_limit(),
|
|
65
|
+
done_task_limit: default_done_task_limit(),
|
|
66
|
+
auto_git_summary: default_auto_git_summary(),
|
|
67
|
+
context_files: Vec::new(),
|
|
68
|
+
custom_fields: HashMap::new(),
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
impl Default for DashboardConfig {
|
|
74
|
+
fn default() -> Self {
|
|
75
|
+
Self {
|
|
76
|
+
scan_dirs: default_scan_dirs(),
|
|
77
|
+
exclude_patterns: Vec::new(),
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
impl Config {
|
|
83
|
+
pub fn new(name: &str, description: &str) -> Self {
|
|
84
|
+
Self {
|
|
85
|
+
project: ProjectConfig {
|
|
86
|
+
name: name.to_string(),
|
|
87
|
+
description: if description.is_empty() {
|
|
88
|
+
None
|
|
89
|
+
} else {
|
|
90
|
+
Some(description.to_string())
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
settings: SettingsConfig::default(),
|
|
94
|
+
dashboard: DashboardConfig::default(),
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
pub fn read_config(path: &Path) -> Result<Config> {
|
|
100
|
+
let content = std::fs::read_to_string(path)
|
|
101
|
+
.with_context(|| format!("Failed to read config: {}", path.display()))?;
|
|
102
|
+
let config: Config = toml::from_str(&content)
|
|
103
|
+
.with_context(|| format!("Failed to parse config: {}", path.display()))?;
|
|
104
|
+
Ok(config)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
pub fn write_config(path: &Path, config: &Config) -> Result<()> {
|
|
108
|
+
let content = toml::to_string_pretty(config).context("Failed to serialize config")?;
|
|
109
|
+
std::fs::write(path, content)
|
|
110
|
+
.with_context(|| format!("Failed to write config: {}", path.display()))?;
|
|
111
|
+
Ok(())
|
|
112
|
+
}
|