Skip to content

Latest commit

ย 

History

History
1063 lines (852 loc) ยท 44.4 KB

File metadata and controls

1063 lines (852 loc) ยท 44.4 KB

Pipeline ๆŒไน…ๅŒ–่ˆ‡ๆŽ’็จ‹ๆœๅฐ‹่จญ่จˆๆ–‡ไปถ

Status: RFC๏ผˆRequest for Comments๏ผ‰ Created: 2026-02-15 Scope: Pipeline ๆŒไน…ๅŒ–ใ€MCP ๆช”ๆกˆๅ‚ณ่ผธใ€ๆŽ’็จ‹ๆœๅฐ‹


1. ๅ•้กŒๆ่ฟฐ

็พ็‹€

็›ฎๅ‰ Pipeline ็ณป็ตฑ๏ผˆv0.4.0๏ผ‰ๆ˜ฏๅฎŒๅ…จ็„ก็‹€ๆ…‹็š„๏ผš

Agent ๅ‚ณๅ…ฅ YAML/JSON โ†’ _parse_pipeline_config() โ†’ PipelineExecutor โ†’ ็ตๆžœ โ†’ ไธŸๆฃ„้…็ฝฎ
้ขๅ‘ ็พ็‹€ ๆœŸๆœ›
Pipeline ้…็ฝฎ ๆฏๆฌก้œ€่ฆ inline ๅ‚ณๅ…ฅ ๅฏไฟๅญ˜ใ€ๅ‘ฝๅใ€้‡่ค‡ไฝฟ็”จ
ๅŸท่กŒ็ตๆžœ ๅƒ…ไธ€ๆฌกๆ€งๅ›žๅ‚ณ ๅฏๆฏ”ๅฐๆญทๅฒ็ตๆžœใ€่ฟฝ่นค่ฎŠๅŒ–
ๆŽ’็จ‹ๅŸท่กŒ ไธๅญ˜ๅœจ ๅฎšๆœŸ่‡ชๅ‹•ๆœๅฐ‹ใ€ๆ–ฐๆ–‡็ป้€š็Ÿฅ
ๅค–้ƒจ่ผธๅ…ฅ ๅƒ… pipeline ๅƒๆ•ธๅญ—ไธฒ ๅฏ่ผ‰ๅ…ฅๆช”ๆกˆใ€URL
MCP ๆช”ๆกˆไบคๆ› ็„ก Agent ๅฏ่ฎ€ๅฏซ pipeline ๆช”ๆกˆ

ไฝฟ็”จ่€…ๆ•…ไบ‹

  1. ็ ”็ฉถ่€… A๏ผšใ€Œๆˆ‘ๆฏ้€ฑ้ƒฝๆœๅฐ‹ๅŒๆจฃ็š„ PICO ๅ•้กŒ๏ผˆremimazolam vs propofol in ICU๏ผ‰๏ผŒ่ƒฝไธ่ƒฝๅญ˜ไธ‹ไพ†ไธ€้ต้‡่ท‘๏ผŸใ€
  2. ็ ”็ฉถ่€… B๏ผšใ€Œๆˆ‘ๆƒณ่จญๅฎšๆฏๆœˆ่‡ชๅ‹•ๆœๅฐ‹ CRISPR gene therapy ็š„ๆ–ฐๆ–‡็ป๏ผŒๆœ‰ๆ–ฐ็ตๆžœๆ™‚้€š็Ÿฅๆˆ‘ใ€‚ใ€
  3. Lab PI๏ผšใ€Œๆˆ‘ๆœ‰ 5 ๅ€‹ไธๅŒไธป้กŒ็š„ๆœๅฐ‹็ญ–็•ฅ๏ผŒๆƒณๅˆ†ไบซ็ตฆๅญธ็”Ÿไฝฟ็”จใ€‚ใ€
  4. ็ณป็ตฑ็ฎก็†ๅ“ก๏ผšใ€Œ้œ€่ฆไธ€ๅ€‹ cron-like ๆฉŸๅˆถๅฎšๆœŸๆ›ดๆ–ฐๆ–‡็ป่ณ‡ๆ–™ๅบซใ€‚ใ€

2. MCP ๅ”่ญฐ่ƒฝๅŠ›ๅˆ†ๆž

2.1 MCP Resource ๆฉŸๅˆถ

MCP ่ฆ็ฏ„ๅฎš็พฉไบ† Resources โ€” server ๆšด้œฒ็ตฆ client ็š„ๅ”ฏ่ฎ€่ณ‡ๆ–™๏ผš

่ƒฝๅŠ› ๆ”ฏๆด ๅ‚™่จป
้œๆ…‹ Resource (@mcp.resource) โœ… ๅ›บๅฎš URI๏ผŒๅฆ‚ pubmed://filters/all
Resource Template (@mcp.resource("uri/{param}")) โœ… ๅ‹•ๆ…‹ URI๏ผŒserver ็ซฏๅฏ่ฎ€ๆช”ๅ›žๅ‚ณ
Client ่ฎ€ๅ– Resource โœ… resources/read method
Client ๅฏซๅ…ฅ Resource โŒ MCP ่ฆ็ฏ„็„กๅฏซๅ…ฅ API
Resource ่จ‚้–ฑ/่ฎŠๆ›ด้€š็Ÿฅ โœ… resources/subscribe + notifications/resources/updated
Binary content (้žๆ–‡ๅญ—) โœ… ๅฏ็”จ BlobResourceContents ๅ›žๅ‚ณ base64

้—œ้ต้™ๅˆถ๏ผšMCP Resources ๆ˜ฏๅ”ฏ่ฎ€็š„ใ€‚Client๏ผˆAgent๏ผ‰็„กๆณ•้€้Ž MCP ็›ดๆŽฅใ€ŒไธŠๅ‚ณๆช”ๆกˆใ€ๆˆ–ใ€Œๅฏซๅ…ฅ Resourceใ€ใ€‚

2.2 MCP Sampling

MCP 2025-03-26 ่ฆ็ฏ„ๆ–ฐๅขž Sampling ่ƒฝๅŠ›๏ผŒไฝ†้€™ๆ˜ฏ server ่ซ‹ๆฑ‚ client ๅš LLM ๆŽจ็†๏ผŒ้žๆช”ๆกˆไบคๆ›ใ€‚

2.3 ๅฏฆ้š›ๅฏ่กŒ็š„ๆช”ๆกˆไบคๆ›ๆ–นๅผ

ๆ–นๆณ• ๆ–นๅ‘ ๅฏฆไฝœ
Tool ๅƒๆ•ธ๏ผˆinline๏ผ‰ Agent โ†’ Server ็พ่กŒๆ–นๅผ๏ผšpipeline="yaml: ..."
Tool ๅƒๆ•ธ๏ผˆfile path๏ผ‰ Agent โ†’ Server Agent ๆไพ›ๆœฌๅœฐ่ทฏๅพ‘๏ผŒserver ่ฎ€ๅ–
Tool ๅƒๆ•ธ๏ผˆURL๏ผ‰ Agent โ†’ Server Agent ๆไพ› URL๏ผŒserver httpx.get()
Resource Template Server โ†’ Agent pipeline://saved/{name} ๅ‹•ๆ…‹่ฎ€ๅ–
Tool ๅ›žๅ‚ณๅ€ผ Server โ†’ Agent ๅ›žๅ‚ณ YAML/JSON ๆ–‡ๅญ—ๆˆ–ๆช”ๆกˆ่ทฏๅพ‘
Notification Server โ†’ Agent resources/updated ้€š็Ÿฅๆ–ฐ็ตๆžœ

3. ๆžถๆง‹่จญ่จˆ

