#!/usr/bin/env node
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";
// 型定義
interface Office {
id: number;
name: string;
floor: string;
layout_data: string;
}
interface Seat {
id: number;
office_id: number;
seat_number: string;
position_x: number;
position_y: number;
}
interface Reservation {
id: number;
user_id: number;
seat_id: number;
start_date: string;
end_date: string;
created_at: string;
user_name?: string;
seat_number?: string;
office_name?: string;
floor?: string;
}
interface Comment {
id: number;
seat_id: number;
name: string;
comment: string;
created_at: string;
seat_number?: string;
office_name?: string;
floor?: string;
}
// APIベースURL
const API_BASE_URL = "http://localhost:5000";
class SeatReservationServer {
private server: Server;
constructor() {
this.server = new Server(
{
name: "seat-reservation-server",
version: "0.1.0",
description: "MCP server for seat reservation system management"
},
{
capabilities: {
tools: {},
},
}
);
this.setupToolHandlers();
this.setupErrorHandling();
}
private setupErrorHandling(): void {
this.server.onerror = (error) => console.error("[MCP Error]", error);
process.on("SIGINT", async () => {
await this.server.close();
process.exit(0);
});
}
private setupToolHandlers(): void {
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
// オフィス関連ツール
{
name: "get_offices",
description: "Get all offices",
inputSchema: {
type: "object",
properties: {},
required: []
}
},
{
name: "get_office",
description: "Get specific office by ID",
inputSchema: {
type: "object",
properties: {
office_id: {
type: "number",
description: "The ID of the office"
}
},
required: ["office_id"]
}
},
{
name: "get_office_seats",
description: "Get all seats for a specific office",
inputSchema: {
type: "object",
properties: {
office_id: {
type: "number",
description: "The ID of the office"
}
},
required: ["office_id"]
}
},
// 予約関連ツール
{
name: "get_reservations",
description: "Get reservations with optional date filtering",
inputSchema: {
type: "object",
properties: {
start_date: {
type: "string",
description: "Start date for filtering (YYYY-MM-DD format)"
},
end_date: {
type: "string",
description: "End date for filtering (YYYY-MM-DD format)"
}
},
required: []
}
},
{
name: "get_reservation",
description: "Get specific reservation by ID",
inputSchema: {
type: "object",
properties: {
reservation_id: {
type: "number",
description: "The ID of the reservation"
}
},
required: ["reservation_id"]
}
},
{
name: "create_reservation",
description: "Create a new seat reservation",
inputSchema: {
type: "object",
properties: {
user_name: {
type: "string",
description: "Name of the user making the reservation"
},
seat_id: {
type: "number",
description: "ID of the seat to reserve"
},
start_date: {
type: "string",
description: "Start date of reservation (YYYY-MM-DD format)"
},
end_date: {
type: "string",
description: "End date of reservation (YYYY-MM-DD format)"
}
},
required: ["user_name", "seat_id", "start_date", "end_date"]
}
},
{
name: "update_reservation",
description: "Update an existing reservation",
inputSchema: {
type: "object",
properties: {
reservation_id: {
type: "number",
description: "ID of the reservation to update"
},
seat_id: {
type: "number",
description: "New seat ID"
},
start_date: {
type: "string",
description: "New start date (YYYY-MM-DD format)"
},
end_date: {
type: "string",
description: "New end date (YYYY-MM-DD format)"
}
},
required: ["reservation_id", "seat_id", "start_date", "end_date"]
}
},
{
name: "cancel_reservation",
description: "Cancel a reservation by ID",
inputSchema: {
type: "object",
properties: {
reservation_id: {
type: "number",
description: "ID of the reservation to cancel"
}
},
required: ["reservation_id"]
}
},
// 座席関連ツール
{
name: "get_available_seats",
description: "Get available seats for a specific period",
inputSchema: {
type: "object",
properties: {
office_id: {
type: "number",
description: "ID of the office"
},
start_date: {
type: "string",
description: "Start date (YYYY-MM-DD format)"
},
end_date: {
type: "string",
description: "End date (YYYY-MM-DD format)"
}
},
required: ["office_id", "start_date", "end_date"]
}
},
{
name: "find_seat_by_number",
description: "Find a seat by seat number in an office",
inputSchema: {
type: "object",
properties: {
office_id: {
type: "number",
description: "ID of the office"
},
seat_number: {
type: "string",
description: "Seat number (e.g., 'A1', 'B2')"
}
},
required: ["office_id", "seat_number"]
}
},
// コメント関連ツール
{
name: "get_all_comments",
description: "Get all comments across all seats",
inputSchema: {
type: "object",
properties: {},
required: []
}
},
{
name: "get_seat_comments",
description: "Get comments for a specific seat",
inputSchema: {
type: "object",
properties: {
seat_id: {
type: "number",
description: "ID of the seat"
}
},
required: ["seat_id"]
}
},
{
name: "add_comment",
description: "Add a comment to a seat",
inputSchema: {
type: "object",
properties: {
seat_id: {
type: "number",
description: "ID of the seat"
},
name: {
type: "string",
description: "Name of the person commenting"
},
comment: {
type: "string",
description: "Comment text"
}
},
required: ["seat_id", "name", "comment"]
}
},
{
name: "delete_comment",
description: "Delete a comment by ID",
inputSchema: {
type: "object",
properties: {
comment_id: {
type: "number",
description: "ID of the comment to delete"
}
},
required: ["comment_id"]
}
},
{
name: "get_comment_counts",
description: "Get comment count for each seat",
inputSchema: {
type: "object",
properties: {},
required: []
}
}
] satisfies Tool[]
};
});
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
if (!args || typeof args !== 'object') {
throw new Error('Invalid arguments provided');
}
switch (name) {
// オフィス関連
case "get_offices":
return await this.getOffices();
case "get_office":
if (typeof args.office_id !== 'number') {
throw new Error('office_id must be a number');
}
return await this.getOffice(args.office_id);
case "get_office_seats":
if (typeof args.office_id !== 'number') {
throw new Error('office_id must be a number');
}
return await this.getOfficeSeats(args.office_id);
// 予約関連
case "get_reservations":
return await this.getReservations(
typeof args.start_date === 'string' ? args.start_date : undefined,
typeof args.end_date === 'string' ? args.end_date : undefined
);
case "get_reservation":
if (typeof args.reservation_id !== 'number') {
throw new Error('reservation_id must be a number');
}
return await this.getReservation(args.reservation_id);
case "create_reservation":
if (typeof args.user_name !== 'string' || typeof args.seat_id !== 'number' ||
typeof args.start_date !== 'string' || typeof args.end_date !== 'string') {
throw new Error('Invalid arguments for create_reservation');
}
return await this.createReservation(args.user_name, args.seat_id, args.start_date, args.end_date);
case "update_reservation":
if (typeof args.reservation_id !== 'number' || typeof args.seat_id !== 'number' ||
typeof args.start_date !== 'string' || typeof args.end_date !== 'string') {
throw new Error('Invalid arguments for update_reservation');
}
return await this.updateReservation(args.reservation_id, args.seat_id, args.start_date, args.end_date);
case "cancel_reservation":
if (typeof args.reservation_id !== 'number') {
throw new Error('reservation_id must be a number');
}
return await this.cancelReservation(args.reservation_id);
// 座席関連
case "get_available_seats":
if (typeof args.office_id !== 'number' || typeof args.start_date !== 'string' ||
typeof args.end_date !== 'string') {
throw new Error('Invalid arguments for get_available_seats');
}
return await this.getAvailableSeats(args.office_id, args.start_date, args.end_date);
case "find_seat_by_number":
if (typeof args.office_id !== 'number' || typeof args.seat_number !== 'string') {
throw new Error('Invalid arguments for find_seat_by_number');
}
return await this.findSeatByNumber(args.office_id, args.seat_number);
// コメント関連
case "get_all_comments":
return await this.getAllComments();
case "get_seat_comments":
if (typeof args.seat_id !== 'number') {
throw new Error('seat_id must be a number');
}
return await this.getSeatComments(args.seat_id);
case "add_comment":
if (typeof args.seat_id !== 'number' || typeof args.name !== 'string' ||
typeof args.comment !== 'string') {
throw new Error('Invalid arguments for add_comment');
}
return await this.addComment(args.seat_id, args.name, args.comment);
case "delete_comment":
if (typeof args.comment_id !== 'number') {
throw new Error('comment_id must be a number');
}
return await this.deleteComment(args.comment_id);
case "get_comment_counts":
return await this.getCommentCounts();
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error";
return {
content: [
{
type: "text",
text: `Error: ${errorMessage}`
}
]
};
}
});
}
private async apiRequest(endpoint: string, options: RequestInit = {}): Promise<any> {
const url = `${API_BASE_URL}/api${endpoint}`;
try {
const response = await fetch(url, {
headers: {
'Content-Type': 'application/json',
...options.headers
},
...options
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(`HTTP ${response.status}: ${errorData.error || response.statusText}`);
}
return await response.json();
} catch (error) {
throw new Error(`API request failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
// オフィス関連メソッド
private async getOffices() {
const offices: Office[] = await this.apiRequest('/offices');
return {
content: [
{
type: "text",
text: `Found ${offices.length} offices:\n\n${JSON.stringify(offices, null, 2)}`
}
]
};
}
private async getOffice(officeId: number) {
const office: Office = await this.apiRequest(`/offices/${officeId}`);
return {
content: [
{
type: "text",
text: `Office details:\n\n${JSON.stringify(office, null, 2)}`
}
]
};
}
private async getOfficeSeats(officeId: number) {
const seats: Seat[] = await this.apiRequest(`/offices/${officeId}/seats`);
return {
content: [
{
type: "text",
text: `Found ${seats.length} seats in office ${officeId}:\n\n${JSON.stringify(seats, null, 2)}`
}
]
};
}
// 予約関連メソッド
private async getReservations(startDate?: string, endDate?: string) {
let endpoint = '/reservations';
const params = new URLSearchParams();
if (startDate) params.append('start_date', startDate);
if (endDate) params.append('end_date', endDate);
if (params.toString()) endpoint += `?${params.toString()}`;
const reservations: Reservation[] = await this.apiRequest(endpoint);
return {
content: [
{
type: "text",
text: `Found ${reservations.length} reservations:\n\n${JSON.stringify(reservations, null, 2)}`
}
]
};
}
private async getReservation(reservationId: number) {
const reservation: Reservation = await this.apiRequest(`/reservations/${reservationId}`);
return {
content: [
{
type: "text",
text: `Reservation details:\n\n${JSON.stringify(reservation, null, 2)}`
}
]
};
}
private async createReservation(userName: string, seatId: number, startDate: string, endDate: string) {
const reservation = await this.apiRequest('/reservations', {
method: 'POST',
body: JSON.stringify({
user_name: userName,
seat_id: seatId,
start_date: startDate,
end_date: endDate
})
});
return {
content: [
{
type: "text",
text: `Reservation created successfully:\n\n${JSON.stringify(reservation, null, 2)}`
}
]
};
}
private async updateReservation(reservationId: number, seatId: number, startDate: string, endDate: string) {
const reservation = await this.apiRequest(`/reservations/${reservationId}`, {
method: 'PUT',
body: JSON.stringify({
seat_id: seatId,
start_date: startDate,
end_date: endDate
})
});
return {
content: [
{
type: "text",
text: `Reservation updated successfully:\n\n${JSON.stringify(reservation, null, 2)}`
}
]
};
}
private async cancelReservation(reservationId: number) {
const result = await this.apiRequest(`/reservations/${reservationId}`, {
method: 'DELETE'
});
return {
content: [
{
type: "text",
text: `Reservation cancelled successfully:\n\n${JSON.stringify(result, null, 2)}`
}
]
};
}
// 座席関連メソッド
private async getAvailableSeats(officeId: number, startDate: string, endDate: string) {
const endpoint = `/seats/available?office_id=${officeId}&start_date=${startDate}&end_date=${endDate}`;
const seats: Seat[] = await this.apiRequest(endpoint);
return {
content: [
{
type: "text",
text: `Found ${seats.length} available seats in office ${officeId} for ${startDate} to ${endDate}:\n\n${JSON.stringify(seats, null, 2)}`
}
]
};
}
private async findSeatByNumber(officeId: number, seatNumber: string) {
const seats: Seat[] = await this.apiRequest(`/offices/${officeId}/seats`);
const seat = seats.find(s => s.seat_number === seatNumber);
if (!seat) {
return {
content: [
{
type: "text",
text: `Seat ${seatNumber} not found in office ${officeId}`
}
]
};
}
return {
content: [
{
type: "text",
text: `Seat ${seatNumber} details:\n\n${JSON.stringify(seat, null, 2)}`
}
]
};
}
// コメント関連メソッド
private async getAllComments() {
const comments: Comment[] = await this.apiRequest('/comments');
return {
content: [
{
type: "text",
text: `Found ${comments.length} comments:\n\n${JSON.stringify(comments, null, 2)}`
}
]
};
}
private async getSeatComments(seatId: number) {
const comments: Comment[] = await this.apiRequest(`/seats/${seatId}/comments`);
return {
content: [
{
type: "text",
text: `Found ${comments.length} comments for seat ${seatId}:\n\n${JSON.stringify(comments, null, 2)}`
}
]
};
}
private async addComment(seatId: number, name: string, comment: string) {
const newComment = await this.apiRequest('/comments', {
method: 'POST',
body: JSON.stringify({
seat_id: seatId,
name: name,
comment: comment
})
});
return {
content: [
{
type: "text",
text: `Comment added successfully:\n\n${JSON.stringify(newComment, null, 2)}`
}
]
};
}
private async deleteComment(commentId: number) {
const result = await this.apiRequest(`/comments/${commentId}`, {
method: 'DELETE'
});
return {
content: [
{
type: "text",
text: `Comment deleted successfully:\n\n${JSON.stringify(result, null, 2)}`
}
]
};
}
private async getCommentCounts() {
const commentCounts = await this.apiRequest('/seats/comments/count');
return {
content: [
{
type: "text",
text: `Comment counts by seat:\n\n${JSON.stringify(commentCounts, null, 2)}`
}
]
};
}
async run(): Promise<void> {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error("Seat Reservation MCP server running on stdio");
}
}
const server = new SeatReservationServer();
server.run().catch(console.error);