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,225 @@
/**
* Lightweight AgentBase for reference implementation.
*
* When instantiated by the platform Host, deps (AgentDeps from platform_impl)
* is passed as the third constructor argument. AgentBase wires callTool to go
* through the platform's MCPlet router with pool-access, rate-limit, and
* Passkey enforcement — without importing platform types directly.
*
* In standalone/test mode, call bindCallTool() to inject a mock implementation.
*/
import { randomUUID } from 'node:crypto';
import type { A2ATaskRequest, A2ATaskResponse } from './platform-types.js';
// Structural shape of the platform deps we need (duck typing, no import required)
interface PlatformDeps {
mcpRouter?: {
callTool?: (
toolName: string,
args: Record<string, unknown>,
) => Promise<{ content: Array<{ type: string; text?: string }> }>;
};
poolRegistry?: {
getAllTools?: () => Array<{
name: string;
meta: {
pool?: string;
mcpletType: string;
auth?: { required?: string; enforcement?: string; promptMessage?: string };
};
}>;
canAgentAccess?: (agentId: string, pool: string | undefined, pools: string[]) => boolean;
checkRateLimit?: (pool: string) => boolean;
};
auditLog?: {
record?: (entry: Record<string, unknown>) => void;
};
passkeyServer?: {
startCeremony?: (message: string) => Promise<unknown>;
};
}
export abstract class AgentBase {
abstract readonly agentId: string;
abstract readonly accessiblePools: string[];
abstract handle(task: A2ATaskRequest): Promise<A2ATaskResponse>;
protected callTool: (
toolName: string,
args: Record<string, unknown>,
) => Promise<unknown>;
/** Set by subclasses (or AgentBase.handle wrapper) before calling tools in a task. */
protected _currentContextId: string | undefined = undefined;
/** Per-contextId assertion cache for 'workflow' enforcement tools. */
protected readonly _assertionCache = new Map<string, unknown>();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
constructor(_agentId?: string, _accessiblePools?: string[], deps?: any) {
if (deps) {
this.callTool = buildPlatformCallTool(this as unknown as AgentBase & { agentId: string; accessiblePools: string[] }, deps as PlatformDeps);
} else {
this.callTool = async (toolName) => {
throw new Error(
`[agent-base] callTool("${toolName}") called without a bound implementation. ` +
'Inject via bindCallTool() or use the platform Host.',
);
};
}
}
/** Bind a tool caller (used in standalone/test mode). */
bindCallTool(fn: (toolName: string, args: Record<string, unknown>) => Promise<unknown>): void {
this.callTool = fn;
}
protected success(task: A2ATaskRequest, result: unknown): A2ATaskResponse {
return {
messageId: randomUUID(),
contextId: task.contextId,
senderId: this.agentId,
recipientId: task.senderId,
timestamp: new Date().toISOString(),
type: 'task_response',
replyToMessageId: task.messageId,
status: 'success',
payload: { result },
};
}
protected error(task: A2ATaskRequest, message: string, code?: string): A2ATaskResponse {
return {
messageId: randomUUID(),
contextId: task.contextId,
senderId: this.agentId,
recipientId: task.senderId,
timestamp: new Date().toISOString(),
type: 'task_response',
replyToMessageId: task.messageId,
status: 'error',
payload: { error: { message, code } },
};
}
protected cancelled(task: A2ATaskRequest, reason: string): A2ATaskResponse {
return {
messageId: randomUUID(),
contextId: task.contextId,
senderId: this.agentId,
recipientId: task.senderId,
timestamp: new Date().toISOString(),
type: 'task_response',
replyToMessageId: task.messageId,
status: 'cancelled',
payload: { error: { message: reason } },
};
}
}
/**
* Build a callTool function that goes through the platform's MCPlet router
* with pool-access, rate-limit, and Passkey enforcement.
*/
function buildPlatformCallTool(
agent: AgentBase & { agentId: string; accessiblePools: string[] },
deps: PlatformDeps,
): (toolName: string, args: Record<string, unknown>) => Promise<unknown> {
// Access protected cache fields via a typed cast (same class file, safe).
const agentCache = agent as unknown as {
_currentContextId: string | undefined;
_assertionCache: Map<string, unknown>;
};
return async (toolName: string, args: Record<string, unknown>) => {
const tools = deps.poolRegistry?.getAllTools?.() ?? [];
const tool = tools.find((t) => t.name === toolName);
// Pool access check
if (tool?.meta.pool) {
const canAccess =
deps.poolRegistry?.canAgentAccess?.(agent.agentId, tool.meta.pool, agent.accessiblePools) ?? true;
if (!canAccess) {
throw new Error(`[${agent.agentId}] Pool access denied for tool "${toolName}" (pool: ${tool.meta.pool})`);
}
}
// Rate limit check
if (tool?.meta.pool) {
const allowed = deps.poolRegistry?.checkRateLimit?.(tool.meta.pool) ?? true;
if (!allowed) {
throw new Error(`[${agent.agentId}] Rate limit exceeded for pool "${tool.meta.pool}"`);
}
}
// Passkey interception for action tools
let callArgs = args;
if (tool?.meta.mcpletType === 'action' && tool.meta.auth?.required === 'passkey') {
const enforcement = tool.meta.auth.enforcement ?? 'strict';
let assertion: unknown;
if (enforcement === 'workflow') {
// Cache key: contextId (scopes assertion to one Director cycle)
const contextId = agentCache._currentContextId ?? 'default';
if (agentCache._assertionCache.has(contextId)) {
assertion = agentCache._assertionCache.get(contextId);
console.log(`[${agent.agentId}] Reusing cached Passkey assertion for contextId="${contextId}" (tool: ${toolName})`);
} else {
assertion = await deps.passkeyServer?.startCeremony?.(
tool.meta.auth.promptMessage ?? `Authorize workflow actions for this task`,
);
if (!assertion) {
throw new Error(`[${agent.agentId}] Passkey authentication cancelled for "${toolName}"`);
}
agentCache._assertionCache.set(contextId, assertion);
console.log(`[${agent.agentId}] Passkey assertion cached for contextId="${contextId}"`);
}
} else {
// 'strict': new ceremony per invocation
assertion = await deps.passkeyServer?.startCeremony?.(
tool.meta.auth.promptMessage ?? `Authorize action: ${toolName}`,
);
if (!assertion) {
throw new Error(`[${agent.agentId}] Passkey authentication cancelled for "${toolName}"`);
}
}
callArgs = { ...args, _mcplet_auth: assertion };
}
// Audit log for action tools
if (tool?.meta.mcpletType === 'action') {
deps.auditLog?.record?.({
type: 'action_invocation',
agentId: agent.agentId,
toolName,
timestamp: new Date().toISOString(),
});
}
if (!deps.mcpRouter?.callTool) {
throw new Error(`[${agent.agentId}] mcpRouter not available for tool "${toolName}"`);
}
const result = await deps.mcpRouter.callTool(toolName, callArgs);
const text = result.content.find((c) => c.type === 'text')?.text ?? '{}';
let parsed: unknown;
try {
parsed = JSON.parse(text);
} catch {
return text;
}
// MCPlet server wraps results in { result, _meta } envelope — unwrap it.
const envelope = parsed as { result?: unknown; error?: { message: string; code: string } };
if (envelope && typeof envelope === 'object' && 'error' in envelope && envelope.error) {
throw new Error(`Tool "${toolName}" returned error: ${envelope.error.message} (${envelope.error.code})`);
}
if (envelope && typeof envelope === 'object' && 'result' in envelope) {
return envelope.result;
}
return parsed;
};
}