3.1 ๅˆ†ๅฑคๆžถๆง‹๏ผˆDDD๏ผ‰

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Presentation Layer (MCP)                                     โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
โ”‚  โ”‚ MCP Tools                                               โ”‚ โ”‚
โ”‚  โ”‚  โ€ข save_pipeline(name, config)     โ†’ ไฟๅญ˜              โ”‚ โ”‚
โ”‚  โ”‚  โ€ข list_pipelines()                โ†’ ๅˆ—่ˆ‰              โ”‚ โ”‚
โ”‚  โ”‚  โ€ข load_pipeline(name|url|path)    โ†’ ่ผ‰ๅ…ฅ              โ”‚ โ”‚
โ”‚  โ”‚  โ€ข delete_pipeline(name)           โ†’ ๅˆช้™ค              โ”‚ โ”‚
โ”‚  โ”‚  โ€ข schedule_pipeline(name, cron)   โ†’ ๆŽ’็จ‹              โ”‚ โ”‚
โ”‚  โ”‚  โ€ข list_schedules()                โ†’ ๅˆ—่ˆ‰ๆŽ’็จ‹          โ”‚ โ”‚
โ”‚  โ”‚  โ€ข get_pipeline_history(name)      โ†’ ๅŸท่กŒๆญทๅฒ          โ”‚ โ”‚
โ”‚  โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚
โ”‚  โ”‚ MCP Resources (ๅ”ฏ่ฎ€)                                    โ”‚ โ”‚
โ”‚  โ”‚  โ€ข pipeline://saved/{name}         โ†’ ่ฎ€ๅ–ๅทฒๅญ˜ pipeline โ”‚ โ”‚
โ”‚  โ”‚  โ€ข pipeline://templates/{name}     โ†’ ๆจกๆฟๅƒ่€ƒ          โ”‚ โ”‚
โ”‚  โ”‚  โ€ข pipeline://history/{name}/latest โ†’ ๆœ€ๆ–ฐๅŸท่กŒ็ตๆžœ     โ”‚ โ”‚
โ”‚  โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚
โ”‚  โ”‚ MCP Notifications                                       โ”‚ โ”‚
โ”‚  โ”‚  โ€ข resources/updated               โ†’ Pipeline ็ตๆžœๆ›ดๆ–ฐ โ”‚ โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ Application Layer                                            โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
โ”‚  โ”‚ PipelineStore                                           โ”‚ โ”‚
โ”‚  โ”‚  โ€ข save(name, config, scope) โ†’ PipelineMeta            โ”‚ โ”‚
โ”‚  โ”‚  โ€ข load(name) โ†’ PipelineConfig  (workspace โ†’ global)   โ”‚ โ”‚
โ”‚  โ”‚  โ€ข load_from_url(url) โ†’ PipelineConfig                 โ”‚ โ”‚
โ”‚  โ”‚  โ€ข load_from_path(path) โ†’ PipelineConfig               โ”‚ โ”‚
โ”‚  โ”‚  โ€ข list(scope?) โ†’ list[PipelineMeta]                   โ”‚ โ”‚
โ”‚  โ”‚  โ€ข delete(name)                                         โ”‚ โ”‚
โ”‚  โ”‚  โ€ข get_history(name, limit) โ†’ list[PipelineRun]        โ”‚ โ”‚
โ”‚  โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚
โ”‚  โ”‚ PipelineScheduler                                       โ”‚ โ”‚
โ”‚  โ”‚  โ€ข schedule(name, cron_expr) โ†’ ScheduleEntry           โ”‚ โ”‚
โ”‚  โ”‚  โ€ข unschedule(name)                                     โ”‚ โ”‚
โ”‚  โ”‚  โ€ข list_schedules() โ†’ list[ScheduleEntry]              โ”‚ โ”‚
โ”‚  โ”‚  โ€ข _tick() โ†’ ๆชขๆŸฅๅˆฐๆœŸไปปๅ‹™                              โ”‚ โ”‚
โ”‚  โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚
โ”‚  โ”‚ PipelineExecutor (ๅทฒๆœ‰)                                 โ”‚ โ”‚
โ”‚  โ”‚  + run_and_store(config) โ†’ ๅŸท่กŒ + ๅ„ฒๅญ˜็ตๆžœ             โ”‚ โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ Domain Layer                                                 โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
โ”‚  โ”‚ Entities                                                โ”‚ โ”‚
โ”‚  โ”‚  โ€ข PipelineConfig (ๅทฒๆœ‰)                                โ”‚ โ”‚
โ”‚  โ”‚  โ€ข PipelineMeta(name, created, updated, tags, hash)     โ”‚ โ”‚
โ”‚  โ”‚  โ€ข PipelineRun(id, pipeline_name, started, finished,    โ”‚ โ”‚
โ”‚  โ”‚    status, article_count, result_summary)               โ”‚ โ”‚
โ”‚  โ”‚  โ€ข ScheduleEntry(pipeline_name, cron, next_run,         โ”‚ โ”‚
โ”‚  โ”‚    enabled, last_run, last_status)                      โ”‚ โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ Infrastructure Layer                                         โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
โ”‚  โ”‚ File Storage                                            โ”‚ โ”‚
โ”‚  โ”‚  Workspace scope (ๅ„ชๅ…ˆ):                                โ”‚ โ”‚
โ”‚  โ”‚  โ€ข {workspace}/.pubmed-search/pipelines/{name}.yaml    โ”‚ โ”‚
โ”‚  โ”‚  โ€ข {workspace}/.pubmed-search/pipeline_runs/{name}/    โ”‚ โ”‚
โ”‚  โ”‚  Global scope (fallback):                               โ”‚ โ”‚
โ”‚  โ”‚  โ€ข ~/.pubmed-search-mcp/pipelines/{name}.yaml          โ”‚ โ”‚
โ”‚  โ”‚  โ€ข ~/.pubmed-search-mcp/pipelines/_index.json          โ”‚ โ”‚
โ”‚  โ”‚  โ€ข ~/.pubmed-search-mcp/pipeline_runs/{name}/          โ”‚ โ”‚
โ”‚  โ”‚    โ””โ”€โ”€ {run_id}.json                                    โ”‚ โ”‚
โ”‚  โ”‚  โ€ข ~/.pubmed-search-mcp/schedules.json                 โ”‚ โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

3.2 ๆช”ๆกˆ็ตๆง‹๏ผˆ้›™ๅฑคๅ„ฒๅญ˜๏ผ‰

ๆฑบ็ญ– D7๏ผšๆŽก็”จ workspace + global ้›™ๅฑคๅ„ฒๅญ˜ใ€‚ Workspace scope ๅฏ git ่ฟฝ่นคใ€ๅˆ†ไบซ็ตฆๅ”ไฝœ่€…๏ผ›Global scope ่ทจๅฐˆๆกˆๅ…ฑ็”จใ€‚

่งฃๆžๅ„ชๅ…ˆ้ †ๅบ

load(name) โ†’ ๅ…ˆๆŸฅ workspace/.pubmed-search/pipelines/{name}.yaml
           โ†’ ๆฒ’ๆ‰พๅˆฐ โ†’ ๆŸฅ ~/.pubmed-search-mcp/pipelines/{name}.yaml
           โ†’ ้ƒฝๆฒ’ๆœ‰ โ†’ 404 error

Workspace scope๏ผˆๅฐˆๆกˆ็ดš๏ผŒๅฏ git ่ฟฝ่นค๏ผ‰

{workspace}/                         # VS Code workspace root
โ””โ”€โ”€ .pubmed-search/                  # ๅฐˆๆกˆ็ดš่จญๅฎš็›ฎ้Œ„
    โ”œโ”€โ”€ pipelines/                   # Pipeline ๅ„ฒๅญ˜
    โ”‚   โ”œโ”€โ”€ weekly_remimazolam.yaml   # ๅทฒๅญ˜ pipeline
    โ”‚   โ””โ”€โ”€ pico_icu_sedation.yaml
    โ””โ”€โ”€ pipeline_runs/               # ๅŸท่กŒๆญทๅฒ๏ผˆๅปบ่ญฐ .gitignore๏ผ‰
        โ””โ”€โ”€ weekly_remimazolam/
            โ””โ”€โ”€ 20260215_143022.json

Global scope๏ผˆไฝฟ็”จ่€…็ดš๏ผŒ่ทจๅฐˆๆกˆ๏ผ‰

~/.pubmed-search-mcp/               # data_dir (็พๆœ‰)
โ”œโ”€โ”€ sessions.json                    # ็พๆœ‰
โ”œโ”€โ”€ article_cache.json               # ็พๆœ‰
โ”œโ”€โ”€ pipelines/                       # ๆ–ฐๅขž๏ผšPipeline ๅ„ฒๅญ˜
โ”‚   โ”œโ”€โ”€ _index.json                  # ็ดขๅผ•๏ผš{name โ†’ PipelineMeta}
โ”‚   โ”œโ”€โ”€ weekly_remimazolam.yaml      # ๅทฒๅญ˜ pipeline
โ”‚   โ””โ”€โ”€ crispr_monthly.yaml
โ”œโ”€โ”€ pipeline_runs/                   # ๆ–ฐๅขž๏ผšๅŸท่กŒๆญทๅฒ
โ”‚   โ”œโ”€โ”€ weekly_remimazolam/
โ”‚   โ”‚   โ”œโ”€โ”€ 20260215_143022.json     # ๆฏๆฌกๅŸท่กŒ็š„็ตๆžœๆ‘˜่ฆ
โ”‚   โ”‚   โ””โ”€โ”€ 20260222_143015.json
โ”‚   โ””โ”€โ”€ pico_icu_sedation/
โ”‚       โ””โ”€โ”€ 20260215_090000.json
โ””โ”€โ”€ schedules.json                   # ๆ–ฐๅขž๏ผšๆŽ’็จ‹่จญๅฎš

