session-collab-mcp 0.4.4 → 0.4.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.
- package/migrations/0003_config.sql +2 -0
- package/package.json +1 -1
- package/src/auth/handlers.ts +2 -2
- package/src/auth/middleware.ts +3 -3
- package/src/cli.ts +4 -35
- package/src/constants.ts +75 -0
- package/src/db/auth-queries.ts +17 -17
- package/src/db/queries.ts +32 -17
- package/src/db/sqlite-adapter.ts +15 -15
- package/src/db/types.ts +18 -0
- package/src/mcp/server.ts +8 -39
- package/src/mcp/tools/claim.ts +72 -6
- package/src/mcp/tools/decision.ts +2 -2
- package/src/mcp/tools/message.ts +2 -2
- package/src/mcp/tools/session.ts +95 -3
- package/src/tokens/handlers.ts +2 -2
package/package.json
CHANGED
package/src/auth/handlers.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Authentication API handlers
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type { DatabaseAdapter } from '../db/sqlite-adapter.js';
|
|
4
4
|
import { hashPassword, verifyPassword, validatePasswordStrength } from './password';
|
|
5
5
|
import { createAccessToken, createRefreshToken, verifyJwt, getTokenExpiry } from './jwt';
|
|
6
6
|
import {
|
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
} from './types';
|
|
30
30
|
|
|
31
31
|
interface HandlerContext {
|
|
32
|
-
db:
|
|
32
|
+
db: DatabaseAdapter;
|
|
33
33
|
jwtSecret: string;
|
|
34
34
|
request: Request;
|
|
35
35
|
}
|
package/src/auth/middleware.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
// Authentication middleware
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type { DatabaseAdapter } from '../db/sqlite-adapter.js';
|
|
4
4
|
import { verifyJwt } from './jwt';
|
|
5
5
|
import { getApiTokenByHash, updateApiTokenLastUsed } from '../db/auth-queries';
|
|
6
6
|
import { sha256, timingSafeEqual } from '../utils/crypto';
|
|
7
7
|
import type { AuthContext } from './types';
|
|
8
8
|
|
|
9
9
|
export interface Env {
|
|
10
|
-
DB:
|
|
10
|
+
DB: DatabaseAdapter;
|
|
11
11
|
JWT_SECRET?: string;
|
|
12
12
|
API_TOKEN?: string; // Legacy single token support
|
|
13
13
|
}
|
|
@@ -52,7 +52,7 @@ export async function validateAuth(request: Request, env: Env): Promise<AuthCont
|
|
|
52
52
|
/**
|
|
53
53
|
* Validate API Token
|
|
54
54
|
*/
|
|
55
|
-
async function validateApiToken(token: string, db:
|
|
55
|
+
async function validateApiToken(token: string, db: DatabaseAdapter): Promise<AuthContext | null> {
|
|
56
56
|
const tokenHash = await sha256(token);
|
|
57
57
|
const apiToken = await getApiTokenByHash(db, tokenHash);
|
|
58
58
|
|
package/src/cli.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { createInterface } from 'readline';
|
|
6
6
|
import { createLocalDatabase, getDefaultDbPath } from './db/sqlite-adapter.js';
|
|
7
7
|
import { handleMcpRequest, getMcpTools } from './mcp/server.js';
|
|
8
|
+
import { VERSION, SERVER_NAME, SERVER_INSTRUCTIONS } from './constants.js';
|
|
8
9
|
import { readFileSync } from 'fs';
|
|
9
10
|
import { join, dirname } from 'path';
|
|
10
11
|
import { fileURLToPath } from 'url';
|
|
@@ -17,6 +18,7 @@ function loadMigrations(): string[] {
|
|
|
17
18
|
return [
|
|
18
19
|
readFileSync(join(migrationsDir, '0001_init.sql'), 'utf-8'),
|
|
19
20
|
readFileSync(join(migrationsDir, '0002_auth.sql'), 'utf-8'),
|
|
21
|
+
readFileSync(join(migrationsDir, '0003_config.sql'), 'utf-8'),
|
|
20
22
|
];
|
|
21
23
|
}
|
|
22
24
|
|
|
@@ -40,39 +42,6 @@ function jsonRpcResponse(id: number | string | null, result: unknown): string {
|
|
|
40
42
|
return JSON.stringify({ jsonrpc: '2.0', id, result });
|
|
41
43
|
}
|
|
42
44
|
|
|
43
|
-
// Server instructions for Claude
|
|
44
|
-
const SERVER_INSTRUCTIONS = `
|
|
45
|
-
# Session Collaboration
|
|
46
|
-
|
|
47
|
-
This MCP server coordinates multiple Claude Code sessions working on the same codebase.
|
|
48
|
-
|
|
49
|
-
## Required Behaviors
|
|
50
|
-
|
|
51
|
-
1. **On conversation start**: Immediately call \`collab_session_start\` with:
|
|
52
|
-
- project_root: Current working directory
|
|
53
|
-
- name: Descriptive session name (e.g., "feature-auth", "bugfix-api")
|
|
54
|
-
|
|
55
|
-
2. **Before editing any file**: Call \`collab_check\` with the file path to verify no conflicts
|
|
56
|
-
|
|
57
|
-
3. **If conflicts detected**:
|
|
58
|
-
- Show warning to user
|
|
59
|
-
- Ask if they want to proceed or coordinate
|
|
60
|
-
- Consider using \`collab_message_send\` to notify other session
|
|
61
|
-
|
|
62
|
-
4. **For significant changes**: Call \`collab_claim\` before starting work on files
|
|
63
|
-
|
|
64
|
-
5. **When done with files**: Call \`collab_release\` to free them for others
|
|
65
|
-
|
|
66
|
-
6. **On conversation end**: Call \`collab_session_end\` to clean up
|
|
67
|
-
|
|
68
|
-
## Best Practices
|
|
69
|
-
|
|
70
|
-
- Claim files early, release when done
|
|
71
|
-
- Use descriptive intents when claiming (e.g., "Refactoring auth module")
|
|
72
|
-
- Check for messages periodically with \`collab_message_list\`
|
|
73
|
-
- Record architectural decisions with \`collab_decision_add\`
|
|
74
|
-
`.trim();
|
|
75
|
-
|
|
76
45
|
function jsonRpcError(id: number | string | null, code: number, message: string): string {
|
|
77
46
|
return JSON.stringify({ jsonrpc: '2.0', id, error: { code, message } });
|
|
78
47
|
}
|
|
@@ -118,8 +87,8 @@ async function main(): Promise<void> {
|
|
|
118
87
|
tools: {},
|
|
119
88
|
},
|
|
120
89
|
serverInfo: {
|
|
121
|
-
name:
|
|
122
|
-
version:
|
|
90
|
+
name: SERVER_NAME,
|
|
91
|
+
version: VERSION,
|
|
123
92
|
},
|
|
124
93
|
instructions: SERVER_INSTRUCTIONS,
|
|
125
94
|
});
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Shared constants for Session Collaboration MCP
|
|
2
|
+
|
|
3
|
+
import { readFileSync } from 'fs';
|
|
4
|
+
import { join, dirname } from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
|
|
7
|
+
// Read version from package.json
|
|
8
|
+
function getVersion(): string {
|
|
9
|
+
try {
|
|
10
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
|
|
12
|
+
return pkg.version;
|
|
13
|
+
} catch {
|
|
14
|
+
return '0.0.0';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const VERSION = getVersion();
|
|
19
|
+
|
|
20
|
+
export const SERVER_NAME = 'session-collab-mcp';
|
|
21
|
+
|
|
22
|
+
export const SERVER_INSTRUCTIONS = `
|
|
23
|
+
# Session Collaboration
|
|
24
|
+
|
|
25
|
+
This MCP server coordinates multiple Claude Code sessions working on the same codebase.
|
|
26
|
+
|
|
27
|
+
## Required Behaviors
|
|
28
|
+
|
|
29
|
+
1. **On conversation start**: Immediately call \`collab_session_start\` with:
|
|
30
|
+
- project_root: Current working directory
|
|
31
|
+
- name: Descriptive session name (e.g., "feature-auth", "bugfix-api")
|
|
32
|
+
|
|
33
|
+
2. **Before editing any file**: Call \`collab_check\` with the file path to verify no conflicts
|
|
34
|
+
|
|
35
|
+
3. **If conflicts detected** (DEFAULT: coordinate):
|
|
36
|
+
- Show warning to user with options:
|
|
37
|
+
a) **Coordinate** (default): Send message to other session via \`collab_message_send\`, wait for response
|
|
38
|
+
b) **Bypass**: Proceed anyway (warn about potential conflicts)
|
|
39
|
+
c) **Request release**: Ask owner to release if claim seems stale
|
|
40
|
+
- NEVER auto-release another session's claim without explicit user permission
|
|
41
|
+
|
|
42
|
+
4. **For significant changes**: Call \`collab_claim\` before starting work on files
|
|
43
|
+
|
|
44
|
+
5. **When done with files**: Call \`collab_release\` with YOUR session_id to free them
|
|
45
|
+
|
|
46
|
+
6. **On conversation end**: Call \`collab_session_end\` to clean up
|
|
47
|
+
|
|
48
|
+
## Permission Rules
|
|
49
|
+
|
|
50
|
+
- You can ONLY release claims that belong to YOUR session
|
|
51
|
+
- To release another session's claim, you must ask the user and they must explicitly confirm
|
|
52
|
+
- Use \`force=true\` in \`collab_release\` only after user explicitly confirms
|
|
53
|
+
- When user chooses "coordinate", send a message first and suggest waiting
|
|
54
|
+
|
|
55
|
+
## Conflict Handling Modes
|
|
56
|
+
|
|
57
|
+
Configure your session behavior with \`collab_config\`:
|
|
58
|
+
|
|
59
|
+
- **"strict"**: Always ask user, never bypass or auto-release
|
|
60
|
+
- **"smart"** (default): Ask user, but suggest auto-release for stale claims (>2hr old)
|
|
61
|
+
- **"bypass"**: Proceed despite conflicts (just warn, don't block)
|
|
62
|
+
|
|
63
|
+
Config options:
|
|
64
|
+
- \`mode\`: strict | smart | bypass
|
|
65
|
+
- \`allow_release_others\`: Allow releasing other sessions' claims (default: false)
|
|
66
|
+
- \`auto_release_stale\`: Auto-release stale claims (default: false)
|
|
67
|
+
- \`stale_threshold_hours\`: Hours before claim is stale (default: 2)
|
|
68
|
+
|
|
69
|
+
## Best Practices
|
|
70
|
+
|
|
71
|
+
- Claim files early, release when done
|
|
72
|
+
- Use descriptive intents when claiming (e.g., "Refactoring auth module")
|
|
73
|
+
- Check for messages periodically with \`collab_message_list\`
|
|
74
|
+
- Record architectural decisions with \`collab_decision_add\`
|
|
75
|
+
`.trim();
|
package/src/db/auth-queries.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
// Database queries for authentication
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type { DatabaseAdapter } from './sqlite-adapter.js';
|
|
4
4
|
import type { User, UserPublic, ApiToken, ApiTokenPublic, RefreshToken } from './types';
|
|
5
5
|
import { generateId, sha256 } from '../utils/crypto';
|
|
6
6
|
|
|
7
7
|
// ============ User Queries ============
|
|
8
8
|
|
|
9
9
|
export async function createUser(
|
|
10
|
-
db:
|
|
10
|
+
db: DatabaseAdapter,
|
|
11
11
|
params: {
|
|
12
12
|
email: string;
|
|
13
13
|
password_hash: string;
|
|
@@ -37,29 +37,29 @@ export async function createUser(
|
|
|
37
37
|
};
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
export async function getUserByEmail(db:
|
|
40
|
+
export async function getUserByEmail(db: DatabaseAdapter, email: string): Promise<User | null> {
|
|
41
41
|
const result = await db.prepare('SELECT * FROM users WHERE email = ? AND status = ?').bind(email.toLowerCase(), 'active').first<User>();
|
|
42
42
|
return result ?? null;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
export async function getUserById(db:
|
|
45
|
+
export async function getUserById(db: DatabaseAdapter, id: string): Promise<User | null> {
|
|
46
46
|
const result = await db.prepare('SELECT * FROM users WHERE id = ? AND status = ?').bind(id, 'active').first<User>();
|
|
47
47
|
return result ?? null;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
export async function updateUserLastLogin(db:
|
|
50
|
+
export async function updateUserLastLogin(db: DatabaseAdapter, id: string): Promise<void> {
|
|
51
51
|
const now = new Date().toISOString();
|
|
52
52
|
await db.prepare('UPDATE users SET last_login_at = ?, updated_at = ? WHERE id = ?').bind(now, now, id).run();
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
export async function updateUserPassword(db:
|
|
55
|
+
export async function updateUserPassword(db: DatabaseAdapter, id: string, password_hash: string): Promise<boolean> {
|
|
56
56
|
const now = new Date().toISOString();
|
|
57
57
|
const result = await db.prepare('UPDATE users SET password_hash = ?, updated_at = ? WHERE id = ?').bind(password_hash, now, id).run();
|
|
58
58
|
return result.meta.changes > 0;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
export async function updateUserProfile(
|
|
62
|
-
db:
|
|
62
|
+
db: DatabaseAdapter,
|
|
63
63
|
id: string,
|
|
64
64
|
params: { display_name?: string }
|
|
65
65
|
): Promise<boolean> {
|
|
@@ -80,7 +80,7 @@ export function toUserPublic(user: User): UserPublic {
|
|
|
80
80
|
// ============ API Token Queries ============
|
|
81
81
|
|
|
82
82
|
export async function createApiToken(
|
|
83
|
-
db:
|
|
83
|
+
db: DatabaseAdapter,
|
|
84
84
|
params: {
|
|
85
85
|
user_id: string;
|
|
86
86
|
name: string;
|
|
@@ -121,7 +121,7 @@ export async function createApiToken(
|
|
|
121
121
|
};
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
export async function getApiTokenByHash(db:
|
|
124
|
+
export async function getApiTokenByHash(db: DatabaseAdapter, tokenHash: string): Promise<ApiToken | null> {
|
|
125
125
|
const result = await db
|
|
126
126
|
.prepare('SELECT * FROM api_tokens WHERE token_hash = ? AND revoked_at IS NULL')
|
|
127
127
|
.bind(tokenHash)
|
|
@@ -137,7 +137,7 @@ export async function getApiTokenByHash(db: D1Database, tokenHash: string): Prom
|
|
|
137
137
|
return result;
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
-
export async function listApiTokens(db:
|
|
140
|
+
export async function listApiTokens(db: DatabaseAdapter, userId: string): Promise<ApiTokenPublic[]> {
|
|
141
141
|
const result = await db
|
|
142
142
|
.prepare('SELECT * FROM api_tokens WHERE user_id = ? AND revoked_at IS NULL ORDER BY created_at DESC')
|
|
143
143
|
.bind(userId)
|
|
@@ -146,13 +146,13 @@ export async function listApiTokens(db: D1Database, userId: string): Promise<Api
|
|
|
146
146
|
return result.results.map(toApiTokenPublic);
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
-
export async function revokeApiToken(db:
|
|
149
|
+
export async function revokeApiToken(db: DatabaseAdapter, id: string, userId: string): Promise<boolean> {
|
|
150
150
|
const now = new Date().toISOString();
|
|
151
151
|
const result = await db.prepare('UPDATE api_tokens SET revoked_at = ? WHERE id = ? AND user_id = ?').bind(now, id, userId).run();
|
|
152
152
|
return result.meta.changes > 0;
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
-
export async function updateApiTokenLastUsed(db:
|
|
155
|
+
export async function updateApiTokenLastUsed(db: DatabaseAdapter, id: string): Promise<void> {
|
|
156
156
|
const now = new Date().toISOString();
|
|
157
157
|
await db.prepare('UPDATE api_tokens SET last_used_at = ? WHERE id = ?').bind(now, id).run();
|
|
158
158
|
}
|
|
@@ -172,7 +172,7 @@ export function toApiTokenPublic(token: ApiToken): ApiTokenPublic {
|
|
|
172
172
|
// ============ Refresh Token Queries ============
|
|
173
173
|
|
|
174
174
|
export async function createRefreshToken(
|
|
175
|
-
db:
|
|
175
|
+
db: DatabaseAdapter,
|
|
176
176
|
params: {
|
|
177
177
|
user_id: string;
|
|
178
178
|
token_hash: string;
|
|
@@ -204,7 +204,7 @@ export async function createRefreshToken(
|
|
|
204
204
|
};
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
-
export async function getRefreshTokenByHash(db:
|
|
207
|
+
export async function getRefreshTokenByHash(db: DatabaseAdapter, tokenHash: string): Promise<RefreshToken | null> {
|
|
208
208
|
const result = await db
|
|
209
209
|
.prepare('SELECT * FROM refresh_tokens WHERE token_hash = ? AND revoked_at IS NULL')
|
|
210
210
|
.bind(tokenHash)
|
|
@@ -220,13 +220,13 @@ export async function getRefreshTokenByHash(db: D1Database, tokenHash: string):
|
|
|
220
220
|
return result;
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
-
export async function revokeRefreshToken(db:
|
|
223
|
+
export async function revokeRefreshToken(db: DatabaseAdapter, id: string): Promise<boolean> {
|
|
224
224
|
const now = new Date().toISOString();
|
|
225
225
|
const result = await db.prepare('UPDATE refresh_tokens SET revoked_at = ? WHERE id = ?').bind(now, id).run();
|
|
226
226
|
return result.meta.changes > 0;
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
-
export async function revokeAllUserRefreshTokens(db:
|
|
229
|
+
export async function revokeAllUserRefreshTokens(db: DatabaseAdapter, userId: string): Promise<number> {
|
|
230
230
|
const now = new Date().toISOString();
|
|
231
231
|
const result = await db.prepare('UPDATE refresh_tokens SET revoked_at = ? WHERE user_id = ? AND revoked_at IS NULL').bind(now, userId).run();
|
|
232
232
|
return result.meta.changes;
|
|
@@ -234,7 +234,7 @@ export async function revokeAllUserRefreshTokens(db: D1Database, userId: string)
|
|
|
234
234
|
|
|
235
235
|
// ============ Cleanup Queries ============
|
|
236
236
|
|
|
237
|
-
export async function cleanupExpiredTokens(db:
|
|
237
|
+
export async function cleanupExpiredTokens(db: DatabaseAdapter): Promise<{ apiTokens: number; refreshTokens: number }> {
|
|
238
238
|
const now = new Date().toISOString();
|
|
239
239
|
|
|
240
240
|
// Mark expired API tokens as revoked
|
package/src/db/queries.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Database queries for Session Collaboration MCP
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type { DatabaseAdapter } from './sqlite-adapter.js';
|
|
4
4
|
import type {
|
|
5
5
|
Session,
|
|
6
6
|
Claim,
|
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
DecisionCategory,
|
|
14
14
|
TodoItem,
|
|
15
15
|
SessionProgress,
|
|
16
|
+
SessionConfig,
|
|
16
17
|
} from './types';
|
|
17
18
|
|
|
18
19
|
// Helper to generate UUID v4
|
|
@@ -23,7 +24,7 @@ function generateId(): string {
|
|
|
23
24
|
// ============ Session Queries ============
|
|
24
25
|
|
|
25
26
|
export async function createSession(
|
|
26
|
-
db:
|
|
27
|
+
db: DatabaseAdapter,
|
|
27
28
|
params: {
|
|
28
29
|
name?: string;
|
|
29
30
|
project_root: string;
|
|
@@ -54,16 +55,17 @@ export async function createSession(
|
|
|
54
55
|
current_task: null,
|
|
55
56
|
progress: null,
|
|
56
57
|
todos: null,
|
|
58
|
+
config: null,
|
|
57
59
|
};
|
|
58
60
|
}
|
|
59
61
|
|
|
60
|
-
export async function getSession(db:
|
|
62
|
+
export async function getSession(db: DatabaseAdapter, id: string): Promise<Session | null> {
|
|
61
63
|
const result = await db.prepare('SELECT * FROM sessions WHERE id = ?').bind(id).first<Session>();
|
|
62
64
|
return result ?? null;
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
export async function listSessions(
|
|
66
|
-
db:
|
|
68
|
+
db: DatabaseAdapter,
|
|
67
69
|
params: {
|
|
68
70
|
include_inactive?: boolean;
|
|
69
71
|
project_root?: string;
|
|
@@ -97,7 +99,7 @@ export async function listSessions(
|
|
|
97
99
|
}
|
|
98
100
|
|
|
99
101
|
export async function updateSessionHeartbeat(
|
|
100
|
-
db:
|
|
102
|
+
db: DatabaseAdapter,
|
|
101
103
|
id: string,
|
|
102
104
|
statusUpdate?: {
|
|
103
105
|
current_task?: string | null;
|
|
@@ -150,7 +152,7 @@ export async function updateSessionHeartbeat(
|
|
|
150
152
|
}
|
|
151
153
|
|
|
152
154
|
export async function updateSessionStatus(
|
|
153
|
-
db:
|
|
155
|
+
db: DatabaseAdapter,
|
|
154
156
|
id: string,
|
|
155
157
|
params: {
|
|
156
158
|
current_task?: string | null;
|
|
@@ -193,7 +195,7 @@ export async function updateSessionStatus(
|
|
|
193
195
|
}
|
|
194
196
|
|
|
195
197
|
export async function endSession(
|
|
196
|
-
db:
|
|
198
|
+
db: DatabaseAdapter,
|
|
197
199
|
id: string,
|
|
198
200
|
release_claims: 'complete' | 'abandon' = 'abandon'
|
|
199
201
|
): Promise<boolean> {
|
|
@@ -211,7 +213,7 @@ export async function endSession(
|
|
|
211
213
|
return result.meta.changes > 0;
|
|
212
214
|
}
|
|
213
215
|
|
|
214
|
-
export async function cleanupStaleSessions(db:
|
|
216
|
+
export async function cleanupStaleSessions(db: DatabaseAdapter, staleMinutes: number = 30): Promise<{ stale_sessions: number; orphaned_claims: number }> {
|
|
215
217
|
const cutoff = new Date(Date.now() - staleMinutes * 60 * 1000).toISOString();
|
|
216
218
|
const now = new Date().toISOString();
|
|
217
219
|
|
|
@@ -238,10 +240,23 @@ export async function cleanupStaleSessions(db: D1Database, staleMinutes: number
|
|
|
238
240
|
};
|
|
239
241
|
}
|
|
240
242
|
|
|
243
|
+
export async function updateSessionConfig(
|
|
244
|
+
db: DatabaseAdapter,
|
|
245
|
+
id: string,
|
|
246
|
+
config: SessionConfig
|
|
247
|
+
): Promise<boolean> {
|
|
248
|
+
const result = await db
|
|
249
|
+
.prepare('UPDATE sessions SET config = ? WHERE id = ?')
|
|
250
|
+
.bind(JSON.stringify(config), id)
|
|
251
|
+
.run();
|
|
252
|
+
|
|
253
|
+
return result.meta.changes > 0;
|
|
254
|
+
}
|
|
255
|
+
|
|
241
256
|
// ============ Claim Queries ============
|
|
242
257
|
|
|
243
258
|
export async function createClaim(
|
|
244
|
-
db:
|
|
259
|
+
db: DatabaseAdapter,
|
|
245
260
|
params: {
|
|
246
261
|
session_id: string;
|
|
247
262
|
files: string[];
|
|
@@ -285,7 +300,7 @@ export async function createClaim(
|
|
|
285
300
|
};
|
|
286
301
|
}
|
|
287
302
|
|
|
288
|
-
export async function getClaim(db:
|
|
303
|
+
export async function getClaim(db: DatabaseAdapter, id: string): Promise<ClaimWithFiles | null> {
|
|
289
304
|
const claim = await db.prepare('SELECT * FROM claims WHERE id = ?').bind(id).first<Claim>();
|
|
290
305
|
|
|
291
306
|
if (!claim) return null;
|
|
@@ -302,7 +317,7 @@ export async function getClaim(db: D1Database, id: string): Promise<ClaimWithFil
|
|
|
302
317
|
}
|
|
303
318
|
|
|
304
319
|
export async function listClaims(
|
|
305
|
-
db:
|
|
320
|
+
db: DatabaseAdapter,
|
|
306
321
|
params: {
|
|
307
322
|
session_id?: string;
|
|
308
323
|
status?: ClaimStatus | 'all';
|
|
@@ -367,7 +382,7 @@ export async function listClaims(
|
|
|
367
382
|
}
|
|
368
383
|
|
|
369
384
|
export async function checkConflicts(
|
|
370
|
-
db:
|
|
385
|
+
db: DatabaseAdapter,
|
|
371
386
|
files: string[],
|
|
372
387
|
excludeSessionId?: string
|
|
373
388
|
): Promise<ConflictInfo[]> {
|
|
@@ -417,7 +432,7 @@ export async function checkConflicts(
|
|
|
417
432
|
}
|
|
418
433
|
|
|
419
434
|
export async function releaseClaim(
|
|
420
|
-
db:
|
|
435
|
+
db: DatabaseAdapter,
|
|
421
436
|
id: string,
|
|
422
437
|
params: {
|
|
423
438
|
status: 'completed' | 'abandoned';
|
|
@@ -437,7 +452,7 @@ export async function releaseClaim(
|
|
|
437
452
|
// ============ Message Queries ============
|
|
438
453
|
|
|
439
454
|
export async function sendMessage(
|
|
440
|
-
db:
|
|
455
|
+
db: DatabaseAdapter,
|
|
441
456
|
params: {
|
|
442
457
|
from_session_id: string;
|
|
443
458
|
to_session_id?: string;
|
|
@@ -466,7 +481,7 @@ export async function sendMessage(
|
|
|
466
481
|
}
|
|
467
482
|
|
|
468
483
|
export async function listMessages(
|
|
469
|
-
db:
|
|
484
|
+
db: DatabaseAdapter,
|
|
470
485
|
params: {
|
|
471
486
|
session_id: string;
|
|
472
487
|
unread_only?: boolean;
|
|
@@ -506,7 +521,7 @@ export async function listMessages(
|
|
|
506
521
|
// ============ Decision Queries ============
|
|
507
522
|
|
|
508
523
|
export async function addDecision(
|
|
509
|
-
db:
|
|
524
|
+
db: DatabaseAdapter,
|
|
510
525
|
params: {
|
|
511
526
|
session_id: string;
|
|
512
527
|
category?: DecisionCategory;
|
|
@@ -536,7 +551,7 @@ export async function addDecision(
|
|
|
536
551
|
}
|
|
537
552
|
|
|
538
553
|
export async function listDecisions(
|
|
539
|
-
db:
|
|
554
|
+
db: DatabaseAdapter,
|
|
540
555
|
params: {
|
|
541
556
|
category?: DecisionCategory;
|
|
542
557
|
limit?: number;
|
package/src/db/sqlite-adapter.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// SQLite adapter: wraps better-sqlite3
|
|
2
|
-
// This allows the same queries.ts to work with both D1 and local SQLite
|
|
1
|
+
// SQLite adapter: wraps better-sqlite3 with a generic database interface
|
|
2
|
+
// This allows the same queries.ts to work with both Cloudflare D1 and local SQLite
|
|
3
3
|
|
|
4
4
|
import Database from 'better-sqlite3';
|
|
5
5
|
import { randomUUID } from 'crypto';
|
|
@@ -14,7 +14,7 @@ if (typeof globalThis.crypto === 'undefined') {
|
|
|
14
14
|
};
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
export interface
|
|
17
|
+
export interface QueryResult<T> {
|
|
18
18
|
results: T[];
|
|
19
19
|
meta: {
|
|
20
20
|
changes: number;
|
|
@@ -22,19 +22,19 @@ export interface D1Result<T> {
|
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
export interface
|
|
26
|
-
bind(...values: unknown[]):
|
|
25
|
+
export interface PreparedStatement {
|
|
26
|
+
bind(...values: unknown[]): PreparedStatement;
|
|
27
27
|
first<T>(): Promise<T | null>;
|
|
28
|
-
all<T>(): Promise<
|
|
28
|
+
all<T>(): Promise<QueryResult<T>>;
|
|
29
29
|
run(): Promise<{ meta: { changes: number } }>;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
export interface
|
|
33
|
-
prepare(sql: string):
|
|
34
|
-
batch(statements:
|
|
32
|
+
export interface DatabaseAdapter {
|
|
33
|
+
prepare(sql: string): PreparedStatement;
|
|
34
|
+
batch(statements: PreparedStatement[]): Promise<QueryResult<unknown>[]>;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
class SqlitePreparedStatement implements
|
|
37
|
+
class SqlitePreparedStatement implements PreparedStatement {
|
|
38
38
|
private bindings: unknown[] = [];
|
|
39
39
|
|
|
40
40
|
constructor(
|
|
@@ -42,7 +42,7 @@ class SqlitePreparedStatement implements D1PreparedStatement {
|
|
|
42
42
|
private sql: string
|
|
43
43
|
) {}
|
|
44
44
|
|
|
45
|
-
bind(...values: unknown[]):
|
|
45
|
+
bind(...values: unknown[]): PreparedStatement {
|
|
46
46
|
this.bindings = values;
|
|
47
47
|
return this;
|
|
48
48
|
}
|
|
@@ -53,7 +53,7 @@ class SqlitePreparedStatement implements D1PreparedStatement {
|
|
|
53
53
|
return result ?? null;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
async all<T>(): Promise<
|
|
56
|
+
async all<T>(): Promise<QueryResult<T>> {
|
|
57
57
|
const stmt = this.db.prepare(this.sql);
|
|
58
58
|
const results = stmt.all(...this.bindings) as T[];
|
|
59
59
|
return {
|
|
@@ -77,7 +77,7 @@ class SqlitePreparedStatement implements D1PreparedStatement {
|
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
class SqliteDatabase implements
|
|
80
|
+
class SqliteDatabase implements DatabaseAdapter {
|
|
81
81
|
private db: Database.Database;
|
|
82
82
|
|
|
83
83
|
constructor(dbPath: string) {
|
|
@@ -92,11 +92,11 @@ class SqliteDatabase implements D1Database {
|
|
|
92
92
|
this.db.pragma('foreign_keys = ON');
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
prepare(sql: string):
|
|
95
|
+
prepare(sql: string): PreparedStatement {
|
|
96
96
|
return new SqlitePreparedStatement(this.db, sql);
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
async batch(statements:
|
|
99
|
+
async batch(statements: PreparedStatement[]): Promise<QueryResult<unknown>[]> {
|
|
100
100
|
const transaction = this.db.transaction(() => {
|
|
101
101
|
return statements.map((stmt) => {
|
|
102
102
|
const sqliteStmt = stmt as SqlitePreparedStatement;
|
package/src/db/types.ts
CHANGED
|
@@ -64,6 +64,23 @@ export interface TodoItem {
|
|
|
64
64
|
status: 'pending' | 'in_progress' | 'completed';
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
// Session configuration
|
|
68
|
+
export type ConflictMode = 'strict' | 'smart' | 'bypass';
|
|
69
|
+
|
|
70
|
+
export interface SessionConfig {
|
|
71
|
+
mode: ConflictMode;
|
|
72
|
+
allow_release_others: boolean;
|
|
73
|
+
auto_release_stale: boolean;
|
|
74
|
+
stale_threshold_hours: number;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export const DEFAULT_SESSION_CONFIG: SessionConfig = {
|
|
78
|
+
mode: 'smart',
|
|
79
|
+
allow_release_others: false,
|
|
80
|
+
auto_release_stale: false,
|
|
81
|
+
stale_threshold_hours: 2,
|
|
82
|
+
};
|
|
83
|
+
|
|
67
84
|
export interface SessionProgress {
|
|
68
85
|
completed: number;
|
|
69
86
|
total: number;
|
|
@@ -82,6 +99,7 @@ export interface Session {
|
|
|
82
99
|
current_task: string | null;
|
|
83
100
|
progress: string | null; // JSON string of SessionProgress
|
|
84
101
|
todos: string | null; // JSON string of TodoItem[]
|
|
102
|
+
config: string | null; // JSON string of SessionConfig
|
|
85
103
|
}
|
|
86
104
|
|
|
87
105
|
export interface Claim {
|
package/src/mcp/server.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// MCP Server implementation for Session Collaboration
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type { DatabaseAdapter } from '../db/sqlite-adapter.js';
|
|
4
4
|
import {
|
|
5
5
|
JsonRpcRequestSchema,
|
|
6
6
|
type JsonRpcRequest,
|
|
@@ -19,55 +19,24 @@ import { claimTools, handleClaimTool } from './tools/claim';
|
|
|
19
19
|
import { messageTools, handleMessageTool } from './tools/message';
|
|
20
20
|
import { decisionTools, handleDecisionTool } from './tools/decision';
|
|
21
21
|
import type { AuthContext } from '../auth/types';
|
|
22
|
+
import { VERSION, SERVER_NAME, SERVER_INSTRUCTIONS } from '../constants.js';
|
|
22
23
|
|
|
23
24
|
const SERVER_INFO: McpServerInfo = {
|
|
24
|
-
name:
|
|
25
|
-
version:
|
|
25
|
+
name: SERVER_NAME,
|
|
26
|
+
version: VERSION,
|
|
26
27
|
};
|
|
27
28
|
|
|
28
29
|
const CAPABILITIES: McpCapabilities = {
|
|
29
30
|
tools: {},
|
|
30
31
|
};
|
|
31
32
|
|
|
32
|
-
const SERVER_INSTRUCTIONS = `
|
|
33
|
-
# Session Collaboration
|
|
34
|
-
|
|
35
|
-
This MCP server coordinates multiple Claude Code sessions working on the same codebase.
|
|
36
|
-
|
|
37
|
-
## Required Behaviors
|
|
38
|
-
|
|
39
|
-
1. **On conversation start**: Immediately call \`collab_session_start\` with:
|
|
40
|
-
- project_root: Current working directory
|
|
41
|
-
- name: Descriptive session name (e.g., "feature-auth", "bugfix-api")
|
|
42
|
-
|
|
43
|
-
2. **Before editing any file**: Call \`collab_check\` with the file path to verify no conflicts
|
|
44
|
-
|
|
45
|
-
3. **If conflicts detected**:
|
|
46
|
-
- Show warning to user
|
|
47
|
-
- Ask if they want to proceed or coordinate
|
|
48
|
-
- Consider using \`collab_message_send\` to notify other session
|
|
49
|
-
|
|
50
|
-
4. **For significant changes**: Call \`collab_claim\` before starting work on files
|
|
51
|
-
|
|
52
|
-
5. **When done with files**: Call \`collab_release\` to free them for others
|
|
53
|
-
|
|
54
|
-
6. **On conversation end**: Call \`collab_session_end\` to clean up
|
|
55
|
-
|
|
56
|
-
## Best Practices
|
|
57
|
-
|
|
58
|
-
- Claim files early, release when done
|
|
59
|
-
- Use descriptive intents when claiming (e.g., "Refactoring auth module")
|
|
60
|
-
- Check for messages periodically with \`collab_message_list\`
|
|
61
|
-
- Record architectural decisions with \`collab_decision_add\`
|
|
62
|
-
`.trim();
|
|
63
|
-
|
|
64
33
|
// Combine all tools
|
|
65
34
|
const ALL_TOOLS: McpTool[] = [...sessionTools, ...claimTools, ...messageTools, ...decisionTools];
|
|
66
35
|
|
|
67
36
|
export class McpServer {
|
|
68
37
|
private authContext?: AuthContext;
|
|
69
38
|
|
|
70
|
-
constructor(private db:
|
|
39
|
+
constructor(private db: DatabaseAdapter, authContext?: AuthContext) {
|
|
71
40
|
this.authContext = authContext;
|
|
72
41
|
}
|
|
73
42
|
|
|
@@ -127,7 +96,7 @@ export class McpServer {
|
|
|
127
96
|
|
|
128
97
|
try {
|
|
129
98
|
// Route to appropriate handler
|
|
130
|
-
if (name.startsWith('collab_session_') || name === 'collab_status_update') {
|
|
99
|
+
if (name.startsWith('collab_session_') || name === 'collab_status_update' || name === 'collab_config') {
|
|
131
100
|
result = await handleSessionTool(this.db, name, args, userId);
|
|
132
101
|
} else if (name.startsWith('collab_claim') || name === 'collab_check' || name === 'collab_release') {
|
|
133
102
|
result = await handleClaimTool(this.db, name, args);
|
|
@@ -165,12 +134,12 @@ export function getMcpTools(): McpTool[] {
|
|
|
165
134
|
|
|
166
135
|
// Handle MCP tool call for CLI
|
|
167
136
|
export async function handleMcpRequest(
|
|
168
|
-
db:
|
|
137
|
+
db: DatabaseAdapter,
|
|
169
138
|
name: string,
|
|
170
139
|
args: Record<string, unknown>
|
|
171
140
|
): Promise<McpToolResult> {
|
|
172
141
|
try {
|
|
173
|
-
if (name.startsWith('collab_session_') || name === 'collab_status_update') {
|
|
142
|
+
if (name.startsWith('collab_session_') || name === 'collab_status_update' || name === 'collab_config') {
|
|
174
143
|
return await handleSessionTool(db, name, args);
|
|
175
144
|
} else if (name.startsWith('collab_claim') || name === 'collab_check' || name === 'collab_release') {
|
|
176
145
|
return await handleClaimTool(db, name, args);
|
package/src/mcp/tools/claim.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// Claim management tools (WIP declarations)
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type { DatabaseAdapter } from '../../db/sqlite-adapter.js';
|
|
4
4
|
import type { McpTool, McpToolResult } from '../protocol';
|
|
5
5
|
import { createToolResult } from '../protocol';
|
|
6
|
-
import type { ClaimScope } from '../../db/types';
|
|
6
|
+
import type { ClaimScope, SessionConfig } from '../../db/types';
|
|
7
|
+
import { DEFAULT_SESSION_CONFIG } from '../../db/types';
|
|
7
8
|
import { createClaim, getClaim, listClaims, checkConflicts, releaseClaim, getSession } from '../../db/queries';
|
|
8
9
|
|
|
9
10
|
export const claimTools: McpTool[] = [
|
|
@@ -58,10 +59,14 @@ export const claimTools: McpTool[] = [
|
|
|
58
59
|
},
|
|
59
60
|
{
|
|
60
61
|
name: 'collab_release',
|
|
61
|
-
description: 'Release a claim when done or abandoning work.',
|
|
62
|
+
description: 'Release a claim when done or abandoning work. By default you can only release your own claims. Use force=true with user confirmation to release stale claims from other sessions.',
|
|
62
63
|
inputSchema: {
|
|
63
64
|
type: 'object',
|
|
64
65
|
properties: {
|
|
66
|
+
session_id: {
|
|
67
|
+
type: 'string',
|
|
68
|
+
description: 'Your session ID (required to verify ownership)',
|
|
69
|
+
},
|
|
65
70
|
claim_id: {
|
|
66
71
|
type: 'string',
|
|
67
72
|
description: 'Claim ID to release',
|
|
@@ -75,8 +80,12 @@ export const claimTools: McpTool[] = [
|
|
|
75
80
|
type: 'string',
|
|
76
81
|
description: 'Optional summary of what was done (for completed claims)',
|
|
77
82
|
},
|
|
83
|
+
force: {
|
|
84
|
+
type: 'boolean',
|
|
85
|
+
description: 'Force release even if claim belongs to another session (requires user confirmation)',
|
|
86
|
+
},
|
|
78
87
|
},
|
|
79
|
-
required: ['claim_id', 'status'],
|
|
88
|
+
required: ['session_id', 'claim_id', 'status'],
|
|
80
89
|
},
|
|
81
90
|
},
|
|
82
91
|
{
|
|
@@ -104,7 +113,7 @@ export const claimTools: McpTool[] = [
|
|
|
104
113
|
];
|
|
105
114
|
|
|
106
115
|
export async function handleClaimTool(
|
|
107
|
-
db:
|
|
116
|
+
db: DatabaseAdapter,
|
|
108
117
|
name: string,
|
|
109
118
|
args: Record<string, unknown>
|
|
110
119
|
): Promise<McpToolResult> {
|
|
@@ -286,9 +295,22 @@ export async function handleClaimTool(
|
|
|
286
295
|
}
|
|
287
296
|
|
|
288
297
|
case 'collab_release': {
|
|
298
|
+
const sessionId = args.session_id as string;
|
|
289
299
|
const claimId = args.claim_id as string;
|
|
290
300
|
const status = args.status as 'completed' | 'abandoned';
|
|
291
301
|
const summary = args.summary as string | undefined;
|
|
302
|
+
const force = args.force as boolean | undefined;
|
|
303
|
+
|
|
304
|
+
// Validate session_id
|
|
305
|
+
if (!sessionId || typeof sessionId !== 'string') {
|
|
306
|
+
return createToolResult(
|
|
307
|
+
JSON.stringify({
|
|
308
|
+
error: 'INVALID_INPUT',
|
|
309
|
+
message: 'session_id is required to verify ownership',
|
|
310
|
+
}),
|
|
311
|
+
true
|
|
312
|
+
);
|
|
313
|
+
}
|
|
292
314
|
|
|
293
315
|
const claim = await getClaim(db, claimId);
|
|
294
316
|
if (!claim) {
|
|
@@ -301,6 +323,46 @@ export async function handleClaimTool(
|
|
|
301
323
|
);
|
|
302
324
|
}
|
|
303
325
|
|
|
326
|
+
// Check ownership - only allow releasing your own claims unless config allows or force=true
|
|
327
|
+
if (claim.session_id !== sessionId) {
|
|
328
|
+
// Get caller's session config
|
|
329
|
+
const callerSession = await getSession(db, sessionId);
|
|
330
|
+
let config: SessionConfig = DEFAULT_SESSION_CONFIG;
|
|
331
|
+
if (callerSession?.config) {
|
|
332
|
+
try {
|
|
333
|
+
config = { ...DEFAULT_SESSION_CONFIG, ...JSON.parse(callerSession.config) };
|
|
334
|
+
} catch {
|
|
335
|
+
// Use default if parse fails
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Check if allowed to release others' claims
|
|
340
|
+
const canRelease = force === true || config.allow_release_others;
|
|
341
|
+
|
|
342
|
+
if (!canRelease) {
|
|
343
|
+
// Calculate how old the claim is
|
|
344
|
+
const claimAge = Date.now() - new Date(claim.created_at).getTime();
|
|
345
|
+
const staleHours = config.stale_threshold_hours;
|
|
346
|
+
const isStale = claimAge > staleHours * 60 * 60 * 1000;
|
|
347
|
+
|
|
348
|
+
return createToolResult(
|
|
349
|
+
JSON.stringify({
|
|
350
|
+
error: 'NOT_OWNER',
|
|
351
|
+
message: 'You can only release your own claims. This claim belongs to another session.',
|
|
352
|
+
claim_owner: claim.session_name,
|
|
353
|
+
claim_age_hours: Math.round(claimAge / (60 * 60 * 1000) * 10) / 10,
|
|
354
|
+
is_stale: isStale,
|
|
355
|
+
suggestions: [
|
|
356
|
+
'Use collab_message_send to ask the owner to release it.',
|
|
357
|
+
isStale ? 'This claim is stale. Ask user for confirmation, then use force=true to release.' : null,
|
|
358
|
+
'Use collab_config to enable allow_release_others for future releases.',
|
|
359
|
+
].filter(Boolean),
|
|
360
|
+
}),
|
|
361
|
+
true
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
304
366
|
if (claim.status !== 'active') {
|
|
305
367
|
return createToolResult(
|
|
306
368
|
JSON.stringify({
|
|
@@ -311,14 +373,18 @@ export async function handleClaimTool(
|
|
|
311
373
|
);
|
|
312
374
|
}
|
|
313
375
|
|
|
376
|
+
const isOwnClaim = claim.session_id === sessionId;
|
|
314
377
|
await releaseClaim(db, claimId, { status, summary });
|
|
315
378
|
|
|
316
379
|
return createToolResult(
|
|
317
380
|
JSON.stringify({
|
|
318
381
|
success: true,
|
|
319
|
-
message:
|
|
382
|
+
message: isOwnClaim
|
|
383
|
+
? `Claim ${status}. Files are now available for other sessions.`
|
|
384
|
+
: `Claim from ${claim.session_name} forcefully ${status}.`,
|
|
320
385
|
files: claim.files,
|
|
321
386
|
summary: summary ?? null,
|
|
387
|
+
was_forced: !isOwnClaim,
|
|
322
388
|
})
|
|
323
389
|
);
|
|
324
390
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Decision recording tools
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type { DatabaseAdapter } from '../../db/sqlite-adapter.js';
|
|
4
4
|
import type { McpTool, McpToolResult } from '../protocol';
|
|
5
5
|
import { createToolResult } from '../protocol';
|
|
6
6
|
import type { DecisionCategory } from '../../db/types';
|
|
@@ -55,7 +55,7 @@ export const decisionTools: McpTool[] = [
|
|
|
55
55
|
];
|
|
56
56
|
|
|
57
57
|
export async function handleDecisionTool(
|
|
58
|
-
db:
|
|
58
|
+
db: DatabaseAdapter,
|
|
59
59
|
name: string,
|
|
60
60
|
args: Record<string, unknown>
|
|
61
61
|
): Promise<McpToolResult> {
|
package/src/mcp/tools/message.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Inter-session messaging tools
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type { DatabaseAdapter } from '../../db/sqlite-adapter.js';
|
|
4
4
|
import type { McpTool, McpToolResult } from '../protocol';
|
|
5
5
|
import { createToolResult } from '../protocol';
|
|
6
6
|
import { sendMessage, listMessages, getSession } from '../../db/queries';
|
|
@@ -53,7 +53,7 @@ export const messageTools: McpTool[] = [
|
|
|
53
53
|
];
|
|
54
54
|
|
|
55
55
|
export async function handleMessageTool(
|
|
56
|
-
db:
|
|
56
|
+
db: DatabaseAdapter,
|
|
57
57
|
name: string,
|
|
58
58
|
args: Record<string, unknown>
|
|
59
59
|
): Promise<McpToolResult> {
|
package/src/mcp/tools/session.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Session management tools
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type { DatabaseAdapter } from '../../db/sqlite-adapter.js';
|
|
4
4
|
import type { McpTool, McpToolResult } from '../protocol';
|
|
5
5
|
import { createToolResult } from '../protocol';
|
|
6
6
|
import {
|
|
@@ -9,11 +9,13 @@ import {
|
|
|
9
9
|
listSessions,
|
|
10
10
|
updateSessionHeartbeat,
|
|
11
11
|
updateSessionStatus,
|
|
12
|
+
updateSessionConfig,
|
|
12
13
|
endSession,
|
|
13
14
|
cleanupStaleSessions,
|
|
14
15
|
listClaims,
|
|
15
16
|
} from '../../db/queries';
|
|
16
|
-
import type { TodoItem } from '../../db/types';
|
|
17
|
+
import type { TodoItem, SessionConfig, ConflictMode } from '../../db/types';
|
|
18
|
+
import { DEFAULT_SESSION_CONFIG } from '../../db/types';
|
|
17
19
|
|
|
18
20
|
export const sessionTools: McpTool[] = [
|
|
19
21
|
{
|
|
@@ -134,10 +136,41 @@ export const sessionTools: McpTool[] = [
|
|
|
134
136
|
required: ['session_id'],
|
|
135
137
|
},
|
|
136
138
|
},
|
|
139
|
+
{
|
|
140
|
+
name: 'collab_config',
|
|
141
|
+
description: 'Configure session behavior for conflict handling. Settings persist for the session duration.',
|
|
142
|
+
inputSchema: {
|
|
143
|
+
type: 'object',
|
|
144
|
+
properties: {
|
|
145
|
+
session_id: {
|
|
146
|
+
type: 'string',
|
|
147
|
+
description: 'Your session ID',
|
|
148
|
+
},
|
|
149
|
+
mode: {
|
|
150
|
+
type: 'string',
|
|
151
|
+
enum: ['strict', 'smart', 'bypass'],
|
|
152
|
+
description: 'Conflict handling mode: strict (always ask), smart (ask but suggest for stale), bypass (warn only)',
|
|
153
|
+
},
|
|
154
|
+
allow_release_others: {
|
|
155
|
+
type: 'boolean',
|
|
156
|
+
description: 'Allow releasing claims from other sessions (default: false)',
|
|
157
|
+
},
|
|
158
|
+
auto_release_stale: {
|
|
159
|
+
type: 'boolean',
|
|
160
|
+
description: 'Automatically release stale claims (default: false)',
|
|
161
|
+
},
|
|
162
|
+
stale_threshold_hours: {
|
|
163
|
+
type: 'number',
|
|
164
|
+
description: 'Hours before a claim is considered stale (default: 2)',
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
required: ['session_id'],
|
|
168
|
+
},
|
|
169
|
+
},
|
|
137
170
|
];
|
|
138
171
|
|
|
139
172
|
export async function handleSessionTool(
|
|
140
|
-
db:
|
|
173
|
+
db: DatabaseAdapter,
|
|
141
174
|
name: string,
|
|
142
175
|
args: Record<string, unknown>,
|
|
143
176
|
userId?: string
|
|
@@ -316,6 +349,65 @@ export async function handleSessionTool(
|
|
|
316
349
|
);
|
|
317
350
|
}
|
|
318
351
|
|
|
352
|
+
case 'collab_config': {
|
|
353
|
+
const sessionId = args.session_id as string;
|
|
354
|
+
|
|
355
|
+
// Validate session_id input
|
|
356
|
+
if (!sessionId || typeof sessionId !== 'string') {
|
|
357
|
+
return createToolResult(
|
|
358
|
+
JSON.stringify({
|
|
359
|
+
error: 'INVALID_INPUT',
|
|
360
|
+
message: 'session_id is required',
|
|
361
|
+
}),
|
|
362
|
+
true
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Validate session exists
|
|
367
|
+
const session = await getSession(db, sessionId);
|
|
368
|
+
if (!session || session.status !== 'active') {
|
|
369
|
+
return createToolResult(
|
|
370
|
+
JSON.stringify({
|
|
371
|
+
error: 'SESSION_INVALID',
|
|
372
|
+
message: 'Session not found or inactive.',
|
|
373
|
+
}),
|
|
374
|
+
true
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Get current config or default
|
|
379
|
+
let currentConfig: SessionConfig = DEFAULT_SESSION_CONFIG;
|
|
380
|
+
if (session.config) {
|
|
381
|
+
try {
|
|
382
|
+
currentConfig = { ...DEFAULT_SESSION_CONFIG, ...JSON.parse(session.config) };
|
|
383
|
+
} catch {
|
|
384
|
+
// Use default if parse fails
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Update with new values
|
|
389
|
+
const newConfig: SessionConfig = {
|
|
390
|
+
mode: (args.mode as ConflictMode) ?? currentConfig.mode,
|
|
391
|
+
allow_release_others: args.allow_release_others !== undefined
|
|
392
|
+
? (args.allow_release_others as boolean)
|
|
393
|
+
: currentConfig.allow_release_others,
|
|
394
|
+
auto_release_stale: args.auto_release_stale !== undefined
|
|
395
|
+
? (args.auto_release_stale as boolean)
|
|
396
|
+
: currentConfig.auto_release_stale,
|
|
397
|
+
stale_threshold_hours: (args.stale_threshold_hours as number) ?? currentConfig.stale_threshold_hours,
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
await updateSessionConfig(db, sessionId, newConfig);
|
|
401
|
+
|
|
402
|
+
return createToolResult(
|
|
403
|
+
JSON.stringify({
|
|
404
|
+
success: true,
|
|
405
|
+
message: 'Configuration updated.',
|
|
406
|
+
config: newConfig,
|
|
407
|
+
})
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
|
|
319
411
|
default:
|
|
320
412
|
return createToolResult(`Unknown session tool: ${name}`, true);
|
|
321
413
|
}
|
package/src/tokens/handlers.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Token management API handlers
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type { DatabaseAdapter } from '../db/sqlite-adapter.js';
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
import { generateApiToken } from './generator';
|
|
6
6
|
import { createApiToken, listApiTokens, revokeApiToken } from '../db/auth-queries';
|
|
@@ -14,7 +14,7 @@ const CreateTokenRequestSchema = z.object({
|
|
|
14
14
|
});
|
|
15
15
|
|
|
16
16
|
interface HandlerContext {
|
|
17
|
-
db:
|
|
17
|
+
db: DatabaseAdapter;
|
|
18
18
|
request: Request;
|
|
19
19
|
}
|
|
20
20
|
|