I'm pretty addicted to the Last Of Us series. Sorry about the cover.
😉MCPs are everywhere and for a good reason. It's the next step in the evolution of apps.
Being able to use everything from a single chat without accessing any app. It feels native for Postiz to schedule all your social posts from the chat! So, I started to dig into the Postiz code and added to it!
Postiz
The MCP Repository Is a Bit Weird 🧐
Each MCP has a transport, which is the method the LLMs use to talk to our system.
There are two primary methods at the moment: Stdio, which is basically a command line, and SSE.
There are two primary methods at the moment
I don't really understand why they chose SSE—it's basically a long request that never ends and streams events to the client.
The problem with this method is that to send information back to the server, you must send another post request (as SSE is a one-way communication), which means you must hold the state.
In their example, they hold the state in the app's memory, and guess what? Many complain about memory leaks because the state is not erased when the user disconnects.
I would use WebSockets. They have a built-in sleep mode, and you don't have to maintain a state for it.
Digging In 🪠
I dug into Anthropic typescript SDK and was not amazed. It feels clunky. Many things are not being used in production, like "Resources." The way they require you to keep everything globally in memory is a disaster waiting to happen.
Also, it is tough to implement authentication and get the user from the context so we can get their details.
I implemented my own "Transport" using rxjs observables - it's fun. Postiz is built with NestJS, so when using an SSE route, it closes the observable once it disconnects, allowing you to remove everything from memory.
import EventEmitter from 'events';
import { finalize, fromEvent, startWith } from 'rxjs';
@Injectable()
export class McpService {
static event = new EventEmitter();
constructor(
private _mainMcp: MainMcp
) {
}
async runServer(apiKey: string, organization: string) {
const server = McpSettings.load(organization, this._mainMcp).server();
const transport = new McpTransport(organization);
const observer = fromEvent(
McpService.event,
`organization-${organization}`
).pipe(
startWith({
type: 'endpoint',
data: process.env.NEXT_PUBLIC_BACKEND_URL + '/mcp/' + apiKey + '/messages',
}),
finalize(() => {
transport.close();
})
);
console.log('MCP transport started');
await server.connect(transport);
return observer;
}
async processPostBody(organization: string, body: object) {
const server = McpSettings.load(organization, this._mainMcp).server();
const message = JSONRPCMessageSchema.parse(body);
const transport = new McpTransport(organization);
await server.connect(transport);
transport.handlePostMessage(message);
return {};
}
}
import EventEmitter from 'events';
import { finalize, fromEvent, startWith } from 'rxjs';
@Injectable()
export class McpService {
static event = new EventEmitter();
constructor(
private _mainMcp: MainMcp
) {
}
async runServer(apiKey: string, organization: string) {
const server = McpSettings.load(organization, this._mainMcp).server();
const transport = new McpTransport(organization);
const observer = fromEvent(
McpService.event,
`organization-${organization}`
).pipe(
startWith({
type: 'endpoint',
data: process.env.NEXT_PUBLIC_BACKEND_URL + '/mcp/' + apiKey + '/messages',
}),
finalize(() => {
transport.close();
})
);
console.log('MCP transport started');
await server.connect(transport);
return observer;
}
async processPostBody(organization: string, body: object) {
const server = McpSettings.load(organization, this._mainMcp).server();
const message = JSONRPCMessageSchema.parse(body);
const transport = new McpTransport(organization);
await server.connect(transport);
transport.handlePostMessage(message);
return {};
}
}
Decorators FTW 🎖️
This is for you if you are a big fan of OOP frameworks like NestJS/Laravel/Spring. I created a cool decorator to create tools like API "endpoints."
@McpTool({ toolName: 'POSTIZ_GET_CONFIG_ID' })
async preRun() {
return [
{
type: 'text',
text: `id: ${makeId(10)} Today date is ${dayjs.utc().format()}`,
},
];
}
@McpTool({ toolName: 'POSTIZ_PROVIDERS_LIST' })
async listOfProviders(organization: string) {
const list = (
await this._integrationService.getIntegrationsList(organization)
).map((org) => ({
id: org.id,
name: org.name,
identifier: org.providerIdentifier,
picture: org.picture,
disabled: org.disabled,
profile: org.profile,
customer: org.customer
? {
id: org.customer.id,
name: org.customer.name,
}
: undefined,
}));
return [{ type: 'text', text: JSON.stringify(list) }];
}
@McpTool({
toolName: 'POSTIZ_SCHEDULE_POST',
zod: {
type: eenum(['draft', 'scheduled']),
configId: string(),
generatePictures: boolean(),
date: string().describe('UTC TIME'),
providerId: string().describe('Use POSTIZ_PROVIDERS_LIST to get the id'),
posts: array(object({ text: string(), images: array(string()) })),
},
})
async schedulePost(
organization: string,
obj: {
type: 'draft' | 'schedule';
generatePictures: boolean;
date: string;
providerId: string;
posts: { text: string }[];
}
) {
const create = await this._postsService.createPost(organization, {
date: obj.date,
type: obj.type,
tags: [],
posts: [
{
group: makeId(10),
value: await Promise.all(
obj.posts.map(async (post) => ({
content: post.text,
id: makeId(10),
image: !obj.generatePictures
? []
: [
{
id: makeId(10),
path: await this._openAiService.generateImage(
post.text,
true
),
},
],
}))
),
// @ts-ignore
settings: {},
integration: {
id: obj.providerId,
},
},
],
});
return [
{
type: 'text',
text: `Post created successfully, check it here: ${process.env.FRONTEND_URL}/p/${create[0].postId}`,
},
];
}
@McpTool({ toolName: 'POSTIZ_GET_CONFIG_ID' })
async preRun() {
return [
{
type: 'text',
text: `id: ${makeId(10)} Today date is ${dayjs.utc().format()}`,
},
];
}
@McpTool({ toolName: 'POSTIZ_PROVIDERS_LIST' })
async listOfProviders(organization: string) {
const list = (
await this._integrationService.getIntegrationsList(organization)
).map((org) => ({
id: org.id,
name: org.name,
identifier: org.providerIdentifier,
picture: org.picture,
disabled: org.disabled,
profile: org.profile,
customer: org.customer
? {
id: org.customer.id,
name: org.customer.name,
}
: undefined,
}));
return [{ type: 'text', text: JSON.stringify(list) }];
}
@McpTool({
toolName: 'POSTIZ_SCHEDULE_POST',
zod: {
type: eenum(['draft', 'scheduled']),
configId: string(),
generatePictures: boolean(),
date: string().describe('UTC TIME'),
providerId: string().describe('Use POSTIZ_PROVIDERS_LIST to get the id'),
posts: array(object({ text: string(), images: array(string()) })),
},
})
async schedulePost(
organization: string,
obj: {
type: 'draft' | 'schedule';
generatePictures: boolean;
date: string;
providerId: string;
posts: { text: string }[];
}
) {
const create = await this._postsService.createPost(organization, {
date: obj.date,
type: obj.type,
tags: [],
posts: [
{
group: makeId(10),
value: await Promise.all(
obj.posts.map(async (post) => ({
content: post.text,
id: makeId(10),
image: !obj.generatePictures
? []
: [
{
id: makeId(10),
path: await this._openAiService.generateImage(
post.text,
true
),
},
],
}))
),
// @ts-ignore
settings: {},
integration: {
id: obj.providerId,
},
},
],
});
return [
{
type: 'text',
text: `Post created successfully, check it here: ${process.env.FRONTEND_URL}/p/${create[0].postId}`,
},
];
}
All the code can be found in Postiz here: https://github.com/gitroomhq/postiz-app/tree/main/libraries/nestjs-libraries/src/mcp
https://github.com/gitroomhq/postiz-app/tree/main/libraries/nestjs-libraries/src/mcp
And here: https://github.com/gitroomhq/postiz-app/tree/main/apps/backend/src/mcp
https://github.com/gitroomhq/postiz-app/tree/main/apps/backend/src/mcpForce the LLM to Do Stuff 💪🏻
It would be nice to have a built-in option to force the LLM to do different stuff before it accesses our stuff.
I faced some interesting problems. Whenever I told Cursor to schedule a post for me, it tried to schedule it for 2024. This is the last time the model was trained.
I needed to pass some config details, so I created the POSTIZ_CONFIGURATION_PRERUN
tool. Hopefully, the LLM will always call it before doing stuff.
POSTIZ_CONFIGURATION_PRERUN
But it ignored it many times (typical), so I had to be creative. In my POSTIZ_SCHEDULE_POST
, I added a new property called configId
and changed the config tool name to POSTIZ_GET_CONFIG_ID.
The output of the config is:id: ${makeId(10)} Today date is ${dayjs.utc().format()}
POSTIZ_SCHEDULE_POST
configId
POSTIZ_GET_CONFIG_ID.
id: ${makeId(10)} Today date is ${dayjs.utc().format()}
It forced the LLM always to call it before, and the date was fixed! :)
It was even better for me because I knew it would send me UTC dates from now on.
Use-cases
I think that it works best when it is combined with multiple sets of tools, for example:
- Connect it to Cursor and ask it to schedule a post about your work today.
- Connect it to Notion and ask to schedule all the team's latest work on social media - check out Composio MCPs.
- Connect it to any SaaS that has CopilotKit and schedule posts based on the app.
Postiz MCP
Postiz is the most robust open-source social media scheduling tool - and now the only scheduler that offers MCP (natively, not with Zapier or something like that)
Postiz
With the new MCP, you can schedule all your posts from Cursor/Windsurf and Anthropic clients.
Everything is 100% free, of course. :)
If you like it, please don't forget to star us ⭐️https://github.com/gitroomhq/postiz-app