Scope ้ธๆ“‡้‚่ผฏ

ๆ“ไฝœ ้ ่จญ scope ๅฏ่ฆ†ๅฏซ
save_pipeline workspace๏ผˆ่‹ฅๆœ‰๏ผ‰โ†’ global scope ๅƒๆ•ธ
load_pipeline workspace โ†’ global fallback ่‡ชๅ‹•
list_pipelines ๅˆไฝต้กฏ็คบ๏ผˆๆจ™่จป scope๏ผ‰ scope ๅƒๆ•ธ้Žๆฟพ
delete_pipeline ็ฒพ็ขบๅŒน้…๏ผˆๅ…ˆ workspace๏ผ‰ ่‡ชๅ‹•
get_pipeline_history ่ทŸ้šจ pipeline ๆ‰€ๅœจ scope ่‡ชๅ‹•
schedule_pipeline ๅƒ… global๏ผˆๆŽ’็จ‹้œ€่ทจ workspace๏ผ‰ ๅ›บๅฎš

3.3 Pipeline YAML ๆ ผๅผ๏ผˆๆŒไน…ๅŒ–็‰ˆๆœฌ๏ผ‰

# ~/.pubmed-search-mcp/pipelines/weekly_remimazolam.yaml
# ่ˆ‡ unified_search pipeline ๅƒๆ•ธๆ ผๅผๅฎŒๅ…จ็›ธๅฎน

name: "Weekly Remimazolam Review"
tags: [anesthesia, sedation, remimazolam]

# ๆ–นๅผไธ€๏ผšไฝฟ็”จๆจกๆฟ
template: comprehensive
template_params:
  query: "remimazolam sedation ICU"
  sources: "pubmed,openalex,europe_pmc"
  limit: 30
  min_year: 2024

# ๆ–นๅผไบŒ๏ผš่‡ชๅฎš็พฉ steps๏ผˆ่ˆ‡็พ่กŒๆ ผๅผๅฎŒๅ…จไธ€่‡ด๏ผ‰
# steps:
#   - id: s1
#     action: search
#     params: { query: "remimazolam", sources: "pubmed", limit: 20 }
#   ...

output:
  format: markdown
  limit: 20
  ranking: quality

# ๆŒไน…ๅŒ–็‰นๆœ‰ๆฌ„ไฝ๏ผˆไธๅฝฑ้ŸฟๅŸท่กŒ๏ผ‰
schedule:
  cron: "0 9 * * 1"        # ๆฏ้€ฑไธ€ 09:00
  enabled: true
  notify: true             # ๆœ‰ๆ–ฐ็ตๆžœๆ™‚้€š็Ÿฅ
  diff_mode: true          # ๅช้กฏ็คบ่ˆ‡ไธŠๆฌกไธๅŒ็š„็ตๆžœ

4. MCP ๅทฅๅ…ท่จญ่จˆ

4.1 Pipeline ็ฎก็†ๅทฅๅ…ท

@mcp.tool()
async def save_pipeline(
    name: str,
    config: str,               # YAML/JSON ๅญ—ไธฒ๏ผˆ่ˆ‡ unified_search pipeline ๅƒๆ•ธ่ฆๆ ผ็›ธๅŒ๏ผ‰
    tags: str = "",            # ้€—่™Ÿๅˆ†้š”ๆจ™็ฑค
    description: str = "",
) -> str:
    """Save a pipeline configuration for reuse.

    The config format is identical to unified_search's pipeline parameter.
    Saved pipelines can be loaded by name in unified_search:
        unified_search(pipeline="saved:weekly_remimazolam")
    """

@mcp.tool()
async def list_pipelines(
    tag: str = "",             # ๆŒ‰ๆจ™็ฑค้Žๆฟพ
) -> str:
    """List all saved pipeline configurations."""

@mcp.tool()
async def load_pipeline(
    source: str,               # name | file:///path | https://url | saved:name
) -> str:
    """Load a pipeline from name, file path, or URL.

    Supports:
    - Saved pipeline: "weekly_remimazolam" or "saved:weekly_remimazolam"
    - Local file: "file:///path/to/pipeline.yaml"
    - URL: "https://example.com/pipelines/my_search.yaml"

    Returns the pipeline YAML for review before execution.
    To execute, pass the result to unified_search(pipeline=...).
    """

@mcp.tool()
async def delete_pipeline(name: str) -> str:
    """Delete a saved pipeline configuration."""

4.2 ๆŽ’็จ‹ๅทฅๅ…ท

@mcp.tool()
async def schedule_pipeline(
    name: str,                 # ๅทฒๅญ˜ pipeline ๅ็จฑ
    cron: str = "",            # cron ่กจ้”ๅผ๏ผˆ็ฉบ = ๅœๆญขๆŽ’็จ‹๏ผ‰
    diff_mode: bool = True,    # ๅช้กฏ็คบๆ–ฐๅขžๆ–‡็ซ 
    notify: bool = True,       # ๆœ‰็ตๆžœๆ™‚้€š็Ÿฅ
) -> str:
    """Schedule a saved pipeline for periodic execution.

    Cron format: "minute hour day month weekday"
    Examples:
    - "0 9 * * 1"    โ†’ Every Monday 9:00 AM
    - "0 0 1 * *"    โ†’ First day of each month
    - "0 */6 * * *"  โ†’ Every 6 hours
    - ""             โ†’ Remove schedule
    """

@mcp.tool()
async def list_schedules() -> str:
    """List all scheduled pipeline executions with next run times."""

@mcp.tool()
async def get_pipeline_history(
    name: str,
    limit: int = 5,
) -> str:
    """Get execution history for a saved pipeline.

    Shows: date, article count, new articles vs. previous run, status.
    """

4.3 unified_search ๆ“ดๅฑ•

# ็พๆœ‰็š„ pipeline ๅƒๆ•ธๆ“ดๅฑ•ๆ”ฏๆด saved: ๅ’Œ url: ๅ‰็ถด
async def unified_search(
    ...,
    pipeline: str | None = None,
    # ๆ–ฐๅขžๆ”ฏๆดๆ ผๅผ๏ผš
    # "saved:weekly_remimazolam"   โ†’ ๅพžๆŒไน…ๅŒ–่ผ‰ๅ…ฅ
    # "url:https://example.com/p.yaml" โ†’ ๅพž URL ่ผ‰ๅ…ฅ
    # "file:///path/to/p.yaml"    โ†’ ๅพžๆœฌๅœฐ่ผ‰ๅ…ฅ๏ผˆ้œ€่ฆ Agent ๆœ‰ fs ๅญ˜ๅ–๏ผ‰
    # ๅŽŸๆœ‰ inline YAML/JSON ไป็„ถๆ”ฏๆด
):

4.4 MCP Resource Templates

@mcp.resource("pipeline://saved/{name}")
async def get_saved_pipeline(name: str) -> str:
    """Read a saved pipeline configuration."""
    store = _get_pipeline_store()
    config = store.load(name)
    return yaml.dump(dataclasses.asdict(config))

@mcp.resource("pipeline://templates/{name}")
async def get_template_info(name: str) -> str:
    """Read template reference with parameters and example."""
    entry = PIPELINE_TEMPLATES.get(name)
    return json.dumps(entry, indent=2)

@mcp.resource("pipeline://history/{name}/latest")
async def get_latest_run(name: str) -> str:
    """Read the latest execution result for a pipeline."""
    store = _get_pipeline_store()
    run = store.get_latest_run(name)
    return json.dumps(dataclasses.asdict(run))

5. ๆŽ’็จ‹ๆฉŸๅˆถ่จญ่จˆ

5.1 ๆ–นๆกˆๆฏ”่ผƒ

ๆ–นๆกˆ ๅ„ช้ปž ็ผบ้ปž
A. ๅ…งๅปบ asyncio scheduler ้›ถไพ่ณดใ€่ˆ‡ MCP server ๅŒ็”Ÿๅ‘ฝ้€ฑๆœŸ ้œ€ server ๆŒ็บŒ้‹่กŒใ€้‡ๅ•ŸๅคฑๅŽปๆŽ’็จ‹
B. OS cron + CLI entrypoint ็ฉฉๅฎšใ€็ณป็ตฑ็ดšๆŽ’็จ‹ ้œ€่ฆ้กๅค– CLIใ€็„กๆณ•ๅ‹•ๆ…‹็ฎก็†
C. APScheduler ๅŠŸ่ƒฝๅฎŒๆ•ดใ€ๆ”ฏๆด persistence ๆ–ฐไพ่ณดใ€ๅฏ่ƒฝ้Žๅบฆ่จญ่จˆ
D. ๆททๅˆๆ–นๆกˆ๏ผšๅ…งๅปบ tick + JSON state ่ผ•้‡ใ€ๅฏๆขๅพฉ ็ฒพๅบฆๅ—้™ๆ–ผ tick ้–“้š”

