feat: add MCP and LSP tooling support

Add global MCP/LSP settings, per-session Activity toggles, sidecar runtime integration, tests, and documentation updates.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
David Kaya
2026-03-23 22:51:03 +01:00
co-authored by Copilot
parent 50a8e5dfbe
commit efa5c44e07
26 changed files with 3196 additions and 47 deletions
+12
View File
@@ -1,4 +1,9 @@
import { buildSessionTitle, type PatternDefinition, type ReasoningEffort } from '@shared/domain/pattern';
import {
createSessionToolingSelection,
normalizeSessionToolingSelection,
type SessionToolingSelection,
} from '@shared/domain/tooling';
export type ChatRole = 'system' | 'user' | 'assistant';
export type SessionStatus = 'idle' | 'running' | 'error';
@@ -32,6 +37,7 @@ export interface SessionRecord {
messages: ChatMessageRecord[];
lastError?: string;
scratchpadConfig?: ScratchpadSessionConfig;
tooling?: SessionToolingSelection;
}
export function resolveSessionTitle(
@@ -60,6 +66,12 @@ export function createScratchpadSessionConfig(
};
}
export function resolveSessionToolingSelection(
session: Pick<SessionRecord, 'tooling'>,
): SessionToolingSelection {
return normalizeSessionToolingSelection(session.tooling ?? createSessionToolingSelection());
}
export function resolveScratchpadSessionConfig(
session: SessionRecord,
pattern: PatternDefinition,
+6
View File
@@ -175,6 +175,12 @@ export function duplicateSessionRecord(
isArchived: false,
lastError: undefined,
scratchpadConfig: session.scratchpadConfig ? { ...session.scratchpadConfig } : undefined,
tooling: session.tooling
? {
enabledMcpServerIds: [...session.tooling.enabledMcpServerIds],
enabledLspProfileIds: [...session.tooling.enabledLspProfileIds],
}
: undefined,
messages: session.messages.map((message): ChatMessageRecord => ({
...message,
pending: false,
+177
View File
@@ -0,0 +1,177 @@
export type McpServerTransport = 'local' | 'http' | 'sse';
export interface BaseMcpServerDefinition {
id: string;
name: string;
transport: McpServerTransport;
tools: string[];
timeoutMs?: number;
createdAt: string;
updatedAt: string;
}
export interface LocalMcpServerDefinition extends BaseMcpServerDefinition {
transport: 'local';
command: string;
args: string[];
cwd?: string;
}
export interface RemoteMcpServerDefinition extends BaseMcpServerDefinition {
transport: 'http' | 'sse';
url: string;
}
export type McpServerDefinition = LocalMcpServerDefinition | RemoteMcpServerDefinition;
export interface LspProfileDefinition {
id: string;
name: string;
command: string;
args: string[];
languageId: string;
fileExtensions: string[];
createdAt: string;
updatedAt: string;
}
export interface WorkspaceToolingSettings {
mcpServers: McpServerDefinition[];
lspProfiles: LspProfileDefinition[];
}
export interface WorkspaceSettings {
tooling: WorkspaceToolingSettings;
}
export interface SessionToolingSelection {
enabledMcpServerIds: string[];
enabledLspProfileIds: string[];
}
export function createWorkspaceSettings(): WorkspaceSettings {
return {
tooling: {
mcpServers: [],
lspProfiles: [],
},
};
}
export function createSessionToolingSelection(): SessionToolingSelection {
return {
enabledMcpServerIds: [],
enabledLspProfileIds: [],
};
}
export function normalizeWorkspaceSettings(settings?: Partial<WorkspaceSettings>): WorkspaceSettings {
return {
tooling: {
mcpServers: (settings?.tooling?.mcpServers ?? []).map(normalizeMcpServerDefinition),
lspProfiles: (settings?.tooling?.lspProfiles ?? []).map(normalizeLspProfileDefinition),
},
};
}
export function normalizeSessionToolingSelection(
selection?: Partial<SessionToolingSelection>,
): SessionToolingSelection {
return {
enabledMcpServerIds: normalizeStringArray(selection?.enabledMcpServerIds),
enabledLspProfileIds: normalizeStringArray(selection?.enabledLspProfileIds),
};
}
export function validateMcpServerDefinition(server: McpServerDefinition): string | undefined {
if (!server.name.trim()) {
return 'MCP server name is required.';
}
if (server.transport === 'local') {
if (!server.command.trim()) {
return `MCP server "${server.name}" needs a command.`;
}
return undefined;
}
if (!server.url.trim()) {
return `MCP server "${server.name}" needs a URL.`;
}
try {
new URL(server.url);
} catch {
return `MCP server "${server.name}" has an invalid URL.`;
}
return undefined;
}
export function validateLspProfileDefinition(profile: LspProfileDefinition): string | undefined {
if (!profile.name.trim()) {
return 'LSP profile name is required.';
}
if (!profile.command.trim()) {
return `LSP profile "${profile.name}" needs a command.`;
}
if (!profile.languageId.trim()) {
return `LSP profile "${profile.name}" needs a language ID.`;
}
if (normalizeStringArray(profile.fileExtensions).length === 0) {
return `LSP profile "${profile.name}" needs at least one file extension.`;
}
return undefined;
}
export function normalizeMcpServerDefinition(server: McpServerDefinition): McpServerDefinition {
const base = {
...server,
name: server.name.trim(),
tools: normalizeStringArray(server.tools),
};
if (server.transport === 'local') {
return {
...base,
transport: 'local',
command: server.command.trim(),
args: normalizeStringArray(server.args),
cwd: server.cwd?.trim() || undefined,
};
}
return {
...base,
transport: server.transport,
url: server.url.trim(),
};
}
export function normalizeLspProfileDefinition(profile: LspProfileDefinition): LspProfileDefinition {
return {
...profile,
name: profile.name.trim(),
command: profile.command.trim(),
args: normalizeStringArray(profile.args),
languageId: profile.languageId.trim(),
fileExtensions: normalizeFileExtensions(profile.fileExtensions),
};
}
function normalizeFileExtensions(fileExtensions: string[]): string[] {
return normalizeStringArray(fileExtensions).map((value) => (value.startsWith('.') ? value : `.${value}`));
}
function normalizeStringArray(values?: ReadonlyArray<string>): string[] {
if (!values) {
return [];
}
return [...new Set(values.map((value) => value.trim()).filter((value) => value.length > 0))];
}
+3
View File
@@ -2,12 +2,14 @@ import type { PatternDefinition } from '@shared/domain/pattern';
import { createBuiltinPatterns } from '@shared/domain/pattern';
import type { ProjectRecord } from '@shared/domain/project';
import type { SessionRecord } from '@shared/domain/session';
import { createWorkspaceSettings, type WorkspaceSettings } from '@shared/domain/tooling';
import { nowIso } from '@shared/utils/ids';
export interface WorkspaceState {
projects: ProjectRecord[];
patterns: PatternDefinition[];
sessions: SessionRecord[];
settings: WorkspaceSettings;
selectedProjectId?: string;
selectedPatternId?: string;
selectedSessionId?: string;
@@ -20,6 +22,7 @@ export function createWorkspaceSeed(): WorkspaceState {
projects: [],
patterns: createBuiltinPatterns(timestamp),
sessions: [],
settings: createWorkspaceSettings(),
lastUpdatedAt: timestamp,
};
}