View File

@@ -0,0 +1,77 @@
/**
* 発信・発注・発令 Agent
*
* accessiblePools: [media-pool]
* 利用可能ツール: read_site_stats (media-pool, read),
* send_email (media-pool, action, passkey-workflow),
* post_sns (media-pool, action, passkey-workflow)
*
* タスク: 企画・Plan Agent が作成した施策を実行する。
* send_email は action ツール (enforcement: workflow)。
* 最初の send_email 呼び出し時に一度だけ Passkey 認証を行い、
* 同じ contextId 内の後続呼び出しはキャッシュされた assertion を再利用する。
*/
import type { A2ATaskRequest, A2ATaskResponse } from '../platform-types.js';
import { AgentBase } from '../agent-base.js';
interface Plan {
title: string;
targetDate: string;
campaign: { dessertItem: string };
targetCustomers: Array<{ customerId: string; name: string }>;
emailTemplate: string;
}
export class DispatchAgent extends AgentBase {
readonly agentId = 'dispatch-agent';
readonly accessiblePools = ['media-pool'];
async handle(task: A2ATaskRequest): Promise<A2ATaskResponse> {
const { plan } = task.payload.parameters as { plan?: Plan };
if (!plan) {
return this.error(task, 'No plan provided in task parameters');
}
// Set contextId so buildPlatformCallTool can scope the workflow assertion cache
this._currentContextId = task.contextId;
const results: Array<{ customerId: string; name: string; status: string; error?: string }> = [];
for (const customer of plan.targetCustomers) {
try {
// send_email enforcement: 'workflow'. First call in this contextId triggers
// one Passkey ceremony; subsequent calls reuse the cached assertion.
await this.callTool('send_email', {
to: `${customer.customerId}@example.com`,
subject: `【特別ご招待】明日のご来店に無料${plan.campaign.dessertItem}をプレゼント`,
body: plan.emailTemplate,
customerId: customer.customerId,
});
results.push({ customerId: customer.customerId, name: customer.name, status: 'sent' });
console.log(`[dispatch] Email sent to ${customer.name} (${customer.customerId})`);
} catch (err) {
results.push({
customerId: customer.customerId,
name: customer.name,
status: 'failed',
error: (err as Error).message,
});
console.error(`[dispatch] Failed to send to ${customer.name}: ${(err as Error).message}`);
}
}
const sent = results.filter((r) => r.status === 'sent').length;
const failed = results.filter((r) => r.status === 'failed').length;
console.log(`[dispatch] Campaign dispatch complete: ${sent} sent, ${failed} failed`);
return this.success(task, {
campaign: plan.title,
targetDate: plan.targetDate,
dispatch: { sent, failed, total: plan.targetCustomers.length },
results,
});
}
}

