First running version

This commit is contained in:
qingjie.du
2026-03-30 17:39:13 +09:00
parent 5ffea3d849
commit bce2a5672c
67 changed files with 16503 additions and 0 deletions

View File

@@ -0,0 +1,42 @@
[
{
"customerId": "C001",
"name": "田中 花子",
"email": "hanako.tanaka@example.com",
"cancelTendency": "high",
"cancelTendencyReason": "雨天キャンセル傾向",
"reservations": ["R001", "R003"]
},
{
"customerId": "C002",
"name": "山田 太郎",
"email": "taro.yamada@example.com",
"cancelTendency": "high",
"cancelTendencyReason": "雨天キャンセル傾向",
"reservations": ["R002"]
},
{
"customerId": "C003",
"name": "鈴木 一郎",
"email": "ichiro.suzuki@example.com",
"cancelTendency": "high",
"cancelTendencyReason": "雨天キャンセル傾向",
"reservations": ["R004"]
},
{
"customerId": "C004",
"name": "佐藤 美咲",
"email": "misaki.sato@example.com",
"cancelTendency": "high",
"cancelTendencyReason": "雨天キャンセル傾向",
"reservations": ["R005"]
},
{
"customerId": "C005",
"name": "伊藤 健",
"email": "ken.ito@example.com",
"cancelTendency": "high",
"cancelTendencyReason": "雨天キャンセル傾向",
"reservations": ["R006"]
}
]

View File

@@ -0,0 +1,37 @@
{
"items": [
{
"itemId": "DESSERT_CHOCO",
"name": "チョコレートケーキ",
"category": "dessert",
"stock": 25,
"unit": "個",
"costPerUnit": 350
},
{
"itemId": "DESSERT_PUDDING",
"name": "プリン",
"category": "dessert",
"stock": 40,
"unit": "個",
"costPerUnit": 200
},
{
"itemId": "DESSERT_TIRAMISU",
"name": "ティラミス",
"category": "dessert",
"stock": 18,
"unit": "個",
"costPerUnit": 420
},
{
"itemId": "DRINK_WINE",
"name": "赤ワイン",
"category": "drink",
"stock": 12,
"unit": "本",
"costPerUnit": 1500
}
],
"updatedAt": "2026-03-27T06:00:00Z"
}

View File

@@ -0,0 +1,62 @@
[
{
"reservationId": "R001",
"customerId": "C001",
"customerName": "田中 花子",
"date": "2026-03-28",
"time": "18:00",
"partySize": 2,
"status": "confirmed",
"notes": ""
},
{
"reservationId": "R002",
"customerId": "C002",
"customerName": "山田 太郎",
"date": "2026-03-28",
"time": "19:00",
"partySize": 4,
"status": "confirmed",
"notes": ""
},
{
"reservationId": "R003",
"customerId": "C001",
"customerName": "田中 花子",
"date": "2026-03-28",
"time": "19:30",
"partySize": 2,
"status": "confirmed",
"notes": "アレルギー: ナッツ"
},
{
"reservationId": "R004",
"customerId": "C003",
"customerName": "鈴木 一郎",
"date": "2026-03-28",
"time": "20:00",
"partySize": 3,
"status": "confirmed",
"notes": ""
},
{
"reservationId": "R005",
"customerId": "C004",
"customerName": "佐藤 美咲",
"date": "2026-03-28",
"time": "18:30",
"partySize": 2,
"status": "confirmed",
"notes": ""
},
{
"reservationId": "R006",
"customerId": "C005",
"customerName": "伊藤 健",
"date": "2026-03-28",
"time": "21:00",
"partySize": 1,
"status": "confirmed",
"notes": ""
}
]

View File

@@ -0,0 +1,16 @@
{
"location": "Tokyo",
"forecasts": [
{
"date": "2026-03-28",
"condition": "rainy",
"conditionJa": "雨",
"tempHigh": 14,
"tempLow": 9,
"precipitationMm": 12,
"precipitationProbability": 90,
"summary": "明日は一日を通して雨が降る見込みです。最高気温14度、傘の持参をお勧めします。"
}
],
"updatedAt": "2026-03-27T06:00:00Z"
}

View File

