mirror of
https://github.com/eliasstepanik/core.git
synced 2026-01-10 23:38:27 +00:00
Feat: single mcp server for all integrations
This commit is contained in:
parent
9d34e5d926
commit
256cdb8bdc
@ -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
|
||||
|
||||
@ -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);
|
||||
};
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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" />
|
||||
|
||||
@ -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,
|
||||
);
|
||||
|
||||
@ -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 };
|
||||
|
||||
@ -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);
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@ -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);
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
0
apps/webapp/app/routes/ingest.tsx
Normal file
0
apps/webapp/app/routes/ingest.tsx
Normal 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) {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
};
|
||||
@ -643,7 +643,7 @@ export type HybridAuthenticationResult =
|
||||
userId: string;
|
||||
};
|
||||
|
||||
async function authenticateHybridRequest(
|
||||
export async function authenticateHybridRequest(
|
||||
request: Request,
|
||||
options: { allowJWT?: boolean } = {},
|
||||
): Promise<HybridAuthenticationResult | null> {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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();
|
||||
},
|
||||
});
|
||||
234
apps/webapp/app/trigger/extension/summary.ts
Normal file
234
apps/webapp/app/trigger/extension/summary.ts
Normal 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,
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
@ -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({
|
||||
|
||||
@ -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
|
||||
*/
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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,
|
||||
@ -8,7 +8,7 @@
|
||||
"tailwind.config.js",
|
||||
"tailwind.config.js",
|
||||
"trigger.config.ts",
|
||||
"server.mjs"
|
||||
"server.ts"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"types": ["@remix-run/node", "vite/client"],
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "core",
|
||||
"private": true,
|
||||
"version": "0.1.17",
|
||||
"version": "0.1.18",
|
||||
"workspaces": [
|
||||
"apps/*",
|
||||
"packages/*"
|
||||
|
||||
@ -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"
|
||||
? [
|
||||
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -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
123
pnpm-lock.yaml
generated
@ -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: {}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user