View File

@@ -0,0 +1,109 @@
/**
* 情報収集・分析 Agent
*
* accessiblePools: [info-pool]
* 利用可能ツール: fetch_web_content (info-pool), call_external_api (info-pool),
* query_crm (pool-less), query_erp (pool-less), query_hr (pool-less)
*
* タスク: 天気予報・在庫・顧客キャンセル傾向を収集し、分析サマリを返す
*/
import type { A2ATaskRequest, A2ATaskResponse } from '../platform-types.js';
import { AgentBase } from '../agent-base.js';
export class InfoGatheringAgent extends AgentBase {
readonly agentId = 'info-gathering-agent';
readonly accessiblePools = ['info-pool'];
async handle(task: A2ATaskRequest): Promise<A2ATaskResponse> {
const instruction = (task.payload.parameters['instruction'] as string | undefined) ?? '';
const targetDate = new Date().toISOString().slice(0, 10);
try {
// 1. Collect weather forecast
const weather = await this.callTool('fetch_web_content', {
source: 'weather_forecast',
date: targetDate,
});
// 2. Collect dessert inventory via external API
const inventory = await this.callTool('call_external_api', {
api: 'inventory',
params: { item: 'dessert' },
});
// 3. Query CRM for high-cancellation-tendency customers
const customers = await this.callTool('query_crm', {
entity: 'customers',
filter: 'rain_cancel_tendency',
});
// 4. Query reservations for tomorrow
const reservations = await this.callTool('query_crm', {
entity: 'reservations',
filter: `date=${targetDate}`,
});
// 5. Synthesize analysis
const analysis = this.synthesize({ weather, inventory, customers, reservations, targetDate, instruction });
console.log(`[info-gathering] Analysis complete: ${analysis.summary}`);
return this.success(task, {
analysis,
rawData: { weather, inventory, customers, reservations },
});
} catch (err) {
return this.error(task, (err as Error).message);
}
}
private synthesize(data: {
weather: unknown;
inventory: unknown;
customers: unknown;
reservations: unknown;
targetDate: string;
instruction: string;
}): Record<string, unknown> {
const wx = data.weather as { forecasts?: Array<{ condition?: string; summary?: string; precipitationProbability?: number }> };
const forecast = wx?.forecasts?.[0];
const isRainy = forecast?.condition === 'rainy';
const precipProb = forecast?.precipitationProbability ?? 0;
const inv = data.inventory as { items?: Array<{ name?: string; stock?: number; category?: string }> };
const desserts = (inv?.items ?? []).filter((i) => i.category === 'dessert');
const hasDessertStock = desserts.some((d) => (d.stock ?? 0) > 0);
const cust = data.customers as { customers?: Array<unknown>; total?: number };
const highCancelCount = cust?.total ?? (cust?.customers?.length ?? 0);
const res = data.reservations as { reservations?: Array<unknown>; total?: number };
const reservationCount = res?.total ?? (res?.reservations?.length ?? 0);
const actionRecommended = isRainy && hasDessertStock && highCancelCount > 0;
return {
targetDate: data.targetDate,
weather: {
condition: forecast?.condition ?? 'unknown',
isRainy,
precipitationProbability: precipProb,
summary: forecast?.summary ?? '',
},
inventory: {
hasDessertStock,
dessertsAvailable: desserts.map((d) => ({ name: d.name, stock: d.stock })),
},
customers: {
highCancelTendencyCount: highCancelCount,
},
reservations: {
tomorrowCount: reservationCount,
},
actionRecommended,
summary: actionRecommended
? `明日は雨予報(${precipProb}%)で、デザート在庫あり、高キャンセル傾向顧客${highCancelCount}名・予約${reservationCount}件あり。無料デザートキャンペーンを推奨。`
: `現時点でキャンペーン実施条件が揃っていません。`,
};
}
}

View File