5.2 ๆŽจ่–ฆๆ–นๆกˆ๏ผšD๏ผˆๆททๅˆๆ–นๆกˆ๏ผ‰

                    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                    โ”‚  MCP Server Lifespan     โ”‚
                    โ”‚                          โ”‚
                    โ”‚  startup:                โ”‚
                    โ”‚    load schedules.json   โ”‚
                    โ”‚    start _tick_loop()    โ”‚
                    โ”‚                          โ”‚
                    โ”‚  _tick_loop (ๆฏ60็ง’):     โ”‚
                    โ”‚    for schedule in sched: โ”‚
                    โ”‚      if should_run():    โ”‚
                    โ”‚        asyncio.create_   โ”‚
                    โ”‚          task(execute()) โ”‚
                    โ”‚        update next_run   โ”‚
                    โ”‚                          โ”‚
                    โ”‚  shutdown:               โ”‚
                    โ”‚    save schedules.json   โ”‚
                    โ”‚    cancel background     โ”‚
                    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

ๆ ธๅฟƒ้‚่ผฏ๏ผš

class PipelineScheduler:
    def __init__(self, store: PipelineStore, executor: PipelineExecutor):
        self._store = store
        self._executor = executor
        self._schedules: dict[str, ScheduleEntry] = {}
        self._task: asyncio.Task | None = None

    async def start(self):
        """Load schedules and start tick loop."""
        self._schedules = self._load_schedules()
        self._task = asyncio.create_task(self._tick_loop())

    async def _tick_loop(self):
        """Check every 60 seconds for due pipelines."""
        while True:
            await asyncio.sleep(60)
            now = datetime.now(UTC)
            for name, entry in self._schedules.items():
                if entry.enabled and entry.next_run <= now:
                    asyncio.create_task(self._execute_scheduled(name, entry))
                    entry.next_run = self._compute_next_run(entry.cron)
            self._save_schedules()

    async def _execute_scheduled(self, name: str, entry: ScheduleEntry):
        """Execute a scheduled pipeline and store results."""
        config = self._store.load(name)
        results = await self._executor.execute(config)
        run = PipelineRun(
            pipeline_name=name,
            started=datetime.now(UTC),
            article_count=len(results.articles),
            # diff with previous run...
        )
        self._store.save_run(name, run)
        # Notify via MCP resource update
        if entry.notify:
            await self._notify_resource_updated(name)

5.3 Diff Mode๏ผˆๅขž้‡ๆฏ”ๅฐ๏ผ‰

def compute_diff(current: list[str], previous: list[str]) -> PipelineDiff:
    """Compare PMID lists between runs.

    Returns:
        new_pmids: ๆœฌๆฌกๆ–ฐๅ‡บ็พ็š„
        removed_pmids: ไธŠๆฌกๆœ‰ไฝ†ๆœฌๆฌกๆฒ’ๆœ‰็š„
        unchanged_count: ๅ…ฉๆฌก้ƒฝๆœ‰็š„
    """
    current_set = set(current)
    previous_set = set(previous)
    return PipelineDiff(
        new_pmids=sorted(current_set - previous_set),
        removed_pmids=sorted(previous_set - current_set),
        unchanged_count=len(current_set & previous_set),
    )

6. ๅฎ‰ๅ…จๆ€ง่€ƒ้‡

้ขจ้šช ็ทฉ่งฃๆŽชๆ–ฝ
URL ่ณ‡ๆบๆณจๅ…ฅ๏ผˆSSRF๏ผ‰ ็™ฝๅๅ–ฎๅŸŸๅ๏ผšgithub.com, gist.github.com, raw.githubusercontent.com
ๆœฌๅœฐ่ทฏๅพ‘็ฉฟ่ถŠ ้™ๅˆถๅœจ data_dir ไธ‹๏ผŒ็ฆๆญข .. ๅ’Œ็ฌฆ่™Ÿ้€ฃ็ต
ๆŽ’็จ‹่ณ‡ๆบ่€—็›ก ๆœ€ๅฐ้–“้š” 1 ๅฐๆ™‚ใ€ๅŒๆ™‚ๆœ€ๅคš 5 ๅ€‹ๆŽ’็จ‹ใ€ๅŸท่กŒ่ถ…ๆ™‚ 5 ๅˆ†้˜
็ฃ็ขŸ็ฉบ้–“ ๆฏๅ€‹ pipeline ๆœ€ๅคšไฟ็•™ 100 ๆฌกๅŸท่กŒๆญทๅฒ
API Rate Limit ๆŽ’็จ‹ๅŸท่กŒไฝฟ็”จ้™็ดšๆจกๅผ๏ผˆๆธ›ๅฐ‘ sourcesใ€้™ไฝŽ limit๏ผ‰

7. ๅฏฆไฝœ้šŽๆฎต

Phase 1: Pipeline ๆŒไน…ๅŒ–๏ผˆๆ ธๅฟƒ๏ผŒไฝŽ้ขจ้šช๏ผ‰

้ ไผฐ๏ผš2-3 ๅ€‹ๅทฅไฝœๆ—ฅ

ไปปๅ‹™ ๅฑค ๆช”ๆกˆ
PipelineMeta, PipelineRun entities Domain domain/entities/pipeline.py
PipelineStore (save/load/list/delete) Application application/pipeline/store.py
save_pipeline, list_pipelines, load_pipeline, delete_pipeline tools Presentation tools/pipeline_tools.py
Resource templates pipeline://saved/{name} Presentation resources.py
DI Container ๆ•ดๅˆ Infrastructure container.py
Tests Tests tests/test_pipeline_store.py

ๆญค้šŽๆฎตๅฎŒๆˆๅพŒ๏ผšAgent ๅฏไปฅๅญ˜/ๅ–/ๅˆ—่ˆ‰ pipeline ้…็ฝฎ๏ผŒunified_search(pipeline="saved:xxx") ๅฏ่ผ‰ๅ…ฅๅทฒๅญ˜ๆ–นๆกˆใ€‚

Phase 2: ๅค–้ƒจ่ผ‰ๅ…ฅ๏ผˆURL / ๆช”ๆกˆ่ทฏๅพ‘๏ผ‰

้ ไผฐ๏ผš1 ๅ€‹ๅทฅไฝœๆ—ฅ

ไปปๅ‹™ ๅฑค ๅ‚™่จป
load_from_url(url) Application httpx.get() + YAML/JSON parse + ๅŸŸๅ็™ฝๅๅ–ฎ
load_from_path(path) Application pathlib.Path.read_text() + ่ทฏๅพ‘้ฉ—่ญ‰
unified_search pipeline ๅƒๆ•ธๆ”ฏๆด url: file: ๅ‰็ถด Presentation ่ทฏ็”ฑๅˆฐ store
ๅฎ‰ๅ…จๆธฌ่ฉฆ Tests SSRF ้˜ฒ่ญทใ€่ทฏๅพ‘็ฉฟ่ถŠๆธฌ่ฉฆ

Phase 3: ๅŸท่กŒๆญทๅฒ่ˆ‡ Diff

้ ไผฐ๏ผš1-2 ๅ€‹ๅทฅไฝœๆ—ฅ

ไปปๅ‹™ ๅฑค ๅ‚™่จป
PipelineRun ๆŒไน…ๅŒ– Application pipeline_runs/{name}/{run_id}.json
Diff ่จˆ็ฎ— Application PMID ้›†ๅˆๅทฎ็•ฐ
get_pipeline_history tool Presentation ้กฏ็คบๆญทๅฒ + diff
Resource pipeline://history/{name}/latest Presentation ๆœ€ๆ–ฐ็ตๆžœ

Phase 4: ๆŽ’็จ‹ๆœๅฐ‹

้ ไผฐ๏ผš2-3 ๅ€‹ๅทฅไฝœๆ—ฅ๏ผˆๆœ€่ค‡้›œ๏ผ‰

