#!/usr/bin/env node
/**
* MCP Server for Postiz Media Management
*
* This server exposes tools for managing Postiz media through the Model Context Protocol (MCP).
* It provides functionality to list, find, and clean up orphaned media files.
*/
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
Tool,
} from '@modelcontextprotocol/sdk/types.js';
import { PostizClient, postizClient } from './postizClient.js';
import { MediaService, createMediaService } from './mediaService.js';
import { config } from './config.js';
/**
* Initialize services
*/
const client = postizClient;
const mediaService = createMediaService(client);
/**
* Define available MCP tools
*/
const TOOLS: Tool[] = [
{
name: 'postiz_list_future_protected_media_ids',
description:
'List all media IDs that are protected (used in future posts with status: draft, scheduled, or queued). These media should NOT be deleted.',
inputSchema: {
type: 'object',
properties: {
statuses: {
type: 'array',
items: { type: 'string' },
description:
'Optional: Post statuses to filter. Defaults to ["draft", "scheduled", "queued"]',
},
},
},
},
{
name: 'postiz_list_all_media',
description: 'List all media items from Postiz (excluding already deleted media).',
inputSchema: {
type: 'object',
properties: {
limit: {
type: 'number',
description: 'Optional: Maximum number of media items to return',
},
},
},
},
{
name: 'postiz_find_orphan_media',
description:
'Find orphaned media (media not used in any future posts). These are candidates for deletion.',
inputSchema: {
type: 'object',
properties: {
limit: {
type: 'number',
description: 'Optional: Maximum number of orphan media to return',
},
},
},
},
{
name: 'postiz_cleanup_orphan_media',
description:
'Clean up orphaned media files. Supports dry-run mode to preview what would be deleted without actually deleting.',
inputSchema: {
type: 'object',
properties: {
dryRun: {
type: 'boolean',
description:
'If true, only simulate deletion and return what would be deleted. Default: false',
default: false,
},
limit: {
type: 'number',
description: 'Optional: Maximum number of media items to clean up',
},
},
},
},
{
name: 'postiz_get_media_stats',
description:
'Get statistics about media usage (total media, protected media, orphan media counts).',
inputSchema: {
type: 'object',
properties: {},
},
},
];
/**
* Create and configure MCP server
*/
const server = new Server(
{
name: 'postiz-media-manager',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
/**
* Handler for listing available tools
*/
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: TOOLS,
};
});
/**
* Handler for tool execution
*/
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case 'postiz_list_future_protected_media_ids': {
const protectedIds = await mediaService.getProtectedMediaIds();
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
protectedMediaIds: Array.from(protectedIds),
count: protectedIds.size,
},
null,
2
),
},
],
};
}
case 'postiz_list_all_media': {
const allMedia = await mediaService.getAllMedia();
const limit = args?.limit as number | undefined;
const mediaToReturn = limit && limit > 0 ? allMedia.slice(0, limit) : allMedia;
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
items: mediaToReturn,
total: allMedia.length,
returned: mediaToReturn.length,
},
null,
2
),
},
],
};
}
case 'postiz_find_orphan_media': {
const limit = args?.limit as number | undefined;
const orphanMedia = await mediaService.findOrphanMedia(limit);
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
items: orphanMedia,
totalCandidates: orphanMedia.length,
},
null,
2
),
},
],
};
}
case 'postiz_cleanup_orphan_media': {
const dryRun = (args?.dryRun as boolean) ?? false;
const limit = args?.limit as number | undefined;
const report = await mediaService.cleanupOrphanMedia({
dryRun,
limit,
});
return {
content: [
{
type: 'text',
text: JSON.stringify(report, null, 2),
},
],
};
}
case 'postiz_get_media_stats': {
const stats = await mediaService.getMediaStats();
return {
content: [
{
type: 'text',
text: JSON.stringify(stats, null, 2),
},
],
};
}
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
error: errorMessage,
tool: name,
},
null,
2
),
},
],
isError: true,
};
}
});
/**
* Start the server
*/
async function main() {
console.error('Starting Postiz Media Manager MCP Server...');
console.error(`Base URL: ${config.postizBaseUrl}`);
console.error('Server ready. Waiting for MCP client connection...\n');
const transport = new StdioServerTransport();
await server.connect(transport);
// Keep the server running
process.on('SIGINT', async () => {
console.error('\nShutting down server...');
await server.close();
process.exit(0);
});
}
main().catch((error) => {
console.error('Fatal error starting server:', error);
process.exit(1);
});