@@ -0,0 +1,108 @@
/**
* 企画・Plan Agent
*
* accessiblePools: [] (pool-less のみ)
* 利用可能ツール: query_crm, query_erp, query_hr (全て pool-less)
*
* タスク: 情報収集結果を受け取り、具体的な施策を立案する。
* Passkey (host-only) で店長に承認を求める。
*/
import type { A2ATaskRequest, A2ATaskResponse } from '../platform-types.js';
import { AgentBase } from '../agent-base.js';
interface AnalysisResult {
targetDate: string;
weather: { isRainy: boolean; precipitationProbability: number; summary: string };
inventory: { hasDessertStock: boolean; dessertsAvailable: Array<{ name: string; stock: number }> };
customers: { highCancelTendencyCount: number };
reservations: { tomorrowCount: number };
actionRecommended: boolean;
summary: string;
}
export class PlanningAgent extends AgentBase {
readonly agentId = 'planning-agent';
readonly accessiblePools: string[] = [];
async handle(task: A2ATaskRequest): Promise<A2ATaskResponse> {
const { analysis, rawData } = task.payload.parameters as {
analysis?: AnalysisResult;
rawData?: unknown;
};
if (!analysis) {
return this.error(task, 'No analysis data provided in task parameters');
}
if (!analysis.actionRecommended) {
return this.cancelled(task, `施策実施条件未達: ${analysis.summary}`);
}
try {
// 1. Get today's reservations from CRM for targeting
const reservationData = await this.callTool('query_crm', {
entity: 'reservations',
filter: `date=${analysis.targetDate}`,
}) as { reservations?: Array<{ customerName: string; customerId: string }> };
// 2. Get dessert inventory details from ERP
const inventoryData = await this.callTool('query_erp', {
entity: 'inventory',
item: 'dessert',
}) as { items?: Array<{ name: string; stock: number; costPerUnit: number }> };
// 3. Build the plan
const desserts = (inventoryData?.items ?? []).filter((i) => (i.stock ?? 0) > 0);
const targetDessert = desserts[0];
const targetCustomers = (reservationData?.reservations ?? []).slice(0, analysis.customers.highCancelTendencyCount);
const plan = {
title: '無料デザートキャンペーン',
targetDate: analysis.targetDate,
rationale: analysis.summary,
campaign: {
dessertItem: targetDessert?.name ?? 'デザート',
freeItemPerCustomer: 1,
totalCost: (targetDessert?.costPerUnit ?? 0) * targetCustomers.length,
},
targetCustomers: targetCustomers.map((c) => ({
customerId: c.customerId,
name: c.customerName,
})),
emailTemplate: buildEmailTemplate(analysis.targetDate, targetDessert?.name ?? 'デザート'),
createdAt: new Date().toISOString(),
status: 'pending_approval',
};
console.log(
`[planning] Plan created: ${plan.title} for ${targetCustomers.length} customers`,
);
// 4. Approval is handled by the Host (Passkey ceremony triggered by DispatchAgent)
// PlanningAgent returns the plan with status: pending_approval.
// The orchestration layer (Director → Dispatch) is responsible for auth flow.
return this.success(task, {
plan,
rawData,
nextAgent: 'dispatch-agent',
});
} catch (err) {
return this.error(task, (err as Error).message);
}
}
}
function buildEmailTemplate(date: string, dessertName: string): string {
return `件名: 【特別ご招待】明日のご来店に無料${dessertName}をプレゼント
お客様へ
明日 ${date} のご予約、誠にありがとうございます。
明日は雨模様のお天気が予想されますが、
特別に無料の${dessertName}をご用意しております。
ぜひお越しください。スタッフ一同、お待ちしております。
※ 本メールは予約システムより自動送信されています。`;
}

View File

@@ -0,0 +1,33 @@
/**
* Minimal A2A type definitions for reference implementation agents.
* Mirrors platform_impl/src/types/a2a.ts — keep in sync.
*/
export interface A2ATaskRequest {
messageId: string;
contextId?: string;
senderId: string;
recipientId: string;
timestamp?: string;
locale?: string;
type: 'task_request';
payload: {
parameters: Record<string, unknown>;
history?: Array<{ role: 'system' | 'user' | 'assistant'; content: string }>;
};
}
export interface A2ATaskResponse {
messageId: string;
contextId?: string;
senderId: string;
recipientId: string;
timestamp?: string;
type: 'task_response';
replyToMessageId: string;
status: 'success' | 'error' | 'timeout' | 'cancelled' | 'partial';
payload?: {
result?: unknown;
error?: { message: string; code?: string };
};
}

View File

@@ -0,0 +1,22 @@
/**
* Register reference implementation Agent classes with the platform Host.
* Import this module before calling MCPletHost.start() to make the agent
* classes available for instantiation from config.
*
* Usage in platform host entry point:
* import '../../reference_impl/agents/register.js';
* import { MCPletHost } from '../platform_impl/src/host/mcplet-host.js';
*/
import { InfoGatheringAgent } from './info-gathering/index.js';
import { PlanningAgent } from './planning/index.js';
import { DispatchAgent } from './dispatch/index.js';
// Lazy import of registerAgentClass to avoid circular dependency at module level
// In production, this registration is done in the host entry point.
export { InfoGatheringAgent, PlanningAgent, DispatchAgent };
export const AGENT_CLASSES: Record<string, new (...args: never[]) => unknown> = {
InfoGatheringAgent,
PlanningAgent,
DispatchAgent,
};

View File

