mirror of
https://github.com/eliasstepanik/core.git
synced 2026-01-11 18:48:27 +00:00
feat: add default spaces and improve integration, space tools discovery in MCP
This commit is contained in:
parent
f28c8ae2d1
commit
fdc52ffc47
@ -14,14 +14,26 @@ interface CreateWorkspaceDto {
|
||||
const spaceService = new SpaceService();
|
||||
|
||||
const profileRule = `
|
||||
Store the user’s stable, non-sensitive identity and preference facts that improve personalization across assistants. Facts must be long-lived (expected validity ≥ 3 months) and broadly useful across contexts (not app-specific).
|
||||
Purpose: Store my identity and preferences to improve personalization across assistants. It should be broadly useful across contexts (not app-specific).
|
||||
Include (examples):
|
||||
• Preferred name, pronunciation, public handles (GitHub/Twitter/LinkedIn URLs), primary email domain
|
||||
• Timezone, locale, working hours, meeting preferences (async/sync bias, default duration)
|
||||
• Role, team, company, office location (city-level only), seniority
|
||||
• Tooling defaults (editor, ticketing system, repo host), keyboard layout, OS
|
||||
• Communication preferences (tone, brevity vs. detail, summary-first)
|
||||
Exclude: secrets/credentials; one-off or short-term states; health/financial/political/religious/sexual data; precise home address; raw event logs; app-specific analytics; anything the user did not explicitly consent to share.`;
|
||||
Exclude:
|
||||
• Sensitive: secrets, health/financial/political/religious/sexual data, precise address
|
||||
• Temporary: one-off states, troubleshooting sessions, query results
|
||||
• Context-specific: app behaviors, work conversations, project-specific preferences
|
||||
• Meta: discussions about this memory system, AI architecture, system design
|
||||
• Anything not explicitly consented to share
|
||||
don't store anything the user did not explicitly consent to share.`;
|
||||
|
||||
const githubDescription = `Everything related to my GitHub work - repos I'm working on, projects I contribute to, code I'm writing, PRs I'm reviewing. Basically my coding life on GitHub.`;
|
||||
|
||||
const healthDescription = `My health and wellness stuff - how I'm feeling, what I'm learning about my body, experiments I'm trying, patterns I notice. Whatever matters to me about staying healthy.`;
|
||||
|
||||
const fitnessDescription = `My workouts and training - what I'm doing at the gym, runs I'm going on, progress I'm making, goals I'm chasing. Anything related to physical exercise and getting stronger.`;
|
||||
|
||||
export async function createWorkspace(
|
||||
input: CreateWorkspaceDto,
|
||||
@ -43,12 +55,33 @@ export async function createWorkspace(
|
||||
|
||||
await ensureBillingInitialized(workspace.id);
|
||||
|
||||
await spaceService.createSpace({
|
||||
name: "Profile",
|
||||
description: profileRule,
|
||||
userId: input.userId,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
// Create default spaces
|
||||
await Promise.all([
|
||||
spaceService.createSpace({
|
||||
name: "Profile",
|
||||
description: profileRule,
|
||||
userId: input.userId,
|
||||
workspaceId: workspace.id,
|
||||
}),
|
||||
spaceService.createSpace({
|
||||
name: "GitHub",
|
||||
description: githubDescription,
|
||||
userId: input.userId,
|
||||
workspaceId: workspace.id,
|
||||
}),
|
||||
spaceService.createSpace({
|
||||
name: "Health",
|
||||
description: healthDescription,
|
||||
userId: input.userId,
|
||||
workspaceId: workspace.id,
|
||||
}),
|
||||
spaceService.createSpace({
|
||||
name: "Fitness",
|
||||
description: fitnessDescription,
|
||||
userId: input.userId,
|
||||
workspaceId: workspace.id,
|
||||
}),
|
||||
]);
|
||||
|
||||
try {
|
||||
const response = await sendEmail({ email: "welcome", to: user.email });
|
||||
|
||||
@ -40,19 +40,12 @@ async function createMcpServer(
|
||||
},
|
||||
);
|
||||
|
||||
// Dynamic tool listing that includes integration tools
|
||||
// Dynamic tool listing - only expose memory tools and meta-tools
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
// Get integration tools
|
||||
let integrationTools: any[] = [];
|
||||
try {
|
||||
integrationTools =
|
||||
await IntegrationLoader.getAllIntegrationTools(sessionId);
|
||||
} catch (error) {
|
||||
logger.error(`Error loading integration tools: ${error}`);
|
||||
}
|
||||
|
||||
// Only return memory tools (which now includes integration meta-tools)
|
||||
// Integration-specific tools are discovered via get_integration_actions
|
||||
return {
|
||||
tools: [...memoryTools, ...integrationTools],
|
||||
tools: memoryTools,
|
||||
};
|
||||
});
|
||||
|
||||
@ -60,9 +53,21 @@ async function createMcpServer(
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
const { name, arguments: args } = request.params;
|
||||
|
||||
// Handle memory tools
|
||||
if (name.startsWith("memory_")) {
|
||||
return await callMemoryTool(name, args, userId, source);
|
||||
// Handle memory tools and integration meta-tools
|
||||
if (
|
||||
name.startsWith("memory_") ||
|
||||
name === "get_integrations" ||
|
||||
name === "get_integration_actions" ||
|
||||
name === "execute_integration_action"
|
||||
) {
|
||||
// Get workspace for integration tools
|
||||
const workspace = await getWorkspaceByUser(userId);
|
||||
return await callMemoryTool(
|
||||
name,
|
||||
{ ...args, sessionId, workspaceId: workspace?.id },
|
||||
userId,
|
||||
source,
|
||||
);
|
||||
}
|
||||
|
||||
// Handle integration tools (prefixed with integration slug)
|
||||
|
||||
@ -9,6 +9,7 @@ import { triggerSpaceAssignment } from "../spaces/space-assignment";
|
||||
import { prisma } from "../utils/prisma";
|
||||
import { EpisodeType } from "@core/types";
|
||||
import { deductCredits, hasCredits } from "../utils/utils";
|
||||
import { assignEpisodesToSpace } from "~/services/graphModels/space";
|
||||
|
||||
export const IngestBodyRequest = z.object({
|
||||
episodeBody: z.string(),
|
||||
@ -148,23 +149,46 @@ export const ingestTask = task({
|
||||
);
|
||||
}
|
||||
|
||||
// Trigger space assignment after successful ingestion
|
||||
// Handle space assignment after successful ingestion
|
||||
try {
|
||||
logger.info(`Triggering space assignment after successful ingestion`, {
|
||||
userId: payload.userId,
|
||||
workspaceId: payload.workspaceId,
|
||||
episodeId: episodeDetails?.episodeUuid,
|
||||
});
|
||||
if (
|
||||
episodeDetails.episodeUuid &&
|
||||
currentStatus === IngestionStatus.COMPLETED
|
||||
) {
|
||||
await triggerSpaceAssignment({
|
||||
// If spaceId was explicitly provided, immediately assign the episode to that space
|
||||
if (episodeBody.spaceId && episodeDetails.episodeUuid) {
|
||||
logger.info(`Assigning episode to explicitly provided space`, {
|
||||
userId: payload.userId,
|
||||
workspaceId: payload.workspaceId,
|
||||
mode: "episode",
|
||||
episodeIds: episodeUuids,
|
||||
episodeId: episodeDetails.episodeUuid,
|
||||
spaceId: episodeBody.spaceId,
|
||||
});
|
||||
|
||||
await assignEpisodesToSpace(
|
||||
[episodeDetails.episodeUuid],
|
||||
episodeBody.spaceId,
|
||||
payload.userId,
|
||||
);
|
||||
|
||||
logger.info(
|
||||
`Skipping LLM space assignment - episode explicitly assigned to space ${episodeBody.spaceId}`,
|
||||
);
|
||||
} else {
|
||||
// Only trigger automatic LLM space assignment if no explicit spaceId was provided
|
||||
logger.info(
|
||||
`Triggering LLM space assignment after successful ingestion`,
|
||||
{
|
||||
userId: payload.userId,
|
||||
workspaceId: payload.workspaceId,
|
||||
episodeId: episodeDetails?.episodeUuid,
|
||||
},
|
||||
);
|
||||
if (
|
||||
episodeDetails.episodeUuid &&
|
||||
currentStatus === IngestionStatus.COMPLETED
|
||||
) {
|
||||
await triggerSpaceAssignment({
|
||||
userId: payload.userId,
|
||||
workspaceId: payload.workspaceId,
|
||||
mode: "episode",
|
||||
episodeIds: episodeUuids,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (assignmentError) {
|
||||
// Don't fail the ingestion if assignment fails
|
||||
|
||||
@ -201,6 +201,45 @@ export class IntegrationLoader {
|
||||
return allTools;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tools from a specific integration
|
||||
*/
|
||||
static async getIntegrationTools(sessionId: string, integrationSlug: string) {
|
||||
const integrationTransports =
|
||||
TransportManager.getSessionIntegrationTransports(sessionId);
|
||||
|
||||
if (integrationTransports.length === 0) {
|
||||
throw new Error(
|
||||
`No integration transports loaded for session ${sessionId}. Make sure integrations are connected and session is initialized properly.`,
|
||||
);
|
||||
}
|
||||
|
||||
const integrationTransport = integrationTransports.find(
|
||||
(t) => t.slug === integrationSlug,
|
||||
);
|
||||
|
||||
if (!integrationTransport) {
|
||||
const availableSlugs = integrationTransports
|
||||
.map((t) => t.slug)
|
||||
.join(", ");
|
||||
throw new Error(
|
||||
`Integration '${integrationSlug}' not found or not connected. Available integrations: ${availableSlugs}`,
|
||||
);
|
||||
}
|
||||
|
||||
const result = await integrationTransport.client.listTools();
|
||||
|
||||
if (result.tools && Array.isArray(result.tools)) {
|
||||
return result.tools.map((tool: any) => ({
|
||||
name: tool.name,
|
||||
description: tool.description || tool.name,
|
||||
inputSchema: tool.inputSchema,
|
||||
}));
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Call a tool on a specific integration
|
||||
*/
|
||||
|
||||
@ -3,6 +3,7 @@ import { addToQueue } from "~/lib/ingest.server";
|
||||
import { logger } from "~/services/logger.service";
|
||||
import { SearchService } from "~/services/search.server";
|
||||
import { SpaceService } from "~/services/space.server";
|
||||
import { IntegrationLoader } from "./integration-loader";
|
||||
|
||||
const searchService = new SearchService();
|
||||
const spaceService = new SpaceService();
|
||||
@ -48,6 +49,10 @@ const IngestSchema = {
|
||||
type: "string",
|
||||
description: "The data to ingest in text format",
|
||||
},
|
||||
spaceId: {
|
||||
type: "string",
|
||||
description: "Optional: UUID of the space to associate this memory with. If working on a specific project, provide the space ID to organize the memory in that project's context.",
|
||||
},
|
||||
},
|
||||
required: ["message"],
|
||||
};
|
||||
@ -93,6 +98,71 @@ export const memoryTools = [
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "memory_get_space",
|
||||
description:
|
||||
"Get a specific memory space by ID or name. **Purpose**: Retrieve detailed information about a space including its summary, description, and context. **Required**: Provide either spaceId or spaceName. **Returns**: Space details with summary in JSON format",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
spaceId: {
|
||||
type: "string",
|
||||
description: "UUID of the space to retrieve",
|
||||
},
|
||||
spaceName: {
|
||||
type: "string",
|
||||
description: "Name of the space to retrieve (e.g., 'Profile', 'GitHub', 'Health')",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "get_integrations",
|
||||
description:
|
||||
"Get list of connected integrations available for use. Returns integration metadata including name, slug, and whether they have MCP capabilities. Use this to discover what integrations you have access to (e.g., GitHub, Linear, Slack). **Required**: No required parameters. **Returns**: Array of available integrations in JSON format",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "get_integration_actions",
|
||||
description:
|
||||
"Get available actions/tools for a specific integration. Use this after discovering integrations to see what operations you can perform (e.g., for GitHub: get_pr, get_issues, create_issue). **Required**: Provide integration slug. **Returns**: List of available tools/actions with their descriptions and input schemas in JSON format",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
integrationSlug: {
|
||||
type: "string",
|
||||
description: "The slug of the integration (e.g., 'github', 'linear', 'slack')",
|
||||
},
|
||||
},
|
||||
required: ["integrationSlug"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "execute_integration_action",
|
||||
description:
|
||||
"Execute a specific action on an integration. Use this to perform operations like fetching GitHub PRs, creating Linear issues, sending Slack messages, etc. **Required**: Provide integration slug and action name. **Optional**: Provide arguments for the action. **Returns**: Result of the action execution",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
integrationSlug: {
|
||||
type: "string",
|
||||
description: "The slug of the integration (e.g., 'github', 'linear', 'slack')",
|
||||
},
|
||||
action: {
|
||||
type: "string",
|
||||
description: "The action/tool name (e.g., 'get_pr', 'get_issues', 'create_issue')",
|
||||
},
|
||||
arguments: {
|
||||
type: "object",
|
||||
description: "Arguments to pass to the action (structure depends on the specific action)",
|
||||
},
|
||||
},
|
||||
required: ["integrationSlug", "action"],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// Function to call memory tools based on toolName
|
||||
@ -112,6 +182,14 @@ export async function callMemoryTool(
|
||||
return await handleMemoryGetSpaces(userId);
|
||||
case "memory_about_user":
|
||||
return await handleUserProfile(userId);
|
||||
case "memory_get_space":
|
||||
return await handleGetSpace({ ...args, userId });
|
||||
case "get_integrations":
|
||||
return await handleGetIntegrations({ ...args, userId });
|
||||
case "get_integration_actions":
|
||||
return await handleGetIntegrationActions({ ...args });
|
||||
case "execute_integration_action":
|
||||
return await handleExecuteIntegrationAction({ ...args });
|
||||
default:
|
||||
throw new Error(`Unknown memory tool: ${toolName}`);
|
||||
}
|
||||
@ -166,6 +244,7 @@ async function handleMemoryIngest(args: any) {
|
||||
referenceTime: new Date().toISOString(),
|
||||
source: args.source,
|
||||
type: EpisodeTypeEnum.CONVERSATION,
|
||||
spaceId: args.spaceId,
|
||||
},
|
||||
args.userId,
|
||||
);
|
||||
@ -235,11 +314,18 @@ async function handleMemoryGetSpaces(userId: string) {
|
||||
try {
|
||||
const spaces = await spaceService.getUserSpaces(userId);
|
||||
|
||||
// Return id, name, and description for listing
|
||||
const simplifiedSpaces = spaces.map((space) => ({
|
||||
id: space.id,
|
||||
name: space.name,
|
||||
description: space.description,
|
||||
}));
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(spaces),
|
||||
text: JSON.stringify(simplifiedSpaces),
|
||||
},
|
||||
],
|
||||
isError: false,
|
||||
@ -258,3 +344,183 @@ async function handleMemoryGetSpaces(userId: string) {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Handler for memory_get_space
|
||||
async function handleGetSpace(args: any) {
|
||||
try {
|
||||
const { spaceId, spaceName, userId } = args;
|
||||
|
||||
if (!spaceId && !spaceName) {
|
||||
throw new Error("Either spaceId or spaceName is required");
|
||||
}
|
||||
|
||||
let space;
|
||||
if (spaceName) {
|
||||
space = await spaceService.getSpaceByName(spaceName, userId);
|
||||
} else {
|
||||
space = await spaceService.getSpace(spaceId, userId);
|
||||
}
|
||||
|
||||
if (!space) {
|
||||
throw new Error(`Space not found: ${spaceName || spaceId}`);
|
||||
}
|
||||
|
||||
// Return id, name, description, and summary for detailed view
|
||||
const spaceDetails = {
|
||||
id: space.id,
|
||||
name: space.name,
|
||||
description: space.description,
|
||||
summary: space.summary,
|
||||
};
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(spaceDetails),
|
||||
},
|
||||
],
|
||||
isError: false,
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error(`MCP get space error: ${error}`);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error getting space: ${error instanceof Error ? error.message : String(error)}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Handler for get_integrations
|
||||
async function handleGetIntegrations(args: any) {
|
||||
try {
|
||||
const { userId, workspaceId } = args;
|
||||
|
||||
if (!workspaceId) {
|
||||
throw new Error("workspaceId is required");
|
||||
}
|
||||
|
||||
const integrations =
|
||||
await IntegrationLoader.getConnectedIntegrationAccounts(
|
||||
userId,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const simplifiedIntegrations = integrations.map((account) => ({
|
||||
slug: account.integrationDefinition.slug,
|
||||
name: account.integrationDefinition.name,
|
||||
accountId: account.id,
|
||||
hasMcp: !!(account.integrationDefinition.spec?.mcp),
|
||||
}));
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(simplifiedIntegrations),
|
||||
},
|
||||
],
|
||||
isError: false,
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error(`MCP get integrations error: ${error}`);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error getting integrations: ${error instanceof Error ? error.message : String(error)}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Handler for get_integration_actions
|
||||
async function handleGetIntegrationActions(args: any) {
|
||||
try {
|
||||
const { integrationSlug, sessionId } = args;
|
||||
|
||||
if (!integrationSlug) {
|
||||
throw new Error("integrationSlug is required");
|
||||
}
|
||||
|
||||
if (!sessionId) {
|
||||
throw new Error("sessionId is required");
|
||||
}
|
||||
|
||||
const tools = await IntegrationLoader.getIntegrationTools(
|
||||
sessionId,
|
||||
integrationSlug,
|
||||
);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(tools),
|
||||
},
|
||||
],
|
||||
isError: false,
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error(`MCP get integration actions error: ${error}`);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error getting integration actions: ${error instanceof Error ? error.message : String(error)}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Handler for execute_integration_action
|
||||
async function handleExecuteIntegrationAction(args: any) {
|
||||
try {
|
||||
const { integrationSlug, action, arguments: actionArgs, sessionId } = args;
|
||||
|
||||
if (!integrationSlug) {
|
||||
throw new Error("integrationSlug is required");
|
||||
}
|
||||
|
||||
if (!action) {
|
||||
throw new Error("action is required");
|
||||
}
|
||||
|
||||
if (!sessionId) {
|
||||
throw new Error("sessionId is required");
|
||||
}
|
||||
|
||||
const toolName = `${integrationSlug}_${action}`;
|
||||
const result = await IntegrationLoader.callIntegrationTool(
|
||||
sessionId,
|
||||
toolName,
|
||||
actionArgs || {},
|
||||
);
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
logger.error(`MCP execute integration action error: ${error}`);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error executing integration action: ${error instanceof Error ? error.message : String(error)}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user