ไปปๅ‹™ ๅฑค ๅ‚™่จป
ScheduleEntry entity Domain cron + state
PipelineScheduler Application tick loop + execute + notify
Lifespan ๆ•ดๅˆ Presentation startup/shutdown
schedule_pipeline, list_schedules tools Presentation
Cron ่งฃๆž๏ผˆcroniter ๆˆ–่‡ชๅฏฆไฝœ๏ผ‰ Infrastructure ่ผ•้‡ cron parser
Resource ๆ›ดๆ–ฐ้€š็Ÿฅ Presentation resources/updated

8. ๆ›ฟไปฃๆ–นๆกˆ

8.1 ไธๅšๆŽ’็จ‹๏ผŒๅƒ…ๅšๆŒไน…ๅŒ– + CLI runner

่‹ฅๆŽ’็จ‹่ค‡้›œๅบฆ้Ž้ซ˜๏ผŒๅฏๆ”น็”จ OS ็ดšๆ–นๆกˆ๏ผš

# ไฝฟ็”จ่€…่‡ช่กŒ่จญๅฎš OS cron / Windows Task Scheduler
# ๆไพ› CLI entrypoint ่ฎ“ cron ๅ‘ผๅซ

# crontab -e
0 9 * * 1  pubmed-search run-pipeline weekly_remimazolam --diff

# ๆˆ– Windows Task Scheduler
schtasks /create /tn "WeeklyPubMed" /tr "uv run python -m pubmed_search.cli run-pipeline weekly_remimazolam" /sc weekly /d MON /st 09:00

ๆญคๆ–นๆกˆ้™ไฝŽๅ…ง้ƒจ่ค‡้›œๅบฆ๏ผŒไฝ†ไฝฟ็”จ่€…้ซ”้ฉ—่ผƒๅทฎ๏ผˆ้œ€ๆ‰‹ๅ‹•่จญๅฎš OS ๆŽ’็จ‹๏ผ‰ใ€‚

8.2 ไธๅš็จ็ซ‹ๅทฅๅ…ท๏ผŒๆ“ดๅฑ• unified_search

ไธๆ–ฐๅขž MCP tools๏ผŒๅƒ…ๆ“ดๅฑ• unified_search ็š„ pipeline ๅƒๆ•ธ๏ผš

# ็›ดๆŽฅๅœจ pipeline ๅƒๆ•ธไธญๆ”ฏๆดๆ‰€ๆœ‰ๆ“ไฝœ
unified_search(pipeline="save:my_plan")           # ไฟๅญ˜็•ถๅ‰ๆœๅฐ‹
unified_search(pipeline="saved:my_plan")           # ่ผ‰ๅ…ฅๅทฒๅญ˜
unified_search(pipeline="url:https://example.com") # ๅพž URL
unified_search(pipeline="list")                    # ๅˆ—่ˆ‰

ๅ„ช้ปž๏ผšไธๅขžๅŠ  MCP tool ๆ•ธ้‡ใ€‚็ผบ้ปž๏ผšpipeline ๅƒๆ•ธ่ชž็พฉ้Ž่ผ‰ใ€‚


9. ๆœช่งฃๆฑบๅ•้กŒ

  1. ้€š็ŸฅๆฉŸๅˆถ๏ผšๆŽ’็จ‹ๅŸท่กŒๅฎŒๆˆๅพŒๅฆ‚ไฝ•้€š็Ÿฅ Agent๏ผŸMCP resources/updated ้œ€่ฆ client ๆœ‰ subscription ๆ”ฏๆด๏ผŒ็›ฎๅ‰ๅคšๆ•ธ MCP client (Claude Desktop, Copilot) ๅฏ่ƒฝไธๆ”ฏๆดใ€‚ๆ›ฟไปฃๆ–นๆกˆ๏ผšไธ‹ๆฌก Agent ้€ฃ็ทšๆ™‚ๅœจ session ไธญ้กฏ็คบๆœช่ฎ€็ตๆžœใ€‚

  2. ๅคš Server ๅฏฆไพ‹๏ผšDocker ้ƒจ็ฝฒไธญๅฏ่ƒฝๆœ‰ๅคšๅ€‹ server ๅ‰ฏๆœฌ๏ผŒๆŽ’็จ‹้œ€้ฟๅ…้‡่ค‡ๅŸท่กŒใ€‚ๅปบ่ญฐไฝฟ็”จ file lock ๆˆ–ๅƒ…ๅ…่จฑไธ€ๅ€‹ๅฏฆไพ‹ๅ•Ÿ็”จๆŽ’็จ‹ใ€‚

  3. YAML ไพ่ณด๏ผš็›ฎๅ‰ pipeline ่งฃๆžๅทฒ็ถ“ๅšไบ† YAML๏ผˆ_parse_pipeline_config ็”จ yaml.safe_load๏ผ‰๏ผŒไฝ† pyyaml ๆ˜ฏ optional dependencyใ€‚ๆŒไน…ๅŒ–ๅ„ฒๅญ˜ๆ ผๅผๆ˜ฏๅฆ็ตฑไธ€็”จ YAML๏ผŸๆˆ–ๅ…่จฑ JSON/YAML ้›™ๆ ผๅผ๏ผŸ

  4. ็ตๆžœๅ„ฒๅญ˜ๅฎน้‡๏ผšๆฏๆฌกๅŸท่กŒ็š„็ตๆžœๆ‘˜่ฆๅญ˜ๅคšๅฐ‘๏ผŸๅปบ่ญฐๅญ˜ PMID ๅˆ—่กจ + ๅ‰ 5 ็ฏ‡ metadata๏ผˆๆจ™้กŒ/ๅนดไปฝ/ๆœŸๅˆŠ๏ผ‰๏ผŒไธๅญ˜ๅฎŒๆ•ด abstractใ€‚

  5. Agent ้ซ”้ฉ—๏ผšๆ–ฐๅขž 4-7 ๅ€‹ MCP tools ๆ˜ฏๅฆๆœƒ้€ ๆˆๅทฅๅ…ท้Žๅคš๏ผŸ่€ƒๆ…ฎๅˆไฝต็‚บ 2 ๅ€‹ๅทฅๅ…ท๏ผˆpipeline_manage + pipeline_schedule๏ผ‰็”จ action ๅƒๆ•ธๅ€ๅˆ†ใ€‚ โœ… ๅทฒๆฑบ็ญ– (D3)๏ผšๆŽก็”จ Option B โ€” 5 ๅ€‹็จ็ซ‹ๅทฅๅ…ทใ€‚่ฆ‹ Section 10 D3 ่ˆ‡ Section 11 ่ฉณ็ดฐ่จญ่จˆใ€‚


10. ๆฑบ็ญ–่จ˜้Œ„

ๅœจๅฏฆไฝœๅ‰๏ผŒ้œ€่ฆๅฐไปฅไธ‹ๅ•้กŒๅšๅ‡บๆฑบ็ญ–๏ผš

# ๆฑบ็ญ– ้ธ้ … ๅปบ่ญฐ
D1 ๅ„ฒๅญ˜ๆ ผๅผ JSON / YAML / ๅ…ฉ่€… YAML๏ผˆไบบ้กžๅฏ่ฎ€๏ผŒ่ˆ‡็พ่กŒ pipeline ๅƒๆ•ธไธ€่‡ด๏ผ‰
D2 ๆŽ’็จ‹ๅฏฆไฝœ ๅ…งๅปบ / OS cron / APScheduler ๅ…งๅปบ tick loop๏ผˆPhase 4๏ผ‰
D3 ๅทฅๅ…ทๆ•ธ้‡ โœ… Option B: 6 ๅ€‹็จ็ซ‹ๅทฅๅ…ท save_pipeline, list_pipelines, load_pipeline, delete_pipeline, get_pipeline_history, schedule_pipelineใ€‚่ฆ‹ Section 11ใ€‚
D4 URL ่ผ‰ๅ…ฅๅฎ‰ๅ…จ ็™ฝๅๅ–ฎ / ไปปๆ„ / ็ฆๆญข ็™ฝๅๅ–ฎ + ไฝฟ็”จ่€…ๅฏ้…็ฝฎ
D5 ็ตๆžœๅ„ฒๅญ˜ ๅฎŒๆ•ด/ๆ‘˜่ฆ/ๅƒ…PMID ๆ‘˜่ฆ๏ผˆPMID + top-5 metadata๏ผ‰
D6 ๆ˜ฏๅฆ้œ€่ฆ croniter ไพ่ณด ๆ˜ฏ/่‡ชๅฏฆไฝœ ่ฆ–่ค‡้›œๅบฆ๏ผŒ็ฐกๅ–ฎ cron ๅฏ่‡ชๅฏฆไฝœ
D7 ๅ„ฒๅญ˜ไฝ็ฝฎ็ฏ„ๅœ โœ… ้›™ๅฑค๏ผšworkspace + global workspace ๅ„ชๅ…ˆ๏ผˆๅฏ git ่ฟฝ่นค/ๅˆ†ไบซ๏ผ‰๏ผŒglobal ไฝœ็‚บ่ทจๅฐˆๆกˆ fallbackใ€‚่ฆ‹ Section 3.2ใ€‚

