#!/usr/bin/env node
/**
* Copyright 2025 LY Corporation
*
* LINE Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import express from "express";
import cors from "cors";
import * as line from "@line/bot-sdk";
import { LINE_BOT_MCP_SERVER_VERSION, USER_AGENT } from "./version.js";
import CancelRichMenuDefault from "./tools/cancelRichMenuDefault.js";
import PushTextMessage from "./tools/pushTextMessage.js";
import PushFlexMessage from "./tools/pushFlexMessage.js";
import BroadcastTextMessage from "./tools/broadcastTextMessage.js";
import BroadcastFlexMessage from "./tools/broadcastFlexMessage.js";
import GetProfile from "./tools/getProfile.js";
import GetMessageQuota from "./tools/getMessageQuota.js";
import GetRichMenuList from "./tools/getRichMenuList.js";
import DeleteRichMenu from "./tools/deleteRichMenu.js";
import SetRichMenuDefault from "./tools/setRichMenuDefault.js";
// Environment variables
const channelAccessToken = process.env.CHANNEL_ACCESS_TOKEN || "";
const destinationId = process.env.DESTINATION_USER_ID || "";
const transport = process.env.MCP_TRANSPORT || "stdio"; // stdio, sse
const port = parseInt(process.env.MCP_PORT || "3000");
// Store transports by session ID for SSE mode
const transports: { [sessionId: string]: SSEServerTransport } = {};
function createServer(): McpServer {
const server = new McpServer({
name: "line-bot",
version: LINE_BOT_MCP_SERVER_VERSION,
});
const messagingApiClient = new line.messagingApi.MessagingApiClient({
channelAccessToken: channelAccessToken,
defaultHeaders: {
"User-Agent": USER_AGENT,
},
});
// Register all tools
new PushTextMessage(messagingApiClient, destinationId).register(server);
new PushFlexMessage(messagingApiClient, destinationId).register(server);
new BroadcastTextMessage(messagingApiClient).register(server);
new BroadcastFlexMessage(messagingApiClient).register(server);
new GetProfile(messagingApiClient, destinationId).register(server);
new GetMessageQuota(messagingApiClient).register(server);
new GetRichMenuList(messagingApiClient).register(server);
new DeleteRichMenu(messagingApiClient).register(server);
new SetRichMenuDefault(messagingApiClient).register(server);
new CancelRichMenuDefault(messagingApiClient).register(server);
return server;
}
async function startStdioServer() {
const server = createServer();
const stdioTransport = new StdioServerTransport();
await server.connect(stdioTransport);
}
async function startSSEServer() {
const app = express();
// Enable CORS for web clients
app.use(
cors({
origin: true,
credentials: true,
}),
);
app.use(express.json());
// SSE endpoint - establish SSE stream
app.get("/sse", async (_req, res) => {
console.log("Received GET request to /sse (SSE transport)");
const transport = new SSEServerTransport("/messages", res);
transports[transport.sessionId] = transport;
res.on("close", () => {
delete transports[transport.sessionId];
});
const server = createServer();
await server.connect(transport);
});
// POST endpoint for receiving messages
app.post("/messages", async (req, res) => {
const sessionId = req.query.sessionId as string;
const transport = transports[sessionId];
if (transport) {
await transport.handlePostMessage(req, res, req.body);
} else {
res.status(400).send("No transport found for sessionId");
}
});
// Health check endpoint
app.get("/health", (_req, res) => {
res.json({
status: "ok",
transport: "sse",
version: LINE_BOT_MCP_SERVER_VERSION,
});
});
const host = process.env.NODE_ENV === "production" ? "0.0.0.0" : "127.0.0.1";
app.listen(port, host, () => {
console.log(
`LINE Bot MCP Server (SSE) listening on http://${host}:${port}`,
);
console.log(`SSE endpoint: http://${host}:${port}/sse`);
console.log(`Messages endpoint: http://${host}:${port}/messages`);
});
// Handle server shutdown
process.on("SIGINT", async () => {
console.log("Shutting down SSE server...");
for (const sessionId in transports) {
try {
await transports[sessionId].close();
delete transports[sessionId];
} catch (error) {
console.error(
`Error closing transport for session ${sessionId}:`,
error,
);
}
}
process.exit(0);
});
}
async function main() {
if (!process.env.CHANNEL_ACCESS_TOKEN) {
console.error("Please set CHANNEL_ACCESS_TOKEN");
process.exit(1);
}
console.log(`Starting LINE Bot MCP Server with transport: ${transport}`);
switch (transport.toLowerCase()) {
case "sse":
await startSSEServer();
break;
case "stdio":
default:
await startStdioServer();
break;
}
}
main().catch(error => {
console.error("Fatal error in main():", error);
process.exit(1);
});