Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
da2ce46
refactor(monitoring): translate log-row comment to English for code c…
JonasJesus42 Jan 29, 2026
1cfa8f0
fix(connections): always call ON_MCP_CONFIGURATION when updating conn…
JonasJesus42 Jan 29, 2026
f2d0321
feat(mesh): adiciona logs detalhados de validação no endpoint decopilot
JonasJesus42 Feb 2, 2026
4d9bc88
Updated agent view (#2283)
rafavalls Jan 29, 2026
d5b0d17
[release]: bump to 2.59.3
github-actions[bot] Jan 29, 2026
a6ce759
fix /mcp (#2359)
tlgimenes Jan 29, 2026
44d72b7
[release]: bump to 2.59.4
github-actions[bot] Jan 29, 2026
a615df0
Refactor virtual MCP schemas and improve metadata handling (#2358)
tlgimenes Jan 29, 2026
e072084
[release]: bump to 2.59.5
github-actions[bot] Jan 29, 2026
34ab7aa
fix: error boundary reset on select other (#2360)
tlgimenes Jan 29, 2026
193e936
[release]: bump to 2.59.6
github-actions[bot] Jan 29, 2026
0cbd51f
fix types (#2362)
tlgimenes Jan 30, 2026
5785464
[release]: bump to 2.59.7
github-actions[bot] Jan 30, 2026
f49e5b7
feat(sidebar): add account switcher and inbox components (#2363)
tlgimenes Jan 30, 2026
3733bb7
[release]: bump to 2.60.0
github-actions[bot] Jan 30, 2026
3dffc72
feat(ui): add breadcrumb navigation and improve table headers (#2364)
tlgimenes Jan 30, 2026
bd763fb
[release]: bump to 2.61.0
github-actions[bot] Jan 30, 2026
b662ba0
feat(ui): unify save button UX across detail pages (#2365)
tlgimenes Jan 30, 2026
4e62ee2
[release]: bump to 2.62.0
github-actions[bot] Jan 30, 2026
0c5a6ff
feat(ui): Add gray sidebar and chat panel styles (#2366)
tlgimenes Jan 30, 2026
35c6111
[release]: bump to 2.63.0
github-actions[bot] Jan 30, 2026
c2ed2fd
fix(sidebar): remove profile and security menu items, fix preferences…
tlgimenes Jan 30, 2026
3d5c47d
[release]: bump to 2.63.1
github-actions[bot] Jan 30, 2026
8090c36
feat(chat): enhance ThoughtSummary streaming UX with auto-scroll and …
tlgimenes Jan 30, 2026
bb09576
[release]: bump to 2.64.0
github-actions[bot] Jan 30, 2026
a30ae08
fix(sidebar): prevent organization button and chevron from appearing …
tlgimenes Jan 30, 2026
13ff18e
[release]: bump to 2.64.1
github-actions[bot] Jan 30, 2026
93be77d
[feat]: Virtual tools (#2351)
mcandeia Feb 2, 2026
0266899
Revert "[feat]: Virtual tools (#2351)" (#2372)
pedrofrxncx Feb 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/mesh/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@decocms/mesh",
"version": "2.59.2",
"version": "2.64.1",
"description": "MCP Mesh - Self-hostable MCP Gateway for managing AI connections and tools",
"author": "Deco team",
"license": "MIT",
Expand Down
23 changes: 16 additions & 7 deletions apps/mesh/src/api/routes/decopilot/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import { Hono } from "hono";
import { HTTPException } from "hono/http-exception";

import type { MeshContext } from "@/core/mesh-context";
import { createVirtualClientFrom } from "@/mcp-clients/virtual-mcp";
import { generatePrefixedId } from "@/shared/utils/generate-id";

import { Metadata } from "@/web/components/chat/types";
import {
DECOPILOT_BASE_PROMPT,
Expand All @@ -24,7 +24,6 @@ import { ensureOrganization, toolsFromMCP } from "./helpers";
import { createModelProviderFromProxy } from "./model-provider";
import { StreamRequestSchema } from "./schemas";
import { generateTitleInBackground } from "./title-generator";
import { createVirtualClientFrom } from "@/mcp-clients/virtual-mcp";

// ============================================================================
// Request Validation
Expand All @@ -38,7 +37,18 @@ async function validateRequest(

const parseResult = StreamRequestSchema.safeParse(rawPayload);
if (!parseResult.success) {
throw new HTTPException(400, { message: "Invalid request body" });
// Log detalhes do erro de validação para debug
console.error("[Decopilot] ❌ Request validation failed:");
console.error(
"Validation errors:",
JSON.stringify(parseResult.error.format(), null, 2),
);
console.error("Raw payload:", JSON.stringify(rawPayload, null, 2));
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Avoid logging the full raw payload on validation errors because it can expose sensitive data in logs. Log only validation errors or redact the payload.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/api/routes/decopilot/routes.ts, line 47:

<comment>Avoid logging the full raw payload on validation errors because it can expose sensitive data in logs. Log only validation errors or redact the payload.</comment>

<file context>
@@ -38,7 +38,18 @@ async function validateRequest(
+      "Validation errors:",
+      JSON.stringify(parseResult.error.format(), null, 2),
+    );
+    console.error("Raw payload:", JSON.stringify(rawPayload, null, 2));
+
+    throw new HTTPException(400, {
</file context>
Suggested change
console.error("Raw payload:", JSON.stringify(rawPayload, null, 2));
console.error("Raw payload:", "[redacted]");
Fix with Cubic


throw new HTTPException(400, {
message: "Invalid request body",
cause: parseResult.error.format(),
});
}

return {
Expand Down Expand Up @@ -72,9 +82,8 @@ app.post("/:org/decopilot/stream", async (c) => {
const threadId = thread_id ?? memoryConfig?.threadId;

// Create virtual MCP client and model provider in parallel
// For virtual MCPs (agents), we need to use storage.virtualMcps.findById which handles null
const [virtualMcp, modelClient] = await Promise.all([
ctx.storage.virtualMcps.findById(agent.id ?? null, organization.id),
ctx.storage.virtualMcps.findById(agent.id, organization.id),
ctx.createMCPProxy(model.connectionId),
]);

Expand All @@ -85,7 +94,7 @@ app.post("/:org/decopilot/stream", async (c) => {
const mcpClient = await createVirtualClientFrom(
virtualMcp,
ctx,
"code_execution",
agent.mode,
);

// 2. Extract tools from virtual MCP client and create model provider
Expand Down Expand Up @@ -177,7 +186,7 @@ app.post("/:org/decopilot/stream", async (c) => {
messageMetadata: ({ part }): Metadata => {
if (part.type === "start") {
return {
agent: { id: agent.id ?? null },
agent: { id: agent.id ?? null, mode: agent.mode },
model: { id: model.id, connectionId: model.connectionId },
created_at: new Date(),
thread_id: memory.thread.id,
Expand Down
7 changes: 6 additions & 1 deletion apps/mesh/src/api/routes/decopilot/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,12 @@ export const StreamRequestSchema = z.object({
.optional(),
})
.loose(),
agent: z.object({ id: z.string() }).loose(),
agent: z
.object({
id: z.string(),
mode: z.enum(["passthrough", "smart_tool_selection", "code_execution"]),
})
.loose(),
stream: z.boolean().optional(),
temperature: z.number().default(0.5),
thread_id: z.string().optional(),
Expand Down
24 changes: 13 additions & 11 deletions apps/mesh/src/api/routes/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,12 @@ export function toServerClient(client: MCPProxyClient): ServerClient {
};
}

const DEFAULT_SERVER_CAPABILITIES = {
tools: {},
resources: {},
prompts: {},
};

async function createMCPProxyDoNotUseDirectly(
connectionIdOrConnection: string | ConnectionEntity,
ctx: MeshContext,
Expand Down Expand Up @@ -558,6 +564,11 @@ async function createMCPProxyDoNotUseDirectly(
params: GetPromptRequest["params"],
): Promise<GetPromptResult> => client.getPrompt(params);

// We are currently exposing the underlying client with tools/resources/prompts capabilities
// This way we have an uniform API the frontend can leverage from.
// Frontend connects to mesh. It's garatee that all mcps have the necessary capabilities. The UI works consistently.
const getServerCapabilities = () => DEFAULT_SERVER_CAPABILITIES;

return {
callTool: (params: CallToolRequest["params"]) =>
executeToolCall({
Expand All @@ -570,7 +581,7 @@ async function createMCPProxyDoNotUseDirectly(
listResourceTemplates,
listPrompts,
getPrompt,
getServerCapabilities: () => client.getServerCapabilities(),
getServerCapabilities,
getInstructions: () => client.getInstructions(),
close: () => client.close(),
callStreamableTool,
Expand Down Expand Up @@ -653,16 +664,7 @@ app.all("/:connectionId", async (c) => {
await server.connect(transport);

// Handle request and cleanup
try {
return await transport.handleRequest(c.req.raw);
} finally {
// Close the transport
try {
await transport.close?.();
} catch {
// Ignore close errors - transport may already be closed
}
}
return await transport.handleRequest(c.req.raw);
} catch (error) {
// Check if this is an auth error - if so, return appropriate 401
// Note: This only applies to HTTP connections
Expand Down
16 changes: 13 additions & 3 deletions apps/mesh/src/api/routes/virtual-mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* - Supports exclusion strategy for inverse tool selection
*/

import { createServerFromClient } from "@decocms/mesh-sdk";
import { createServerFromClient, getDecopilotId } from "@decocms/mesh-sdk";
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
import { Hono } from "hono";
import type { MeshContext } from "../../core/mesh-context";
Expand Down Expand Up @@ -62,8 +62,18 @@ export async function handleVirtualMcpRequest(
.then((org) => org?.id)
: null;

const virtualId = virtualMcpId
? virtualMcpId
: organizationId
? getDecopilotId(organizationId)
: null;

if (!virtualId) {
return c.json({ error: "Agent ID or organization ID is required" }, 400);
}

const virtualMcp = await ctx.storage.virtualMcps.findById(
virtualMcpId ?? null,
virtualId,
organizationId ?? undefined,
);

Expand All @@ -85,6 +95,7 @@ export async function handleVirtualMcpRequest(
}

// Set connection context (Virtual MCPs are now connections)
// Note: virtualMcp.id can be null for Decopilot agent, but connectionId should be set for routing
ctx.connectionId = virtualMcp.id ?? undefined;

// Set organization context
Expand Down Expand Up @@ -130,7 +141,6 @@ export async function handleVirtualMcpRequest(
// Connect server to transport
await server.connect(transport);

// Handle the incoming MCP message
return await transport.handleRequest(c.req.raw);
} catch (error) {
const err = error as Error;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,6 @@ export class PassthroughClient extends Client {
* Get server instructions from virtual MCP metadata
*/
override getInstructions(): string | undefined {
return this.options.virtualMcp.metadata?.instructions;
return this.options.virtualMcp.metadata?.instructions ?? undefined;
}
}
21 changes: 4 additions & 17 deletions apps/mesh/src/storage/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,23 +102,10 @@ export class ConnectionStorage implements ConnectionStoragePort {
organizationId?: string,
): Promise<ConnectionEntity | null> {
// Handle Decopilot ID - return Decopilot connection entity
if (isDecopilot(id)) {
if (!organizationId) {
// Extract orgId from decopilot_{orgId} pattern
const orgIdMatch = id.match(/^decopilot_(.+)$/);
if (!orgIdMatch) {
throw new Error(
`Invalid Decopilot ID format: ${id}. Expected decopilot_{orgId}`,
);
}
organizationId = orgIdMatch[1];
}
if (!organizationId) {
throw new Error(
`Organization ID is required for Decopilot connection: ${id}`,
);
}
return getWellKnownDecopilotConnection(organizationId);
const decopilotOrgId = isDecopilot(id);
if (decopilotOrgId) {
const resolvedOrgId = organizationId ?? decopilotOrgId;
return getWellKnownDecopilotConnection(resolvedOrgId);
}

let query = this.db
Expand Down
2 changes: 1 addition & 1 deletion apps/mesh/src/storage/ports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ export interface VirtualMCPStoragePort {
data: VirtualMCPCreateData,
): Promise<VirtualMCPEntity>;
findById(
id: string | null,
id: string,
organizationId?: string,
): Promise<VirtualMCPEntity | null>;
list(organizationId: string): Promise<VirtualMCPEntity[]>;
Expand Down
54 changes: 10 additions & 44 deletions apps/mesh/src/storage/virtual.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,23 +118,13 @@ export class VirtualMCPStorage implements VirtualMCPStoragePort {
}

async findById(
id: string | null,
id: string,
organizationId?: string,
): Promise<VirtualMCPEntity | null> {
// Handle Decopilot ID - return Decopilot agent with all org connections
if (id && isDecopilot(id)) {
// Extract orgId from decopilot_{orgId} pattern or use provided organizationId
const orgIdMatch = id.match(/^decopilot_(.+)$/);
let resolvedOrgId: string;
if (organizationId) {
resolvedOrgId = organizationId;
} else if (orgIdMatch && orgIdMatch[1]) {
resolvedOrgId = orgIdMatch[1];
} else {
throw new Error(
`Invalid Decopilot ID format: ${id}. Expected decopilot_{orgId} or provide organizationId`,
);
}
const decopilotOrgId = isDecopilot(id);
if (decopilotOrgId) {
const resolvedOrgId = organizationId ?? decopilotOrgId;

// Get all active connections for the organization
const connections = await this.db
Expand All @@ -157,35 +147,6 @@ export class VirtualMCPStorage implements VirtualMCPStoragePort {
};
}

// Handle null ID - treat as Decopilot for backward compatibility
if (!id) {
if (!organizationId) {
throw new Error(
"organizationId is required when id is null (Decopilot agent)",
);
}

// Get all active connections for the organization
const connections = await this.db
.selectFrom("connections")
.selectAll()
.where("organization_id", "=", organizationId)
.where("status", "!=", "inactive")
.where("status", "!=", "error")
.execute();

// Return Decopilot agent with connections populated
return {
...getWellKnownDecopilotVirtualMCP(organizationId),
connections: connections.map((c) => ({
connection_id: c.id,
selected_tools: null, // null = all tools
selected_resources: null, // null = all resources
selected_prompts: null, // null = all prompts
})),
};
}

// Normal database lookup for string IDs
return this.findByIdInternal(this.db, id);
}
Expand Down Expand Up @@ -422,6 +383,8 @@ export class VirtualMCPStorage implements VirtualMCPStoragePort {
const status: "active" | "inactive" =
row.status === "active" ? "active" : "inactive";

const metadata = this.parseJson<{ instructions?: string }>(row.metadata);

return {
id: row.id,
organization_id: row.organization_id,
Expand All @@ -433,7 +396,10 @@ export class VirtualMCPStorage implements VirtualMCPStoragePort {
updated_at: updatedAt,
created_by: row.created_by,
updated_by: undefined, // connections table doesn't have updated_by
metadata: this.parseJson<{ instructions?: string }>(row.metadata),
metadata: {
...metadata,
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Spreading metadata can throw when parseJson returns null. Guard the spread with a fallback object to avoid runtime errors when metadata is null.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/storage/virtual.ts, line 400:

<comment>Spreading `metadata` can throw when parseJson returns null. Guard the spread with a fallback object to avoid runtime errors when metadata is null.</comment>

<file context>
@@ -433,7 +396,10 @@ export class VirtualMCPStorage implements VirtualMCPStoragePort {
       updated_by: undefined, // connections table doesn't have updated_by
-      metadata: this.parseJson<{ instructions?: string }>(row.metadata),
+      metadata: {
+        ...metadata,
+        instructions: metadata?.instructions ?? null,
+      },
</file context>
Suggested change
...metadata,
...(metadata ?? {}),
Fix with Cubic

instructions: metadata?.instructions ?? null,
},
connections: aggregationRows.map((agg) => ({
connection_id: agg.child_connection_id,
selected_tools: this.parseJson<string[]>(agg.selected_tools),
Expand Down
Loading