Feat: single mcp server for all integrations

This commit is contained in:
Harshith Mullapudi 2025-08-24 10:57:20 +05:30
parent 9d34e5d926
commit 256cdb8bdc
30 changed files with 669 additions and 1192 deletions

View File

@ -1,4 +1,4 @@
VERSION=0.1.17
VERSION=0.1.18
# Nest run in docker, change host to database container name
DB_HOST=localhost

View File

@ -30,41 +30,6 @@ export function SpaceSearch({
}
}, [debouncedSearchQuery, searchQuery, onSearchChange]);
// Count statement nodes that match the search
const matchingStatements = useMemo(() => {
if (!debouncedSearchQuery.trim()) return 0;
const query = debouncedSearchQuery.toLowerCase();
let count = 0;
const isStatementNode = (node: any) => {
return (
node.attributes?.fact ||
(node.labels && node.labels.includes("Statement"))
);
};
triplets.forEach((triplet) => {
// Check if source node is a statement and matches
if (
isStatementNode(triplet.sourceNode) &&
triplet.sourceNode.attributes?.fact?.toLowerCase().includes(query)
) {
count++;
}
// Check if target node is a statement and matches
if (
isStatementNode(triplet.targetNode) &&
triplet.targetNode.attributes?.fact?.toLowerCase().includes(query)
) {
count++;
}
});
return count;
}, [triplets, debouncedSearchQuery]);
// Helper to determine if a node is a statement
const isStatementNode = useCallback((node: any) => {
// Check if node has a fact attribute (indicates it's a statement)
@ -74,6 +39,34 @@ export function SpaceSearch({
);
}, []);
// Count statement nodes that match the search
const matchingStatements = useMemo(() => {
if (!debouncedSearchQuery.trim()) return 0;
const query = debouncedSearchQuery.toLowerCase();
const statements: Record<string, number> = {};
triplets.forEach((triplet) => {
// Check if source node is a statement and matches
if (
isStatementNode(triplet.sourceNode) &&
triplet.sourceNode.attributes?.fact?.toLowerCase().includes(query)
) {
statements[triplet.sourceNode.uuid] = 1;
}
// Check if target node is a statement and matches
if (
isStatementNode(triplet.targetNode) &&
triplet.targetNode.attributes?.fact?.toLowerCase().includes(query)
) {
statements[triplet.targetNode.uuid] = 1;
}
});
return Object.keys(statements).length;
}, [triplets, debouncedSearchQuery]);
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(event.target.value);
};

View File