@@ -0,0 +1,105 @@
# MCPletA2A Reference Implementation — Configuration
# Cancel-Rate Reduction Scenario (キャンセル率低減シナリオ)
#llm:
# provider: claude
# model: claude-sonnet-4-6
# apiKey: ${ANTHROPIC_API_KEY}
# --- OpenRouter alternative ---
llm:
provider: openrouter
model: anthropic/claude-sonnet-4-5
apiKey: ${OPENROUTER_API_KEY}
#siteUrl: https://your-site.example.com
#siteName: MCPletA2A
# Mock service base URL (replace with real service URLs in production)
mockServiceUrl: http://localhost:5100
pools:
media-pool:
rateLimitPerMinute: 60
info-pool:
rateLimitPerMinute: 120
# MCPlet server processes (spawned by the Host via stdio transport).
# ${REF_IMPL_DIST} is set by start.sh to the reference_impl/dist directory.
mcpletServers:
- name: crm-mcplet
command: node
args: ["${REF_IMPL_DIST}/mcplets/internal/crm/index.js"]
env:
MOCK_SERVICE_URL: http://localhost:5100
- name: erp-mcplet
command: node
args: ["${REF_IMPL_DIST}/mcplets/internal/erp/index.js"]
env:
MOCK_SERVICE_URL: http://localhost:5100
- name: hr-mcplet
command: node
args: ["${REF_IMPL_DIST}/mcplets/internal/hr/index.js"]
env:
MOCK_SERVICE_URL: http://localhost:5100
- name: web-access-mcplet
command: node
args: ["${REF_IMPL_DIST}/mcplets/info-pool/web-access/index.js"]
env:
MOCK_SERVICE_URL: http://localhost:5100
- name: api-access-mcplet
command: node
args: ["${REF_IMPL_DIST}/mcplets/info-pool/api-access/index.js"]
env:
MOCK_SERVICE_URL: http://localhost:5100
- name: site-access-mcplet
command: node
args: ["${REF_IMPL_DIST}/mcplets/media-pool/site-access/index.js"]
env:
MOCK_SERVICE_URL: http://localhost:5100
- name: email-mcplet
command: node
args: ["${REF_IMPL_DIST}/mcplets/media-pool/email/index.js"]
env:
MOCK_SERVICE_URL: http://localhost:5100
- name: sns-mcplet
command: node
args: ["${REF_IMPL_DIST}/mcplets/media-pool/sns/index.js"]
env:
MOCK_SERVICE_URL: http://localhost:5100
agents:
info-gathering-agent:
class: InfoGatheringAgent
accessiblePools: [info-pool]
description: 天気予報・在庫・顧客キャンセル傾向を収集・分析するAgent
planning-agent:
class: PlanningAgent
accessiblePools: []
description: 収集データを元にキャンペーン施策を立案するAgent
dispatch-agent:
class: DispatchAgent
accessiblePools: [media-pool]
description: 承認済みの施策を実行Email・SNS送信するAgent
directorAgent:
# 毎朝 7:00 にトリガー
schedule: "0 7 * * *"
targetAgentId: info-gathering-agent
promptTemplate: |
今日の天気予報と在庫情報、キャンセル傾向の高い予約客情報を収集・分析し、
キャンセル率を下げる施策を立案するためのデータを収集してください。
対象日付: {date}
maxRetries: 3
backoffMs: 5000
externalAgents: []
passkey:
mode: localhost
rpId: localhost
dashboard:
port: 4000
a2aExternalEndpoint:
port: 4001

View File

@@ -0,0 +1,48 @@
/**
* 外部 API アクセス MCPlet
* pool: info-pool | mcpletType: read | visibility: [model]
*/
import { MCPletServer } from '../../mcplet-server.js';
const MOCK_SERVICE_URL = process.env.MOCK_SERVICE_URL ?? 'http://localhost:5100';
const server = new MCPletServer('api-access-mcplet');
server.registerTool({
name: 'call_external_api',
description: '外部APIを呼び出してデータを取得します。在庫情報や外部データソースの参照に使用します。',
inputSchema: {
type: 'object',
properties: {
api: {
type: 'string',
description: '呼び出すAPI識別子 (例: inventory, customer_stats)',
},
params: {
type: 'object',
description: 'APIクエリパラメータ',
additionalProperties: true,
},
},
required: ['api'],
},
mcpletType: 'read',
pool: 'info-pool',
visibility: ['model'],
handler: async (args) => {
const api = args['api'] as string;
const params = (args['params'] as Record<string, string> | undefined) ?? {};
if (api === 'inventory') {
const item = params['item'] ?? '';
const qs = item ? `?item=${encodeURIComponent(item)}` : '';
const res = await fetch(`${MOCK_SERVICE_URL}/erp/inventory${qs}`);
if (!res.ok) throw new Error(`Inventory API returned ${res.status}`);
return res.json();
}
return { api, params, data: `Mock API response for api="${api}"` };
},
});
await server.listen();

View File

@@ -0,0 +1,45 @@
/**
* 外部 Web アクセス MCPlet
* pool: info-pool | mcpletType: read | visibility: [model]
*/
import { MCPletServer } from '../../mcplet-server.js';
const MOCK_SERVICE_URL = process.env.MOCK_SERVICE_URL ?? 'http://localhost:5100';
const server = new MCPletServer('web-access-mcplet');
server.registerTool({
name: 'fetch_web_content',
description: '外部Webサイトのコンテンツを取得します。天気予報など外部情報の収集に使用します。',
inputSchema: {
type: 'object',
properties: {
source: {
type: 'string',
description: '取得する情報ソース (例: weather_forecast, news)',
},
date: {
type: 'string',
description: '対象日付 (YYYY-MM-DD形式)',
},
},
required: ['source'],
},
mcpletType: 'read',
pool: 'info-pool',
visibility: ['model'],
handler: async (args) => {
const source = args['source'] as string;
const date = (args['date'] as string | undefined) ?? new Date().toISOString().slice(0, 10);
if (source === 'weather_forecast' || source === 'weather') {
const res = await fetch(`${MOCK_SERVICE_URL}/weather/forecast?date=${date}`);
if (!res.ok) throw new Error(`Weather service returned ${res.status}`);
return res.json();
}
return { source, date, content: `Mock web content for source="${source}" on ${date}` };
},
});
await server.listen();

