windsurfmcp-aws-mcp-server1
Version:
Claude Desktop用のAWS MCPサーバーパッケージです。WebSocketを通じてAWS上のLambda関数に接続し、ZohoのCRM/Desk/Books APIとの連携機能を提供します。
1,663 lines (1,502 loc) • 68.4 kB
JavaScript
#!/usr/bin/env node
// 公式MCPサーバー実装(ESモジュール形式)- Zoho API統合版
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { zohoConfig } from './config/zoho-config.mjs';
import { tokenManager } from './utils/token-manager.mjs';
import { logger, logMessage } from './utils/logger.mjs';
import { zohoCRMClient } from './api/zoho-crm.mjs';
import { zohoDeskClient } from './api/zoho-desk.mjs';
import { zohoBooksClient } from './api/zoho-books.mjs';
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
import fetch from "node-fetch";
import dotenv from "dotenv";
import http from "http";
import url from "url";
// 環境変数を読み込む
dotenv.config();
// __dirnameの代替(ESモジュールでは使用できないため)
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// 設定ファイルのパスを確認
const configPath = 'C:\\Users\\Owner\\AppData\\Roaming\\Claude\\claude_desktop_config.json';
if (fs.existsSync(configPath)) {
const configData = fs.readFileSync(configPath, 'utf8');
const desktopConfig = JSON.parse(configData);
logger.info(`設定ファイルを読み込みました: ${configPath}`);
logger.debug(`設定ファイルの内容: ${JSON.stringify(desktopConfig, null, 2)}`);
} else {
logger.error(`設定ファイルが見つかりません: ${configPath}`);
}
// 環境設定
const isDevelopment = process.env.NODE_ENV !== 'production';
const useMockTokens = isDevelopment && process.env.USE_MOCK_TOKENS === 'true';
logger.info(`実行モード: ${isDevelopment ? '開発' : '本番'}`);
logger.info(`モックトークン使用: ${useMockTokens ? '有効' : '無効'}`);
// 標準出力への直接のログ出力を抑制する関数
function setupConsoleRedirect() {
const originalConsoleLog = console.log;
console.log = function() {
const args = Array.from(arguments).map(arg =>
typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
);
logger.debug(args.join(' '));
};
console._originalLog = originalConsoleLog;
}
// Zoho接続状態を取得する関数
async function getZohoConnectionStatus() {
logger.info('Zoho接続状態を取得しています...');
try {
// トークンの存在確認で接続状態を判断
const crmConnected = await tokenManager.hasValidToken('crm');
const deskConnected = await tokenManager.hasValidToken('desk');
const booksConnected = await tokenManager.hasValidToken('books');
const result = {
crm: {
connected: crmConnected,
lastUpdated: crmConnected ? new Date().toISOString() : null,
authUrl: !crmConnected ? await generateAuthUrl('crm') : null
},
desk: {
connected: deskConnected,
lastUpdated: deskConnected ? new Date().toISOString() : null,
authUrl: !deskConnected ? await generateAuthUrl('desk') : null
},
books: {
connected: booksConnected,
lastUpdated: booksConnected ? new Date().toISOString() : null,
authUrl: !booksConnected ? await generateAuthUrl('books') : null
}
};
logger.info(`Zoho接続状態: CRM=${crmConnected}, Desk=${deskConnected}, Books=${booksConnected}`);
return result;
} catch (error) {
logger.error(`Zoho接続状態の取得に失敗しました: ${error.message}`);
// エラーが発生した場合はデフォルト値を返す
return {
crm: { connected: false, lastUpdated: null, authUrl: await generateAuthUrl('crm') },
desk: { connected: false, lastUpdated: null, authUrl: await generateAuthUrl('desk') },
books: { connected: false, lastUpdated: null, authUrl: await generateAuthUrl('books') }
};
}
}
// ZohoCRMからデータを取得する関数
async function getZohoData(params) {
const { module, filters = {} } = params;
const moduleStr = typeof module === 'string' ? module : String(module);
logger.info(`ZohoCRMから${moduleStr}データを取得しています...`);
try {
// 取得するフィールドを設定
const fields = [
'Owner',
'Created_Time',
'Modified_Time',
'Deal_Name',
'Amount',
'Stage',
'Closing_Date',
'Account_Name',
'Contact_Name',
'Description',
'Pipeline',
'Probability',
'Expected_Revenue',
'Lead_Source',
'Type'
];
// リクエストパラメータを構築
const requestParams = {
page: 1,
per_page: 200,
fields: fields.join(',')
};
// フィルターがある場合は追加
if (Object.keys(filters).length > 0) {
Object.assign(requestParams, filters);
}
logger.debug('リクエストパラメータ:', requestParams);
// データを取得
const result = await zohoCRMClient.getRecords(moduleStr, requestParams);
logger.info(`ZohoCRMの${moduleStr}データ取得が完了しました`);
// 結果をテキスト形式で整形
let formattedResult = `ZohoCRM ${moduleStr}データ:\n\n`;
if (result && result.data && Array.isArray(result.data) && result.data.length > 0) {
formattedResult += `合計: ${result.data.length}件\n\n`;
result.data.forEach((item, index) => {
formattedResult += `【${index + 1}】\n`;
Object.keys(item).forEach(key => {
const value = item[key];
if (typeof value === 'object' && value !== null) {
formattedResult += `${key}:\n`;
Object.keys(value).forEach(subKey => {
formattedResult += ` ${subKey}: ${JSON.stringify(value[subKey])}\n`;
});
} else {
formattedResult += `${key}: ${JSON.stringify(value)}\n`;
}
});
formattedResult += '\n';
});
if (result.info) {
formattedResult += `ページ情報: ${result.info.page}/${Math.ceil(result.info.count / result.info.perPage)}ページ (全${result.info.count}件)\n`;
formattedResult += `次のページがあるか: ${result.info.moreRecords ? 'はい' : 'いいえ'}\n`;
}
} else {
formattedResult += '該当するデータはありません。';
}
const responseObj = {
content: [{ type: 'text', text: formattedResult }]
};
console.error('[DEBUG][getZohoData] return:', JSON.stringify(responseObj));
return responseObj;
} catch (error) {
logger.error(`ZohoCRMの${moduleStr}データ取得中にエラーが発生しました: ${error.message}`);
const errorObj = {
content: [{ type: 'text', text: `エラーが発生しました: ${error.message}` }]
};
console.error('[DEBUG][getZohoData] return:', JSON.stringify(errorObj));
return errorObj;
}
const fallbackObj = { content: [{ type: 'text', text: '不明なエラーが発生しました。' }] };
console.error('[DEBUG][getZohoData] return:', JSON.stringify(fallbackObj));
return fallbackObj;
}
// Zoho Deskからデータを取得する関数
async function getZohoDeskData(params) {
const { module, filters = {} } = params;
logger.info(`Zoho Deskから${module}データを取得しています...`);
try {
// 組織IDを取得(configから取得)
const organizationId = zohoConfig.getServiceConfig('desk').organizationId;
logger.info(`使用する組織ID: ${organizationId}`);
// filtersがJSON文字列として渡された場合はパースする
let parsedFilters = filters || {};
if (typeof parsedFilters === 'string') {
try {
parsedFilters = JSON.parse(parsedFilters);
logger.info(`フィルターをパースしました: ${JSON.stringify(parsedFilters)}`);
} catch (parseError) {
logger.error(`フィルターのパースに失敗しました: ${parseError.message}`);
parsedFilters = {};
}
}
// 日付フィルターの処理
if (parsedFilters && (parsedFilters.date_start || parsedFilters.date_end)) {
logger.info(`日付フィルターが指定されています: 開始=${parsedFilters.date_start}, 終了=${parsedFilters.date_end}`);
}
logger.info(`使用するフィルター: ${JSON.stringify(parsedFilters)}`);
const result = await zohoDeskClient.getData(module, organizationId, parsedFilters);
logger.info(`Zoho Deskの${module}データ取得が完了しました`);
// レスポンスの整形
return {
content: [{
type: 'text',
text: `Zoho Desk ${module}データ:\n${JSON.stringify(result, null, 2)}`
}]
};
} catch (error) {
logger.error(`Zoho Deskの${module}データ取得中にエラーが発生しました: ${error.message}`);
return {
content: [{
type: 'text',
text: `エラーが発生しました: ${error.message}`
}]
};
}
}
// Zoho Booksからデータを取得する関数
async function getZohoBooksData(params) {
const { module, filters = {} } = params;
logger.info(`Zoho Booksから${module}データを取得しています...`);
try {
// 組織IDを取得(configから取得)
const organizationId = zohoConfig.getServiceConfig('books').organizationId;
logger.info(`使用する組織ID: ${organizationId}`);
// filtersがJSON文字列として渡された場合はパースする
let parsedFilters = filters || {};
if (typeof parsedFilters === 'string') {
try {
parsedFilters = JSON.parse(parsedFilters);
logger.info(`フィルターをパースしました: ${JSON.stringify(parsedFilters)}`);
} catch (parseError) {
logger.error(`フィルターのパースに失敗しました: ${parseError.message}`);
parsedFilters = {};
}
}
// 日付フィルターの処理
if (parsedFilters && (parsedFilters.date_start || parsedFilters.date_end)) {
logger.info(`日付フィルターが指定されています: 開始=${parsedFilters.date_start}, 終了=${parsedFilters.date_end}`);
}
logger.info(`使用するフィルター: ${JSON.stringify(parsedFilters)}`);
const result = await zohoBooksClient.getData(module, organizationId, parsedFilters);
logger.info(`Zoho Booksの${module}データ取得が完了しました`);
// レスポンスの整形
return {
content: [{
type: 'text',
text: `Zoho Books ${module}データ:\n${JSON.stringify(result, null, 2)}`
}]
};
} catch (error) {
logger.error(`Zoho Booksの${module}データ取得中にエラーが発生しました: ${error.message}`);
return {
content: [{
type: 'text',
text: `エラーが発生しました: ${error.message}`
}]
};
}
}
/**
* 今月の請求書データを取得
* @param {object} params - パラメータ
* @returns {Promise<object>} 請求書データ
*/
async function getCurrentMonthInvoices(params = {}) {
try {
logger.info('Zoho Booksから今月の請求書データを取得します...');
// アクセストークンを取得
const accessToken = await tokenManager.getValidAccessToken('books');
logger.info(`アクセストークンを取得しました: ${accessToken.substring(0, 10)}...`);
// 組織IDを取得
let organizationId = params.orgId;
if (!organizationId) {
organizationId = zohoConfig.getServiceConfig('books').organizationId;
logger.info(`設定ファイルから組織IDを取得しました: ${organizationId}`);
}
logger.info(`使用する組織ID: ${organizationId}`);
// APIエンドポイントを取得
const apiBaseUrl = 'https://www.zohoapis.com/books/v3';
logger.info(`使用するAPIエンドポイント: ${apiBaseUrl}`);
// 今月の日付範囲を計算
const now = new Date();
const year = now.getFullYear();
const month = now.getMonth() + 1;
// 日付フォーマットをYYYY-MM-DDに変換
const formatDate = (date) => {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
};
// 今月の最初の日と最後の日を取得
const firstDay = new Date(year, month - 1, 1);
const lastDay = new Date(year, month, 0);
const startDate = formatDate(firstDay);
const endDate = formatDate(lastDay);
logger.info(`期間: ${startDate} から ${endDate}`);
// フィルターを設定 - APIの仕様に合わせて正しいパラメータを使用
const filters = {
date_start: startDate,
date_end: endDate
};
logger.info(`フィルター: ${JSON.stringify(filters)}`);
// 請求書データを取得
logger.info('Zoho Booksから請求書データを取得します...');
const invoicesData = await zohoBooksClient.getData('invoices', organizationId, filters);
logger.info(`Zoho Booksのinvoicesデータ取得が完了しました`);
// 請求書データを整形
const invoices = invoicesData.invoices || [];
logger.info(`請求書データ取得が完了しました: ${invoices.length}件`);
// レスポンスを作成 - 簡潔な表示に改善
let responseText = `Zoho Books 今月の請求書データ:\n\n`;
responseText += `使用した組織ID: ${organizationId}\n`;
responseText += `対象期間: ${startDate} から ${endDate}\n\n`;
if (invoices.length > 0) {
responseText += `合計: ${invoices.length}件\n\n`;
// 各請求書の情報を追加
invoices.forEach((invoice, index) => {
responseText += `【${index + 1}】\n`;
responseText += `請求書番号: ${invoice.invoice_number}\n`;
responseText += `顧客名: ${invoice.customer_name}\n`;
responseText += `金額: ${invoice.currency_symbol}${invoice.total}\n`;
responseText += `残高: ${invoice.currency_symbol}${invoice.balance}\n`;
responseText += `日付: ${invoice.date}\n`;
responseText += `期限: ${invoice.due_date}\n`;
responseText += `ステータス: ${invoice.status}\n\n`;
});
} else {
responseText += '今月の請求書データはありません。';
}
return {
content: [
{
type: 'text',
text: responseText
}
]
};
} catch (error) {
logger.error(`Zoho Booksの請求書データ取得中にエラーが発生しました: ${error.message}`);
return {
content: [
{
type: 'text',
text: `エラーが発生しました: ${error.message}`
}
]
};
}
}
// Zoho認証URLを生成する関数
async function generateAuthUrl(service) {
logger.info(`${service}の認証URLを生成しています...`);
try {
// zohoConfigから設定を取得
if (!zohoConfig) {
throw new Error('zohoConfigが初期化されていません');
}
// 設定の有効性を確認
if (!zohoConfig.isConfigValid(service)) {
throw new Error(`${service}の設定が無効です。clientId、clientSecret、redirectUriを確認してください`);
}
const authUrl = zohoConfig.generateAuthUrl(service);
logger.info(`${service}の認証URLを生成しました: ${authUrl}`);
return authUrl;
} catch (error) {
logger.error(`${service}の認証URL生成中にエラーが発生しました: ${error.message}`);
throw error;
}
}
// 認証コードを処理
async function handleAuthCode(service, code) {
try {
logger.info(`${service}の認証コードを処理します: ${code}`);
// サービス設定を取得
const config = zohoConfig.getServiceConfig(service);
// トークンURLを取得
const tokenUrl = zohoConfig.getTokenUrl(service);
// トークンリクエストのパラメータを設定
const params = new URLSearchParams({
code: code,
client_id: config.clientId,
client_secret: config.clientSecret,
redirect_uri: config.redirectUri,
grant_type: 'authorization_code'
});
// トークンリクエストを送信
const response = await fetch(tokenUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: params.toString()
});
// レスポンスを解析
if (!response.ok) {
const errorText = await response.text();
logger.error(`認証エラー: ${response.status} ${errorText}`);
return {
success: false,
error: `認証エラー: ${response.status} ${errorText}`
};
}
const tokenData = await response.json();
// エラーチェック
if (tokenData.error) {
logger.error(`認証エラー: ${tokenData.error}`);
return {
success: false,
error: tokenData.error
};
}
// 現在のタイムスタンプを追加
tokenData.created_at = Math.floor(Date.now() / 1000);
// トークンを保存
await tokenManager.saveTokens(service, tokenData);
logger.info(`${service}の認証が完了しました`);
return {
success: true,
message: `${service}の認証が完了しました`
};
} catch (error) {
logger.error(`認証処理エラー: ${error.message}`);
return {
success: false,
error: error.message
};
}
}
// Zoho認証情報をリセットする関数
async function resetZohoAuth() {
try {
logger.info('Zoho認証情報のリセットを開始します');
// すべてのトークンを削除
await tokenManager.deleteAllTokens();
logger.info('Zoho認証情報のリセットが完了しました');
return {
success: true,
message: 'Zoho認証情報のリセットが完了しました'
};
} catch (error) {
logger.error(`認証情報リセットエラー: ${error.message}`);
return {
success: false,
error: error.message
};
}
}
// サーバー起動時にトークンの有効性を確認する関数
async function validateAndRefreshTokens() {
try {
logger.info('トークンの有効性を確認しています...');
const services = ['crm', 'desk', 'books'];
for (const service of services) {
try {
// トークンの有効性をチェック
const isValid = await tokenManager.hasValidToken(service);
if (isValid) {
logger.info(`${service}の有効なトークンが見つかりました`);
} else {
// リフレッシュトークンが存在するか確認
const hasToken = await tokenManager.hasRefreshToken?.(service);
if (hasToken) {
logger.info(`${service}のアクセストークンが期限切れ。リフレッシュを試みます...`);
if (typeof tokenManager.refreshAccessToken === 'function') {
await tokenManager.refreshAccessToken(service);
logger.info(`${service}のトークンをリフレッシュしました`);
} else {
logger.info(`${service}のトークンリフレッシュ機能が利用できません`);
}
} else {
logger.info(`${service}の有効なリフレッシュトークンがありません。再認証が必要です`);
}
}
} catch (err) {
logger.error(`${service}トークンの検証中にエラーが発生しました: ${err.message}`);
}
}
logger.info('トークン検証処理が完了しました');
} catch (error) {
logger.error(`トークン検証中に一般エラーが発生しました: ${error.message}`);
}
}
// Zoho認証URLを取得する関数
async function getZohoAuthUrl(params) {
try {
if (!params || !params.service) {
throw new Error('serviceパラメータが必要です');
}
const service = params.service;
logger.info(`${service}の認証URLを取得します`);
const authUrl = await generateAuthUrl(service);
logger.info(`${service}の認証URLを生成しました: ${authUrl}`);
return {
success: true,
auth_url: authUrl
};
} catch (error) {
logger.error(`認証URL取得エラー: ${error.message}`);
return {
success: false,
error: error.message
};
}
}
// Zoho認証コードを処理する関数
async function handleZohoAuthCode(service, code) {
try {
logger.info(`${service}の認証コードを処理します`);
const result = await handleAuthCode(service, code);
if (!result.success) {
throw new Error(result.error);
}
logger.info(`${service}の認証コード処理が完了しました`);
return {
success: true,
message: result.message
};
} catch (error) {
logger.error(`認証コード処理エラー: ${error.message}`);
return {
success: false,
error: error.message
};
}
}
// モックトークン生成処理
async function generateMockTokensIfNeeded() {
if (!useMockTokens) {
logger.info('モックトークン生成はスキップされました(本番モードまたは明示的に無効化)');
return;
}
logger.info('モックトークンの生成を開始します');
const services = ['crm', 'desk', 'books'];
for (const service of services) {
try {
const hasToken = await tokenManager.hasValidToken(service);
if (!hasToken) {
logger.info(`${service}の有効なトークンがないため、モックトークンを生成します`);
const mockToken = {
access_token: `mock_token_${service}_${Date.now()}`,
refresh_token: `mock_refresh_${service}_${Date.now()}`,
expires_in: 3600,
created_at: Math.floor(Date.now() / 1000)
};
await tokenManager.saveTokens(service, mockToken);
logger.info(`${service}のモックトークンを生成しました`);
} else {
logger.info(`${service}の有効なトークンが既に存在します`);
}
} catch (error) {
logger.error(`${service}のモックトークン生成中にエラーが発生しました: ${error.message}`);
}
}
}
/**
* Zoho Booksの銀行口座情報を取得
* @param {string} organizationId - 組織ID
* @param {object} filters - 検索フィルター
* @returns {Promise<object>} 銀行口座情報
*/
export async function getZohoBooksBankAccounts(organizationId, filters = {}) {
try {
logger.info("Zoho Booksの銀行口座情報を取得します");
const response = await zohoBooksClient.getBankAccounts(organizationId, filters);
return {
code: 0,
message: "success",
data: response.bankaccounts || []
};
} catch (error) {
logger.error(`Zoho Booksの銀行口座情報取得に失敗しました: ${error.message}`, error);
return {
code: 1,
message: error.message,
data: []
};
}
}
/**
* Zoho Booksの勘定科目情報を取得
* @param {string} organizationId - 組織ID
* @param {object} filters - 検索フィルター
* @returns {Promise<object>} 勘定科目情報
*/
export async function getZohoBooksChartOfAccounts(organizationId, filters = {}) {
try {
logger.info("Zoho Booksの勘定科目情報を取得します");
const response = await zohoBooksClient.getChartOfAccounts(organizationId, filters);
return {
code: 0,
message: "success",
data: response.chartofaccounts || []
};
} catch (error) {
logger.error(`Zoho Booksの勘定科目情報取得に失敗しました: ${error.message}`, error);
return {
code: 1,
message: error.message,
data: []
};
}
}
/**
* Zoho Booksの為替調整情報を取得
* @param {string} organizationId - 組織ID
* @param {object} filters - 検索フィルター
* @returns {Promise<object>} 為替調整情報
*/
export async function getZohoBooksBaseCurrencyAdjustments(organizationId, filters = {}) {
try {
logger.info("Zoho Booksの為替調整情報を取得します");
const response = await zohoBooksClient.getBaseCurrencyAdjustments(organizationId, filters);
return {
code: 0,
message: "success",
data: response.adjustments || []
};
} catch (error) {
logger.error(`Zoho Booksの為替調整情報取得に失敗しました: ${error.message}`, error);
return {
code: 1,
message: error.message,
data: []
};
}
}
async function main() {
try {
// 標準出力へのログ出力をリダイレクト
setupConsoleRedirect();
logger.info("Zoho統合MCPサーバーを初期化中...");
// Claude Desktop設定ファイルを読み込む
const configPath = 'C:\\Users\\Owner\\AppData\\Roaming\\Claude\\claude_desktop_config.json';
let config;
try {
if (fs.existsSync(configPath)) {
const configData = fs.readFileSync(configPath, 'utf8');
config = JSON.parse(configData);
logger.info("Claude Desktop設定ファイルを読み込みました");
} else {
throw new Error("Claude Desktop設定ファイルが見つかりません");
}
} catch (configError) {
logger.error(`設定ファイルの読み込みに失敗しました: ${configError.message}`);
throw configError;
}
// モックトークンの生成
await generateMockTokensIfNeeded();
// トークンの有効性確認は本番モードでのみ実行
if (!isDevelopment) {
await validateAndRefreshTokens();
}
// サーバーの作成
const server = new McpServer({
name: "ZohoIntegratedMCP",
version: "1.0.0"
});
// Zoho接続状態取得ツールを登録
server.tool(
"get_zoho_connection_status",
{},
async () => {
const result = await getZohoConnectionStatus();
return {
content: [
{
type: "text",
text: `Zoho接続状態:\n${JSON.stringify(result, null, 2)}`
}
]
};
}
);
// Zoho認証URL取得ツールを登録
server.tool(
"get_zoho_auth_url",
{
service: z.string().describe("認証するサービス名(crm, desk, books)")
},
async (params) => {
const result = await getZohoAuthUrl(params);
return {
content: [
{
type: "text",
text: `${result.auth_url}`
}
]
};
}
);
// Zoho認証コード処理ツールを登録
server.tool(
"handle_zoho_auth_code",
{
service: z.string().describe("認証するサービス名(crm, desk, books)"),
code: z.string().describe("認証コード")
},
async (params) => {
await handleZohoAuthCode(params.service, params.code);
return {
content: [
{
type: "text",
text: `${params.service}の認証が完了しました。これでAPIを使用できます。`
}
]
};
}
);
// Zoho認証リセットツールを登録
server.tool(
"reset_zoho_auth",
{},
async () => {
await resetZohoAuth();
return {
content: [
{
type: "text",
text: `Zohoの認証情報をリセットしました。再度認証が必要です。`
}
]
};
}
);
// ZohoCRMデータ取得ツールを登録
server.tool(
"get_zoho_data",
{
module: z.string().describe("取得するデータのモジュール名(leads, contacts等)"),
filters: z.record(z.any()).optional().describe("検索フィルター")
},
async (params) => {
// getZohoDataの戻り値({content: [...]})をそのまま返すことでtype/contentの不整合を防止
return await getZohoData(params);
}
);
// Zoho Deskデータ取得ツールを登録
server.tool(
"get_zohodesk_data",
{
module: z.string().describe("取得するデータのモジュール名(tickets, contacts等)"),
filters: z.record(z.any()).optional().describe("検索フィルター")
},
async (params) => {
try {
const result = await getZohoDeskData(params);
return {
content: [
{
type: "text",
text: `Zoho Desk ${params.module}データ:\n${JSON.stringify(result, null, 2)}`
}
]
};
} catch (err) {
console.error(`[Zoho Deskデータ取得エラー]`, err);
return {
content: [
{
type: "text",
text: `Zoho Deskデータ取得エラー: ${err.message || err}`
}
]
};
}
}
);
// 今月のチケット取得ツールを登録
server.tool(
"get_current_month_tickets",
{},
async () => {
const result = await zohoDeskClient.getCurrentMonthTickets();
return {
content: [
{
type: "text",
text: `Zoho Desk 今月のチケットデータ:\n${JSON.stringify(result, null, 2)}`
}
]
};
}
);
// 前月のチケット取得ツールを登録
server.tool(
"get_previous_month_tickets",
{
orgId: z.string().describe("組織ID")
},
async (params) => {
// 組織IDが未指定または不正な場合はconfigから取得
// 常にconfigからDeskの組織IDを使用
const orgId = zohoConfig.getServiceConfig('desk').organizationId;
const result = await getPreviousMonthTickets(orgId);
return {
content: [
{
type: "text",
text: `Zoho Desk 前月のチケットデータ:\n${JSON.stringify(result, null, 2)}`
}
]
};
}
);
// 特定月のチケット取得ツールを登録
server.tool(
"get_tickets_by_month",
{
orgId: z.string().describe("組織ID"),
year: z.string().describe("年(例:2024)"),
month: z.string().describe("月(1〜12)")
},
async (params) => {
const result = await getTicketsByMonth(params.orgId, params.year, params.month);
return {
content: [
{
type: "text",
text: `Zoho Desk ${params.year}年${params.month}月のチケットデータ:\n${JSON.stringify(result, null, 2)}`
}
]
};
}
);
// Zoho Booksデータ取得ツールを登録
server.tool(
"get_zohobooks_data",
{
module: z.string().describe("取得するデータのモジュール名(invoices, customers等)"),
filters: z.record(z.any()).optional().describe("検索フィルター")
},
async (params) => {
// 組織IDが未指定または不正な場合はconfigから取得
// 常にconfigからBooksの組織IDを使用
const orgId = zohoConfig.getServiceConfig('books').organizationId;
const newParams = { ...params, orgId };
const result = await getZohoBooksData(newParams);
return {
content: [
{
type: "text",
text: `Zoho Books ${params.module}データ:\n${JSON.stringify(result, null, 2)}`
}
]
};
}
);
// 今月の請求書取得ツールを登録
server.tool(
"get_current_month_invoices",
{
orgId: z.string().optional().describe("組織ID(省略可能、省略時は設定ファイルの値を使用)")
},
async (params) => {
const result = await getCurrentMonthInvoices(params?.orgId);
return {
content: [
{
type: "text",
text: `Zoho Books 今月の請求書データ:\n${JSON.stringify(result, null, 2)}`
}
]
};
}
);
// Zoho Booksの銀行口座情報取得ツールを登録
server.tool(
"get_zohobooks_bank_accounts",
{
orgId: z.string().describe("組織ID"),
filters: z.record(z.any()).optional().describe("検索フィルター")
},
async (params) => {
try {
const result = await getZohoBooksBankAccounts(params.orgId, params.filters || {});
return {
content: [
{
type: "text",
text: `Zoho Booksの銀行口座情報:\n${JSON.stringify(result, null, 2)}`
}
]
};
} catch (error) {
logger.error("銀行口座情報取得エラー:", error);
return {
content: [
{
type: "text",
text: `銀行口座情報取得エラー: ${error.message}`
}
]
};
}
}
);
// Zoho Booksの勘定科目情報取得ツールを登録
server.tool(
"get_zohobooks_chart_of_accounts",
{
orgId: z.string().describe("組織ID"),
filters: z.record(z.any()).optional().describe("検索フィルター")
},
async (params) => {
try {
const result = await getZohoBooksChartOfAccounts(params.orgId, params.filters || {});
return {
content: [
{
type: "text",
text: `Zoho Booksの勘定科目情報:\n${JSON.stringify(result, null, 2)}`
}
]
};
} catch (error) {
logger.error("勘定科目情報取得エラー:", error);
return {
content: [
{
type: "text",
text: `勘定科目情報取得エラー: ${error.message}`
}
]
};
}
}
);
// Zoho Booksの為替調整情報取得ツールを登録
server.tool(
"get_zohobooks_base_currency_adjustments",
{
orgId: z.string().describe("組織ID"),
filters: z.record(z.any()).optional().describe("検索フィルター")
},
async (params) => {
try {
const result = await getZohoBooksBaseCurrencyAdjustments(params.orgId, params.filters || {});
return {
content: [
{
type: "text",
text: `Zoho Booksの為替調整情報:\n${JSON.stringify(result, null, 2)}`
}
]
};
} catch (error) {
logger.error("為替調整情報取得エラー:", error);
return {
content: [
{
type: "text",
text: `為替調整情報取得エラー: ${error.message}`
}
]
};
}
}
);
// CRM: 任意モジュールのレコード作成
server.tool(
"zoho_crm_create_record",
{
module: z.string().describe("作成対象のモジュール名(例: Leads, Contacts等)"),
data: z.record(z.any()).describe("作成するレコードのデータ")
},
async (params) => {
try {
if (!zohoCRMClient.createRecord) {
throw new Error("CRMレコード作成APIが未実装です");
}
const result = await zohoCRMClient.createRecord(params.module, params.data);
const responseObj = {
content: [
{
type: "text",
text: `ZohoCRMレコード作成結果:\n${JSON.stringify(result, null, 2)}`
}
]
};
console.error('[DEBUG][zoho_crm_create_record] return:', JSON.stringify(responseObj));
return responseObj;
} catch (err) {
const errorObj = {
content: [
{
type: "text",
text: `CRMレコード作成エラー: ${err.message || err}`
}
]
};
console.error('[DEBUG][zoho_crm_create_record] return:', JSON.stringify(errorObj));
return errorObj;
}
}
);
// Desk: チケット作成
server.tool(
"zoho_desk_create_ticket",
{
data: z.record(z.any()).describe("作成するチケットのデータ")
},
async (params) => {
try {
if (!zohoDeskClient.createTicket) {
throw new Error("Deskチケット作成APIが未実装です");
}
// 必須フィールドの定義
const requiredFields = {
subject: "件名",
departmentId: "部門ID",
email: "メールアドレス",
description: "説明",
priority: "優先度",
status: "ステータス"
};
// 不足している必須フィールドをチェック
const missingFields = Object.entries(requiredFields)
.filter(([field]) => !params.data[field])
.map(([_, label]) => label);
if (missingFields.length > 0) {
const errorMessage = `Deskチケット作成エラー: 以下の必須項目が不足しています → ${missingFields.join(', ')}\n\n必須項目の説明:\n` +
Object.entries(requiredFields)
.map(([_, label]) => `- ${label}`)
.join('\n');
const errorObj = {
content: [
{
type: "text",
text: errorMessage
}
]
};
console.error('[DEBUG][zoho_desk_create_ticket] missing:', JSON.stringify(missingFields));
return errorObj;
}
// API仕様に準拠したフィールドのみを送信
const ticketData = {
subject: params.data.subject,
departmentId: params.data.departmentId,
email: params.data.email,
description: params.data.description,
priority: params.data.priority,
status: params.data.status
};
// オプショナルフィールドの追加
if (params.data.contactId) ticketData.contactId = params.data.contactId;
if (params.data.contact) ticketData.contact = params.data.contact;
if (params.data.notes) ticketData.notes = params.data.notes;
// ログ出力強化
console.error('[DEBUG][zoho_desk_create_ticket] sending:', JSON.stringify(ticketData));
try {
const result = await zohoDeskClient.createTicket(ticketData);
if (result && result.code && result.code !== 0 && result.message) {
// APIからエラーが返却された場合はそのままクライアントへ
const errorObj = {
content: [
{
type: "text",
text: `Desk APIエラー: ${result.message}`
}
]
};
console.error('[DEBUG][zoho_desk_create_ticket] api error:', JSON.stringify(result));
return errorObj;
}
const responseObj = {
content: [
{
type: "text",
text: `ZohoDeskチケット作成結果:\n${JSON.stringify(result, null, 2)}`
}
]
};
console.error('[DEBUG][zoho_desk_create_ticket] return:', JSON.stringify(responseObj));
return responseObj;
} catch (err) {
const errorObj = {
content: [
{
type: "text",
text: `Deskチケット作成エラー: ${err.message || err}`
}
]
};
console.error('[DEBUG][zoho_desk_create_ticket] catch:', JSON.stringify(errorObj));
return errorObj;
}
} catch (err) {
const errorObj = {
content: [
{
type: "text",
text: `Deskチケット作成エラー: ${err.message || err}`
}
]
};
console.error('[DEBUG][zoho_desk_create_ticket] return:', JSON.stringify(errorObj));
return errorObj;
}
}
);
// Books: 見積書作成
server.tool(
"zoho_books_create_estimate",
{
orgId: z.string().describe("組織ID"),
data: z.record(z.any()).describe("作成する見積書のデータ")
},
async (params) => {
try {
if (!zohoBooksClient.createEstimate) {
throw new Error("Books見積書作成APIが未実装です");
}
// 組織IDの取得
const organizationId = params.orgId || zohoConfig.getServiceConfig('books').organizationId;
if (!organizationId) {
throw new Error("組織IDが指定されていません");
}
// リクエストデータのログ出力
logger.info(`見積書作成リクエスト: ${JSON.stringify(params.data, null, 2)}`);
// APIリクエストを実行
const result = await zohoBooksClient.createEstimate(organizationId, params.data);
// レスポンスのログ出力
logger.info(`見積書作成レスポンス: ${JSON.stringify(result, null, 2)}`);
// エラーレスポンスの処理
if (result.code && result.code !== 0) {
return {
content: [
{
type: "text",
text: `Books見積書作成エラー: ${result.message}`
}
]
};
}
return {
content: [
{
type: "text",
text: `ZohoBooks見積書作成結果:\n${JSON.stringify(result, null, 2)}`
}
]
};
} catch (err) {
logger.error(`見積書作成エラー: ${err.message}`, err);
return {
content: [
{
type: "text",
text: `Books見積書作成エラー: ${err.message}`
}
]
};
}
}
);
// Books: 受取請求書作成
server.tool(
"zoho_books_create_bill",
{
orgId: z.string().describe("組織ID"),
data: z.record(z.any()).describe("作成する受取請求書のデータ")
},
async (params) => {
try {
if (!zohoBooksClient.createBill) {
throw new Error("Books受取請求書作成APIが未実装です");
}
// 組織IDの取得
const organizationId = params.orgId || zohoConfig.getServiceConfig('books').organizationId;
if (!organizationId) {
throw new Error("組織IDが指定されていません");
}
// リクエストデータのログ出力
logger.info(`受取請求書作成リクエスト: ${JSON.stringify(params.data, null, 2)}`);
// APIリクエストを実行
const result = await zohoBooksClient.createBill(organizationId, params.data);
// レスポンスのログ出力
logger.info(`受取請求書作成レスポンス: ${JSON.stringify(result, null, 2)}`);
// エラーレスポンスの処理
if (result.code && result.code !== 0) {
return {
content: [
{
type: "text",
text: `Books受取請求書作成エラー: ${result.message}`
}
]
};
}
return {
content: [
{
type: "text",
text: `ZohoBooks受取請求書作成結果:\n${JSON.stringify(result, null, 2)}`
}
]
};
} catch (err) {
logger.error(`受取請求書作成エラー: ${err.message}`, err);
return {
content: [
{
type: "text",
text: `Books受取請求書作成エラー: ${err.message}`
}
]
};
}
}
);
// Books: 請求書作成
server.tool(
"zoho_books_create_invoice",
{
orgId: z.string().describe("組織ID"),
data: z.object({
customer_id: z.string().describe("顧客ID"),
line_items: z.array(z.object({
item_id: z.string().describe("商品ID"),
name: z.string().describe("商品名"),
quantity: z.number().describe("数量"),
rate: z.number().describe("単価")
})).describe("明細項目"),
date: z.string().describe("請求書日付(YYYY-MM-DD形式)"),
due_date: z.string().optional().describe("支払期限(YYYY-MM-DD形式)"),
reference_number: z.string().optional().describe("参照番号"),
description: z.string().optional().describe("請求書の説明"),
exchange_rate: z.number().optional().describe("為替レート"),
discount: z.number().optional().describe("割引額"),
is_discount_before_tax: z.boolean().optional().describe("税抜き割引かどうか"),
discount_type: z.string().optional().describe("割引タイプ(item_level, document_level)"),
shipping_charge: z.number().optional().describe("送料"),
adjustment: z.number().optional().describe("調整額"),
adjustment_description: z.string().optional().describe("調整の説明"),
notes: z.string().optional().describe("備考"),
terms: z.string().optional().describe("支払条件"),
salesperson_name: z.string().optional().describe("営業担当者名"),
custom_fields: z.array(z.object({
label: z.string().describe("カスタムフィールド名"),
value: z.any().describe("カスタムフィールド値")
})).optional().describe("カスタムフィールド")
}).describe("請求書データ")
},
async (params) => {
try {
if (!zohoBooksClient.createInvoice) {
throw new Error("Books請求書作成APIが未実装です");
}
// 組織IDの取得
const organizationId = params.orgId || zohoConfig.getServiceConfig('books').organizationId;
if (!organizationId) {
throw new Error("組織IDが指定されていません");
}
// リクエストデータのログ出力
logger.info(`請求書作成リクエスト: ${JSON.stringify(params.data, null, 2)}`);
// APIリクエストを実行
const result = await zohoBooksClient.createInvoice(organizationId, params.data);
// レスポンスのログ出力
logger.info(`請求書作成レスポンス: ${JSON.stringify(result, null, 2)}`);
// エラーレスポンスの処理
if (result.code && result.code !== 0) {
return {
content: [
{
type: "text",
text: `Books請求書作成エラー: ${result.message}`
}
]
};
}
return {
content: [
{
type: "text",
text: `ZohoBooks請求書作成結果:\n${JSON.stringify(result, null, 2)}`
}
]
};
} catch (err) {
logger.error(`請求書作成エラー: ${err.message}`, err);
return {
content: [
{
type: "text",
text: `Books請求書作成エラー: ${err.message}`
}
]
};
}
}
);
// Books: 請求書支払い作成
server.tool(
"zoho_books_create_customer_payment",
{
orgId: z.string().describe("組織ID"),
data: z.object({
customer_id: z.string().describe("顧客ID"),
payment_mode: z.string().describe("支払い方法(check, cash, creditcard, banktransfer, bankremittance, autotransaction, others)"),
amount: z.number().describe("支払い金額"),
date: z.string().describe("支払い日付(YYYY-MM-DD形式)"),
reference_number: z.string().optional().describe("参照番号"),
description: z.string().optional().describe("支払いの説明"),
invoices: z.array(z.object({
invoice_id: z.string().describe("請求書ID"),
amount_applied: z.number().describe("支払い金額")
})).describe("支払い対象の請求書リスト"),
exchange_rate: z.number().optional().describe("為替レート"),
bank_charges: z.number().optional().describe("銀行手数料"),
account_id: z.string().optional().describe("入金口座ID")
}).describe("支払いデータ")
},
async (params) => {
try {
logger.info("請求書支払い作成リクエスト:", { orgId: params.orgId, data: params.data });
const result = await zohoBooksClient.createCustomerPayment(params.orgId, params.data);
logger.info("請求書支払い作成レスポンス:", result);
return {
content: [
{
type: "text",
text: `ZohoBooks請求書支払い作成結果:\n${JSON.stringify(result, null, 2)}`
}
]
};
} catch (error) {
logger.error("請求書支払い作成エラー:", error);
return {
content: [
{
type: "text",
text: `Books請求書支払い作成エラー: ${error.message}`
}
]
};
}
}
);
// Books: 受取請求書支払い作成
server.tool(
"zoho_books_create_vendor_payment",
{
orgId: z.string().describe("組織ID"),
data: z.object({
vendor_id: z.string().describe("仕入先ID"),
amount: z.number().describe("支払い金額"),
paid_through_account_id: z.string().describe("支払い元の口座ID"),
payment_mode: z.string().describe("支払い方法"),
description: z.string().optional().describe("支払いの説明"),
reference_number: z.string().optional().describe("参照番号"),
bills: z.array(z.object({
bill_id: z.string().describe("受取請求書ID"),
amount_applied: z.number().describe("支払い金額")
})).describe("支払い対象の受取請求書リスト")
}).describe("支払いデータ")
},
async (params) => {
try {
logger.info("受取請求書支払い作成リクエスト:", { orgId: params.orgId, data: params.data });
const result = await zohoBooksClient.createVendorPayment(params.orgId, params.data);
logger.info("受取請求書支払い作成レスポンス:", result);
return {
content: [
{
type: "text",
text: `ZohoBooks受取請求書支払い作成結果:\n${JSON.stringify(result, null, 2)}`
}
]
};
} catch (error) {
logger.error("受取請求書支払い作成エラー:", error);
return {
content: [
{
type: "text",
text: `Books受取請求書支払い作成エラー: ${error.message}`
}
]
};
}
}
);
logger.info("MCPサーバーを初期化しました");
// 標準入出力トランスポートを作成
const transport = new StdioServerTransport();
logger.info("標準入出力トランスポートを初期化しました");
logger.info("サーバーを接続しています...");
// サーバーを接続
await server.connect(transport);
logger.info("サーバーが正常に接続されました");
// コールバックサーバーを起動(トークン取得用)
const callbackPort = config.server?.port || 3005;
try {
const callbackServer = http.createServer((req, res) => {
handleCallbackRequest(req, res);
});
callbackServer.on('error', (err) => {
if (err.code === 'EADDRINUSE') {
logger.info(`ポート${callbackPort}は既に使用されています。コールバックサーバーは起動しませんが、既存のサーバーを使用します。`);
} else {
logger.error(`コールバックサーバーの起動中にエラーが発生しました: ${err.message}`);
}
});
callbackServer.listen(callbackPort, () => {
logger.info(`コールバックサーバーがポート${callbackPort}で起動しました`);
});
} catch (serverError) {
logger.error(`コールバックサーバーの起動に失敗しました: ${serverError.message}`);
// コールバックサーバーの失敗はMCPサーバーには影響しないので続行
}
} catch (error) {
logger.error(`エラーが発生しました: ${error.message}`);
logger.error(`スタックトレース: ${error.stack}`);
process.exit(1);
}
}
// プロセスが終了しないようにする
process.on('SIGINT', () => {
logger.info('SIGINTシグナルを受信しました');
// シグナルを無視
});
process.on('SIGTERM', () => {
logger.info('SIGTERMシグナルを受信しました');
// シグナルを無視
});
// 未処理の例外をキャッチ
process.on('uncaughtException', (error) => {
logger.error(`未処理の例外が発生しました: ${error.message}`);
logger.error(`スタックトレース: ${error.stack}`);
// プロセスは終了させない
});
// メインプロセスを実行
main().catch(error => {
logger.error(`致命的なエラーが発生しました: ${error.message}`);
logger.error(`スタックトレース: ${error.stack}`);
process.exit(1);
});
// メッセージを処理する関数
async function handleMessage(message) {
try {
// 受信したメッセージをログに出力(メソッド種別によってレベルを切り替え)
let methodForLog = null;
try {
const parsedTmp = typeof message === 'string' ? JSON.parse(message) : {};
methodForLog = parsedTmp.method;
} catch(e) {}
if (methodForLog === 'resources/list' || methodForLog === 'prompts/list') {
logger.debug(`受信したメッセージ: ${typeof message === 'string' ? message : 'Message is not a string'}`);
} else {
logger.info(`受信したメッセージ: ${typeof message === 'string' ? message : 'Message is not a string'}`);
}
// メッセージの前処理
if (typeof message !== 'string') {
throw new Error('メッセージは文字列である必要があります');
}
let cleanMessage = message.trim();
// メッセージが空の場合はエラーを返す
if (!cleanMessage) {
logger.info('空のメッセージを受信しました');
return JSON.stringify({
jsonrpc: '2.0',
id: null,
error: {
code: -32700,
message: '空のメッセージ'
}
});
}
// メッセージをJSONとしてパース
let request;
try {
request = JSON.parse(cleanMessage);
logger.info(`パースされたリクエスト: ${JSON.stringify(request, null, 2)}`);
} catch (parseError) {
logger.error(`JSONパースエラー: ${parseError.message}`);
logger.error(`問題のメッセージ: ${cleanMessage}`);
return JSON.stringify({
jsonrpc: '2.0',
id: null,
error: {
code: -32700,
message: `Invalid JSON: ${parseError.message}`
}
});
}
// リクエストの基本構造を検証
if (!request.jsonrpc || request.jsonrpc !== '2.0') {
logger.error('[ERROR] Invalid jsonrpc version:', request.jsonrpc);
return JSON.stringify({
jsonrpc: '2.0',
id: request.id || null,
error: {
code: -32600,
message: 'Invalid Request: jsonrpc must be "2.0"'
}
});
}
if (!request.method) {
logger.error('[ERROR] Method is missing in the request');
return JSON.stringify({
jsonrpc: '2.0',
id: request.id || null,
error: {
code