@@ -0,0 +1,200 @@
/**
* Mock Service Server — port 5100
*
* Simulates external/internal enterprise systems for the reference implementation.
* MCPlet handlers call these endpoints; replacing with real services requires only
* updating the mockServiceUrl in reference.yaml.
*/
import http from 'node:http';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const dataDir = path.join(__dirname, 'data');
const PORT = 5100;
// ---- Data loaders ----
function loadJson<T>(filename: string): T {
return JSON.parse(fs.readFileSync(path.join(dataDir, filename), 'utf-8')) as T;
}
interface Customer {
customerId: string;
name: string;
email: string;
cancelTendency: string;
cancelTendencyReason: string;
reservations: string[];
}
interface Reservation {
reservationId: string;
customerId: string;
customerName: string;
date: string;
time: string;
partySize: number;
status: string;
notes: string;
}
interface SentEmail {
to: string;
subject: string;
body: string;
sentAt: string;
}
interface SentPost {
platform: string;
content: string;
postedAt: string;
}
// In-memory log for mock sends
const sentEmails: SentEmail[] = [];
const sentPosts: SentPost[] = [];
// ---- Route handlers ----
function handleCrmCustomers(url: URL, res: http.ServerResponse): void {
const filter = url.searchParams.get('filter');
const customers = loadJson<Customer[]>('customers.json');
const result = filter === 'rain_cancel_tendency'
? customers.filter((c) => c.cancelTendency === 'high')
: customers;
sendJson(res, 200, { customers: result, total: result.length });
}
function handleCrmReservations(_url: URL, res: http.ServerResponse): void {
// Fixed mock: return all reservations regardless of date query
const reservations = loadJson<Reservation[]>('reservations.json');
sendJson(res, 200, { reservations, total: reservations.length });
}
function handleErpInventory(url: URL, res: http.ServerResponse): void {
const item = url.searchParams.get('item');
const data = loadJson<{ items: Array<{ category: string }> }>('inventory.json');
const items = item
? data.items.filter((i) => i.category === item || i.category.includes(item))
: data.items;
sendJson(res, 200, { items, total: items.length });
}
function handleWeatherForecast(_url: URL, res: http.ServerResponse): void {
// Fixed mock: always return rainy forecast regardless of date query
const data = loadJson<{ location: string; forecasts: Array<{ date: string }> }>('weather.json');
sendJson(res, 200, { location: data.location, forecasts: data.forecasts });
}
function handleSiteStats(_url: URL, res: http.ServerResponse): void {
sendJson(res, 200, {
site: 'EPARK',
period: 'last_7_days',
pageViews: 12480,
uniqueVisitors: 3920,
reservationPageViews: 2150,
conversionRate: 0.18,
updatedAt: new Date().toISOString(),
});
}
async function handleEmailSend(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
const body = await parseJsonBody<{ to: string; subject: string; body: string }>(req);
const entry: SentEmail = { ...body, sentAt: new Date().toISOString() };
sentEmails.push(entry);
console.log(`[mock-email] Sent to ${body.to}: ${body.subject}`);
sendJson(res, 200, { ok: true, messageId: `mock-${Date.now()}`, sentAt: entry.sentAt });
}
async function handleSnsPost(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
const body = await parseJsonBody<{ platform: string; content: string }>(req);
const entry: SentPost = { ...body, postedAt: new Date().toISOString() };
sentPosts.push(entry);
console.log(`[mock-sns] Posted to ${body.platform}: ${body.content.slice(0, 50)}...`);
sendJson(res, 200, { ok: true, postId: `mock-${Date.now()}`, postedAt: entry.postedAt });
}
function handleSentEmails(_url: URL, res: http.ServerResponse): void {
sendJson(res, 200, { sentEmails, total: sentEmails.length });
}
function handleSentPosts(_url: URL, res: http.ServerResponse): void {
sendJson(res, 200, { sentPosts, total: sentPosts.length });
}
// ---- Router ----
const server = http.createServer((req, res) => {
const url = new URL(req.url ?? '/', `http://localhost:${PORT}`);
const method = req.method ?? 'GET';
try {
if (method === 'GET' && url.pathname === '/crm/customers') {
handleCrmCustomers(url, res); return;
}
if (method === 'GET' && url.pathname === '/crm/reservations') {
handleCrmReservations(url, res); return;
}
if (method === 'GET' && url.pathname === '/erp/inventory') {
handleErpInventory(url, res); return;
}
if (method === 'GET' && url.pathname === '/weather/forecast') {
handleWeatherForecast(url, res); return;
}
if (method === 'GET' && url.pathname === '/site/stats') {
handleSiteStats(url, res); return;
}
if (method === 'POST' && url.pathname === '/email/send') {
void handleEmailSend(req, res); return;
}
if (method === 'POST' && url.pathname === '/sns/post') {
void handleSnsPost(req, res); return;
}
if (method === 'GET' && url.pathname === '/debug/emails') {
handleSentEmails(url, res); return;
}
if (method === 'GET' && url.pathname === '/debug/posts') {
handleSentPosts(url, res); return;
}
sendJson(res, 404, { error: `Not found: ${method} ${url.pathname}` });
} catch (err) {
sendJson(res, 500, { error: (err as Error).message });
}
});
server.listen(PORT, () => {
console.log(`[mock-services] Listening on http://localhost:${PORT}`);
console.log('[mock-services] Endpoints:');
console.log(' GET /crm/customers?filter=rain_cancel_tendency');
console.log(' GET /crm/reservations?date=YYYY-MM-DD');
console.log(' GET /erp/inventory?item=dessert');
console.log(' GET /weather/forecast?date=YYYY-MM-DD');
console.log(' GET /site/stats');
console.log(' POST /email/send');
console.log(' POST /sns/post');
console.log(' GET /debug/emails (sent email log)');
console.log(' GET /debug/posts (sent SNS post log)');
});
// ---- Helpers ----
function sendJson(res: http.ServerResponse, status: number, body: unknown): void {
res.writeHead(status, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(body, null, 2));
}
function parseJsonBody<T>(req: http.IncomingMessage): Promise<T> {
return new Promise((resolve, reject) => {
let data = '';
req.on('data', (chunk: Buffer) => { data += chunk.toString(); });
req.on('end', () => {
try { resolve(JSON.parse(data) as T); }
catch { reject(new Error('Invalid JSON')); }
});
req.on('error', reject);
});
}