11. Option B ่ฉณ็ดฐ่จญ่จˆ๏ผš6 ๅ€‹็จ็ซ‹ MCP ๅทฅๅ…ท

ๆฑบ็ญ–ๆ—ฅๆœŸ๏ผš2026-02-15 ๆฑบ็ญ–่€…๏ผšๅฐˆๆกˆ็ถญ่ญท่€… ็†็”ฑ๏ผšๆฏๅ€‹ๅทฅๅ…ท่ท่ฒฌๅ–ฎไธ€ๆ˜Ž็ขบ๏ผŒ็ฌฆๅˆ MCP ๅทฅๅ…ท็š„่ชž็พฉ่จญ่จˆๅ“ฒๅญธ๏ผˆไธ€ๅ€‹ๅทฅๅ…ท = ไธ€ๅ€‹ๅ‹•ไฝœ๏ผ‰ใ€‚ ็›ธ่ผƒ Option A๏ผˆ2 ๅ€‹ๅˆไฝตๅทฅๅ…ท็”จ action ๅƒๆ•ธๅ€ๅˆ†๏ผ‰่ชž็พฉๆ›ดๆธ…ๆ™ฐ๏ผŒAgent ไธ้œ€็†่งฃ action ๅญๅ‘ฝไปคใ€‚ ็›ธ่ผƒ Option C๏ผˆๅ…จๅˆไฝต้€ฒ unified_search๏ผ‰้ฟๅ… unified_search ๅƒๆ•ธ้Ž่ผ‰ใ€‚

11.1 ๅทฅๅ…ท็ธฝ่ฆฝ

# ๅทฅๅ…ทๅ็จฑ Phase ้กžๅˆฅ ็”จ้€”
1 save_pipeline 1 pipeline ไฟๅญ˜ pipeline ้…็ฝฎ๏ผˆๆ”ฏๆด scope ้ธๆ“‡๏ผ‰
2 list_pipelines 1 pipeline ๅˆ—่ˆ‰ๅทฒๅญ˜ pipeline๏ผˆๅˆไฝต workspace + global๏ผ‰
3 load_pipeline 1 pipeline ่ผ‰ๅ…ฅ pipeline๏ผˆname / URL / path๏ผ‰
4 delete_pipeline 1 pipeline ๅˆช้™คๅทฒๅญ˜ pipeline
5 get_pipeline_history 3 pipeline ๆŸฅ่ฉขๅŸท่กŒๆญทๅฒ่ˆ‡ diff
6 schedule_pipeline 4 pipeline ๆŽ’็จ‹ / ่งฃ้™คๆŽ’็จ‹ / ๆŸฅ็œ‹ๆŽ’็จ‹

ๆ–ฐๅขž TOOL_CATEGORIES ๆข็›ฎ๏ผš

TOOL_CATEGORIES = {
    ...,
    "pipeline": [
        save_pipeline,
        list_pipelines,
        load_pipeline,
        delete_pipeline,
        get_pipeline_history,
        schedule_pipeline,
    ],
}

ๅทฅๅ…ท็ธฝๆ•ธ่ฎŠๅŒ–๏ผš33 โ†’ 39๏ผˆ+6๏ผ‰

11.2 ๅ„ๅทฅๅ…ท่ฉณ็ดฐ่ฆๆ ผ

Tool 1: save_pipeline

@mcp.tool()
async def save_pipeline(
    name: str,
    config: str,
    tags: str = "",
    description: str = "",
    scope: str = "auto",
) -> str:
    """Save a pipeline configuration for later reuse.

    The config format is identical to unified_search's pipeline parameter
    (YAML or JSON). Saved pipelines can be loaded later by name:
        unified_search(pipeline="saved:weekly_remimazolam")

    Args:
        name: Unique identifier (alphanumeric + hyphens/underscores, max 64 chars).
              Overwrites if name already exists (upsert semantics).
        config: Pipeline YAML/JSON string. Same format as unified_search pipeline param.
        tags: Comma-separated tags for filtering (e.g., "anesthesia,sedation").
        description: Human-readable description of the pipeline's purpose.
        scope: Storage scope - "workspace" (project-level, git-trackable),
               "global" (user-level, cross-project), or "auto" (workspace if
               available, otherwise global). Default: "auto".

    Returns:
        Confirmation with pipeline metadata (name, scope, created/updated timestamp, step count).
    """

ๅ›žๅ‚ณๆ ผๅผ๏ผš

โœ… Pipeline "weekly_remimazolam" saved successfully.

๐Ÿ“‹ Metadata:
  Name: weekly_remimazolam
  Description: Weekly remimazolam sedation literature review
  Tags: anesthesia, sedation
  Steps: 3 (search โ†’ filter โ†’ rank)
  Created: 2026-02-15 14:30:22 UTC
  Config hash: a1b2c3d4

๐Ÿ’ก Usage:
  โ€ข Execute: unified_search(pipeline="saved:weekly_remimazolam")
  โ€ข View: load_pipeline(source="weekly_remimazolam")
  โ€ข Schedule: schedule_pipeline(name="weekly_remimazolam", cron="0 9 * * 1")

้ฉ—่ญ‰่ฆๅ‰‡๏ผš

  • name ๅŒน้… ^[a-zA-Z0-9_-]{1,64}$
  • config ๅฟ…้ ˆๆ˜ฏๆœ‰ๆ•ˆ YAML/JSON ไธ”ๅฏ่งฃๆž็‚บ PipelineConfig
  • ้‡่ค‡ name ๆ™‚ๅŸท่กŒ upsert๏ผˆๆ›ดๆ–ฐ + ไฟ็•™ๆญทๅฒ๏ผ‰

Tool 2: list_pipelines

@mcp.tool()
async def list_pipelines(
    tag: str = "",
    scope: str = "",
) -> str:
    """List all saved pipeline configurations.

    Args:
        tag: Filter by tag (e.g., "sedation"). Empty = show all.
        scope: Filter by scope: "workspace", "global", or "" (show all). Default: "".

    Returns:
        Table of saved pipelines with name, scope, description, tags, last modified, execution count.
    """

ๅ›žๅ‚ณๆ ผๅผ๏ผš

๐Ÿ“ฆ Saved Pipelines (3 total, 2 workspace + 1 global):

| Name                   | Scope     | Description                          | Tags                  | Modified            | Runs |
|------------------------|-----------|--------------------------------------|-----------------------|---------------------|------|
| weekly_remimazolam     | workspace | Weekly remimazolam sedation review   | anesthesia, sedation  | 2026-02-15 14:30    | 12   |
| pico_icu_sedation      | workspace | PICO: remimazolam vs propofol in ICU | pico, icu             | 2026-02-10 09:00    | 3    |
| crispr_monthly         | global    | Monthly CRISPR gene therapy update   | gene_therapy, crispr  | 2026-02-01 00:00    | 5    |

๐Ÿ’ก Load: load_pipeline(source="<name>")
๐Ÿ’ก Execute: unified_search(pipeline="saved:<name>")

Tool 3: load_pipeline

@mcp.tool()
async def load_pipeline(
    source: str,
) -> str:
    """Load a pipeline configuration for review or editing.

    Loads from one of three sources:
    - Saved name: "weekly_remimazolam" or "saved:weekly_remimazolam"
    - Local file: "file:path/to/pipeline.yaml" (relative to data_dir or absolute)
    - URL: "url:https://example.com/pipelines/my_search.yaml"

    The returned YAML can be reviewed, modified, and then:
    - Executed directly: unified_search(pipeline="<yaml>")
    - Saved with changes: save_pipeline(name="...", config="<yaml>")

    Args:
        source: Pipeline source identifier (see above).

    Returns:
        Full pipeline YAML content + metadata.
    """

ๅ›žๅ‚ณๆ ผๅผ๏ผš

๐Ÿ“„ Pipeline: weekly_remimazolam
๐Ÿ“ Source: saved (local)
๐Ÿ“… Last modified: 2026-02-15 14:30:22 UTC

---
template: comprehensive
template_params:
  query: "remimazolam sedation ICU"
  sources: "pubmed,openalex,europe_pmc"
  limit: 30
  min_year: 2024
output:
  format: markdown
  limit: 20
  ranking: quality
---