View File

@@ -0,0 +1,56 @@
/**
* CRM MCPlet (内部システム)
* pool: なし (pool-less) | mcpletType: read | visibility: [model]
*/
import { MCPletServer } from '../../mcplet-server.js';
const MOCK_SERVICE_URL = process.env.MOCK_SERVICE_URL ?? 'http://localhost:5100';
const server = new MCPletServer('crm-mcplet');
server.registerTool({
name: 'query_crm',
description: 'CRMシステムから顧客・予約情報を取得します。キャンセル傾向分析や顧客リスト取得に使用します。',
inputSchema: {
type: 'object',
properties: {
entity: {
type: 'string',
enum: ['customers', 'reservations'],
description: '取得するエンティティ種別',
},
filter: {
type: 'string',
description: 'フィルタ条件 (例: rain_cancel_tendency, date=YYYY-MM-DD)',
},
},
required: ['entity'],
},
mcpletType: 'read',
visibility: ['model'],
handler: async (args) => {
const entity = args['entity'] as string;
const filter = args['filter'] as string | undefined;
if (entity === 'customers') {
const qs = filter ? `?filter=${encodeURIComponent(filter)}` : '';
const res = await fetch(`${MOCK_SERVICE_URL}/crm/customers${qs}`);
if (!res.ok) throw new Error(`CRM API returned ${res.status}`);
return res.json();
}
if (entity === 'reservations') {
// filter may be "date=2026-03-28"
const dateMatch = filter?.match(/date=(\d{4}-\d{2}-\d{2})/);
const date = dateMatch ? dateMatch[1] : '';
const qs = date ? `?date=${date}` : '';
const res = await fetch(`${MOCK_SERVICE_URL}/crm/reservations${qs}`);
if (!res.ok) throw new Error(`CRM API returned ${res.status}`);
return res.json();
}
throw new Error(`Unknown CRM entity: ${entity}`);
},
});
await server.listen();

View File

@@ -0,0 +1,46 @@
/**
* ERP MCPlet (内部システム)
* pool: なし (pool-less) | mcpletType: read | visibility: [model]
*/
import { MCPletServer } from '../../mcplet-server.js';
const MOCK_SERVICE_URL = process.env.MOCK_SERVICE_URL ?? 'http://localhost:5100';
const server = new MCPletServer('erp-mcplet');
server.registerTool({
name: 'query_erp',
description: 'ERPシステムから在庫・発注情報を取得します。商品在庫確認や調達状況の把握に使用します。',
inputSchema: {
type: 'object',
properties: {
entity: {
type: 'string',
enum: ['inventory', 'orders'],
description: '取得するエンティティ種別',
},
item: {
type: 'string',
description: '商品カテゴリ (例: dessert, drink)',
},
},
required: ['entity'],
},
mcpletType: 'read',
visibility: ['model'],
handler: async (args) => {
const entity = args['entity'] as string;
const item = args['item'] as string | undefined;
if (entity === 'inventory') {
const qs = item ? `?item=${encodeURIComponent(item)}` : '';
const res = await fetch(`${MOCK_SERVICE_URL}/erp/inventory${qs}`);
if (!res.ok) throw new Error(`ERP API returned ${res.status}`);
return res.json();
}
return { entity, message: 'Mock ERP orders data' };
},
});
await server.listen();

View File

@@ -0,0 +1,57 @@
/**
* HR MCPlet (内部システム)
* pool: なし (pool-less) | mcpletType: read | visibility: [model]
*/
import { MCPletServer } from '../../mcplet-server.js';
const server = new MCPletServer('hr-mcplet');
server.registerTool({
name: 'query_hr',
description: 'HRシステムからスタッフ情報・シフトを取得します。施策実行に必要な人員確認に使用します。',
inputSchema: {
type: 'object',
properties: {
entity: {
type: 'string',
enum: ['staff', 'shifts'],
description: '取得するエンティティ種別',
},
date: {
type: 'string',
description: '対象日付 (YYYY-MM-DD形式)',
},
},
required: ['entity'],
},
mcpletType: 'read',
visibility: ['model'],
handler: async (args) => {
const entity = args['entity'] as string;
const date = (args['date'] as string | undefined) ?? new Date().toISOString().slice(0, 10);
if (entity === 'staff') {
return {
staff: [
{ staffId: 'S001', name: '中村 りか', role: 'manager', available: true },
{ staffId: 'S002', name: '小林 けん', role: 'waiter', available: true },
{ staffId: 'S003', name: '加藤 みゆ', role: 'waiter', available: false },
],
};
}
if (entity === 'shifts') {
return {
date,
shifts: [
{ staffId: 'S001', startTime: '17:00', endTime: '23:00' },
{ staffId: 'S002', startTime: '17:00', endTime: '22:00' },
],
};
}
throw new Error(`Unknown HR entity: ${entity}`);
},
});
await server.listen();

