Gatherings MCP Server
by abutbul
Verified
#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ErrorCode,
ListToolsRequestSchema,
McpError,
} from '@modelcontextprotocol/sdk/types.js';
import { exec } from 'child_process';
import { promisify } from 'util';
import path from 'path';
const execAsync = promisify(exec);
class GatheringsServer {
private server: Server;
private pythonPath: string;
constructor() {
this.server = new Server(
{
name: 'gatherings-server',
version: '0.1.0',
},
{
capabilities: {
tools: {},
},
}
);
// Get Python script path from environment or use default
this.pythonPath = process.env.GATHERINGS_SCRIPT || path.join(process.cwd(), 'gatherings.py');
this.setupToolHandlers();
// Error handling
this.server.onerror = (error) => console.error('[MCP Error]', error);
process.on('SIGINT', async () => {
await this.server.close();
process.exit(0);
});
}
private setupToolHandlers() {
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'create_gathering',
description: 'Create a new gathering',
inputSchema: {
type: 'object',
properties: {
gathering_id: {
type: 'string',
description: 'Unique ID for the gathering (format: yyyy-mm-dd-type)',
},
members: {
type: 'number',
description: 'Number of members in the gathering',
},
},
required: ['gathering_id', 'members'],
},
},
{
name: 'add_expense',
description: 'Add an expense for a member',
inputSchema: {
type: 'object',
properties: {
gathering_id: {
type: 'string',
description: 'ID of the gathering',
},
member_name: {
type: 'string',
description: 'Name of the member who paid',
},
amount: {
type: 'number',
description: 'Amount paid by the member',
},
},
required: ['gathering_id', 'member_name', 'amount'],
},
},
{
name: 'calculate_reimbursements',
description: 'Calculate reimbursements for a gathering',
inputSchema: {
type: 'object',
properties: {
gathering_id: {
type: 'string',
description: 'ID of the gathering',
},
},
required: ['gathering_id'],
},
},
{
name: 'record_payment',
description: 'Record a payment made by a member',
inputSchema: {
type: 'object',
properties: {
gathering_id: {
type: 'string',
description: 'ID of the gathering',
},
member_name: {
type: 'string',
description: 'Name of the member making the payment',
},
amount: {
type: 'number',
description: 'Amount paid (negative for reimbursements)',
},
},
required: ['gathering_id', 'member_name', 'amount'],
},
},
{
name: 'rename_member',
description: 'Rename an unnamed member',
inputSchema: {
type: 'object',
properties: {
gathering_id: {
type: 'string',
description: 'ID of the gathering',
},
old_name: {
type: 'string',
description: 'Current name of the member',
},
new_name: {
type: 'string',
description: 'New name for the member',
},
},
required: ['gathering_id', 'old_name', 'new_name'],
},
},
{
name: 'show_gathering',
description: 'Show details of a gathering',
inputSchema: {
type: 'object',
properties: {
gathering_id: {
type: 'string',
description: 'ID of the gathering to display',
},
},
required: ['gathering_id'],
},
},
{
name: 'list_gatherings',
description: 'List all gatherings',
inputSchema: {
type: 'object',
properties: {},
},
},
{
name: 'close_gathering',
description: 'Close a gathering',
inputSchema: {
type: 'object',
properties: {
gathering_id: {
type: 'string',
description: 'ID of the gathering to close',
},
},
required: ['gathering_id'],
},
},
{
name: 'delete_gathering',
description: 'Delete a gathering',
inputSchema: {
type: 'object',
properties: {
gathering_id: {
type: 'string',
description: 'ID of the gathering to delete',
},
force: {
type: 'boolean',
description: 'Force deletion even if gathering is closed',
default: false,
},
},
required: ['gathering_id'],
},
},
{
name: 'add_member',
description: 'Add a new member to a gathering',
inputSchema: {
type: 'object',
properties: {
gathering_id: {
type: 'string',
description: 'ID of the gathering',
},
member_name: {
type: 'string',
description: 'Name of the member to add',
},
},
required: ['gathering_id', 'member_name'],
},
},
{
name: 'remove_member',
description: 'Remove a member from a gathering',
inputSchema: {
type: 'object',
properties: {
gathering_id: {
type: 'string',
description: 'ID of the gathering',
},
member_name: {
type: 'string',
description: 'Name of the member to remove',
},
},
required: ['gathering_id', 'member_name'],
},
},
],
}));
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
// Type guard functions
const isCreateGatheringArgs = (args: any): args is { gathering_id: string; members: number } =>
typeof args === 'object' && args !== null &&
typeof args.gathering_id === 'string' &&
typeof args.members === 'number';
const isGatheringIdArg = (args: any): args is { gathering_id: string } =>
typeof args === 'object' && args !== null &&
typeof args.gathering_id === 'string';
const isExpenseArgs = (args: any): args is { gathering_id: string; member_name: string; amount: number } =>
typeof args === 'object' && args !== null &&
typeof args.gathering_id === 'string' &&
typeof args.member_name === 'string' &&
typeof args.amount === 'number';
const isRenameMemberArgs = (args: any): args is { gathering_id: string; old_name: string; new_name: string } =>
typeof args === 'object' && args !== null &&
typeof args.gathering_id === 'string' &&
typeof args.old_name === 'string' &&
typeof args.new_name === 'string';
const isDeleteGatheringArgs = (args: any): args is { gathering_id: string; force?: boolean } =>
typeof args === 'object' && args !== null &&
typeof args.gathering_id === 'string' &&
(args.force === undefined || typeof args.force === 'boolean');
const isMemberArgs = (args: any): args is { gathering_id: string; member_name: string } =>
typeof args === 'object' && args !== null &&
typeof args.gathering_id === 'string' &&
typeof args.member_name === 'string';
try {
let command = `python3 "${this.pythonPath}" --json`;
switch (name) {
case 'create_gathering':
if (!isCreateGatheringArgs(args)) {
throw new McpError(ErrorCode.InvalidParams, 'Invalid create_gathering arguments');
}
command += ` create "${args.gathering_id}" --members ${args.members}`;
break;
case 'add_expense':
if (!isExpenseArgs(args)) {
throw new McpError(ErrorCode.InvalidParams, 'Invalid add_expense arguments');
}
command += ` add-expense "${args.gathering_id}" "${args.member_name}" ${args.amount}`;
break;
case 'calculate_reimbursements':
if (!isGatheringIdArg(args)) {
throw new McpError(ErrorCode.InvalidParams, 'Invalid calculate_reimbursements arguments');
}
command += ` calculate "${args.gathering_id}"`;
break;
case 'record_payment':
if (!isExpenseArgs(args)) {
throw new McpError(ErrorCode.InvalidParams, 'Invalid record_payment arguments');
}
command += ` record-payment "${args.gathering_id}" "${args.member_name}" ${args.amount}`;
break;
case 'rename_member':
if (!isRenameMemberArgs(args)) {
throw new McpError(ErrorCode.InvalidParams, 'Invalid rename_member arguments');
}
command += ` rename-member "${args.gathering_id}" "${args.old_name}" "${args.new_name}"`;
break;
case 'show_gathering':
if (!isGatheringIdArg(args)) {
throw new McpError(ErrorCode.InvalidParams, 'Invalid show_gathering arguments');
}
command += ` show "${args.gathering_id}"`;
break;
case 'list_gatherings':
command += ' list';
break;
case 'close_gathering':
if (!isGatheringIdArg(args)) {
throw new McpError(ErrorCode.InvalidParams, 'Invalid close_gathering arguments');
}
command += ` close "${args.gathering_id}"`;
break;
case 'delete_gathering':
if (!isDeleteGatheringArgs(args)) {
throw new McpError(ErrorCode.InvalidParams, 'Invalid delete_gathering arguments');
}
command += ` delete "${args.gathering_id}"${args.force ? ' --force' : ''}`;
break;
case 'add_member':
if (!isMemberArgs(args)) {
throw new McpError(ErrorCode.InvalidParams, 'Invalid add_member arguments');
}
command += ` add-member "${args.gathering_id}" "${args.member_name}"`;
break;
case 'remove_member':
if (!isMemberArgs(args)) {
throw new McpError(ErrorCode.InvalidParams, 'Invalid remove_member arguments');
}
command += ` remove-member "${args.gathering_id}" "${args.member_name}"`;
break;
default:
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
}
const { stdout, stderr } = await execAsync(command, {
env: {
...process.env,
GATHERINGS_DB_PATH: process.env.GATHERINGS_DB_PATH || 'gatherings.db',
},
});
if (stderr) {
console.error('[Command Error]', stderr);
}
try {
const result = JSON.parse(stdout);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
isError: !result.success,
};
} catch (e) {
return {
content: [
{
type: 'text',
text: stdout,
},
],
isError: true,
};
}
} catch (error) {
throw new McpError(
ErrorCode.InternalError,
error instanceof Error ? error.message : String(error)
);
}
});
}
async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('Gatherings MCP server running on stdio');
}
}
const server = new GatheringsServer();
server.run().catch(console.error);