mirror of
https://github.com/getpaseo/paseo.git
synced 2026-08-14 12:23:16 +00:00
fix: complete MCP split implementation
- Fix socket path conflict: main server defaults to TCP (localhost:6767), Self-ID MCP uses Unix socket (self-id-mcp.sock) - Remove set_title/set_branch from mcp-server.ts (they belong only in agent-self-id-mcp.ts which is used by coding agents) - Rename mcpSocketPath to selfIdMcpSocketPath for consistency - Pass --socket arg to self-id-bridge so non-default paths work - Wire up PID lock to prevent multiple daemon instances - Add PASEO_SELF_ID_MCP_SOCK env var for testing override
This commit is contained in:
@@ -59,6 +59,8 @@ export type AgentManagerOptions = {
|
|||||||
registry?: AgentStorage;
|
registry?: AgentStorage;
|
||||||
onAgentAttention?: AgentAttentionCallback;
|
onAgentAttention?: AgentAttentionCallback;
|
||||||
logger: Logger;
|
logger: Logger;
|
||||||
|
/** Path to the Self-ID MCP Unix socket for UI agent injection */
|
||||||
|
selfIdMcpSocketPath?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type WaitForAgentOptions = {
|
export type WaitForAgentOptions = {
|
||||||
@@ -106,7 +108,7 @@ type ManagedAgentBase = {
|
|||||||
/**
|
/**
|
||||||
* User-defined labels for categorizing agents (e.g., { ui: "true" }).
|
* User-defined labels for categorizing agents (e.g., { ui: "true" }).
|
||||||
*/
|
*/
|
||||||
labels?: Record<string, string>;
|
labels: Record<string, string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ManagedAgentWithSession = ManagedAgentBase & {
|
type ManagedAgentWithSession = ManagedAgentBase & {
|
||||||
@@ -190,6 +192,7 @@ export class AgentManager {
|
|||||||
private readonly idFactory: () => string;
|
private readonly idFactory: () => string;
|
||||||
private readonly registry?: AgentStorage;
|
private readonly registry?: AgentStorage;
|
||||||
private readonly previousStatuses = new Map<string, AgentLifecycleStatus>();
|
private readonly previousStatuses = new Map<string, AgentLifecycleStatus>();
|
||||||
|
private readonly selfIdMcpSocketPath?: string;
|
||||||
private onAgentAttention?: AgentAttentionCallback;
|
private onAgentAttention?: AgentAttentionCallback;
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
|
|
||||||
@@ -198,6 +201,7 @@ export class AgentManager {
|
|||||||
options?.maxTimelineItems ?? DEFAULT_MAX_TIMELINE_ITEMS;
|
options?.maxTimelineItems ?? DEFAULT_MAX_TIMELINE_ITEMS;
|
||||||
this.idFactory = options?.idFactory ?? (() => randomUUID());
|
this.idFactory = options?.idFactory ?? (() => randomUUID());
|
||||||
this.registry = options?.registry;
|
this.registry = options?.registry;
|
||||||
|
this.selfIdMcpSocketPath = options?.selfIdMcpSocketPath;
|
||||||
this.onAgentAttention = options?.onAgentAttention;
|
this.onAgentAttention = options?.onAgentAttention;
|
||||||
this.logger = options.logger.child({ module: "agent", component: "agent-manager" });
|
this.logger = options.logger.child({ module: "agent", component: "agent-manager" });
|
||||||
if (options?.clients) {
|
if (options?.clients) {
|
||||||
@@ -313,13 +317,18 @@ export class AgentManager {
|
|||||||
agentId?: string,
|
agentId?: string,
|
||||||
options?: { labels?: Record<string, string> }
|
options?: { labels?: Record<string, string> }
|
||||||
): Promise<ManagedAgent> {
|
): Promise<ManagedAgent> {
|
||||||
const normalizedConfig = await this.normalizeConfig(config, { labels: options?.labels });
|
// Generate agent ID early so we can use it in MCP config
|
||||||
|
const resolvedAgentId = agentId ?? this.idFactory();
|
||||||
|
const normalizedConfig = await this.normalizeConfig(config, {
|
||||||
|
labels: options?.labels,
|
||||||
|
agentId: resolvedAgentId,
|
||||||
|
});
|
||||||
const client = this.requireClient(normalizedConfig.provider);
|
const client = this.requireClient(normalizedConfig.provider);
|
||||||
const session = await client.createSession(normalizedConfig);
|
const session = await client.createSession(normalizedConfig);
|
||||||
return this.registerSession(
|
return this.registerSession(
|
||||||
session,
|
session,
|
||||||
normalizedConfig,
|
normalizedConfig,
|
||||||
agentId ?? this.idFactory(),
|
resolvedAgentId,
|
||||||
{ labels: options?.labels }
|
{ labels: options?.labels }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -835,7 +844,7 @@ export class AgentManager {
|
|||||||
lastUserMessageAt: options?.lastUserMessageAt ?? null,
|
lastUserMessageAt: options?.lastUserMessageAt ?? null,
|
||||||
attention: { requiresAttention: false },
|
attention: { requiresAttention: false },
|
||||||
internal: config.internal ?? false,
|
internal: config.internal ?? false,
|
||||||
labels: options?.labels,
|
labels: options?.labels ?? {},
|
||||||
} as ActiveManagedAgent;
|
} as ActiveManagedAgent;
|
||||||
|
|
||||||
this.agents.set(agentId, managed);
|
this.agents.set(agentId, managed);
|
||||||
@@ -1103,7 +1112,7 @@ export class AgentManager {
|
|||||||
|
|
||||||
private async normalizeConfig(
|
private async normalizeConfig(
|
||||||
config: AgentSessionConfig,
|
config: AgentSessionConfig,
|
||||||
options?: { labels?: Record<string, string> }
|
options?: { labels?: Record<string, string>; agentId?: string }
|
||||||
): Promise<AgentSessionConfig> {
|
): Promise<AgentSessionConfig> {
|
||||||
const normalized: AgentSessionConfig = { ...config };
|
const normalized: AgentSessionConfig = { ...config };
|
||||||
|
|
||||||
@@ -1117,12 +1126,29 @@ export class AgentManager {
|
|||||||
normalized.model = trimmed.length > 0 ? trimmed : undefined;
|
normalized.model = trimmed.length > 0 ? trimmed : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inject paseoPromptInstructions for UI agents (with ui=true label)
|
// Inject paseoPromptInstructions and MCP config for UI agents (with ui=true label)
|
||||||
const isUiAgent = options?.labels?.ui === "true";
|
const isUiAgent = options?.labels?.ui === "true";
|
||||||
if (isUiAgent) {
|
if (isUiAgent) {
|
||||||
normalized.paseoPromptInstructions = getSelfIdentificationInstructions({
|
normalized.paseoPromptInstructions = getSelfIdentificationInstructions({
|
||||||
cwd: normalized.cwd,
|
cwd: normalized.cwd,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Inject Self-ID MCP server config (stdio bridge to self-id-mcp.sock)
|
||||||
|
if (this.selfIdMcpSocketPath && options?.agentId) {
|
||||||
|
const existingMcpServers = normalized.mcpServers ?? {};
|
||||||
|
normalized.mcpServers = {
|
||||||
|
...existingMcpServers,
|
||||||
|
"paseo-self-id": {
|
||||||
|
type: "stdio",
|
||||||
|
command: "paseo",
|
||||||
|
args: [
|
||||||
|
"self-id-bridge",
|
||||||
|
"--socket", this.selfIdMcpSocketPath,
|
||||||
|
"--agent-id", options.agentId,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return normalized;
|
return normalized;
|
||||||
|
|||||||
@@ -25,13 +25,8 @@ import { toAgentPayload } from "./agent-projections.js";
|
|||||||
import { curateAgentActivity } from "./activity-curator.js";
|
import { curateAgentActivity } from "./activity-curator.js";
|
||||||
import { AGENT_PROVIDER_DEFINITIONS } from "./provider-registry.js";
|
import { AGENT_PROVIDER_DEFINITIONS } from "./provider-registry.js";
|
||||||
import { AgentStorage } from "./agent-storage.js";
|
import { AgentStorage } from "./agent-storage.js";
|
||||||
import {
|
import { createWorktree } from "../../utils/worktree.js";
|
||||||
createWorktree,
|
|
||||||
isPaseoOwnedWorktreeCwd,
|
|
||||||
validateBranchSlug,
|
|
||||||
} from "../../utils/worktree.js";
|
|
||||||
import { WaitForAgentTracker } from "./wait-for-agent-tracker.js";
|
import { WaitForAgentTracker } from "./wait-for-agent-tracker.js";
|
||||||
import { NotGitRepoError, renameCurrentBranch } from "../../utils/checkout-git.js";
|
|
||||||
import { injectLeadingPaseoInstructionTag } from "./paseo-instructions-tag.js";
|
import { injectLeadingPaseoInstructionTag } from "./paseo-instructions-tag.js";
|
||||||
|
|
||||||
export interface AgentMcpServerOptions {
|
export interface AgentMcpServerOptions {
|
||||||
@@ -112,18 +107,6 @@ function expandPath(path: string): string {
|
|||||||
return resolve(path);
|
return resolve(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
type ToolErrorCode = "NOT_ALLOWED" | "NOT_GIT_REPO" | "INVALID_BRANCH";
|
|
||||||
|
|
||||||
class AgentMcpToolError extends Error {
|
|
||||||
readonly code: ToolErrorCode;
|
|
||||||
|
|
||||||
constructor(code: ToolErrorCode, message: string) {
|
|
||||||
super(message);
|
|
||||||
this.name = "AgentMcpToolError";
|
|
||||||
this.code = code;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wraps agentManager.waitForAgentEvent with a self-imposed timeout.
|
* Wraps agentManager.waitForAgentEvent with a self-imposed timeout.
|
||||||
* Returns a friendly message when timeout occurs, rather than letting
|
* Returns a friendly message when timeout occurs, rather than letting
|
||||||
@@ -903,134 +886,6 @@ export async function createAgentMcpServer(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
server.registerTool(
|
|
||||||
"set_title",
|
|
||||||
{
|
|
||||||
title: "Set Agent Title",
|
|
||||||
description: "Update the agent's title in the registry.",
|
|
||||||
inputSchema: {
|
|
||||||
title: z
|
|
||||||
.string()
|
|
||||||
.min(1)
|
|
||||||
.max(60)
|
|
||||||
.describe("Short descriptive title (<= 60 chars)."),
|
|
||||||
},
|
|
||||||
outputSchema: {
|
|
||||||
success: z.boolean(),
|
|
||||||
title: z.string(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
async ({ title }) => {
|
|
||||||
if (!callerAgentId) {
|
|
||||||
throw new AgentMcpToolError(
|
|
||||||
"NOT_ALLOWED",
|
|
||||||
"set_title can only be called by a managed agent"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const agent = agentManager.getAgent(callerAgentId);
|
|
||||||
if (!agent) {
|
|
||||||
throw new Error(`Agent ${callerAgentId} not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const normalizedTitle = title.trim();
|
|
||||||
if (!normalizedTitle) {
|
|
||||||
throw new AgentMcpToolError("NOT_ALLOWED", "Title cannot be empty");
|
|
||||||
}
|
|
||||||
if (normalizedTitle.length > 60) {
|
|
||||||
throw new AgentMcpToolError(
|
|
||||||
"NOT_ALLOWED",
|
|
||||||
"Title must be 60 characters or fewer"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await agentManager.setTitle(agent.id, normalizedTitle);
|
|
||||||
return {
|
|
||||||
content: [],
|
|
||||||
structuredContent: ensureValidJson({
|
|
||||||
success: true,
|
|
||||||
title: normalizedTitle,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
server.registerTool(
|
|
||||||
"set_branch",
|
|
||||||
{
|
|
||||||
title: "Set Agent Branch",
|
|
||||||
description:
|
|
||||||
"Rename the current git branch. Allowed only inside Paseo-owned worktrees.",
|
|
||||||
inputSchema: {
|
|
||||||
name: z
|
|
||||||
.string()
|
|
||||||
.min(1)
|
|
||||||
.describe("Git branch name (lowercase letters, numbers, hyphens, slashes)."),
|
|
||||||
},
|
|
||||||
outputSchema: {
|
|
||||||
success: z.boolean(),
|
|
||||||
branch: z.string(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
async ({ name }) => {
|
|
||||||
if (!callerAgentId) {
|
|
||||||
throw new AgentMcpToolError(
|
|
||||||
"NOT_ALLOWED",
|
|
||||||
"set_branch can only be called by a managed agent"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const agent = agentManager.getAgent(callerAgentId);
|
|
||||||
if (!agent) {
|
|
||||||
throw new Error(`Agent ${callerAgentId} not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const validation = validateBranchSlug(name);
|
|
||||||
if (!validation.valid) {
|
|
||||||
throw new AgentMcpToolError(
|
|
||||||
"INVALID_BRANCH",
|
|
||||||
validation.error ?? "Invalid branch name"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let ownership;
|
|
||||||
try {
|
|
||||||
ownership = await isPaseoOwnedWorktreeCwd(agent.cwd, { paseoHome: options.paseoHome });
|
|
||||||
} catch (error) {
|
|
||||||
const notGitError =
|
|
||||||
error instanceof NotGitRepoError
|
|
||||||
? error
|
|
||||||
: new NotGitRepoError(agent.cwd);
|
|
||||||
throw new AgentMcpToolError(
|
|
||||||
"NOT_GIT_REPO",
|
|
||||||
notGitError.message
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ownership.allowed) {
|
|
||||||
throw new AgentMcpToolError(
|
|
||||||
"NOT_ALLOWED",
|
|
||||||
"Branch renames are only allowed inside Paseo-owned worktrees"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await renameCurrentBranch(agent.cwd, name);
|
|
||||||
if (result.currentBranch !== name) {
|
|
||||||
throw new Error(
|
|
||||||
`Branch rename failed (expected ${name}, got ${result.currentBranch ?? "unknown"})`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
content: [],
|
|
||||||
structuredContent: ensureValidJson({
|
|
||||||
success: true,
|
|
||||||
branch: name,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
server.registerTool(
|
server.registerTool(
|
||||||
"set_agent_mode",
|
"set_agent_mode",
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ import { AgentManager } from "./agent/agent-manager.js";
|
|||||||
import { AgentStorage } from "./agent/agent-storage.js";
|
import { AgentStorage } from "./agent/agent-storage.js";
|
||||||
import { attachAgentStoragePersistence } from "./persistence-hooks.js";
|
import { attachAgentStoragePersistence } from "./persistence-hooks.js";
|
||||||
import { createAgentMcpServer } from "./agent/mcp-server.js";
|
import { createAgentMcpServer } from "./agent/mcp-server.js";
|
||||||
|
import { createAgentSelfIdMcpServer } from "./agent/agent-self-id-mcp.js";
|
||||||
import { createAllClients, shutdownProviders } from "./agent/provider-registry.js";
|
import { createAllClients, shutdownProviders } from "./agent/provider-registry.js";
|
||||||
import { createTerminalManager, type TerminalManager } from "../terminal/terminal-manager.js";
|
import { createTerminalManager, type TerminalManager } from "../terminal/terminal-manager.js";
|
||||||
import {
|
import {
|
||||||
@@ -54,6 +55,7 @@ import {
|
|||||||
} from "./connection-offer.js";
|
} from "./connection-offer.js";
|
||||||
import { printPairingQrIfEnabled } from "./pairing-qr.js";
|
import { printPairingQrIfEnabled } from "./pairing-qr.js";
|
||||||
import { startRelayTransport, type RelayTransportController } from "./relay-transport.js";
|
import { startRelayTransport, type RelayTransportController } from "./relay-transport.js";
|
||||||
|
import { acquirePidLock, releasePidLock } from "./pid-lock.js";
|
||||||
import type {
|
import type {
|
||||||
AgentClient,
|
AgentClient,
|
||||||
AgentProvider,
|
AgentProvider,
|
||||||
@@ -70,6 +72,7 @@ export type PaseoOpenAIConfig = {
|
|||||||
export type PaseoDaemonConfig = {
|
export type PaseoDaemonConfig = {
|
||||||
listen: string;
|
listen: string;
|
||||||
paseoHome: string;
|
paseoHome: string;
|
||||||
|
selfIdMcpSocketPath: string;
|
||||||
corsAllowedOrigins: string[];
|
corsAllowedOrigins: string[];
|
||||||
agentMcpRoute: string;
|
agentMcpRoute: string;
|
||||||
agentMcpAllowedHosts: string[];
|
agentMcpAllowedHosts: string[];
|
||||||
@@ -209,6 +212,7 @@ export async function createPaseoDaemon(
|
|||||||
...config.agentClients,
|
...config.agentClients,
|
||||||
},
|
},
|
||||||
registry: agentStorage,
|
registry: agentStorage,
|
||||||
|
selfIdMcpSocketPath: config.selfIdMcpSocketPath,
|
||||||
logger,
|
logger,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -221,6 +225,7 @@ export async function createPaseoDaemon(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const agentMcpTransports: AgentMcpTransportMap = new Map();
|
const agentMcpTransports: AgentMcpTransportMap = new Map();
|
||||||
|
const selfIdMcpTransports: AgentMcpTransportMap = new Map();
|
||||||
const allowedHosts = config.agentMcpAllowedHosts;
|
const allowedHosts = config.agentMcpAllowedHosts;
|
||||||
|
|
||||||
const createAgentMcpTransport = async (callerAgentId?: string) => {
|
const createAgentMcpTransport = async (callerAgentId?: string) => {
|
||||||
@@ -328,7 +333,118 @@ export async function createPaseoDaemon(
|
|||||||
app.post(agentMcpRoute, handleAgentMcpRequest);
|
app.post(agentMcpRoute, handleAgentMcpRequest);
|
||||||
app.get(agentMcpRoute, handleAgentMcpRequest);
|
app.get(agentMcpRoute, handleAgentMcpRequest);
|
||||||
app.delete(agentMcpRoute, handleAgentMcpRequest);
|
app.delete(agentMcpRoute, handleAgentMcpRequest);
|
||||||
logger.info({ route: agentMcpRoute }, "Agent MCP server mounted");
|
logger.info({ route: agentMcpRoute }, "Agent MCP server mounted on main app");
|
||||||
|
|
||||||
|
// Create dedicated Self-ID MCP server on Unix socket for agent self-identification
|
||||||
|
// This only provides set_title and set_branch tools for coding agents
|
||||||
|
// Host validation is disabled since Unix sockets don't have HTTP hosts
|
||||||
|
const selfIdMcpSocketPath = config.selfIdMcpSocketPath;
|
||||||
|
|
||||||
|
const createSelfIdMcpTransport = async (callerAgentId: string) => {
|
||||||
|
const selfIdMcpServer = await createAgentSelfIdMcpServer({
|
||||||
|
agentManager,
|
||||||
|
paseoHome: config.paseoHome,
|
||||||
|
callerAgentId,
|
||||||
|
logger,
|
||||||
|
});
|
||||||
|
|
||||||
|
const transport = new StreamableHTTPServerTransport({
|
||||||
|
sessionIdGenerator: () => randomUUID(),
|
||||||
|
onsessioninitialized: (sessionId) => {
|
||||||
|
selfIdMcpTransports.set(sessionId, transport);
|
||||||
|
logger.debug({ sessionId, callerAgentId }, "Self-ID MCP session initialized");
|
||||||
|
},
|
||||||
|
onsessionclosed: (sessionId) => {
|
||||||
|
selfIdMcpTransports.delete(sessionId);
|
||||||
|
logger.debug({ sessionId }, "Self-ID MCP session closed");
|
||||||
|
},
|
||||||
|
// Disable host validation for Unix socket
|
||||||
|
enableDnsRebindingProtection: false,
|
||||||
|
});
|
||||||
|
transport.onclose = () => {
|
||||||
|
if (transport.sessionId) {
|
||||||
|
selfIdMcpTransports.delete(transport.sessionId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
transport.onerror = (err) => {
|
||||||
|
logger.error({ err }, "Self-ID MCP transport error");
|
||||||
|
};
|
||||||
|
|
||||||
|
await selfIdMcpServer.connect(transport);
|
||||||
|
|
||||||
|
return transport;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelfIdMcpRequest: express.RequestHandler = async (req, res) => {
|
||||||
|
if (config.mcpDebug) {
|
||||||
|
logger.debug(
|
||||||
|
{
|
||||||
|
method: req.method,
|
||||||
|
url: req.originalUrl,
|
||||||
|
sessionId: req.header("mcp-session-id"),
|
||||||
|
body: req.body,
|
||||||
|
},
|
||||||
|
"Self-ID MCP request"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const sessionId = req.header("mcp-session-id");
|
||||||
|
let transport = sessionId ? selfIdMcpTransports.get(sessionId) : undefined;
|
||||||
|
|
||||||
|
if (!transport) {
|
||||||
|
if (req.method !== "POST") {
|
||||||
|
res.status(400).json({
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
error: { code: -32000, message: "Missing or invalid MCP session" },
|
||||||
|
id: null,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!isInitializeRequest(req.body)) {
|
||||||
|
res.status(400).json({
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
error: { code: -32000, message: "Initialization request expected" },
|
||||||
|
id: null,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const callerAgentIdRaw = req.query.callerAgentId;
|
||||||
|
const callerAgentId =
|
||||||
|
typeof callerAgentIdRaw === "string"
|
||||||
|
? callerAgentIdRaw
|
||||||
|
: Array.isArray(callerAgentIdRaw) && typeof callerAgentIdRaw[0] === "string"
|
||||||
|
? callerAgentIdRaw[0]
|
||||||
|
: undefined;
|
||||||
|
if (!callerAgentId) {
|
||||||
|
res.status(400).json({
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
error: { code: -32000, message: "callerAgentId query parameter is required for Self-ID MCP" },
|
||||||
|
id: null,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
transport = await createSelfIdMcpTransport(callerAgentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
await transport.handleRequest(req as any, res as any, req.body);
|
||||||
|
} catch (err) {
|
||||||
|
logger.error({ err }, "Failed to handle Self-ID MCP request");
|
||||||
|
if (!res.headersSent) {
|
||||||
|
res.status(500).json({
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
error: { code: -32603, message: "Internal MCP server error" },
|
||||||
|
id: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const selfIdMcpApp = express();
|
||||||
|
selfIdMcpApp.use(express.json());
|
||||||
|
selfIdMcpApp.post("/", handleSelfIdMcpRequest);
|
||||||
|
selfIdMcpApp.get("/", handleSelfIdMcpRequest);
|
||||||
|
selfIdMcpApp.delete("/", handleSelfIdMcpRequest);
|
||||||
|
const selfIdMcpSocketServer = createHTTPServer(selfIdMcpApp);
|
||||||
|
|
||||||
let sttService: OpenAISTT | null = null;
|
let sttService: OpenAISTT | null = null;
|
||||||
let ttsService: OpenAITTS | null = null;
|
let ttsService: OpenAITTS | null = null;
|
||||||
@@ -376,12 +492,38 @@ export async function createPaseoDaemon(
|
|||||||
downloadTokenStore,
|
downloadTokenStore,
|
||||||
config.paseoHome,
|
config.paseoHome,
|
||||||
agentMcpRoute,
|
agentMcpRoute,
|
||||||
|
config.selfIdMcpSocketPath,
|
||||||
{ allowedOrigins },
|
{ allowedOrigins },
|
||||||
{ stt: sttService, tts: ttsService },
|
{ stt: sttService, tts: ttsService },
|
||||||
terminalManager
|
terminalManager
|
||||||
);
|
);
|
||||||
|
|
||||||
const start = async () => {
|
const start = async () => {
|
||||||
|
// Acquire PID lock to prevent multiple daemon instances
|
||||||
|
await acquirePidLock(config.paseoHome, selfIdMcpSocketPath);
|
||||||
|
|
||||||
|
// Start Self-ID MCP socket server first
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
const onError = (err: Error) => {
|
||||||
|
selfIdMcpSocketServer.off("listening", onListening);
|
||||||
|
reject(err);
|
||||||
|
};
|
||||||
|
const onListening = () => {
|
||||||
|
selfIdMcpSocketServer.off("error", onError);
|
||||||
|
logger.info({ path: selfIdMcpSocketPath }, `Self-ID MCP server listening on ${selfIdMcpSocketPath}`);
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
selfIdMcpSocketServer.once("error", onError);
|
||||||
|
selfIdMcpSocketServer.once("listening", onListening);
|
||||||
|
|
||||||
|
// Remove stale socket file if it exists
|
||||||
|
if (existsSync(selfIdMcpSocketPath)) {
|
||||||
|
unlinkSync(selfIdMcpSocketPath);
|
||||||
|
}
|
||||||
|
selfIdMcpSocketServer.listen(selfIdMcpSocketPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start main HTTP server
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
const onError = (err: Error) => {
|
const onError = (err: Error) => {
|
||||||
httpServer.off("listening", onListening);
|
httpServer.off("listening", onListening);
|
||||||
@@ -456,10 +598,18 @@ export async function createPaseoDaemon(
|
|||||||
await new Promise<void>((resolve) => {
|
await new Promise<void>((resolve) => {
|
||||||
httpServer.close(() => resolve());
|
httpServer.close(() => resolve());
|
||||||
});
|
});
|
||||||
// Clean up socket file
|
await new Promise<void>((resolve) => {
|
||||||
|
selfIdMcpSocketServer.close(() => resolve());
|
||||||
|
});
|
||||||
|
// Clean up socket files
|
||||||
if (listenTarget.type === "socket" && existsSync(listenTarget.path)) {
|
if (listenTarget.type === "socket" && existsSync(listenTarget.path)) {
|
||||||
unlinkSync(listenTarget.path);
|
unlinkSync(listenTarget.path);
|
||||||
}
|
}
|
||||||
|
if (existsSync(selfIdMcpSocketPath)) {
|
||||||
|
unlinkSync(selfIdMcpSocketPath);
|
||||||
|
}
|
||||||
|
// Release PID lock
|
||||||
|
await releasePidLock(config.paseoHome);
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -5,11 +5,25 @@ import type { STTConfig } from "./agent/stt-openai.js";
|
|||||||
import type { TTSConfig } from "./agent/tts-openai.js";
|
import type { TTSConfig } from "./agent/tts-openai.js";
|
||||||
import { loadPersistedConfig } from "./persisted-config.js";
|
import { loadPersistedConfig } from "./persisted-config.js";
|
||||||
|
|
||||||
const DEFAULT_LISTEN = "127.0.0.1:6767";
|
const DEFAULT_PORT = 6767;
|
||||||
const DEFAULT_AGENT_MCP_ROUTE = "/mcp/agents";
|
const DEFAULT_AGENT_MCP_ROUTE = "/mcp/agents";
|
||||||
const DEFAULT_RELAY_ENDPOINT = "relay.paseo.sh:443";
|
const DEFAULT_RELAY_ENDPOINT = "relay.paseo.sh:443";
|
||||||
const DEFAULT_APP_BASE_URL = "https://app.paseo.sh";
|
const DEFAULT_APP_BASE_URL = "https://app.paseo.sh";
|
||||||
|
|
||||||
|
function getDefaultListen(): string {
|
||||||
|
// Main HTTP server defaults to TCP
|
||||||
|
return `127.0.0.1:${DEFAULT_PORT}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSelfIdMcpSocketPath(paseoHome: string, env: NodeJS.ProcessEnv): string {
|
||||||
|
// Allow override via PASEO_SELF_ID_MCP_SOCK for testing
|
||||||
|
if (env.PASEO_SELF_ID_MCP_SOCK) {
|
||||||
|
return env.PASEO_SELF_ID_MCP_SOCK;
|
||||||
|
}
|
||||||
|
// Default to ${PASEO_HOME}/self-id-mcp.sock
|
||||||
|
return path.join(paseoHome, "self-id-mcp.sock");
|
||||||
|
}
|
||||||
|
|
||||||
function parseOpenAIConfig(env: NodeJS.ProcessEnv) {
|
function parseOpenAIConfig(env: NodeJS.ProcessEnv) {
|
||||||
const apiKey = env.OPENAI_API_KEY;
|
const apiKey = env.OPENAI_API_KEY;
|
||||||
if (!apiKey) return undefined;
|
if (!apiKey) return undefined;
|
||||||
@@ -66,8 +80,14 @@ export function loadConfig(
|
|||||||
): PaseoDaemonConfig {
|
): PaseoDaemonConfig {
|
||||||
const persisted = loadPersistedConfig(paseoHome);
|
const persisted = loadPersistedConfig(paseoHome);
|
||||||
|
|
||||||
const listen = env.PASEO_LISTEN ?? persisted.listen ?? DEFAULT_LISTEN;
|
// PASEO_LISTEN can be:
|
||||||
|
// - host:port (TCP)
|
||||||
|
// - /path/to/socket (Unix socket)
|
||||||
|
// - unix:///path/to/socket (Unix socket)
|
||||||
|
// Default is TCP at 127.0.0.1:6767
|
||||||
|
const listen = env.PASEO_LISTEN ?? persisted.listen ?? getDefaultListen();
|
||||||
const mcpListen = getListenForMcp(listen);
|
const mcpListen = getListenForMcp(listen);
|
||||||
|
const selfIdMcpSocketPath = getSelfIdMcpSocketPath(paseoHome, env);
|
||||||
|
|
||||||
const envCorsOrigins = env.PASEO_CORS_ORIGINS
|
const envCorsOrigins = env.PASEO_CORS_ORIGINS
|
||||||
? env.PASEO_CORS_ORIGINS.split(",").map((s) => s.trim())
|
? env.PASEO_CORS_ORIGINS.split(",").map((s) => s.trim())
|
||||||
@@ -76,6 +96,7 @@ export function loadConfig(
|
|||||||
return {
|
return {
|
||||||
listen,
|
listen,
|
||||||
paseoHome,
|
paseoHome,
|
||||||
|
selfIdMcpSocketPath,
|
||||||
corsAllowedOrigins: [...persisted.cors.allowedOrigins, ...envCorsOrigins],
|
corsAllowedOrigins: [...persisted.cors.allowedOrigins, ...envCorsOrigins],
|
||||||
agentMcpRoute: DEFAULT_AGENT_MCP_ROUTE,
|
agentMcpRoute: DEFAULT_AGENT_MCP_ROUTE,
|
||||||
agentMcpAllowedHosts: [mcpListen, `localhost:${mcpListen.split(":")[1]}`],
|
agentMcpAllowedHosts: [mcpListen, `localhost:${mcpListen.split(":")[1]}`],
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export class VoiceAssistantWebSocketServer {
|
|||||||
downloadTokenStore: DownloadTokenStore,
|
downloadTokenStore: DownloadTokenStore,
|
||||||
paseoHome: string,
|
paseoHome: string,
|
||||||
agentMcpRoute: string,
|
agentMcpRoute: string,
|
||||||
|
selfIdMcpSocketPath: string,
|
||||||
wsConfig: WebSocketServerConfig,
|
wsConfig: WebSocketServerConfig,
|
||||||
speech?: { stt: OpenAISTT | null; tts: OpenAITTS | null },
|
speech?: { stt: OpenAISTT | null; tts: OpenAITTS | null },
|
||||||
terminalManager?: TerminalManager | null
|
terminalManager?: TerminalManager | null
|
||||||
@@ -42,6 +43,7 @@ export class VoiceAssistantWebSocketServer {
|
|||||||
downloadTokenStore,
|
downloadTokenStore,
|
||||||
paseoHome,
|
paseoHome,
|
||||||
agentMcpRoute,
|
agentMcpRoute,
|
||||||
|
selfIdMcpSocketPath,
|
||||||
speech,
|
speech,
|
||||||
terminalManager
|
terminalManager
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export class WebSocketSessionBridge {
|
|||||||
private readonly pushTokenStore: PushTokenStore;
|
private readonly pushTokenStore: PushTokenStore;
|
||||||
private readonly pushService: PushService;
|
private readonly pushService: PushService;
|
||||||
private readonly agentMcpRoute: string;
|
private readonly agentMcpRoute: string;
|
||||||
|
private readonly selfIdMcpSocketPath: string;
|
||||||
private readonly stt: OpenAISTT | null;
|
private readonly stt: OpenAISTT | null;
|
||||||
private readonly tts: OpenAITTS | null;
|
private readonly tts: OpenAITTS | null;
|
||||||
private readonly terminalManager: TerminalManager | null;
|
private readonly terminalManager: TerminalManager | null;
|
||||||
@@ -42,6 +43,7 @@ export class WebSocketSessionBridge {
|
|||||||
downloadTokenStore: DownloadTokenStore,
|
downloadTokenStore: DownloadTokenStore,
|
||||||
paseoHome: string,
|
paseoHome: string,
|
||||||
agentMcpRoute: string,
|
agentMcpRoute: string,
|
||||||
|
selfIdMcpSocketPath: string,
|
||||||
speech?: { stt: OpenAISTT | null; tts: OpenAITTS | null },
|
speech?: { stt: OpenAISTT | null; tts: OpenAITTS | null },
|
||||||
terminalManager?: TerminalManager | null
|
terminalManager?: TerminalManager | null
|
||||||
) {
|
) {
|
||||||
@@ -51,6 +53,7 @@ export class WebSocketSessionBridge {
|
|||||||
this.downloadTokenStore = downloadTokenStore;
|
this.downloadTokenStore = downloadTokenStore;
|
||||||
this.paseoHome = paseoHome;
|
this.paseoHome = paseoHome;
|
||||||
this.agentMcpRoute = agentMcpRoute;
|
this.agentMcpRoute = agentMcpRoute;
|
||||||
|
this.selfIdMcpSocketPath = selfIdMcpSocketPath;
|
||||||
this.stt = speech?.stt ?? null;
|
this.stt = speech?.stt ?? null;
|
||||||
this.tts = speech?.tts ?? null;
|
this.tts = speech?.tts ?? null;
|
||||||
this.terminalManager = terminalManager ?? null;
|
this.terminalManager = terminalManager ?? null;
|
||||||
@@ -87,6 +90,7 @@ export class WebSocketSessionBridge {
|
|||||||
this.agentManager,
|
this.agentManager,
|
||||||
this.agentStorage,
|
this.agentStorage,
|
||||||
this.agentMcpRoute,
|
this.agentMcpRoute,
|
||||||
|
this.selfIdMcpSocketPath,
|
||||||
this.stt,
|
this.stt,
|
||||||
this.tts,
|
this.tts,
|
||||||
this.terminalManager,
|
this.terminalManager,
|
||||||
|
|||||||
Reference in New Issue
Block a user