View File

@@ -0,0 +1,125 @@
/**
* MCPlet Server base — wraps MCP SDK's low-level Server to include
* _meta.mcpletType in tools/list responses as required by the MCPlet spec.
*/
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
export type MCPletType = 'read' | 'prepare' | 'action';
export type Visibility = 'model' | 'app';
export interface MCPletToolAuth {
required: 'passkey';
enforcement: 'strict' | 'workflow' | 'host-only';
promptMessage?: string;
}
export interface MCPletToolDef {
name: string;
description: string;
inputSchema: Record<string, unknown>;
mcpletType: MCPletType;
pool?: string;
visibility: Visibility[];
auth?: MCPletToolAuth;
handler: (args: Record<string, unknown>, authPayload?: Record<string, unknown>) => Promise<unknown>;
}
export class MCPletServer {
private readonly server: Server;
private readonly tools = new Map<string, MCPletToolDef>();
constructor(private readonly serverName: string, version = '0.1.0') {
this.server = new Server(
{ name: serverName, version },
{ capabilities: { tools: {} } },
);
this.setupHandlers();
}
registerTool(def: MCPletToolDef): this {
this.tools.set(def.name, def);
return this;
}
async listen(): Promise<void> {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error(`[mcplet-server] ${this.serverName} listening on stdio`);
}
private setupHandlers(): void {
// tools/list — include _meta for MCPlet discovery (Spec Section 5.3.1)
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [...this.tools.values()].map((t) => ({
name: t.name,
description: t.description,
inputSchema: t.inputSchema,
_meta: {
mcpletType: t.mcpletType,
pool: t.pool,
visibility: t.visibility,
auth: t.auth ?? null,
},
})),
}));
// tools/call
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const toolName = request.params.name;
const tool = this.tools.get(toolName);
if (!tool) {
return {
content: [{ type: 'text', text: this.errorEnvelope(toolName, `Tool "${toolName}" not found`, 'NOT_FOUND') }],
isError: true,
};
}
// Extract auth payload injected by Host (Spec Section 7.3.1)
const requestMeta = request.params._meta as Record<string, unknown> | undefined;
const authPayload = requestMeta?.mcplet_auth as Record<string, unknown> | undefined;
// Enforce auth requirement ('strict' = per-call ceremony; 'workflow' = cached ceremony per contextId)
if (tool.auth?.required === 'passkey' &&
(tool.auth.enforcement === 'strict' || tool.auth.enforcement === 'workflow') &&
!authPayload) {
return {
content: [{ type: 'text', text: this.errorEnvelope(toolName, 'Authentication required', 'AUTH_REQUIRED', tool.mcpletType) }],
isError: true,
};
}
try {
const args = (request.params.arguments ?? {}) as Record<string, unknown>;
const result = await tool.handler(args, authPayload);
const envelope = {
result,
_meta: {
timestamp: new Date().toISOString(),
toolId: toolName,
mcpletType: tool.mcpletType,
visibility: tool.visibility,
},
};
return { content: [{ type: 'text', text: JSON.stringify(envelope) }] };
} catch (err) {
return {
content: [{ type: 'text', text: this.errorEnvelope(toolName, (err as Error).message, 'UNKNOWN_ERROR', tool.mcpletType) }],
isError: true,
};
}
});
}
private errorEnvelope(toolId: string, message: string, code: string, mcpletType: MCPletType = 'read'): string {
return JSON.stringify({
error: { message, code },
_meta: { timestamp: new Date().toISOString(), toolId, mcpletType, visibility: ['model'] },
});
}
}

View File

@@ -0,0 +1,71 @@
/**
* Email MCPlet
* pool: media-pool | mcpletType: action | visibility: [app] | auth: passkey workflow
*
* visibility: ['app'] means this tool is NOT exposed to the LLM directly.
* It is invoked only through the Host-controlled action-dispatch path
* (Dispatch Agent) after Passkey authentication.
* enforcement: 'workflow' — one Passkey ceremony per Director cycle (contextId),
* not one per individual email send.
*/
import { MCPletServer } from '../../mcplet-server.js';
const MOCK_SERVICE_URL = process.env.MOCK_SERVICE_URL ?? 'http://localhost:5100';
const server = new MCPletServer('email-mcplet');
server.registerTool({
name: 'send_email',
description: '指定した宛先にメールを送信します。承認済みキャンペーン通知や予約リマインダーの送信に使用します。',
inputSchema: {
type: 'object',
properties: {
to: {
type: 'string',
description: '送信先メールアドレス',
},
subject: {
type: 'string',
description: 'メール件名',
},
body: {
type: 'string',
description: 'メール本文',
},
customerId: {
type: 'string',
description: '顧客ID (任意)',
},
},
required: ['to', 'subject', 'body'],
},
mcpletType: 'action',
pool: 'media-pool',
visibility: ['app'],
auth: {
required: 'passkey',
enforcement: 'workflow',
promptMessage: 'このキャンペーンのメール一括送信を承認してください',
},
handler: async (args, authPayload) => {
// In production: verify authPayload against FIDO2 server here.
// For demo: log and proceed if authPayload present.
if (authPayload) {
console.error(`[email-mcplet] Auth verified (challenge: ${authPayload['challenge'] ?? 'n/a'})`);
}
const res = await fetch(`${MOCK_SERVICE_URL}/email/send`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
to: args['to'],
subject: args['subject'],
body: args['body'],
}),
});
if (!res.ok) throw new Error(`Email service returned ${res.status}`);
return res.json();
},
});
await server.listen();