๐Ÿ’ก Execute: unified_search(pipeline="saved:weekly_remimazolam")
๐Ÿ’ก Edit & re-save: save_pipeline(name="weekly_remimazolam", config="<modified yaml>")

URL ๅฎ‰ๅ…จ๏ผš

  • ็™ฝๅๅ–ฎๅŸŸๅ๏ผšgithub.com, gist.github.com, raw.githubusercontent.com
  • ไฝฟ็”จ่€…ๅฏ้€้Ž็’ฐๅขƒ่ฎŠๆ•ธ PIPELINE_URL_ALLOWLIST ๆ“ดๅ……
  • ๅ›žๆ‡‰ Content-Type ๅฟ…้ ˆ็‚บ text/plain, text/yaml, application/json, application/yaml
  • ๆœ€ๅคงไธ‹่ผ‰ๅคงๅฐ๏ผš100 KB

Tool 4: delete_pipeline

@mcp.tool()
async def delete_pipeline(
    name: str,
) -> str:
    """Delete a saved pipeline configuration and its execution history.

    Args:
        name: Name of the saved pipeline to delete.

    Returns:
        Confirmation of deletion.
    """

ๅ›žๅ‚ณๆ ผๅผ๏ผš

๐Ÿ—‘๏ธ Pipeline "weekly_remimazolam" deleted.
  - Configuration removed
  - 12 execution history records removed
  - Schedule removed (was: every Monday 09:00)

่กŒ็‚บ๏ผš

  • ๅˆช้™ค ~/.pubmed-search-mcp/pipelines/{name}.yaml
  • ๅˆช้™ค ~/.pubmed-search-mcp/pipeline_runs/{name}/ ๆ•ดๅ€‹็›ฎ้Œ„
  • ่‹ฅๆœ‰ๆŽ’็จ‹๏ผŒๅŒๆ™‚็งป้™คๆŽ’็จ‹ๆข็›ฎ
  • ไธๅญ˜ๅœจๆ™‚ๅ›žๅ‚ณ 404 ่ชž็พฉ้Œฏ่ชค่จŠๆฏ

Tool 5: get_pipeline_history

@mcp.tool()
async def get_pipeline_history(
    name: str,
    limit: int = 5,
) -> str:
    """Get execution history for a saved pipeline.

    Shows past execution results with diff analysis: which articles are new
    compared to the previous run.

    Args:
        name: Name of the saved pipeline.
        limit: Maximum number of history entries to return (default: 5).

    Returns:
        Execution history with date, article count, new/removed articles, status.
    """

ๅ›žๅ‚ณๆ ผๅผ๏ผš

๐Ÿ“Š Execution History for "weekly_remimazolam" (showing 5 of 12):

| # | Date                | Articles | New | Removed | Status |
|---|---------------------|----------|-----|---------|--------|
| 12| 2026-02-15 09:00    | 15       | +3  | -0      | โœ… OK  |
| 11| 2026-02-08 09:00    | 12       | +1  | -0      | โœ… OK  |
| 10| 2026-02-01 09:00    | 11       | +2  | -1      | โœ… OK  |
|  9| 2026-01-25 09:00    | 10       | +0  | -0      | โœ… OK  |
|  8| 2026-01-18 09:00    | 10       | +4  | -0      | โœ… OK  |

