Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -294,3 +294,25 @@ export function verifyEcosystemWebhook(
}
```

### 8.2. Crove Post MCP Tool Names & Aliasing Catalog (Tier 2)

Crove Post exposes high-level MCP tool calling via dual-registration (aliasing), supporting upstream Postiz conventions as well as Crove OS ecosystem conventions (`crove_post.*` and `post.*`):

| Capability | Canonical / Modern Alias | Ecosystem Aliases | Upstream Tool Name |
| :--- | :--- | :--- | :--- |
| **Schedule Post** | `schedule_post` | `crove_post_schedule_post`, `post_schedule_post` | `integrationSchedulePostTool` |
| **List Channels** | `list_channels` | `crove_post_list_channels`, `post_list_channels` | `integrationList` |
| **List Posts** | `list_posts` | `crove_post_list_posts`, `post_list_posts`, `get_posts` | `postsListTool` |
| **List Groups** | `list_groups` | `crove_post_list_groups`, `post_list_groups`, `list_customers` | `groupList` |
| **Update Post Settings** | `update_post_settings` | `crove_post_update_post_settings`, `post_settings` | `postSettingsTool` |
| **Trigger Channel Sync** | `trigger_integration` | `crove_post_trigger_integration` | `integrationTriggerTool` |
| **Validate Channel** | `validate_integration` | `crove_post_validate_integration` | `integrationValidationTool` |
| **Upload Media URL** | `upload_from_url` | `crove_post_upload_from_url` | `uploadFromUrlTool` |
| **Generate Image** | `generate_image` | `crove_post_generate_image` | `generateImageTool` |
| **Generate Video** | `generate_video` | `crove_post_generate_video` | `generateVideoTool` |
| **Video Options** | `generate_video_options` | `crove_post_generate_video_options` | `generateVideoOptions` |
| **Video Processing** | `video_function` | `crove_post_video_function` | `videoFunctionTool` |

Agents connecting via MCP (e.g. DOSClaw, Cursor, Claude Desktop, ChatGPT) can invoke tools using either convention without breaking changes.


90 changes: 83 additions & 7 deletions libraries/nestjs-libraries/src/chat/load.tools.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { array, object, string } from 'zod';
import { ModuleRef } from '@nestjs/core';
import { toolList } from '@gitroom/nestjs-libraries/chat/tools/tool.list';
import dayjs from 'dayjs';
import { getBrandConfig } from '@gitroom/helpers/utils/brand.config';

export const AgentState = object({
proverbs: array(string()).default([]),
Expand All @@ -22,7 +23,7 @@ export class LoadToolsService {
constructor(private _moduleRef: ModuleRef) {}

async loadTools() {
return (
const directTools = (
await Promise.all<{ name: string; tool: any }>(
toolList
.map((p) => this._moduleRef.get(p, { strict: false }))
Expand All @@ -38,21 +39,96 @@ export class LoadToolsService {
}),
{} as Record<string, any>
);

// Provide friendly alias mappings (snake_case and prefixed aliases) for MCP agents (DOSClaw, Claude, Cursor)
const aliases: Record<string, string[]> = {
integrationSchedulePostTool: [
'schedule_post',
'crove_post_schedule_post',
'post_schedule_post',
'postiz_schedule_post',
],
integrationList: [
'list_channels',
'list_integrations',
'crove_post_list_channels',
'post_list_channels',
'postiz_list_channels',
],
postsListTool: [
'list_posts',
'get_posts',
'crove_post_list_posts',
'post_list_posts',
'postiz_list_posts',
],
groupList: [
'list_groups',
'list_customers',
'crove_post_list_groups',
'post_list_groups',
],
postSettingsTool: [
'update_post_settings',
'post_settings',
'crove_post_update_post_settings',
],
integrationTriggerTool: [
'trigger_integration',
'crove_post_trigger_integration',
],
integrationValidationTool: [
'validate_integration',
'crove_post_validate_integration',
],
uploadFromUrlTool: [
'upload_from_url',
'crove_post_upload_from_url',
],
generateImageTool: [
'generate_image',
'crove_post_generate_image',
],
generateVideoTool: [
'generate_video',
'crove_post_generate_video',
],
generateVideoOptions: [
'generate_video_options',
'crove_post_generate_video_options',
],
videoFunctionTool: [
'video_function',
'crove_post_video_function',
],
};

const expandedTools = { ...directTools };
for (const [originalName, aliasList] of Object.entries(aliases)) {
if (directTools[originalName]) {
for (const alias of aliasList) {
expandedTools[alias] = directTools[originalName];
}
}
}
Comment on lines +106 to +113

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Assigning the same tool instance reference to multiple alias keys will not work as expected because Mastra registers tools using their internal name property. Since all aliases share the same reference, they will all have the original name (e.g., integrationSchedulePostTool), causing them to overwrite each other or fail to register under their alias names.

To fix this, we should shallow-clone each tool instance and update its internal name property to the respective alias.

Suggested change
const expandedTools = { ...directTools };
for (const [originalName, aliasList] of Object.entries(aliases)) {
if (directTools[originalName]) {
for (const alias of aliasList) {
expandedTools[alias] = directTools[originalName];
}
}
}
const expandedTools = { ...directTools };
for (const [originalName, aliasList] of Object.entries(aliases)) {
const originalTool = directTools[originalName];
if (originalTool) {
for (const alias of aliasList) {
const clonedTool = Object.create(
Object.getPrototypeOf(originalTool),
Object.getOwnPropertyDescriptors(originalTool)
);
clonedTool.name = alias;
expandedTools[alias] = clonedTool;
}
}
}


return expandedTools;
}

async agent() {
async agent(agentId = 'postiz') {
const tools = await this.loadTools();
const brand = getBrandConfig();
return new Agent({
id: 'postiz',
name: 'postiz',
description: 'Agent that helps schedule and list social media posts for users',
id: agentId,
name: agentId,
description: `Agent that helps schedule and list social media posts for users (${brand.name})`,
instructions: ({ requestContext }) => {
const ui: string = requestContext.get('ui' as never);
return `
Global information:
- Date (UTC): ${dayjs().format('YYYY-MM-DD HH:mm:ss')}