View File

@@ -0,0 +1,35 @@
/**
* サイトアクセス MCPlet
* pool: media-pool | mcpletType: read | visibility: [model]
*/
import { MCPletServer } from '../../mcplet-server.js';
const MOCK_SERVICE_URL = process.env.MOCK_SERVICE_URL ?? 'http://localhost:5100';
const server = new MCPletServer('site-access-mcplet');
server.registerTool({
name: 'read_site_stats',
description: 'EPARKサイトのアクセス統計を取得します。予約ページの閲覧数やコンバージョン率などを確認できます。',
inputSchema: {
type: 'object',
properties: {
period: {
type: 'string',
enum: ['last_7_days', 'last_30_days', 'today'],
description: '集計期間',
},
},
required: [],
},
mcpletType: 'read',
pool: 'media-pool',
visibility: ['model'],
handler: async (_args) => {
const res = await fetch(`${MOCK_SERVICE_URL}/site/stats`);
if (!res.ok) throw new Error(`Site stats API returned ${res.status}`);
return res.json();
},
});
await server.listen();

View File

@@ -0,0 +1,60 @@
/**
* SNS MCPlet
* pool: media-pool | mcpletType: action | visibility: [app] | auth: passkey workflow
*/
import { MCPletServer } from '../../mcplet-server.js';
const MOCK_SERVICE_URL = process.env.MOCK_SERVICE_URL ?? 'http://localhost:5100';
const server = new MCPletServer('sns-mcplet');
server.registerTool({
name: 'post_sns',
description: 'SNSプラットフォームに投稿します。承認済みキャンペーン告知の拡散に使用します。',
inputSchema: {
type: 'object',
properties: {
platform: {
type: 'string',
enum: ['twitter', 'instagram', 'line'],
description: '投稿するSNSプラットフォーム',
},
content: {
type: 'string',
description: '投稿内容',
},
scheduleAt: {
type: 'string',
description: '予約投稿日時 (ISO 8601形式、任意)',
},
},
required: ['platform', 'content'],
},
mcpletType: 'action',
pool: 'media-pool',
visibility: ['app'],
auth: {
required: 'passkey',
enforcement: 'workflow',
promptMessage: 'このキャンペーンのSNS投稿を承認してください',
},
handler: async (args, authPayload) => {
if (authPayload) {
console.error(`[sns-mcplet] Auth verified (challenge: ${authPayload['challenge'] ?? 'n/a'})`);
}
const res = await fetch(`${MOCK_SERVICE_URL}/sns/post`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
platform: args['platform'],
content: args['content'],
scheduleAt: args['scheduleAt'],
}),
});
if (!res.ok) throw new Error(`SNS service returned ${res.status}`);
return res.json();
},
});
await server.listen();

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);
});
}

4866
reference_impl/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,33 @@
{
"name": "mcplet-a2a-reference",
"version": "0.1.0",
"description": "MCPletA2A reference implementation — cancel-rate reduction scenario",
"type": "module",
"scripts": {
"build": "tsc && cp -r mock-services/data dist/mock-services/data",
"mock": "node dist/mock-services/server.js",
"dev": "tsc --watch",
"test": "jest",
"mcplet:crm": "node dist/mcplets/internal/crm/index.js",
"mcplet:erp": "node dist/mcplets/internal/erp/index.js",
"mcplet:hr": "node dist/mcplets/internal/hr/index.js",
"mcplet:web-access": "node dist/mcplets/info-pool/web-access/index.js",
"mcplet:api-access": "node dist/mcplets/info-pool/api-access/index.js",
"mcplet:site-access": "node dist/mcplets/media-pool/site-access/index.js",
"mcplet:email": "node dist/mcplets/media-pool/email/index.js",
"mcplet:sns": "node dist/mcplets/media-pool/sns/index.js"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.10.2",
"js-yaml": "^4.1.0",
"uuid": "^11.1.0"
},
"devDependencies": {
"@types/js-yaml": "^4.0.9",
"@types/node": "^22.0.0",
"@types/uuid": "^10.0.0",
"jest": "^29.7.0",
"ts-jest": "^29.2.0",
"typescript": "^5.7.0"
}
}

View File

@@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "dist",
"rootDir": ".",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["mcplets/**/*", "agents/**/*", "mock-services/**/*", "config/**/*"],
"exclude": ["node_modules", "dist"]
}