@ -1,8 +1,7 @@
import React, { useCallback, useState } from "react";
import { useFetcher } from "@remix-run/react";
import { Button } from "~/components/ui/button";
import { Check, Copy } from "lucide-react";
import { Input } from "../ui/input";
import { Check } from "lucide-react";
interface MCPAuthSectionProps {
integration: {
@ -19,49 +18,6 @@ interface MCPAuthSectionProps {
hasMCPAuth: boolean;
}
interface MCPUrlBoxProps {
mcpUrl: string;
}
function MCPUrlBox({ mcpUrl }: MCPUrlBoxProps) {
const [copied, setCopied] = useState(false);
const handleCopy = useCallback(() => {
navigator.clipboard.writeText(mcpUrl).then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
});
}, [mcpUrl]);
return (
<div className="mb-3 flex items-center gap-2">
<Input
type="text"
value={mcpUrl}
readOnly
className="w-full rounded px-2 py-1 font-mono text-sm"
style={{ maxWidth: 400 }}
onFocus={(e) => e.target.select()}
/>
<Button
type="button"
variant={copied ? "secondary" : "ghost"}
onClick={handleCopy}
aria-label={copied ? "Copied" : "Copy MCP URL"}
disabled={copied}
>
{copied ? (
<span className="flex items-center gap-1">
<Check size={16} /> Copied
</span>
) : (
<Copy size={16} />
)}
</Button>
</div>
);
}
export function MCPAuthSection({
integration,
activeAccount,
@ -74,8 +30,6 @@ export function MCPAuthSection({
const isMCPConnected = !!activeAccount?.integrationConfiguration?.mcp;
const isConnected = !!activeAccount;
const mcpUrl = `https://core.heysol.ai/api/v1/mcp/${integration.slug}`;
const handleMCPConnect = useCallback(() => {
setIsMCPConnecting(true);
mcpFetcher.submit(
@ -135,8 +89,8 @@ export function MCPAuthSection({
<div className="mt-6 space-y-2">
<h3 className="text-lg font-medium">MCP Authentication</h3>
{hasMCPAuth ? (
isMCPConnected ? (
{hasMCPAuth &&
(isMCPConnected ? (
<div className="bg-background-3 rounded-lg p-4">
<div className="text-sm">
<p className="inline-flex items-center gap-2 font-medium">
@ -145,7 +99,6 @@ export function MCPAuthSection({
<p className="text-muted-foreground mb-3">
MCP (Model Context Protocol) authentication is active
</p>
<MCPUrlBox mcpUrl={mcpUrl} />
<div className="flex w-full justify-end">
<Button
variant="destructive"
@ -182,21 +135,7 @@ export function MCPAuthSection({
</Button>
</div>
</div>
)
) : (
// hasMCPAuth is false, but integration is connected: show just the MCPUrlBox
<div className="bg-background-3 rounded-lg p-4">
<div className="text-sm">
<p className="inline-flex items-center gap-2 font-medium">
<Check size={16} /> Integration Connected
</p>
<p className="text-muted-foreground mb-3">
You can use the MCP endpoint for this integration:
</p>
<MCPUrlBox mcpUrl={mcpUrl} />
</div>
</div>
)}
))}
</div>
);
}

View File

@ -39,7 +39,7 @@ export function SpaceFactCard({ fact }: SpaceFactCardProps) {
<div className="inline-flex min-h-[24px] min-w-[0px] shrink cursor-pointer items-center justify-start">
<div className={cn("truncate text-left")}>{displayText}</div>
</div>
<div className="text-muted-foreground flex shrink-0 items-center justify-end text-xs">
<div className="text-muted-foreground flex shrink-0 items-center justify-end gap-2 text-xs">
{!!recallCount && <span>Recalled: {recallCount} times</span>}
<Badge variant="secondary" className="rounded text-xs">
<Calendar className="h-3 w-3" />

View File

@ -1,12 +1,4 @@
import {
Prisma,
PrismaClient,
type PrismaClientOrTransaction,
type PrismaReplicaClient,
type PrismaTransactionClient,
type PrismaTransactionOptions,
$transaction as transac,
} from "@core/database";
import { Prisma, PrismaClient } from "@core/database";
import invariant from "tiny-invariant";
import { z } from "zod";
import { env } from "./env.server";
@ -16,72 +8,11 @@ import { singleton } from "./utils/singleton";
import { type Span } from "@opentelemetry/api";
export type {
PrismaTransactionClient,
PrismaClientOrTransaction,
PrismaTransactionOptions,
PrismaReplicaClient,
};
export async function $transaction<R>(
prisma: PrismaClientOrTransaction,
name: string,
fn: (prisma: PrismaTransactionClient, span?: Span) => Promise<R>,
options?: PrismaTransactionOptions,
): Promise<R | undefined>;
export async function $transaction<R>(
prisma: PrismaClientOrTransaction,
fn: (prisma: PrismaTransactionClient) => Promise<R>,
options?: PrismaTransactionOptions,
): Promise<R | undefined>;
export async function $transaction<R>(
prisma: PrismaClientOrTransaction,
fnOrName: ((prisma: PrismaTransactionClient) => Promise<R>) | string,
fnOrOptions?:
| ((prisma: PrismaTransactionClient) => Promise<R>)
| PrismaTransactionOptions,
options?: PrismaTransactionOptions,
): Promise<R | undefined> {
if (typeof fnOrName === "string") {
const fn = fnOrOptions as (prisma: PrismaTransactionClient) => Promise<R>;
return await transac(
prisma,
(client) => fn(client),
(error) => {
logger.error("prisma.$transaction error", {
code: error.code,
meta: error.meta,
stack: error.stack,
message: error.message,
name: error.name,
});
},
options,
);
} else {
return transac(
prisma,
fnOrName,
(error) => {
logger.error("prisma.$transaction error", {
code: error.code,
meta: error.meta,
stack: error.stack,
message: error.message,
name: error.name,
});
},
typeof fnOrOptions === "function" ? undefined : fnOrOptions,
);
}
}
export { Prisma };
export const prisma = singleton("prisma", getClient);
export const $replica: PrismaReplicaClient = singleton(
export const $replica = singleton(
"replica",
() => getReplicaClient() ?? prisma,
);

View File

@ -15,6 +15,8 @@ import { RemixServer } from "@remix-run/react";
import { isbot } from "isbot";
import { renderToPipeableStream } from "react-dom/server";
import { initializeStartupServices } from "./utils/startup";
import { handleMCPRequest, handleSessionRequest } from "~/services/mcp.server";
import { authenticateHybridRequest } from "~/services/routeBuilders/apiBuilder.server";
const ABORT_DELAY = 5_000;
@ -149,3 +151,5 @@ function handleBrowserRequest(
setTimeout(abort, ABORT_DELAY);
});
}
export { handleMCPRequest, handleSessionRequest, authenticateHybridRequest };

View File

@ -1,33 +1,12 @@
import { z } from "zod";
import { createActionApiRoute } from "~/services/routeBuilders/apiBuilder.server";
import { json } from "@remix-run/node";
import { searchMemoryAgent } from "~/agents/searchMemoryAgent.server";
import { extensionSearch } from "~/trigger/extension/search";
export const ExtensionSearchBodyRequest = z.object({
input: z.string().min(1, "Input text is required"),
});
/**
* Generate context summary from user input using SearchMemoryAgent
*/
async function generateContextSummary(
userInput: string,
userId: string,
): Promise<string> {
try {
const summary = await searchMemoryAgent.generateContextSummary({
userInput,
userId,
});
return summary;
} catch (error) {
console.error("Error generating context with agent:", error);
// Fallback: use simple context description
return `Context related to: ${userInput}. Looking for relevant background information, previous discussions, and related concepts that would help provide a comprehensive answer.`;
}
}
const { action, loader } = createActionApiRoute(
{
body: ExtensionSearchBodyRequest,
@ -39,18 +18,12 @@ const { action, loader } = createActionApiRoute(
corsStrategy: "all",
},
async ({ body, authentication }) => {
// Generate context summary using SearchMemoryAgent
const contextSummary = await generateContextSummary(
body.input,
authentication.userId,
);
const trigger = await extensionSearch.trigger({
userInput: body.input,
userId: authentication.userId,
});
// Return results with agent-generated context summary
const finalResults = {
context_summary: contextSummary, // Agent's context summary
};
return json(finalResults);
return json(trigger);
},
);

View File

@ -1,9 +1,7 @@
import { z } from "zod";
import { createActionApiRoute } from "~/services/routeBuilders/apiBuilder.server";
import { makeModelCall } from "~/lib/model.server";
import { json } from "@remix-run/node";
import type { CoreMessage } from "ai";
import * as cheerio from "cheerio";
import { extensionSummary } from "~/trigger/extension/summary";
export const ExtensionSummaryBodyRequest = z.object({
html: z.string().min(1, "HTML content is required"),
@ -11,163 +9,6 @@ export const ExtensionSummaryBodyRequest = z.object({
title: z.string().optional(),
});
export type PageType = "text" | "video";
interface ContentExtractionResult {
pageType: PageType;
title: string;
content: string;
metadata: {
url: string;
wordCount: number;
};
supported: boolean;
}
/**
* Detect if page contains video content
*/
function isVideoPage(url: string, $: cheerio.CheerioAPI): boolean {
const hostname = new URL(url).hostname.toLowerCase();
// Known video platforms
if (
hostname.includes("youtube.com") ||
hostname.includes("youtu.be") ||
hostname.includes("vimeo.com") ||
hostname.includes("twitch.tv") ||
hostname.includes("tiktok.com")
) {
return true;
}
// Generic video content detection
const videoElements = $("video").length;
const videoPlayers = $(
'.video-player, [class*="video-player"], [data-testid*="video"]',
).length;
// If there are multiple video indicators, likely a video-focused page
return videoElements > 0 || videoPlayers > 2;
}
/**
* Extract all text content from any webpage
*/
function extractTextContent(
$: cheerio.CheerioAPI,
url: string,
): ContentExtractionResult {
// Extract title from multiple possible locations
const title =
$("title").text() ||
$('meta[property="og:title"]').attr("content") ||
$('meta[name="title"]').attr("content") ||
$("h1").first().text() ||
"Untitled Page";
// Check if this is primarily a video page
const isVideo = isVideoPage(url, $);
const pageType: PageType = isVideo ? "video" : "text";
let content = "";
if (isVideo) {
// For video pages, try to get description/transcript text
content =
$("#description, .video-description, .description").text() ||
$('meta[name="description"]').attr("content") ||
$('[class*="transcript"], [class*="caption"]').text() ||
"Video content detected - text summarization not available";
} else {
// Simple universal text extraction
// Remove non-content elements
$("script, style, noscript, nav, header, footer").remove();
// Get all text content
const allText = $("body").text();
// Split into sentences and filter for meaningful content
const sentences = allText
.split(/[.!?]+/)
.map((s) => s.trim())
.filter((s) => s.length > 20) // Keep sentences with substance
.filter(
(s) =>
!/^(click|menu|button|nav|home|search|login|signup|subscribe)$/i.test(
s.toLowerCase(),
),
) // Remove UI text
.filter((s) => s.split(" ").length > 3); // Keep sentences with multiple words
content = sentences.join(". ").slice(0, 10000);
}
// Clean up whitespace and normalize text
content = content.replace(/\s+/g, " ").trim();
const wordCount = content
.split(/\s+/)
.filter((word) => word.length > 0).length;
const supported = !isVideo && content.length > 50;
return {
pageType,
title: title.trim(),
content: content.slice(0, 10000), // Limit content size for processing
metadata: {
url,
wordCount,
},
supported,
};
}
/**
* Generate summary using LLM
*/
async function generateSummary(
title: string,
content: string,
): Promise<string> {
const messages: CoreMessage[] = [
{
role: "system",
content: `You are a helpful assistant that creates concise summaries of web content in HTML format.
Create a clear, informative summary that captures the key points and main ideas from the provided content. The summary should:
- Focus on the most important information and key takeaways
- Be concise but comprehensive
- Maintain the original context and meaning
- Be useful for someone who wants to quickly understand the content
- Format the summary in HTML, using appropriate tags like <h1>, <p>, <ul>, <li> to structure the information
Extract the essential information while preserving important details, facts, or insights.`,
},
{
role: "user",
content: `Title: ${title}
Content: ${content}
Please provide a concise summary of this content in HTML format.`,
},
];
try {
const response = await makeModelCall(
false,
messages,
() => {}, // onFinish callback
{ temperature: 0.3 },
);
return response as string;
} catch (error) {
console.error("Error generating summary:", error);
return "<p>Unable to generate summary at this time.</p>";
}
}
const { action, loader } = createActionApiRoute(
{
body: ExtensionSummaryBodyRequest,
@ -178,64 +19,9 @@ const { action, loader } = createActionApiRoute(
corsStrategy: "all",
},
async ({ body }) => {
try {
const $ = cheerio.load(body.html);
const response = await extensionSummary.trigger(body);
// Extract content from any webpage
const extraction = extractTextContent($, body.url);
// Override title if provided
if (body.title) {
extraction.title = body.title;
}
let summary = "";
if (extraction.supported && extraction.content.length > 0) {
// Generate summary for text content
summary = await generateSummary(extraction.title, extraction.content);
} else {
// Handle unsupported content types
if (extraction.pageType === "video") {
summary =
"Video content detected. Text summarization not available for video-focused pages.";
} else {
summary =
"Unable to extract sufficient text content for summarization.";
}
}
const response = {
success: true,
pageType: extraction.pageType,
title: extraction.title,
summary,
content: extraction.content.slice(0, 1000), // Return first 1000 chars of content
supported: extraction.supported,
metadata: extraction.metadata,
};
return json(response);
} catch (error) {
console.error("Error processing extension summary request:", error);
return json(
{
success: false,
error: "Failed to process page content",
pageType: "text" as PageType,
title: body.title || "Error",
summary: "Unable to process this page content.",
content: "",
supported: false,
metadata: {
url: body.url,
wordCount: 0,
},
},
{ status: 500 },
);
}
return json(response);
},
);

View File

View File

@ -1,6 +1,6 @@
import { findUserByToken } from "~/models/personal-token.server";
import { oauth2Service } from "~/services/oauth2.server";
import { type Request as ERequest } from "express";
// See this for more: https://twitter.com/mattpocockuk/status/1653403198885904387?s=20
export type Prettify<T> = {
[K in keyof T]: T[K];
@ -111,8 +111,13 @@ export function isSecretApiKey(key: string) {
return key.startsWith("rc_");
}
export function getApiKeyFromRequest(request: Request) {
return getApiKeyFromHeader(request.headers.get("Authorization"));
export function getApiKeyFromRequest(request: Request | ERequest) {
const authorizationHeader =
request instanceof Request
? request.headers.get("Authorization")
: request.headers["authorization"];
return getApiKeyFromHeader(authorizationHeader);
}
export function getApiKeyFromHeader(authorization?: string | null) {

View File

@ -1,5 +1,6 @@
import { createCookieSessionStorage, type Session } from "@remix-run/node";
import { env } from "~/env.server";
import { type Request as ERequest } from "express";
export const impersonationSessionStorage = createCookieSessionStorage({
cookie: {
@ -13,8 +14,13 @@ export const impersonationSessionStorage = createCookieSessionStorage({
},
});
export function getImpersonationSession(request: Request) {
return impersonationSessionStorage.getSession(request.headers.get("Cookie"));
export function getImpersonationSession(request: Request | ERequest) {
const cookieHeader =
request instanceof Request
? request.headers.get("Cookie")
: request.headers["cookie"];
return impersonationSessionStorage.getSession(cookieHeader);
}
export function commitImpersonationSession(session: Session) {

View File

@ -1,4 +1,3 @@
import { json } from "@remix-run/node";
import { randomUUID } from "node:crypto";
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
@ -8,87 +7,18 @@ import {
CallToolRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
import {
createHybridActionApiRoute,
createLoaderApiRoute,
} from "~/services/routeBuilders/apiBuilder.server";
import { handleTransport } from "~/utils/mcp";
import { MCPSessionManager } from "~/utils/mcp/session-manager";
import { TransportManager } from "~/utils/mcp/transport-manager";
import { IntegrationLoader } from "~/utils/mcp/integration-loader";
import { callMemoryTool, memoryTools } from "~/utils/mcp/memory";
import { logger } from "~/services/logger.service";
import { type Response, type Request } from "express";
// Request schemas
const MCPRequestSchema = z.object({}).passthrough();
const QueryParams = z.object({
source: z.string().optional(),
integrations: z.string().optional(), // comma-separated slugs
});
// Common function to create and setup transport
async function createTransport(
sessionId: string,
source: string,
integrations: string[],
userId: string,
workspaceId: string,
): Promise<StreamableHTTPServerTransport> {
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => sessionId,
onsessioninitialized: async (sessionId) => {
// Clean up old sessions (24+ hours) during new session initialization
try {
const [dbCleanupCount, memoryCleanupCount] = await Promise.all([
MCPSessionManager.cleanupOldSessions(),
TransportManager.cleanupOldSessions(),
]);
if (dbCleanupCount > 0 || memoryCleanupCount > 0) {
logger.log(`Cleaned up ${dbCleanupCount} DB sessions and ${memoryCleanupCount} memory sessions`);
}
} catch (error) {
logger.error(`Error during session cleanup: ${error}`);
}
// Store session in database
await MCPSessionManager.upsertSession(sessionId, source, integrations);
// Store main transport
TransportManager.setMainTransport(sessionId, transport);
},
});
// Setup cleanup on close
transport.onclose = async () => {
await MCPSessionManager.deleteSession(sessionId);
await TransportManager.cleanupSession(sessionId);
};
// Load integration transports
try {
const result = await IntegrationLoader.loadIntegrationTransports(
sessionId,
userId,
workspaceId,
integrations.length > 0 ? integrations : undefined,
);
logger.log(
`Loaded ${result.loaded} integration transports for session ${sessionId}`,
);
if (result.failed.length > 0) {
logger.warn(`Failed to load some integrations: ${result.failed}`);
}
} catch (error) {
logger.error(`Error loading integration transports: ${error}`);
}
// Create and connect MCP server
const server = await createMcpServer(userId, sessionId);
await server.connect(transport);
return transport;
}
// Create MCP server with memory tools + dynamic integration tools
async function createMcpServer(userId: string, sessionId: string) {
const server = new Server(
@ -152,49 +82,82 @@ async function createMcpServer(userId: string, sessionId: string) {
throw new Error(`Unknown tool: ${name}`);
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
// Handle memory tools
if (name.startsWith("memory_")) {
return await callMemoryTool(name, args, userId);
}
// Handle integration tools (prefixed with integration slug)
if (name.includes("_") && !name.startsWith("memory_")) {
try {
return await IntegrationLoader.callIntegrationTool(
sessionId,
name,
args,
);
} catch (error) {
return {
content: [
{
type: "text",
text: `Error calling integration tool: ${error instanceof Error ? error.message : String(error)}`,
},
],
isError: true,
};
}
}
throw new Error(`Unknown tool: ${name}`);
});
return server;
}
// Handle MCP requests
const handleMCPRequest = async (
// Common function to create and setup transport
async function createTransport(
sessionId: string,
source: string,
integrations: string[],
userId: string,
workspaceId: string,
): Promise<StreamableHTTPServerTransport> {
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => sessionId,
onsessioninitialized: async (sessionId) => {
// Clean up old sessions (24+ hours) during new session initialization
try {
const [dbCleanupCount, memoryCleanupCount] = await Promise.all([
MCPSessionManager.cleanupOldSessions(),
TransportManager.cleanupOldSessions(),
]);
if (dbCleanupCount > 0 || memoryCleanupCount > 0) {
logger.log(
`Cleaned up ${dbCleanupCount} DB sessions and ${memoryCleanupCount} memory sessions`,
);
}
} catch (error) {
logger.error(`Error during session cleanup: ${error}`);
}
// Store session in database
await MCPSessionManager.upsertSession(sessionId, source, integrations);
// Store main transport
TransportManager.setMainTransport(sessionId, transport);
},
});
// Setup cleanup on close
transport.onclose = async () => {
await MCPSessionManager.deleteSession(sessionId);
await TransportManager.cleanupSession(sessionId);
};
// Load integration transports
try {
const result = await IntegrationLoader.loadIntegrationTransports(
sessionId,
userId,
workspaceId,
integrations.length > 0 ? integrations : undefined,
);
logger.log(
`Loaded ${result.loaded} integration transports for session ${sessionId}`,
);
if (result.failed.length > 0) {
logger.warn(`Failed to load some integrations: ${result.failed}`);
}
} catch (error) {
logger.error(`Error loading integration transports: ${error}`);
}
// Create and connect MCP server
const server = await createMcpServer(userId, sessionId);
await server.connect(transport);
return transport;
}
export const handleMCPRequest = async (
request: Request,
res: Response,
body: any,
authentication: any,
queryParams: z.infer<typeof QueryParams>,
) => {
const sessionId = request.headers.get("mcp-session-id") as string | undefined;
const sessionId = request.headers["mcp-session-id"] as string | undefined;
const source = queryParams.source || "api";
const integrations = queryParams.integrations
? queryParams.integrations.split(",").map((s) => s.trim())
@ -240,134 +203,34 @@ const handleMCPRequest = async (
);
} else {
// Invalid request
return json(
{
jsonrpc: "2.0",
error: {
code: -32000,
message:
"Bad Request: No valid session ID provided or session inactive",
},
id: body?.id || null,
},
{ status: 400 },
);
throw new Error("No session id");
}
// Handle the request through existing transport utility
const response = await handleTransport(transport!, request, body);
return response;
return await transport.handleRequest(request, res, body);
} catch (error) {
console.error("MCP SSE request error:", error);
return json(
{
jsonrpc: "2.0",
error: {
code: -32000,
message:
error instanceof Error ? error.message : "Internal server error",
},
id: body?.id || null,
},
{ status: 500 },
);
throw new Error("MCP SSE request error");
}
};
// Handle DELETE requests for session cleanup
const handleDelete = async (request: Request) => {
const sessionId = request.headers.get("mcp-session-id") as string | undefined;
if (!sessionId) {
return new Response("Missing session ID", { status: 400 });
}
try {
// Mark session as deleted in database
await MCPSessionManager.deleteSession(sessionId);
// Clean up all transports
await TransportManager.cleanupSession(sessionId);
return new Response(null, { status: 204 });
} catch (error) {
console.error("Error deleting session:", error);
return new Response("Internal server error", { status: 500 });
}
};
const { action } = createHybridActionApiRoute(
{
body: MCPRequestSchema,
searchParams: QueryParams,
allowJWT: true,
authorization: {
action: "mcp",
},
corsStrategy: "all",
},
async ({ body, authentication, request, searchParams }) => {
const method = request.method;
if (method === "POST") {
return await handleMCPRequest(
request,
body,
authentication,
searchParams,
);
} else if (method === "DELETE") {
return await handleDelete(request);
} else {
return json(
{
jsonrpc: "2.0",
error: {
code: -32601,
message: "Method not allowed",
},
id: null,
},
{ status: 405 },
);
}
},
);
const loader = createLoaderApiRoute(
{
allowJWT: true,
corsStrategy: "all",
findResource: async () => 1,
},
async ({ request }) => {
// Handle SSE requests (for server-to-client notifications)
const sessionId = request.headers.get("mcp-session-id");
if (!sessionId) {
return new Response("Missing session ID for SSE", { status: 400 });
}
export const handleSessionRequest = async (req: Request, res: Response) => {
const sessionId = req.headers["mcp-session-id"] as string | undefined;
if (sessionId && (await MCPSessionManager.isSessionActive(sessionId))) {
const sessionData = TransportManager.getSessionInfo(sessionId);
if (!sessionData.exists) {
// Check if session exists in database and recreate transport
const sessionDetails = await MCPSessionManager.getSession(sessionId);
if (!sessionDetails) {
return new Response("Session not found", { status: 404 });
}
// Session exists in DB but not in memory - need authentication to recreate
return new Response("Session not found", { status: 404 });
if (sessionData.exists) {
const transport =
sessionData.mainTransport as StreamableHTTPServerTransport;
await transport.handleRequest(req, res);
} else {
res.status(400).send("Invalid or missing session ID");
return;
}
// Return SSE stream (this would be handled by the transport's handleRequest method)
// For now, just return session info
return json({
sessionId,
active: await MCPSessionManager.isSessionActive(sessionId),
integrationCount: sessionData.integrationCount,
createdAt: sessionData.createdAt,
});
},
);
export { action, loader };
} else {
res.status(400).send("Invalid or missing session ID");
return;
}
};

View File

@ -643,7 +643,7 @@ export type HybridAuthenticationResult =
userId: string;
};
async function authenticateHybridRequest(
export async function authenticateHybridRequest(
request: Request,
options: { allowJWT?: boolean } = {},
): Promise<HybridAuthenticationResult | null> {

View File

@ -2,9 +2,7 @@ import type { EpisodicNode, StatementNode } from "@core/types";
import { logger } from "./logger.service";
import {
applyCrossEncoderReranking,
applyMultiFactorReranking,
applyMultiFactorMMRReranking,
applyWeightedRRF,
} from "./search/rerank";
import {
getEpisodesByStatements,

View File

@ -3,13 +3,21 @@ import { getUserById, getUserLeftCredits } from "~/models/user.server";
import { sessionStorage } from "./sessionStorage.server";
import { getImpersonationId } from "./impersonation.server";
import { getWorkspaceByUser } from "~/models/workspace.server";
import { type Request as ERequest } from "express";
export async function getUserId(request: Request): Promise<string | undefined> {
const impersonatedUserId = await getImpersonationId(request);
export async function getUserId(
request: Request | ERequest,
): Promise<string | undefined> {
const impersonatedUserId = await getImpersonationId(request as Request);
if (impersonatedUserId) return impersonatedUserId;
let session = await sessionStorage.getSession(request.headers.get("cookie"));
const cookieHeader =
request instanceof Request
? request.headers.get("Cookie")
: request.headers["cookie"];
let session = await sessionStorage.getSession(cookieHeader);
let user = session.get("user");
return user?.userId;

View File

@ -1,11 +1,16 @@
import { metadata, task } from "@trigger.dev/sdk";
import { streamText, type CoreMessage, tool } from "ai";
import { z } from "zod";
import { openai } from "@ai-sdk/openai";
import { type CoreMessage, generateText, tool } from "ai";
import { logger } from "~/services/logger.service";
import { SearchService } from "~/services/search.server";
// Input schema for the agent
export const SearchMemoryAgentInput = z.object({
import { openai } from "@ai-sdk/openai";
import { logger } from "~/services/logger.service";
import {
deletePersonalAccessToken,
getOrCreatePersonalAccessToken,
} from "../utils/utils";
import axios from "axios";
export const ExtensionSearchBodyRequest = z.object({
userInput: z.string().min(1, "User input is required"),
userId: z.string().min(1, "User ID is required"),
context: z
@ -14,20 +19,18 @@ export const SearchMemoryAgentInput = z.object({
.describe("Additional context about the user's current work"),
});
/**
* Search Memory Agent - Designed to find relevant context from user's memory
*
* This agent searches the user's memory using a searchMemory tool, retrieves relevant
* facts and episodes, then summarizes them into a concise, relevant context summary.
*/
export class SearchMemoryAgent {
private model = openai("gpt-4o");
private searchService = new SearchService();
// Export a singleton instance
export const extensionSearch = task({
id: "extensionSearch",
maxDuration: 3000,
run: async (body: z.infer<typeof ExtensionSearchBodyRequest>) => {
const { userInput, userId, context } =
ExtensionSearchBodyRequest.parse(body);
async generateContextSummary(
input: z.infer<typeof SearchMemoryAgentInput>,
): Promise<string> {
const { userInput, userId, context } = SearchMemoryAgentInput.parse(input);
const pat = await getOrCreatePersonalAccessToken({
name: "extensionSearch",
userId: userId as string,
});
// Define the searchMemory tool that actually calls the search service
const searchMemoryTool = tool({
@ -38,7 +41,16 @@ export class SearchMemoryAgent {
}),
execute: async ({ query }) => {
try {
const searchResult = await this.searchService.search(query, userId);
const response = await axios.post(
`${process.env.API_BASE_URL}/api/v1/search`,
{ query },
{
headers: {
Authorization: `Bearer ${pat.token}`,
},
},
);
const searchResult = response.data;
return {
facts: searchResult.facts || [],
@ -79,8 +91,8 @@ If no relevant information is found, provide a brief statement indicating that.`
];
try {
const result = await generateText({
model: this.model,
const result = streamText({
model: openai(process.env.MODEL as string),
messages,
tools: {
searchMemory: searchMemoryTool,
@ -90,14 +102,21 @@ If no relevant information is found, provide a brief statement indicating that.`
maxTokens: 600,
});
return result.text.trim();
const stream = await metadata.stream("messages", result.textStream);
let finalText: string = "";
for await (const chunk of stream) {
finalText = finalText + chunk;
}
await deletePersonalAccessToken(pat.id);
return finalText;
} catch (error) {
logger.error(`SearchMemoryAgent error: ${error}`);
await deletePersonalAccessToken(pat.id);
return `Context related to: ${userInput}. Looking for relevant background information, previous discussions, and related concepts that would help provide a comprehensive answer.`;
}
}
}
// Export a singleton instance
export const searchMemoryAgent = new SearchMemoryAgent();
},
});

View File

@ -0,0 +1,234 @@
import { metadata, task } from "@trigger.dev/sdk";
import { type CoreMessage } from "ai";
import * as cheerio from "cheerio";
import { z } from "zod";
import { makeModelCall } from "~/lib/model.server";
export type PageType = "text" | "video";
export const ExtensionSummaryBodyRequest = z.object({
html: z.string().min(1, "HTML content is required"),
url: z.string().url("Valid URL is required"),
title: z.string().optional(),
});
interface ContentExtractionResult {
pageType: PageType;
title: string;
content: string;
metadata: {
url: string;
wordCount: number;
};
supported: boolean;
}
/**
* Detect if page contains video content
*/
function isVideoPage(url: string, $: cheerio.CheerioAPI): boolean {
const hostname = new URL(url).hostname.toLowerCase();
// Known video platforms
if (
hostname.includes("youtube.com") ||
hostname.includes("youtu.be") ||
hostname.includes("vimeo.com") ||
hostname.includes("twitch.tv") ||
hostname.includes("tiktok.com")
) {
return true;
}
// Generic video content detection
const videoElements = $("video").length;
const videoPlayers = $(
'.video-player, [class*="video-player"], [data-testid*="video"]',
).length;
// If there are multiple video indicators, likely a video-focused page
return videoElements > 0 || videoPlayers > 2;
}
/**
* Extract all text content from any webpage
*/
function extractTextContent(
$: cheerio.CheerioAPI,
url: string,
): ContentExtractionResult {
// Extract title from multiple possible locations
const title =
$("title").text() ||
$('meta[property="og:title"]').attr("content") ||
$('meta[name="title"]').attr("content") ||
$("h1").first().text() ||
"Untitled Page";
// Check if this is primarily a video page
const isVideo = isVideoPage(url, $);
const pageType: PageType = isVideo ? "video" : "text";
let content = "";
if (isVideo) {
// For video pages, try to get description/transcript text
content =
$("#description, .video-description, .description").text() ||
$('meta[name="description"]').attr("content") ||
$('[class*="transcript"], [class*="caption"]').text() ||
"Video content detected - text summarization not available";
} else {
// Simple universal text extraction
// Remove non-content elements
$("script, style, noscript, nav, header, footer").remove();
// Get all text content
const allText = $("body").text();
// Split into sentences and filter for meaningful content
const sentences = allText
.split(/[.!?]+/)
.map((s) => s.trim())
.filter((s) => s.length > 20) // Keep sentences with substance
.filter(
(s) =>
!/^(click|menu|button|nav|home|search|login|signup|subscribe)$/i.test(
s.toLowerCase(),
),
) // Remove UI text
.filter((s) => s.split(" ").length > 3); // Keep sentences with multiple words
content = sentences.join(". ").slice(0, 10000);
}
// Clean up whitespace and normalize text
content = content.replace(/\s+/g, " ").trim();
const wordCount = content
.split(/\s+/)
.filter((word) => word.length > 0).length;
const supported = !isVideo && content.length > 50;
return {
pageType,
title: title.trim(),
content: content.slice(0, 10000), // Limit content size for processing
metadata: {
url,
wordCount,
},
supported,
};
}
/**
* Generate summary using LLM
*/
async function generateSummary(title: string, content: string) {
const messages: CoreMessage[] = [
{
role: "system",
content: `You are a helpful assistant that creates concise summaries of web content in HTML format.
Create a clear, informative summary that captures the key points and main ideas from the provided content. The summary should:
- Focus on the most important information and key takeaways
- Be concise but comprehensive
- Maintain the original context and meaning
- Be useful for someone who wants to quickly understand the content
- Format the summary in clean HTML using appropriate tags like <h1>, <h2>, <p>, <ul>, <li> to structure the information
IMPORTANT: Return ONLY the HTML content without any markdown code blocks or formatting. Do not wrap the response in \`\`\`html or any other markdown syntax. Return the raw HTML directly.
Extract the essential information while preserving important details, facts, or insights.`,
},
{
role: "user",
content: `Title: ${title}
Content: ${content}
Please provide a concise summary of this content in HTML format.`,
},
];
return await makeModelCall(
true,
messages,
() => {}, // onFinish callback
{ temperature: 0.3 },
);
}
export const extensionSummary = task({
id: "extensionSummary",
maxDuration: 3000,
run: async (body: z.infer<typeof ExtensionSummaryBodyRequest>) => {
try {
const $ = cheerio.load(body.html);
// Extract content from any webpage
const extraction = extractTextContent($, body.url);
// Override title if provided
if (body.title) {
extraction.title = body.title;
}
let summary = "";
if (extraction.supported && extraction.content.length > 0) {
// Generate summary for text content
const response = (await generateSummary(
extraction.title,
extraction.content,
)) as any;
const stream = await metadata.stream("messages", response.textStream);
let finalText: string = "";
for await (const chunk of stream) {
finalText = finalText + chunk;
}
summary = finalText;
} else {
// Handle unsupported content types
if (extraction.pageType === "video") {
summary =
"Video content detected. Text summarization not available for video-focused pages.";
} else {
summary =
"Unable to extract sufficient text content for summarization.";
}
}
const response = {
success: true,
pageType: extraction.pageType,
title: extraction.title,
summary,
content: extraction.content.slice(0, 1000), // Return first 1000 chars of content
supported: extraction.supported,
metadata: extraction.metadata,
};
return response;
} catch (error) {
console.error("Error processing extension summary request:", error);
return {
success: false,
error: "Failed to process page content",
pageType: "text" as PageType,
title: body.title || "Error",
summary: "Unable to process this page content.",
content: "",
supported: false,
metadata: {
url: body.url,
wordCount: 0,
},
};
}
},
});

View File

@ -1,5 +1,7 @@
import { prisma } from "~/db.server";
import { TransportManager } from "./transport-manager";
import { configureStdioMCPEnvironment } from "~/trigger/utils/mcp";
import { getDefaultEnvironment } from "@core/mcp-proxy";
export interface IntegrationAccountWithDefinition {
id: string;
@ -132,11 +134,23 @@ export class IntegrationLoader {
loaded++;
} else {
// Skip non-HTTP transports for now
failed.push({
slug: account.integrationDefinition.slug,
error: `Unsupported transport type: ${mcpConfig.type}`,
});
const { env, args } = configureStdioMCPEnvironment(spec, account);
const slug = account.integrationDefinition.slug;
// Extract headers from the incoming request and convert to environment variables
const extractedEnv = { ...getDefaultEnvironment(), ...env };
// Use the saved local file instead of command
const executablePath = `./integrations/${slug}/main`;
await TransportManager.addStdioIntegrationTransport(
sessionId,
account.id,
account.integrationDefinition.slug,
executablePath,
args,
extractedEnv,
);
}
} catch (error) {
failed.push({

View File

@ -1,13 +1,14 @@
import { type StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import { Client as McpClient } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
export interface IntegrationTransport {
client: McpClient;
transport: StreamableHTTPClientTransport;
transport: StreamableHTTPClientTransport | StdioClientTransport;
integrationAccountId: string;
slug: string;
url: string;
url?: string;
}
export interface SessionTransports {
@ -102,6 +103,47 @@ export class TransportManager {
return integrationTransport;
}
static async addStdioIntegrationTransport(
sessionId: string,
integrationAccountId: string,
slug: string,
command: string,
args: string[],
env?: any,
): Promise<IntegrationTransport> {
const session = this.getOrCreateSession(sessionId);
const transport = new StdioClientTransport({
command,
args: args || [],
env,
});
// Create MCP client
const client = new McpClient({
name: `core-client-${slug}`,
version: "1.0.0",
});
// Connect client to transport
await client.connect(transport);
const integrationTransport: IntegrationTransport = {
client,
transport,
integrationAccountId,
slug,
url: command,
};
session.integrationTransports.set(
integrationAccountId,
integrationTransport,
);
return integrationTransport;
}
/**
* Get integration transport by account ID
*/

View File

@ -4,11 +4,11 @@
"sideEffects": false,
"type": "module",
"scripts": {
"build": "remix vite:build",
"dev": "node ./server.mjs",
"build": "remix vite:build && tsc server.ts --outDir ./ --module ESNext --moduleResolution bundler --target ES2022 --allowSyntheticDefaultImports --skipLibCheck",
"dev": "tsx watch server.ts",
"lint": "eslint --fix --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .",
"lint:fix": "eslint 'app/**/*.{ts,tsx,js,jsx}' --rule 'turbo/no-undeclared-env-vars:error' -f table",
"start": "remix-serve ./build/server/index.js",
"start": "node server.js",
"typecheck": "tsc",
"trigger:dev": "pnpm dlx trigger.dev@4.0.0-v4-beta.22 dev",
"trigger:deploy": "pnpm dlx trigger.dev@4.0.0-v4-beta.22 deploy"
@ -175,7 +175,8 @@
"tailwindcss": "4.1.7",
"typescript": "5.8.3",
"vite": "^6.0.0",
"vite-tsconfig-paths": "^4.2.1"
"vite-tsconfig-paths": "^4.2.1",
"tsx": "4.20.4"
},
"engines": {
"node": ">=20.0.0"

View File

@ -3,7 +3,13 @@ import compression from "compression";
import express from "express";
import morgan from "morgan";
let viteDevServer;
// import {
// handleMCPRequest,
// handleSessionRequest,
// } from "~/services/mcp.server";
// import { authenticateHybridRequest } from "~/services/routeBuilders/apiBuilder.server";
let viteDevServer: any;
let remixHandler;
async function init() {
@ -14,10 +20,13 @@ async function init() {
});
}
const build = viteDevServer
const build: any = viteDevServer
? () => viteDevServer.ssrLoadModule("virtual:remix/server-build")
: await import("./build/server/index.js");
const { authenticateHybridRequest, handleMCPRequest, handleSessionRequest } =
build.entry.module;
remixHandler = createRequestHandler({ build });
const app = express();
@ -44,6 +53,68 @@ async function init() {
app.use(morgan("tiny"));
app.get("/api/v1/mcp", async (req, res) => {
const authenticationResult = await authenticateHybridRequest(req as any, {
allowJWT: true,
});
if (!authenticationResult) {
res.status(401).json({ error: "Authentication required" });
return;
}
await handleSessionRequest(req, res);
});
app.post("/api/v1/mcp", async (req, res) => {
const authenticationResult = await authenticateHybridRequest(req as any, {
allowJWT: true,
});
if (!authenticationResult) {
res.status(401).json({ error: "Authentication required" });
return;
}
let body = "";
req.on("data", (chunk) => {
body += chunk;
});
req.on("end", async () => {
try {
const parsedBody = JSON.parse(body);
const queryParams = req.query; // Get query parameters from the request
await handleMCPRequest(
req,
res,
parsedBody,
authenticationResult,
queryParams,
);
} catch (error) {
res.status(400).json({ error: "Invalid JSON" });
}
});
});
app.delete("/api/v1/mcp", async (req, res) => {
const authenticationResult = await authenticateHybridRequest(req as any, {
allowJWT: true,
});
if (!authenticationResult) {
res.status(401).json({ error: "Authentication required" });
return;
}
await handleSessionRequest(req, res);
});
app.options("/api/v1/mcp", (_, res) => {
res.json({});
});
app.get("/.well-known/oauth-authorization-server", (req, res) => {
res.json({
issuer: process.env.APP_ORIGIN,

View File

@ -8,7 +8,7 @@
"tailwind.config.js",
"tailwind.config.js",
"trigger.config.ts",
"server.mjs"
"server.ts"
],
"compilerOptions": {
"types": ["@remix-run/node", "vite/client"],

View File

@ -64,7 +64,7 @@ ENV NODE_ENV production
COPY --from=base /usr/bin/dumb-init /usr/bin/dumb-init
COPY --from=pruner --chown=node:node /core/out/full/ .
COPY --from=production-deps --chown=node:node /core .
COPY --from=builder --chown=node:node /core/apps/webapp/server.mjs ./apps/webapp/server.mjs
COPY --from=builder --chown=node:node /core/apps/webapp/server.js ./apps/webapp/server.js
COPY --from=builder --chown=node:node /core/apps/webapp/build ./apps/webapp/build
COPY --from=builder --chown=node:node /core/apps/webapp/public ./apps/webapp/public
COPY --from=builder --chown=node:node /core/scripts ./scripts

View File

@ -15,4 +15,4 @@ cp packages/database/prisma/schema.prisma apps/webapp/prisma/
cd /core/apps/webapp
# exec dumb-init pnpm run start:local
NODE_PATH='/core/node_modules/.pnpm/node_modules' exec dumb-init node --max-old-space-size=8192 ./server.mjs
NODE_PATH='/core/node_modules/.pnpm/node_modules' exec dumb-init node --max-old-space-size=8192 ./server.js

View File

@ -1,4 +1,4 @@
VERSION=0.1.17
VERSION=0.1.18
# Nest run in docker, change host to database container name
DB_HOST=postgres

View File

@ -1,7 +1,7 @@
{
"name": "core",
"private": true,
"version": "0.1.17",
"version": "0.1.18",
"workspaces": [
"apps/*",
"packages/*"

View File

@ -1,26 +1,10 @@
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import {
MCPRemoteClientConfig,
AuthenticationResult,
ProxyConnectionConfig,
CredentialLoadCallback,
MCPProxyFunction,
StoredCredentials,
TransportStrategy,
} from "../types/remote-client.js";
import { MCPRemoteClientConfig, AuthenticationResult } from "../types/remote-client.js";
import { MCPAuthProxyError } from "../utils/errors.js";
import { NodeOAuthClientProvider } from "../lib/node-oauth-client-provider.js";
import { globalAuthStorage } from "../lib/in-memory-auth-storage.js";
import { getServerUrlHash } from "../lib/utils.js";
import { RemixMCPTransport } from "../utils/mcp-transport.js";
import { createMCPTransportBridge } from "../utils/mcp-transport-bridge.js";
import {
createAuthProviderFromConfig,
createAuthProviderForProxy,
} from "../utils/auth-provider-factory.js";
import { createAuthProviderFromConfig } from "../utils/auth-provider-factory.js";
/**
* Creates an MCP authentication client that handles OAuth flow
@ -32,424 +16,10 @@ export function createMCPAuthClient(config: MCPRemoteClientConfig): MCPAuthentic
return new MCPAuthenticationClient(config);
}
/**
* Creates an MCP proxy that forwards requests to the remote MCP server
* Consolidates all proxy functionality into a single function
* @param config Configuration for the proxy connection
* @param onCredentialLoad Callback to load credentials from your database
* @returns Proxy function that can be used in your Remix API routes
*/
export function createMCPProxy(
config: ProxyConnectionConfig & {
/** Enable debug logging */
debug?: boolean;
},
onCredentialLoad: CredentialLoadCallback
): MCPProxyFunction {
return async (request: Request, userApiKey: string): Promise<Response> => {
return new Promise<Response>(async (resolve) => {
let bridge: any = null;
try {
// Load credentials for this user and server
const credentials = await onCredentialLoad(userApiKey, config.serverUrl);
if (!credentials) {
return resolve(
new Response(
JSON.stringify({
error: "No credentials found for this service",
}),
{
status: 401,
headers: { "Content-Type": "application/json" },
}
)
);
}
// Check if tokens are expired
if (credentials.expiresAt && credentials.expiresAt < new Date()) {
return resolve(
new Response(
JSON.stringify({
error: "Credentials expired - please re-authenticate",
}),
{
status: 401,
headers: { "Content-Type": "application/json" },
}
)
);
}
// Extract session ID and last event ID from incoming request
const clientSessionId = request.headers.get("Mcp-Session-Id");
const lastEventId = request.headers.get("Last-Event-Id");
// Create remote transport (connects to the MCP server) FIRST
const serverTransport = await createRemoteTransport(
credentials.serverUrl,
credentials,
config.redirectUrl,
config.transportStrategy || "sse-first",
{ sessionId: clientSessionId, lastEventId } // Pass both session and event IDs
);
// Start server transport and wait for connection
await serverTransport.start();
// Create Remix transport (converts HTTP to MCP messages)
const clientTransport = new RemixMCPTransport(request, resolve);
// Bridge the transports
const bridgeOptions: any = {
debug: config.debug || false,
onError: (error: Error, source: string) => {
console.error(`[MCP Bridge] ${source} error:`, error);
},
};
if (config.debug) {
bridgeOptions.onMessage = (direction: string, message: any) => {
console.log(`[MCP Bridge] ${direction}:`, message.method || message.id);
};
}
bridge = createMCPTransportBridge(
clientTransport as any,
serverTransport as any,
bridgeOptions
);
// Start only the client transport (server is already started)
await clientTransport.start();
} catch (error) {
console.error("MCP Transport Proxy Error:", error);
if (bridge) {
bridge.close().catch(console.error);
}
const errorMessage = error instanceof Error ? error.message : String(error);
resolve(
new Response(
JSON.stringify({
error: `Transport proxy error: ${errorMessage}`,
}),
{
status: 500,
headers: { "Content-Type": "application/json" },
}
)
);
}
});
};
// Helper function to create remote transport
async function createRemoteTransport(
serverUrl: string,
credentials: StoredCredentials,
redirectUrl: string,
transportStrategy: TransportStrategy = "sse-first",
clientHeaders?: { sessionId?: string | null; lastEventId?: string | null }
): Promise<SSEClientTransport | StreamableHTTPClientTransport | StdioClientTransport> {
// Create auth provider with stored credentials using common factory
const authProvider = await createAuthProviderForProxy(serverUrl, credentials, redirectUrl);
const url = new URL(serverUrl);
const headers: Record<string, string> = {
Authorization: `Bearer ${credentials.tokens.access_token}`,
"Content-Type": "application/json",
...config.headers,
};
// Add session and event headers if provided
if (clientHeaders?.sessionId) {
headers["Mcp-Session-Id"] = clientHeaders.sessionId;
}
if (clientHeaders?.lastEventId) {
headers["Last-Event-Id"] = clientHeaders.lastEventId;
}
// Create transport based on strategy (don't start yet)
let transport: SSEClientTransport | StreamableHTTPClientTransport | StdioClientTransport;
switch (transportStrategy) {
case "stdio":
// For stdio transport, serverUrl should contain the command to execute
// This is mainly for completeness - prefer using createMCPStdioProxy directly
throw new Error(
"Stdio transport not supported in createRemoteTransport. Use createMCPStdioProxy instead."
);
case "sse-only":
transport = new SSEClientTransport(url, {
authProvider,
requestInit: { headers },
});
break;
case "http-only":
transport = new StreamableHTTPClientTransport(url, {
requestInit: { headers },
});
break;
case "sse-first":
// Try SSE first, fallback to HTTP on error
try {
transport = new SSEClientTransport(url, {
authProvider,
requestInit: { headers },
});
} catch (error) {
console.warn("SSE transport failed, falling back to HTTP:", error);
transport = new StreamableHTTPClientTransport(url, {
requestInit: { headers },
});
}
break;
case "http-first":
// Try HTTP first, fallback to SSE on error
try {
transport = new StreamableHTTPClientTransport(url, {
requestInit: { headers },
});
} catch (error) {
console.warn("HTTP transport failed, falling back to SSE:", error);
transport = new SSEClientTransport(url, {
authProvider,
requestInit: { headers },
});
}
break;
default:
throw new Error(`Unknown transport strategy: ${transportStrategy}`);
}
return transport;
}
}
/**
* Creates an MCP proxy that forwards requests to a stdio process.
* Maintains a mapping of sessionId -> StdioClientTransport for reuse.
* If sessionId is provided, it is returned in the response header as mcp_session_id.
* @param request The incoming HTTP request
* @param command The command to execute for the stdio process
* @param args Arguments for the command
* @param options Optional configuration for the proxy
* @param sessionId Optional session id for transport reuse
* @returns Promise that resolves to the HTTP response
*/
// Track both the transport and its last used timestamp
type StdioTransportEntry = {
transport: StdioClientTransport;
lastUsed: number; // ms since epoch
};
const stdioTransports: Map<string, StdioTransportEntry> = new Map();
/**
* Cleans up any stdio transports that have not been used in the last 5 minutes.
* Closes and removes them from the map.
*/
function cleanupOldStdioTransports() {
const now = Date.now();
const FIVE_MINUTES = 5 * 60 * 1000;
for (const [sessionId, entry] of stdioTransports.entries()) {
if (now - entry.lastUsed > FIVE_MINUTES) {
try {
entry.transport.close?.();
} catch (err) {
// ignore
}
stdioTransports.delete(sessionId);
}
}
}
export function createMCPStdioProxy(
request: Request,
command: string,
args?: string[],
options?: {
/** Enable debug logging */
debug?: boolean;
/** Environment variables to pass to the process */
env?: Record<string, string>;
/** Custom header-to-environment variable mapping */
headerMapping?: Record<string, string>;
/** Optional session id for transport reuse */
sessionId?: string;
}
): Promise<Response> {
return new Promise<Response>(async (resolve) => {
let bridge: any = null;
let serverTransport: StdioClientTransport | undefined;
let sessionId: string | undefined =
options?.sessionId || request.headers.get("Mcp-Session-Id") || undefined;
// Clean up old transports before handling new connection
cleanupOldStdioTransports();
try {
// Extract headers from the incoming request and convert to environment variables
const env = createEnvironmentFromRequest(
request,
options?.env || {},
options?.headerMapping || {}
);
// If sessionId is provided, try to reuse the transport
let entry: StdioTransportEntry | undefined;
if (sessionId) {
entry = stdioTransports.get(sessionId);
if (entry) {
serverTransport = entry.transport;
entry.lastUsed = Date.now();
}
}
// If no transport exists for this sessionId, create a new one and store it
if (!serverTransport) {
serverTransport = new StdioClientTransport({
command,
args: args || [],
env,
});
await serverTransport.start();
if (sessionId) {
stdioTransports.set(sessionId, {
transport: serverTransport,
lastUsed: Date.now(),
});
}
}
// Create Remix transport (converts HTTP to MCP messages)
// We need to wrap resolve to inject the sessionId header if present
const resolveWithSessionId = (response: Response) => {
if (sessionId) {
// Clone the response and add the mcp_session_id header
const headers = new Headers(response.headers);
headers.set("mcp-session-id", sessionId);
resolve(
new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers,
})
);
} else {
resolve(response);
}
};
const clientTransport = new RemixMCPTransport(request, resolveWithSessionId);
// Bridge the transports
const bridgeOptions: any = {
debug: options?.debug || false,
onError: (error: Error, source: string) => {
console.error(`[MCP Stdio Bridge] ${source} error:`, error);
},
};
if (options?.debug) {
bridgeOptions.onMessage = (direction: string, message: any) => {
console.log(`[MCP Stdio Bridge] ${direction}:`, message.method || message.id);
};
}
bridge = createMCPTransportBridge(
clientTransport as any,
serverTransport as any,
bridgeOptions
);
// Start only the client transport (server is already started)
await clientTransport.start();
} catch (error) {
console.error("MCP Stdio Proxy Error:", error);
if (bridge) {
bridge.close().catch(console.error);
}
const errorMessage = error instanceof Error ? error.message : String(error);
// Always include mcp_session_id header if sessionId is present
const headers: Record<string, string> = { "Content-Type": "application/json" };
if (sessionId) {
headers["mcp-session-id"] = sessionId;
}
resolve(
new Response(
JSON.stringify({
error: `Stdio proxy error: ${errorMessage}`,
}),
{
status: 500,
headers,
}
)
);
}
});
}
/**
* Creates environment variables from request headers
*/
function createEnvironmentFromRequest(
request: Request,
baseEnv: Record<string, string>,
headerMapping: Record<string, string>
): Record<string, string> {
// Start with base environment (inherit safe environment variables)
const env: Record<string, string> = {
...getDefaultEnvironment(),
...baseEnv,
};
// Add standard MCP headers as environment variables
const sessionId = request.headers.get("Mcp-Session-Id");
const lastEventId = request.headers.get("Last-Event-Id");
const contentType = request.headers.get("Content-Type");
const userAgent = request.headers.get("User-Agent");
if (sessionId) {
env["MCP_SESSION_ID"] = sessionId;
}
if (lastEventId) {
env["MCP_LAST_EVENT_ID"] = lastEventId;
}
if (contentType) {
env["MCP_CONTENT_TYPE"] = contentType;
}
if (userAgent) {
env["MCP_USER_AGENT"] = userAgent;
}
// Apply custom header-to-environment variable mapping
for (const [headerName, envVarName] of Object.entries(headerMapping)) {
const headerValue = request.headers.get(headerName);
if (headerValue) {
env[envVarName] = headerValue;
}
}
return env;
}
/**
* Returns a default environment object including only environment variables deemed safe to inherit.
*/
function getDefaultEnvironment(): Record<string, string> {
export function getDefaultEnvironment(): Record<string, string> {
const DEFAULT_INHERITED_ENV_VARS =
process.platform === "win32"
? [

View File

@ -4,8 +4,7 @@ export * from "./types/index.js";
// MCP Remote Client exports (new simplified interface)
export {
createMCPAuthClient,
createMCPProxy,
createMCPStdioProxy,
getDefaultEnvironment,
MCPAuthenticationClient,
} from "./core/mcp-remote-client.js";

View File

@ -54,7 +54,7 @@ export interface StatementNode {
userId: string;
space?: string; // Legacy field - deprecated in favor of spaceIds
spaceIds?: string[]; // Array of space UUIDs this statement belongs to
recallCount?: number;
recallCount?: { low: number; high: number };
provenanceCount?: number;
}

123
pnpm-lock.yaml generated
View File

@ -646,7 +646,7 @@ importers:
devDependencies:
'@remix-run/dev':
specifier: 2.16.7
version: 2.16.7(@remix-run/react@2.16.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3))(@remix-run/serve@2.16.7(typescript@5.8.3))(@types/node@22.16.0)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)(tsx@4.17.0)(typescript@5.8.3)(vite@6.3.5(@types/node@22.16.0)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)(tsx@4.17.0)(yaml@2.8.0))(yaml@2.8.0)
version: 2.16.7(@remix-run/react@2.16.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3))(@remix-run/serve@2.16.7(typescript@5.8.3))(@types/node@20.19.7)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)(tsx@4.20.4)(typescript@5.8.3)(vite@6.3.5(@types/node@20.19.7)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)(tsx@4.20.4)(yaml@2.8.0))(yaml@2.8.0)
'@remix-run/eslint-config':
specifier: 2.16.7
version: 2.16.7(eslint@8.57.1)(react@18.3.1)(typescript@5.8.3)
@ -661,7 +661,7 @@ importers:
version: 0.5.16(tailwindcss@4.1.7)
'@tailwindcss/vite':
specifier: ^4.1.7
version: 4.1.9(vite@6.3.5(@types/node@22.16.0)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)(tsx@4.17.0)(yaml@2.8.0))
version: 4.1.9(vite@6.3.5(@types/node@20.19.7)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)(tsx@4.20.4)(yaml@2.8.0))
'@trigger.dev/build':
specifier: 4.0.0-v4-beta.22
version: 4.0.0-v4-beta.22(typescript@5.8.3)
@ -752,15 +752,18 @@ importers:
tailwindcss:
specifier: 4.1.7
version: 4.1.7
tsx:
specifier: 4.20.4
version: 4.20.4
typescript:
specifier: 5.8.3
version: 5.8.3
vite:
specifier: ^6.0.0
version: 6.3.5(@types/node@22.16.0)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)(tsx@4.17.0)(yaml@2.8.0)
version: 6.3.5(@types/node@20.19.7)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)(tsx@4.20.4)(yaml@2.8.0)
vite-tsconfig-paths:
specifier: ^4.2.1
version: 4.3.2(typescript@5.8.3)(vite@6.3.5(@types/node@22.16.0)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)(tsx@4.17.0)(yaml@2.8.0))
version: 4.3.2(typescript@5.8.3)(vite@6.3.5(@types/node@20.19.7)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)(tsx@4.20.4)(yaml@2.8.0))
packages/database:
dependencies:
@ -829,7 +832,7 @@ importers:
version: 20.19.7
tsup:
specifier: ^8.0.1
version: 8.5.0(@swc/core@1.3.101)(jiti@2.4.2)(postcss@8.5.5)(tsx@4.17.0)(typescript@5.8.3)(yaml@2.8.0)
version: 8.5.0(@swc/core@1.3.101(@swc/helpers@0.5.17))(jiti@2.4.2)(postcss@8.5.5)(tsx@4.20.4)(typescript@5.8.3)(yaml@2.8.0)
typescript:
specifier: ^5.0.0
version: 5.8.3
@ -866,7 +869,7 @@ importers:
version: 6.0.1
tsup:
specifier: ^8.0.1
version: 8.5.0(@swc/core@1.3.101)(jiti@2.4.2)(postcss@8.5.5)(tsx@4.17.0)(typescript@5.8.3)(yaml@2.8.0)
version: 8.5.0(@swc/core@1.3.101(@swc/helpers@0.5.17))(jiti@2.4.2)(postcss@8.5.5)(tsx@4.20.4)(typescript@5.8.3)(yaml@2.8.0)
typescript:
specifier: ^5.3.0
version: 5.8.3
@ -10880,6 +10883,11 @@ packages:
engines: {node: '>=18.0.0'}
hasBin: true
tsx@4.20.4:
resolution: {integrity: sha512-yyxBKfORQ7LuRt/BQKBXrpcq59ZvSW0XxwfjAt3w2/8PmdxaFzijtMhTawprSHhpzeM5BgU2hXHG3lklIERZXg==}
engines: {node: '>=18.0.0'}
hasBin: true
tty-table@4.2.3:
resolution: {integrity: sha512-Fs15mu0vGzCrj8fmJNP7Ynxt5J7praPXqFN0leZeZBXJwkMxv9cb2D454k1ltrtUSJbZ4yH4e0CynsHLxmUfFA==}
engines: {node: '>=8.0.0'}
@ -12911,7 +12919,7 @@ snapshots:
dependencies:
'@floating-ui/dom': 1.7.1
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
react-dom: 18.2.0(react@18.3.1)
'@floating-ui/react-dom@2.1.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
@ -13664,7 +13672,7 @@ snapshots:
dependencies:
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
react-dom: 18.2.0(react@18.3.1)
optionalDependencies:
'@types/react': 18.2.47
'@types/react-dom': 18.2.18
@ -13718,7 +13726,7 @@ snapshots:
'@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.2.47)(react@18.2.0)
'@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.2.47)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
react-dom: 18.2.0(react@18.3.1)
optionalDependencies:
'@types/react': 18.2.47
'@types/react-dom': 18.2.18
@ -13746,7 +13754,7 @@ snapshots:
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-slot': 1.1.0(@types/react@18.2.47)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
react-dom: 18.2.0(react@18.3.1)
optionalDependencies:
'@types/react': 18.2.47
'@types/react-dom': 18.2.18
@ -13879,7 +13887,7 @@ snapshots:
'@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.2.47)(react@18.2.0)
'@radix-ui/react-use-escape-keydown': 1.1.0(@types/react@18.2.47)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
react-dom: 18.2.0(react@18.3.1)
optionalDependencies:
'@types/react': 18.2.47
'@types/react-dom': 18.2.18
@ -13944,7 +13952,7 @@ snapshots:
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.2.47)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
react-dom: 18.2.0(react@18.3.1)
optionalDependencies:
'@types/react': 18.2.47
'@types/react-dom': 18.2.18
@ -14036,7 +14044,7 @@ snapshots:
'@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.2.47)(react@18.2.0)
aria-hidden: 1.2.6
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
react-dom: 18.2.0(react@18.3.1)
react-remove-scroll: 2.5.7(@types/react@18.2.47)(react@18.2.0)
optionalDependencies:
'@types/react': 18.2.47
@ -14078,7 +14086,7 @@ snapshots:
'@radix-ui/react-use-size': 1.1.0(@types/react@18.2.47)(react@18.2.0)
'@radix-ui/rect': 1.1.0
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
react-dom: 18.2.0(react@18.3.1)
optionalDependencies:
'@types/react': 18.2.47
'@types/react-dom': 18.2.18
@ -14113,7 +14121,7 @@ snapshots:
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.2.47)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
react-dom: 18.2.0(react@18.3.1)
optionalDependencies:
'@types/react': 18.2.47
'@types/react-dom': 18.2.18
@ -14141,7 +14149,7 @@ snapshots:
'@radix-ui/react-compose-refs': 1.1.0(@types/react@18.2.47)(react@18.2.0)
'@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.2.47)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
react-dom: 18.2.0(react@18.3.1)
optionalDependencies:
'@types/react': 18.2.47
'@types/react-dom': 18.2.18
@ -14167,7 +14175,7 @@ snapshots:
dependencies:
'@radix-ui/react-slot': 1.1.0(@types/react@18.2.47)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
react-dom: 18.2.0(react@18.3.1)
optionalDependencies:
'@types/react': 18.2.47
'@types/react-dom': 18.2.18
@ -14193,7 +14201,7 @@ snapshots:
'@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.2.47)(react@18.2.0)
'@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.2.47)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
react-dom: 18.2.0(react@18.3.1)
optionalDependencies:
'@types/react': 18.2.47
'@types/react-dom': 18.2.18
@ -14378,7 +14386,7 @@ snapshots:
'@radix-ui/react-toggle': 1.1.0(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.2.47)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
react-dom: 18.2.0(react@18.3.1)
optionalDependencies:
'@types/react': 18.2.47
'@types/react-dom': 18.2.18
@ -14389,7 +14397,7 @@ snapshots:
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.2.47)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
react-dom: 18.2.0(react@18.3.1)
optionalDependencies:
'@types/react': 18.2.47
'@types/react-dom': 18.2.18
@ -14409,7 +14417,7 @@ snapshots:
'@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.2.47)(react@18.2.0)
'@radix-ui/react-visually-hidden': 1.1.0(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
react-dom: 18.2.0(react@18.3.1)
optionalDependencies:
'@types/react': 18.2.47
'@types/react-dom': 18.2.18
@ -14561,7 +14569,7 @@ snapshots:
dependencies:
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
react-dom: 18.2.0(react@18.3.1)
optionalDependencies:
'@types/react': 18.2.47
'@types/react-dom': 18.2.18
@ -14682,7 +14690,7 @@ snapshots:
html-to-text: 9.0.5
js-beautify: 1.15.4
react: 18.3.1
react-dom: 18.2.0(react@18.2.0)
react-dom: 18.2.0(react@18.3.1)
react-promise-suspense: 0.3.4
'@react-email/row@0.0.7(react@18.3.1)':
@ -14716,7 +14724,7 @@ snapshots:
transitivePeerDependencies:
- encoding
'@remix-run/dev@2.16.7(@remix-run/react@2.16.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3))(@remix-run/serve@2.16.7(typescript@5.8.3))(@types/node@22.16.0)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)(tsx@4.17.0)(typescript@5.8.3)(vite@6.3.5(@types/node@22.16.0)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)(tsx@4.17.0)(yaml@2.8.0))(yaml@2.8.0)':
'@remix-run/dev@2.16.7(@remix-run/react@2.16.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3))(@remix-run/serve@2.16.7(typescript@5.8.3))(@types/node@20.19.7)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)(tsx@4.20.4)(typescript@5.8.3)(vite@6.3.5(@types/node@20.19.7)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)(tsx@4.20.4)(yaml@2.8.0))(yaml@2.8.0)':
dependencies:
'@babel/core': 7.27.4
'@babel/generator': 7.27.5
@ -14733,7 +14741,7 @@ snapshots:
'@remix-run/router': 1.23.0
'@remix-run/server-runtime': 2.16.7(typescript@5.8.3)
'@types/mdx': 2.0.13
'@vanilla-extract/integration': 6.5.0(@types/node@22.16.0)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)
'@vanilla-extract/integration': 6.5.0(@types/node@20.19.7)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)
arg: 5.0.2
cacache: 17.1.4
chalk: 4.1.2
@ -14773,12 +14781,12 @@ snapshots:
tar-fs: 2.1.3
tsconfig-paths: 4.2.0
valibot: 0.41.0(typescript@5.8.3)
vite-node: 3.2.3(@types/node@22.16.0)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)(tsx@4.17.0)(yaml@2.8.0)
vite-node: 3.2.3(@types/node@20.19.7)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)(tsx@4.20.4)(yaml@2.8.0)
ws: 7.5.10
optionalDependencies:
'@remix-run/serve': 2.16.7(typescript@5.8.3)
typescript: 5.8.3
vite: 6.3.5(@types/node@22.16.0)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)(tsx@4.17.0)(yaml@2.8.0)
vite: 6.3.5(@types/node@20.19.7)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)(tsx@4.20.4)(yaml@2.8.0)
transitivePeerDependencies:
- '@types/node'
- babel-plugin-macros
@ -15517,12 +15525,12 @@ snapshots:
postcss-selector-parser: 6.0.10
tailwindcss: 4.1.7
'@tailwindcss/vite@4.1.9(vite@6.3.5(@types/node@22.16.0)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)(tsx@4.17.0)(yaml@2.8.0))':
'@tailwindcss/vite@4.1.9(vite@6.3.5(@types/node@20.19.7)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)(tsx@4.20.4)(yaml@2.8.0))':
dependencies:
'@tailwindcss/node': 4.1.9
'@tailwindcss/oxide': 4.1.9
tailwindcss: 4.1.9
vite: 6.3.5(@types/node@22.16.0)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)(tsx@4.17.0)(yaml@2.8.0)
vite: 6.3.5(@types/node@20.19.7)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)(tsx@4.20.4)(yaml@2.8.0)
'@tanstack/react-table@8.21.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
@ -16531,7 +16539,7 @@ snapshots:
transitivePeerDependencies:
- babel-plugin-macros
'@vanilla-extract/integration@6.5.0(@types/node@22.16.0)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)':
'@vanilla-extract/integration@6.5.0(@types/node@20.19.7)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)':
dependencies:
'@babel/core': 7.27.4
'@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.27.4)
@ -16544,8 +16552,8 @@ snapshots:
lodash: 4.17.21
mlly: 1.7.4
outdent: 0.8.0
vite: 5.4.19(@types/node@22.16.0)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)
vite-node: 1.6.1(@types/node@22.16.0)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)
vite: 5.4.19(@types/node@20.19.7)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)
vite-node: 1.6.1(@types/node@20.19.7)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)
transitivePeerDependencies:
- '@types/node'
- babel-plugin-macros
@ -18904,7 +18912,7 @@ snapshots:
optionalDependencies:
'@emotion/is-prop-valid': 0.8.8
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
react-dom: 18.2.0(react@18.3.1)
framework-utils@1.1.0: {}
@ -20754,7 +20762,7 @@ snapshots:
graceful-fs: 4.2.11
postcss: 8.4.31
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
react-dom: 18.2.0(react@18.3.1)
styled-jsx: 5.1.1(@babel/core@7.24.5)(react@18.2.0)
optionalDependencies:
'@next/swc-darwin-arm64': 14.1.4
@ -21378,13 +21386,13 @@ snapshots:
optionalDependencies:
postcss: 8.5.5
postcss-load-config@6.0.1(jiti@2.4.2)(postcss@8.5.5)(tsx@4.17.0)(yaml@2.8.0):
postcss-load-config@6.0.1(jiti@2.4.2)(postcss@8.5.5)(tsx@4.20.4)(yaml@2.8.0):
dependencies:
lilconfig: 3.1.3
optionalDependencies:
jiti: 2.4.2
postcss: 8.5.5
tsx: 4.17.0
tsx: 4.20.4
yaml: 2.8.0
postcss-loader@8.1.1(postcss@8.5.5)(typescript@5.8.3)(webpack@5.99.9(esbuild@0.25.5)):
@ -21808,6 +21816,12 @@ snapshots:
react: 18.2.0
scheduler: 0.23.2
react-dom@18.2.0(react@18.3.1):
dependencies:
loose-envify: 1.4.0
react: 18.3.1
scheduler: 0.23.2
react-dom@18.3.1(react@18.3.1):
dependencies:
loose-envify: 1.4.0
@ -21847,7 +21861,7 @@ snapshots:
postcss: 8.4.38
prism-react-renderer: 2.1.0(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
react-dom: 18.2.0(react@18.3.1)
socket.io: 4.7.3
socket.io-client: 4.7.3
sonner: 1.3.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@ -22650,7 +22664,7 @@ snapshots:
sonner@1.3.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies:
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
react-dom: 18.2.0(react@18.3.1)
source-map-js@1.0.2: {}
@ -23177,7 +23191,7 @@ snapshots:
tslib@2.8.1: {}
tsup@8.5.0(@swc/core@1.3.101)(jiti@2.4.2)(postcss@8.5.5)(tsx@4.17.0)(typescript@5.8.3)(yaml@2.8.0):
tsup@8.5.0(@swc/core@1.3.101(@swc/helpers@0.5.17))(jiti@2.4.2)(postcss@8.5.5)(tsx@4.20.4)(typescript@5.8.3)(yaml@2.8.0):
dependencies:
bundle-require: 5.1.0(esbuild@0.25.5)
cac: 6.7.14
@ -23188,7 +23202,7 @@ snapshots:
fix-dts-default-cjs-exports: 1.0.1
joycon: 3.1.1
picocolors: 1.1.1
postcss-load-config: 6.0.1(jiti@2.4.2)(postcss@8.5.5)(tsx@4.17.0)(yaml@2.8.0)
postcss-load-config: 6.0.1(jiti@2.4.2)(postcss@8.5.5)(tsx@4.20.4)(yaml@2.8.0)
resolve-from: 5.0.0
rollup: 4.43.0
source-map: 0.8.0-beta.0
@ -23218,6 +23232,13 @@ snapshots:
optionalDependencies:
fsevents: 2.3.3
tsx@4.20.4:
dependencies:
esbuild: 0.25.5
get-tsconfig: 4.10.1
optionalDependencies:
fsevents: 2.3.3
tty-table@4.2.3:
dependencies:
chalk: 4.1.2
@ -23580,13 +23601,13 @@ snapshots:
'@types/unist': 3.0.3
vfile-message: 4.0.2
vite-node@1.6.1(@types/node@22.16.0)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0):
vite-node@1.6.1(@types/node@20.19.7)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0):
dependencies:
cac: 6.7.14
debug: 4.4.1(supports-color@10.0.0)
pathe: 1.1.2
picocolors: 1.1.1
vite: 5.4.19(@types/node@22.16.0)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)
vite: 5.4.19(@types/node@20.19.7)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)
transitivePeerDependencies:
- '@types/node'
- less
@ -23598,13 +23619,13 @@ snapshots:
- supports-color
- terser
vite-node@3.2.3(@types/node@22.16.0)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)(tsx@4.17.0)(yaml@2.8.0):
vite-node@3.2.3(@types/node@20.19.7)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)(tsx@4.20.4)(yaml@2.8.0):
dependencies:
cac: 6.7.14
debug: 4.4.1(supports-color@10.0.0)
es-module-lexer: 1.7.0
pathe: 2.0.3
vite: 6.3.5(@types/node@22.16.0)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)(tsx@4.17.0)(yaml@2.8.0)
vite: 6.3.5(@types/node@20.19.7)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)(tsx@4.20.4)(yaml@2.8.0)
transitivePeerDependencies:
- '@types/node'
- jiti
@ -23619,31 +23640,31 @@ snapshots:
- tsx
- yaml
vite-tsconfig-paths@4.3.2(typescript@5.8.3)(vite@6.3.5(@types/node@22.16.0)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)(tsx@4.17.0)(yaml@2.8.0)):
vite-tsconfig-paths@4.3.2(typescript@5.8.3)(vite@6.3.5(@types/node@20.19.7)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)(tsx@4.20.4)(yaml@2.8.0)):
dependencies:
debug: 4.4.1(supports-color@10.0.0)
globrex: 0.1.2
tsconfck: 3.1.6(typescript@5.8.3)
optionalDependencies:
vite: 6.3.5(@types/node@22.16.0)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)(tsx@4.17.0)(yaml@2.8.0)
vite: 6.3.5(@types/node@20.19.7)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)(tsx@4.20.4)(yaml@2.8.0)
transitivePeerDependencies:
- supports-color
- typescript
vite@5.4.19(@types/node@22.16.0)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0):
vite@5.4.19(@types/node@20.19.7)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0):
dependencies:
esbuild: 0.21.5
postcss: 8.5.5
rollup: 4.43.0
optionalDependencies:
'@types/node': 22.16.0
'@types/node': 20.19.7
fsevents: 2.3.3
less: 4.4.0
lightningcss: 1.30.1
sass: 1.89.2
terser: 5.42.0
vite@6.3.5(@types/node@22.16.0)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)(tsx@4.17.0)(yaml@2.8.0):
vite@6.3.5(@types/node@20.19.7)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)(tsx@4.20.4)(yaml@2.8.0):
dependencies:
esbuild: 0.25.5
fdir: 6.4.6(picomatch@4.0.2)
@ -23652,14 +23673,14 @@ snapshots:
rollup: 4.43.0
tinyglobby: 0.2.14
optionalDependencies:
'@types/node': 22.16.0
'@types/node': 20.19.7
fsevents: 2.3.3
jiti: 2.4.2
less: 4.4.0
lightningcss: 1.30.1
sass: 1.89.2
terser: 5.42.0
tsx: 4.17.0
tsx: 4.20.4
yaml: 2.8.0
w3c-keyname@2.2.8: {}