doc_id: DEV-202606-004 feature_id: FEAT-202606-003-sqlite-his-mock-mcp-tools type: dev-progress title: SQLite HIS Mock MCP Tools Implementation Plan status: implemented owner: 医梦研发团队 created_at: 2026-06-04 updated_at: 2026-06-04 reviewers: [] related_docs:
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Build the demo HIS interface set inside the AI platform process, backed by SQLite, so registration demo workflows can call controlled MCP Tool APIs for patient, department, doctor, schedule, slot lock, mock payment, and appointment creation.
Architecture: Implement the mock HIS adapter in emoon-ai-mcp, because HIS Mock/Real Adapter belongs to MCP per the engineering constraints. Expose the v1.2 standard Tool API (GET /tools/catalog, POST /tools/{toolName}/invoke) and keep existing /api/mcp/his/tools/* endpoints as compatibility wrappers. Use an isolated SQLite DataSource and JdbcTemplate instead of the platform MySQL/dynamic datasource.
Tech Stack: Java 17, Spring Boot 3, Maven, JUnit 5, AssertJ, Mockito, SQLite JDBC, Spring JdbcTemplate.
Read these completely before touching code:
docs/standards/AI中台工程约束.mddocs/standards/工程规约生效说明.mddocs/initiatives/FEAT-202606-001-unified-entry-client/对外接口联调基准.mddocs/api/openapi/医梦AI中台开放平台接口-v1.2.openapi.yamldocs/initiatives/FEAT-202606-001-unified-entry-client/需求文档.mddocs/initiatives/FEAT-202606-001-unified-entry-client/dev-progress/统一入口挂号演示实施计划.mddocs/initiatives/FEAT-202606-001-unified-entry-client/dev-progress/演示闭环纵向任务拆分.mdImportant constraints:
emoon-openplatform, emoon-ai-agent, or emoon-ai-card.Implement these v1.2-compatible endpoints:
| Method | Path | Purpose |
|---|---|---|
GET |
/tools/catalog |
Return authorized demo tool catalog |
POST |
/tools/{toolName}/invoke |
Invoke a controlled HIS/payment demo tool |
Keep these compatibility endpoints by delegating to the new invoke service:
| Method | Path |
|---|---|
POST |
/api/mcp/his/tools/get_departments |
POST |
/api/mcp/his/tools/get_doctors |
POST |
/api/mcp/his/tools/get_doctor_schedules |
POST |
/api/mcp/his/tools/create_registration |
POST |
/api/mcp/his/tools/invoke |
Tool names to implement:
| Tool | Risk | Type | Demo Use |
|---|---|---|---|
his.searchPatient |
L2_SENSITIVE_READ |
read | Find patient before filing |
his.createPatient |
L3_BUSINESS_WRITE |
write | Create demo patient file |
his.queryDepartments |
L1_QUERY |
read | Department recommendation card |
his.queryDoctors |
L1_QUERY |
read | Doctor selection card |
his.querySchedules |
L1_QUERY |
read | Time slot selection card |
his.lockSchedule |
L3_BUSINESS_WRITE |
write | Lock selected slot for 5 minutes |
his.releaseSchedule |
L3_BUSINESS_WRITE |
write | Release slot lock |
payment.createOrder |
L4_FINANCIAL_ACTION |
write | Create Mock payment QR payload |
payment.queryStatus |
L4_FINANCIAL_ACTION |
read | Read Mock payment status |
payment.markMockPaid |
L4_FINANCIAL_ACTION |
write | Demo-only simulated payment success |
his.createRegistration |
L3_BUSINESS_WRITE |
write | Create appointment after payment |
his.queryRegistration |
L2_SENSITIVE_READ |
read | Read appointment success detail |
pom.xml
sqlite-jdbc.version.org.xerial:sqlite-jdbc.emoon-infra/emoon-modules/emoon-ai/emoon-ai-mcp/pom.xml
org.xerial:sqlite-jdbc.emoon-infra/emoon-modules/emoon-ai/emoon-ai-mcp/src/main/java/com/emoon/mcp/his/config/HisMockProperties.java
enabled, jdbcUrl, resetOnStart, lockTtlSeconds.emoon-infra/emoon-modules/emoon-ai/emoon-ai-mcp/src/main/java/com/emoon/mcp/his/config/HisMockSqliteConfig.java
hisMockDataSource and hisMockJdbcTemplate.emoon-infra/emoon-modules/emoon-ai/emoon-ai-mcp/src/main/java/com/emoon/mcp/his/config/HisMockDatabaseInitializer.java
schema.sql and seed.sql.emoon-infra/emoon-modules/emoon-ai/emoon-ai-mcp/src/main/resources/db/his-mock/schema.sqlemoon-infra/emoon-modules/emoon-ai/emoon-ai-mcp/src/main/resources/db/his-mock/seed.sqlemoon-infra/emoon-modules/emoon-ai/emoon-ai-mcp/src/main/java/com/emoon/mcp/his/client/HisClient.java
emoon-infra/emoon-modules/emoon-ai/emoon-ai-mcp/src/main/java/com/emoon/mcp/his/domain/emoon-infra/emoon-modules/emoon-ai/emoon-ai-mcp/src/main/java/com/emoon/mcp/his/tool/dto/ToolInvokeRequest.javaemoon-infra/emoon-modules/emoon-ai/emoon-ai-mcp/src/main/java/com/emoon/mcp/his/tool/dto/ToolContext.javaemoon-infra/emoon-modules/emoon-ai/emoon-ai-mcp/src/main/java/com/emoon/mcp/his/tool/dto/ToolInvokeData.javaemoon-infra/emoon-modules/emoon-ai/emoon-ai-mcp/src/main/java/com/emoon/mcp/his/tool/dto/ToolCatalogItem.javaThese DTOs must match the OpenAPI schema:
{
"toolCallId": "tool-call-001",
"context": {
"projectId": "demo-project",
"cardInstanceId": "card-001",
"traceId": "trace-001"
},
"input": {},
"riskLevel": "L1_QUERY"
}
Response data shape:
{
"toolCallId": "tool-call-001",
"toolName": "his.queryDepartments",
"status": "success",
"result": {
"mock": true
}
}
emoon-infra/emoon-modules/emoon-ai/emoon-ai-mcp/src/main/java/com/emoon/mcp/his/client/SqliteMockHisClient.java
HisClient.emoon-infra/emoon-modules/emoon-ai/emoon-ai-mcp/src/main/java/com/emoon/mcp/his/application/HisToolCatalogService.java
emoon-infra/emoon-modules/emoon-ai/emoon-ai-mcp/src/main/java/com/emoon/mcp/his/application/HisToolInvokeService.java
HisClient.emoon-infra/emoon-modules/emoon-ai/emoon-ai-mcp/src/main/java/com/emoon/mcp/his/application/HisMockIdempotencyService.java
Keep McpToolService as the internal facade used by existing agent/card code, but delegate to SqliteMockHisClient instead of in-memory data.
emoon-infra/emoon-modules/emoon-ai/emoon-ai-mcp/src/main/java/com/emoon/mcp/his/web/ToolController.java
GET /tools/catalogPOST /tools/{toolName}/invokeemoon-infra/emoon-modules/emoon-ai/emoon-ai-mcp/src/main/java/com/emoon/mcp/his/web/HisMcpController.java
HisToolInvokeService.McpToolResponse.emoon-infra/emoon-modules/emoon-ai/emoon-ai-mcp/src/test/java/com/emoon/mcp/his/client/SqliteMockHisClientTest.javaemoon-infra/emoon-modules/emoon-ai/emoon-ai-mcp/src/test/java/com/emoon/mcp/his/application/HisToolCatalogServiceTest.javaemoon-infra/emoon-modules/emoon-ai/emoon-ai-mcp/src/test/java/com/emoon/mcp/his/application/HisToolInvokeServiceTest.javaemoon-infra/emoon-modules/emoon-ai/emoon-ai-mcp/src/test/java/com/emoon/mcp/his/web/ToolControllerTest.javaemoon-infra/emoon-modules/emoon-ai/emoon-ai-mcp/src/test/java/com/emoon/ai/mcp/application/McpToolServiceTest.javaUse these tables:
CREATE TABLE IF NOT EXISTS his_patient (
patient_id TEXT PRIMARY KEY,
patient_name TEXT NOT NULL,
id_card TEXT,
phone TEXT,
gender TEXT,
birth_date TEXT,
created_at TEXT NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS uk_his_patient_id_card ON his_patient(id_card) WHERE id_card IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_his_patient_phone ON his_patient(phone);
CREATE TABLE IF NOT EXISTS his_department (
department_id TEXT PRIMARY KEY,
department_name TEXT NOT NULL,
description TEXT,
location TEXT,
sort_no INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE IF NOT EXISTS his_doctor (
doctor_id TEXT PRIMARY KEY,
department_id TEXT NOT NULL,
doctor_name TEXT NOT NULL,
title TEXT NOT NULL,
specialty TEXT,
consultation_fee INTEGER NOT NULL,
FOREIGN KEY(department_id) REFERENCES his_department(department_id)
);
CREATE TABLE IF NOT EXISTS his_schedule (
schedule_id TEXT PRIMARY KEY,
doctor_id TEXT NOT NULL,
schedule_date TEXT NOT NULL,
time_period TEXT NOT NULL,
start_time TEXT NOT NULL,
end_time TEXT NOT NULL,
total_count INTEGER NOT NULL,
remaining_count INTEGER NOT NULL,
fee INTEGER NOT NULL,
FOREIGN KEY(doctor_id) REFERENCES his_doctor(doctor_id)
);
CREATE TABLE IF NOT EXISTS his_slot_lock (
lock_id TEXT PRIMARY KEY,
schedule_id TEXT NOT NULL,
patient_id TEXT NOT NULL,
status TEXT NOT NULL,
expires_at TEXT NOT NULL,
idempotency_key TEXT NOT NULL UNIQUE,
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS his_payment_order (
order_id TEXT PRIMARY KEY,
lock_id TEXT NOT NULL,
amount INTEGER NOT NULL,
status TEXT NOT NULL,
qr_content TEXT NOT NULL,
idempotency_key TEXT NOT NULL UNIQUE,
created_at TEXT NOT NULL,
paid_at TEXT
);
CREATE TABLE IF NOT EXISTS his_appointment (
appointment_id TEXT PRIMARY KEY,
appointment_no TEXT NOT NULL UNIQUE,
patient_id TEXT NOT NULL,
department_id TEXT NOT NULL,
doctor_id TEXT NOT NULL,
schedule_id TEXT NOT NULL,
lock_id TEXT NOT NULL,
order_id TEXT NOT NULL,
status TEXT NOT NULL,
queue_no TEXT NOT NULL,
idempotency_key TEXT NOT NULL UNIQUE,
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS his_idempotency_record (
idempotency_key TEXT PRIMARY KEY,
tool_name TEXT NOT NULL,
request_hash TEXT NOT NULL,
response_json TEXT NOT NULL,
created_at TEXT NOT NULL
);
Seed at least:
dept_neurology, dept_digestive, dept_generalpatient_demo_001Files:
pom.xmlemoon-infra/emoon-modules/emoon-ai/emoon-ai-mcp/pom.xml.../config/HisMockProperties.javaCreate: .../config/HisMockSqliteConfig.java
[ ] Step 1: Add failing configuration test
Create emoon-infra/emoon-modules/emoon-ai/emoon-ai-mcp/src/test/java/com/emoon/mcp/his/config/HisMockSqliteConfigTest.java:
package com.emoon.mcp.his.config;
import org.junit.jupiter.api.Test;
import org.springframework.jdbc.core.JdbcTemplate;
import static org.assertj.core.api.Assertions.assertThat;
class HisMockSqliteConfigTest {
@Test
void createsJdbcTemplateForSqliteDatasource() {
HisMockProperties properties = new HisMockProperties();
properties.setJdbcUrl("jdbc:sqlite::memory:");
properties.setEnabled(true);
properties.setLockTtlSeconds(300);
HisMockSqliteConfig config = new HisMockSqliteConfig();
JdbcTemplate jdbcTemplate = config.hisMockJdbcTemplate(config.hisMockDataSource(properties));
Integer result = jdbcTemplate.queryForObject("select 1", Integer.class);
assertThat(result).isEqualTo(1);
}
}
Run:
mvn -pl emoon-infra/emoon-modules/emoon-ai/emoon-ai-mcp -DskipTests=false -Dtest=HisMockSqliteConfigTest test
Expected: FAIL because config classes and SQLite dependency do not exist.
Add sqlite-jdbc.version and dependencyManagement in root pom.xml, then dependency in emoon-ai-mcp/pom.xml.
Create HisMockProperties:
package com.emoon.mcp.his.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
@Data
@ConfigurationProperties(prefix = "emoon.his.mock")
public class HisMockProperties {
private boolean enabled = true;
private String jdbcUrl = "jdbc:sqlite:file:./data/his-mock.db";
private boolean resetOnStart = false;
private long lockTtlSeconds = 300;
}
Create HisMockSqliteConfig:
package com.emoon.mcp.his.config;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import javax.sql.DataSource;
@Configuration
@EnableConfigurationProperties(HisMockProperties.class)
public class HisMockSqliteConfig {
@Bean
public DataSource hisMockDataSource(HisMockProperties properties) {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("org.sqlite.JDBC");
dataSource.setUrl(properties.getJdbcUrl());
return dataSource;
}
@Bean
public JdbcTemplate hisMockJdbcTemplate(DataSource hisMockDataSource) {
return new JdbcTemplate(hisMockDataSource);
}
}
Run the same Maven command. Expected: PASS.
Files:
.../config/HisMockDatabaseInitializer.java.../resources/db/his-mock/schema.sql.../resources/db/his-mock/seed.sqlTest: .../config/HisMockDatabaseInitializerTest.java
[ ] Step 1: Write failing initializer test
Test must create an in-memory SQLite datasource, run initializer, then assert table and seed row exist:
Integer departments = jdbcTemplate.queryForObject("select count(*) from his_department", Integer.class);
assertThat(departments).isGreaterThanOrEqualTo(3);
[ ] Step 2: Run failing test
mvn -pl emoon-infra/emoon-modules/emoon-ai/emoon-ai-mcp -DskipTests=false -Dtest=HisMockDatabaseInitializerTest test
Expected: FAIL because initializer and SQL resources do not exist.
Use ResourceDatabasePopulator with schema.sql and seed.sql. If resetOnStart=true, run a small drop script before schema creation. Keep default resetOnStart=false.
Expected: PASS.
Files:
HisClient.javaSqliteMockHisClient.java.../domainTest: SqliteMockHisClientTest.java
[ ] Step 1: Write failing tests
Cover:
queryDepartments returns seeded departments with mock=true available in wrapper result.queryDoctors("dept_neurology") returns neurologists.querySchedules(doctorId, date) returns remaining count and fee.unknown department/doctor returns empty list, not exception.
[ ] Step 2: Run failing tests
mvn -pl emoon-infra/emoon-modules/emoon-ai/emoon-ai-mcp -DskipTests=false -Dtest=SqliteMockHisClientTest test
[ ] Step 3: Implement SQLite reads
Use JdbcTemplate.query(...); do not add MyBatis Mapper for this SQLite mock.
Expected: PASS.
Files:
HisClient.javaSqliteMockHisClient.javaHisPatient.javaTest: SqliteMockHisClientTest.java
[ ] Step 1: Write failing tests
Cover:
searchPatient finds by phone.searchPatient finds by id card.createPatient creates a patient and returns same patient for duplicate idempotency key.duplicate id card returns existing patient instead of creating a second row.
[ ] Step 2: Run failing tests
Expected: FAIL.
Use his_patient and his_idempotency_record. Return:
{
"patientId": "patient_xxx",
"patientName": "张三",
"mock": true
}
Expected: PASS.
Files:
HisClient.javaSqliteMockHisClient.javaHisSlotLock.javaTest: SqliteMockHisClientTest.java
[ ] Step 1: Write failing tests
Cover:
remaining_count > 0.RELEASED.expired locks do not block new lock attempts.
[ ] Step 2: Run failing tests
Expected: FAIL.
Use a transaction. For demo SQLite, synchronize around write methods in SqliteMockHisClient to avoid double-lock in same JVM.
Do not decrement remaining_count on lock. Decrement only when appointment is created.
Expected: PASS.
Files:
HisClient.javaSqliteMockHisClient.javaHisPaymentOrder.javaTest: SqliteMockHisClientTest.java
[ ] Step 1: Write failing tests
Cover:
demo-payment://orderId=.markMockPaid changes status to PAID.payment result includes mock=true and mockPayment=true.
[ ] Step 2: Run failing tests
Expected: FAIL.
Statuses:
PENDING
PAID
CANCELLED
Do not use WeChat, Alipay,医保, or any real payment brand in response fields.
Expected: PASS.
Files:
HisClient.javaSqliteMockHisClient.javaHisRegistrationRequest.javaHisRegistrationResult.javaTest: SqliteMockHisClientTest.java
[ ] Step 1: Write failing tests
Cover:
remaining_count.response includes appointmentId, appointmentNo, queueNo, mock=true.
[ ] Step 2: Run failing tests
Expected: FAIL.
Use his_appointment. Generate:
appointmentId = appt_<12 chars>
appointmentNo = REGyyyyMMdd<sequence>
queueNo = A01/A02...
Expected: PASS.
Files:
HisToolCatalogService.javaToolCatalogItem.javaTest: HisToolCatalogServiceTest.java
[ ] Step 1: Write failing catalog test
Assert catalog contains exactly the scoped demo tools from this plan and each item has:
toolNametoolTypedescriptioninputSchemaoutputSchemariskLevel
[ ] Step 2: Run failing test
Expected: FAIL.
Keep schemas small but concrete. Example:
{
"toolName": "his.queryDoctors",
"toolType": "read",
"riskLevel": "L1_QUERY",
"inputSchema": {
"type": "object",
"required": ["departmentId"],
"properties": {
"departmentId": {"type": "string"}
}
}
}
Expected: PASS.
Files:
HisToolInvokeService.javaToolInvokeRequest.javaToolContext.javaToolInvokeData.javaTest: HisToolInvokeServiceTest.java
[ ] Step 1: Write failing invoke tests
Cover:
status=failed.toolCallId is echoed.status=success.status=failed.status=forbidden.payment.markMockPaid is allowed only because result has explicit mockPayment=true.
[ ] Step 2: Run failing test
Expected: FAIL.
Dispatch by path toolName, not request body toolName, to match OpenAPI:
public ToolInvokeData invoke(String toolName, String idempotencyKey, ToolInvokeRequest request)
Read idempotency key from:
X-Emoon-Idempotency-Key headerrequest.input.idempotencyKeyWrite tools must have one of them.
Expected: PASS.
Files:
ToolController.javaTest: ToolControllerTest.java
[ ] Step 1: Write failing controller tests
Use @WebMvcTest(ToolController.class) or direct controller test depending on project test stability.
Assert:
GET /tools/catalog returns R.ok style response with data.tools.POST /tools/his.queryDepartments/invoke accepts OpenAPI ToolInvokeRequest.header X-Emoon-Idempotency-Key is passed to invoke service.
[ ] Step 2: Run failing test
Expected: FAIL.
Controller methods:
@GetMapping("/tools/catalog")
public R<Map<String, Object>> catalog()
@PostMapping("/tools/{toolName}/invoke")
public R<ToolInvokeData> invoke(
@PathVariable String toolName,
@RequestHeader(value = "X-Emoon-Idempotency-Key", required = false) String idempotencyKey,
@Valid @RequestBody ToolInvokeRequest request)
Expected: PASS.
Files:
HisMcpController.javaTest: ToolControllerTest.java or HisMcpControllerTest.java
[ ] Step 1: Write failing compatibility tests
Cover old calls:
/api/mcp/his/tools/get_departments/api/mcp/his/tools/get_doctors/api/mcp/his/tools/get_doctor_schedules/api/mcp/his/tools/create_registration/api/mcp/his/tools/invoke with old toolName values
[ ] Step 2: Run failing tests
Expected: FAIL after service replacement until compatibility mapping is done.
Map old names:
| Old | New |
|---|---|
his_get_departments |
his.queryDepartments |
his_get_doctors |
his.queryDoctors |
his_get_doctor_schedules |
his.querySchedules |
his_create_registration |
his.createRegistration |
For old create_registration, if there is no paid order in request, return a clear failed demo response rather than silently bypassing payment. If compatibility with existing tests requires success, add an explicit demo-only path with generated lock/order paid in one transaction and response mock=true.
Expected: PASS.
McpToolServiceFiles:
McpToolService.javaModify: McpToolServiceTest.java
[ ] Step 1: Update failing tests
Replace mock expectations so:
lockSchedule calls real HisClient.lockSchedule(...) style method.createAppointment includes orderId or delegates to the new registration flow.returned map still contains mock=true.
[ ] Step 2: Run failing test
Expected: FAIL.
Keep existing public methods if used by agent/card code, but route through new client methods. Do not duplicate business rules here.
Expected: PASS.
Files:
scripts/demo-registration/tools-catalog.shCreate: scripts/demo-registration/registration-tool-flow.sh
[ ] Step 1: Write scripts
The flow script must call:
GET /tools/catalog
POST /tools/his.searchPatient/invoke
POST /tools/his.queryDepartments/invoke
POST /tools/his.queryDoctors/invoke
POST /tools/his.querySchedules/invoke
POST /tools/his.lockSchedule/invoke
POST /tools/payment.createOrder/invoke
POST /tools/payment.markMockPaid/invoke
POST /tools/his.createRegistration/invoke
Each write step should echo the idempotency key and expected response marker:
mock=true
mockPayment=true for payment tools
status=success
[ ] Step 1: Run MCP module compile
mvn -pl emoon-infra/emoon-modules/emoon-ai/emoon-ai-mcp -am -DskipTests compile
Expected: BUILD SUCCESS.
[ ] Step 2: Run MCP module tests
mvn -pl emoon-infra/emoon-modules/emoon-ai/emoon-ai-mcp -DskipTests=false test
Expected: BUILD SUCCESS.
[ ] Step 3: Run architecture test
mvn -pl emoon-admin -DskipTests=false -Dprofiles.active= -Dtest=AiPlatformArchitectureTest test
Expected: BUILD SUCCESS.
Start AI platform normally, then call:
curl -s http://localhost:8080/tools/catalog
Expected: response contains his.queryDepartments, his.createRegistration, and payment.createOrder.
/tools/catalog returns the demo HIS/payment catalog using v1.2 ToolCatalogItem shape./tools/{toolName}/invoke accepts v1.2 ToolInvokeRequest shape./api/mcp/his/tools/* endpoints still work as compatibility wrappers.emoon-openplatform.