Refactor the MCP (Model Context Protocol) integration to use a port-based proxying architecture where each VS Code project instance exposes a single HTTP port (starting from 50001), and requests are transparently proxied to the appropriate MCP server instance based on project root directory.
- Single port per VS Code instance starting from 50001
- Transparent request proxying based on project root directory
- Seamless integration with MCP clients (especially Claude Code)
- Persistent port assignments across restarts
- Convert all MCP resources to tools for consistent API
Current Behavior:
- Each project gets a port assigned using workspace ID hash
- Port mapping stored in JSON:
{workspaceId: port}
New Behavior:
- Each VS Code instance gets a port starting from 50001
- If port 50001 is taken, increment to 50002, 50003, etc.
- Port mapping stored in JSON:
{projectRoot: port}where projectRoot is full absolute path - Port assignments persist across VS Code restarts (same project gets same port)
- JSON file location: current location (as used by existing implementation)
Startup (Extension Activation):
- Check if MCP feature is enabled via
Devdb.enableMcpServersetting - If disabled at any level (following hierarchy: project > workspace > global), skip MCP server initialization
- Read workspace folder path (first/active workspace only - no multi-root support)
- Check JSON mapping for existing port assignment for this project root
- If port exists, attempt to bind to it
- If port unavailable, find next available port starting from 50001
- Update JSON mapping immediately after port allocation:
{projectRoot: port} - Start HTTP server on allocated port
- Spawn MCP SDK server process (stdio transport)
Shutdown (Extension Deactivation):
- Clean up port mapping entry from JSON file for current project root
- Stop HTTP server
- Terminate MCP SDK server process
Port Conflict Resolution:
- If assigned port is occupied on restart, silently find next available port and update JSON mapping
- No user intervention required
File Location: Use existing location from current implementation
Structure:
{
"/Users/user/project1": 50001,
"/Users/user/project2": 50002,
"/home/user/workspace": 50003
}Key Format: Full absolute path (platform-specific)
Cleanup:
- Entries removed on extension deactivation
- No automatic pruning of stale entries
- "Last one wins" if multiple VS Code instances open same project (overwrites port mapping)
Current Implementation:
- Resources:
tables,schema,database-type - Tool:
run-query
New Implementation (All Tools):
{
name: 'get-tables',
description: 'Get list of tables in database',
inputSchema: {
projectRoot: z.string(),
}
}{
name: 'get-schema',
description: 'Get schema for specified table',
inputSchema: {
projectRoot: z.string(),
table: z.string(),
}
}{
name: 'get-database-type',
description: 'Get database type (e.g. mysql2, postgres, mssql) to determine SQL syntax',
inputSchema: {
projectRoot: z.string(),
}
}{
name: 'run-query',
description: 'Run a SQL query',
inputSchema: {
projectRoot: z.string(),
query: z.string(),
}
}Client Request Flow:
- MCP client (e.g., Claude Code) connects to MCP server via stdio
- Client calls tool (e.g.,
get-tables) withprojectRootparameter - Tool handler receives
projectRootparameter - Handler validates
projectRoot:- Must be absolute path
- Must exist on filesystem
- If validation fails, return MCP standard error response:
{isError: true, content: [{type: 'text', text: 'error message'}]}
- Handler calls corresponding fetch function (e.g.,
fetchTables(projectRoot)) - Fetch function calls
getServerUrl(projectRoot) getServerUrlcallsgetPort(projectRoot)to lookup port in JSON file- If project not found in JSON, return MCP error: "MCP server not running for this project"
- If port found, construct URL:
http://localhost:{port} - Make HTTP request to that port (e.g.,
GET http://localhost:50001/tables) - HTTP server at that port handles request using local database connection
- Response returned through the chain back to client
Key Points:
- Proxying happens transparently in
server.tsbefore fetch calls - HTTP server endpoints (
http-server.ts) unchanged - they always serve local project's database - No logging of proxy operations (silent, normal behavior)
- Always use HTTP fetch even if projectRoot matches current instance (consistent code path)
- No local optimization or shortcuts
Changes:
-
Update
McpConfiginterface:interface McpConfig { [projectRoot: string]: number; }
-
Update
savePortfunction signature:export function savePort(port: number, projectRoot: string): void
- Change parameter from
workspaceIdtoprojectRoot - Update logic to use
projectRootas key - Remove hash logic (store full path directly)
- Change parameter from
-
Update
getPortfunction signature:export function getPort(projectRoot: string): number | null
- Remove
process.env.WORKSPACE_IDcheck - Accept
projectRootas parameter - Lookup port by
projectRootin config - Return port or null
- Remove
-
Update
clearPortfunction signature:export function clearPort(projectRoot: string): void
- Change parameter from
workspaceIdtoprojectRoot
- Change parameter from
Changes:
-
Update
getWorkspaceId():- Rename to
getProjectRoot() - Return full workspace path instead of hash:
export function getProjectRoot(): string { const workspaceFolders = vscode.workspace.workspaceFolders; const workspacePath = workspaceFolders?.[0]?.uri.fsPath; if (!workspacePath) { throw new Error('No workspace found'); } return workspacePath; }
- Rename to
-
Update
startHttpServer():- Call
getProjectRoot()instead ofgetWorkspaceId() - Pass
projectRoottosavePort(availablePort, projectRoot) - Update logging to use
projectRootinstead ofworkspaceId
- Call
-
Update
stopHttpServer():- Call
getProjectRoot()instead ofgetWorkspaceId() - Pass
projectRoottoclearPort(projectRoot)
- Call
-
Keep all endpoint handlers unchanged:
/tables,/tables/:tableName/schema,/query,/database-type- These serve the local project's database
- No proxying logic needed here
Changes:
-
Update server version:
const server = new McpServer({ name: "DevDB", version: "1.1.0" // bump from 1.0.2 }, { /* capabilities */ });
-
Remove all
registerResourcecalls -
Add new tool registrations:
Tool: get-tables
server.registerTool( 'get-tables', { title: 'Get tables', description: 'Get list of tables in database', inputSchema: { projectRoot: z.string() } }, async ({ projectRoot }) => { if (!path.isAbsolute(projectRoot)) { return { content: [{ type: 'text', text: 'Error: projectRoot must be an absolute path' }], isError: true }; } if (!fs.existsSync(projectRoot)) { return { content: [{ type: 'text', text: 'Error: projectRoot does not exist' }], isError: true }; } try { const tables = await fetchTables(projectRoot); return { content: [{ type: 'text', text: JSON.stringify(tables) }] }; } catch (error) { return { content: [{ type: 'text', text: String(error) }], isError: true }; } } );
Tool: get-schema
server.registerTool( 'get-schema', { title: 'Get table schema', description: 'Get schema for specified table', inputSchema: { projectRoot: z.string(), table: z.string() } }, async ({ projectRoot, table }) => { // Similar validation as get-tables // Call fetchTableSchema(projectRoot, table) } );
Tool: get-database-type
server.registerTool( 'get-database-type', { title: 'Get database type', description: 'Get database type to determine SQL syntax', inputSchema: { projectRoot: z.string() } }, async ({ projectRoot }) => { // Similar validation as get-tables // Call fetchDatabaseType(projectRoot) } );
Tool: run-query (Updated)
server.registerTool( 'run-query', { title: 'Run a query', description: 'Run a SQL query', inputSchema: { projectRoot: z.string(), query: z.string() } }, async ({ projectRoot, query }) => { // Add projectRoot validation // Call executeQuery(projectRoot, query) } );
-
Update
getServerUrlfunction:function getServerUrl(projectRoot: string): string { const port = getPort(projectRoot); if (!port) { throw new Error(`MCP server not running for project: ${projectRoot}`); } return `http://localhost:${port}`; }
-
Update fetch functions:
async function fetchTables(projectRoot: string): Promise<string[]> { const baseUrl = getServerUrl(projectRoot); const resp = await fetch(`${baseUrl}/tables`); if (!resp.ok) { throw new Error('Could not establish database connection'); } const { tables } = await resp.json(); return tables; } async function fetchTableSchema(projectRoot: string, name: string): Promise<string> { const baseUrl = getServerUrl(projectRoot); const resp = await fetch(`${baseUrl}/tables/${encodeURIComponent(name)}/schema`); if (!resp.ok) { throw new Error('Could not establish database connection'); } const { schema } = await resp.json(); return schema; } async function executeQuery(projectRoot: string, query: string): Promise<any> { const baseUrl = getServerUrl(projectRoot); const resp = await fetch(`${baseUrl}/query`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query }) }); if (!resp.ok) { const errorData = await resp.json().catch(() => ({ message: 'Unknown error' })); throw new Error(errorData.message); } const { result } = await resp.json(); return result; } async function fetchDatabaseType(projectRoot: string): Promise<string> { const baseUrl = getServerUrl(projectRoot); const resp = await fetch(`${baseUrl}/database-type`); if (!resp.ok) { throw new Error('Could not establish database connection'); } const { type } = await resp.json(); return type; }
-
Remove
getPort()call without parameter (now requiresprojectRoot)
Validation Errors:
- Invalid projectRoot (not absolute): "Error: projectRoot must be an absolute path"
- projectRoot doesn't exist: "Error: projectRoot does not exist"
- Project not in port mapping: "MCP server not running for project: {projectRoot}"
Connection Errors:
- HTTP request fails: "Could not establish database connection"
- Port lookup fails: "MCP server not running for this project"
Error Response Format: All errors use MCP standard error response:
{
content: [{
type: 'text',
text: 'error message'
}],
isError: true
}Failure Modes:
- If target MCP server is not running or port is stale: Return error to client immediately (no retry, no auto-restart)
- If HTTP request fails: Return connection error to client
- If projectRoot validation fails: Return validation error to client
User Notifications:
- On MCP server failure: Show toast notification (info only, no action buttons)
- No notifications for successful operations
- No notifications for proxy operations (transparent)
Existing Setting: Devdb.enableMcpServer (boolean)
Behavior:
- Hierarchy: project > workspace > global (most specific wins)
- When disabled: MCP server doesn't start on extension activation
- When enabled: MCP server starts automatically on extension activation
- No runtime enable/disable behavior (takes effect on next activation)
Current Implementation: Already exists in package.json, no changes needed
Commands:
- No new commands needed
- No port mapping viewer command
- Users can manually check JSON file if needed
Multi-root Workspaces:
- Not supported
- Only first/active workspace folder is used
- No special handling for multi-root scenarios
Manual Testing with Claude Code:
- Open project A in VS Code instance 1
- Verify MCP server starts on port 50001
- Connect Claude Code via stdio
- Call
get-tableswith projectRoot pointing to project A - Verify tables are returned
- Open project B in VS Code instance 2
- Verify MCP server starts on port 50002
- Call
get-tablesfrom instance 1 with projectRoot pointing to project B - Verify request is proxied and returns project B's tables
- Close instance 2, verify port mapping is cleaned up
- Reopen project B, verify it gets port 50002 again (persistence)
Error Testing:
- Call tool with invalid projectRoot (relative path)
- Call tool with non-existent projectRoot
- Call tool with projectRoot for closed project
- Verify proper error messages
Port Conflict Testing:
- Manually bind port 50001
- Start VS Code with project
- Verify MCP server finds port 50002
- Verify JSON mapping is updated
README.md:
- No updates needed for now
- MCP section remains minimal
- Can be enhanced later after implementation stabilizes
Migration Guide:
- Not needed (breaking change but internal architecture)
- Clients just need to pass
projectRootparameter to all tools
-
Resources → Tools: All MCP resources converted to tools
db://tables→get-tablestooldb://tables/{table}/schema→get-schematooldb://database-type→get-database-typetool
-
Required Parameter: All tools now require
projectRootparameter- Clients must pass absolute path to project root
- No default or inference
-
Version Bump: MCP server version: 1.0.2 → 1.1.0
-
JSON Structure Change: Port mapping keys change from workspace ID hash to full project root path
- Update
port-manager.tsinterface and function signatures - Update
http-server.tsto use project root instead of workspace ID - Convert all resources to tools in
server.ts - Add
projectRootparameter validation to all tools - Update all fetch functions to accept
projectRootparameter - Update
getServerUrlandgetPortto useprojectRoot - Remove
WORKSPACE_IDenvironment variable dependencies - Update port cleanup on extension deactivation
- Bump MCP server version to 1.1.0
- Test with Claude Code
- Test port persistence across restarts
- Test port conflict resolution
- Test error handling for invalid projectRoot
- Test proxying between multiple VS Code instances
- Verify settings hierarchy (project > workspace > global)
- ✓ Single port per VS Code instance (50001, 50002, ...)
- ✓ Transparent proxying based on projectRoot parameter
- ✓ Port assignments persist across restarts
- ✓ All resources converted to tools with consistent API
- ✓ Validation errors return proper MCP error responses
- ✓ Works seamlessly with Claude Code
- ✓ JSON mapping uses full project root paths
- ✓ Extension deactivation cleans up port mapping
- ✓ Port conflicts resolved automatically
- ✓ Settings integration working (enable/disable)