You are an agent that helps manage and schedule social media posts for users, you can:
You are an agent that helps manage and schedule social media posts for users (${brand.name}), you can:
- Schedule posts into the future, or now, adding texts, images and videos
- List the posts scheduled between two dates (postsListTool)
- Update the settings of a scheduled post or draft that was not published yet (postSettingsTool)
Expand Down Expand Up @@ -81,7 +157,7 @@ export class LoadToolsService {
- To find or inspect existing posts, use postsListTool with a UTC start and end date - it returns every post scheduled in that window. To cover "all my upcoming posts", pass a wide window starting now.
- To change the provider settings of an existing post that was not published yet (scheduled or draft), first find it with postsListTool, then use postSettingsTool with the post's id. It only updates the settings - the content and the publish date stay as they are - and only the keys you pass are changed (get them with the integrationSchema tool). Show the user which post and which settings will change and get their confirmation first.
- Never open the "modal with populated content" to edit an existing post - that modal only CREATES a new post, so using it to edit would duplicate the post. It is only for brand new posts.
- You can create, schedule and update posts, but you CANNOT delete posts - there is no delete capability. Never offer to delete a post. If the user asks you to delete one, tell them deletion is a destructive action and they should delete it themselves in the Postiz app (the calendar).
- You can create, schedule and update posts, but you CANNOT delete posts - there is no delete capability. Never offer to delete a post. If the user asks you to delete one, tell them deletion is a destructive action and they should delete it themselves in the application (the calendar).
- Between tools, we will reference things like: [output:name] and [input:name] to set the information right.
- When outputting a date for the user, make sure it's human readable with time
- The content of the post, HTML, Each line must be wrapped in <p> here is the possible tags: h1, h2, h3, u, strong, li, ul, p (you can\'t have u and strong together), don't use a "code" box
Expand Down
4 changes: 3 additions & 1 deletion libraries/nestjs-libraries/src/chat/mastra.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ export class MastraService {
new Mastra({
storage: pStore,
agents: {
postiz: await this._loadToolsService.agent(),
postiz: await this._loadToolsService.agent('postiz'),
crove_post: await this._loadToolsService.agent('crove_post'),
post: await this._loadToolsService.agent('post'),
},
logger: new ConsoleLogger({
level: 'info',
Expand Down
14 changes: 13 additions & 1 deletion libraries/nestjs-libraries/src/chat/start.mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@ export const startMcp = async (app: INestApplication) => {
'generateVideoTool',
'generateVideoOptions',
'videoFunctionTool',
'generate_image',
'generate_video',
'generate_video_options',
'video_function',
'crove_post_generate_image',
'crove_post_generate_video',
'crove_post_generate_video_options',
'crove_post_video_function',
];
const claudeTools = Object.fromEntries(
Object.entries(tools).filter(([name]) => !claudeHiddenTools.includes(name))
Expand All @@ -61,7 +69,11 @@ export const startMcp = async (app: INestApplication) => {
name: `${brand.name} MCP`,
version: '1.0.0',
tools,
agents: { postiz: agent },
agents: {
postiz: agent,
crove_post: agent,
post: agent,
},
Comment on lines +72 to +76

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

In mastra.service.ts, three distinct agent instances (postiz, crove_post, and post) are created with their respective IDs and names. However, here you are registering the same postiz agent instance (agent) under all three keys. This causes the other agents to report their ID and name as postiz instead of their respective values.

We should retrieve the correct agent instance for each key from mastra.

Suggested change
agents: {
postiz: agent,
crove_post: agent,
post: agent,
},
agents: {
postiz: mastra.getAgent('postiz'),
crove_post: mastra.getAgent('crove_post'),
post: mastra.getAgent('post'),
},

};

const server = new MCPServer(serverConfig);
Expand Down
Loading