Skip to content

Commit f25a14c

Browse files
committed
feat: implement MCP Session Architecture with automated root fetching - Enhanced ClientInfo with Env/Roots fields - Added ClientSession convenience methods - Transport-aware session data extraction - Automated workspace root fetching per MCP protocol - Full compliance across all three MCP protocol versions - 500+ tests passing
1 parent 9baa40f commit f25a14c

File tree

9 files changed

+1310
-70
lines changed

9 files changed

+1310
-70
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,4 @@ node_modules/
4848

4949
# Specification files (kept locally but not in repo)
5050
specification/
51+
.taskmaster

MCP_Session_Architecture.md

Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
# MCP Session Architecture and Project Root Detection
2+
3+
## Overview
4+
5+
This document explains how Model Context Protocol (MCP) sessions work, how project root detection is implemented, and how MCP servers can access session information from clients like Cursor, Claude Desktop, and other MCP-compatible tools.
6+
7+
## How MCP Sessions Are Created
8+
9+
### Client-Side Session Creation
10+
11+
**Important**: The MCP session is **created by the MCP client** (Cursor, Claude Desktop, etc.), not by the MCP server. The server receives this session object and can extract project information from it.
12+
13+
When an MCP client starts a server, it creates a session object containing:
14+
15+
- **`session.roots`** - Workspace/project directories the client wants to expose
16+
- **`session.env`** - Environment variables from the client context
17+
- **`session.capabilities`** - Features the client supports
18+
- **`session.loggingLevel`** - Logging level set by the client
19+
20+
### How `session.roots` Gets Populated
21+
22+
The `session.roots` array is populated differently by each MCP client:
23+
24+
#### **Cursor IDE:**
25+
- Automatically passes the **currently open workspace root** as `session.roots[0].uri`
26+
- Uses the workspace directory you have open in Cursor
27+
- Happens automatically when Cursor starts the MCP server process
28+
29+
#### **Claude Desktop:**
30+
- Uses the working directory where the MCP server process is started
31+
- Can be configured in Claude Desktop's MCP configuration
32+
33+
#### **Other MCP Clients:**
34+
- Implementation varies by client
35+
- Usually current working directory or configured project paths
36+
37+
## Session Object Structure
38+
39+
```javascript
40+
{
41+
roots: [
42+
{
43+
uri: "file:///Users/username/workspace/project", // Project root URI
44+
name: "project" // Optional name
45+
}
46+
// Can contain multiple roots
47+
],
48+
env: {
49+
// Environment variables from the client
50+
TASK_MASTER_PROJECT_ROOT: "/custom/path", // If set by user
51+
DEBUG: "true",
52+
// ... other environment variables
53+
},
54+
capabilities: {
55+
// Features the client supports
56+
progress: true, // Can show progress indicators
57+
sampling: false, // Can request LLM completions
58+
// ... other capabilities
59+
},
60+
loggingLevel: "info" // Logging level preference
61+
}
62+
```
63+
64+
## URI Format and Normalization
65+
66+
The `session.roots[0].uri` contains a **file:// URI** that requires processing:
67+
68+
1. **URI Decoding** - Handle spaces and special characters
69+
2. **Protocol Stripping** - Remove `file://` prefix
70+
3. **Path Normalization** - Handle OS-specific paths (especially Windows `/C:/...`)
71+
72+
### Example URI Processing
73+
74+
```javascript
75+
// Input from session
76+
const rawUri = "file:///Users/username/My%20Project";
77+
78+
// Processing steps
79+
const decoded = decodeURIComponent(rawUri); // "file:///Users/username/My Project"
80+
const withoutProtocol = decoded.replace('file://', ''); // "/Users/username/My Project"
81+
82+
// Handle Windows paths (remove leading slash from /C:/...)
83+
const normalized = withoutProtocol.startsWith('/') && /[A-Za-z]:/.test(withoutProtocol.substring(1, 3))
84+
? withoutProtocol.substring(1) // Remove leading slash for Windows
85+
: withoutProtocol;
86+
87+
const finalPath = path.resolve(normalized); // OS-normalized absolute path
88+
```
89+
90+
## Accessing the Session Object in MCP Tools
91+
92+
### Basic Pattern
93+
94+
```javascript
95+
server.addTool({
96+
name: 'example_tool',
97+
description: 'Shows how to access session data',
98+
parameters: z.object({
99+
// tool parameters
100+
}),
101+
execute: async (args, { log, session }) => {
102+
// Access session properties
103+
const projectRoot = session?.roots?.[0]?.uri;
104+
const envVars = session?.env;
105+
const capabilities = session?.capabilities;
106+
107+
log.info(`Project root: ${projectRoot}`);
108+
log.info(`Environment vars: ${JSON.stringify(envVars)}`);
109+
110+
return "Tool executed with session data";
111+
}
112+
});
113+
```
114+
115+
### Real-World Example (Task Master)
116+
117+
```javascript
118+
// From Task Master's get-tasks tool
119+
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
120+
try {
121+
// Session is used to resolve project paths
122+
let tasksJsonPath = resolveTasksPath(args, session);
123+
let complexityReportPath = resolveComplexityReportPath(args, session);
124+
125+
// Tool logic using resolved paths...
126+
const result = await listTasksDirect({
127+
tasksJsonPath: tasksJsonPath,
128+
// ... other parameters
129+
}, log);
130+
131+
return handleApiResult(result, log, 'Error getting tasks');
132+
} catch (error) {
133+
log.error(`Error: ${error.message}`);
134+
return createErrorResponse(error.message);
135+
}
136+
})
137+
```
138+
139+
### Context Object Structure
140+
141+
The complete context object passed to MCP tools:
142+
143+
```javascript
144+
{
145+
log: { // Logger with methods
146+
info(msg), // Log info message
147+
warn(msg), // Log warning
148+
error(msg), // Log error
149+
debug(msg) // Log debug info
150+
},
151+
session: { // MCP session from client
152+
roots: [...], // Project roots
153+
env: {...}, // Environment variables
154+
capabilities: {...}, // Client capabilities
155+
loggingLevel: "info"
156+
}
157+
// ... other MCP context properties
158+
}
159+
```
160+
161+
## Project Root Detection Strategy
162+
163+
A robust MCP server should implement a hierarchical fallback system:
164+
165+
### 1. Environment Variable Override (Highest Priority)
166+
```javascript
167+
if (process.env.PROJECT_ROOT) {
168+
return process.env.PROJECT_ROOT;
169+
}
170+
if (session?.env?.PROJECT_ROOT) {
171+
return session.env.PROJECT_ROOT;
172+
}
173+
```
174+
175+
### 2. Explicit Parameter
176+
```javascript
177+
if (args.projectRoot) {
178+
return normalizeProjectRoot(args.projectRoot);
179+
}
180+
```
181+
182+
### 3. Session-Based Detection
183+
```javascript
184+
if (session?.roots?.[0]?.uri) {
185+
return normalizeUri(session.roots[0].uri);
186+
}
187+
```
188+
189+
### 4. Project Marker Search
190+
```javascript
191+
// Search upward for project indicators
192+
const markers = ['.git', '.taskmaster', 'package.json', 'go.mod'];
193+
return findProjectRoot(process.cwd(), markers);
194+
```
195+
196+
### 5. Current Directory (Fallback)
197+
```javascript
198+
return process.cwd();
199+
```
200+
201+
## Implementation Considerations
202+
203+
### For MCP Server Developers
204+
205+
1. **Always access session through context parameter**
206+
2. **Implement proper URI normalization** for cross-platform compatibility
207+
3. **Use hierarchical fallback** for project root detection
208+
4. **Handle missing session gracefully** (some clients may not provide full session data)
209+
5. **Log session usage** for debugging client integration issues
210+
211+
### For MCP Client Developers
212+
213+
1. **Always populate `session.roots`** with workspace directories
214+
2. **Use proper file:// URI format** for root paths
215+
3. **Include environment variables** in `session.env`
216+
4. **Set appropriate capabilities** in `session.capabilities`
217+
5. **Maintain session consistency** across tool calls
218+
219+
## Testing Session Handling
220+
221+
### Unit Test Example
222+
223+
```javascript
224+
// Test session-based project root detection
225+
const mockSession = {
226+
roots: [
227+
{ uri: "file:///Users/test/workspace/project" }
228+
],
229+
env: {},
230+
capabilities: {}
231+
};
232+
233+
const result = await toolFunction(args, { log: mockLog, session: mockSession });
234+
// Assert result uses correct project root
235+
```
236+
237+
### Integration Test
238+
239+
```javascript
240+
// Test with actual MCP client
241+
const client = new MCPClient("path/to/server");
242+
await client.connect();
243+
244+
// Verify server receives session data correctly
245+
const tools = await client.listTools();
246+
const result = await client.callTool("get_project_info", {});
247+
```
248+
249+
## Security Considerations
250+
251+
1. **Validate session data** - Don't trust session content blindly
252+
2. **Sanitize file paths** - Prevent directory traversal attacks
253+
3. **Respect client boundaries** - Only access paths in `session.roots`
254+
4. **Environment variable safety** - Be cautious with `session.env` data
255+
256+
## Compatibility Notes
257+
258+
- **FastMCP (Python)**: Full session support with automatic normalization
259+
- **FastMCP (TypeScript)**: Complete session handling implementation
260+
- **Official MCP SDKs**: Basic session support, may need manual normalization
261+
- **Custom implementations**: Session support varies
262+
263+
## Conclusion
264+
265+
The MCP session architecture provides a standardized way for clients to communicate workspace context to servers. By properly implementing session handling, MCP servers can automatically detect project roots and provide seamless integration with development tools like Cursor.
266+
267+
The key insight is that **clients create and populate the session** - servers should extract and normalize this information rather than trying to detect project context independently.

server/context.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ type Context struct {
4646

4747
// Metadata for storing contextual information during request processing
4848
Metadata map[string]interface{}
49+
50+
// this is a session id that is used to track the session
51+
Session *ClientSession
4952
}
5053

5154
// Request represents an incoming JSON-RPC 2.0 request.
@@ -113,6 +116,7 @@ func NewContext(ctx context.Context, requestBytes []byte, server *serverImpl) (*
113116
server: server,
114117
Logger: server.logger,
115118
Metadata: make(map[string]interface{}),
119+
Session: server.defaultSession, // ✅ Attach the current session
116120
}
117121

118122
// Parse the request

server/sampling.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -800,6 +800,8 @@ type ClientInfo struct {
800800
SamplingSupported bool
801801
SamplingCaps SamplingCapabilities
802802
ProtocolVersion string
803+
Env map[string]string // Environment variables from the client session
804+
Roots []string // Workspace root paths from the client session
803805
// Add other client capabilities here
804806
}
805807

0 commit comments

Comments
 (0)