Latest new articles (run #12):
  1. PMID 39876543 - "Remimazolam vs propofol for ICU sedation..." (2026)
  2. PMID 39876100 - "Safety profile of remimazolam in critically..." (2026)
  3. PMID 39875999 - "Pharmacokinetics of remimazolam in renal..." (2026)

๐Ÿ’ก Full details: fetch_article_details(pmids="39876543,39876100,39875999")

่กŒ็‚บ๏ผš

  • ๅพž pipeline ๆ‰€ๅœจ scope๏ผˆworkspace ๆˆ– global๏ผ‰่ฎ€ๅ– pipeline_runs/{name}/ ็›ฎ้Œ„
  • ๆฏ็ญ†ๅŸท่กŒ่จ˜้Œ„ๅ„ฒๅญ˜๏ผšPMID ๅˆ—่กจ + ๅ‰ 5 ็ฏ‡ metadata๏ผˆๆจ™้กŒ/ๅนดไปฝ/ๆœŸๅˆŠ๏ผ‰
  • Diff ่จˆ็ฎ—๏ผš่ˆ‡ๅ‰ไธ€ๆฌกๅŸท่กŒ็š„ PMID ้›†ๅˆๅทฎ็•ฐ
  • ไธๅญ˜ๅœจๅŸท่กŒๆญทๅฒๆ™‚ๅ›žๅ‚ณๆ็คบใ€ŒๅฐšๆœชๅŸท่กŒ้Žใ€

Tool 6: schedule_pipeline

@mcp.tool()
async def schedule_pipeline(
    name: str,
    cron: str = "",
    diff_mode: bool = True,
    notify: bool = True,
    action: str = "set",
) -> str:
    """Schedule a saved pipeline for periodic execution, or list all schedules.

    Args:
        name: Saved pipeline name. Use "*" with action="list" to list all schedules.
        cron: Cron expression (5-field). Empty string with action="set" removes schedule.
              Examples: "0 9 * * 1" (Mon 9am), "0 0 1 * *" (monthly), "0 */6 * * *" (6h).
              Minimum interval: 1 hour.
        diff_mode: When True, only report articles not seen in previous run.
        notify: When True, emit MCP resource notification on new results.
        action: "set" (default) to create/update/remove schedule,
                "list" to list all active schedules,
                "status" to show specific pipeline schedule status.

    Returns:
        Schedule confirmation, list of schedules, or status details.
    """

action="set" ๅ›žๅ‚ณ๏ผš

โฐ Schedule set for "weekly_remimazolam":
  Cron: 0 9 * * 1 (Every Monday at 09:00 UTC)
  Next run: 2026-02-17 09:00 UTC
  Diff mode: enabled (only new articles)
  Notify: enabled

action="list" ๅ›žๅ‚ณ๏ผš

๐Ÿ“… Active Schedules (2 total):

| Pipeline               | Cron          | Next Run            | Last Run            | Status  |
|------------------------|---------------|---------------------|---------------------|---------|
| weekly_remimazolam     | 0 9 * * 1     | 2026-02-17 09:00    | 2026-02-10 09:00    | โœ… OK   |
| crispr_monthly         | 0 0 1 * *     | 2026-03-01 00:00    | 2026-02-01 00:00    | โœ… OK   |

๐Ÿ’ก Modify: schedule_pipeline(name="<name>", cron="<new>")
๐Ÿ’ก Remove: schedule_pipeline(name="<name>", cron="")

action="status" ๅ›žๅ‚ณ๏ผš

๐Ÿ“Š Schedule status for "weekly_remimazolam":
  Cron: 0 9 * * 1 (Every Monday at 09:00 UTC)
  Enabled: true
  Next run: 2026-02-17 09:00 UTC
  Last run: 2026-02-10 09:00 UTC
  Last status: โœ… Success (15 articles, 3 new)
  Total runs: 12
  Diff mode: enabled

็ด„ๆŸ๏ผš

  • name ๅฟ…้ ˆๆ˜ฏๅทฒๅญ˜ๅœจ็š„ saved pipeline
  • ๆœ€ๅฐ cron ้–“้š”๏ผš1 ๅฐๆ™‚
  • ๅŒๆ™‚ๆœ€ๅคš 5 ๅ€‹ๆดป่บๆŽ’็จ‹
  • ๆฏๆฌกๆŽ’็จ‹ๅŸท่กŒ่ถ…ๆ™‚๏ผš5 ๅˆ†้˜

11.3 ๅ…ฉ่ทฏ็”ฑๆจกๅž‹ๅฎŒๆ•ดๆต็จ‹

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                        Agent ไฝฟ็”จๆต็จ‹ๅœ–                                     โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                                              โ”‚
โ”‚  ไฝฟ็”จ่€…ๅ•้กŒ โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€ ็ฐกๅ–ฎๆŸฅ่ฉข โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”     โ”‚
โ”‚               โ”‚     "ๆœๅฐ‹ remimazolam"                                โ”‚     โ”‚
โ”‚               โ”‚                                                       โ–ผ     โ”‚
โ”‚               โ”‚                          โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”‚
โ”‚               โ”‚                          โ”‚ unified_search(query=...)    โ”‚   โ”‚
โ”‚               โ”‚                          โ”‚ โ†’ ๅณๆ™‚ๅ›žๅ‚ณ็ตๆžœ               โ”‚   โ”‚
โ”‚               โ”‚                          โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ”‚
โ”‚               โ”‚                                                              โ”‚
โ”‚               โ””โ”€โ”€โ”€โ”€ ่ค‡้›œ/้‡่ค‡้œ€ๆฑ‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”         โ”‚
โ”‚                     "ๆฏ้€ฑๆœๅฐ‹ remimazolam vs propofol"            โ”‚         โ”‚
โ”‚                                                                    โ–ผ         โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”‚
โ”‚  โ”‚ Step 1: ๅปบ็ซ‹ pipeline                                              โ”‚    โ”‚
โ”‚  โ”‚   save_pipeline(name="weekly_remi", config="template: pico\n...")  โ”‚    โ”‚
โ”‚  โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค    โ”‚
โ”‚  โ”‚ Step 2: ๆธฌ่ฉฆๅŸท่กŒ                                                   โ”‚    โ”‚
โ”‚  โ”‚   unified_search(pipeline="saved:weekly_remi")                     โ”‚    โ”‚
โ”‚  โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค    โ”‚
โ”‚  โ”‚ Step 3: ่จญๅฎšๆŽ’็จ‹๏ผˆๅฏ้ธ๏ผ‰                                            โ”‚    โ”‚
โ”‚  โ”‚   schedule_pipeline(name="weekly_remi", cron="0 9 * * 1")          โ”‚    โ”‚
โ”‚  โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค    โ”‚
โ”‚  โ”‚ Step 4: ไน‹ๅพŒไปปไฝ•ๆ™‚ๅ€™                                                โ”‚    โ”‚
โ”‚  โ”‚   unified_search(pipeline="saved:weekly_remi")  โ†’ ๆ‰‹ๅ‹•้‡่ท‘         โ”‚    โ”‚
โ”‚  โ”‚   load_pipeline(source="weekly_remi")           โ†’ ๆŸฅ็œ‹/็ทจ่ผฏ        โ”‚    โ”‚
โ”‚  โ”‚   list_pipelines()                              โ†’ ๅˆ—่ˆ‰ๆ‰€ๆœ‰         โ”‚    โ”‚
โ”‚  โ”‚   schedule_pipeline(name="*", action="list")    โ†’ ๆŸฅ็œ‹ๆŽ’็จ‹         โ”‚    โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ”‚
โ”‚                                                                              โ”‚
โ”‚  ้—œ้ตๅŽŸๅ‰‡๏ผš                                                                  โ”‚
โ”‚  โ€ข unified_search ๆ˜ฏๅ”ฏไธ€็š„ใ€Œๆœๅฐ‹ๅŸท่กŒใ€ๅ…ฅๅฃ                                    โ”‚
โ”‚  โ€ข Pipeline ๅทฅๅ…ทๅชๅš CRUD + ๆŽ’็จ‹็ฎก็†๏ผŒไธ็›ดๆŽฅๅŸท่กŒๆœๅฐ‹                           โ”‚
โ”‚  โ€ข ๆŽ’็จ‹่งธ็™ผๆ™‚็”ฑ PipelineScheduler ๅ…ง้ƒจๅ‘ผๅซ PipelineExecutor                   โ”‚
โ”‚                                                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

11.4 DDD ๅฏฆไฝœๆช”ๆกˆๅฐ็…ง

ๅฑค ๆ–ฐๅขž/ไฟฎๆ”น ๆช”ๆกˆ่ทฏๅพ‘ ่ชชๆ˜Ž
Domain ๆ–ฐๅขž domain/entities/pipeline.py ๆ–ฐๅขž PipelineMeta, PipelineRun, ScheduleEntry
Application ๆ–ฐๅขž application/pipeline/store.py PipelineStore CRUD + ้›™ๅฑค scope ่งฃๆž
Application ๆ–ฐๅขž application/pipeline/scheduler.py PipelineScheduler tick loop
Application ไฟฎๆ”น application/pipeline/executor.py ๆ–ฐๅขž run_and_store() ๆ–นๆณ•
Presentation ๆ–ฐๅขž presentation/mcp_server/tools/pipeline_tools.py 6 ๅ€‹ MCP ๅทฅๅ…ท
Presentation ไฟฎๆ”น presentation/mcp_server/tools/__init__.py ่จปๅ†Š pipeline tools
Presentation ไฟฎๆ”น presentation/mcp_server/tool_registry.py ๆ–ฐๅขž pipeline category
Presentation ไฟฎๆ”น presentation/mcp_server/resources.py ๆ–ฐๅขž Resource templates
Presentation ไฟฎๆ”น presentation/mcp_server/server.py Lifespan ๆ•ดๅˆ scheduler
Infrastructure ไฟฎๆ”น container.py DI ่จปๅ†Š PipelineStore + Scheduler
Tests ๆ–ฐๅขž tests/test_pipeline_store.py Store CRUD + scope ๆธฌ่ฉฆ
Tests ๆ–ฐๅขž tests/test_pipeline_scheduler.py Scheduler ๆธฌ่ฉฆ
Tests ๆ–ฐๅขž tests/test_pipeline_tools.py MCP ๅทฅๅ…ทๆ•ดๅˆๆธฌ่ฉฆ
Tests ๆ–ฐๅขž tests/test_pipeline_history.py ๅŸท่กŒๆญทๅฒ + diff ๆธฌ่ฉฆ

11.5 ๆ›พ่€ƒๆ…ฎไฝ†ๆœชๆŽก็”จ็š„ๆ–นๆกˆ

Option A: 2 ๅ€‹ๅˆไฝตๅทฅๅ…ท

pipeline_manage(action="save|list|load|delete", name=..., config=...)
pipeline_schedule(action="set|remove|list|status", name=..., cron=...)

ๆœชๆŽก็”จๅŽŸๅ› ๏ผšaction ๅƒๆ•ธ่ฎ“ Agent ้œ€่ฆ็†่งฃๅญๅ‘ฝไปค่ชž็พฉ๏ผŒๅขžๅŠ ่ช็Ÿฅ่ฒ ๆ“”ใ€‚ไธ็ฌฆๅˆ MCP ๅทฅๅ…ทใ€Œไธ€ๅ€‹ๅทฅๅ…ท = ไธ€ๅ€‹ๅ‹•ไฝœใ€็š„่จญ่จˆๅ“ฒๅญธใ€‚

Option C: ๅ…จๅˆไฝต้€ฒ unified_search

unified_search(pipeline="save:my_plan")
unified_search(pipeline="list")
unified_search(pipeline="delete:my_plan")

ๆœชๆŽก็”จๅŽŸๅ› ๏ผšunified_search ็š„่ชž็พฉๆ˜ฏใ€ŒๅŸท่กŒๆœๅฐ‹ใ€๏ผŒCRUD ๆ“ไฝœ่ชž็พฉไธ็ฌฆใ€‚pipeline ๅƒๆ•ธ้Ž่ผ‰ๅฐŽ่‡ด็”จ้€”ๆจก็ณŠใ€‚


้™„้Œ„ A๏ผš็พๆœ‰ Pipeline ็ณป็ตฑไบ’ๅ‹•็ฏ„ไพ‹

# ็พ่กŒๆ–นๅผ๏ผšAgent ๅœจ unified_search ไธญ inline ๅ‚ณๅ…ฅ
# unified_search(pipeline="...")

template: comprehensive
template_params:
  query: "remimazolam sedation"
  sources: "pubmed,openalex"
  limit: 20
# ๆ่ญฐ็š„ๆŒไน…ๅŒ–ๆ–นๅผ
# Step 1: save_pipeline(name="weekly_remi", config="...")
# Step 2: unified_search(pipeline="saved:weekly_remi")
# Step 3: schedule_pipeline(name="weekly_remi", cron="0 9 * * 1")

้™„้Œ„ B๏ผšMCP Resource Template ็ฏ„ไพ‹

# FastMCP resource template ่ชžๆณ•
@mcp.resource("pipeline://saved/{name}")
async def read_saved_pipeline(name: str) -> str:
    """MCP clients can read saved pipelines via resources/read."""
    store: PipelineStore = ctx.request_context.lifespan_context["pipeline_store"]
    config = store.load(name)
    return yaml.dump(config.to_dict(), allow_unicode=True)

้™„้Œ„ C๏ผšCron ่กจ้”ๅผๅƒ่€ƒ

่กจ้”ๅผ ๅซ็พฉ
0 9 * * 1 ๆฏ้€ฑไธ€ 09:00
0 0 1 * * ๆฏๆœˆ 1 ๆ—ฅ 00:00
0 */6 * * * ๆฏ 6 ๅฐๆ™‚
30 8 * * 1-5 ้€ฑไธ€่‡ณไบ” 08:30
0 0 * * 0 ๆฏ้€ฑๆ—ฅ 00:00