feat: add default spaces and improve integration, space tools discovery in MCP

This commit is contained in:
Manoj 2025-10-08 23:46:15 +05:30
parent f28c8ae2d1
commit fdc52ffc47
5 changed files with 404 additions and 37 deletions

View File

@ -14,14 +14,26 @@ interface CreateWorkspaceDto {
const spaceService = new SpaceService(); const spaceService = new SpaceService();
const profileRule = ` const profileRule = `
Store the users 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): Include (examples):
Preferred name, pronunciation, public handles (GitHub/Twitter/LinkedIn URLs), primary email domain Preferred name, pronunciation, public handles (GitHub/Twitter/LinkedIn URLs), primary email domain
Timezone, locale, working hours, meeting preferences (async/sync bias, default duration) Timezone, locale, working hours, meeting preferences (async/sync bias, default duration)
Role, team, company, office location (city-level only), seniority Role, team, company, office location (city-level only), seniority
Tooling defaults (editor, ticketing system, repo host), keyboard layout, OS Tooling defaults (editor, ticketing system, repo host), keyboard layout, OS
Communication preferences (tone, brevity vs. detail, summary-first) 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( export async function createWorkspace(
input: CreateWorkspaceDto, input: CreateWorkspaceDto,
@ -43,12 +55,33 @@ export async function createWorkspace(
await ensureBillingInitialized(workspace.id); await ensureBillingInitialized(workspace.id);
await spaceService.createSpace({ // Create default spaces
name: "Profile", await Promise.all([
description: profileRule, spaceService.createSpace({
userId: input.userId, name: "Profile",
workspaceId: workspace.id, 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 { try {
const response = await sendEmail({ email: "welcome", to: user.email }); const response = await sendEmail({ email: "welcome", to: user.email });

View File

@ -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 () => { server.setRequestHandler(ListToolsRequestSchema, async () => {
// Get integration tools // Only return memory tools (which now includes integration meta-tools)
let integrationTools: any[] = []; // Integration-specific tools are discovered via get_integration_actions
try {
integrationTools =
await IntegrationLoader.getAllIntegrationTools(sessionId);
} catch (error) {
logger.error(`Error loading integration tools: ${error}`);
}
return { return {
tools: [...memoryTools, ...integrationTools], tools: memoryTools,
}; };
}); });
@ -60,9 +53,21 @@ async function createMcpServer(
server.setRequestHandler(CallToolRequestSchema, async (request) => { server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params; const { name, arguments: args } = request.params;
// Handle memory tools // Handle memory tools and integration meta-tools
if (name.startsWith("memory_")) { if (
return await callMemoryTool(name, args, userId, source); 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) // Handle integration tools (prefixed with integration slug)

View File

@ -9,6 +9,7 @@ import { triggerSpaceAssignment } from "../spaces/space-assignment";
import { prisma } from "../utils/prisma"; import { prisma } from "../utils/prisma";
import { EpisodeType } from "@core/types"; import { EpisodeType } from "@core/types";
import { deductCredits, hasCredits } from "../utils/utils"; import { deductCredits, hasCredits } from "../utils/utils";
import { assignEpisodesToSpace } from "~/services/graphModels/space";
export const IngestBodyRequest = z.object({ export const IngestBodyRequest = z.object({
episodeBody: z.string(), episodeBody: z.string(),
@ -148,23 +149,46 @@ export const ingestTask = task({
); );
} }
// Trigger space assignment after successful ingestion // Handle space assignment after successful ingestion
try { try {
logger.info(`Triggering space assignment after successful ingestion`, { // If spaceId was explicitly provided, immediately assign the episode to that space
userId: payload.userId, if (episodeBody.spaceId && episodeDetails.episodeUuid) {
workspaceId: payload.workspaceId, logger.info(`Assigning episode to explicitly provided space`, {
episodeId: episodeDetails?.episodeUuid,
});
if (
episodeDetails.episodeUuid &&
currentStatus === IngestionStatus.COMPLETED
) {
await triggerSpaceAssignment({
userId: payload.userId, userId: payload.userId,
workspaceId: payload.workspaceId, episodeId: episodeDetails.episodeUuid,
mode: "episode", spaceId: episodeBody.spaceId,
episodeIds: episodeUuids,
}); });
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) { } catch (assignmentError) {
// Don't fail the ingestion if assignment fails // Don't fail the ingestion if assignment fails

View File

@ -201,6 +201,45 @@ export class IntegrationLoader {
return allTools; 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 * Call a tool on a specific integration
*/ */

View File

@ -3,6 +3,7 @@ import { addToQueue } from "~/lib/ingest.server";
import { logger } from "~/services/logger.service"; import { logger } from "~/services/logger.service";
import { SearchService } from "~/services/search.server"; import { SearchService } from "~/services/search.server";
import { SpaceService } from "~/services/space.server"; import { SpaceService } from "~/services/space.server";
import { IntegrationLoader } from "./integration-loader";
const searchService = new SearchService(); const searchService = new SearchService();
const spaceService = new SpaceService(); const spaceService = new SpaceService();
@ -48,6 +49,10 @@ const IngestSchema = {
type: "string", type: "string",
description: "The data to ingest in text format", 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"], 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 // Function to call memory tools based on toolName
@ -112,6 +182,14 @@ export async function callMemoryTool(
return await handleMemoryGetSpaces(userId); return await handleMemoryGetSpaces(userId);
case "memory_about_user": case "memory_about_user":
return await handleUserProfile(userId); 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: default:
throw new Error(`Unknown memory tool: ${toolName}`); throw new Error(`Unknown memory tool: ${toolName}`);
} }
@ -166,6 +244,7 @@ async function handleMemoryIngest(args: any) {
referenceTime: new Date().toISOString(), referenceTime: new Date().toISOString(),
source: args.source, source: args.source,
type: EpisodeTypeEnum.CONVERSATION, type: EpisodeTypeEnum.CONVERSATION,
spaceId: args.spaceId,
}, },
args.userId, args.userId,
); );
@ -235,11 +314,18 @@ async function handleMemoryGetSpaces(userId: string) {
try { try {
const spaces = await spaceService.getUserSpaces(userId); 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 { return {
content: [ content: [
{ {
type: "text", type: "text",
text: JSON.stringify(spaces), text: JSON.stringify(simplifiedSpaces),
}, },
], ],
isError: false, 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,
};
}
}