diff --git a/.gitignore b/.gitignore
index 7d4250b8..5d350aa2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -92,3 +92,10 @@ fordiff/
# Local SQL workspace
sql/
+
+sandbox/server/backends/resources/mcp/mock_runtime/.env
+sandbox/server/backends/resources/mcp/mock_runtime/certs/
+sandbox/server/backends/resources/mcp/mock_runtime/logs/
+sandbox/server/backends/resources/mcp/mock_runtime/run/
+
+docs/superpowers
\ No newline at end of file
diff --git a/configs/sandbox-server/coding_config.json b/configs/sandbox-server/coding_config.json
new file mode 100644
index 00000000..8a146405
--- /dev/null
+++ b/configs/sandbox-server/coding_config.json
@@ -0,0 +1,17 @@
+{
+ "server": {
+ "url": "http://127.0.0.1:18890",
+ "port": 18890,
+ "session_ttl": 900
+ },
+ "resources": {
+ "code": {
+ "enabled": true,
+ "description": "Local coding backend for symbolic checks, Lean/Coq validation, bib processing, and plotting",
+ "backend_class": "sandbox.server.backends.resources.code.CodeBackend",
+ "config": {
+ "workspace_root": "${CODE_WORKSPACE_ROOT}"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/configs/sandbox-server/mcp_config.json b/configs/sandbox-server/mcp_config.json
index 18baf9c8..76cd9b78 100644
--- a/configs/sandbox-server/mcp_config.json
+++ b/configs/sandbox-server/mcp_config.json
@@ -10,14 +10,12 @@
"description": "Toolathlon-GYM MCP backend",
"backend_class": "sandbox.server.backends.resources.mcp.toolathlon_gym.ToolathlonGymBackend",
"config": {
- "enabled_mcp_servers": ["filesystem", "terminal", "snowflake"],
+ "enabled_mcp_servers": ["excel", "filesystem", "memory", "pdf-tools", "playwright_with_chunk", "pptx", "terminal", "word", "canvas", "notion", "woocommerce"],
"workspace_root": "${TOOLATHLON_WORKSPACE_ROOT:-/tmp/agentflow_mcp}",
"env_overrides": {
- "PGHOST": "${PGHOST:-toolathlon_pg}",
- "PGPORT": "${PGPORT:-5432}",
- "PGUSER": "${PGUSER:-eigent}",
- "PGPASSWORD": "${PGPASSWORD:-camel}",
- "PGDATABASE": "${PGDATABASE:-toolathlon_gym}"
+ "CANVAS_DOMAIN": "${AGENTFLOW_MCP_CANVAS_ENDPOINT:-127.0.0.1:38080}",
+ "BASE_URL": "${AGENTFLOW_MCP_NOTION_ENDPOINT:-http://127.0.0.1:38081}",
+ "WORDPRESS_SITE_URL": "${AGENTFLOW_MCP_WOOCOMMERCE_ENDPOINT:-http://127.0.0.1:38082}"
}
}
}
diff --git a/configs/synthesis/coding.json b/configs/synthesis/coding.json
new file mode 100644
index 00000000..663790c9
--- /dev/null
+++ b/configs/synthesis/coding.json
@@ -0,0 +1,45 @@
+{
+ "model_name": "deepseek/deepseek-v4-flash",
+ "api_key": "${OPENROUTER_API_KEY}",
+ "base_url": "https://openrouter.ai/api/v1",
+ "max_depth": 12,
+ "branching_factor": 2,
+ "depth_threshold": 2,
+ "min_depth": 4,
+ "max_selected_traj": 3,
+ "path_similarity_threshold": 0.72,
+ "number_of_seed": null,
+ "sandbox_server_url": "http://127.0.0.1:18890",
+ "sandbox_auto_start": true,
+ "sandbox_config_path": "configs/sandbox-server/coding_config.json",
+ "sandbox_timeout": 300,
+ "available_tools": [
+ "code-*"
+ ],
+ "sampling_tips": [
+ "You are exploring a local code repository, not a knowledge base.",
+ "Use only code tools to inspect files, search symbols, and run lightweight shell commands inside the workspace.",
+ "Prioritize repository structure, entrypoints, dependency files, configuration files, scripts, and tests.",
+ "Ground every conclusion in concrete evidence from files or command output.",
+ "Do not rely on outside knowledge or invent project behavior that is not supported by the repository."
+ ],
+ "synthesis_tips": [
+ "We are training an assistant for repository-level coding tasks.",
+ "Generate realistic engineering questions that can be answered strictly from the explored repository.",
+ "Prefer questions about entrypoints, commands, configs, dependencies, file locations, and module relationships.",
+ "Answers should be short, factual, and grounded in trajectory evidence.",
+ "Avoid generic software trivia and avoid questions that require external documentation."
+ ],
+ "seeds_file": "seeds/coding/coding.jsonl",
+ "output_dir": "results/coding",
+ "resource_types": [
+ "code"
+ ],
+ "resource_init_configs": {
+ "code": {
+ "content": {
+ "source_dir": "${SOURCE_DIR}"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/configs/synthesis/mcp.json b/configs/synthesis/mcp.json
new file mode 100644
index 00000000..f0f7517a
--- /dev/null
+++ b/configs/synthesis/mcp.json
@@ -0,0 +1,44 @@
+{
+ "model_name": "deepseek/deepseek-v4-flash",
+ "api_key": "${OPENROUTER_API_KEY}",
+ "base_url": "https://openrouter.ai/api/v1",
+ "max_depth": 12,
+ "branching_factor": 2,
+ "depth_threshold": 2,
+ "min_depth": 4,
+ "max_selected_traj": 3,
+ "path_similarity_threshold": 0.72,
+ "number_of_seed": null,
+ "sandbox_server_url": "http://127.0.0.1:18890",
+ "sandbox_auto_start": true,
+ "sandbox_config_path": "configs/sandbox-server/mcp_config.json",
+ "sandbox_timeout": 300,
+ "available_tools": [
+ "mcp:canvas.*",
+ "mcp:filesystem.*",
+ "mcp:memory.*",
+ "mcp:pdf-tools.*",
+ "mcp:playwright_with_chunk.*",
+ "mcp:pptx.*",
+ "mcp:terminal.*",
+ "mcp:word.*",
+ "mcp:excel.*"
+ ],
+ "sampling_tips": [
+ "Canvas communication for large courses is scattered across inbox conversations, announcements, discussion topics, course context, grades, submissions, and due-date signals. Teachers often need to answer repeated student questions, identify which messages require attention, and prepare targeted follow-up without manually scanning every course artifact or gradebook row.",
+ "They expect a context-aware communication assistant that summarizes Canvas inbox conversations and course discussion topics, drafts replies to common student questions using relevant course context, identifies students or groups who may need follow-up based on grades, late submissions, missing grading status, or upcoming deadlines, and prepares instructor-approved Canvas messages or announcements for targeted communication.",
+ "You have access to Canvas, a Learning Management System, which records 22 courses, 28,865 users, 32,663 enrollments, 206 assignments, 173,912 submissions and 77 quizzes. You can also use other tools provided.",
+ "We need mountains of training data that is diverse and challenging to train the agents help teachers meet their expectations."
+ ],
+ "synthesis_tips": [
+ "Canvas communication for large courses is scattered across inbox conversations, announcements, discussion topics, course context, grades, submissions, and due-date signals. Teachers often need to answer repeated student questions, identify which messages require attention, and prepare targeted follow-up without manually scanning every course artifact or gradebook row.",
+ "They expect a context-aware communication assistant that summarizes Canvas inbox conversations and course discussion topics, drafts replies to common student questions using relevant course context, identifies students or groups who may need follow-up based on grades, late submissions, missing grading status, or upcoming deadlines, and prepares instructor-approved Canvas messages or announcements for targeted communication.",
+ "Please adopt the perspective of a teacher or a professor, and think about what kinds of questions you would ask a agent in the specific scenario, as well as what kind of answers would truly meet your needs.",
+ "We need mountains of training data that is diverse and challenging to train the agents help teachers meet their expectations."
+ ],
+ "seeds_file": "seeds/mcp/seeds.jsonl",
+ "output_dir": "results/communication",
+ "resource_types": [
+ "mcp"
+ ]
+}
\ No newline at end of file
diff --git a/examples/CodingAgent.md b/examples/CodingAgent.md
new file mode 100644
index 00000000..0e50aede
--- /dev/null
+++ b/examples/CodingAgent.md
@@ -0,0 +1,466 @@
+# 💻 CodingAgent: Local Repository Coding Backend — Data Synthesis & Debugging Guide
+
+This guide explains how to use AgentFlow's **Coding Backend** for **repository-grounded data synthesis** and **backend debugging**.
+
+Note: this guide covers **sandbox startup, synthesis runs, and debugging only**. It does **not** cover model training, deployment, inference, or evaluation.
+
+## 📋 Table of Contents
+
+- [Overview](#overview)
+- [Prerequisites](#prerequisites)
+- [Pipeline Overview](#pipeline-overview)
+- [Step 1: Start the Sandbox Server](#step-1-start-the-sandbox-server)
+- [Step 2: Run QA Synthesis](#step-2-run-qa-synthesis)
+- [Step 3: Inspect Outputs](#step-3-inspect-outputs)
+- [Configuration Reference](#configuration-reference)
+- [FAQ / Debugging](#faq--debugging)
+
+---
+
+## Overview
+
+CodingAgent is a **local repository coding agent**. For each seed, AgentFlow creates a coding workspace by copying a local repository into a sandbox directory, then lets the agent inspect or modify that workspace only through `code-*` tools.
+
+The Coding Backend in this repo exposes 6 core tools:
+
+| Tool | Description | Parameters |
+|------|-------------|------------|
+| `code-read` | Read a text file with line numbers | `file_path`, `offset` (optional), `limit` (optional) |
+| `code-glob` | Find files by glob pattern | `pattern`, `path` (optional) |
+| `code-grep` | Search file contents with a regex | `pattern`, `path` (optional), `glob` (optional) |
+| `code-bash` | Run a shell command inside the current workspace | `command` |
+| `code-edit` | Replace an exact string in an existing file | `file_path`, `old_string`, `new_string`, `replace_all` (optional) |
+| `code-write` | Write a full file, creating parent directories if needed | `file_path`, `content` |
+
+Typical use cases:
+
+- Synthesize QA pairs grounded in a real repository
+- Test whether `code-*` tools work correctly on a copied workspace
+- Debug session initialization, workspace copying, and tool execution behavior
+
+---
+
+## Prerequisites
+
+### 1) Install AgentFlow
+
+```bash
+git clone https://github.com/OpenDCAI/AgentFlow
+cd AgentFlow
+pip install -e .
+```
+
+### 2) Configure LLM credentials
+
+The default coding synthesis config uses an OpenAI-compatible endpoint.
+
+```bash
+export OPENROUTER_API_KEY="YOUR_KEY"
+```
+
+If you keep the default config values, the synthesis config will use:
+
+- `model_name`: `deepseek/deepseek-v4-flash`
+- `base_url`: `https://openrouter.ai/api/v1`
+
+### 3) Configure workspace-related environment variables
+
+The two most important paths are:
+
+- `SOURCE_DIR`: the repository that will be copied into each coding workspace
+- `CODE_WORKSPACE_ROOT`: the parent directory where sandbox workspaces are created
+
+Example:
+
+```bash
+export SOURCE_DIR="/path/to/your/repository"
+export CODE_WORKSPACE_ROOT="/tmp/agentflow_code"
+```
+
+### 4) Prepare a seed file
+
+Coding synthesis still requires a JSONL seed file. Each line must contain at least `content` and `kwargs`.
+
+Example:
+
+```jsonl
+{"content":"Inspect the repository and identify the main entrypoint used to run the project.", "kwargs": {}}
+{"content":"Find how this repository installs dependencies and how its test suite is executed.", "kwargs": {}}
+```
+
+Default seed file:
+
+- `seeds/coding/coding.jsonl`
+
+Important: for the Coding Backend, the seed describes **what to explore**, but the actual repository source comes from `resource_init_configs.code.content.source_dir` in the synthesis config, not from the seed itself.
+
+In the current pipeline, seeds are processed **sequentially**.
+
+### 5) Relevant config files
+
+- Sandbox config: `configs/sandbox-server/coding_config.json`
+- Synthesis config: `configs/synthesis/coding.json`
+
+---
+
+## Pipeline Overview
+
+The Coding Backend flow verified in this repo is:
+
+```text
+Sandbox Server -> Code Session Initialization -> Workspace Copy -> Trajectory Sampling -> QA + Trajectory Output
+```
+
+For each seed, the synthesis pipeline does the following:
+
+1. Start or connect to the sandbox server
+2. Create or reinitialize the `code` session
+3. Copy `source_dir` into a workspace under `CODE_WORKSPACE_ROOT`
+4. Let the agent explore that workspace using `code-*` tools
+5. Save synthesized QA pairs and selected trajectories
+6. On later seeds, reinitialize the `code` session for isolation; workspace removal happens later when that session is destroyed during cleanup
+
+Important behavior:
+
+- The workspace is recreated from `source_dir` when the `code` session is initialized or reinitialized
+- The workspace is deleted when the corresponding `code` session is destroyed during cleanup
+- Because the workspace is mutable state, **set `branching_factor=1` for Coding Backend runs**
+
+---
+
+## Step 1: Start the Sandbox Server
+
+The sandbox server provides the execution environment for `code-read`, `code-glob`, `code-grep`, `code-bash`, `code-edit`, and `code-write`.
+
+**Command:**
+
+```bash
+./start_sandbox_server.sh --config configs/sandbox-server/coding_config.json
+```
+
+> Note: `--host` and `--port` flags are ignored by `start_sandbox_server.sh`; use `server.url` and `server.port` in the config file instead.
+
+**Config file** `configs/sandbox-server/coding_config.json`:
+
+```json
+{
+ "server": {
+ "url": "http://127.0.0.1:18890",
+ "port": 18890,
+ "session_ttl": 900
+ },
+ "resources": {
+ "code": {
+ "enabled": true,
+ "description": "Local coding backend for symbolic checks, Lean/Coq validation, bib processing, and plotting",
+ "backend_class": "sandbox.server.backends.resources.code.CodeBackend",
+ "config": {
+ "workspace_root": "${CODE_WORKSPACE_ROOT}"
+ }
+ }
+ }
+}
+```
+
+**Verification:**
+
+```bash
+curl http://127.0.0.1:18890/health
+```
+
+Expected result:
+
+```json
+{"status":"healthy"}
+```
+
+### Does Coding Backend need warmup?
+
+Usually, **no special warmup is needed**.
+
+Unlike heavy backends such as RAG or VM, the Coding Backend does not load a model or a global resource pool in `warmup()`. The important step is **session initialization**, where the backend creates the workspace and copies `source_dir` into it.
+
+So for Coding Backend debugging, focus on:
+
+- whether the server starts correctly
+- whether the `code` session is created
+- whether `source_dir` is copied into the workspace
+- whether `code-*` tools can execute against that workspace
+
+---
+
+## Step 2: Run QA Synthesis
+
+### Recommended config for pure Coding Backend
+
+In `configs/synthesis/coding.json`, keep the run strictly code-only:
+
+```json
+{
+ "available_tools": ["code-*"],
+ "resource_types": ["code"],
+ "sandbox_config_path": "configs/sandbox-server/coding_config.json"
+}
+```
+
+Also set:
+
+```json
+{
+ "branching_factor": 1
+}
+```
+
+These are strong recommendations for Coding Backend debugging, not just tuning suggestions.
+
+Reasons:
+
+- `branching_factor > 1`: sibling branches in the current sampler are explored concurrently, while a single seed uses one `code` session and one mutable workspace. Different branches may read or write the same workspace and contaminate each other.
+
+### Run from CLI
+
+```bash
+python3 synthesis/pipeline.py \
+ --config configs/synthesis/coding.json \
+ --seeds seeds/coding/coding.jsonl \
+ --output-dir results/coding
+```
+
+If `seeds_file` and `output_dir` are already set in the config, the shorter form also works:
+
+```bash
+python3 synthesis/pipeline.py --config configs/synthesis/coding.json
+```
+
+### What you should see in logs
+
+For a healthy pure coding run, logs usually include signals like:
+
+- sandbox server started or connected successfully
+- `Warming up backends: ['code']` or no meaningful warmup work
+- `Session created: code -> ...`
+- `Available tools: ['code-read', 'code-glob', 'code-grep', 'code-bash', 'code-edit', 'code-write']`
+- tool execution logs for `code-*`
+
+If you see tools such as `rag-search`, `web-search`, or `text2sql-execute`, your synthesis config is not pure coding.
+
+---
+
+## Step 3: Inspect Outputs
+
+By default, synthesis writes two JSONL files into the output directory:
+
+- `synthesized_qa.jsonl`
+- `trajectories.jsonl`
+
+Example:
+
+```bash
+ls results/coding
+```
+
+Expected files:
+
+```text
+synthesized_qa.jsonl
+trajectories.jsonl
+```
+
+Use these files for two different debugging purposes:
+
+- `synthesized_qa.jsonl`: check whether the final QA pairs are repository-grounded and realistic
+- `trajectories.jsonl`: check the actual tool-use path, including which files were read, which commands were run, and whether code-edit/code-write changed the workspace
+
+If you are debugging the backend itself, `trajectories.jsonl` is usually the more important file.
+
+---
+
+## Configuration Reference
+
+### Coding synthesis config
+
+File: `configs/synthesis/coding.json`
+
+Key fields:
+
+| Field | Description |
+|-------|-------------|
+| `model_name` | LLM used for trajectory sampling and QA synthesis |
+| `api_key` | OpenAI-compatible API key |
+| `base_url` | OpenAI-compatible API base URL |
+| `available_tools` | For pure coding, use `["code-*"]` |
+| `resource_types` | For pure coding, use `["code"]` |
+| `sandbox_server_url` | Sandbox server address |
+| `sandbox_auto_start` | Whether the synthesis worker auto-starts the sandbox server |
+| `sandbox_config_path` | Sandbox server config path used when auto-starting |
+| `branching_factor` | Strongly recommend `1` for Coding Backend |
+| `resource_init_configs.code.content.source_dir` | Initial repository directory copied into each workspace |
+| `seeds_file` | Seed JSONL path |
+| `output_dir` | Output directory for QA and trajectories |
+
+Recommended minimal shape:
+
+```json
+{
+ "available_tools": ["code-*"],
+ "resource_types": ["code"],
+ "branching_factor": 1,
+ "sandbox_server_url": "http://127.0.0.1:18890",
+ "sandbox_auto_start": true,
+ "sandbox_config_path": "configs/sandbox-server/coding_config.json",
+ "resource_init_configs": {
+ "code": {
+ "content": {
+ "source_dir": "${SOURCE_DIR}"
+ }
+ }
+ }
+}
+```
+
+### Coding sandbox config
+
+File: `configs/sandbox-server/coding_config.json`
+
+Key fields:
+
+| Field | Description |
+|-------|-------------|
+| `server.url` | Sandbox listen address |
+| `server.port` | Sandbox port |
+| `server.session_ttl` | Session TTL in seconds |
+| `resources.code.backend_class` | Backend implementation class |
+| `resources.code.config.workspace_root` | Parent directory where workspaces are created |
+
+Recommended minimal shape:
+
+```json
+{
+ "server": {
+ "url": "http://127.0.0.1:18890",
+ "port": 18890,
+ "session_ttl": 900
+ },
+ "resources": {
+ "code": {
+ "enabled": true,
+ "backend_class": "sandbox.server.backends.resources.code.CodeBackend",
+ "config": {
+ "workspace_root": "${CODE_WORKSPACE_ROOT}"
+ }
+ }
+ }
+}
+```
+
+---
+
+## FAQ / Debugging
+
+### 1) Does each run create a fresh workspace?
+
+Yes, for each seed the pipeline creates or reinitializes the `code` session, and the Coding Backend copies `source_dir` into the workspace again.
+
+That means:
+
+- if you rerun the pipeline, the workspace starts from `source_dir` again
+- if you process multiple seeds, later seeds do not continue from earlier seed edits
+
+### 2) Will the workspace be deleted after the run?
+
+Not always immediately.
+
+The Coding Backend deletes the workspace when `code` session cleanup runs. In the current pipeline path, intermediate seed workspaces are removed during `reinitialize()`, because the old `code` session is destroyed before the next one is created. However, the final workspace is not guaranteed to be deleted the moment the worker stops, because this pipeline currently closes the sandbox connection without always explicitly destroying the last per-seed session first.
+
+So if you are checking final on-disk edits, remember:
+
+- the workspace exists during the run
+- earlier seed workspaces are typically removed when the next seed reinitializes `code` (that is, every seed use the same, brand-new workspace)
+- the final workspace may remain until the session is explicitly destroyed or cleaned by session TTL expiry
+
+### 3) Why is `branching_factor=1` strongly recommended?
+
+Because the current Coding Backend uses one mutable workspace per worker/session for a given seed, while the sampler can explore sibling branches concurrently.
+
+With `branching_factor > 1`, one branch may:
+
+- edit files that another branch later reads
+- overwrite files written by another branch
+- make trajectories depend on sibling side effects
+
+That makes trajectory data hard to interpret and unsafe for backend debugging. For Coding Backend runs, set:
+
+```json
+{
+ "branching_factor": 1
+}
+```
+
+### 4) How do I confirm the run is pure Coding Backend?
+
+Check three places:
+
+1. `configs/synthesis/coding.json`
+ - `available_tools` should be `["code-*"]`
+ - `resource_types` should be `["code"]`
+2. runtime logs
+ - available tools should all be `code-*`
+3. sandbox config
+ - `configs/sandbox-server/coding_config.json` should only enable the `code` resource
+
+### 5) How are multiple seeds handled?
+
+Seeds are processed sequentially in the current pipeline, not in parallel.
+
+For Coding Backend runs, that means:
+
+- each new seed reinitializes the `code` session
+- each new seed recreates the workspace from `source_dir`
+- later seeds do not continue from earlier seed edits
+
+### 6) What if `source_dir` is wrong?
+
+If `source_dir` does not exist or is not a directory, `code` session initialization will fail.
+
+Typical checks:
+
+```bash
+echo "$SOURCE_DIR"
+ls "$SOURCE_DIR"
+```
+
+### 7) What if `code-glob` or `code-read` returns nothing useful?
+
+First verify that the workspace was actually copied from the expected repository.
+
+Check:
+
+- `SOURCE_DIR` points to the intended repository
+- `resource_init_configs.code.content.source_dir` expands correctly
+- `CODE_WORKSPACE_ROOT` is writable
+- the run logs show successful `code` session creation
+
+### 8) What if `code-bash` fails?
+
+`code-bash` runs inside the copied workspace. If it fails, the most common causes are:
+
+- the command itself exits non-zero
+- the expected toolchain is not installed in the runtime environment
+- the command assumes a different working directory layout than the copied repository
+
+For debugging, start with lightweight commands such as:
+
+```bash
+pwd
+ls
+find . -maxdepth 2 -type f | head
+```
+
+### 9) What is the difference between seeds and `source_dir`?
+
+They serve different purposes:
+
+- `seed.content`: tells the agent what to investigate
+- `source_dir`: tells the backend which repository to copy into the workspace
+
+Changing seeds changes the exploration task. Changing `source_dir` changes the repository being explored.
diff --git a/examples/MCPAgent.md b/examples/MCPAgent.md
new file mode 100644
index 00000000..06381139
--- /dev/null
+++ b/examples/MCPAgent.md
@@ -0,0 +1,314 @@
+# MCPAgent: MCP Data Synthesis Guide (Canvas Example)
+
+This guide explains how to use AgentFlow's MCP backend to synthesize QA and trajectory data from the MCP server snapshots integrated into this repo. It focuses on the data synthesis flow and uses Canvas communication as a concrete example.
+
+AgentFlow itself only talks to the MCP endpoints configured in `configs/sandbox-server/mcp_config.json`. Those endpoints can point either to real services or to an optional local mock runtime.
+
+Note: this guide covers data synthesis only. It does not cover model training or deployment.
+
+## Table of Contents
+
+- [Overview](#overview)
+- [Prerequisites](#prerequisites)
+- [Pipeline Overview](#pipeline-overview)
+- [Step 1: Prepare MCP Endpoints](#step-1-prepare-mcp-endpoints)
+- [Step 2: Start the Sandbox Server](#step-2-start-the-sandbox-server)
+- [Step 3: Synthesize QA Data](#step-3-synthesize-qa-data)
+- [Configuration Reference](#configuration-reference)
+- [FAQ](#faq)
+
+---
+
+## Overview
+
+AgentFlow's MCP backend wraps a set of MCP servers behind the sandbox server. The synthesis pipeline then samples tool-use trajectories and turns them into QA data.
+
+For the Canvas communication example in this repo, the relevant synthesis config is:
+
+- `configs/synthesis/mcp.json`
+
+The sandbox entrypoint is:
+
+- `configs/sandbox-server/mcp_config.json`
+
+At a high level, the flow is:
+
+```
+MCP endpoints -> Sandbox server (MCP backend) -> Synthesis pipeline -> QA + trajectories
+```
+
+The important boundary is that AgentFlow only sees endpoint values. It does not need to know whether those endpoints are backed by real APIs or by a local mock runtime.
+
+---
+
+## Prerequisites
+
+### 1) Install AgentFlow
+
+```bash
+git clone https://github.com/OpenDCAI/AgentFlow
+cd AgentFlow
+pip install -e .
+```
+
+### 2) Configure LLM credentials
+
+The example Canvas synthesis config reads its API key from the environment:
+
+```bash
+export OPENROUTER_API_KEY='YOUR_KEY'
+```
+
+If you change the model or provider, update `model_name`, `api_key`, and `base_url` in `configs/synthesis/mcp.json` accordingly.
+
+### 3) Prepare seed data
+
+The Canvas example uses a JSONL seed file. By default, the path comes from the `seeds_file` field in `configs/synthesis/mcp.json`. You can keep that default or override it with `--seeds`.
+
+This file uses JSONL format, one JSON object per line. Example:
+
+```jsonl
+{"content":"Communication triage: instructors with large Canvas courses want an agent to inspect inbox conversations, course context, announcements, discussion topics, and gradebook signals to decide which student communications need attention.","kwargs":{}}
+```
+
+You can reuse the provided file or replace it with your own JSONL file and pass it through `--seeds`.
+
+### 4) MCP server snapshots are already wired in
+
+For the MCP integration in this repo, the copied MCP server snapshots and the main mock database snapshot from [toolathlon_gym](https://github.com/eigent-ai/toolathlon_gym) have already been wired in. You do not need to manually copy vendor resources or import database snapshots before running the example below.
+
+If you choose the optional mock runtime, its shim scripts still expect `TOOLATHLON_GYM_ROOT` to point at the exported MCP server snapshot root directory.
+
+### 5) Choose endpoint source: real services or optional mock runtime
+
+`configs/sandbox-server/mcp_config.json` reads these AgentFlow-side endpoint variables:
+
+- `AGENTFLOW_MCP_CANVAS_ENDPOINT`
+- `AGENTFLOW_MCP_NOTION_ENDPOINT`
+- `AGENTFLOW_MCP_WOOCOMMERCE_ENDPOINT`
+
+These are just endpoint values. They can point to real services, or to the optional local mock runtime described below.
+
+#### Option A: Point AgentFlow at real services
+
+Export whichever endpoints you want AgentFlow to use:
+
+```bash
+export AGENTFLOW_MCP_CANVAS_ENDPOINT='canvas.example.edu:443'
+export AGENTFLOW_MCP_NOTION_ENDPOINT='https://notion.example.internal'
+export AGENTFLOW_MCP_WOOCOMMERCE_ENDPOINT='https://shop.example.internal'
+```
+
+#### Option B: Start the optional local mock runtime
+
+The mock runtime is extra infrastructure. AgentFlow does not depend on it unless you choose to point the MCP endpoints at it.
+
+The mock runtime in this repo uses the copied mock database setup and local HTTP shims from [toolathlon_gym](https://github.com/eigent-ai/toolathlon_gym) for:
+
+- `canvas`
+- `notion`
+- `woocommerce`
+
+It also requires local `docker compose`, `node`, and the exported MCP server snapshot root referenced by `TOOLATHLON_GYM_ROOT`.
+
+1. Copy the mock runtime env file:
+
+```bash
+cp sandbox/server/backends/resources/mcp/mock_runtime/.env.example \
+ sandbox/server/backends/resources/mcp/mock_runtime/.env
+```
+
+2. Generate a local Canvas TLS certificate and key:
+
+```bash
+mkdir -p sandbox/server/backends/resources/mcp/mock_runtime/certs
+
+openssl req -x509 -newkey rsa:2048 -nodes \
+ -keyout sandbox/server/backends/resources/mcp/mock_runtime/certs/canvas-key.pem \
+ -out sandbox/server/backends/resources/mcp/mock_runtime/certs/canvas-cert.pem \
+ -subj "/CN=127.0.0.1" \
+ -days 365 \
+ -addext "subjectAltName=IP:127.0.0.1,DNS:localhost"
+```
+
+3. Export the MCP server snapshot root used by the runtime scripts:
+
+```bash
+export TOOLATHLON_GYM_ROOT=/path/to/exported_mcp_snapshot
+```
+
+4. If you keep the default local ports from `.env.example`, the MCP endpoint variables are optional because `mcp_config.json` already has matching fallbacks. Exporting them explicitly is still a good idea:
+
+```bash
+export AGENTFLOW_MCP_CANVAS_ENDPOINT='127.0.0.1:38080'
+export AGENTFLOW_MCP_NOTION_ENDPOINT='http://127.0.0.1:38081'
+export AGENTFLOW_MCP_WOOCOMMERCE_ENDPOINT='http://127.0.0.1:38082'
+```
+
+5. Start the mock runtime:
+
+```bash
+bash sandbox/server/backends/resources/mcp/mock_runtime/scripts/start_mock_runtime.sh
+```
+
+6. Check runtime health:
+
+```bash
+bash sandbox/server/backends/resources/mcp/mock_runtime/scripts/status_mock_runtime.sh
+```
+
+For more detail on the mock runtime, see `sandbox/server/backends/resources/mcp/mock_runtime/README.md`.
+
+---
+
+## Pipeline Overview
+
+The MCP synthesis pipeline in this repo is:
+
+```
+Endpoint preparation
+ -> Sandbox server with MCP backend
+ -> Trajectory tree sampling
+ -> Trajectory selection
+ -> QA synthesis
+ -> synthesized_qa.jsonl + trajectories.jsonl
+```
+
+For the Canvas example, the synthesis config already points at the MCP sandbox config:
+
+- `sandbox_config_path = configs/sandbox-server/mcp_config.json`
+
+It also enables sandbox auto-start:
+
+- `sandbox_auto_start = true`
+
+That means you can either start the sandbox server yourself first, or let the synthesis pipeline start it when needed.
+
+---
+
+## Step 1: Prepare MCP Endpoints
+
+Before starting the sandbox, make sure the MCP endpoints referenced by `configs/sandbox-server/mcp_config.json` resolve to something reachable.
+
+You have two supported choices:
+
+1. Real service endpoints
+2. The optional local mock runtime
+
+For the Canvas example, either choice is valid. AgentFlow only sees endpoint values and sends requests to them through the MCP backend.
+
+---
+
+## Step 2: Start the Sandbox Server
+
+Start the sandbox server with the MCP backend config:
+
+```bash
+./start_sandbox_server.sh --config configs/sandbox-server/mcp_config.json
+```
+
+Optional health check:
+
+```bash
+curl http://127.0.0.1:18890/health
+```
+
+Expected result:
+
+```json
+{"status":"healthy"}
+```
+
+If you prefer, you can skip this manual step and rely on `sandbox_auto_start=true` in the synthesis config.
+
+---
+
+## Step 3: Synthesize QA Data
+
+Use the MCP synthesis config as the example entrypoint:
+
+- `configs/synthesis/mcp.json`
+
+Run:
+
+```bash
+python -m synthesis.pipeline \
+ --config configs/synthesis/mcp.json \
+ --seeds /path/to/canvas_communication.jsonl \
+ --output-dir results/canvas_communication
+```
+
+This produces:
+
+- `results/canvas_communication/synthesized_qa.jsonl`
+- `results/canvas_communication/trajectories.jsonl`
+
+If you want to use the default seed file from `configs/synthesis/mcp.json`, you can omit `--seeds`. If you want a smaller smoke run, create a short JSONL file with one or a few seeds and pass that path through `--seeds`.
+
+---
+
+## Configuration Reference
+
+### MCP sandbox config
+
+File:
+
+- `configs/sandbox-server/mcp_config.json`
+
+Key fields:
+
+- `resources.mcp.config.enabled_mcp_servers`: MCP servers to enable inside the backend
+- `resources.mcp.config.workspace_root`: working directory exposed to MCP servers that need local workspace access
+- `resources.mcp.config.env_overrides`: maps AgentFlow-side endpoint variables to the native environment variables expected by each MCP server
+- `warmup.enabled` / `warmup.resources`: optional sandbox warmup configuration
+
+The current endpoint mapping is:
+
+- `AGENTFLOW_MCP_CANVAS_ENDPOINT -> CANVAS_DOMAIN`
+- `AGENTFLOW_MCP_NOTION_ENDPOINT -> BASE_URL`
+- `AGENTFLOW_MCP_WOOCOMMERCE_ENDPOINT -> WORDPRESS_SITE_URL`
+
+### Canvas synthesis config
+
+File:
+
+- `configs/synthesis/mcp.json`
+
+Key fields:
+
+- `model_name`, `api_key`, `base_url`: LLM configuration
+- `sandbox_server_url`: sandbox server address
+- `sandbox_auto_start`: whether the synthesis pipeline should start the sandbox automatically
+- `sandbox_config_path`: sandbox config path used when auto-start is enabled
+- `available_tools`: tool families exposed to the synthesis agent
+- `seeds_file`: default seed JSONL path
+- `max_depth`, `branching_factor`, `max_selected_traj`: trajectory sampling and selection controls
+
+---
+
+## FAQ
+
+### 1) Is the mock runtime required?
+
+No. The mock runtime is optional. You can point AgentFlow at real MCP-backed services instead.
+
+### 2) Does AgentFlow know whether it is talking to a real service or a mock backend?
+
+No. AgentFlow only uses the endpoint values configured for the MCP backend.
+
+### 3) Why does the Canvas endpoint look different from Notion and WooCommerce?
+
+The AgentFlow-side names are unified as `AGENTFLOW_MCP_*_ENDPOINT`, but the underlying MCP servers expect different native variables. In the current config, Canvas is passed through `CANVAS_DOMAIN`, while Notion and WooCommerce expect full base URLs.
+
+### 4) Do I have to start the sandbox server manually?
+
+No. The provided Canvas synthesis config already has `sandbox_auto_start=true`. Manual startup is still useful when you want to separate sandbox bring-up from synthesis execution.
+
+### 5) What should I check if synthesis fails after the sandbox starts?
+
+Check the following first:
+
+- the MCP endpoints resolve to reachable services
+- `OPENROUTER_API_KEY` or your chosen provider credentials are set correctly
+- your seed file path is correct
+- the selected model/provider combination is compatible with your synthesis workload
diff --git a/sandbox/result_formatter.py b/sandbox/result_formatter.py
index e1344332..9b85f41b 100644
--- a/sandbox/result_formatter.py
+++ b/sandbox/result_formatter.py
@@ -45,7 +45,7 @@
- rag:search: {"context": str, "query": str}
- rag:batch_search: {"contexts": List[str], "count": int, "errors"?: List[Dict]}
- bash: {"stdout": str, "stderr": str, "return_code": int, "cwd"?: str}
-- code: {"stdout": str, "stderr": str, "return_code": int, ...}
+- code: str for workspace/file tools, or {"stdout": str, "stderr": str, "return_code": int, ...}
============================================================================
@@ -57,7 +57,7 @@
- rag:stats: RAGStatsResult - RAG stats
- text2sql:*: SQLResult - SQL tool results (list_databases, get_schema, execute)
- bash: BashResult - bash execution result (`stdout/stderr`)
-- code: CodeExecutionResult - code execution result (`stdout/stderr`)
+- code: CodeExecutionResult - code tool result (string payloads or `stdout/stderr`)
- browser: BrowserResult - browser operation result
- vm: VMResult - VM operation result (accessibility tree only)
- session:*: SessionResult - session/status API result
@@ -121,7 +121,7 @@ class ToolResult(ABC):
- Support custom filtering rules when needed.
"""
- def __init__(self, raw_data: Dict[str, Any], metadata: Optional[Dict[str, Any]] = None):
+ def __init__(self, raw_data: Any, metadata: Optional[Dict[str, Any]] = None):
"""
Initialize a tool result object.
@@ -228,13 +228,15 @@ class CodeExecutionResult(ToolResult):
Code execution result.
Raw data schema:
- {
- "stdout": str,
- "stderr": str,
- "return_code": int,
- "execution_time_ms": float,
- "memory_used_mb": float
- }
+ - Workspace/file tools: str
+ - Execution-style tools:
+ {
+ "stdout": str,
+ "stderr": str,
+ "return_code": int,
+ "execution_time_ms": float,
+ "memory_used_mb": float
+ }
"""
def to_str(self, verbose: bool = False) -> str:
@@ -251,6 +253,15 @@ def to_str(self, verbose: bool = False) -> str:
error_msg = self.metadata.get("message", "Code execution failed")
return f"[Error] {error_msg}"
+ if isinstance(self.raw_data, str):
+ return self.raw_data.rstrip("\r\n") or "[Code executed successfully with no output]"
+
+ if not isinstance(self.raw_data, dict):
+ try:
+ return json.dumps(self.raw_data, ensure_ascii=False, separators=(",", ":"))
+ except Exception:
+ return str(self.raw_data)
+
stdout = self.raw_data.get("stdout", "")
stderr = self.raw_data.get("stderr", "")
return_code = self.raw_data.get("return_code", 0)
@@ -571,6 +582,62 @@ def to_str(self, verbose: bool = False) -> str:
return str(self.raw_data)
+# ============================================================================
+# MCP tool result.
+# ============================================================================
+
+class MCPResult(ToolResult):
+ """MCP tool result."""
+
+ def to_str(self, verbose: bool = False) -> str:
+ del verbose
+
+ if not self.success:
+ error_msg = self.metadata.get("message", "MCP tool failed")
+ return f"[Error] {error_msg}"
+
+ content = self.raw_data.get("content", [])
+ text_blocks = []
+ has_error_block = False
+ if isinstance(content, list):
+ for block in content:
+ if (
+ isinstance(block, dict)
+ and block.get("type") == "text"
+ and isinstance(block.get("text"), str)
+ and block.get("text").strip()
+ ):
+ if block.get("error") is True:
+ has_error_block = True
+ text_blocks.append(block["text"].rstrip())
+
+ if text_blocks:
+ result = "\n".join(text_blocks)
+ else:
+ structured_content = self.raw_data.get("structuredContent")
+ if (
+ isinstance(structured_content, dict)
+ and isinstance(structured_content.get("content"), str)
+ and structured_content.get("content").strip()
+ ):
+ result = structured_content["content"].rstrip()
+ else:
+ fallback_data = structured_content if structured_content else self.raw_data
+ try:
+ result = json.dumps(
+ fallback_data,
+ ensure_ascii=False,
+ separators=(",", ":"),
+ )
+ except Exception:
+ result = str(fallback_data)
+
+ if self.raw_data.get("isError") or has_error_block:
+ return f"[Error] {result}"
+
+ return result
+
+
# ============================================================================
# SQL tool result (text2sql).
# ============================================================================
@@ -724,6 +791,7 @@ class ResultFormatter:
"vm": VMResult,
"doc": DocResult,
"ds": DocResult,
+ "mcp": MCPResult,
}
@classmethod
diff --git a/sandbox/server/backends/resources/__init__.py b/sandbox/server/backends/resources/__init__.py
index 0042aa97..a981292f 100644
--- a/sandbox/server/backends/resources/__init__.py
+++ b/sandbox/server/backends/resources/__init__.py
@@ -62,12 +62,14 @@
from .vm import VMBackend, create_vm_backend
from .rag import RAGBackend, create_rag_backend
from .mcp import MCPBackend, ToolathlonGymBackend
+from .code import CodeBackend
__all__ = [
# Backend classes
"VMBackend",
"RAGBackend",
"MCPBackend",
+ "CodeBackend",
"ToolathlonGymBackend",
# Convenience factories
diff --git a/sandbox/server/backends/resources/code.py b/sandbox/server/backends/resources/code.py
new file mode 100644
index 00000000..a4d3248a
--- /dev/null
+++ b/sandbox/server/backends/resources/code.py
@@ -0,0 +1,342 @@
+"""
+Code backend skeleton for lightweight coding workspace integration.
+"""
+
+from __future__ import annotations
+
+import re
+import shutil
+import time
+import uuid
+from pathlib import Path
+from types import SimpleNamespace
+from typing import Any
+
+from sandbox.server.backends.base import Backend, BackendConfig
+from sandbox.server.backends.error_codes import ErrorCode
+from sandbox.server.backends.response_builder import (
+ build_error_response,
+ build_success_response,
+)
+from sandbox.server.backends.resources.code_vendor.edit_tools import EditTool, WriteTool
+from sandbox.server.backends.resources.code_vendor.file_tools import (
+ BashTool,
+ GlobTool,
+ GrepTool,
+ ReadTool,
+)
+
+
+class CodeBackend(Backend):
+ name = "code"
+ description = "Code Backend - lightweight coding workspace integration"
+ version = "1.0.0"
+
+ def __init__(self, config: BackendConfig | None = None):
+ if config is None:
+ config = BackendConfig(
+ enabled=True,
+ default_config={
+ "workspace_root": "/tmp/agentflow_code",
+ },
+ description="Code backend",
+ )
+ super().__init__(config)
+ self._tool_instances: dict[str, Any] | None = None
+
+ def bind_server(self, server) -> None:
+ super().bind_server(server)
+ for tool_name in ("read", "glob", "grep", "bash", "edit", "write"):
+ server.register_tool(
+ f"code:{tool_name}",
+ self._make_bridge_tool(tool_name),
+ resource_type="code",
+ )
+
+ async def initialize(self, worker_id: str, config: dict) -> dict:
+ source_dir = self._resolve_source_dir(config)
+ workspace, staged_workspace, previous_workspace = self._prepare_workspace(worker_id)
+
+ try:
+ if source_dir:
+ self._copy_source_dir(source_dir, staged_workspace)
+
+ self._load_code_tools()
+ self._commit_prepared_workspace(workspace, staged_workspace, previous_workspace)
+ except Exception:
+ if staged_workspace.exists():
+ shutil.rmtree(staged_workspace)
+ if previous_workspace is not None and previous_workspace.exists() and not workspace.exists():
+ previous_workspace.rename(workspace)
+ raise
+
+ return {
+ "workspace": str(workspace),
+ "source_dir": str(source_dir) if source_dir else "",
+ }
+
+ async def cleanup(self, worker_id: str, session_info: dict) -> None:
+ workspace_value = ((session_info or {}).get("data") or {}).get("workspace")
+ if not isinstance(workspace_value, str) or not workspace_value.strip():
+ return None
+
+ try:
+ workspace = Path(workspace_value).resolve()
+ workspace_root = self._get_workspace_root().resolve()
+ expected_workspace = (workspace_root / self._validate_worker_id(worker_id)).resolve(
+ strict=False
+ )
+ workspace.relative_to(workspace_root)
+ except (OSError, RuntimeError, ValueError, TypeError):
+ return None
+
+ if workspace != expected_workspace:
+ return None
+ if workspace.exists() and workspace.is_dir():
+ shutil.rmtree(workspace)
+ return None
+
+ def _get_workspace_root(self) -> Path:
+ value = self.get_default_config().get("workspace_root") or "/tmp/agentflow_code"
+ return Path(value)
+
+ def _prepare_workspace(self, worker_id: str) -> tuple[Path, Path, Path | None]:
+ safe_worker_id = self._validate_worker_id(worker_id)
+ workspace_root = self._get_workspace_root()
+ workspace_root.mkdir(parents=True, exist_ok=True)
+ workspace = workspace_root / safe_worker_id
+ staged_workspace = workspace_root / f".{safe_worker_id}.staged-{uuid.uuid4().hex}"
+ previous_workspace = (
+ workspace_root / f".{safe_worker_id}.previous-{uuid.uuid4().hex}"
+ if workspace.exists()
+ else None
+ )
+ staged_workspace.mkdir(parents=True, exist_ok=False)
+ return workspace, staged_workspace, previous_workspace
+
+ def _commit_prepared_workspace(
+ self,
+ workspace: Path,
+ staged_workspace: Path,
+ previous_workspace: Path | None,
+ ) -> None:
+ if previous_workspace is not None:
+ workspace.rename(previous_workspace)
+ staged_workspace.rename(workspace)
+ if previous_workspace is not None and previous_workspace.exists():
+ shutil.rmtree(previous_workspace)
+
+ def _validate_worker_id(self, worker_id: str) -> str:
+ if not isinstance(worker_id, str) or not worker_id:
+ raise ValueError("worker_id must be a non-empty string")
+ if worker_id in {".", ".."}:
+ raise ValueError("worker_id contains unsafe path traversal")
+ if worker_id != Path(worker_id).name:
+ raise ValueError("worker_id must be a single safe path component")
+ if not re.fullmatch(r"[A-Za-z0-9._-]+", worker_id):
+ raise ValueError("worker_id contains unsupported characters")
+ return worker_id
+
+ def _resolve_source_dir(self, config: dict | None) -> Path | None:
+ config = config or {}
+ value = config.get("source_dir")
+ if not value:
+ return None
+ source_dir = Path(value)
+ if not source_dir.exists():
+ raise ValueError(f"source_dir does not exist: {source_dir}")
+ if not source_dir.is_dir():
+ raise ValueError(f"source_dir is not a directory: {source_dir}")
+ return source_dir
+
+ def _copy_source_dir(self, source_dir: Path, workspace: Path) -> None:
+ if not source_dir.exists():
+ return
+ for child in source_dir.iterdir():
+ destination = workspace / child.name
+ if child.is_dir():
+ shutil.copytree(child, destination, dirs_exist_ok=True)
+ else:
+ shutil.copy2(child, destination)
+
+ def _load_code_tools(self) -> dict[str, Any]:
+ if self._tool_instances is None:
+ self._tool_instances = {
+ "read": ReadTool(),
+ "glob": GlobTool(),
+ "grep": GrepTool(),
+ "bash": BashTool(),
+ "edit": EditTool(),
+ "write": WriteTool(),
+ }
+ return self._tool_instances
+
+ def _make_bridge_tool(self, tool_name: str):
+ async def bridge_tool(session_info: dict, **params):
+ return await self._dispatch(tool_name, session_info, params)
+
+ bridge_tool.__name__ = f"code_{tool_name}"
+ return bridge_tool
+
+ async def _dispatch(
+ self,
+ tool_name: str,
+ session_info: dict,
+ params: dict[str, Any],
+ ) -> dict[str, Any]:
+ start_time = time.time()
+ full_name = f"{self.name}:{tool_name}"
+ session_id = (session_info or {}).get("session_id")
+ runtime_params = dict(params or {})
+ trace_id = runtime_params.pop("trace_id", None)
+ worker_id = runtime_params.pop("worker_id", None)
+ runtime_params.pop("session_id", None)
+
+ tool = self._load_code_tools().get(tool_name)
+ if tool is None:
+ return build_error_response(
+ code=ErrorCode.INVALID_REQUEST_FORMAT,
+ message=f"Unknown code tool: {tool_name}",
+ tool=full_name,
+ execution_time_ms=(time.time() - start_time) * 1000,
+ resource_type=self.name,
+ session_id=session_id,
+ trace_id=trace_id,
+ )
+
+ workspace_value = ((session_info or {}).get("data") or {}).get("workspace")
+ if not isinstance(workspace_value, str) or not workspace_value.strip():
+ return build_error_response(
+ code=ErrorCode.BUSINESS_FAILURE,
+ message="Invalid session workspace: missing or empty data.workspace",
+ tool=full_name,
+ execution_time_ms=(time.time() - start_time) * 1000,
+ resource_type=self.name,
+ session_id=session_id,
+ trace_id=trace_id,
+ )
+
+ try:
+ workspace = Path(workspace_value).resolve(strict=False)
+ workspace_root = self._get_workspace_root().resolve()
+ expected_workspace = (workspace_root / self._validate_worker_id(worker_id)).resolve(
+ strict=False
+ )
+ workspace.relative_to(workspace_root)
+ except (OSError, RuntimeError, ValueError, TypeError):
+ return build_error_response(
+ code=ErrorCode.BUSINESS_FAILURE,
+ message="Invalid session workspace: must resolve inside workspace_root",
+ tool=full_name,
+ execution_time_ms=(time.time() - start_time) * 1000,
+ resource_type=self.name,
+ session_id=session_id,
+ trace_id=trace_id,
+ )
+
+ if workspace != expected_workspace or not workspace.exists() or not workspace.is_dir():
+ return build_error_response(
+ code=ErrorCode.BUSINESS_FAILURE,
+ message="Invalid session workspace: must match existing worker workspace",
+ tool=full_name,
+ execution_time_ms=(time.time() - start_time) * 1000,
+ resource_type=self.name,
+ session_id=session_id,
+ trace_id=trace_id,
+ )
+
+ ctx = SimpleNamespace(cwd=str(workspace))
+ try:
+ normalized_params = self._normalize_tool_params(
+ tool_name=tool_name,
+ params=runtime_params,
+ workspace=workspace,
+ )
+ except ValueError as exc:
+ return build_error_response(
+ code=ErrorCode.BUSINESS_FAILURE,
+ message=str(exc),
+ tool=full_name,
+ execution_time_ms=(time.time() - start_time) * 1000,
+ resource_type=self.name,
+ session_id=session_id,
+ trace_id=trace_id,
+ )
+
+ try:
+ result = await tool.call(normalized_params, ctx)
+ except Exception as exc:
+ return build_error_response(
+ code=ErrorCode.EXECUTION_ERROR,
+ message=str(exc),
+ tool=full_name,
+ execution_time_ms=(time.time() - start_time) * 1000,
+ resource_type=self.name,
+ session_id=session_id,
+ trace_id=trace_id,
+ )
+
+ if isinstance(result, str) and result.startswith("Error:"):
+ return build_error_response(
+ code=ErrorCode.BUSINESS_FAILURE,
+ message=result,
+ tool=full_name,
+ execution_time_ms=(time.time() - start_time) * 1000,
+ resource_type=self.name,
+ session_id=session_id,
+ trace_id=trace_id,
+ )
+
+ return build_success_response(
+ data=result,
+ tool=full_name,
+ execution_time_ms=(time.time() - start_time) * 1000,
+ resource_type=self.name,
+ session_id=session_id,
+ trace_id=trace_id,
+ )
+
+ def _normalize_tool_params(
+ self,
+ tool_name: str,
+ params: dict[str, Any],
+ workspace: Path,
+ ) -> dict[str, Any]:
+ normalized = dict(params)
+ workspace_path = workspace.resolve(strict=False)
+
+ path_keys: tuple[str, ...] = ()
+ if tool_name in {"read", "edit", "write"}:
+ path_keys = ("file_path",)
+ elif tool_name in {"glob", "grep"}:
+ path_keys = ("path",)
+
+ for key in path_keys:
+ raw_value = normalized.get(key)
+ if not isinstance(raw_value, str) or not raw_value:
+ continue
+ value_path = Path(raw_value)
+ if value_path.is_absolute():
+ resolved = value_path.resolve(strict=False)
+ else:
+ resolved = (workspace_path / value_path).resolve(strict=False)
+
+ try:
+ resolved.relative_to(workspace_path)
+ except ValueError as exc:
+ raise ValueError(
+ f"Path parameter '{key}' must stay inside workspace"
+ ) from exc
+
+ normalized[key] = str(resolved)
+
+ if tool_name == "glob":
+ pattern = normalized.get("pattern")
+ if (
+ isinstance(pattern, str)
+ and pattern
+ and re.search(r"(^|[\\/])\.\.([\\/]|$)", pattern)
+ ):
+ raise ValueError("Glob pattern must not contain parent traversal segments")
+
+ return normalized
diff --git a/sandbox/server/backends/resources/code_vendor/__init__.py b/sandbox/server/backends/resources/code_vendor/__init__.py
new file mode 100644
index 00000000..1fc9cfa4
--- /dev/null
+++ b/sandbox/server/backends/resources/code_vendor/__init__.py
@@ -0,0 +1,11 @@
+from .edit_tools import EditTool, WriteTool
+from .file_tools import BashTool, GlobTool, GrepTool, ReadTool
+
+__all__ = [
+ "BashTool",
+ "EditTool",
+ "GlobTool",
+ "GrepTool",
+ "ReadTool",
+ "WriteTool",
+]
diff --git a/sandbox/server/backends/resources/code_vendor/edit_tools.py b/sandbox/server/backends/resources/code_vendor/edit_tools.py
new file mode 100644
index 00000000..622658d7
--- /dev/null
+++ b/sandbox/server/backends/resources/code_vendor/edit_tools.py
@@ -0,0 +1,85 @@
+from __future__ import annotations
+
+from pathlib import Path
+from typing import Any
+
+from .tool import Tool
+
+
+class EditTool(Tool):
+ name = "Edit"
+ description = (
+ "Perform an exact string replacement in a file. "
+ "old_string must uniquely identify the target location unless replace_all=true."
+ )
+
+ @property
+ def input_schema(self) -> dict[str, Any]:
+ return {
+ "type": "object",
+ "properties": {
+ "file_path": {"type": "string"},
+ "old_string": {"type": "string"},
+ "new_string": {"type": "string"},
+ "replace_all": {"type": "boolean", "default": False},
+ },
+ "required": ["file_path", "old_string", "new_string"],
+ }
+
+ async def call(self, args: dict[str, Any], ctx: Any) -> str:
+ del ctx
+ path = Path(args["file_path"])
+ old_string = args["old_string"]
+ new_string = args["new_string"]
+ replace_all = args.get("replace_all", False)
+
+ if not path.exists():
+ return f"Error: file not found: {path}"
+
+ content = path.read_text(encoding="utf-8")
+ count = content.count(old_string)
+ if count == 0:
+ return f"Error: old_string not found in {path}. Read the file first to verify the exact text."
+ if count > 1 and not replace_all:
+ return (
+ f"Error: old_string appears {count} times in {path}. "
+ "Provide more surrounding context to make it unique, or set replace_all=true."
+ )
+
+ if replace_all:
+ updated = content.replace(old_string, new_string)
+ replacements = count
+ else:
+ updated = content.replace(old_string, new_string, 1)
+ replacements = 1
+
+ path.write_text(updated, encoding="utf-8")
+ return f"Replaced {replacements} occurrence(s) in {path}"
+
+
+class WriteTool(Tool):
+ name = "Write"
+ description = "Write content to a file, creating parent directories if needed."
+
+ @property
+ def input_schema(self) -> dict[str, Any]:
+ return {
+ "type": "object",
+ "properties": {
+ "file_path": {"type": "string"},
+ "content": {"type": "string"},
+ },
+ "required": ["file_path", "content"],
+ }
+
+ async def call(self, args: dict[str, Any], ctx: Any) -> str:
+ del ctx
+ path = Path(args["file_path"])
+ content = args["content"]
+ path.parent.mkdir(parents=True, exist_ok=True)
+ path.write_text(content, encoding="utf-8")
+
+ line_count = content.count("\n")
+ if content and not content.endswith("\n"):
+ line_count += 1
+ return f"Wrote {len(content)} bytes ({line_count} lines) to {path}"
diff --git a/sandbox/server/backends/resources/code_vendor/file_tools.py b/sandbox/server/backends/resources/code_vendor/file_tools.py
new file mode 100644
index 00000000..1a61ff2d
--- /dev/null
+++ b/sandbox/server/backends/resources/code_vendor/file_tools.py
@@ -0,0 +1,185 @@
+from __future__ import annotations
+
+import asyncio
+import io
+import locale
+import os
+import signal
+import subprocess
+from pathlib import Path
+from typing import Any
+
+from .tool import Tool
+
+
+class BashTool(Tool):
+ name = "Bash"
+ description = "Execute a shell command and return stdout/stderr."
+
+ @property
+ def input_schema(self) -> dict[str, Any]:
+ return {
+ "type": "object",
+ "properties": {
+ "command": {"type": "string", "description": "Shell command to run"},
+ },
+ "required": ["command"],
+ }
+
+ async def call(self, args: dict[str, Any], ctx: Any) -> str:
+ proc = await asyncio.create_subprocess_shell(
+ args["command"],
+ shell=True,
+ cwd=ctx.cwd,
+ stdout=asyncio.subprocess.PIPE,
+ stderr=asyncio.subprocess.PIPE,
+ start_new_session=True,
+ )
+
+ try:
+ stdout_bytes, stderr_bytes = await proc.communicate()
+ except asyncio.CancelledError:
+ if proc.returncode is None:
+ try:
+ os.killpg(proc.pid, signal.SIGKILL)
+ except (ProcessLookupError, PermissionError):
+ proc.kill()
+ await proc.communicate()
+ raise
+
+ output = _decode_text_mode_output(stdout_bytes)
+ stderr = _decode_text_mode_output(stderr_bytes)
+ if proc.returncode:
+ return _format_command_error("bash", proc.returncode, output, stderr)
+ return _format_command_output(output, stderr)
+
+
+def _decode_text_mode_output(data: bytes | None) -> str:
+ if not data:
+ return ""
+
+ text_stream = io.TextIOWrapper(
+ io.BytesIO(data),
+ encoding=locale.getpreferredencoding(False),
+ newline=None,
+ )
+ try:
+ return text_stream.read()
+ finally:
+ text_stream.detach()
+
+
+def _format_command_output(stdout: str, stderr: str) -> str:
+ output = stdout
+ if stderr:
+ output += f"\n[stderr]:\n{stderr}" if output else f"[stderr]:\n{stderr}"
+ return output.strip() or "(no output)"
+
+
+def _format_command_error(tool_name: str, returncode: int, stdout: str, stderr: str) -> str:
+ if returncode < 0:
+ status = f"signal {-returncode}"
+ else:
+ status = f"exit status {returncode}"
+
+ summary = f"Error: {tool_name} command failed with {status}"
+ details = _format_command_output(stdout, stderr)
+ if details == "(no output)":
+ return summary
+ return f"{summary}\n{details}"
+
+
+class ReadTool(Tool):
+ name = "Read"
+ description = "Read a file and return its contents with line numbers."
+
+ @property
+ def input_schema(self) -> dict[str, Any]:
+ return {
+ "type": "object",
+ "properties": {
+ "file_path": {"type": "string"},
+ "offset": {"type": "integer", "description": "Start line (1-indexed)"},
+ "limit": {"type": "integer", "description": "Maximum lines to return"},
+ },
+ "required": ["file_path"],
+ }
+
+ async def call(self, args: dict[str, Any], ctx: Any) -> str:
+ del ctx
+ path = Path(args["file_path"])
+ if not path.exists():
+ return f"Error: file not found: {path}"
+
+ lines = path.read_text(encoding="utf-8").splitlines()
+ offset = max(0, args.get("offset", 1) - 1)
+ limit = args.get("limit", 2000)
+ selected = lines[offset : offset + limit]
+ return "\n".join(
+ f"{line_number:4}→{line}"
+ for line_number, line in enumerate(selected, start=offset + 1)
+ )
+
+ def is_read_only(self, args: dict[str, Any]) -> bool:
+ del args
+ return True
+
+
+class GlobTool(Tool):
+ name = "Glob"
+ description = "Find files matching a glob pattern."
+
+ @property
+ def input_schema(self) -> dict[str, Any]:
+ return {
+ "type": "object",
+ "properties": {
+ "pattern": {"type": "string", "description": "Glob pattern"},
+ "path": {"type": "string", "description": "Directory to search from"},
+ },
+ "required": ["pattern"],
+ }
+
+ async def call(self, args: dict[str, Any], ctx: Any) -> str:
+ base = Path(args.get("path", ctx.cwd))
+ pattern = args["pattern"]
+ matches = sorted(base.glob(pattern))
+ return "\n".join(str(match) for match in matches) or "(no matches)"
+
+ def is_read_only(self, args: dict[str, Any]) -> bool:
+ del args
+ return True
+
+
+class GrepTool(Tool):
+ name = "Grep"
+ description = "Search file contents with a regex pattern."
+
+ @property
+ def input_schema(self) -> dict[str, Any]:
+ return {
+ "type": "object",
+ "properties": {
+ "pattern": {"type": "string", "description": "Regex pattern"},
+ "path": {"type": "string", "description": "Directory to search"},
+ "glob": {"type": "string", "description": "Optional file glob filter"},
+ },
+ "required": ["pattern"],
+ }
+
+ async def call(self, args: dict[str, Any], ctx: Any) -> str:
+ base = Path(args.get("path", ctx.cwd))
+ cmd = ["grep", "-r", "-n"]
+ if "glob" in args:
+ cmd += ["--include", args["glob"]]
+ cmd += ["--", args["pattern"], str(base)]
+ result = subprocess.run(cmd, capture_output=True, text=True)
+ if result.returncode == 0:
+ return result.stdout or "(no matches)"
+ if result.returncode == 1:
+ return "(no matches)"
+ return _format_command_error("grep", result.returncode, result.stdout, result.stderr)
+
+ def is_read_only(self, args: dict[str, Any]) -> bool:
+ del args
+ return True
diff --git a/sandbox/server/backends/resources/code_vendor/tool.py b/sandbox/server/backends/resources/code_vendor/tool.py
new file mode 100644
index 00000000..bef70845
--- /dev/null
+++ b/sandbox/server/backends/resources/code_vendor/tool.py
@@ -0,0 +1,29 @@
+from __future__ import annotations
+
+from abc import ABC, abstractmethod
+from typing import Any
+
+
+class Tool(ABC):
+ name: str
+ description: str
+
+ @property
+ @abstractmethod
+ def input_schema(self) -> dict[str, Any]:
+ raise NotImplementedError
+
+ @abstractmethod
+ async def call(self, args: dict[str, Any], ctx: Any) -> str:
+ raise NotImplementedError
+
+ def is_read_only(self, args: dict[str, Any]) -> bool:
+ del args
+ return False
+
+ def to_api_format(self) -> dict[str, Any]:
+ return {
+ "name": self.name,
+ "description": self.description,
+ "input_schema": self.input_schema,
+ }
diff --git a/sandbox/server/backends/resources/mcp/client.py b/sandbox/server/backends/resources/mcp/client.py
index da83acc8..17b17aa6 100644
--- a/sandbox/server/backends/resources/mcp/client.py
+++ b/sandbox/server/backends/resources/mcp/client.py
@@ -14,6 +14,7 @@
logger = logging.getLogger("MCPStdioClient")
+_MCP_STDIO_STREAM_LIMIT = 8 * 1024 * 1024 # Covers observed Canvas conversations payloads with room to spare.
_PLACEHOLDER_PATTERN = re.compile(r"\$\{([^}]+)\}")
_SUPPORTED_PLACEHOLDERS = {"local_servers_paths", "agent_workspace", "task_dir"}
_BUNDLED_CONFIG_DIR = Path(__file__).parent / "configs"
@@ -186,6 +187,7 @@ async def start(self) -> None:
stdin=asyncio.subprocess.PIPE,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
+ limit=_MCP_STDIO_STREAM_LIMIT,
env=self._config.env,
cwd=self._config.cwd,
)
diff --git a/sandbox/server/backends/resources/mcp/configs/canvas.yaml b/sandbox/server/backends/resources/mcp/configs/canvas.yaml
index f3c2ffcd..7162d95d 100644
--- a/sandbox/server/backends/resources/mcp/configs/canvas.yaml
+++ b/sandbox/server/backends/resources/mcp/configs/canvas.yaml
@@ -7,17 +7,12 @@ name: canvas
params:
command: node
args:
- - "${local_servers_paths}/mcp-canvas-lms/environment/build/index.js"
+ - "${local_servers_paths}/mcp-canvas-lms/build/index.js"
env:
CANVAS_API_TOKEN: "${token.canvas_api_token}"
CANVAS_STUDENT_EMAIL: "${token.canvas_student_email}"
CANVAS_DOMAIN: "localhost:8080"
NODE_TLS_REJECT_UNAUTHORIZED: "0"
- PG_HOST: "${config.pg_host}"
- PG_PORT: "5434"
- PG_DATABASE: "${config.pg_database}"
- PG_USER: "eigent"
- PG_PASSWORD: "camel"
cwd: "${agent_workspace}"
client_session_timeout_seconds: 60
cache_tools_list: true
diff --git a/sandbox/server/backends/resources/mcp/configs/excel.yaml b/sandbox/server/backends/resources/mcp/configs/excel.yaml
index b0285a45..dee1fb3c 100644
--- a/sandbox/server/backends/resources/mcp/configs/excel.yaml
+++ b/sandbox/server/backends/resources/mcp/configs/excel.yaml
@@ -7,7 +7,7 @@ params:
command: uv
args:
- "--directory"
- - "${local_servers_paths}/excel-mcp-server/environment"
+ - "${local_servers_paths}/excel-mcp-server"
- "run"
- "excel-mcp-server"
- "stdio"
diff --git a/sandbox/server/backends/resources/mcp/configs/filesystem.yaml b/sandbox/server/backends/resources/mcp/configs/filesystem.yaml
index 077ffb97..fee30d19 100644
--- a/sandbox/server/backends/resources/mcp/configs/filesystem.yaml
+++ b/sandbox/server/backends/resources/mcp/configs/filesystem.yaml
@@ -6,7 +6,7 @@ name: filesystem
params:
command: node
args:
- - "${local_servers_paths}/filesystem/environment/dist/index.js"
+ - "${local_servers_paths}/filesystem/dist/index.js"
- "${agent_workspace}"
cwd: "${agent_workspace}"
client_session_timeout_seconds: 900
diff --git a/sandbox/server/backends/resources/mcp/configs/notion.yaml b/sandbox/server/backends/resources/mcp/configs/notion.yaml
index 570f3bd8..ee20d2f5 100644
--- a/sandbox/server/backends/resources/mcp/configs/notion.yaml
+++ b/sandbox/server/backends/resources/mcp/configs/notion.yaml
@@ -7,14 +7,9 @@ name: notion
params:
command: node
args:
- - "${local_servers_paths}/notion-mcp-server/environment/bin/cli-dev.mjs"
+ - "${local_servers_paths}/notion-mcp-server/bin/cli.mjs"
env:
OPENAPI_MCP_HEADERS: "{\"Authorization\": \"Bearer placeholder\", \"Notion-Version\": \"2022-06-28\" }"
- PG_HOST: "${config.pg_host}"
- PG_PORT: "5434"
- PG_DATABASE: "${config.pg_database}"
- PG_USER: "eigent"
- PG_PASSWORD: "camel"
HTTP_PROXY: ""
HTTPS_PROXY: ""
http_proxy: ""
diff --git a/sandbox/server/backends/resources/mcp/configs/pdf-tools.yaml b/sandbox/server/backends/resources/mcp/configs/pdf-tools.yaml
index 6446da85..9289979f 100644
--- a/sandbox/server/backends/resources/mcp/configs/pdf-tools.yaml
+++ b/sandbox/server/backends/resources/mcp/configs/pdf-tools.yaml
@@ -7,7 +7,7 @@ params:
command: uv
args:
- "--directory"
- - "${local_servers_paths}/pdf-tools-mcp/environment"
+ - "${local_servers_paths}/pdf-tools-mcp"
- "run"
- "pdf-tools-mcp"
- "--workspace_path"
diff --git a/sandbox/server/backends/resources/mcp/configs/pptx.yaml b/sandbox/server/backends/resources/mcp/configs/pptx.yaml
index 4e5753c1..79d89e77 100644
--- a/sandbox/server/backends/resources/mcp/configs/pptx.yaml
+++ b/sandbox/server/backends/resources/mcp/configs/pptx.yaml
@@ -7,7 +7,7 @@ params:
command: uv
args:
- "--directory"
- - "${local_servers_paths}/Office-PowerPoint-MCP-Server/environment"
+ - "${local_servers_paths}/Office-PowerPoint-MCP-Server"
- "run"
- "ppt_mcp_server"
cwd: "${agent_workspace}"
diff --git a/sandbox/server/backends/resources/mcp/configs/terminal.yaml b/sandbox/server/backends/resources/mcp/configs/terminal.yaml
index 1b897230..4876af42 100644
--- a/sandbox/server/backends/resources/mcp/configs/terminal.yaml
+++ b/sandbox/server/backends/resources/mcp/configs/terminal.yaml
@@ -8,7 +8,7 @@ params:
command: uv
args:
- "--directory"
- - "${local_servers_paths}/cli-mcp-server/environment"
+ - "${local_servers_paths}/cli-mcp-server"
- "run"
- "cli-mcp-server"
env:
diff --git a/sandbox/server/backends/resources/mcp/configs/woocommerce.yaml b/sandbox/server/backends/resources/mcp/configs/woocommerce.yaml
index eacd62c4..035dcb2a 100644
--- a/sandbox/server/backends/resources/mcp/configs/woocommerce.yaml
+++ b/sandbox/server/backends/resources/mcp/configs/woocommerce.yaml
@@ -6,16 +6,11 @@ name: woocommerce
params:
command: node
args:
- - "${local_servers_paths}/woocommerce-mcp/environment/dist/index.js"
+ - "${local_servers_paths}/woocommerce-mcp/dist/index.js"
env:
WORDPRESS_SITE_URL: "http://localhost"
WOOCOMMERCE_CONSUMER_KEY: "placeholder"
WOOCOMMERCE_CONSUMER_SECRET: "placeholder"
- PG_HOST: "${config.pg_host}"
- PG_PORT: "5434"
- PG_DATABASE: "${config.pg_database}"
- PG_USER: "eigent"
- PG_PASSWORD: "camel"
cwd: "${agent_workspace}"
client_session_timeout_seconds: 60
cache_tools_list: true
diff --git a/sandbox/server/backends/resources/mcp/configs/word.yaml b/sandbox/server/backends/resources/mcp/configs/word.yaml
index a15a1e5b..9f162511 100644
--- a/sandbox/server/backends/resources/mcp/configs/word.yaml
+++ b/sandbox/server/backends/resources/mcp/configs/word.yaml
@@ -7,7 +7,7 @@ params:
command: uv
args:
- "--directory"
- - "${local_servers_paths}/Office-Word-MCP-Server/environment"
+ - "${local_servers_paths}/Office-Word-MCP-Server"
- "run"
- "word_mcp_server"
cwd: "${agent_workspace}"
diff --git a/sandbox/server/backends/resources/mcp/mock_runtime/.env.example b/sandbox/server/backends/resources/mcp/mock_runtime/.env.example
new file mode 100644
index 00000000..309a80f0
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/mock_runtime/.env.example
@@ -0,0 +1,15 @@
+POSTGRES_HOST_PORT=5432
+POSTGRES_DB=toolathlon_gym
+POSTGRES_USER=eigent
+POSTGRES_PASSWORD=camel
+
+CANVAS_SHIM_HOST=127.0.0.1
+CANVAS_SHIM_PORT=38080
+CANVAS_TLS_CERT_PATH=./certs/canvas-cert.pem
+CANVAS_TLS_KEY_PATH=./certs/canvas-key.pem
+
+NOTION_SHIM_HOST=127.0.0.1
+NOTION_SHIM_PORT=38081
+
+WOOCOMMERCE_SHIM_HOST=127.0.0.1
+WOOCOMMERCE_SHIM_PORT=38082
diff --git a/sandbox/server/backends/resources/mcp/mock_runtime/README.md b/sandbox/server/backends/resources/mcp/mock_runtime/README.md
new file mode 100644
index 00000000..93961193
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/mock_runtime/README.md
@@ -0,0 +1,103 @@
+# MCP Mock Runtime
+
+This directory contains an optional standalone mock runtime for AgentFlow's MCP backend.
+
+AgentFlow is transparent to mock vs. real services. It only depends on the endpoint values wired into its own MCP config:
+
+- `AGENTFLOW_MCP_CANVAS_ENDPOINT`
+- `AGENTFLOW_MCP_NOTION_ENDPOINT`
+- `AGENTFLOW_MCP_WOOCOMMERCE_ENDPOINT`
+
+The current mock-runtime scope only covers these three HTTP-backed MCP integrations:
+
+- `canvas`
+- `notion`
+- `woocommerce`
+
+## What Starts Here
+
+`scripts/start_mock_runtime.sh` brings up:
+
+- the local `postgres` container from `docker-compose.yml`
+- the three shim servers under `shims/canvas`, `shims/notion`, and `shims/woocommerce`
+
+This runtime is extra infrastructure. AgentFlow itself still just starts the MCP servers listed in its default config and uses the endpoint overrides above for the three HTTP-backed services.
+
+At the current defaults, that means the following self-contained MCP servers can start alongside the mock-backed HTTP trio:
+
+- `excel`
+- `filesystem`
+- `howtocook`
+- `memory`
+- `pdf-tools`
+- `playwright_with_chunk`
+- `pptx`
+- `terminal`
+- `word`
+
+## Bring-Up
+
+1. Copy the example env file:
+
+```bash
+cp sandbox/server/backends/resources/mcp/mock_runtime/.env.example \
+ sandbox/server/backends/resources/mcp/mock_runtime/.env
+```
+
+2. Update the copied `.env` before startup:
+
+- `CANVAS_TLS_CERT_PATH` and `CANVAS_TLS_KEY_PATH` in `.env.example` are placeholder paths.
+- Point both values at real certificate files before running the shim.
+- If your team already keeps test certificates under `TOOLATHLON_GYM_ROOT`, you can repoint these paths there.
+
+3. Export the runtime root and the AgentFlow-visible MCP endpoints:
+
+```bash
+export TOOLATHLON_GYM_ROOT=/path/to/exported_mcp_snapshot
+export AGENTFLOW_MCP_CANVAS_ENDPOINT=127.0.0.1:38080
+export AGENTFLOW_MCP_NOTION_ENDPOINT=http://127.0.0.1:38081
+export AGENTFLOW_MCP_WOOCOMMERCE_ENDPOINT=http://127.0.0.1:38082
+```
+
+4. Start the mock runtime:
+
+```bash
+bash sandbox/server/backends/resources/mcp/mock_runtime/scripts/start_mock_runtime.sh
+```
+
+Current default shim ports from `.env.example` are:
+
+- `canvas`: `https://127.0.0.1:38080`
+- `notion`: `http://127.0.0.1:38081`
+- `woocommerce`: `http://127.0.0.1:38082`
+
+Those defaults match AgentFlow's current MCP env overrides:
+
+- `CANVAS_DOMAIN=${AGENTFLOW_MCP_CANVAS_ENDPOINT:-127.0.0.1:38080}`
+- `BASE_URL=${AGENTFLOW_MCP_NOTION_ENDPOINT:-http://127.0.0.1:38081}`
+- `WORDPRESS_SITE_URL=${AGENTFLOW_MCP_WOOCOMMERCE_ENDPOINT:-http://127.0.0.1:38082}`
+
+## Status And Shutdown
+
+Check runtime status:
+
+```bash
+bash sandbox/server/backends/resources/mcp/mock_runtime/scripts/status_mock_runtime.sh
+```
+
+Stop the runtime:
+
+```bash
+bash sandbox/server/backends/resources/mcp/mock_runtime/scripts/stop_mock_runtime.sh
+```
+
+`status_mock_runtime.sh` reports postgres health plus each shim's PID/process state and `/healthz` result. `stop_mock_runtime.sh` stops the three shim processes and runs `docker compose down` for the local postgres stack.
+
+## Validation
+
+This phase does not include a dedicated smoke-test suite for the mock runtime. Validation is manual:
+
+- bring the runtime up with `start_mock_runtime.sh`
+- confirm healthy output from `status_mock_runtime.sh`
+- point AgentFlow at the three exported `AGENTFLOW_MCP_*_ENDPOINT` values
+- verify requests against `canvas`, `notion`, and `woocommerce` end-to-end by hand
diff --git a/sandbox/server/backends/resources/mcp/db/init.sql.gz b/sandbox/server/backends/resources/mcp/mock_runtime/db/init.sql.gz
similarity index 100%
rename from sandbox/server/backends/resources/mcp/db/init.sql.gz
rename to sandbox/server/backends/resources/mcp/mock_runtime/db/init.sql.gz
diff --git a/sandbox/server/backends/resources/mcp/mock_runtime/docker-compose.yml b/sandbox/server/backends/resources/mcp/mock_runtime/docker-compose.yml
new file mode 100644
index 00000000..dce41ede
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/mock_runtime/docker-compose.yml
@@ -0,0 +1,18 @@
+services:
+ postgres:
+ image: postgres:16
+ environment:
+ POSTGRES_DB: ${POSTGRES_DB:-toolathlon_gym}
+ POSTGRES_USER: ${POSTGRES_USER:-eigent}
+ POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-camel}
+ ports:
+ - "${POSTGRES_HOST_PORT}:5432"
+ volumes:
+ - ./db/init.sql.gz:/docker-entrypoint-initdb.d/init.sql.gz:ro
+ healthcheck:
+ test:
+ - CMD-SHELL
+ - pg_isready -U ${POSTGRES_USER:-eigent} -d ${POSTGRES_DB:-toolathlon_gym}
+ interval: 5s
+ timeout: 5s
+ retries: 12
diff --git a/sandbox/server/backends/resources/mcp/mock_runtime/scripts/common.sh b/sandbox/server/backends/resources/mcp/mock_runtime/scripts/common.sh
new file mode 100644
index 00000000..80c07dbb
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/mock_runtime/scripts/common.sh
@@ -0,0 +1,265 @@
+#!/usr/bin/env bash
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+MOCK_RUNTIME_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
+ENV_FILE="$MOCK_RUNTIME_DIR/.env"
+COMPOSE_FILE="$MOCK_RUNTIME_DIR/docker-compose.yml"
+LOG_DIR="$MOCK_RUNTIME_DIR/logs"
+RUN_DIR="$MOCK_RUNTIME_DIR/run"
+NODE_BIN="${NODE_BIN:-node}"
+
+load_mock_runtime_env() {
+ if [[ ! -f "$ENV_FILE" ]]; then
+ echo "mock runtime env file not found: $ENV_FILE" >&2
+ return 1
+ fi
+
+ set -a
+ # shellcheck disable=SC1090
+ source "$ENV_FILE"
+ set +a
+}
+
+require_toolathlon_root() {
+ if [[ -z "${TOOLATHLON_GYM_ROOT:-}" ]]; then
+ echo "TOOLATHLON_GYM_ROOT is required" >&2
+ return 1
+ fi
+
+ if [[ ! -d "$TOOLATHLON_GYM_ROOT" ]]; then
+ echo "TOOLATHLON_GYM_ROOT does not exist: $TOOLATHLON_GYM_ROOT" >&2
+ return 1
+ fi
+}
+
+ensure_runtime_dirs() {
+ mkdir -p "$LOG_DIR" "$RUN_DIR"
+}
+
+service_script_path() {
+ local service="$1"
+ printf '%s/shims/%s/server.mjs\n' "$MOCK_RUNTIME_DIR" "$service"
+}
+
+service_pid_path() {
+ local service="$1"
+ background_pid_path "$service"
+}
+
+service_log_path() {
+ local service="$1"
+ background_log_path "$service"
+}
+
+background_pid_path() {
+ local name="$1"
+ printf '%s/%s.pid\n' "$RUN_DIR" "$name"
+}
+
+background_log_path() {
+ local name="$1"
+ printf '%s/%s.log\n' "$LOG_DIR" "$name"
+}
+
+service_host() {
+ local service="$1"
+ case "$service" in
+ canvas) printf '%s\n' "${CANVAS_SHIM_HOST:-127.0.0.1}" ;;
+ notion) printf '%s\n' "${NOTION_SHIM_HOST:-127.0.0.1}" ;;
+ woocommerce) printf '%s\n' "${WOOCOMMERCE_SHIM_HOST:-127.0.0.1}" ;;
+ *)
+ echo "unknown service: $service" >&2
+ return 1
+ ;;
+ esac
+}
+
+service_port() {
+ local service="$1"
+ case "$service" in
+ canvas) printf '%s\n' "${CANVAS_SHIM_PORT:-38080}" ;;
+ notion) printf '%s\n' "${NOTION_SHIM_PORT:-38081}" ;;
+ woocommerce) printf '%s\n' "${WOOCOMMERCE_SHIM_PORT:-38082}" ;;
+ *)
+ echo "unknown service: $service" >&2
+ return 1
+ ;;
+ esac
+}
+
+service_healthz_url() {
+ local service="$1"
+ local host
+ local port
+ host="$(service_host "$service")" || return 1
+ port="$(service_port "$service")" || return 1
+
+ if [[ "$service" == "canvas" ]]; then
+ printf 'https://%s:%s/healthz\n' "$host" "$port"
+ return 0
+ fi
+
+ printf 'http://%s:%s/healthz\n' "$host" "$port"
+}
+
+docker_compose() {
+ docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" "$@"
+}
+
+postgres_container_id() {
+ docker_compose ps -q postgres
+}
+
+postgres_health_status() {
+ local container_id
+ container_id="$(postgres_container_id)"
+ if [[ -z "$container_id" ]]; then
+ printf 'not_running\n'
+ return 1
+ fi
+
+ docker inspect \
+ --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' \
+ "$container_id"
+}
+
+wait_for_postgres_healthy() {
+ local attempts="${1:-30}"
+ local sleep_seconds="${2:-2}"
+ local status=""
+ local attempt
+
+ for ((attempt = 1; attempt <= attempts; attempt++)); do
+ status="$(postgres_health_status 2>/dev/null || true)"
+ if [[ "$status" == "healthy" ]]; then
+ return 0
+ fi
+ sleep "$sleep_seconds"
+ done
+
+ echo "postgres did not become healthy; last status: ${status:-unknown}" >&2
+ return 1
+}
+
+is_pid_running() {
+ local pid="$1"
+ kill -0 "$pid" 2>/dev/null
+}
+
+service_pid() {
+ local service="$1"
+ local pid_file
+ pid_file="$(service_pid_path "$service")"
+ if [[ -f "$pid_file" ]]; then
+ cat "$pid_file"
+ fi
+}
+
+service_is_running() {
+ local service="$1"
+ local pid
+ pid="$(service_pid "$service")"
+ [[ -n "$pid" ]] && is_pid_running "$pid"
+}
+
+start_service_process() {
+ local service="$1"
+ local script_path
+
+ script_path="$(service_script_path "$service")"
+
+ if [[ ! -f "$script_path" ]]; then
+ echo "service script not found: $script_path" >&2
+ return 1
+ fi
+
+ if service_is_running "$service"; then
+ echo "$service is already running" >&2
+ return 1
+ fi
+
+ start_node_background "$service" env TOOLATHLON_GYM_ROOT="$TOOLATHLON_GYM_ROOT" \
+ "$NODE_BIN" "$script_path"
+}
+
+start_node_background() {
+ local name="$1"
+ shift
+
+ local pid_file
+ local log_file
+ local pid
+
+ pid_file="$(background_pid_path "$name")"
+ log_file="$(background_log_path "$name")"
+
+ rm -f "$pid_file"
+ (
+ cd "$MOCK_RUNTIME_DIR"
+ nohup "$@" >>"$log_file" 2>&1 &
+ echo $! >"$pid_file"
+ )
+
+ pid="$(cat "$pid_file")"
+ if [[ -z "$pid" ]] || ! is_pid_running "$pid"; then
+ echo "failed to start $name" >&2
+ return 1
+ fi
+}
+
+stop_service_process() {
+ local service="$1"
+ local pid_file
+ local pid
+
+ pid_file="$(service_pid_path "$service")"
+ if [[ ! -f "$pid_file" ]]; then
+ return 0
+ fi
+
+ pid="$(cat "$pid_file")"
+ if [[ -n "$pid" ]] && is_pid_running "$pid"; then
+ kill "$pid"
+ for _ in {1..10}; do
+ if ! is_pid_running "$pid"; then
+ break
+ fi
+ sleep 1
+ done
+ if is_pid_running "$pid"; then
+ kill -9 "$pid"
+ fi
+ fi
+
+ rm -f "$pid_file"
+}
+
+probe_service_healthz() {
+ local service="$1"
+ local url
+ local curl_args=(-fsS --max-time 5)
+
+ url="$(service_healthz_url "$service")" || return 1
+ if [[ "$service" == "canvas" ]]; then
+ curl_args+=(-k)
+ fi
+
+ curl "${curl_args[@]}" "$url" >/dev/null
+}
+
+wait_for_service_healthz() {
+ local service="$1"
+ local attempts="${2:-20}"
+ local sleep_seconds="${3:-1}"
+ local attempt
+
+ for ((attempt = 1; attempt <= attempts; attempt++)); do
+ if probe_service_healthz "$service"; then
+ return 0
+ fi
+ sleep "$sleep_seconds"
+ done
+
+ echo "$service health check failed" >&2
+ return 1
+}
diff --git a/sandbox/server/backends/resources/mcp/mock_runtime/scripts/start_mock_runtime.sh b/sandbox/server/backends/resources/mcp/mock_runtime/scripts/start_mock_runtime.sh
new file mode 100644
index 00000000..3da4da01
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/mock_runtime/scripts/start_mock_runtime.sh
@@ -0,0 +1,45 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+# shellcheck disable=SC1091
+source "$SCRIPT_DIR/common.sh"
+
+SERVICES=(canvas notion woocommerce)
+STARTED_SERVICES=()
+POSTGRES_STARTED_BY_SCRIPT=0
+
+cleanup_on_failure() {
+ local service
+ for ((idx = ${#STARTED_SERVICES[@]} - 1; idx >= 0; idx--)); do
+ service="${STARTED_SERVICES[$idx]}"
+ stop_service_process "$service" || true
+ done
+
+ if [[ "$POSTGRES_STARTED_BY_SCRIPT" -eq 1 ]]; then
+ docker_compose stop postgres || true
+ fi
+}
+
+trap cleanup_on_failure ERR
+
+load_mock_runtime_env
+require_toolathlon_root
+ensure_runtime_dirs
+
+if [[ -z "$(postgres_container_id)" ]]; then
+ POSTGRES_STARTED_BY_SCRIPT=1
+fi
+
+docker_compose up -d postgres
+wait_for_postgres_healthy
+
+for service in "${SERVICES[@]}"; do
+ start_service_process "$service"
+ STARTED_SERVICES+=("$service")
+ wait_for_service_healthz "$service"
+done
+
+trap - ERR
+
+echo "mock runtime started"
diff --git a/sandbox/server/backends/resources/mcp/mock_runtime/scripts/status_mock_runtime.sh b/sandbox/server/backends/resources/mcp/mock_runtime/scripts/status_mock_runtime.sh
new file mode 100644
index 00000000..05ea7e30
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/mock_runtime/scripts/status_mock_runtime.sh
@@ -0,0 +1,45 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+# shellcheck disable=SC1091
+source "$SCRIPT_DIR/common.sh"
+
+SERVICES=(canvas notion woocommerce)
+overall_status=0
+
+load_mock_runtime_env
+ensure_runtime_dirs
+
+postgres_status="$(postgres_health_status 2>/dev/null || true)"
+if [[ -z "$postgres_status" ]]; then
+ postgres_status="not_running"
+ overall_status=1
+elif [[ "$postgres_status" != "healthy" ]]; then
+ overall_status=1
+fi
+echo "postgres: $postgres_status"
+
+for service in "${SERVICES[@]}"; do
+ pid="$(service_pid "$service")"
+ if [[ -n "$pid" ]] && is_pid_running "$pid"; then
+ process_status="running"
+ elif [[ -n "$pid" ]]; then
+ process_status="stale_pid"
+ overall_status=1
+ else
+ process_status="not_running"
+ overall_status=1
+ fi
+
+ if probe_service_healthz "$service"; then
+ health_status="healthy"
+ else
+ health_status="unhealthy"
+ overall_status=1
+ fi
+
+ echo "$service: pid=${pid:-none} process=$process_status healthz=$health_status"
+done
+
+exit "$overall_status"
diff --git a/sandbox/server/backends/resources/mcp/mock_runtime/scripts/stop_mock_runtime.sh b/sandbox/server/backends/resources/mcp/mock_runtime/scripts/stop_mock_runtime.sh
new file mode 100644
index 00000000..cbb5222d
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/mock_runtime/scripts/stop_mock_runtime.sh
@@ -0,0 +1,19 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+# shellcheck disable=SC1091
+source "$SCRIPT_DIR/common.sh"
+
+SERVICES=(canvas notion woocommerce)
+
+load_mock_runtime_env
+ensure_runtime_dirs
+
+for service in "${SERVICES[@]}"; do
+ stop_service_process "$service"
+done
+
+docker_compose down
+
+echo "mock runtime stopped"
diff --git a/sandbox/server/backends/resources/mcp/mock_runtime/shims/canvas/rewrite_urls.mjs b/sandbox/server/backends/resources/mcp/mock_runtime/shims/canvas/rewrite_urls.mjs
new file mode 100644
index 00000000..a8f8f5cc
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/mock_runtime/shims/canvas/rewrite_urls.mjs
@@ -0,0 +1,16 @@
+const MOCK_CANVAS_PREFIX = "https://mock-canvas.local/";
+
+export function rewriteCanvasPublicUrl(url, publicBaseUrl) {
+ if (typeof url !== "string" || !url.startsWith(MOCK_CANVAS_PREFIX)) {
+ return url;
+ }
+
+ const sourceUrl = new URL(url);
+ const targetUrl = new URL(publicBaseUrl);
+
+ targetUrl.pathname = sourceUrl.pathname;
+ targetUrl.search = sourceUrl.search;
+ targetUrl.hash = sourceUrl.hash;
+
+ return targetUrl.toString();
+}
diff --git a/sandbox/server/backends/resources/mcp/mock_runtime/shims/canvas/rewrite_urls.test.mjs b/sandbox/server/backends/resources/mcp/mock_runtime/shims/canvas/rewrite_urls.test.mjs
new file mode 100644
index 00000000..f50b29a6
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/mock_runtime/shims/canvas/rewrite_urls.test.mjs
@@ -0,0 +1,23 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+
+import { rewriteCanvasPublicUrl } from "./rewrite_urls.mjs";
+
+test("rewrites mock canvas upload URLs to the public shim base URL", () => {
+ assert.equal(
+ rewriteCanvasPublicUrl(
+ "https://mock-canvas.local/upload/123",
+ "https://127.0.0.1:38080",
+ ),
+ "https://127.0.0.1:38080/upload/123",
+ );
+});
+
+test("leaves normal URLs unchanged", () => {
+ const url = "https://example.com/upload/123?x=1";
+
+ assert.equal(
+ rewriteCanvasPublicUrl(url, "https://127.0.0.1:38080"),
+ url,
+ );
+});
diff --git a/sandbox/server/backends/resources/mcp/mock_runtime/shims/canvas/server.mjs b/sandbox/server/backends/resources/mcp/mock_runtime/shims/canvas/server.mjs
new file mode 100644
index 00000000..71efd87a
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/mock_runtime/shims/canvas/server.mjs
@@ -0,0 +1,396 @@
+import fs from "node:fs";
+import https from "node:https";
+import path from "node:path";
+import { pathToFileURL } from "node:url";
+
+import { rewriteCanvasPublicUrl } from "./rewrite_urls.mjs";
+
+function resolveToolathlonGymRoot() {
+ const root = process.env.TOOLATHLON_GYM_ROOT;
+ if (!root) {
+ throw new Error("TOOLATHLON_GYM_ROOT is required");
+ }
+
+ return path.resolve(root);
+}
+
+function deriveRouterEnv() {
+ process.env.PG_HOST ||= "127.0.0.1";
+ process.env.PG_PORT ||= process.env.POSTGRES_HOST_PORT || "5432";
+ process.env.PG_DATABASE ||= process.env.POSTGRES_DB || "toolathlon";
+ process.env.PG_USER ||= process.env.POSTGRES_USER || "postgres";
+ process.env.PG_PASSWORD ||= process.env.POSTGRES_PASSWORD || "postgres";
+}
+
+async function loadPgCanvasRouter() {
+ const gymRoot = resolveToolathlonGymRoot();
+ const routerModulePath = path.join(
+ gymRoot,
+ "local_servers/mcp-canvas-lms/build/pg-canvas-router.js",
+ );
+
+ if (!fs.existsSync(routerModulePath)) {
+ throw new Error(`PgCanvasRouter build not found: ${routerModulePath}`);
+ }
+
+ const routerModule = await import(pathToFileURL(routerModulePath).href);
+ if (!routerModule.PgCanvasRouter) {
+ throw new Error(`PgCanvasRouter export missing: ${routerModulePath}`);
+ }
+
+ return routerModule.PgCanvasRouter;
+}
+
+function resolveRuntimePath(filePath) {
+ return path.isAbsolute(filePath)
+ ? filePath
+ : path.resolve(process.cwd(), filePath);
+}
+
+function getPublicBaseUrl() {
+ const host = process.env.CANVAS_SHIM_HOST || "127.0.0.1";
+ const port = Number(process.env.CANVAS_SHIM_PORT || "38080");
+ return `https://${host}:${port}`;
+}
+
+function getServerOptions() {
+ const certPath = process.env.CANVAS_TLS_CERT_PATH;
+ const keyPath = process.env.CANVAS_TLS_KEY_PATH;
+
+ if (!certPath || !keyPath) {
+ throw new Error("CANVAS_TLS_CERT_PATH and CANVAS_TLS_KEY_PATH are required");
+ }
+
+ return {
+ host: process.env.CANVAS_SHIM_HOST || "127.0.0.1",
+ port: Number(process.env.CANVAS_SHIM_PORT || "38080"),
+ cert: fs.readFileSync(resolveRuntimePath(certPath)),
+ key: fs.readFileSync(resolveRuntimePath(keyPath)),
+ };
+}
+
+function sendJson(res, statusCode, payload) {
+ res.writeHead(statusCode, { "content-type": "application/json" });
+ res.end(JSON.stringify(payload));
+}
+
+function collectQueryParams(searchParams) {
+ const params = {};
+
+ for (const [key, value] of searchParams) {
+ if (params[key] === undefined) {
+ params[key] = value;
+ continue;
+ }
+
+ params[key] = Array.isArray(params[key])
+ ? [...params[key], value]
+ : [params[key], value];
+ }
+
+ return params;
+}
+
+async function readRequestBody(req) {
+ const chunks = [];
+
+ for await (const chunk of req) {
+ chunks.push(chunk);
+ }
+
+ return Buffer.concat(chunks);
+}
+
+async function drainRequestBody(req) {
+ for await (const _chunk of req) {
+ // Drain upload bodies without buffering them in memory.
+ }
+}
+
+async function parseRouterBody(req) {
+ const bodyBuffer = await readRequestBody(req);
+ if (bodyBuffer.length === 0) {
+ return {};
+ }
+
+ const contentType = req.headers["content-type"] || "";
+ const rawBody = bodyBuffer.toString("utf8");
+
+ if (contentType.includes("application/json")) {
+ return JSON.parse(rawBody);
+ }
+
+ if (contentType.includes("application/x-www-form-urlencoded")) {
+ return Object.fromEntries(new URLSearchParams(rawBody));
+ }
+
+ try {
+ return JSON.parse(rawBody);
+ } catch {
+ return {};
+ }
+}
+
+function rewriteCanvasPayload(value, publicBaseUrl) {
+ if (Array.isArray(value)) {
+ return value.map((item) => rewriteCanvasPayload(item, publicBaseUrl));
+ }
+
+ if (!value || typeof value !== "object") {
+ return value;
+ }
+
+ const rewritten = {};
+
+ for (const [key, nestedValue] of Object.entries(value)) {
+ if (
+ (key === "upload_url" || key === "location" || key === "url")
+ && typeof nestedValue === "string"
+ ) {
+ rewritten[key] = rewriteCanvasPublicUrl(nestedValue, publicBaseUrl);
+ continue;
+ }
+
+ rewritten[key] = rewriteCanvasPayload(nestedValue, publicBaseUrl);
+ }
+
+ return rewritten;
+}
+
+function rememberUploadMapping(payload, publicBaseUrl, uploadMappings) {
+ if (
+ !payload
+ || typeof payload !== "object"
+ || Array.isArray(payload)
+ || payload.id === undefined
+ || typeof payload.upload_url !== "string"
+ ) {
+ return;
+ }
+
+ const uploadUrl = new URL(
+ rewriteCanvasPublicUrl(payload.upload_url, publicBaseUrl),
+ );
+
+ uploadMappings.set(uploadUrl.pathname, String(payload.id));
+}
+
+function rememberFileMapping(payload, publicBaseUrl, fileMappings) {
+ if (
+ !payload
+ || typeof payload !== "object"
+ || Array.isArray(payload)
+ || payload.id === undefined
+ || typeof payload.url !== "string"
+ ) {
+ return;
+ }
+
+ const fileUrl = new URL(
+ rewriteCanvasPublicUrl(payload.url, publicBaseUrl),
+ );
+ const publicOrigin = new URL(publicBaseUrl).origin;
+
+ if (
+ fileUrl.origin !== publicOrigin
+ || !fileUrl.pathname.startsWith("/files/")
+ ) {
+ return;
+ }
+
+ fileMappings.set(fileUrl.pathname, String(payload.id));
+}
+
+async function forwardToCanvasRouter(
+ req,
+ res,
+ requestUrl,
+ router,
+ publicBaseUrl,
+ uploadMappings,
+ fileMappings,
+) {
+ const canvasPath = requestUrl.pathname.replace(/^\/api\/v1\/?/, "");
+ const queryParams = collectQueryParams(requestUrl.searchParams);
+ const method = req.method || "GET";
+
+ let routerResponse;
+
+ if (method === "GET") {
+ routerResponse = await router.get(canvasPath, { params: queryParams });
+ } else if (method === "POST") {
+ routerResponse = await router.post(
+ canvasPath,
+ await parseRouterBody(req),
+ { params: queryParams },
+ );
+ } else if (method === "PUT") {
+ routerResponse = await router.put(
+ canvasPath,
+ await parseRouterBody(req),
+ { params: queryParams },
+ );
+ } else if (method === "DELETE") {
+ routerResponse = await router.delete(canvasPath, {
+ data: await parseRouterBody(req),
+ params: queryParams,
+ });
+ } else {
+ sendJson(res, 405, { error: "method_not_allowed", method });
+ return;
+ }
+
+ const payload = rewriteCanvasPayload(routerResponse.data, publicBaseUrl);
+ rememberUploadMapping(payload, publicBaseUrl, uploadMappings);
+ rememberFileMapping(payload, publicBaseUrl, fileMappings);
+ sendJson(res, routerResponse.status || 200, payload);
+}
+
+async function handleUploadRequest(req, res, requestUrl, publicBaseUrl, uploadMappings) {
+ if (req.method !== "POST") {
+ sendJson(res, 405, { error: "method_not_allowed", method: req.method });
+ return;
+ }
+
+ const fileId = uploadMappings.get(requestUrl.pathname);
+ if (!fileId) {
+ sendJson(res, 404, { error: "upload_not_found", path: requestUrl.pathname });
+ return;
+ }
+
+ await drainRequestBody(req);
+
+ sendJson(res, 200, {
+ location: new URL(`/api/v1/files/${fileId}`, publicBaseUrl).toString(),
+ });
+}
+
+async function handleFileRequest(req, res, requestUrl, router, publicBaseUrl, fileMappings) {
+ if (req.method !== "GET") {
+ sendJson(res, 405, { error: "method_not_allowed", method: req.method });
+ return;
+ }
+
+ const fileId = fileMappings.get(requestUrl.pathname);
+ if (!fileId) {
+ sendJson(res, 404, { error: "file_not_found", path: requestUrl.pathname });
+ return;
+ }
+
+ const routerResponse = await router.get(`files/${fileId}`, {
+ params: collectQueryParams(requestUrl.searchParams),
+ });
+ const payload = rewriteCanvasPayload(routerResponse.data, publicBaseUrl);
+ rememberFileMapping(payload, publicBaseUrl, fileMappings);
+ sendJson(res, routerResponse.status || 200, payload);
+}
+
+export async function createCanvasShimServer() {
+ deriveRouterEnv();
+
+ const PgCanvasRouter = await loadPgCanvasRouter();
+ const router = new PgCanvasRouter();
+ const uploadMappings = new Map();
+ const fileMappings = new Map();
+ const publicBaseUrl = getPublicBaseUrl();
+ const serverOptions = getServerOptions();
+
+ const server = https.createServer(serverOptions, async (req, res) => {
+ try {
+ const requestUrl = new URL(req.url || "/", publicBaseUrl);
+
+ if (req.method === "GET" && requestUrl.pathname === "/healthz") {
+ sendJson(res, 200, { ok: true });
+ return;
+ }
+
+ if (requestUrl.pathname.startsWith("/upload/")) {
+ await handleUploadRequest(
+ req,
+ res,
+ requestUrl,
+ publicBaseUrl,
+ uploadMappings,
+ );
+ return;
+ }
+
+ if (requestUrl.pathname.startsWith("/files/")) {
+ await handleFileRequest(
+ req,
+ res,
+ requestUrl,
+ router,
+ publicBaseUrl,
+ fileMappings,
+ );
+ return;
+ }
+
+ if (requestUrl.pathname.startsWith("/api/v1/")) {
+ await forwardToCanvasRouter(
+ req,
+ res,
+ requestUrl,
+ router,
+ publicBaseUrl,
+ uploadMappings,
+ fileMappings,
+ );
+ return;
+ }
+
+ sendJson(res, 404, { error: "not_found", path: requestUrl.pathname });
+ } catch (error) {
+ sendJson(res, 500, { error: String(error) });
+ }
+ });
+
+ return { server, router, host: serverOptions.host, port: serverOptions.port };
+}
+
+export const __test__ = {
+ drainRequestBody,
+ handleFileRequest,
+};
+
+export async function startCanvasShimServer() {
+ const { server, router, host, port } = await createCanvasShimServer();
+
+ await new Promise((resolve) => {
+ server.listen(port, host, resolve);
+ });
+
+ return { server, router };
+}
+
+async function shutdown(server, router, code = 0) {
+ server.close(() => {
+ router.pool?.end?.().finally(() => {
+ process.exit(code);
+ });
+ });
+}
+
+if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
+ try {
+ const { server, router } = await startCanvasShimServer();
+ const address = server.address();
+
+ if (!address || typeof address === "string") {
+ throw new Error("Canvas shim failed to bind a TCP port");
+ }
+
+ console.log(`READY ${address.port}`);
+
+ process.on("SIGTERM", () => {
+ shutdown(server, router, 0);
+ });
+ process.on("SIGINT", () => {
+ shutdown(server, router, 0);
+ });
+ } catch (error) {
+ console.error(String(error));
+ process.exit(1);
+ }
+}
diff --git a/sandbox/server/backends/resources/mcp/mock_runtime/shims/canvas/server.test.mjs b/sandbox/server/backends/resources/mcp/mock_runtime/shims/canvas/server.test.mjs
new file mode 100644
index 00000000..6497b4a2
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/mock_runtime/shims/canvas/server.test.mjs
@@ -0,0 +1,74 @@
+import assert from "node:assert/strict";
+import { Readable } from "node:stream";
+import test from "node:test";
+
+import { __test__ } from "./server.mjs";
+
+function createMockResponse() {
+ return {
+ body: "",
+ headers: undefined,
+ statusCode: undefined,
+ end(chunk = "") {
+ this.body = String(chunk);
+ },
+ writeHead(statusCode, headers) {
+ this.statusCode = statusCode;
+ this.headers = headers;
+ },
+ };
+}
+
+test("bridges rewritten file URLs to the router GET files/:id endpoint", async () => {
+ const response = createMockResponse();
+ const routerCalls = [];
+ const fileMappings = new Map([["/files/report.pdf", "file-42"]]);
+ const publicBaseUrl = "https://127.0.0.1:38080";
+
+ await __test__.handleFileRequest(
+ { method: "GET" },
+ response,
+ new URL("/files/report.pdf", publicBaseUrl),
+ {
+ async get(path, options) {
+ routerCalls.push({ path, options });
+ return {
+ status: 200,
+ data: {
+ id: "file-42",
+ url: "https://mock-canvas.local/files/report.pdf",
+ },
+ };
+ },
+ },
+ publicBaseUrl,
+ fileMappings,
+ );
+
+ assert.deepEqual(routerCalls, [{
+ path: "files/file-42",
+ options: { params: {} },
+ }]);
+ assert.equal(response.statusCode, 200);
+ assert.deepEqual(JSON.parse(response.body), {
+ id: "file-42",
+ url: "https://127.0.0.1:38080/files/report.pdf",
+ });
+});
+
+test("drains upload request streams without concatenating buffered chunks", async () => {
+ const originalConcat = Buffer.concat;
+
+ Buffer.concat = () => {
+ throw new Error("Buffer.concat should not be used while draining uploads");
+ };
+
+ try {
+ await __test__.drainRequestBody(Readable.from([
+ Buffer.from("chunk-1"),
+ Buffer.from("chunk-2"),
+ ]));
+ } finally {
+ Buffer.concat = originalConcat;
+ }
+});
diff --git a/sandbox/server/backends/resources/mcp/mock_runtime/shims/notion/lookup_operation.mjs b/sandbox/server/backends/resources/mcp/mock_runtime/shims/notion/lookup_operation.mjs
new file mode 100644
index 00000000..52b65c09
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/mock_runtime/shims/notion/lookup_operation.mjs
@@ -0,0 +1,120 @@
+function normalizePathname(pathname) {
+ if (!pathname) {
+ return "/";
+ }
+
+ if (pathname.length > 1 && pathname.endsWith("/")) {
+ return pathname.slice(0, -1);
+ }
+
+ return pathname;
+}
+
+function splitPathSegments(pathname) {
+ return normalizePathname(pathname)
+ .split("/")
+ .filter(Boolean);
+}
+
+function isPlaceholderSegment(segment) {
+ return segment.startsWith("{") && segment.endsWith("}");
+}
+
+function getPlaceholderName(segment) {
+ return segment.slice(1, -1);
+}
+
+function matchPath(templatePath, requestPath) {
+ const templateSegments = splitPathSegments(templatePath);
+ const requestSegments = splitPathSegments(requestPath);
+
+ if (templateSegments.length !== requestSegments.length) {
+ return null;
+ }
+
+ const pathParams = {};
+ let literalCount = 0;
+
+ for (let index = 0; index < templateSegments.length; index += 1) {
+ const templateSegment = templateSegments[index];
+ const requestSegment = requestSegments[index];
+
+ if (isPlaceholderSegment(templateSegment)) {
+ pathParams[getPlaceholderName(templateSegment)] = decodeURIComponent(
+ requestSegment,
+ );
+ continue;
+ }
+
+ if (templateSegment !== requestSegment) {
+ return null;
+ }
+
+ literalCount += 1;
+ }
+
+ return {
+ literalCount,
+ pathParams,
+ };
+}
+
+export function lookupOperation(spec, method, pathname) {
+ const paths = spec?.paths;
+ if (!paths || !method) {
+ return null;
+ }
+
+ const normalizedPath = normalizePathname(pathname);
+ const normalizedMethod = method.toLowerCase();
+ const exactPathItem = paths[normalizedPath];
+ const exactOperation = exactPathItem?.[normalizedMethod];
+
+ if (exactOperation) {
+ return {
+ operation: exactOperation,
+ pathParams: {},
+ };
+ }
+
+ let bestMatch = null;
+
+ for (const [templatePath, pathItem] of Object.entries(paths)) {
+ const operation = pathItem?.[normalizedMethod];
+ if (!operation) {
+ continue;
+ }
+
+ const matchedPath = matchPath(templatePath, normalizedPath);
+ if (!matchedPath) {
+ continue;
+ }
+
+ const candidate = {
+ operation,
+ pathParams: matchedPath.pathParams,
+ score: matchedPath.literalCount,
+ templatePath,
+ };
+
+ if (
+ !bestMatch
+ || candidate.score > bestMatch.score
+ || (
+ candidate.score === bestMatch.score
+ && candidate.templatePath < bestMatch.templatePath
+ )
+ ) {
+ bestMatch = candidate;
+ }
+ }
+
+ if (!bestMatch) {
+ return null;
+ }
+
+ return {
+ operation: bestMatch.operation,
+ pathParams: bestMatch.pathParams,
+ };
+}
diff --git a/sandbox/server/backends/resources/mcp/mock_runtime/shims/notion/lookup_operation.test.mjs b/sandbox/server/backends/resources/mcp/mock_runtime/shims/notion/lookup_operation.test.mjs
new file mode 100644
index 00000000..6b4edd6a
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/mock_runtime/shims/notion/lookup_operation.test.mjs
@@ -0,0 +1,57 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+
+import { lookupOperation } from "./lookup_operation.mjs";
+
+const spec = {
+ paths: {
+ "/v1/users/{user_id}": {
+ get: {
+ operationId: "get-user",
+ },
+ },
+ "/v1/users": {
+ get: {
+ operationId: "get-users",
+ },
+ },
+ "/v1/users/me": {
+ get: {
+ operationId: "get-self",
+ },
+ },
+ "/v1/databases/{database_id}/query": {
+ post: {
+ operationId: "post-database-query",
+ },
+ },
+ },
+};
+
+test("prefers exact notion route matches over placeholder routes", () => {
+ const match = lookupOperation(spec, "GET", "/v1/users/me");
+
+ assert.deepEqual(match, {
+ operation: {
+ operationId: "get-self",
+ },
+ pathParams: {},
+ });
+});
+
+test("matches notion placeholder routes and extracts path params", () => {
+ const match = lookupOperation(spec, "POST", "/v1/databases/abc/query");
+
+ assert.deepEqual(match, {
+ operation: {
+ operationId: "post-database-query",
+ },
+ pathParams: {
+ database_id: "abc",
+ },
+ });
+});
+
+test("returns null when no notion operation matches the request", () => {
+ assert.equal(lookupOperation(spec, "DELETE", "/v1/users/me"), null);
+});
diff --git a/sandbox/server/backends/resources/mcp/mock_runtime/shims/notion/server.mjs b/sandbox/server/backends/resources/mcp/mock_runtime/shims/notion/server.mjs
new file mode 100644
index 00000000..d5daafcd
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/mock_runtime/shims/notion/server.mjs
@@ -0,0 +1,263 @@
+import fs from "node:fs";
+import http from "node:http";
+import path from "node:path";
+import { pathToFileURL } from "node:url";
+
+import { lookupOperation } from "./lookup_operation.mjs";
+
+function resolveToolathlonGymRoot() {
+ const root = process.env.TOOLATHLON_GYM_ROOT;
+ if (!root) {
+ throw new Error("TOOLATHLON_GYM_ROOT is required");
+ }
+
+ return path.resolve(root);
+}
+
+function deriveClientEnv() {
+ process.env.PG_HOST ||= "127.0.0.1";
+ process.env.PG_PORT ||= process.env.POSTGRES_HOST_PORT || "5432";
+ process.env.PG_DATABASE ||= process.env.POSTGRES_DB || "toolathlon_gym";
+ process.env.PG_USER ||= process.env.POSTGRES_USER || "eigent";
+ process.env.PG_PASSWORD ||= process.env.POSTGRES_PASSWORD || "camel";
+}
+
+function getServerOptions() {
+ return {
+ host: process.env.NOTION_SHIM_HOST || "127.0.0.1",
+ port: Number(process.env.NOTION_SHIM_PORT || "38081"),
+ };
+}
+
+function sendJson(res, statusCode, payload) {
+ res.writeHead(statusCode, { "content-type": "application/json" });
+ res.end(JSON.stringify(payload));
+}
+
+function collectQueryParams(searchParams) {
+ const params = {};
+
+ for (const [key, value] of searchParams) {
+ if (params[key] === undefined) {
+ params[key] = value;
+ continue;
+ }
+
+ params[key] = Array.isArray(params[key])
+ ? [...params[key], value]
+ : [params[key], value];
+ }
+
+ return params;
+}
+
+async function readRequestBody(req) {
+ const chunks = [];
+
+ for await (const chunk of req) {
+ chunks.push(chunk);
+ }
+
+ return Buffer.concat(chunks).toString("utf8");
+}
+
+async function parseJsonBody(req) {
+ const rawBody = await readRequestBody(req);
+ if (!rawBody) {
+ return {};
+ }
+
+ try {
+ const parsed = JSON.parse(rawBody);
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
+ ? parsed
+ : {};
+ } catch (error) {
+ if (error instanceof SyntaxError) {
+ error.code = "invalid_json";
+ }
+
+ throw error;
+ }
+}
+
+function buildOperationParams(pathParams, queryParams, bodyParams) {
+ return {
+ ...queryParams,
+ ...bodyParams,
+ ...pathParams,
+ };
+}
+
+function extractResponsePayload(result) {
+ if (result?.body !== undefined) {
+ return result.body;
+ }
+
+ if (result?.data !== undefined) {
+ return result.data;
+ }
+
+ return {};
+}
+
+function getOperationStatus(result) {
+ return Number.isInteger(result?.status) ? result.status : 200;
+}
+
+function getOperationResponse(result) {
+ return {
+ payload: extractResponsePayload(result),
+ status: getOperationStatus(result),
+ };
+}
+
+function loadOpenApiSpec() {
+ const gymRoot = resolveToolathlonGymRoot();
+ const specPath = path.join(
+ gymRoot,
+ "local_servers/notion-mcp-server/scripts/notion-openapi.json",
+ );
+
+ if (!fs.existsSync(specPath)) {
+ throw new Error(`Notion OpenAPI spec not found: ${specPath}`);
+ }
+
+ return JSON.parse(fs.readFileSync(specPath, "utf8"));
+}
+
+async function loadPgHttpClient() {
+ const gymRoot = resolveToolathlonGymRoot();
+ const clientModulePath = path.join(
+ gymRoot,
+ "local_servers/notion-mcp-server/build/src/openapi-mcp-server/client/pg-client.js",
+ );
+
+ if (!fs.existsSync(clientModulePath)) {
+ throw new Error(`PgHttpClient build not found: ${clientModulePath}`);
+ }
+
+ const clientModule = await import(pathToFileURL(clientModulePath).href);
+ const PgHttpClient = clientModule.PgHttpClient
+ || clientModule.default?.PgHttpClient
+ || clientModule.default;
+
+ if (typeof PgHttpClient !== "function") {
+ throw new Error(`PgHttpClient export missing: ${clientModulePath}`);
+ }
+
+ return PgHttpClient;
+}
+
+async function forwardToNotionClient(req, res, requestUrl, spec, client) {
+ const match = lookupOperation(spec, req.method || "GET", requestUrl.pathname);
+
+ if (!match) {
+ sendJson(res, 404, { error: "not_found", path: requestUrl.pathname });
+ return;
+ }
+
+ const bodyParams = ["POST", "PATCH", "PUT"].includes(req.method || "")
+ ? await parseJsonBody(req)
+ : {};
+ const params = buildOperationParams(
+ match.pathParams,
+ collectQueryParams(requestUrl.searchParams),
+ bodyParams,
+ );
+ const result = await client.executeOperation(match.operation, params);
+ const response = getOperationResponse(result);
+
+ sendJson(res, response.status, response.payload);
+}
+
+export async function createNotionShimServer() {
+ deriveClientEnv();
+
+ const [spec, PgHttpClient] = await Promise.all([
+ loadOpenApiSpec(),
+ loadPgHttpClient(),
+ ]);
+ const client = new PgHttpClient();
+ const serverOptions = getServerOptions();
+ const baseUrl = `http://${serverOptions.host}:${serverOptions.port}`;
+
+ const server = http.createServer(async (req, res) => {
+ try {
+ const requestUrl = new URL(req.url || "/", baseUrl);
+
+ if (req.method === "GET" && requestUrl.pathname === "/healthz") {
+ sendJson(res, 200, { ok: true });
+ return;
+ }
+
+ if (requestUrl.pathname.startsWith("/v1/")) {
+ await forwardToNotionClient(req, res, requestUrl, spec, client);
+ return;
+ }
+
+ sendJson(res, 404, { error: "not_found", path: requestUrl.pathname });
+ } catch (error) {
+ if (error instanceof SyntaxError && error.code === "invalid_json") {
+ sendJson(res, 400, { error: "invalid_json" });
+ return;
+ }
+
+ console.error(error);
+ sendJson(res, 500, { error: "internal_error" });
+ }
+ });
+
+ return {
+ client,
+ host: serverOptions.host,
+ port: serverOptions.port,
+ server,
+ };
+}
+
+export async function startNotionShimServer() {
+ const { server, client, host, port } = await createNotionShimServer();
+
+ await new Promise((resolve) => {
+ server.listen(port, host, resolve);
+ });
+
+ return { client, server };
+}
+
+export const __test__ = {
+ buildOperationParams,
+};
+
+async function shutdown(server, client, code = 0) {
+ server.close(() => {
+ Promise.resolve(client.pool?.end?.())
+ .finally(() => {
+ process.exit(code);
+ });
+ });
+}
+
+if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
+ try {
+ const { server, client } = await startNotionShimServer();
+ const address = server.address();
+
+ if (!address || typeof address === "string") {
+ throw new Error("Notion shim failed to bind a TCP port");
+ }
+
+ console.log(`READY ${address.port}`);
+
+ process.on("SIGTERM", () => {
+ shutdown(server, client, 0);
+ });
+ process.on("SIGINT", () => {
+ shutdown(server, client, 0);
+ });
+ } catch (error) {
+ console.error(String(error));
+ process.exit(1);
+ }
+}
diff --git a/sandbox/server/backends/resources/mcp/mock_runtime/shims/notion/server.test.mjs b/sandbox/server/backends/resources/mcp/mock_runtime/shims/notion/server.test.mjs
new file mode 100644
index 00000000..005583ed
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/mock_runtime/shims/notion/server.test.mjs
@@ -0,0 +1,124 @@
+import fs from "node:fs";
+import assert from "node:assert/strict";
+import http from "node:http";
+import os from "node:os";
+import path from "node:path";
+import test from "node:test";
+
+import * as serverModule from "./server.mjs";
+
+test("path params override query params and body params", () => {
+ const params = serverModule.__test__?.buildOperationParams(
+ { database_id: "abc", user_id: "123" },
+ { user_id: "456", filter: "active" },
+ { database_id: "override", page_size: 10 },
+ );
+
+ assert.deepEqual(params, {
+ database_id: "abc",
+ filter: "active",
+ page_size: 10,
+ user_id: "123",
+ });
+});
+
+test("internal server errors stay opaque to clients", async () => {
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "notion-shim-"));
+ fs.mkdirSync(
+ path.join(
+ root,
+ "local_servers/notion-mcp-server/scripts",
+ ),
+ { recursive: true },
+ );
+ fs.mkdirSync(
+ path.join(
+ root,
+ "local_servers/notion-mcp-server/build/src/openapi-mcp-server/client",
+ ),
+ { recursive: true },
+ );
+
+ fs.writeFileSync(
+ path.join(root, "package.json"),
+ JSON.stringify({ type: "module" }),
+ );
+ fs.writeFileSync(
+ path.join(
+ root,
+ "local_servers/notion-mcp-server/scripts/notion-openapi.json",
+ ),
+ JSON.stringify({ paths: { "/v1/test": { get: {} } } }),
+ );
+ fs.writeFileSync(
+ path.join(
+ root,
+ "local_servers/notion-mcp-server/build/src/openapi-mcp-server/client/pg-client.js",
+ ),
+ "export class PgHttpClient { async executeOperation() { throw new Error('boom: secret detail'); } }\n",
+ );
+
+ const previousEnv = {
+ TOOLATHLON_GYM_ROOT: process.env.TOOLATHLON_GYM_ROOT,
+ NOTION_SHIM_HOST: process.env.NOTION_SHIM_HOST,
+ NOTION_SHIM_PORT: process.env.NOTION_SHIM_PORT,
+ };
+ process.env.TOOLATHLON_GYM_ROOT = root;
+ process.env.NOTION_SHIM_HOST = "127.0.0.1";
+ process.env.NOTION_SHIM_PORT = "0";
+
+ const consoleErrors = [];
+ const originalConsoleError = console.error;
+ console.error = (...args) => {
+ consoleErrors.push(args);
+ };
+
+ let server;
+ try {
+ ({ server } = await serverModule.createNotionShimServer());
+
+ await new Promise((resolve) => {
+ server.listen(0, "127.0.0.1", resolve);
+ });
+
+ const address = server.address();
+ assert.ok(address && typeof address !== "string");
+
+ const response = await new Promise((resolve, reject) => {
+ const req = http.request(
+ {
+ hostname: "127.0.0.1",
+ port: address.port,
+ path: "/v1/test",
+ method: "GET",
+ },
+ (res) => {
+ const chunks = [];
+ res.on("data", (chunk) => chunks.push(chunk));
+ res.on("end", () => {
+ resolve({
+ body: Buffer.concat(chunks).toString("utf8"),
+ statusCode: res.statusCode,
+ });
+ });
+ },
+ );
+
+ req.on("error", reject);
+ req.end();
+ });
+
+ assert.equal(response.statusCode, 500);
+ assert.deepEqual(JSON.parse(response.body), { error: "internal_error" });
+ assert.equal(consoleErrors.length > 0, true);
+ assert.match(String(consoleErrors[0][0]), /boom: secret detail/);
+ } finally {
+ console.error = originalConsoleError;
+ process.env.TOOLATHLON_GYM_ROOT = previousEnv.TOOLATHLON_GYM_ROOT;
+ process.env.NOTION_SHIM_HOST = previousEnv.NOTION_SHIM_HOST;
+ process.env.NOTION_SHIM_PORT = previousEnv.NOTION_SHIM_PORT;
+ await new Promise((resolve) => {
+ server?.close(resolve);
+ });
+ }
+});
diff --git a/sandbox/server/backends/resources/mcp/mock_runtime/shims/woocommerce/server.mjs b/sandbox/server/backends/resources/mcp/mock_runtime/shims/woocommerce/server.mjs
new file mode 100644
index 00000000..70341e37
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/mock_runtime/shims/woocommerce/server.mjs
@@ -0,0 +1,295 @@
+import fs from "node:fs";
+import http from "node:http";
+import path from "node:path";
+import { pathToFileURL } from "node:url";
+
+import { stripWooPrefix } from "./strip_prefix.mjs";
+
+function resolveToolathlonGymRoot() {
+ const root = process.env.TOOLATHLON_GYM_ROOT;
+ if (!root) {
+ throw new Error("TOOLATHLON_GYM_ROOT is required");
+ }
+
+ return path.resolve(root);
+}
+
+function deriveRouterEnv() {
+ return {
+ PG_HOST: process.env.PG_HOST || "127.0.0.1",
+ PG_PORT: process.env.PG_PORT || process.env.POSTGRES_HOST_PORT || "5432",
+ PG_DATABASE: process.env.PG_DATABASE || process.env.POSTGRES_DB || "toolathlon_gym",
+ PG_USER: process.env.PG_USER || process.env.POSTGRES_USER || "eigent",
+ PG_PASSWORD: process.env.PG_PASSWORD || process.env.POSTGRES_PASSWORD || "camel",
+ };
+}
+
+async function withRouterEnv(callback) {
+ const derivedEnv = deriveRouterEnv();
+ const previousEnv = {};
+
+ for (const [key, value] of Object.entries(derivedEnv)) {
+ previousEnv[key] = process.env[key];
+ process.env[key] = value;
+ }
+
+ try {
+ return await callback();
+ } finally {
+ for (const [key, value] of Object.entries(previousEnv)) {
+ if (value === undefined) {
+ delete process.env[key];
+ continue;
+ }
+
+ process.env[key] = value;
+ }
+ }
+}
+
+async function loadPgRestRouter() {
+ const gymRoot = resolveToolathlonGymRoot();
+ const routerModulePath = path.join(
+ gymRoot,
+ "local_servers/woocommerce-mcp/dist/services/pg-rest-router.js",
+ );
+
+ if (!fs.existsSync(routerModulePath)) {
+ throw new Error(`PgRestRouter build not found: ${routerModulePath}`);
+ }
+
+ const routerModule = await import(pathToFileURL(routerModulePath).href);
+ const PgRestRouter = routerModule.PgRestRouter
+ || routerModule.default?.PgRestRouter
+ || routerModule.default;
+
+ if (typeof PgRestRouter !== "function") {
+ throw new Error(`PgRestRouter export missing: ${routerModulePath}`);
+ }
+
+ return PgRestRouter;
+}
+
+function getServerOptions() {
+ return {
+ host: process.env.WOOCOMMERCE_SHIM_HOST || "127.0.0.1",
+ port: Number(process.env.WOOCOMMERCE_SHIM_PORT || "38082"),
+ };
+}
+
+function sendJson(res, statusCode, payload) {
+ res.writeHead(statusCode, { "content-type": "application/json" });
+ res.end(JSON.stringify(payload));
+}
+
+function collectQueryParams(searchParams) {
+ const params = {};
+
+ for (const [key, value] of searchParams) {
+ if (params[key] === undefined) {
+ params[key] = value;
+ continue;
+ }
+
+ params[key] = Array.isArray(params[key])
+ ? [...params[key], value]
+ : [params[key], value];
+ }
+
+ return params;
+}
+
+async function readRequestBody(req) {
+ const chunks = [];
+
+ for await (const chunk of req) {
+ chunks.push(chunk);
+ }
+
+ return Buffer.concat(chunks).toString("utf8");
+}
+
+async function parseJsonBody(req) {
+ const rawBody = await readRequestBody(req);
+ if (!rawBody) {
+ return undefined;
+ }
+
+ try {
+ return JSON.parse(rawBody);
+ } catch (error) {
+ if (error instanceof SyntaxError) {
+ error.code = "invalid_json";
+ }
+
+ throw error;
+ }
+}
+
+async function forwardToWooRouter(req, res, requestUrl, router) {
+ const method = req.method || "GET";
+ const routerPath = stripWooPrefix(requestUrl.pathname);
+ const params = collectQueryParams(requestUrl.searchParams);
+
+ let routerResponse;
+
+ if (method === "GET") {
+ routerResponse = await router.get(routerPath, { params });
+ } else if (method === "POST") {
+ routerResponse = await router.post(
+ routerPath,
+ await parseJsonBody(req),
+ { params },
+ );
+ } else if (method === "PUT") {
+ routerResponse = await router.put(
+ routerPath,
+ await parseJsonBody(req),
+ { params },
+ );
+ } else if (method === "DELETE") {
+ const data = await parseJsonBody(req);
+ const config = { params };
+
+ if (data !== undefined) {
+ config.data = data;
+ }
+
+ routerResponse = await router.delete(routerPath, config);
+ } else {
+ sendJson(res, 405, { error: "method_not_allowed", method });
+ return;
+ }
+
+ sendJson(res, routerResponse.status || 200, routerResponse.data);
+}
+
+export async function createWooCommerceShimServer() {
+ const { router } = await withRouterEnv(async () => {
+ const LoadedPgRestRouter = await loadPgRestRouter();
+ return {
+ router: new LoadedPgRestRouter(),
+ };
+ });
+ const serverOptions = getServerOptions();
+ const baseUrl = `http://${serverOptions.host}:${serverOptions.port}`;
+
+ const server = http.createServer(async (req, res) => {
+ try {
+ const requestUrl = new URL(req.url || "/", baseUrl);
+
+ if (req.method === "GET" && requestUrl.pathname === "/healthz") {
+ sendJson(res, 200, { ok: true });
+ return;
+ }
+
+ if (requestUrl.pathname.startsWith("/wp-json/wc/v3/")) {
+ await forwardToWooRouter(req, res, requestUrl, router);
+ return;
+ }
+
+ sendJson(res, 404, { error: "not_found", path: requestUrl.pathname });
+ } catch (error) {
+ if (error instanceof SyntaxError && error.code === "invalid_json") {
+ sendJson(res, 400, { error: "invalid_json" });
+ return;
+ }
+
+ console.error(error);
+ sendJson(res, 500, { error: "internal_error" });
+ }
+ });
+
+ return {
+ host: serverOptions.host,
+ port: serverOptions.port,
+ router,
+ server,
+ };
+}
+
+async function cleanupStartFailure(server, router) {
+ await new Promise((resolve) => {
+ try {
+ server.close(() => {
+ resolve();
+ });
+ } catch {
+ resolve();
+ }
+ });
+
+ try {
+ await router.pool?.end?.();
+ } catch {
+ // Best-effort cleanup for startup failures.
+ }
+}
+
+export async function startWooCommerceShimServer() {
+ const { server, router, host, port } = await createWooCommerceShimServer();
+
+ await new Promise((resolve, reject) => {
+ const rejectWithCleanup = (error) => {
+ server.off("listening", handleListening);
+ server.off("error", handleError);
+ cleanupStartFailure(server, router).finally(() => {
+ reject(error);
+ });
+ };
+ const handleError = (error) => {
+ rejectWithCleanup(error);
+ };
+ const handleListening = () => {
+ server.off("error", handleError);
+ resolve();
+ };
+
+ server.once("error", handleError);
+ server.once("listening", handleListening);
+
+ try {
+ server.listen(port, host);
+ } catch (error) {
+ server.off("error", handleError);
+ server.off("listening", handleListening);
+ cleanupStartFailure(server, router).finally(() => {
+ reject(error);
+ });
+ }
+ });
+
+ return { router, server };
+}
+
+async function shutdown(server, router, code = 0) {
+ server.close(() => {
+ Promise.resolve(router.pool?.end?.())
+ .finally(() => {
+ process.exit(code);
+ });
+ });
+}
+
+if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
+ try {
+ const { server, router } = await startWooCommerceShimServer();
+ const address = server.address();
+
+ if (!address || typeof address === "string") {
+ throw new Error("WooCommerce shim failed to bind a TCP port");
+ }
+
+ console.log(`READY ${address.port}`);
+
+ process.on("SIGTERM", () => {
+ shutdown(server, router, 0);
+ });
+ process.on("SIGINT", () => {
+ shutdown(server, router, 0);
+ });
+ } catch (error) {
+ console.error(String(error));
+ process.exit(1);
+ }
+}
diff --git a/sandbox/server/backends/resources/mcp/mock_runtime/shims/woocommerce/server.test.mjs b/sandbox/server/backends/resources/mcp/mock_runtime/shims/woocommerce/server.test.mjs
new file mode 100644
index 00000000..60096599
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/mock_runtime/shims/woocommerce/server.test.mjs
@@ -0,0 +1,412 @@
+import assert from "node:assert/strict";
+import { EventEmitter } from "node:events";
+import fs from "node:fs";
+import http from "node:http";
+import os from "node:os";
+import path from "node:path";
+import test from "node:test";
+
+import * as serverModule from "./server.mjs";
+
+function createGymRoot(routerSource) {
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "woocommerce-shim-"));
+
+ fs.mkdirSync(
+ path.join(
+ root,
+ "local_servers/woocommerce-mcp/dist/services",
+ ),
+ { recursive: true },
+ );
+ fs.writeFileSync(
+ path.join(root, "package.json"),
+ JSON.stringify({ type: "module" }),
+ );
+ fs.writeFileSync(
+ path.join(
+ root,
+ "local_servers/woocommerce-mcp/dist/services/pg-rest-router.js",
+ ),
+ routerSource,
+ );
+
+ return root;
+}
+
+async function makeRequest(port, { body, method, path: requestPath }) {
+ return await new Promise((resolve, reject) => {
+ const req = http.request(
+ {
+ hostname: "127.0.0.1",
+ port,
+ path: requestPath,
+ method,
+ headers: body
+ ? {
+ "content-type": "application/json",
+ "content-length": Buffer.byteLength(body),
+ }
+ : undefined,
+ },
+ (res) => {
+ const chunks = [];
+ res.on("data", (chunk) => chunks.push(chunk));
+ res.on("end", () => {
+ resolve({
+ body: Buffer.concat(chunks).toString("utf8"),
+ statusCode: res.statusCode,
+ });
+ });
+ },
+ );
+
+ req.on("error", reject);
+
+ if (body) {
+ req.write(body);
+ }
+
+ req.end();
+ });
+}
+
+async function startServerWithEnv(root) {
+ const previousEnv = {
+ TOOLATHLON_GYM_ROOT: process.env.TOOLATHLON_GYM_ROOT,
+ WOOCOMMERCE_SHIM_HOST: process.env.WOOCOMMERCE_SHIM_HOST,
+ WOOCOMMERCE_SHIM_PORT: process.env.WOOCOMMERCE_SHIM_PORT,
+ };
+
+ const restoreEnv = () => {
+ for (const [key, value] of Object.entries(previousEnv)) {
+ if (value === undefined) {
+ delete process.env[key];
+ continue;
+ }
+
+ process.env[key] = value;
+ }
+ };
+
+ process.env.TOOLATHLON_GYM_ROOT = root;
+ process.env.WOOCOMMERCE_SHIM_HOST = "127.0.0.1";
+ process.env.WOOCOMMERCE_SHIM_PORT = "0";
+
+ let server;
+ try {
+ ({ server } = await serverModule.createWooCommerceShimServer());
+
+ await new Promise((resolve) => {
+ server.listen(0, "127.0.0.1", resolve);
+ });
+
+ const address = server.address();
+ assert.ok(address && typeof address !== "string");
+
+ return {
+ port: address.port,
+ restore() {
+ restoreEnv();
+ },
+ server,
+ };
+ } catch (error) {
+ restoreEnv();
+ await new Promise((resolve) => {
+ server?.close(resolve);
+ });
+ throw error;
+ }
+}
+
+function withEnv(overrides) {
+ const previousEnv = {};
+
+ for (const [key, value] of Object.entries(overrides)) {
+ previousEnv[key] = process.env[key];
+
+ if (value === undefined) {
+ delete process.env[key];
+ continue;
+ }
+
+ process.env[key] = value;
+ }
+
+ return () => {
+ for (const [key, value] of Object.entries(previousEnv)) {
+ if (value === undefined) {
+ delete process.env[key];
+ continue;
+ }
+
+ process.env[key] = value;
+ }
+ };
+}
+
+test("forwards WooCommerce POST requests to the router", async () => {
+ const root = createGymRoot(`
+ export class PgRestRouter {
+ async post(path, data, options) {
+ return {
+ status: 201,
+ data: { data, options, path },
+ };
+ }
+ }
+ `);
+
+ const runtime = await startServerWithEnv(root);
+
+ try {
+ const response = await makeRequest(runtime.port, {
+ method: "POST",
+ path: "/wp-json/wc/v3/orders?status=pending&status=paid",
+ body: JSON.stringify({ id: 42 }),
+ });
+
+ assert.equal(response.statusCode, 201);
+ assert.deepEqual(JSON.parse(response.body), {
+ data: { id: 42 },
+ options: { params: { status: ["pending", "paid"] } },
+ path: "orders",
+ });
+ } finally {
+ runtime.restore();
+ await new Promise((resolve) => {
+ runtime.server.close(resolve);
+ });
+ }
+});
+
+test("rejects malformed WooCommerce JSON payloads with 400", async () => {
+ const root = createGymRoot(`
+ export class PgRestRouter {
+ async post() {
+ return {
+ status: 201,
+ data: { ok: true },
+ };
+ }
+ }
+ `);
+
+ const runtime = await startServerWithEnv(root);
+
+ try {
+ const response = await makeRequest(runtime.port, {
+ method: "POST",
+ path: "/wp-json/wc/v3/orders",
+ body: "{",
+ });
+
+ assert.equal(response.statusCode, 400);
+ assert.deepEqual(JSON.parse(response.body), { error: "invalid_json" });
+ } finally {
+ runtime.restore();
+ await new Promise((resolve) => {
+ runtime.server.close(resolve);
+ });
+ }
+});
+
+test("maps downstream SyntaxError failures to opaque 500 responses", async () => {
+ const root = createGymRoot(`
+ export class PgRestRouter {
+ async get() {
+ throw new SyntaxError("boom: secret detail");
+ }
+ }
+ `);
+
+ const runtime = await startServerWithEnv(root);
+ const consoleErrors = [];
+ const originalConsoleError = console.error;
+ console.error = (...args) => {
+ consoleErrors.push(args);
+ };
+
+ try {
+ const response = await makeRequest(runtime.port, {
+ method: "GET",
+ path: "/wp-json/wc/v3/orders",
+ });
+
+ assert.equal(response.statusCode, 500);
+ assert.deepEqual(JSON.parse(response.body), { error: "internal_error" });
+ assert.equal(consoleErrors.length > 0, true);
+ assert.match(String(consoleErrors[0][0]), /boom: secret detail/);
+ } finally {
+ console.error = originalConsoleError;
+ runtime.restore();
+ await new Promise((resolve) => {
+ runtime.server.close(resolve);
+ });
+ }
+});
+
+test("rejects when the shim server fails to bind", async () => {
+ const root = createGymRoot(`
+ globalThis.__wooCleanupStats = { endCalls: 0 };
+
+ export class PgRestRouter {
+ constructor() {
+ this.pool = {
+ end: async () => {
+ globalThis.__wooCleanupStats.endCalls += 1;
+ },
+ };
+ }
+ }
+ `);
+ const restoreEnv = withEnv({
+ TOOLATHLON_GYM_ROOT: root,
+ WOOCOMMERCE_SHIM_HOST: "127.0.0.1",
+ WOOCOMMERCE_SHIM_PORT: "38082",
+ });
+ const originalCreateServer = http.createServer;
+ let closed = 0;
+
+ http.createServer = () => {
+ const server = new EventEmitter();
+ server.close = (callback) => {
+ closed += 1;
+ callback?.();
+ };
+ server.listen = () => {
+ process.nextTick(() => {
+ if (server.listenerCount("error") > 0) {
+ const error = new Error("bind failed");
+ error.code = "EADDRINUSE";
+ server.emit("error", error);
+ }
+ });
+ return server;
+ };
+ return server;
+ };
+
+ try {
+ const startPromise = Promise.race([
+ serverModule.startWooCommerceShimServer(),
+ new Promise((_, reject) => {
+ setTimeout(() => {
+ reject(new Error("timed out waiting for start failure"));
+ }, 200);
+ }),
+ ]);
+
+ await assert.rejects(startPromise, { code: "EADDRINUSE" });
+ await assert.rejects(startPromise, { code: "EADDRINUSE" });
+ } finally {
+ http.createServer = originalCreateServer;
+ restoreEnv();
+ }
+
+ assert.equal(closed, 1);
+ assert.equal(globalThis.__wooCleanupStats.endCalls, 1);
+ delete globalThis.__wooCleanupStats;
+});
+
+test("does not leak derived PG env across repeated server creation", async () => {
+ const restoreEnv = withEnv({
+ PG_HOST: undefined,
+ PG_PORT: undefined,
+ PG_DATABASE: undefined,
+ PG_USER: undefined,
+ PG_PASSWORD: undefined,
+ WOOCOMMERCE_SHIM_HOST: "127.0.0.1",
+ WOOCOMMERCE_SHIM_PORT: "0",
+ });
+
+ try {
+ const root = createGymRoot(`
+ export class PgRestRouter {
+ constructor() {
+ this.snapshot = {
+ host: process.env.PG_HOST,
+ port: process.env.PG_PORT,
+ database: process.env.PG_DATABASE,
+ user: process.env.PG_USER,
+ password: process.env.PG_PASSWORD,
+ };
+ }
+ }
+ `);
+ let firstRuntime;
+ const restoreFirstEnv = withEnv({
+ TOOLATHLON_GYM_ROOT: root,
+ POSTGRES_HOST_PORT: "15432",
+ POSTGRES_DB: "first_db",
+ POSTGRES_USER: "first_user",
+ POSTGRES_PASSWORD: "first_password",
+ PG_USER: "explicit_first_user",
+ });
+ try {
+ firstRuntime = await serverModule.createWooCommerceShimServer();
+ } finally {
+ restoreFirstEnv();
+ }
+
+ let secondRuntime;
+ const restoreSecondEnv = withEnv({
+ TOOLATHLON_GYM_ROOT: root,
+ POSTGRES_HOST_PORT: "25432",
+ POSTGRES_DB: "second_db",
+ POSTGRES_USER: "second_user",
+ POSTGRES_PASSWORD: "second_password",
+ });
+ try {
+ secondRuntime = await serverModule.createWooCommerceShimServer();
+ } finally {
+ restoreSecondEnv();
+ }
+
+ assert.deepEqual(firstRuntime.router.snapshot, {
+ host: "127.0.0.1",
+ port: "15432",
+ database: "first_db",
+ user: "explicit_first_user",
+ password: "first_password",
+ });
+ assert.deepEqual(secondRuntime.router.snapshot, {
+ host: "127.0.0.1",
+ port: "25432",
+ database: "second_db",
+ user: "second_user",
+ password: "second_password",
+ });
+ assert.equal(process.env.PG_PORT, undefined);
+ assert.equal(process.env.PG_USER, undefined);
+ } finally {
+ restoreEnv();
+ }
+});
+
+test("serves /healthz without touching the router", async () => {
+ const root = createGymRoot(`
+ export class PgRestRouter {
+ async get() {
+ throw new Error("router should not be called");
+ }
+ }
+ `);
+
+ const runtime = await startServerWithEnv(root);
+
+ try {
+ const response = await makeRequest(runtime.port, {
+ method: "GET",
+ path: "/healthz",
+ });
+
+ assert.equal(response.statusCode, 200);
+ assert.deepEqual(JSON.parse(response.body), { ok: true });
+ } finally {
+ runtime.restore();
+ await new Promise((resolve) => {
+ runtime.server.close(resolve);
+ });
+ }
+});
diff --git a/sandbox/server/backends/resources/mcp/mock_runtime/shims/woocommerce/strip_prefix.mjs b/sandbox/server/backends/resources/mcp/mock_runtime/shims/woocommerce/strip_prefix.mjs
new file mode 100644
index 00000000..72c0ab3a
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/mock_runtime/shims/woocommerce/strip_prefix.mjs
@@ -0,0 +1,9 @@
+const WOO_PREFIX = "/wp-json/wc/v3/";
+
+export function stripWooPrefix(pathname) {
+ if (!pathname.startsWith(WOO_PREFIX)) {
+ throw new Error(`WooCommerce REST prefix not found: ${pathname}`);
+ }
+
+ return pathname.slice(WOO_PREFIX.length);
+}
diff --git a/sandbox/server/backends/resources/mcp/mock_runtime/shims/woocommerce/strip_prefix.test.mjs b/sandbox/server/backends/resources/mcp/mock_runtime/shims/woocommerce/strip_prefix.test.mjs
new file mode 100644
index 00000000..44d8fc91
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/mock_runtime/shims/woocommerce/strip_prefix.test.mjs
@@ -0,0 +1,18 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+
+import { stripWooPrefix } from "./strip_prefix.mjs";
+
+test("strips the WooCommerce REST prefix from matching paths", () => {
+ assert.equal(
+ stripWooPrefix("/wp-json/wc/v3/orders/12"),
+ "orders/12",
+ );
+});
+
+test("rejects non-matching WooCommerce REST paths", () => {
+ assert.throws(
+ () => stripWooPrefix("/api/orders/12"),
+ /WooCommerce REST prefix/,
+ );
+});
diff --git a/sandbox/server/backends/resources/mcp/toolathlon_gym.py b/sandbox/server/backends/resources/mcp/toolathlon_gym.py
index 9a95baec..a365c589 100644
--- a/sandbox/server/backends/resources/mcp/toolathlon_gym.py
+++ b/sandbox/server/backends/resources/mcp/toolathlon_gym.py
@@ -22,6 +22,7 @@
from .client import MCPStdioClient, load_mcp_process_config
logger = logging.getLogger("MCPBackend")
+_BUNDLED_MCP_SERVERS_PATH = Path(__file__).resolve().parent / "vendor" / "local_servers"
class ToolathlonGymBackend(Backend):
@@ -235,16 +236,34 @@ async def _dispatch(
session_id=session_id,
)
- def _get_mcp_servers_path(self) -> str | None:
- """Return the configured path to MCP server executables, or None."""
+ def _get_mcp_servers_path(self) -> str:
+ """Return the configured path to MCP server executables."""
value = self.get_default_config().get("mcp_servers_path")
if value:
- return str(value)
+ explicit_path = Path(value)
+ if not explicit_path.is_dir():
+ raise RuntimeError(
+ "Configured mcp_servers_path does not exist or is not a directory: "
+ f"'{explicit_path}'"
+ )
+ return str(explicit_path)
# Backward compat: derive from toolathlon_root.
toolathlon_root = self.get_default_config().get("toolathlon_root")
if toolathlon_root:
- return str(Path(toolathlon_root) / "local_servers")
- return None
+ derived_path = Path(toolathlon_root) / "local_servers"
+ if not derived_path.is_dir():
+ raise RuntimeError(
+ "Configured toolathlon_root resolves to a missing or invalid "
+ f"local_servers directory: '{derived_path}'"
+ )
+ return str(derived_path)
+ if _BUNDLED_MCP_SERVERS_PATH.exists():
+ return str(_BUNDLED_MCP_SERVERS_PATH)
+ raise RuntimeError(
+ "Bundled MCP servers are missing at "
+ f"'{_BUNDLED_MCP_SERVERS_PATH}'. Vendor them into the package or "
+ "configure an explicit mcp_servers_path."
+ )
def _get_workspace_root(self) -> Path:
value = self.get_default_config().get("workspace_root") or "/tmp/agentflow_mcp"
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/.github/ISSUE_TEMPLATE/bug-report.yaml b/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/.github/ISSUE_TEMPLATE/bug-report.yaml
new file mode 100644
index 00000000..f05398a3
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/.github/ISSUE_TEMPLATE/bug-report.yaml
@@ -0,0 +1,30 @@
+name: Bug反馈
+description: 反馈使用遇到的问题。
+title: "[BUG] 请在标题中简短描述问题,便于查看。"
+labels: bug
+
+body:
+ - type: textarea
+ id: bug-description
+ attributes:
+ label: BUG描述
+ description: 尽量详细描述BUG问题。A clear and concise description of what the bug is.
+ placeholder: 尽量详细描述BUG问题。A clear and concise description of what the bug is.
+ validations:
+ required: true
+ - type: textarea
+ id: screenshots
+ attributes:
+ label: Screenshot截图
+ description: 尽量提供运行时报错或者结果错误的参数截图,包括调用参数。Please try to provide screenshots of the parameters that cause runtime errors or incorrect results, including the invocation parameters.
+ placeholder: 尽量提供运行时报错或者结果错误的参数截图,包括调用参数。Please try to provide screenshots of the parameters that cause runtime errors or incorrect results, including the invocation parameters.
+ validations:
+ required: true
+ - type: textarea
+ id: addtional-context
+ attributes:
+ label: Additional context其他信息
+ description: 其他信息。Add any other context about the problem here.
+ placeholder: 其他信息。Add any other context about the problem here.
+ validations:
+ required: true
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/.gitignore b/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/.gitignore
new file mode 100644
index 00000000..1170717c
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/.gitignore
@@ -0,0 +1,136 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+.pnpm-debug.log*
+
+# Diagnostic reports (https://nodejs.org/api/report.html)
+report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+*.lcov
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# Snowpack dependency directory (https://snowpack.dev/)
+web_modules/
+
+# TypeScript cache
+*.tsbuildinfo
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Optional stylelint cache
+.stylelintcache
+
+# Microbundle cache
+.rpt2_cache/
+.rts2_cache_cjs/
+.rts2_cache_es/
+.rts2_cache_umd/
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variable files
+.env
+.env.development.local
+.env.test.local
+.env.production.local
+.env.local
+
+# parcel-bundler cache (https://parceljs.org/)
+.cache
+.parcel-cache
+
+# Next.js build output
+.next
+out
+
+# Nuxt.js build / generate output
+.nuxt
+dist
+
+# Gatsby files
+.cache/
+# Comment in the public line in if your project uses Gatsby and not Next.js
+# https://nextjs.org/blog/next-9-1#public-directory-support
+# public
+
+# vuepress build output
+.vuepress/dist
+
+# vuepress v2.x temp and cache directory
+.temp
+.cache
+
+# vitepress build output
+**/.vitepress/dist
+
+# vitepress cache directory
+**/.vitepress/cache
+
+# Docusaurus cache and generated files
+.docusaurus
+
+# Serverless directories
+.serverless/
+
+# FuseBox cache
+.fusebox/
+
+# DynamoDB Local files
+.dynamodb/
+
+# TernJS port file
+.tern-port
+
+# Stores VSCode versions used for testing VSCode extensions
+.vscode-test
+
+# yarn v2
+.yarn/cache
+.yarn/unplugged
+.yarn/build-state.yml
+.yarn/install-state.gz
+.pnp.*
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/.prettierrc b/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/.prettierrc
new file mode 100644
index 00000000..35895267
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/.prettierrc
@@ -0,0 +1,11 @@
+{
+ "semi": true,
+ "singleQuote": true,
+ "tabWidth": 4,
+ "useTabs": false,
+ "trailingComma": "es5",
+ "printWidth": 80,
+ "bracketSpacing": true,
+ "arrowParens": "always",
+ "endOfLine": "auto"
+ }
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/Dockerfile b/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/Dockerfile
new file mode 100644
index 00000000..8a26789e
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/Dockerfile
@@ -0,0 +1,3 @@
+FROM node:lts-alpine
+WORKDIR /mcp
+RUN npm install 12306-mcp
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/LICENSE b/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/LICENSE
new file mode 100644
index 00000000..f95efca1
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2025 Jok
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/README.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/README.md
new file mode 100644
index 00000000..77fc9a50
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/README.md
@@ -0,0 +1,116 @@
+#
12306-mcp
+
+
+
+[](https://github.com/Joooook)
+[](https://space.bilibili.com/3546386788255839)
+
+
+
+
+
+
+A 12306 ticket search server based on the Model Context Protocol (MCP). The server provides a simple API interface that allows users to search for 12306 tickets.
+
+基于 Model Context Protocol (MCP) 的12306购票搜索服务器。提供了简单的API接口,允许大模型利用接口搜索12306购票信息。
+
+## 🚩Features
+
+
+| 功能描述 | 状态 |
+|------------------------------|--------|
+| 查询12306购票信息 | ✅ 已完成 |
+| 过滤列车信息 | ✅ 已完成 |
+| 过站查询 | ✅ 已完成 |
+| 中转查询 | ✅ 已完成 |
+| 其余接口,欢迎提feature | 🚧 计划内 |
+
+
+
+
+
+
+
+
+
+## ⚙️Installation
+
+~~~bash
+git clone https://github.com/Joooook/12306-mcp.git
+npm i
+~~~
+
+
+## ▶️Quick Start
+
+### CLI-stdio
+~~~bash
+npx -y 12306-mcp
+~~~
+
+### CLI-http
+~~~bash
+npx -y 12306-mcp --port [端口号]
+~~~
+
+### MCP sever configuration
+
+~~~json
+{
+ "mcpServers": {
+ "12306-mcp": {
+ "command": "npx",
+ "args": [
+ "-y",
+ "12306-mcp"
+ ]
+ }
+ }
+}
+
+~~~
+
+### Docker-stdio
+~~~bash
+docker build . -t 12306-mcp
+docker run --rm -it 12306-mcp npx 12306-mcp
+~~~
+
+### Docker-http
+~~~bash
+docker build . -t 12306-mcp
+docker run -p [your_port]:8080 -d 12306-mcp npx 12306-mcp --port 8080
+~~~
+
+
+
+## 📚Documentation
+
+- [服务原理详解](./docs/principle.md) 12306-MCP服务的工作原理
+- [架构图](./docs/architecture.md) 12306-MCP服务的架构图
+ 
+
+## 👉️Reference
+- [modelcontextprotocol/modelcontextprotocol](https://github.com/modelcontextprotocol/modelcontextprotocol)
+- [modelcontextprotocol/typescript-sdk](https://github.com/modelcontextprotocol/typescript-sdk)
+
+## 💭Murmurs
+本项目仅用于学习,欢迎催更。
+
+## 🎫Badges
+
+
+
+
+
+[](https://mseep.ai/app/joooook-12306-mcp)
+
+
+
+## ☕️Donate
+请我喝杯奶茶吧。
+
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/docs/architecture.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/docs/architecture.md
new file mode 100644
index 00000000..42da4c4e
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/docs/architecture.md
@@ -0,0 +1,67 @@
+# 12306-MCP 服务架构
+
+## 整体架构
+
+
+
+
+```mermaid
+graph TB
+ subgraph "用户层"
+ User["用户"]
+ LLM["大语言模型"]
+ end
+
+ subgraph "MCP 服务层"
+ McpServer["MCP Server"]
+
+ subgraph "基础工具层"
+ GetDate["get-current-date"]
+ GetCityStations["get-stations-code-in-city"]
+ GetCityCodes["get-station-code-of-citys"]
+ GetStationNames["get-station-code-by-names"]
+ GetStationInfo["get-station-by-telecode"]
+ end
+
+ subgraph "核心工具层"
+ GetTickets["get-tickets"]
+ GetInterline["get-interline-tickets"]
+ GetRoute["get-train-route-stations"]
+ end
+ end
+
+ subgraph "数据层"
+ Stations["STATIONS (车站id→车站信息)"]
+ CityStationsMap["CITY_STATIONS (城市名→车站id(可能一个城市多个站))"]
+ CityCodes["CITY_CODES (车站名(与城市名相同,只会有一个) →车站id)"]
+ NameStations["NAME_STATIONS (车站名→车站id)"]
+ end
+
+ subgraph "外部服务"
+ Api12306["12306 API"]
+ end
+
+ User --> LLM
+ LLM <--> McpServer
+
+ GetDate -.-> GetTickets
+ GetDate -.-> GetInterline
+ GetDate -.-> GetRoute
+ GetCityCodes -.-> GetTickets
+ GetCityCodes -.-> GetInterline
+ GetStationNames -.-> GetTickets
+ GetStationNames -.-> GetInterline
+ GetStationNames -.-> GetRoute
+ GetTickets -.-> GetRoute
+
+
+ GetTickets --> Api12306
+ GetInterline --> Api12306
+ GetRoute --> Api12306
+
+ GetCityStations --> CityStationsMap
+ GetCityCodes --> CityCodes
+ GetStationNames --> NameStations
+ GetStationInfo --> Stations
+```
+
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/docs/architecture.png b/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/docs/architecture.png
new file mode 100644
index 00000000..bb172276
Binary files /dev/null and b/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/docs/architecture.png differ
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/docs/principle.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/docs/principle.md
new file mode 100644
index 00000000..71aa6db5
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/docs/principle.md
@@ -0,0 +1,164 @@
+# 12306-MCP 服务 原理
+
+## 1. 启动初始化
+
+### 1.1 车站数据加载
+
+服务启动时通过 `getStations()` 函数从 12306 API 获取全国车站信息,构建四个核心索引:
+
+**具体流程:**
+1. 访问 12306 首页 (https://www.12306.cn/index/)
+2. 从 HTML 中提取车站名称 JS 文件路径
+3. 下载并解析 JS 文件,获取原始车站数据
+4. 补充缺失的车站信息 (MISSING_STATIONS)
+5. 基于车站数据得到四个核心数据结构映射表
+
+```typescript
+// 1. 车站id -> 车站信息
+STATIONS: Record
+
+// "AAA": {
+// "station_id": "@aaa",
+// "station_name": "北京北",
+// "station_code": "AAA",
+// "station_pinyin": "beijingbei",
+// "station_short": "aaa",
+// "station_index": "0",
+// "code": "1234",
+// "city": "北京",
+// "r1": "",
+// "r2": ""
+// }
+
+
+// 2. 城市名 -> 车站id 和 站名 (可能一个城市多个站)
+CITY_STATIONS: Record
+
+// "北京": [{"station_code": "AAA","station_name": "北京北"},{"station_code": "BBB","station_name": "京东"},...]
+
+// 3. 车站名(与城市名相同,只会有一个) -> 车站id 和 站名
+CITY_CODES: Record
+
+// "北京":{"station_code":"CCC","station_name":"北京"}
+
+// 4. 车站名 -> 车站id 和 站名
+NAME_STATIONS: Record
+
+// "北京北":{"station_code":"AAA","station_name":"北京北"}
+
+```
+
+## 2. MCP tools
+
+### 2.1 基础tool
+
+- **`get-current-date`**: 获取上海时区当前日期
+ - 返回当前上海时区的时间日期字符串("yyyy-MM-dd")
+ - 为其他工具提供准确的查询日期基准
+
+- **`get-stations-code-in-city`**: 查询城市内所有车站(使用 `CITY_STATIONS`)
+ - 输入:中文城市名
+ - 查找:`CITY_STATIONS[city]` 获取该城市所有车站列表
+ - 返回:包含 `station_code` 和 `station_name` 的数组
+
+- **`get-station-code-of-citys`**: 获取城市代表车站id(使用 `CITY_CODES`)
+ - 输入:城市名(支持 "|" 分隔的多个城市)
+ - 查找:`CITY_CODES[city]` 获取与城市同名的主要车站
+ - 返回:每个城市对应的代表车站信息
+
+- **`get-station-code-by-names`**: 车站名转车站id(使用 `NAME_STATIONS`)
+ - 输入:具体车站名(支持 "|" 分隔的多个车站)
+ - 查找:`NAME_STATIONS[stationName]` 精确匹配车站名
+ - 返回:车站id和车站名
+
+- **`get-station-by-telecode`**: 车站id查车站信息(使用 `STATIONS`)
+ - 输入:车站id
+ - 查找:`STATIONS[telecode]` 获取完整车站信息
+ - 返回:包含拼音、城市等详细信息
+
+### 2.2 核心tool (输入可通过基础tool获取)
+
+- **`get-tickets`**: 查询12306余票信息
+ - 输入:出发日期、出发站id、到达站id、车次类型筛选
+ - 参数处理:检查日期不早于当前日期,验证车站id存在性, 构造请求入参
+ - Cookie 处理:先获取 12306 Cookie 用于身份验证
+ - API 调用:访问 `/otn/leftTicket/query` 接口
+ - 数据处理, 车次类型筛选
+ - 返回格式化数据
+
+- **`get-interline-tickets`**: 中转换乘查询,支持指定中转站
+ - 输入:出发站id、到达站id、中转站id、是否显示无座、车次类型筛选
+ - 参数处理:检查日期不早于当前日期,验证车站id存在性, 构造请求入参
+ - Cookie 处理:先获取 12306 Cookie 用于身份验证
+ - API 调用:访问 `/lcquery/queryU` 接口
+ - 数据处理, 车次类型筛选
+ - 返回格式化数据
+
+- **`get-train-route-stations`**: 列车经停站查询
+ - 输入:车次编码(可以调用get-tickets获取)、出发站id、到达站id、出发日期
+ - 参数处理:检查日期不早于当前日期,验证车站id存在性, 构造请求入参
+ - Cookie 处理:先获取 12306 Cookie 用于身份验证
+ - API 调用:访问 `/otn/czxx/queryByTrainNo` 接口
+ - 返回格式化数据
+
+## 3. 数据流程与工具关系
+
+### 3.1 车票查询流程
+
+```
+用户查询 "后天北京到上海的高铁" - 大模型调用流程:
+ ↓
+1. get-current-date() → "2024-01-15" (获取当前日期)
+2. 大模型理解后天日期 → "2024-01-17"
+3. get-station-code-of-citys("北京|上海") → {"北京": {"station_code": "BJP","station_name": "北京"}, "上海": {"station_code": "SHH","station_name": "上海"}}
+ ↓
+4. get-tickets(date: "2024-01-17", fromStation: "BJP", toStation: "SHH", trainFilterFlags: "G")
+ ↓
+5. 内部数据处理(参数验证, Cookie获取, API调用, 格式化输出文本)
+ ↓
+6. 返回格式化的高铁车次信息(车次、时间、价格、余票等)
+```
+
+### 3.2 中转查询流程
+
+```
+用户查询 "深圳到拉萨,经过西安中转"
+ ↓
+1. 获取三个城市的车站id
+2. get-interline-tickets(from: "SZQ", to: "LSO", transfer: "XAY")
+ ↓
+3. 内部数据处理(参数验证, Cookie获取, API调用, 格式化输出文本)
+ ↓
+4. 返回中转方案(第一程 + 第二程)
+```
+
+### 3.3 经停站查询流程
+
+```
+用户查询 "G1次列车经停哪些站"
+ ↓
+1. get-train-route-stations(trainNo: "G1", from: "BJP", to: "SHH")
+ ↓
+2. 数据处理:parseRouteStationsData() → parseRouteStationsInfo()
+ ↓
+3. 返回经停站列表(站名、到达时间、出发时间、停留时间)
+```
+
+### 3.4 工具依赖关系
+
+```
+基础工具层:
+├── get-current-date
+├── get-stations-code-in-city
+└── get-station-code-of-citys
+└── get-station-code-by-names
+└── get-station-by-telecode
+
+ ↓ 为核心工具层提供基础数据
+
+核心工具层:
+├── get-tickets (依赖车站id)
+├── get-interline-tickets (依赖车站id)
+└── get-train-route-stations (依赖车站id和车次号)
+```
+
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/glama.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/glama.json
new file mode 100644
index 00000000..e88e8ede
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/glama.json
@@ -0,0 +1,6 @@
+{
+ "$schema": "https://glama.ai/mcp/schemas/server.json",
+ "maintainers": [
+ "Joooook"
+ ]
+}
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/package-lock.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/package-lock.json
new file mode 100644
index 00000000..0c8add91
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/package-lock.json
@@ -0,0 +1,3301 @@
+{
+ "name": "12306-mcp",
+ "version": "0.3.5",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "12306-mcp",
+ "version": "0.3.5",
+ "license": "MIT",
+ "dependencies": {
+ "@modelcontextprotocol/inspector": "^0.16.5",
+ "@modelcontextprotocol/sdk": "^1.9.0",
+ "@types/pg": "^8.18.0",
+ "axios": "^1.8.4",
+ "commander": "^14.0.0",
+ "date-fns": "^4.1.0",
+ "date-fns-tz": "^3.2.0",
+ "mcp-http-server": "^1.1.5",
+ "pg": "^8.20.0",
+ "zod": "^3.24.2"
+ },
+ "bin": {
+ "12306-mcp": "build/index.js"
+ },
+ "devDependencies": {
+ "@types/node": "^22.14.1",
+ "run-script-os": "^1.1.6",
+ "typescript": "^5.8.3"
+ }
+ },
+ "node_modules/@cspotcode/source-map-support": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
+ "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "0.3.9"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@floating-ui/core": {
+ "version": "1.7.3",
+ "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz",
+ "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/utils": "^0.2.10"
+ }
+ },
+ "node_modules/@floating-ui/dom": {
+ "version": "1.7.4",
+ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz",
+ "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/core": "^1.7.3",
+ "@floating-ui/utils": "^0.2.10"
+ }
+ },
+ "node_modules/@floating-ui/react-dom": {
+ "version": "2.1.6",
+ "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz",
+ "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/dom": "^1.7.4"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
+ }
+ },
+ "node_modules/@floating-ui/utils": {
+ "version": "0.2.10",
+ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
+ "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.9",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
+ "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.0.3",
+ "@jridgewell/sourcemap-codec": "^1.4.10"
+ }
+ },
+ "node_modules/@modelcontextprotocol/inspector": {
+ "version": "0.16.5",
+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/inspector/-/inspector-0.16.5.tgz",
+ "integrity": "sha512-3viCtcLn1p6ZYZG8L9DxnNx2XUw6damCZM37nBByKAd74stH7nZa2VxFAwLpUCjKylYqawCRthzYSyeSlWGf1g==",
+ "license": "MIT",
+ "workspaces": [
+ "client",
+ "server",
+ "cli"
+ ],
+ "dependencies": {
+ "@modelcontextprotocol/inspector-cli": "^0.16.5",
+ "@modelcontextprotocol/inspector-client": "^0.16.5",
+ "@modelcontextprotocol/inspector-server": "^0.16.5",
+ "@modelcontextprotocol/sdk": "^1.17.3",
+ "concurrently": "^9.2.0",
+ "open": "^10.2.0",
+ "shell-quote": "^1.8.3",
+ "spawn-rx": "^5.1.2",
+ "ts-node": "^10.9.2",
+ "zod": "^3.25.76"
+ },
+ "bin": {
+ "mcp-inspector": "cli/build/cli.js"
+ },
+ "engines": {
+ "node": ">=22.7.5"
+ }
+ },
+ "node_modules/@modelcontextprotocol/inspector-cli": {
+ "version": "0.16.5",
+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/inspector-cli/-/inspector-cli-0.16.5.tgz",
+ "integrity": "sha512-6Flp9goLJutjUZTx6clDo4x/6TA7BwqeTGSYcC8ZeP8IEKjx+EZpvpYPVAV++rkHJYa0F5PViy02CMoGBGkzkg==",
+ "license": "MIT",
+ "dependencies": {
+ "@modelcontextprotocol/sdk": "^1.17.3",
+ "commander": "^13.1.0",
+ "spawn-rx": "^5.1.2"
+ },
+ "bin": {
+ "mcp-inspector-cli": "build/cli.js"
+ }
+ },
+ "node_modules/@modelcontextprotocol/inspector-cli/node_modules/commander": {
+ "version": "13.1.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz",
+ "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@modelcontextprotocol/inspector-client": {
+ "version": "0.16.5",
+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/inspector-client/-/inspector-client-0.16.5.tgz",
+ "integrity": "sha512-KjgtTRdFSDt964a9KtmF3aRpi4ntd+6wn3e4WUa5vcK7H8f34rq4wfIGF22dd/xoKbALP3zVVAsPqVN94SCGmg==",
+ "license": "MIT",
+ "dependencies": {
+ "@modelcontextprotocol/sdk": "^1.17.3",
+ "@radix-ui/react-checkbox": "^1.1.4",
+ "@radix-ui/react-dialog": "^1.1.3",
+ "@radix-ui/react-icons": "^1.3.0",
+ "@radix-ui/react-label": "^2.1.0",
+ "@radix-ui/react-popover": "^1.1.3",
+ "@radix-ui/react-select": "^2.1.2",
+ "@radix-ui/react-slot": "^1.1.0",
+ "@radix-ui/react-tabs": "^1.1.1",
+ "@radix-ui/react-toast": "^1.2.6",
+ "@radix-ui/react-tooltip": "^1.1.8",
+ "ajv": "^6.12.6",
+ "class-variance-authority": "^0.7.0",
+ "clsx": "^2.1.1",
+ "cmdk": "^1.0.4",
+ "lucide-react": "^0.523.0",
+ "pkce-challenge": "^4.1.0",
+ "prismjs": "^1.30.0",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "react-simple-code-editor": "^0.14.1",
+ "serve-handler": "^6.1.6",
+ "tailwind-merge": "^2.5.3",
+ "tailwindcss-animate": "^1.0.7",
+ "zod": "^3.25.76"
+ },
+ "bin": {
+ "mcp-inspector-client": "bin/start.js"
+ }
+ },
+ "node_modules/@modelcontextprotocol/inspector-client/node_modules/pkce-challenge": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-4.1.0.tgz",
+ "integrity": "sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@modelcontextprotocol/inspector-server": {
+ "version": "0.16.5",
+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/inspector-server/-/inspector-server-0.16.5.tgz",
+ "integrity": "sha512-mWKrEpimfNdSFOxMJGj3cYN1PxHANW9vjpek+tR0TLiP7ogq7DLV4bbsa+lPPGwOVogUIyUiXbffY8M1YHRZVA==",
+ "license": "MIT",
+ "dependencies": {
+ "@modelcontextprotocol/sdk": "^1.17.3",
+ "cors": "^2.8.5",
+ "express": "^5.1.0",
+ "ws": "^8.18.0",
+ "zod": "^3.25.76"
+ },
+ "bin": {
+ "mcp-inspector-server": "build/index.js"
+ }
+ },
+ "node_modules/@modelcontextprotocol/sdk": {
+ "version": "1.17.3",
+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.17.3.tgz",
+ "integrity": "sha512-JPwUKWSsbzx+DLFznf/QZ32Qa+ptfbUlHhRLrBQBAFu9iI1iYvizM4p+zhhRDceSsPutXp4z+R/HPVphlIiclg==",
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^6.12.6",
+ "content-type": "^1.0.5",
+ "cors": "^2.8.5",
+ "cross-spawn": "^7.0.5",
+ "eventsource": "^3.0.2",
+ "eventsource-parser": "^3.0.0",
+ "express": "^5.0.1",
+ "express-rate-limit": "^7.5.0",
+ "pkce-challenge": "^5.0.0",
+ "raw-body": "^3.0.0",
+ "zod": "^3.23.8",
+ "zod-to-json-schema": "^3.24.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@radix-ui/number": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
+ "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==",
+ "license": "MIT"
+ },
+ "node_modules/@radix-ui/primitive": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
+ "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
+ "license": "MIT"
+ },
+ "node_modules/@radix-ui/react-arrow": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz",
+ "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-checkbox": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz",
+ "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-previous": "1.1.1",
+ "@radix-ui/react-use-size": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-collection": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
+ "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-compose-refs": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
+ "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-context": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
+ "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dialog": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz",
+ "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-focus-guards": "1.1.3",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.6.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-direction": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
+ "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dismissable-layer": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz",
+ "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-escape-keydown": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-focus-guards": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz",
+ "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-focus-scope": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz",
+ "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-icons": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.2.tgz",
+ "integrity": "sha512-fyQIhGDhzfc9pK2kH6Pl9c4BDJGfMkPqkyIgYDthyNYoNg3wVhoJMMh19WS4Up/1KMPFVpNsT2q3WmXn2N1m6g==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.x || ^17.x || ^18.x || ^19.0.0 || ^19.0.0-rc"
+ }
+ },
+ "node_modules/@radix-ui/react-id": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
+ "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-label": {
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz",
+ "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popover": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz",
+ "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-focus-guards": "1.1.3",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-popper": "1.2.8",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.6.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popper": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz",
+ "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/react-dom": "^2.0.0",
+ "@radix-ui/react-arrow": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-layout-effect": "1.1.1",
+ "@radix-ui/react-use-rect": "1.1.1",
+ "@radix-ui/react-use-size": "1.1.1",
+ "@radix-ui/rect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-portal": {
+ "version": "1.1.9",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz",
+ "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-presence": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
+ "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-roving-focus": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz",
+ "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-select": {
+ "version": "2.2.6",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz",
+ "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/number": "1.1.1",
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-focus-guards": "1.1.3",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-popper": "1.2.8",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1",
+ "@radix-ui/react-use-previous": "1.1.1",
+ "@radix-ui/react-visually-hidden": "1.2.3",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.6.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-tabs": {
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz",
+ "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-roving-focus": "1.1.11",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-toast": {
+ "version": "1.2.15",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.15.tgz",
+ "integrity": "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1",
+ "@radix-ui/react-visually-hidden": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-tooltip": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz",
+ "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-popper": "1.2.8",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-visually-hidden": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-callback-ref": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
+ "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-controllable-state": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
+ "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-effect-event": "0.0.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-effect-event": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz",
+ "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-escape-keydown": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz",
+ "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-callback-ref": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-layout-effect": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
+ "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-previous": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz",
+ "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-rect": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz",
+ "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/rect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-size": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz",
+ "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-visually-hidden": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz",
+ "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/rect": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz",
+ "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
+ "license": "MIT"
+ },
+ "node_modules/@tsconfig/node10": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",
+ "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==",
+ "license": "MIT"
+ },
+ "node_modules/@tsconfig/node12": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
+ "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
+ "license": "MIT"
+ },
+ "node_modules/@tsconfig/node14": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
+ "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
+ "license": "MIT"
+ },
+ "node_modules/@tsconfig/node16": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
+ "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "22.14.1",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz",
+ "integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==",
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
+ "node_modules/@types/pg": {
+ "version": "8.18.0",
+ "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.18.0.tgz",
+ "integrity": "sha512-gT+oueVQkqnj6ajGJXblFR4iavIXWsGAFCk3dP4Kki5+a9R4NMt0JARdk6s8cUKcfUoqP5dAtDSLU8xYUTFV+Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "pg-protocol": "*",
+ "pg-types": "^2.2.0"
+ }
+ },
+ "node_modules/accepts": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
+ "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
+ "dependencies": {
+ "mime-types": "^3.0.0",
+ "negotiator": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.15.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
+ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-walk": {
+ "version": "8.3.4",
+ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
+ "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
+ "license": "MIT",
+ "dependencies": {
+ "acorn": "^8.11.0"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/arg": {
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
+ "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
+ "license": "MIT"
+ },
+ "node_modules/aria-hidden": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz",
+ "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
+ },
+ "node_modules/axios": {
+ "version": "1.8.4",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz",
+ "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==",
+ "dependencies": {
+ "follow-redirects": "^1.15.6",
+ "form-data": "^4.0.0",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "license": "MIT"
+ },
+ "node_modules/body-parser": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
+ "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==",
+ "dependencies": {
+ "bytes": "^3.1.2",
+ "content-type": "^1.0.5",
+ "debug": "^4.4.0",
+ "http-errors": "^2.0.0",
+ "iconv-lite": "^0.6.3",
+ "on-finished": "^2.4.1",
+ "qs": "^6.14.0",
+ "raw-body": "^3.0.0",
+ "type-is": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/bundle-name": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz",
+ "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==",
+ "license": "MIT",
+ "dependencies": {
+ "run-applescript": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/chalk/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/class-variance-authority": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
+ "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "clsx": "^2.1.1"
+ },
+ "funding": {
+ "url": "https://polar.sh/cva"
+ }
+ },
+ "node_modules/cliui": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
+ "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/cmdk": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz",
+ "integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "^1.1.1",
+ "@radix-ui/react-dialog": "^1.1.6",
+ "@radix-ui/react-id": "^1.1.0",
+ "@radix-ui/react-primitive": "^2.0.2"
+ },
+ "peerDependencies": {
+ "react": "^18 || ^19 || ^19.0.0-rc",
+ "react-dom": "^18 || ^19 || ^19.0.0-rc"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "license": "MIT"
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/commander": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.0.tgz",
+ "integrity": "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "license": "MIT"
+ },
+ "node_modules/concurrently": {
+ "version": "9.2.0",
+ "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.0.tgz",
+ "integrity": "sha512-IsB/fiXTupmagMW4MNp2lx2cdSN2FfZq78vF90LBB+zZHArbIQZjQtzXCiXnvTxCZSvXanTqFLWBjw2UkLx1SQ==",
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^4.1.2",
+ "lodash": "^4.17.21",
+ "rxjs": "^7.8.1",
+ "shell-quote": "^1.8.1",
+ "supports-color": "^8.1.1",
+ "tree-kill": "^1.2.2",
+ "yargs": "^17.7.2"
+ },
+ "bin": {
+ "conc": "dist/bin/concurrently.js",
+ "concurrently": "dist/bin/concurrently.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
+ }
+ },
+ "node_modules/content-disposition": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz",
+ "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==",
+ "dependencies": {
+ "safe-buffer": "5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
+ "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
+ "engines": {
+ "node": ">=6.6.0"
+ }
+ },
+ "node_modules/cors": {
+ "version": "2.8.5",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
+ "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
+ "dependencies": {
+ "object-assign": "^4",
+ "vary": "^1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/create-require": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
+ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
+ "license": "MIT"
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/date-fns": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
+ "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/kossnocorp"
+ }
+ },
+ "node_modules/date-fns-tz": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-3.2.0.tgz",
+ "integrity": "sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "date-fns": "^3.0.0 || ^4.0.0"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
+ "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/default-browser": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz",
+ "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==",
+ "license": "MIT",
+ "dependencies": {
+ "bundle-name": "^4.1.0",
+ "default-browser-id": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/default-browser-id": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz",
+ "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/define-lazy-prop": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz",
+ "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/detect-node-es": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
+ "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
+ "license": "MIT"
+ },
+ "node_modules/diff": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
+ "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.3.1"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
+ },
+ "node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "license": "MIT"
+ },
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/eventsource": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.6.tgz",
+ "integrity": "sha512-l19WpE2m9hSuyP06+FbuUUf1G+R0SFLrtQfbRb9PRr+oimOfxQhgGCbVaXg5IvZyyTThJsxh6L/srkMiCeBPDA==",
+ "dependencies": {
+ "eventsource-parser": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/eventsource-parser": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.1.tgz",
+ "integrity": "sha512-VARTJ9CYeuQYb0pZEPbzi740OWFgpHe7AYJ2WFZVnUDUQp5Dk2yJUgF36YsZ81cOyxT0QxmXD2EQpapAouzWVA==",
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/express": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz",
+ "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
+ "dependencies": {
+ "accepts": "^2.0.0",
+ "body-parser": "^2.2.0",
+ "content-disposition": "^1.0.0",
+ "content-type": "^1.0.5",
+ "cookie": "^0.7.1",
+ "cookie-signature": "^1.2.1",
+ "debug": "^4.4.0",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "etag": "^1.8.1",
+ "finalhandler": "^2.1.0",
+ "fresh": "^2.0.0",
+ "http-errors": "^2.0.0",
+ "merge-descriptors": "^2.0.0",
+ "mime-types": "^3.0.0",
+ "on-finished": "^2.4.1",
+ "once": "^1.4.0",
+ "parseurl": "^1.3.3",
+ "proxy-addr": "^2.0.7",
+ "qs": "^6.14.0",
+ "range-parser": "^1.2.1",
+ "router": "^2.2.0",
+ "send": "^1.1.0",
+ "serve-static": "^2.2.0",
+ "statuses": "^2.0.1",
+ "type-is": "^2.0.1",
+ "vary": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/express-rate-limit": {
+ "version": "7.5.0",
+ "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz",
+ "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==",
+ "engines": {
+ "node": ">= 16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/express-rate-limit"
+ },
+ "peerDependencies": {
+ "express": "^4.11 || 5 || ^5.0.0-beta.1"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "license": "MIT"
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "license": "MIT"
+ },
+ "node_modules/finalhandler": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz",
+ "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==",
+ "dependencies": {
+ "debug": "^4.4.0",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "on-finished": "^2.4.1",
+ "parseurl": "^1.3.3",
+ "statuses": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/follow-redirects": {
+ "version": "1.15.9",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
+ "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
+ "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/form-data/node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/form-data/node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
+ "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "license": "ISC",
+ "engines": {
+ "node": "6.* || 8.* || >= 10.*"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-nonce": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
+ "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
+ "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
+ "dependencies": {
+ "depd": "2.0.0",
+ "inherits": "2.0.4",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "toidentifier": "1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/is-docker": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz",
+ "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==",
+ "license": "MIT",
+ "bin": {
+ "is-docker": "cli.js"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-inside-container": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz",
+ "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==",
+ "license": "MIT",
+ "dependencies": {
+ "is-docker": "^3.0.0"
+ },
+ "bin": {
+ "is-inside-container": "cli.js"
+ },
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-promise": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
+ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="
+ },
+ "node_modules/is-wsl": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz",
+ "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==",
+ "license": "MIT",
+ "dependencies": {
+ "is-inside-container": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "license": "MIT"
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "license": "MIT"
+ },
+ "node_modules/lodash": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+ "license": "MIT"
+ },
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
+ "node_modules/lucide-react": {
+ "version": "0.523.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.523.0.tgz",
+ "integrity": "sha512-rUjQoy7egZT9XYVXBK1je9ckBnNp7qzRZOhLQx5RcEp2dCGlXo+mv6vf7Am4LimEcFBJIIZzSGfgTqc9QCrPSw==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/make-error": {
+ "version": "1.3.6",
+ "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
+ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
+ "license": "ISC"
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/mcp-http-server": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/mcp-http-server/-/mcp-http-server-1.1.5.tgz",
+ "integrity": "sha512-bmKEo88wtJ88GSHrBd6zmjM19HU00c4FZ5YRt61nhHzpQjiryvRwmhNsKvbDwapBq94F7EePH9BJehBjXh17Bg==",
+ "license": "MIT",
+ "dependencies": {
+ "@modelcontextprotocol/sdk": "^1.11.2",
+ "express": "^5.1.0"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
+ "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
+ "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.54.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
+ "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
+ "dependencies": {
+ "mime-db": "^1.54.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+ },
+ "node_modules/negotiator": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
+ "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/open": {
+ "version": "10.2.0",
+ "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz",
+ "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==",
+ "license": "MIT",
+ "dependencies": {
+ "default-browser": "^5.2.1",
+ "define-lazy-prop": "^3.0.0",
+ "is-inside-container": "^1.0.0",
+ "wsl-utils": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/path-is-inside": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz",
+ "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==",
+ "license": "(WTFPL OR MIT)"
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "8.2.0",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz",
+ "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==",
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/pg": {
+ "version": "8.20.0",
+ "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz",
+ "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==",
+ "license": "MIT",
+ "dependencies": {
+ "pg-connection-string": "^2.12.0",
+ "pg-pool": "^3.13.0",
+ "pg-protocol": "^1.13.0",
+ "pg-types": "2.2.0",
+ "pgpass": "1.0.5"
+ },
+ "engines": {
+ "node": ">= 16.0.0"
+ },
+ "optionalDependencies": {
+ "pg-cloudflare": "^1.3.0"
+ },
+ "peerDependencies": {
+ "pg-native": ">=3.0.1"
+ },
+ "peerDependenciesMeta": {
+ "pg-native": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/pg-cloudflare": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz",
+ "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/pg-connection-string": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.12.0.tgz",
+ "integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==",
+ "license": "MIT"
+ },
+ "node_modules/pg-int8": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
+ "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/pg-pool": {
+ "version": "3.13.0",
+ "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz",
+ "integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "pg": ">=8.0"
+ }
+ },
+ "node_modules/pg-protocol": {
+ "version": "1.13.0",
+ "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz",
+ "integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==",
+ "license": "MIT"
+ },
+ "node_modules/pg-types": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
+ "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
+ "license": "MIT",
+ "dependencies": {
+ "pg-int8": "1.0.1",
+ "postgres-array": "~2.0.0",
+ "postgres-bytea": "~1.0.0",
+ "postgres-date": "~1.0.4",
+ "postgres-interval": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/pgpass": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
+ "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
+ "license": "MIT",
+ "dependencies": {
+ "split2": "^4.1.0"
+ }
+ },
+ "node_modules/pkce-challenge": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz",
+ "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==",
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/postgres-array": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
+ "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/postgres-bytea": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz",
+ "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/postgres-date": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
+ "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/postgres-interval": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
+ "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "xtend": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/prismjs": {
+ "version": "1.30.0",
+ "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz",
+ "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/qs": {
+ "version": "6.14.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
+ "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
+ "dependencies": {
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz",
+ "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.6.3",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/react": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
+ "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
+ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.23.2"
+ },
+ "peerDependencies": {
+ "react": "^18.3.1"
+ }
+ },
+ "node_modules/react-remove-scroll": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz",
+ "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==",
+ "license": "MIT",
+ "dependencies": {
+ "react-remove-scroll-bar": "^2.3.7",
+ "react-style-singleton": "^2.2.3",
+ "tslib": "^2.1.0",
+ "use-callback-ref": "^1.3.3",
+ "use-sidecar": "^1.1.3"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-remove-scroll-bar": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz",
+ "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==",
+ "license": "MIT",
+ "dependencies": {
+ "react-style-singleton": "^2.2.2",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-simple-code-editor": {
+ "version": "0.14.1",
+ "resolved": "https://registry.npmjs.org/react-simple-code-editor/-/react-simple-code-editor-0.14.1.tgz",
+ "integrity": "sha512-BR5DtNRy+AswWJECyA17qhUDvrrCZ6zXOCfkQY5zSmb96BVUbpVAv03WpcjcwtCwiLbIANx3gebHOcXYn1EHow==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
+ }
+ },
+ "node_modules/react-style-singleton": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
+ "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==",
+ "license": "MIT",
+ "dependencies": {
+ "get-nonce": "^1.0.0",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/require-directory": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/router": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
+ "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
+ "dependencies": {
+ "debug": "^4.4.0",
+ "depd": "^2.0.0",
+ "is-promise": "^4.0.0",
+ "parseurl": "^1.3.3",
+ "path-to-regexp": "^8.0.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/run-applescript": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz",
+ "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/run-script-os": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/run-script-os/-/run-script-os-1.1.6.tgz",
+ "integrity": "sha512-ql6P2LzhBTTDfzKts+Qo4H94VUKpxKDFz6QxxwaUZN0mwvi7L3lpOI7BqPCq7lgDh3XLl0dpeXwfcVIitlrYrw==",
+ "dev": true,
+ "bin": {
+ "run-os": "index.js",
+ "run-script-os": "index.js"
+ }
+ },
+ "node_modules/rxjs": {
+ "version": "7.8.2",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
+ "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
+ },
+ "node_modules/scheduler": {
+ "version": "0.23.2",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
+ "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ }
+ },
+ "node_modules/send": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz",
+ "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==",
+ "dependencies": {
+ "debug": "^4.3.5",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "etag": "^1.8.1",
+ "fresh": "^2.0.0",
+ "http-errors": "^2.0.0",
+ "mime-types": "^3.0.1",
+ "ms": "^2.1.3",
+ "on-finished": "^2.4.1",
+ "range-parser": "^1.2.1",
+ "statuses": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/serve-handler": {
+ "version": "6.1.6",
+ "resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.6.tgz",
+ "integrity": "sha512-x5RL9Y2p5+Sh3D38Fh9i/iQ5ZK+e4xuXRd/pGbM4D13tgo/MGwbttUk8emytcr1YYzBYs+apnUngBDFYfpjPuQ==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "3.0.0",
+ "content-disposition": "0.5.2",
+ "mime-types": "2.1.18",
+ "minimatch": "3.1.2",
+ "path-is-inside": "1.0.2",
+ "path-to-regexp": "3.3.0",
+ "range-parser": "1.2.0"
+ }
+ },
+ "node_modules/serve-handler/node_modules/bytes": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
+ "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/serve-handler/node_modules/content-disposition": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz",
+ "integrity": "sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/serve-handler/node_modules/mime-db": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz",
+ "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/serve-handler/node_modules/mime-types": {
+ "version": "2.1.18",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz",
+ "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "~1.33.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/serve-handler/node_modules/path-to-regexp": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz",
+ "integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==",
+ "license": "MIT"
+ },
+ "node_modules/serve-handler/node_modules/range-parser": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz",
+ "integrity": "sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/serve-static": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz",
+ "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==",
+ "dependencies": {
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "parseurl": "^1.3.3",
+ "send": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shell-quote": {
+ "version": "1.8.3",
+ "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz",
+ "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/spawn-rx": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/spawn-rx/-/spawn-rx-5.1.2.tgz",
+ "integrity": "sha512-/y7tJKALVZ1lPzeZZB9jYnmtrL7d0N2zkorii5a7r7dhHkWIuLTzZpZzMJLK1dmYRgX/NCc4iarTO3F7BS2c/A==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.3.7",
+ "rxjs": "^7.8.1"
+ }
+ },
+ "node_modules/split2": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
+ "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">= 10.x"
+ }
+ },
+ "node_modules/statuses": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
+ "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
+ }
+ },
+ "node_modules/tailwind-merge": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.0.tgz",
+ "integrity": "sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/dcastil"
+ }
+ },
+ "node_modules/tailwindcss": {
+ "version": "4.1.12",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.12.tgz",
+ "integrity": "sha512-DzFtxOi+7NsFf7DBtI3BJsynR+0Yp6etH+nRPTbpWnS2pZBaSksv/JGctNwSWzbFjp0vxSqknaUylseZqMDGrA==",
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/tailwindcss-animate": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz",
+ "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "tailwindcss": ">=3.0.0 || insiders"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/tree-kill": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
+ "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
+ "license": "MIT",
+ "bin": {
+ "tree-kill": "cli.js"
+ }
+ },
+ "node_modules/ts-node": {
+ "version": "10.9.2",
+ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
+ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@cspotcode/source-map-support": "^0.8.0",
+ "@tsconfig/node10": "^1.0.7",
+ "@tsconfig/node12": "^1.0.7",
+ "@tsconfig/node14": "^1.0.0",
+ "@tsconfig/node16": "^1.0.2",
+ "acorn": "^8.4.1",
+ "acorn-walk": "^8.1.1",
+ "arg": "^4.1.0",
+ "create-require": "^1.1.0",
+ "diff": "^4.0.1",
+ "make-error": "^1.1.1",
+ "v8-compile-cache-lib": "^3.0.1",
+ "yn": "3.1.1"
+ },
+ "bin": {
+ "ts-node": "dist/bin.js",
+ "ts-node-cwd": "dist/bin-cwd.js",
+ "ts-node-esm": "dist/bin-esm.js",
+ "ts-node-script": "dist/bin-script.js",
+ "ts-node-transpile-only": "dist/bin-transpile.js",
+ "ts-script": "dist/bin-script-deprecated.js"
+ },
+ "peerDependencies": {
+ "@swc/core": ">=1.2.50",
+ "@swc/wasm": ">=1.2.50",
+ "@types/node": "*",
+ "typescript": ">=2.7"
+ },
+ "peerDependenciesMeta": {
+ "@swc/core": {
+ "optional": true
+ },
+ "@swc/wasm": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
+ },
+ "node_modules/type-is": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
+ "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
+ "dependencies": {
+ "content-type": "^1.0.5",
+ "media-typer": "^1.1.0",
+ "mime-types": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.8.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
+ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/use-callback-ref": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz",
+ "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/use-sidecar": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz",
+ "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==",
+ "license": "MIT",
+ "dependencies": {
+ "detect-node-es": "^1.1.0",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/v8-compile-cache-lib": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
+ "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
+ "license": "MIT"
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
+ },
+ "node_modules/ws": {
+ "version": "8.18.3",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
+ "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/wsl-utils": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz",
+ "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==",
+ "license": "MIT",
+ "dependencies": {
+ "is-wsl": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/xtend": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
+ "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4"
+ }
+ },
+ "node_modules/y18n": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yargs": {
+ "version": "17.7.2",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
+ "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
+ "license": "MIT",
+ "dependencies": {
+ "cliui": "^8.0.1",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.3",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^21.1.1"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/yargs-parser": {
+ "version": "21.1.1",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+ "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/yn": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
+ "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/zod": {
+ "version": "3.25.76",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
+ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
+ "node_modules/zod-to-json-schema": {
+ "version": "3.24.5",
+ "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz",
+ "integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==",
+ "peerDependencies": {
+ "zod": "^3.24.1"
+ }
+ }
+ }
+}
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/package.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/package.json
new file mode 100644
index 00000000..00d45448
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/package.json
@@ -0,0 +1,50 @@
+{
+ "name": "12306-mcp",
+ "version": "0.3.5",
+ "main": "build/index.js",
+ "scripts": {
+ "prebuild": "run-script-os",
+ "prebuild:win32": "del /q /s build\\* >nul 2>&1",
+ "prebuild:default": "rm -rf build/*",
+ "build": "tsc",
+ "postbuild": "run-script-os",
+ "postbuild:darwin:linux": "chmod +x build/index.js",
+ "postbuild:default": "",
+ "prepare": "npm run build",
+ "test": "tsc && node ./build/index.js",
+ "test:http": "tsc && node ./build/index.js --port 8080",
+ "debug": "tsc && npx @modelcontextprotocol/inspector node ./build/index.js"
+ },
+ "keywords": [
+ "mcp",
+ "12306",
+ "mcp-server"
+ ],
+ "author": "joooook",
+ "license": "MIT",
+ "description": "This is a 12306 ticket search server based on the Model Context Protocol (MCP). ",
+ "dependencies": {
+ "@modelcontextprotocol/inspector": "^0.16.5",
+ "@modelcontextprotocol/sdk": "^1.9.0",
+ "@types/pg": "^8.18.0",
+ "axios": "^1.8.4",
+ "commander": "^14.0.0",
+ "date-fns": "^4.1.0",
+ "date-fns-tz": "^3.2.0",
+ "mcp-http-server": "^1.1.5",
+ "pg": "^8.20.0",
+ "zod": "^3.24.2"
+ },
+ "devDependencies": {
+ "@types/node": "^22.14.1",
+ "run-script-os": "^1.1.6",
+ "typescript": "^5.8.3"
+ },
+ "type": "module",
+ "bin": {
+ "12306-mcp": "./build/index.js"
+ },
+ "files": [
+ "build"
+ ]
+}
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/src/index.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/src/index.ts
new file mode 100644
index 00000000..70905a4c
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/src/index.ts
@@ -0,0 +1,432 @@
+#!/usr/bin/env node
+
+// PostgreSQL-backed 12306 MCP server
+// All API calls to kyfw.12306.cn are replaced with local PostgreSQL queries.
+
+import { program } from 'commander';
+import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
+import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
+import { z } from 'zod';
+import { format } from 'date-fns';
+import { toZonedTime } from 'date-fns-tz';
+import pg from 'pg';
+import {
+ StationData,
+ StationDataKeys,
+ TicketInfo,
+ RouteStationInfo,
+ Price,
+} from './types.js';
+
+const { Pool } = pg;
+
+const VERSION = '0.3.5-pg';
+
+// PostgreSQL pool
+const pool = new Pool({
+ host: process.env.PG_HOST || 'localhost',
+ port: parseInt(process.env.PG_PORT || '5432'),
+ database: process.env.PG_DATABASE || 'toolathlon',
+ user: process.env.PG_USER || 'postgres',
+ password: process.env.PG_PASSWORD || 'postgres',
+ idleTimeoutMillis: 10000,
+});
+pool.on('error', () => { /* swallow idle connection errors */ });
+
+// ---------------------------------------------------------------------------
+// Station dictionaries (loaded from PostgreSQL at startup)
+// ---------------------------------------------------------------------------
+let STATIONS: Record = {};
+let CITY_STATIONS: Record = {};
+let CITY_CODES: Record = {};
+let NAME_STATIONS: Record = {};
+
+async function loadStations(): Promise {
+ const result = await pool.query(
+ `SELECT station_code, station_name, station_pinyin, station_short, city,
+ station_id, station_index, code, r1, r2
+ FROM train.stations`
+ );
+ STATIONS = {};
+ CITY_STATIONS = {};
+ CITY_CODES = {};
+ NAME_STATIONS = {};
+ for (const row of result.rows) {
+ const station = row as StationData;
+ STATIONS[station.station_code] = station;
+ // CITY_STATIONS
+ if (!CITY_STATIONS[station.city]) CITY_STATIONS[station.city] = [];
+ CITY_STATIONS[station.city].push({ station_code: station.station_code, station_name: station.station_name });
+ // CITY_CODES: prefer station whose name == city
+ if (station.station_name === station.city || !CITY_CODES[station.city]) {
+ CITY_CODES[station.city] = { station_code: station.station_code, station_name: station.station_name };
+ }
+ // NAME_STATIONS
+ NAME_STATIONS[station.station_name] = { station_code: station.station_code, station_name: station.station_name };
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Seat type constants (kept from original)
+// ---------------------------------------------------------------------------
+const SEAT_TYPES: Record = {
+ '9': { name: '商务座', short: 'swz' },
+ P: { name: '特等座', short: 'tz' },
+ M: { name: '一等座', short: 'zy' },
+ O: { name: '二等座', short: 'ze' },
+ '6': { name: '高级软卧', short: 'gr' },
+ '4': { name: '软卧', short: 'rw' },
+ F: { name: '动卧', short: 'rw' },
+ '3': { name: '硬卧', short: 'yw' },
+ '2': { name: '软座', short: 'rz' },
+ '1': { name: '硬座', short: 'yz' },
+ W: { name: '无座', short: 'wz' },
+ H: { name: '其他', short: 'qt' },
+};
+
+function formatTicketStatus(num: string): string {
+ if (num.match(/^\d+$/)) {
+ const count = parseInt(num);
+ return count === 0 ? '无票' : `剩余${count}张票`;
+ }
+ switch (num) {
+ case '有': case '充足': return '有票';
+ case '无': case '--': case '': return '无票';
+ case '候补': return '无票需候补';
+ default: return `${num}票`;
+ }
+}
+
+function formatTicketsInfo(ticketsInfo: TicketInfo[]): string {
+ if (ticketsInfo.length === 0) return '没有查询到相关车次信息';
+ let result = '车次 | 出发站 -> 到达站 | 出发时间 -> 到达时间 | 历时\n';
+ for (const ti of ticketsInfo) {
+ let s = `${ti.start_train_code}(实际车次train_no: ${ti.train_no}) ${ti.from_station}(telecode: ${ti.from_station_telecode}) -> ${ti.to_station}(telecode: ${ti.to_station_telecode}) ${ti.start_time} -> ${ti.arrive_time} 历时:${ti.lishi}`;
+ for (const price of ti.prices) {
+ s += `\n- ${price.seat_name}: ${formatTicketStatus(price.num)} ${price.price}元`;
+ }
+ result += `${s}\n`;
+ }
+ return result;
+}
+
+function formatTicketsInfoCSV(ticketsInfo: TicketInfo[]): string {
+ if (ticketsInfo.length === 0) return '没有查询到相关车次信息';
+ let result = '车次,实际车次train_no,出发站,到达站,出发时间,到达时间,历时,票价信息,特色标签\n';
+ for (const ti of ticketsInfo) {
+ let priceStr = '[';
+ for (const p of ti.prices) priceStr += `${p.seat_name}: ${formatTicketStatus(p.num)}${p.price}元,`;
+ priceStr += ']';
+ result += `${ti.start_train_code},${ti.train_no},${ti.from_station}(telecode:${ti.from_station_telecode}),${ti.to_station}(telecode:${ti.to_station_telecode}),${ti.start_time},${ti.arrive_time},${ti.lishi},${priceStr},${ti.dw_flag.join('&') || '/'}\n`;
+ }
+ return result;
+}
+
+const TIME_COMPARATORS: Record number> = {
+ startTime: (a, b) => {
+ const [ah, am] = a.start_time.split(':').map(Number);
+ const [bh, bm] = b.start_time.split(':').map(Number);
+ return ah * 60 + am - (bh * 60 + bm);
+ },
+ arriveTime: (a, b) => {
+ const [ah, am] = a.arrive_time.split(':').map(Number);
+ const [bh, bm] = b.arrive_time.split(':').map(Number);
+ return ah * 60 + am - (bh * 60 + bm);
+ },
+ duration: (a, b) => {
+ const [ah, am] = a.lishi.split(':').map(Number);
+ const [bh, bm] = b.lishi.split(':').map(Number);
+ return ah * 60 + am - (bh * 60 + bm);
+ },
+};
+
+function filterTicketsInfo(
+ tickets: TicketInfo[],
+ trainFilterFlags: string,
+ earliestStartTime = 0,
+ latestStartTime = 24,
+ sortFlag = '',
+ sortReverse = false,
+ limitedNum = 0
+): TicketInfo[] {
+ let result = trainFilterFlags
+ ? tickets.filter(t => {
+ for (const flag of trainFilterFlags) {
+ if (flag === 'G' && (t.start_train_code.startsWith('G') || t.start_train_code.startsWith('C'))) return true;
+ if (flag === 'D' && t.start_train_code.startsWith('D')) return true;
+ if (flag === 'Z' && t.start_train_code.startsWith('Z')) return true;
+ if (flag === 'T' && t.start_train_code.startsWith('T')) return true;
+ if (flag === 'K' && t.start_train_code.startsWith('K')) return true;
+ }
+ return false;
+ })
+ : tickets;
+ result = result.filter(t => {
+ const h = parseInt(t.start_time.split(':')[0], 10);
+ return h >= earliestStartTime && h < latestStartTime;
+ });
+ if (sortFlag && TIME_COMPARATORS[sortFlag]) {
+ result.sort(TIME_COMPARATORS[sortFlag]);
+ if (sortReverse) result.reverse();
+ }
+ return limitedNum > 0 ? result.slice(0, limitedNum) : result;
+}
+
+function checkDate(date: string): boolean {
+ return true; // date check disabled — mock data uses historical dates
+}
+
+// ---------------------------------------------------------------------------
+// Query tickets from PostgreSQL
+// ---------------------------------------------------------------------------
+async function queryTickets(date: string, fromStation: string, toStation: string): Promise {
+ const result = await pool.query(
+ `SELECT t.id, t.train_no, t.station_train_code, t.from_station_telecode, t.to_station_telecode,
+ t.start_time, t.arrive_time, t.lishi, t.dw_flags,
+ sf.station_name AS from_name, st.station_name AS to_name
+ FROM train.trains t
+ JOIN train.stations sf ON sf.station_code = t.from_station_telecode
+ JOIN train.stations st ON st.station_code = t.to_station_telecode
+ WHERE t.from_station_telecode = $1
+ AND t.to_station_telecode = $2
+ AND t.depart_date = $3`,
+ [fromStation, toStation, date]
+ );
+
+ const tickets: TicketInfo[] = [];
+ for (const row of result.rows) {
+ const seatsResult = await pool.query(
+ `SELECT seat_type_code, seat_name, seat_short, num, price, discount
+ FROM train.train_seats WHERE train_id = $1`,
+ [row.id]
+ );
+ const prices: Price[] = seatsResult.rows.map(s => ({
+ seat_name: s.seat_name,
+ short: s.seat_short,
+ seat_type_code: s.seat_type_code,
+ num: s.num,
+ price: parseFloat(s.price),
+ discount: s.discount,
+ }));
+ tickets.push({
+ train_no: row.train_no,
+ start_train_code: row.station_train_code,
+ start_date: date,
+ arrive_date: date,
+ start_time: row.start_time,
+ arrive_time: row.arrive_time,
+ lishi: row.lishi,
+ from_station: row.from_name,
+ to_station: row.to_name,
+ from_station_telecode: row.from_station_telecode,
+ to_station_telecode: row.to_station_telecode,
+ prices,
+ dw_flag: row.dw_flags ? row.dw_flags.split('#').filter((f: string) => f && f !== '0') : [],
+ });
+ }
+ return tickets;
+}
+
+// ---------------------------------------------------------------------------
+// MCP Server
+// ---------------------------------------------------------------------------
+export const server = new McpServer({
+ name: '12306-mcp',
+ version: VERSION,
+ capabilities: { resources: {}, tools: {} },
+ instructions: '该服务用于查询中国铁路火车票信息(本地模拟数据)。',
+});
+
+server.resource('stations', 'data://all-stations', async (uri) => ({
+ contents: [{ uri: uri.href, text: JSON.stringify(STATIONS) }],
+}));
+
+server.tool(
+ 'get-current-date',
+ '获取当前日期,以上海时区(Asia/Shanghai, UTC+8)为准,返回格式为 "yyyy-MM-dd"。主要用于解析用户提到的相对日期(如“明天”、“下周三”),为其他需要日期的接口提供准确的日期输入。',
+ {},
+ async () => {
+ const nowInShanghai = toZonedTime(new Date(), 'Asia/Shanghai');
+ return { content: [{ type: 'text', text: format(nowInShanghai, 'yyyy-MM-dd') }] };
+ }
+);
+
+server.tool(
+ 'get-stations-code-in-city',
+ '通过中文城市名查询该城市 **所有** 火车站的名称及其对应的 `station_code`,结果是一个包含多个车站信息的列表。',
+ { city: z.string().describe('中文城市名称,例如:"北京", "上海"') },
+ async ({ city }) => {
+ if (!(city in CITY_STATIONS)) return { content: [{ type: 'text', text: 'Error: City not found.' }] };
+ return { content: [{ type: 'text', text: JSON.stringify(CITY_STATIONS[city]) }] };
+ }
+);
+
+server.tool(
+ 'get-station-code-of-citys',
+ '通过中文城市名查询代表该城市的 `station_code`。此接口主要用于在用户提供**城市名**作为出发地或到达地时,为接口准备 `station_code` 参数。',
+ { citys: z.string().describe('要查询的城市,比如"北京"。若要查询多个城市,请用|分割,比如"北京|上海"。') },
+ async ({ citys }) => {
+ const result: Record = {};
+ for (const city of citys.split('|')) {
+ result[city] = city in CITY_CODES ? CITY_CODES[city] : { error: '未检索到城市。' };
+ }
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
+ }
+);
+
+server.tool(
+ 'get-station-code-by-names',
+ '通过具体的中文车站名查询其 `station_code` 和车站名。此接口主要用于在用户提供**具体车站名**作为出发地或到达地时,为接口准备 `station_code` 参数。',
+ { stationNames: z.string().describe('具体的中文车站名称,例如:"北京南", "上海虹桥"。若要查询多个站点,请用|分割,比如"北京南|上海虹桥"。') },
+ async ({ stationNames }) => {
+ const result: Record = {};
+ for (const name of stationNames.split('|')) {
+ result[name] = name in NAME_STATIONS ? NAME_STATIONS[name] : { error: '未检索到车站。' };
+ }
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
+ }
+);
+
+server.tool(
+ 'get-station-by-telecode',
+ '通过车站的 `station_telecode` 查询车站的详细信息,包括名称、拼音、所属城市等。此接口主要用于在已知 `telecode` 的情况下获取更完整的车站数据,或用于特殊查询及调试目的。一般用户对话流程中较少直接触发。',
+ { stationTelecode: z.string().describe('车站的 `station_telecode` (3位字母编码)') },
+ async ({ stationTelecode }) => {
+ if (!STATIONS[stationTelecode]) return { content: [{ type: 'text', text: 'Error: Station not found.' }] };
+ return { content: [{ type: 'text', text: JSON.stringify(STATIONS[stationTelecode]) }] };
+ }
+);
+
+server.tool(
+ 'get-tickets',
+ '查询12306余票信息。',
+ {
+ date: z.string().length(10).describe('查询日期,格式为 "yyyy-MM-dd"。如果用户提供的是相对日期(如“明天”),请务必先调用 `get-current-date` 接口获取当前日期,并计算出目标日期。'),
+ fromStation: z.string().describe('出发地的 `station_code` 。必须是通过 `get-station-code-by-names` 或 `get-station-code-of-citys` 接口查询得到的编码,严禁直接使用中文地名。'),
+ toStation: z.string().describe('到达地的 `station_code` 。必须是通过 `get-station-code-by-names` 或 `get-station-code-of-citys` 接口查询得到的编码,严禁直接使用中文地名。'),
+ trainFilterFlags: z.string().regex(/^[GDZTKOFS]*$/).max(8).optional().default('').describe('车次筛选条件,默认为空,即不筛选。支持多个标志同时筛选。例如用户说“高铁票”,则应使用 "G"。可选标志:[G(高铁/城际),D(动车),Z(直达特快),T(特快),K(快速),O(其他),F(复兴号),S(智能动车组)]'),
+ earliestStartTime: z.number().min(0).max(24).optional().default(0).describe('最早出发时间(0-24),默认为0。'),
+ latestStartTime: z.number().min(0).max(24).optional().default(24).describe('最迟出发时间(0-24),默认为24。'),
+ sortFlag: z.string().optional().default('').describe('排序方式,默认为空,即不排序。仅支持单一标识。可选标志:[startTime(出发时间从早到晚), arriveTime(抵达时间从早到晚), duration(历时从短到长)]'),
+ sortReverse: z.boolean().optional().default(false).describe('是否逆向排序结果,默认为false。仅在设置了sortFlag时生效。'),
+ limitedNum: z.number().min(0).optional().default(0).describe('返回的余票数量限制,默认为0,即不限制。'),
+ csvFormat: z.boolean().default(false).optional().describe('是否使用CSV格式返回。'),
+ },
+ async ({ date, fromStation, toStation, trainFilterFlags, earliestStartTime, latestStartTime, sortFlag, sortReverse, limitedNum, csvFormat }) => {
+ if (!checkDate(date)) return { content: [{ type: 'text', text: 'Error: The date cannot be earlier than today.' }] };
+ if (!STATIONS[fromStation] || !STATIONS[toStation]) return { content: [{ type: 'text', text: 'Error: Station not found.' }] };
+ const tickets = await queryTickets(date, fromStation, toStation);
+ const filtered = filterTicketsInfo(tickets, trainFilterFlags || '', earliestStartTime, latestStartTime, sortFlag || '', sortReverse, limitedNum);
+ const text = csvFormat ? formatTicketsInfoCSV(filtered) : formatTicketsInfo(filtered);
+ return { content: [{ type: 'text', text }] };
+ }
+);
+
+server.tool(
+ 'get-interline-tickets',
+ '查询12306中转余票信息。尚且只支持查询前十条。',
+ {
+ date: z.string().length(10).describe('查询日期,格式为 "yyyy-MM-dd"。如果用户提供的是相对日期(如“明天”),请务必先调用 `get-current-date` 接口获取当前日期,并计算出目标日期。'),
+ fromStation: z.string().describe('出发地的 `station_code` 。必须是通过 `get-station-code-by-names` 或 `get-station-code-of-citys` 接口查询得到的编码,严禁直接使用中文地名。'),
+ toStation: z.string().describe('出发地的 `station_code` 。必须是通过 `get-station-code-by-names` 或 `get-station-code-of-citys` 接口查询得到的编码,严禁直接使用中文地名。'),
+ middleStation: z.string().optional().default('').describe('中转地的 `station_code` ,可选。必须是通过 `get-station-code-by-names` 或 `get-station-code-of-citys` 接口查询得到的编码,严禁直接使用中文地名。'),
+ showWZ: z.boolean().optional().default(false).describe('是否显示无座车,默认不显示无座车。'),
+ trainFilterFlags: z.string().regex(/^[GDZTKOFS]*$/).max(8).optional().default('').describe('车次筛选条件,默认为空。从以下标志中选取多个条件组合[G(高铁/城际),D(动车),Z(直达特快),T(特快),K(快速),O(其他),F(复兴号),S(智能动车组)]'),
+ earliestStartTime: z.number().min(0).max(24).optional().default(0).describe('最早出发时间(0-24),默认为0。'),
+ latestStartTime: z.number().min(0).max(24).optional().default(24).describe('最迟出发时间(0-24),默认为24。'),
+ sortFlag: z.string().optional().default('').describe('排序方式,默认为空,即不排序。仅支持单一标识。可选标志:[startTime(出发时间从早到晚), arriveTime(抵达时间从早到晚), duration(历时从短到长)]'),
+ sortReverse: z.boolean().optional().default(false).describe('是否逆向排序结果,默认为false。仅在设置了sortFlag时生效。'),
+ limitedNum: z.number().min(1).optional().default(10).describe('返回的中转余票数量限制,默认为10。'),
+ },
+ async ({ date, fromStation, toStation, middleStation, trainFilterFlags, earliestStartTime, latestStartTime, sortFlag, sortReverse, limitedNum }) => {
+ if (!checkDate(date)) return { content: [{ type: 'text', text: 'Error: The date cannot be earlier than today.' }] };
+
+ // Find all possible middle stations that have trains from fromStation and to toStation
+ const midResult = await pool.query(
+ `SELECT DISTINCT t1.to_station_telecode AS mid
+ FROM train.trains t1
+ JOIN train.trains t2 ON t1.to_station_telecode = t2.from_station_telecode
+ WHERE t1.from_station_telecode = $1
+ AND t2.to_station_telecode = $2
+ AND t1.depart_date = $3 AND t2.depart_date = $3
+ ${middleStation ? 'AND t1.to_station_telecode = $4' : ''}
+ LIMIT 5`,
+ middleStation ? [fromStation, toStation, date, middleStation] : [fromStation, toStation, date]
+ );
+
+ if (midResult.rows.length === 0) {
+ return { content: [{ type: 'text', text: '很抱歉,未查到相关的中转余票信息。' }] };
+ }
+
+ let output = '出发时间 -> 到达时间 | 出发车站 -> 中转车站 -> 到达车站 | 换乘标志 |换乘等待时间| 总历时\n\n';
+ let count = 0;
+ for (const { mid } of midResult.rows) {
+ if (count >= limitedNum) break;
+ const leg1 = await queryTickets(date, fromStation, mid);
+ const leg2 = await queryTickets(date, mid, toStation);
+ const midStation = STATIONS[mid];
+ const midName = midStation?.station_name || mid;
+ for (const t1 of leg1.slice(0, 3)) {
+ for (const t2 of leg2.slice(0, 3)) {
+ if (count >= limitedNum) break;
+ const fromName = STATIONS[fromStation]?.station_name || fromStation;
+ const toName = STATIONS[toStation]?.station_name || toStation;
+ output += `${date} ${t1.start_time} -> ${date} ${t2.arrive_time} | `;
+ output += `${fromName} -> ${midName} -> ${toName} | 同站换乘 | - | -\n\n`;
+ output += '\t第一段: ' + t1.start_train_code + ' ' + t1.start_time + ' -> ' + t1.arrive_time + '\n';
+ output += '\t第二段: ' + t2.start_train_code + ' ' + t2.start_time + ' -> ' + t2.arrive_time + '\n\n';
+ count++;
+ }
+ }
+ }
+ return { content: [{ type: 'text', text: output }] };
+ }
+);
+
+server.tool(
+ 'get-train-route-stations',
+ '查询特定列车车次在指定区间内的途径车站、到站时间、出发时间及停留时间等详细经停信息。当用户询问某趟具体列车的经停站时使用此接口。',
+ {
+ trainNo: z.string().describe('要查询的实际车次编号 `train_no`,例如 "240000G10336",而非"G1033"。此编号通常可以从 `get-tickets` 的查询结果中获取,或者由用户直接提供。'),
+ fromStationTelecode: z.string().describe('该列车行程的**出发站**的 `station_telecode` (3位字母编码`)。通常来自 `get-tickets` 结果中的 `telecode` 字段,或者通过 `get-station-code-by-names` 得到。'),
+ toStationTelecode: z.string().describe('该列车行程的**到达站**的 `station_telecode` (3位字母编码)。通常来自 `get-tickets` 结果中的 `telecode` 字段,或者通过 `get-station-code-by-names` 得到。'),
+ departDate: z.string().length(10).describe('列车从 `fromStationTelecode` 指定的车站出发的日期 (格式: yyyy-MM-dd)。如果用户提供的是相对日期,请务必先调用 `get-current-date` 解析。'),
+ },
+ async ({ trainNo }) => {
+ const result = await pool.query(
+ `SELECT station_no, station_telecode, station_name, arrive_time, depart_time, stopover_time
+ FROM train.train_routes WHERE train_no = $1 ORDER BY station_no`,
+ [trainNo]
+ );
+ if (result.rows.length === 0) return { content: [{ type: 'text', text: '未查询到相关车次信息。' }] };
+ const routeInfo: RouteStationInfo[] = result.rows.map(r => ({
+ arrive_time: r.arrive_time,
+ station_name: r.station_name,
+ stopover_time: r.stopover_time,
+ station_no: r.station_no,
+ }));
+ return { content: [{ type: 'text', text: JSON.stringify(routeInfo) }] };
+ }
+);
+
+// ---------------------------------------------------------------------------
+// Startup
+// ---------------------------------------------------------------------------
+async function startServer() {
+ await loadStations();
+ console.error(`12306 MCP Server (pg-backed) started. Loaded ${Object.keys(STATIONS).length} stations.`);
+
+ program
+ .name('mcp-server-12306')
+ .version(VERSION)
+ .option('--stdio', 'use stdio transport (default)', true)
+ .parse(process.argv);
+
+ const transport = new StdioServerTransport();
+ await server.connect(transport);
+}
+
+startServer().catch(err => {
+ console.error('Failed to start server:', err);
+ process.exit(1);
+});
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/src/types.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/src/types.ts
new file mode 100644
index 00000000..c0371f0d
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/src/types.ts
@@ -0,0 +1,302 @@
+#!/usr/bin/env node
+export type TicketData = {
+ secret_Sstr: string;
+ button_text_info: string;
+ train_no: string;
+ station_train_code: string;
+ start_station_telecode: string;
+ end_station_telecode: string;
+ from_station_telecode: string;
+ to_station_telecode: string;
+ start_time: string;
+ arrive_time: string;
+ lishi: string;
+ canWebBuy: string;
+ yp_info: string;
+ start_train_date: string;
+ train_seat_feature: string;
+ location_code: string;
+ from_station_no: string;
+ to_station_no: string;
+ is_support_card: string;
+ controlled_train_flag: string;
+ gg_num: string;
+ gr_num: string;
+ qt_num: string;
+ rw_num: string;
+ rz_num: string;
+ tz_num: string;
+ wz_num: string;
+ yb_num: string;
+ yw_num: string;
+ yz_num: string;
+ ze_num: string;
+ zy_num: string;
+ swz_num: string;
+ srrb_num: string;
+ yp_ex: string;
+ seat_types: string;
+ exchange_train_flag: string;
+ houbu_train_flag: string;
+ houbu_seat_limit: string;
+ yp_info_new: string;
+ '40': string;
+ '41': string;
+ '42': string;
+ '43': string;
+ '44': string;
+ '45': string;
+ dw_flag: string;
+ '47': string;
+ stopcheckTime: string;
+ country_flag: string;
+ local_arrive_time: string;
+ local_start_time: string;
+ '52': string;
+ bed_level_info: string;
+ seat_discount_info: string;
+ sale_time: string;
+ '56': string;
+};
+
+export const TicketDataKeys: (keyof TicketData)[] = [
+ 'secret_Sstr',
+ 'button_text_info',
+ 'train_no',
+ 'station_train_code',
+ 'start_station_telecode',
+ 'end_station_telecode',
+ 'from_station_telecode',
+ 'to_station_telecode',
+ 'start_time',
+ 'arrive_time',
+ 'lishi',
+ 'canWebBuy',
+ 'yp_info',
+ 'start_train_date',
+ 'train_seat_feature',
+ 'location_code',
+ 'from_station_no',
+ 'to_station_no',
+ 'is_support_card',
+ 'controlled_train_flag',
+ 'gg_num',
+ 'gr_num',
+ 'qt_num',
+ 'rw_num',
+ 'rz_num',
+ 'tz_num',
+ 'wz_num',
+ 'yb_num',
+ 'yw_num',
+ 'yz_num',
+ 'ze_num',
+ 'zy_num',
+ 'swz_num',
+ 'srrb_num',
+ 'yp_ex',
+ 'seat_types',
+ 'exchange_train_flag',
+ 'houbu_train_flag',
+ 'houbu_seat_limit',
+ 'yp_info_new',
+ '40',
+ '41',
+ '42',
+ '43',
+ '44',
+ '45',
+ 'dw_flag',
+ '47',
+ 'stopcheckTime',
+ 'country_flag',
+ 'local_arrive_time',
+ 'local_start_time',
+ '52',
+ 'bed_level_info',
+ 'seat_discount_info',
+ 'sale_time',
+ '56',
+];
+
+export type TicketInfo = {
+ train_no: string;
+ start_train_code: string;
+ start_date: string;
+ start_time: string;
+ arrive_date: string;
+ arrive_time: string;
+ lishi: string;
+ from_station: string;
+ to_station: string;
+ from_station_telecode: string;
+ to_station_telecode: string;
+ prices: Price[];
+ dw_flag: string[];
+};
+
+export type StationData = {
+ station_id: string;
+ station_name: string;
+ station_code: string;
+ station_pinyin: string;
+ station_short: string;
+ station_index: string;
+ code: string;
+ city: string;
+ r1: string;
+ r2: string;
+};
+
+export const StationDataKeys: (keyof StationData)[] = [
+ 'station_id',
+ 'station_name',
+ 'station_code',
+ 'station_pinyin',
+ 'station_short',
+ 'station_index',
+ 'code',
+ 'city',
+ 'r1',
+ 'r2',
+];
+
+export interface Price {
+ seat_name: string;
+ short: string;
+ seat_type_code: string;
+ num: string;
+ price: number;
+ discount: number | null;
+}
+
+export type RouteStationData = {
+ arrive_time: string;
+ station_name: string;
+ isChina: string;
+ start_time: string;
+ stopover_time: string;
+ station_no: string;
+ country_code: string;
+ country_name: string;
+ isEnabled: boolean;
+ train_class_name?: string;
+ service_type?: string;
+ end_station_name?: string;
+ start_station_name?: string;
+ station_train_code?: string;
+};
+
+export type RouteStationInfo = {
+ arrive_time: string;
+ station_name: string;
+ stopover_time: string;
+ station_no: number;
+};
+
+export type InterlineData = {
+ all_lishi: string;
+ all_lishi_minutes: number;
+ arrive_date: string;
+ arrive_time: string;
+ end_station_code: string;
+ end_station_name: string;
+ first_train_no: string;
+ from_station_code: string;
+ from_station_name: string;
+ fullList: InterlineTicketData[];
+ isHeatTrain: string;
+ isOutStation: string;
+ lCWaitTime: string;
+ lishi_flag: string;
+ middle_date: string;
+ middle_station_code: string;
+ middle_station_name: string;
+ same_station: string;
+ same_train: string;
+ score: number;
+ score_str: string;
+ scretstr: string;
+ second_train_no: string;
+ start_time: string;
+ train_count: number;
+ train_date: string; // 出发时间
+ use_time: string;
+ wait_time: string;
+ wait_time_minutes: number;
+};
+
+export type InterlineInfo = {
+ lishi: string;
+ //all_lishi_minutes: number;
+ start_time: string;
+ start_date: string;
+ middle_date: string;
+ arrive_date: string;
+ arrive_time: string;
+ from_station_code: string;
+ from_station_name: string;
+ middle_station_code: string;
+ middle_station_name: string;
+ end_station_code: string;
+ end_station_name: string;
+ start_train_code: string; // 用于过滤
+ first_train_no: string;
+ second_train_no: string;
+ train_count: number;
+ ticketList: TicketInfo[];
+ //isHeatTrain: string;
+ //isOutStation: string;
+ //lCWaitTime: string;
+ //lishi_flag: string;
+ same_station: boolean;
+ same_train: boolean;
+ wait_time: string;
+ //wait_time_minutes: number;
+};
+
+export type InterlineTicketData = {
+ arrive_time: string;
+ bed_level_info: string;
+ controlled_train_flag: string;
+ country_flag: string;
+ day_difference: string;
+ dw_flag: string;
+ end_station_name: string;
+ end_station_telecode: string;
+ from_station_name: string;
+ from_station_no: string;
+ from_station_telecode: string;
+ gg_num: string;
+ gr_num: string;
+ is_support_card: string;
+ lishi: string;
+ local_arrive_time: string;
+ local_start_time: string;
+ qt_num: string;
+ rw_num: string;
+ rz_num: string;
+ seat_discount_info: string;
+ seat_types: string;
+ srrb_num: string;
+ start_station_name: string;
+ start_station_telecode: string;
+ start_time: string;
+ start_train_date: string;
+ station_train_code: string;
+ swz_num: string;
+ to_station_name: string;
+ to_station_no: string;
+ to_station_telecode: string;
+ train_no: string;
+ train_seat_feature: string;
+ trms_train_flag: string;
+ tz_num: string;
+ wz_num: string;
+ yb_num: string;
+ yp_info: string;
+ yw_num: string;
+ yz_num: string;
+ ze_num: string;
+ zy_num: string;
+};
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/tsconfig.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/tsconfig.json
new file mode 100644
index 00000000..1c7e3c36
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/12306-mcp/tsconfig.json
@@ -0,0 +1,15 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "Node16",
+ "moduleResolution": "Node16",
+ "outDir": "./build",
+ "rootDir": "./src",
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true
+ },
+ "include": ["src/**/*"],
+ "exclude": ["node_modules"]
+ }
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Calendar-Autoauth-MCP-Server/.gitignore b/sandbox/server/backends/resources/mcp/vendor/local_servers/Calendar-Autoauth-MCP-Server/.gitignore
new file mode 100644
index 00000000..6d370fc5
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Calendar-Autoauth-MCP-Server/.gitignore
@@ -0,0 +1,31 @@
+# Dependencies
+node_modules/
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# Build
+build/
+dist/
+*.tsbuildinfo
+
+# Environment
+.env
+.env.local
+.env.*.local
+
+# IDE
+.idea/
+.vscode/
+*.swp
+*.swo
+
+# OS
+.DS_Store
+Thumbs.db
+
+# Project specific
+gcp-oauth.keys.json
+.calendar-mcp/
+credentials.json
+.calendar-server-credentials.json
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Calendar-Autoauth-MCP-Server/Dockerfile b/sandbox/server/backends/resources/mcp/vendor/local_servers/Calendar-Autoauth-MCP-Server/Dockerfile
new file mode 100644
index 00000000..7a61c37a
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Calendar-Autoauth-MCP-Server/Dockerfile
@@ -0,0 +1,27 @@
+FROM node:20-alpine
+
+WORKDIR /app
+
+# Copy package files
+COPY package*.json ./
+
+# Install dependencies
+RUN npm install
+
+# Copy source code
+COPY . .
+
+# Build the application
+RUN npm run build
+
+# Create data directory
+RUN mkdir -p /app/calendar-data
+
+# Set permissions for the data directory
+RUN chown -R node:node /app/calendar-data
+
+# Switch to non-root user
+USER node
+
+# Start the server
+CMD ["node", "build/index.js"]
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Calendar-Autoauth-MCP-Server/LICENSE b/sandbox/server/backends/resources/mcp/vendor/local_servers/Calendar-Autoauth-MCP-Server/LICENSE
new file mode 100644
index 00000000..31c022e6
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Calendar-Autoauth-MCP-Server/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2025 GongRzhe
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Calendar-Autoauth-MCP-Server/README.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/Calendar-Autoauth-MCP-Server/README.md
new file mode 100644
index 00000000..746c8a8d
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Calendar-Autoauth-MCP-Server/README.md
@@ -0,0 +1,217 @@
+# Calendar AutoAuth MCP Server
+
+A Model Context Protocol (MCP) server for Google Calendar integration in Cluade Desktop with auto authentication support. This server enables AI assistants to manage Google Calendar events through natural language interactions.
+
+
+[](https://smithery.ai/server/@gongrzhe/server-calendar-autoauth-mcp)
+[](https://www.npmjs.com/package/@gongrzhe/server-calendar-autoauth-mcp)
+[](https://opensource.org/licenses/ISC)
+
+## Features
+
+- Create calendar events with title, time, description, and location
+- Retrieve event details by event ID
+- Update existing events (title, time, description, location)
+- Delete events
+- List events within a specified time range
+- Full integration with Google Calendar API
+- Simple OAuth2 authentication flow with auto browser launch
+- Support for both Desktop and Web application credentials
+- Global credential storage for convenience
+
+## Installation & Authentication
+
+### Installing via Smithery
+
+To install Calendar AutoAuth Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@gongrzhe/server-calendar-autoauth-mcp):
+
+```bash
+npx -y @smithery/cli install @gongrzhe/server-calendar-autoauth-mcp --client claude
+```
+
+1. Create a Google Cloud Project and obtain credentials:
+
+ a. Create a Google Cloud Project:
+ - Go to [Google Cloud Console](https://console.cloud.google.com/)
+ - Create a new project or select an existing one
+ - Enable the Google Calendar API for your project
+
+ b. Create OAuth 2.0 Credentials:
+ - Go to "APIs & Services" > "Credentials"
+ - Click "Create Credentials" > "OAuth client ID"
+ - Choose either "Desktop app" or "Web application" as application type
+ - Give it a name and click "Create"
+ - For Web application, add `http://localhost:3000/oauth2callback` to the authorized redirect URIs
+ - Download the JSON file of your client's OAuth keys
+ - Rename the key file to `gcp-oauth.keys.json`
+
+2. Run Authentication:
+
+ You can authenticate in two ways:
+
+ a. Global Authentication (Recommended):
+ ```bash
+ # First time: Place gcp-oauth.keys.json in your home directory's .calendar-mcp folder
+ mkdir -p ~/.calendar-mcp
+ mv gcp-oauth.keys.json ~/.calendar-mcp/
+
+ # Run authentication from anywhere
+ npx @gongrzhe/server-calendar-autoauth-mcp auth
+ ```
+
+ b. Local Authentication:
+ ```bash
+ # Place gcp-oauth.keys.json in your current directory
+ # The file will be automatically copied to global config
+ npx @gongrzhe/server-calendar-autoauth-mcp auth
+ ```
+
+ The authentication process will:
+ - Look for `gcp-oauth.keys.json` in the current directory or `~/.calendar-mcp/`
+ - If found in current directory, copy it to `~/.calendar-mcp/`
+ - Open your default browser for Google authentication
+ - Save credentials as `~/.calendar-mcp/credentials.json`
+
+ > **Note**:
+ > - After successful authentication, credentials are stored globally in `~/.calendar-mcp/` and can be used from any directory
+ > - Both Desktop app and Web application credentials are supported
+ > - For Web application credentials, make sure to add `http://localhost:3000/oauth2callback` to your authorized redirect URIs
+
+3. Configure in Claude Desktop:
+
+```json
+{
+ "mcpServers": {
+ "calendar": {
+ "command": "npx",
+ "args": [
+ "@gongrzhe/server-calendar-autoauth-mcp"
+ ]
+ }
+ }
+}
+```
+
+### Docker Support
+
+If you prefer using Docker:
+
+1. Authentication:
+```bash
+docker run -i --rm \
+ --mount type=bind,source=/path/to/gcp-oauth.keys.json,target=/gcp-oauth.keys.json \
+ -v mcp-calendar:/calendar-server \
+ -e CALENDAR_OAUTH_PATH=/gcp-oauth.keys.json \
+ -e "CALENDAR_CREDENTIALS_PATH=/calendar-server/credentials.json" \
+ -p 3000:3000 \
+ mcp/calendar auth
+```
+
+2. Usage:
+```json
+{
+ "mcpServers": {
+ "calendar": {
+ "command": "docker",
+ "args": [
+ "run",
+ "-i",
+ "--rm",
+ "-v",
+ "mcp-calendar:/calendar-server",
+ "-e",
+ "CALENDAR_CREDENTIALS_PATH=/calendar-server/credentials.json",
+ "mcp/calendar"
+ ]
+ }
+ }
+}
+```
+
+## Usage Examples
+
+The server provides several tools that can be used through the Claude Desktop:
+
+### Create Event
+```json
+{
+ "summary": "Team Meeting",
+ "start": {
+ "dateTime": "2024-01-20T10:00:00Z"
+ },
+ "end": {
+ "dateTime": "2024-01-20T11:00:00Z"
+ },
+ "description": "Weekly team sync",
+ "location": "Conference Room A"
+}
+```
+
+### List Events
+```json
+{
+ "timeMin": "2024-01-01T00:00:00Z",
+ "timeMax": "2024-12-31T23:59:59Z",
+ "maxResults": 10,
+ "orderBy": "startTime"
+}
+```
+
+### Update Event
+```json
+{
+ "eventId": "event123",
+ "summary": "Updated Meeting Title",
+ "start": {
+ "dateTime": "2024-01-20T11:00:00Z"
+ },
+ "end": {
+ "dateTime": "2024-01-20T12:00:00Z"
+ }
+}
+```
+
+### Delete Event
+```json
+{
+ "eventId": "event123"
+}
+```
+
+## Security Notes
+
+- OAuth credentials are stored securely in your local environment (`~/.calendar-mcp/`)
+- The server uses offline access to maintain persistent authentication
+- Never share or commit your credentials to version control
+- Regularly review and revoke unused access in your Google Account settings
+- Credentials are stored globally but are only accessible by the current user
+
+## Troubleshooting
+
+1. **OAuth Keys Not Found**
+ - Make sure `gcp-oauth.keys.json` is in either your current directory or `~/.calendar-mcp/`
+ - Check file permissions
+
+2. **Invalid Credentials Format**
+ - Ensure your OAuth keys file contains either `web` or `installed` credentials
+ - For web applications, verify the redirect URI is correctly configured
+
+3. **Port Already in Use**
+ - If port 3000 is already in use, please free it up before running authentication
+ - You can find and stop the process using that port
+
+## Contributing
+
+Contributions are welcome! Please feel free to submit a Pull Request.
+
+## License
+
+This project is licensed under the ISC License.
+
+## Author
+
+gongrzhe
+
+## Support
+
+If you encounter any issues or have questions, please file an issue on the GitHub repository.
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Calendar-Autoauth-MCP-Server/docker-compose.yml b/sandbox/server/backends/resources/mcp/vendor/local_servers/Calendar-Autoauth-MCP-Server/docker-compose.yml
new file mode 100644
index 00000000..7867583e
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Calendar-Autoauth-MCP-Server/docker-compose.yml
@@ -0,0 +1,15 @@
+version: '3.8'
+
+services:
+ redis:
+ image: redis:latest
+ container_name: my-redis
+ ports:
+ - "6379:6379"
+ volumes:
+ - redis_data:/data
+ command: redis-server --appendonly yes
+ restart: always
+
+volumes:
+ redis_data:
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Calendar-Autoauth-MCP-Server/package-lock.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/Calendar-Autoauth-MCP-Server/package-lock.json
new file mode 100644
index 00000000..8d3776f4
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Calendar-Autoauth-MCP-Server/package-lock.json
@@ -0,0 +1,985 @@
+{
+ "name": "@gongrzhe/server-calendar-autoauth-mcp",
+ "version": "1.0.2",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "@gongrzhe/server-calendar-autoauth-mcp",
+ "version": "1.0.2",
+ "license": "ISC",
+ "dependencies": {
+ "@modelcontextprotocol/sdk": "^0.4.0",
+ "googleapis": "^133.0.0",
+ "open": "^8.4.2",
+ "pg": "^8.13.0",
+ "zod": "^3.24.1",
+ "zod-to-json-schema": "^3.22.4"
+ },
+ "bin": {
+ "server-calendar-autoauth-mcp": "build/index.js"
+ },
+ "devDependencies": {
+ "@types/node": "^22.10.2",
+ "@types/open": "^6.2.1",
+ "@types/pg": "^8.18.0",
+ "typescript": "^5.7.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@modelcontextprotocol/sdk": {
+ "version": "0.4.0",
+ "license": "MIT",
+ "dependencies": {
+ "content-type": "^1.0.5",
+ "raw-body": "^3.0.0",
+ "zod": "^3.23.8"
+ }
+ },
+ "node_modules/@types/node": {
+ "version": "22.10.2",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz",
+ "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.20.0"
+ }
+ },
+ "node_modules/@types/open": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/@types/open/-/open-6.2.1.tgz",
+ "integrity": "sha512-CzV16LToFaKwm1FfplVTF08E3pznw4fQNCQ87N+A1RU00zu/se7npvb6IC9db3/emnSThQ6R8qFKgrei2M4EYQ==",
+ "deprecated": "This is a stub types definition. open provides its own type definitions, so you do not need this installed.",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "open": "*"
+ }
+ },
+ "node_modules/@types/pg": {
+ "version": "8.18.0",
+ "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.18.0.tgz",
+ "integrity": "sha512-gT+oueVQkqnj6ajGJXblFR4iavIXWsGAFCk3dP4Kki5+a9R4NMt0JARdk6s8cUKcfUoqP5dAtDSLU8xYUTFV+Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "pg-protocol": "*",
+ "pg-types": "^2.2.0"
+ }
+ },
+ "node_modules/agent-base": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz",
+ "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/bignumber.js": {
+ "version": "9.1.2",
+ "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz",
+ "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==",
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/buffer-equal-constant-time": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
+ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz",
+ "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz",
+ "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "get-intrinsic": "^1.2.6"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
+ "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/define-lazy-prop": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz",
+ "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/ecdsa-sig-formatter": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
+ "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz",
+ "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/extend": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
+ "license": "MIT"
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gaxios": {
+ "version": "6.7.1",
+ "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz",
+ "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "extend": "^3.0.2",
+ "https-proxy-agent": "^7.0.1",
+ "is-stream": "^2.0.0",
+ "node-fetch": "^2.6.9",
+ "uuid": "^9.0.1"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/gcp-metadata": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.0.tgz",
+ "integrity": "sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "gaxios": "^6.0.0",
+ "json-bigint": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.6.tgz",
+ "integrity": "sha512-qxsEs+9A+u85HhllWJJFicJfPDhRmjzoYdl64aMWW9yRIJmSyxdn8IEkuIM530/7T+lv0TIHd8L6Q/ra0tEoeA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "dunder-proto": "^1.0.0",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.0.0",
+ "function-bind": "^1.1.2",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/google-auth-library": {
+ "version": "9.15.0",
+ "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.0.tgz",
+ "integrity": "sha512-7ccSEJFDFO7exFbO6NRyC+xH8/mZ1GZGG2xxx9iHxZWcjUjJpjWxIMw3cofAKcueZ6DATiukmmprD7yavQHOyQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "base64-js": "^1.3.0",
+ "ecdsa-sig-formatter": "^1.0.11",
+ "gaxios": "^6.1.1",
+ "gcp-metadata": "^6.1.0",
+ "gtoken": "^7.0.0",
+ "jws": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/googleapis": {
+ "version": "133.0.0",
+ "resolved": "https://registry.npmjs.org/googleapis/-/googleapis-133.0.0.tgz",
+ "integrity": "sha512-6xyc49j+x7N4smawJs/q1i7mbSkt6SYUWWd9RbsmmDW7gRv+mhwZ4xT+XkPihZcNyo/diF//543WZq4szdS74w==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "google-auth-library": "^9.0.0",
+ "googleapis-common": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/googleapis-common": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-7.2.0.tgz",
+ "integrity": "sha512-/fhDZEJZvOV3X5jmD+fKxMqma5q2Q9nZNSF3kn1F18tpxmA86BcTxAGBQdM0N89Z3bEaIs+HVznSmFJEAmMTjA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "extend": "^3.0.2",
+ "gaxios": "^6.0.3",
+ "google-auth-library": "^9.7.0",
+ "qs": "^6.7.0",
+ "url-template": "^2.0.8",
+ "uuid": "^9.0.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gtoken": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz",
+ "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==",
+ "license": "MIT",
+ "dependencies": {
+ "gaxios": "^6.0.0",
+ "jws": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
+ "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "2.0.0",
+ "inherits": "2.0.4",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "toidentifier": "1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
+ "node_modules/is-docker": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
+ "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
+ "license": "MIT",
+ "bin": {
+ "is-docker": "cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-stream": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
+ "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-wsl": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
+ "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
+ "license": "MIT",
+ "dependencies": {
+ "is-docker": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/json-bigint": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
+ "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "bignumber.js": "^9.0.0"
+ }
+ },
+ "node_modules/jwa": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz",
+ "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==",
+ "license": "MIT",
+ "dependencies": {
+ "buffer-equal-constant-time": "1.0.1",
+ "ecdsa-sig-formatter": "1.0.11",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/jws": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz",
+ "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==",
+ "license": "MIT",
+ "dependencies": {
+ "jwa": "^2.0.0",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/node-fetch": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
+ "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-url": "^5.0.0"
+ },
+ "engines": {
+ "node": "4.x || >=6.0.0"
+ },
+ "peerDependencies": {
+ "encoding": "^0.1.0"
+ },
+ "peerDependenciesMeta": {
+ "encoding": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.3",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz",
+ "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/open": {
+ "version": "8.4.2",
+ "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz",
+ "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==",
+ "license": "MIT",
+ "dependencies": {
+ "define-lazy-prop": "^2.0.0",
+ "is-docker": "^2.1.1",
+ "is-wsl": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/pg": {
+ "version": "8.20.0",
+ "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz",
+ "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==",
+ "license": "MIT",
+ "dependencies": {
+ "pg-connection-string": "^2.12.0",
+ "pg-pool": "^3.13.0",
+ "pg-protocol": "^1.13.0",
+ "pg-types": "2.2.0",
+ "pgpass": "1.0.5"
+ },
+ "engines": {
+ "node": ">= 16.0.0"
+ },
+ "optionalDependencies": {
+ "pg-cloudflare": "^1.3.0"
+ },
+ "peerDependencies": {
+ "pg-native": ">=3.0.1"
+ },
+ "peerDependenciesMeta": {
+ "pg-native": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/pg-cloudflare": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz",
+ "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/pg-connection-string": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.12.0.tgz",
+ "integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==",
+ "license": "MIT"
+ },
+ "node_modules/pg-int8": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
+ "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/pg-pool": {
+ "version": "3.13.0",
+ "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz",
+ "integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "pg": ">=8.0"
+ }
+ },
+ "node_modules/pg-protocol": {
+ "version": "1.13.0",
+ "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz",
+ "integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==",
+ "license": "MIT"
+ },
+ "node_modules/pg-types": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
+ "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
+ "license": "MIT",
+ "dependencies": {
+ "pg-int8": "1.0.1",
+ "postgres-array": "~2.0.0",
+ "postgres-bytea": "~1.0.0",
+ "postgres-date": "~1.0.4",
+ "postgres-interval": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/pgpass": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
+ "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
+ "license": "MIT",
+ "dependencies": {
+ "split2": "^4.1.0"
+ }
+ },
+ "node_modules/postgres-array": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
+ "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/postgres-bytea": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz",
+ "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/postgres-date": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
+ "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/postgres-interval": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
+ "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "xtend": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/qs": {
+ "version": "6.13.1",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.1.tgz",
+ "integrity": "sha512-EJPeIn0CYrGu+hli1xilKAPXODtJ12T0sP63Ijx2/khC2JtuaN3JyNIpvmnkmaEtha9ocbG4A4cMcr+TvqvwQg==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.0.6"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz",
+ "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.6.3",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "license": "ISC"
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/split2": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
+ "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">= 10.x"
+ }
+ },
+ "node_modules/statuses": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
+ "license": "MIT"
+ },
+ "node_modules/typescript": {
+ "version": "5.7.2",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz",
+ "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "6.20.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
+ "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/url-template": {
+ "version": "2.0.8",
+ "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz",
+ "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==",
+ "license": "BSD"
+ },
+ "node_modules/uuid": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
+ "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
+ "node_modules/webidl-conversions": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/whatwg-url": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+ "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "~0.0.3",
+ "webidl-conversions": "^3.0.0"
+ }
+ },
+ "node_modules/xtend": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
+ "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4"
+ }
+ },
+ "node_modules/zod": {
+ "version": "3.24.1",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz",
+ "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
+ "node_modules/zod-to-json-schema": {
+ "version": "3.24.1",
+ "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.1.tgz",
+ "integrity": "sha512-3h08nf3Vw3Wl3PK+q3ow/lIil81IT2Oa7YpQyUUDsEWbXveMesdfK1xBd2RhCkynwZndAxixji/7SYJJowr62w==",
+ "license": "ISC",
+ "peerDependencies": {
+ "zod": "^3.24.1"
+ }
+ }
+ }
+}
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Calendar-Autoauth-MCP-Server/package.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/Calendar-Autoauth-MCP-Server/package.json
new file mode 100644
index 00000000..ced72e4a
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Calendar-Autoauth-MCP-Server/package.json
@@ -0,0 +1,60 @@
+{
+ "name": "@gongrzhe/server-calendar-autoauth-mcp",
+ "version": "1.0.2",
+ "description": "A Model Context Protocol server for Google Calendar integration with auto authentication",
+ "main": "build/index.js",
+ "type": "module",
+ "bin": {
+ "server-calendar-autoauth-mcp": "./build/index.js"
+ },
+ "scripts": {
+ "build": "tsc",
+ "prepublishOnly": "npm run build",
+ "auth": "node ./build/index.js auth"
+ },
+ "files": [
+ "build",
+ "README.md"
+ ],
+ "keywords": [
+ "calendar",
+ "events",
+ "scheduling",
+ "mcp",
+ "model-context-protocol",
+ "google-calendar",
+ "claude",
+ "cursor",
+ "auto-auth"
+ ],
+ "author": "gongrzhe",
+ "license": "ISC",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/gongrzhe/server-calendar-autoauth-mcp.git"
+ },
+ "bugs": {
+ "url": "https://github.com/gongrzhe/server-calendar-autoauth-mcp/issues"
+ },
+ "homepage": "https://github.com/gongrzhe/server-calendar-autoauth-mcp#readme",
+ "publishConfig": {
+ "access": "public"
+ },
+ "devDependencies": {
+ "@types/node": "^22.10.2",
+ "@types/open": "^6.2.1",
+ "@types/pg": "^8.18.0",
+ "typescript": "^5.7.2"
+ },
+ "dependencies": {
+ "@modelcontextprotocol/sdk": "^0.4.0",
+ "googleapis": "^133.0.0",
+ "open": "^8.4.2",
+ "pg": "^8.13.0",
+ "zod": "^3.24.1",
+ "zod-to-json-schema": "^3.22.4"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+}
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Calendar-Autoauth-MCP-Server/smithery.yaml b/sandbox/server/backends/resources/mcp/vendor/local_servers/Calendar-Autoauth-MCP-Server/smithery.yaml
new file mode 100644
index 00000000..d0023504
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Calendar-Autoauth-MCP-Server/smithery.yaml
@@ -0,0 +1,23 @@
+# Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml
+
+startCommand:
+ type: stdio
+ configSchema:
+ # JSON Schema defining the configuration options for the MCP.
+ type: object
+ required:
+ - calendarOauthPath
+ - calendarCredentialsPath
+ properties:
+ calendarOauthPath:
+ type: string
+ default: ~/.calendar-mcp/gcp-oauth.keys.json
+ description: Path to the Google OAuth credentials file.
+ calendarCredentialsPath:
+ type: string
+ default: ~/.calendar-mcp/credentials.json
+ description: Path where the OAuth tokens will be stored.
+ commandFunction:
+ # A function that produces the CLI command to start the MCP on stdio.
+ |-
+ (config) => ({ command: 'node', args: ['build/index.js'], env: { CALENDAR_OAUTH_PATH: config.calendarOauthPath, CALENDAR_CREDENTIALS_PATH: config.calendarCredentialsPath } })
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Calendar-Autoauth-MCP-Server/src/index.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/Calendar-Autoauth-MCP-Server/src/index.ts
new file mode 100644
index 00000000..52d4bed2
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Calendar-Autoauth-MCP-Server/src/index.ts
@@ -0,0 +1,422 @@
+#!/usr/bin/env node
+
+import { Server } from "@modelcontextprotocol/sdk/server/index.js";
+import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
+import {
+ CallToolRequestSchema,
+ ListToolsRequestSchema,
+} from "@modelcontextprotocol/sdk/types.js";
+import { z } from "zod";
+import { zodToJsonSchema } from "zod-to-json-schema";
+import pg from 'pg';
+
+const { Pool } = pg;
+
+// PostgreSQL connection pool
+const pool = new Pool({
+ host: process.env.PG_HOST || 'localhost',
+ port: parseInt(process.env.PG_PORT || '5432'),
+ database: process.env.PG_DATABASE || 'toolathlon',
+ user: process.env.PG_USER || 'postgres',
+ password: process.env.PG_PASSWORD || 'postgres',
+});
+
+// Helper to format a DB row into Google Calendar event JSON
+function formatEvent(row: any) {
+ return {
+ id: row.id,
+ summary: row.summary,
+ description: row.description,
+ location: row.location,
+ start: {
+ dateTime: row.start_datetime ? new Date(row.start_datetime).toISOString() : null,
+ timeZone: row.start_timezone || undefined,
+ },
+ end: {
+ dateTime: row.end_datetime ? new Date(row.end_datetime).toISOString() : null,
+ timeZone: row.end_timezone || undefined,
+ },
+ status: row.status,
+ htmlLink: row.html_link,
+ creator: row.creator,
+ organizer: row.organizer,
+ attendees: row.attendees,
+ recurrence: row.recurrence,
+ reminders: row.reminders,
+ created: row.created ? new Date(row.created).toISOString() : null,
+ updated: row.updated ? new Date(row.updated).toISOString() : null,
+ };
+}
+
+// PgCalendar class that mimics the google calendar.events.* interface
+class PgCalendar {
+ events: {
+ insert: (params: { calendarId: string; requestBody: any }) => Promise<{ data: any }>;
+ get: (params: { calendarId: string; eventId: string }) => Promise<{ data: any }>;
+ patch: (params: { calendarId: string; eventId: string; requestBody: any }) => Promise<{ data: any }>;
+ delete: (params: { calendarId: string; eventId: string }) => Promise<{ data: any }>;
+ list: (params: { calendarId: string; timeMin?: string; timeMax?: string; maxResults?: number; orderBy?: string; singleEvents?: boolean }) => Promise<{ data: { items: any[] } }>;
+ };
+
+ constructor(pool: pg.Pool) {
+ const self = this;
+
+ this.events = {
+ async insert({ calendarId, requestBody }: { calendarId: string; requestBody: any }) {
+ const startDateTime = requestBody.start?.dateTime;
+ const startTimeZone = requestBody.start?.timeZone || null;
+ const endDateTime = requestBody.end?.dateTime;
+ const endTimeZone = requestBody.end?.timeZone || null;
+ const result = await pool.query(
+ `INSERT INTO gcal.events (summary, description, location, start_datetime, start_timezone, end_datetime, end_timezone)
+ VALUES ($1, $2, $3, $4, $5, $6, $7)
+ RETURNING *`,
+ [
+ requestBody.summary || null,
+ requestBody.description || null,
+ requestBody.location || null,
+ startDateTime,
+ startTimeZone,
+ endDateTime,
+ endTimeZone,
+ ]
+ );
+ return { data: formatEvent(result.rows[0]) };
+ },
+
+ async get({ calendarId, eventId }: { calendarId: string; eventId: string }) {
+ const result = await pool.query(
+ `SELECT * FROM gcal.events WHERE id = $1`,
+ [eventId]
+ );
+ if (result.rows.length === 0) {
+ throw new Error(`Event not found: ${eventId}`);
+ }
+ return { data: formatEvent(result.rows[0]) };
+ },
+
+ async patch({ calendarId, eventId, requestBody }: { calendarId: string; eventId: string; requestBody: any }) {
+ const setClauses: string[] = [];
+ const values: any[] = [];
+ let paramIndex = 1;
+
+ if (requestBody.summary !== undefined) {
+ setClauses.push(`summary = $${paramIndex++}`);
+ values.push(requestBody.summary);
+ }
+ if (requestBody.description !== undefined) {
+ setClauses.push(`description = $${paramIndex++}`);
+ values.push(requestBody.description);
+ }
+ if (requestBody.location !== undefined) {
+ setClauses.push(`location = $${paramIndex++}`);
+ values.push(requestBody.location);
+ }
+ if (requestBody.start?.dateTime !== undefined) {
+ setClauses.push(`start_datetime = $${paramIndex++}`);
+ values.push(requestBody.start.dateTime);
+ }
+ if (requestBody.start?.timeZone !== undefined) {
+ setClauses.push(`start_timezone = $${paramIndex++}`);
+ values.push(requestBody.start.timeZone);
+ }
+ if (requestBody.end?.dateTime !== undefined) {
+ setClauses.push(`end_datetime = $${paramIndex++}`);
+ values.push(requestBody.end.dateTime);
+ }
+ if (requestBody.end?.timeZone !== undefined) {
+ setClauses.push(`end_timezone = $${paramIndex++}`);
+ values.push(requestBody.end.timeZone);
+ }
+
+ // Always update the updated timestamp
+ setClauses.push(`updated = NOW()`);
+
+ if (setClauses.length === 1) {
+ // Only the updated timestamp, no real changes; just fetch
+ const result = await pool.query(`SELECT * FROM gcal.events WHERE id = $1`, [eventId]);
+ if (result.rows.length === 0) throw new Error(`Event not found: ${eventId}`);
+ return { data: formatEvent(result.rows[0]) };
+ }
+
+ values.push(eventId);
+ const result = await pool.query(
+ `UPDATE gcal.events SET ${setClauses.join(', ')} WHERE id = $${paramIndex} RETURNING *`,
+ values
+ );
+ if (result.rows.length === 0) {
+ throw new Error(`Event not found: ${eventId}`);
+ }
+ return { data: formatEvent(result.rows[0]) };
+ },
+
+ async delete({ calendarId, eventId }: { calendarId: string; eventId: string }) {
+ const result = await pool.query(
+ `DELETE FROM gcal.events WHERE id = $1`,
+ [eventId]
+ );
+ if (result.rowCount === 0) {
+ throw new Error(`Event not found: ${eventId}`);
+ }
+ return { data: {} };
+ },
+
+ async list({ calendarId, timeMin, timeMax, maxResults, orderBy, singleEvents }: {
+ calendarId: string;
+ timeMin?: string;
+ timeMax?: string;
+ maxResults?: number;
+ orderBy?: string;
+ singleEvents?: boolean;
+ }) {
+ const conditions: string[] = [];
+ const values: any[] = [];
+ let paramIndex = 1;
+
+ if (timeMin) {
+ conditions.push(`start_datetime >= $${paramIndex++}`);
+ values.push(timeMin);
+ }
+ if (timeMax) {
+ conditions.push(`end_datetime <= $${paramIndex++}`);
+ values.push(timeMax);
+ }
+
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
+
+ let orderClause = 'ORDER BY start_datetime ASC';
+ if (orderBy === 'updated') {
+ orderClause = 'ORDER BY updated DESC';
+ }
+
+ const limitClause = maxResults ? `LIMIT $${paramIndex++}` : '';
+ if (maxResults) {
+ values.push(maxResults);
+ }
+
+ const result = await pool.query(
+ `SELECT * FROM gcal.events ${whereClause} ${orderClause} ${limitClause}`,
+ values
+ );
+
+ return { data: { items: result.rows.map(formatEvent) } };
+ },
+ };
+ }
+}
+
+// Schema definitions
+const CreateEventSchema = z.object({
+ summary: z.string().describe("Event title"),
+ start: z.object({
+ dateTime: z.string().describe("Start time (ISO format)"),
+ timeZone: z.string().optional().describe("Time zone"),
+ }),
+ end: z.object({
+ dateTime: z.string().describe("End time (ISO format)"),
+ timeZone: z.string().optional().describe("Time zone"),
+ }),
+ description: z.string().optional().describe("Event description"),
+ location: z.string().optional().describe("Event location"),
+});
+
+const GetEventSchema = z.object({
+ eventId: z.string().describe("ID of the event to retrieve"),
+});
+
+const UpdateEventSchema = z.object({
+ eventId: z.string().describe("ID of the event to update"),
+ summary: z.string().optional().describe("New event title"),
+ start: z.object({
+ dateTime: z.string().describe("New start time (ISO format)"),
+ timeZone: z.string().optional().describe("Time zone"),
+ }).optional(),
+ end: z.object({
+ dateTime: z.string().describe("New end time (ISO format)"),
+ timeZone: z.string().optional().describe("Time zone"),
+ }).optional(),
+ description: z.string().optional().describe("New event description"),
+ location: z.string().optional().describe("New event location"),
+});
+
+const DeleteEventSchema = z.object({
+ eventId: z.string().describe("ID of the event to delete"),
+});
+
+const ListEventsSchema = z.object({
+ timeMin: z.string().describe("Start of time range (ISO format)"),
+ timeMax: z.string().describe("End of time range (ISO format)"),
+ maxResults: z.number().optional().describe("Maximum number of events to return"),
+ orderBy: z.enum(['startTime', 'updated']).optional().describe("Sort order"),
+});
+
+// Main function
+async function main() {
+ // Initialize PgCalendar
+ const calendar = new PgCalendar(pool);
+ const calendarId = 'primary';
+
+ // Server implementation
+ const server = new Server({
+ name: "google-calendar",
+ version: "1.0.0",
+ capabilities: {
+ tools: {},
+ },
+ });
+
+ // Tool handlers
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
+ tools: [
+ {
+ name: "create_event",
+ description: "Creates a new event in Google Calendar",
+ inputSchema: zodToJsonSchema(CreateEventSchema),
+ },
+ {
+ name: "get_event",
+ description: "Retrieves details of a specific event",
+ inputSchema: zodToJsonSchema(GetEventSchema),
+ },
+ {
+ name: "update_event",
+ description: "Updates an existing event",
+ inputSchema: zodToJsonSchema(UpdateEventSchema),
+ },
+ {
+ name: "delete_event",
+ description: "Deletes an event from the calendar",
+ inputSchema: zodToJsonSchema(DeleteEventSchema),
+ },
+ {
+ name: "list_events",
+ description: "Lists events within a specified time range",
+ inputSchema: zodToJsonSchema(ListEventsSchema),
+ },
+ ],
+ }));
+
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
+ const { name, arguments: args } = request.params;
+
+ try {
+ switch (name) {
+ case "create_event": {
+ const validatedArgs = CreateEventSchema.parse(args);
+ const response = await calendar.events.insert({
+ calendarId,
+ requestBody: validatedArgs,
+ });
+ return {
+ content: [
+ {
+ type: "text",
+ text: `Event created with ID: ${response.data.id}\n` +
+ `Title: ${validatedArgs.summary}\n` +
+ `Start: ${validatedArgs.start.dateTime}\n` +
+ `End: ${validatedArgs.end.dateTime}`,
+ },
+ ],
+ };
+ }
+
+ case "get_event": {
+ const validatedArgs = GetEventSchema.parse(args);
+ const response = await calendar.events.get({
+ calendarId,
+ eventId: validatedArgs.eventId,
+ });
+ return {
+ content: [
+ {
+ type: "text",
+ text: JSON.stringify(response.data, null, 2),
+ },
+ ],
+ };
+ }
+
+ case "update_event": {
+ const validatedArgs = UpdateEventSchema.parse(args);
+ const { eventId, ...updates } = validatedArgs;
+ const response = await calendar.events.patch({
+ calendarId,
+ eventId,
+ requestBody: updates,
+ });
+ return {
+ content: [
+ {
+ type: "text",
+ text: `Event updated: ${eventId}\n` +
+ `New title: ${updates.summary || '(unchanged)'}\n` +
+ `New start: ${updates.start?.dateTime || '(unchanged)'}\n` +
+ `New end: ${updates.end?.dateTime || '(unchanged)'}`,
+ },
+ ],
+ };
+ }
+
+ case "delete_event": {
+ const validatedArgs = DeleteEventSchema.parse(args);
+ await calendar.events.delete({
+ calendarId,
+ eventId: validatedArgs.eventId,
+ });
+ return {
+ content: [
+ {
+ type: "text",
+ text: `Event deleted: ${validatedArgs.eventId}`,
+ },
+ ],
+ };
+ }
+
+ case "list_events": {
+ const validatedArgs = ListEventsSchema.parse(args);
+ const response = await calendar.events.list({
+ calendarId,
+ timeMin: validatedArgs.timeMin,
+ timeMax: validatedArgs.timeMax,
+ maxResults: validatedArgs.maxResults || 10,
+ orderBy: validatedArgs.orderBy || 'startTime',
+ singleEvents: true,
+ });
+ return {
+ content: [
+ {
+ type: "text",
+ text: `Found ${response.data.items?.length || 0} events:\n` +
+ JSON.stringify(response.data.items, null, 2),
+ },
+ ],
+ };
+ }
+
+ default:
+ throw new Error(`Unknown tool: ${name}`);
+ }
+ } catch (error) {
+ return {
+ content: [
+ {
+ type: "text",
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`,
+ },
+ ],
+ isError: true,
+ };
+ }
+ });
+
+ // Start the server
+ const transport = new StdioServerTransport();
+ server.connect(transport).catch((error) => {
+ console.error("Fatal error running server:", error);
+ process.exit(1);
+ });
+ console.error('Google Calendar MCP Server running on stdio');
+}
+
+main().catch(console.error);
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Calendar-Autoauth-MCP-Server/src/pg-calendar.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/Calendar-Autoauth-MCP-Server/src/pg-calendar.ts
new file mode 100644
index 00000000..d9eb98e5
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Calendar-Autoauth-MCP-Server/src/pg-calendar.ts
@@ -0,0 +1,239 @@
+import pg from 'pg';
+const { Pool } = pg;
+
+export interface CalendarEvent {
+ id: string;
+ summary: string | null;
+ description: string | null;
+ location: string | null;
+ start: { dateTime: string | null; timeZone?: string };
+ end: { dateTime: string | null; timeZone?: string };
+ status: string | null;
+ htmlLink: string | null;
+ creator: any;
+ organizer: any;
+ attendees: any;
+ recurrence: any;
+ reminders: any;
+ created: string | null;
+ updated: string | null;
+}
+
+interface EventRow {
+ id: string;
+ summary: string | null;
+ description: string | null;
+ location: string | null;
+ start_datetime: string | null;
+ start_timezone: string | null;
+ end_datetime: string | null;
+ end_timezone: string | null;
+ status: string | null;
+ html_link: string | null;
+ creator: any;
+ organizer: any;
+ attendees: any;
+ recurrence: any;
+ reminders: any;
+ created: string | null;
+ updated: string | null;
+}
+
+function formatEvent(row: EventRow): CalendarEvent {
+ return {
+ id: row.id,
+ summary: row.summary,
+ description: row.description,
+ location: row.location,
+ start: {
+ dateTime: row.start_datetime ? new Date(row.start_datetime).toISOString() : null,
+ timeZone: row.start_timezone || undefined,
+ },
+ end: {
+ dateTime: row.end_datetime ? new Date(row.end_datetime).toISOString() : null,
+ timeZone: row.end_timezone || undefined,
+ },
+ status: row.status,
+ htmlLink: row.html_link,
+ creator: row.creator,
+ organizer: row.organizer,
+ attendees: row.attendees,
+ recurrence: row.recurrence,
+ reminders: row.reminders,
+ created: row.created ? new Date(row.created).toISOString() : null,
+ updated: row.updated ? new Date(row.updated).toISOString() : null,
+ };
+}
+
+export class PgCalendar {
+ events: {
+ insert(params: { calendarId: string; requestBody: any }): Promise<{ data: CalendarEvent }>;
+ get(params: { calendarId: string; eventId: string }): Promise<{ data: CalendarEvent }>;
+ patch(params: { calendarId: string; eventId: string; requestBody: any }): Promise<{ data: CalendarEvent }>;
+ delete(params: { calendarId: string; eventId: string }): Promise<{ data: {} }>;
+ list(params: {
+ calendarId: string;
+ timeMin?: string;
+ timeMax?: string;
+ maxResults?: number;
+ orderBy?: string;
+ singleEvents?: boolean;
+ }): Promise<{ data: { items: CalendarEvent[] } }>;
+ };
+
+ constructor(pool: InstanceType) {
+ this.events = {
+ async insert({ calendarId, requestBody }) {
+ const startDateTime = requestBody.start?.dateTime;
+ const startTimeZone = requestBody.start?.timeZone || null;
+ const endDateTime = requestBody.end?.dateTime;
+ const endTimeZone = requestBody.end?.timeZone || null;
+ const attendeesJson = requestBody.attendees
+ ? JSON.stringify(requestBody.attendees)
+ : '[]';
+
+ const result = await pool.query(
+ `INSERT INTO gcal.events (summary, description, location, start_datetime, start_timezone, end_datetime, end_timezone, attendees)
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb)
+ RETURNING *`,
+ [
+ requestBody.summary || null,
+ requestBody.description || null,
+ requestBody.location || null,
+ startDateTime,
+ startTimeZone,
+ endDateTime,
+ endTimeZone,
+ attendeesJson,
+ ]
+ );
+ return { data: formatEvent(result.rows[0]) };
+ },
+
+ async get({ calendarId, eventId }) {
+ const result = await pool.query(
+ `SELECT * FROM gcal.events WHERE id = $1`,
+ [eventId]
+ );
+ if (result.rows.length === 0) {
+ throw new Error(`Event not found: ${eventId}`);
+ }
+ return { data: formatEvent(result.rows[0]) };
+ },
+
+ async patch({ calendarId, eventId, requestBody }) {
+ const setClauses: string[] = [];
+ const values: any[] = [];
+ let paramIndex = 1;
+
+ if (requestBody.summary !== undefined) {
+ setClauses.push(`summary = $${paramIndex++}`);
+ values.push(requestBody.summary);
+ }
+ if (requestBody.description !== undefined) {
+ setClauses.push(`description = $${paramIndex++}`);
+ values.push(requestBody.description);
+ }
+ if (requestBody.location !== undefined) {
+ setClauses.push(`location = $${paramIndex++}`);
+ values.push(requestBody.location);
+ }
+ if (requestBody.start?.dateTime !== undefined) {
+ setClauses.push(`start_datetime = $${paramIndex++}`);
+ values.push(requestBody.start.dateTime);
+ }
+ if (requestBody.start?.timeZone !== undefined) {
+ setClauses.push(`start_timezone = $${paramIndex++}`);
+ values.push(requestBody.start.timeZone);
+ }
+ if (requestBody.end?.dateTime !== undefined) {
+ setClauses.push(`end_datetime = $${paramIndex++}`);
+ values.push(requestBody.end.dateTime);
+ }
+ if (requestBody.end?.timeZone !== undefined) {
+ setClauses.push(`end_timezone = $${paramIndex++}`);
+ values.push(requestBody.end.timeZone);
+ }
+
+ // Always update the updated timestamp
+ setClauses.push(`updated = NOW()`);
+
+ if (setClauses.length === 1) {
+ // Only the updated timestamp, no real changes; just fetch
+ const result = await pool.query(
+ `SELECT * FROM gcal.events WHERE id = $1`,
+ [eventId]
+ );
+ if (result.rows.length === 0) throw new Error(`Event not found: ${eventId}`);
+ return { data: formatEvent(result.rows[0]) };
+ }
+
+ values.push(eventId);
+ const result = await pool.query(
+ `UPDATE gcal.events SET ${setClauses.join(', ')} WHERE id = $${paramIndex} RETURNING *`,
+ values
+ );
+ if (result.rows.length === 0) {
+ throw new Error(`Event not found: ${eventId}`);
+ }
+ return { data: formatEvent(result.rows[0]) };
+ },
+
+ async delete({ calendarId, eventId }) {
+ const result = await pool.query(
+ `DELETE FROM gcal.events WHERE id = $1`,
+ [eventId]
+ );
+ if (result.rowCount === 0) {
+ throw new Error(`Event not found: ${eventId}`);
+ }
+ return { data: {} };
+ },
+
+ async list({ calendarId, timeMin, timeMax, maxResults, orderBy, singleEvents }) {
+ const conditions: string[] = [];
+ const values: any[] = [];
+ let paramIndex = 1;
+
+ if (timeMin) {
+ conditions.push(`start_datetime >= $${paramIndex++}`);
+ values.push(timeMin);
+ }
+ if (timeMax) {
+ conditions.push(`end_datetime <= $${paramIndex++}`);
+ values.push(timeMax);
+ }
+
+ const whereClause = conditions.length > 0
+ ? `WHERE ${conditions.join(' AND ')}`
+ : '';
+
+ let orderClause = 'ORDER BY start_datetime ASC';
+ if (orderBy === 'updated') {
+ orderClause = 'ORDER BY updated DESC';
+ }
+
+ const limitClause = maxResults ? `LIMIT $${paramIndex++}` : '';
+ if (maxResults) {
+ values.push(maxResults);
+ }
+
+ const result = await pool.query(
+ `SELECT * FROM gcal.events ${whereClause} ${orderClause} ${limitClause}`,
+ values
+ );
+ return { data: { items: result.rows.map(formatEvent) } };
+ },
+ };
+ }
+}
+
+export function createPool(): InstanceType {
+ return new Pool({
+ host: process.env.PG_HOST || 'localhost',
+ port: parseInt(process.env.PG_PORT || '5432'),
+ database: process.env.PG_DATABASE || 'toolathlon',
+ user: process.env.PG_USER || 'postgres',
+ password: process.env.PG_PASSWORD || 'postgres',
+ });
+}
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Calendar-Autoauth-MCP-Server/tsconfig.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/Calendar-Autoauth-MCP-Server/tsconfig.json
new file mode 100644
index 00000000..3e3350f9
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Calendar-Autoauth-MCP-Server/tsconfig.json
@@ -0,0 +1,16 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "module": "ES2020",
+ "moduleResolution": "node",
+ "outDir": "./build",
+ "rootDir": "./src",
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true
+ },
+ "include": ["src/**/*"],
+ "exclude": ["node_modules", "build"]
+ }
+
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/.gitignore b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/.gitignore
new file mode 100644
index 00000000..b736b4a1
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/.gitignore
@@ -0,0 +1,3 @@
+node_modules
+build
+package-lock.json
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/HowToCook-mcp.dxt b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/HowToCook-mcp.dxt
new file mode 100644
index 00000000..720879d3
Binary files /dev/null and b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/HowToCook-mcp.dxt differ
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/README.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/README.md
new file mode 100644
index 00000000..be2af351
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/README.md
@@ -0,0 +1,227 @@
+# 🍳 HowToCook-MCP Server 🥘 -- 炫一周好饭,拒绝拼好饭
+
+[English](./README_EN.md) | 简体中文
+
+
+
+本项目 CDN 加速及安全防护由 Tencent EdgeOne 赞助
+
+[亚洲最佳 CDN、边缘和安全解决方案 - Tencent EdgeOne](https://edgeone.ai/zh?from=github)
+
+
+
+
+
+> 让 AI 助手变身私人大厨,为你的一日三餐出谋划策!
+
+基于[Anduin2017/HowToCook](https://github.com/Anduin2017/HowToCook)打造的 MCP(Model Context Protocol)服务器,让 AI 助手能够为你推荐菜谱、规划膳食,解决"今天吃什么"的世纪难题!
+
+数据来源:[Anduin2017/HowToCook](https://github.com/Anduin2017/HowToCook) ⭐ 没有 star 的同学快去点个星星吧!
+
+🎉 **想直接使用当前 MCP?立即体验** [https://howtocookmcp.weilei.site/](https://howtocookmcp.weilei.site/)
+
+🎉 **同时,我们也提供了 DXT(Desktop Extensions)供大家体验,一键安装到 Claude Desktop**
+
+如下:请确保你已经安装了最新版的 Claude Desktop, 当前 MCP 的 DXT 文件已上传代码库,可以自行下载或者 Fork 本仓库自行构建
+
+
+
+
+
+本地开发如何打包成 DXT?
+
+1.运行 `npm install -g @anthropic-ai/dxt`
+
+2.在包含本地 MCP 服务器的文件夹中,运行 `dxt init`。也就是您 MCP 的根目录,此命令将引导您创建`manifest.json`
+
+3.运行`dxt pack`创建 dxt 文件
+
+现在,任何支持 DXT 的应用都可以运行您的本地 MCP 服务器。例如,使用适用于 macOS 和 Windows 的 Claude 打开该文件即可显示安装对话框
+
+具体参阅:[anthropics/dxt](https://github.com/anthropics/dxt)
+
+## 📸 效果预览
+
+
+
+
+## 🔌 支持的 MCP 客户端
+
+本服务器适用于所有支持 MCP 协议的 AI 助手和客户端,包括但不限于:
+
+- 🤖 Claude 桌面应用
+- 📝 Cursor
+- 💼 其他支持 MCP 的客户端
+
+## ✨ 美味功能
+
+该 MCP 服务器提供以下美食工具:
+
+1. **📚 查询全部菜谱** - 获取所有可用菜谱数据,做菜百科全书 -- 慎用这个--上下文太大
+2. **🔍 根据分类查询菜谱** - 按照分类筛选菜谱,想吃水产?早餐?荤菜?主食?一键搞定!
+3. **📖 查询指定菜谱** - 根据菜谱名称查询特定菜谱的完整详情,包括食材、步骤等
+4. **🧩 智能推荐膳食** - 根据你的忌口、过敏原和用餐人数,为你规划整整一周的美味佳肴
+5. **🎲 不知道吃什么** - 选择困难症福音!根据人数直接推荐今日菜单,再也不用纠结了
+
+## 🚀 快速上手
+
+### 📋 先决条件
+
+- Node.js 16.0.0+ 🟢
+- npm 或 yarn 📦
+
+### 💻 安装步骤
+
+1. 克隆美食仓库
+
+```bash
+git clone https://github.com/worryzyy/howtocook-mcp.git
+cd howtocook-mcp
+```
+
+2. 安装依赖(就像准备食材一样简单!)
+
+```bash
+npm install
+```
+
+3. 编译代码(烹饪过程...)
+
+```bash
+npm run build
+```
+
+### 🎯 命令行参数
+
+服务器支持以下命令行参数:
+
+- `--transport ` - 选择传输方式(默认为 stdio)
+- `--port ` - 使用 http 或 sse 传输时的监听端口(默认为 3000)
+
+示例:使用 http 传输并监听 8080 端口
+
+```bash
+node build/index.js --transport http --port 8080
+```
+
+## 🍽️ 开始使用
+
+### 🔥 启动服务器
+
+```bash
+npm start
+```
+
+### 🔧 配置 MCP 客户端
+
+#### 推荐使用 Cursor 快速体验(两种方式)
+
+1. 使用 npm 包:请先运行 `npm i -g howtocook-mcp` ,否则会出现 `Failed to create client`
+
+然后在 Cursor 设置中添加 MCP 服务器配置:
+
+```json
+{
+ "mcpServers": {
+ "howtocook-mcp": {
+ "command": "npx",
+ "args": ["-y", "howtocook-mcp"]
+ }
+ }
+}
+```
+
+2. 如果是克隆仓库本地运行,请使用如下配置
+
+```json
+{
+ "mcpServers": {
+ "howtocook-mcp": {
+ "command": "node",
+ "args": ["youpath\\howtocook-mcp\\build\\index.js"]
+ }
+ }
+}
+```
+
+#### 其他 MCP 客户端
+
+对于其他支持 MCP 协议的客户端,请参考各自的文档进行配置,通常需要指定:
+
+- 服务器名称: `howtocook-mcp`
+- 命令: `npx -y howtocook-mcp`
+
+3. 重启客户端,让美食魔法生效 ✨
+
+## 🧙♂️ 菜单魔法使用指南
+
+以下是在各种 MCP 客户端中使用的示例提示语:
+
+### 1. 📚 查询全部菜谱
+
+无需参数,直接召唤美食全书!
+
+```
+请使用howtocook的MCP服务查询所有菜谱
+```
+
+### 2. 🔍 根据分类查询菜谱
+
+```
+请使用howtocook的MCP服务查询水产类的菜谱
+```
+
+参数:
+
+- `category`: 菜谱分类(水产、早餐、荤菜、主食等)
+
+### 3. 🧩 智能推荐一周菜谱
+
+```
+请使用howtocook的MCP服务为3人推荐一周菜谱,我们家不吃香菜,对虾过敏
+```
+
+参数:
+
+- `allergies`: 过敏原列表,如 ["大蒜", "虾"]
+- `avoidItems`: 忌口食材,如 ["葱", "姜"]
+- `peopleCount`: 用餐人数 (1-10)
+
+### 4. 🎲 今天吃什么?
+
+```
+请使用howtocook的MCP服务为4人晚餐推荐菜单
+```
+
+参数:
+
+- `peopleCount`: 用餐人数 (1-10)
+
+## 📝 小贴士
+
+- 该包已发布至 npm,可直接通过`npm install -g howtocook-mcp`全局安装
+- 本服务兼容所有支持 MCP 协议的 AI 助手和应用
+- 首次使用时,AI 可能需要一点时间来熟悉如何使用这些工具(就像烧热锅一样)
+
+## 🤝 贡献
+
+欢迎 Fork 和 Pull Request,让我们一起完善这个美食助手!
+
+## 📄 许可
+
+MIT License - 随意使用,就像分享美食配方一样慷慨!
+
+---
+
+> 🍴 美食即将开始,胃口准备好了吗?
+
+## 写在最后
+
+平时关注 MCP 比较多,特意新建了一个 MCP 的群聊,欢迎各位大佬加群讨论更多 MCP 的话题
+
+
+
+
+
+
+或者直接加作者 VX 进群:`worry3stone`, 请注明`MCP Exchange`,否则会被忽略哦
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/README_EN.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/README_EN.md
new file mode 100644
index 00000000..d52436cf
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/README_EN.md
@@ -0,0 +1,228 @@
+# 🍳 HowToCook-MCP Server 🥘 -- Plan Your Weekly Meals, No More Daily Struggles
+
+English | [简体中文](./README.md)
+
+
+
+CDN acceleration and security protection for this project are sponsored by Tencent EdgeOne.
+
+[Best Asian CDN, Edge, and Secure Solutions - Tencent EdgeOne](https://edgeone.ai/zh?from=github)
+
+
+
+
+
+> Turn your AI assistant into a personal chef that helps plan your daily meals!
+
+An MCP (Model Context Protocol) server based on [Anduin2017/HowToCook](https://github.com/Anduin2017/HowToCook), allowing AI assistants to recommend recipes, plan meals, and solve the age-old question of "what should I eat today?"
+
+Data Source: [Anduin2017/HowToCook](https://github.com/Anduin2017/HowToCook) ⭐ Don't forget to star the repo if you haven't already!
+
+🎉 **Want to use MCP right away? Try it now** [https://howtocookmcp.weilei.site/](https://howtocookmcp.weilei.site/)
+
+🎉 **At the same time, we also provide DXT (Desktop Extensions) for everyone to experience, one-click installation to Claude Desktop**
+
+As follows: Please make sure you have installed the latest version of Claude Desktop. The current MCP DXT file has been uploaded to the code library. You can download it yourself or fork this repository to build it yourself
+
+
+
+
+
+
+
+How to package local development into DXT?
+
+1. Run `npm install -g @anthropic-ai/dxt`
+
+2. In the folder containing the local MCP server, run `dxt init`. That is, the root directory of your MCP. This command will guide you to create `manifest.json`
+
+3. Run `dxt pack` to create a dxt file
+
+Now, any application that supports DXT can run your local MCP server. For example, opening the file with Claude for macOS and Windows will display the installation dialog
+
+For more information, see: [anthropics/dxt](https://github.com/anthropics/dxt)
+
+## 📸 Preview
+
+
+
+
+## 🔌 Supported MCP Clients
+
+This server works with all AI assistants and clients that support the MCP protocol, including but not limited to:
+
+- 🤖 Claude Desktop App
+- 📝 Cursor
+- 💼 Other MCP-compatible clients
+
+## ✨ Delicious Features
+
+This MCP server provides the following culinary tools:
+
+1. **📚 Query All Recipes** - Access all available recipe data, your complete cooking encyclopedia -- Use with caution due to large context size
+2. **🔍 Query Recipes by Category** - Filter recipes by category: seafood, breakfast, meat dishes, staple foods, and more!
+3. **🧩 Smart Meal Planning** - Get a full week's meal plan based on dietary restrictions, allergies, and number of diners
+4. **🎲 Don't Know What to Eat?** - Perfect for the indecisive! Get instant menu recommendations based on party size
+5. **🔎 Query Specific Recipe** - Search for specific recipes by name or ID, supports both exact and fuzzy matching to save tokens
+
+## 🚀 Quick Start
+
+### 📋 Prerequisites
+
+- Node.js 16.0.0+ 🟢
+- npm or yarn 📦
+
+### 💻 Installation
+
+1. Clone the repository
+
+```bash
+git clone https://github.com/worryzyy/howtocook-mcp.git
+cd howtocook-mcp
+```
+
+2. Install dependencies (as simple as preparing ingredients!)
+
+```bash
+npm install
+```
+
+3. Build the code (the cooking process...)
+
+```bash
+npm run build
+```
+
+### 🎯 CLI Arguments
+
+The server accepts the following command-line arguments:
+
+- `--transport ` - Transport to use (stdio by default)
+- `--port ` - Port to listen on when using http or sse transport (default 3000)
+
+Example with http transport and port 8080:
+
+```bash
+node build/index.js --transport http --port 8080
+```
+
+## ��️ Getting Started
+
+### 🔥 Start the Server
+
+```bash
+npm start
+```
+
+### 🔧 Configure MCP Clients
+
+#### It is recommended to use Cursor for quick experience (two methods)Cursor Configuration
+
+1. Using npm package: Please run `npm i -g howtocook-mcp` first, otherwise `Failed to create client` will appear
+
+Then add the MCP server configuration in Cursor settings:
+
+```json
+{
+ "mcpServers": {
+ "howtocook-mcp": {
+ "command": "npx",
+ "args": ["-y", "howtocook-mcp"]
+ }
+ }
+}
+```
+
+2. If running from a local cloned repository, use this configuration:
+
+```json
+{
+ "mcpServers": {
+ "howtocook-mcp": {
+ "command": "node",
+ "args": ["yourpath\\howtocook-mcp\\build\\index.js"]
+ }
+ }
+}
+```
+
+#### Other MCP Clients
+
+For other clients supporting the MCP protocol, refer to their respective documentation. Generally, you'll need to specify:
+
+- Server name: `howtocook-mcp`
+- Command: `npx -y howtocook-mcp`
+
+3. Restart the client to activate the culinary magic ✨
+
+## 🧙♂️ Culinary Magic Usage Guide
+
+Here are example prompts for using these tools in MCP clients:
+
+### 1. 📚 Query All Recipes
+
+No parameters needed, just summon the culinary encyclopedia!
+
+```
+Please use the howtocook MCP service to query all recipes
+```
+
+### 2. 🔍 Query Recipes by Category
+
+```
+Please use the howtocook MCP service to query seafood recipes
+```
+
+Parameters:
+
+- `category`: Recipe category (seafood, breakfast, meat dishes, staple foods, etc.)
+
+### 3. 🧩 Smart Meal Planning
+
+```
+Please use the howtocook MCP service to recommend a weekly meal plan for 3 people. We don't eat cilantro and are allergic to shrimp.
+```
+
+Parameters:
+
+- `allergies`: List of allergens, e.g., ["garlic", "shrimp"]
+- `avoidItems`: Dietary restrictions, e.g., ["green onion", "ginger"]
+- `peopleCount`: Number of diners (1-10)
+
+### 4. 🎲 What to Eat Today?
+
+```
+Please use the howtocook MCP service to recommend a dinner menu for 4 people
+```
+
+Parameters:
+
+- `peopleCount`: Number of diners (1-10)
+
+### 5. 🔎 Query Specific Recipe
+
+```
+Please use the howtocook MCP service to query the recipe for "Kung Pao Chicken"
+```
+
+Parameters:
+
+- `recipeId`: Recipe name or ID to search for
+
+## 📝 Tips
+
+- This package is published on npm and can be installed globally via `npm install -g howtocook-mcp`
+- Compatible with all AI assistants and applications that support the MCP protocol
+- On first use, AI may need some time to familiarize itself with these tools (like preheating an oven)
+
+## 🤝 Contributing
+
+Forks and Pull Requests are welcome! Let's improve this culinary assistant together!
+
+## 📄 License
+
+MIT License - Feel free to use, just like sharing your favorite recipes!
+
+---
+
+> 🍴 The feast is about to begin, is your appetite ready?
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/manifest.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/manifest.json
new file mode 100644
index 00000000..5f0534cf
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/manifest.json
@@ -0,0 +1,51 @@
+{
+ "dxt_version": "0.1",
+ "name": "howtocook-mcp",
+ "version": "0.1.1",
+ "description": "MCP Server for howtocook recipe database - 炫一周好饭,拒绝拼好饭",
+ "author": {
+ "name": "worry",
+ "email": "weileihhh@gmail.com",
+ "url": "https://github.com/worryzyy"
+ },
+ "homepage": "https://howtocookmcp.weilei.site",
+ "documentation": "https://github.com/worryzyy/HowToCook-mcp/blob/master/README.md",
+ "server": {
+ "type": "node",
+ "entry_point": "build/index.js",
+ "mcp_config": {
+ "command": "node",
+ "args": [
+ "${__dirname}/build/index.js"
+ ],
+ "env": {}
+ }
+ },
+ "tools": [
+ {
+ "name": "mcp_howtocook_getAllRecipes",
+ "description": "获取所有菜谱"
+ },
+ {
+ "name": "mcp_howtocook_getRecipeById",
+ "description": "根据菜谱名称或ID查询指定菜谱的完整详情,包括食材、步骤等"
+ },
+ {
+ "name": "mcp_howtocook_getRecipesByCategory",
+ "description": "根据分类查询菜谱,菜谱分类名称,如水产、早餐、荤菜、主食等"
+ },
+ {
+ "name": "mcp_howtocook_recommendMeals",
+ "description": "根据用户的忌口、过敏原、人数智能推荐菜谱,创建一周的膳食计划以及大致的购物清单"
+ },
+ {
+ "name": "mcp_howtocook_whatToEat",
+ "description": "不知道吃什么?根据人数直接推荐适合的菜品组合"
+ }
+ ],
+ "license": "ISC",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/worryzyy/HowToCook-mcp"
+ }
+}
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/package.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/package.json
new file mode 100644
index 00000000..32b8be97
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/package.json
@@ -0,0 +1,56 @@
+{
+ "name": "howtocook-mcp",
+ "version": "0.1.1",
+ "type": "module",
+ "main": "build/index.js",
+ "bin": {
+ "howtocook-mcp": "./build/index.js"
+ },
+ "files": [
+ "build",
+ "README.md"
+ ],
+ "scripts": {
+ "build": "tsc",
+ "prepare": "npm run build",
+ "start": "node build/index.js",
+ "start:stdio": "node build/index.js --transport stdio",
+ "start:http": "node build/index.js --transport http",
+ "start:sse": "node build/index.js --transport sse",
+ "dev": "tsc && node build/index.js",
+ "dev:stdio": "tsc && node build/index.js --transport stdio",
+ "dev:http": "tsc && node build/index.js --transport http",
+ "dev:sse": "tsc && node build/index.js --transport sse",
+ "prepublishOnly": "npm run build",
+ "publish:npm": "npm publish",
+ "publish:patch": "npm version patch && npm publish",
+ "publish:minor": "npm version minor && npm publish",
+ "publish:major": "npm version major && npm publish"
+ },
+ "keywords": [
+ "howtocook",
+ "mcp",
+ "server",
+ "recipe",
+ "food",
+ "cook"
+ ],
+ "author": "worry",
+ "license": "ISC",
+ "description": "MCP Server for howtocook recipe database - 炫一周好饭,拒绝拼好饭",
+ "dependencies": {
+ "@modelcontextprotocol/sdk": "^1.9.0",
+ "@types/express": "^5.0.3",
+ "commander": "^14.0.0",
+ "express": "^5.1.0",
+ "zod": "^3.22.4"
+ },
+ "devDependencies": {
+ "@types/node": "^20.11.24",
+ "typescript": "^5.3.3"
+ },
+ "publishConfig": {
+ "access": "public",
+ "registry": "https://registry.npmjs.org"
+ }
+}
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/public/dxt.png b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/public/dxt.png
new file mode 100644
index 00000000..aa7c52a8
Binary files /dev/null and b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/public/dxt.png differ
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/public/dxt2.png b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/public/dxt2.png
new file mode 100644
index 00000000..054d85c0
Binary files /dev/null and b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/public/dxt2.png differ
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/public/dxt3.png b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/public/dxt3.png
new file mode 100644
index 00000000..58e16a8a
Binary files /dev/null and b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/public/dxt3.png differ
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/public/edgeone.png b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/public/edgeone.png
new file mode 100644
index 00000000..576d9154
Binary files /dev/null and b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/public/edgeone.png differ
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/public/wechat.jpg b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/public/wechat.jpg
new file mode 100644
index 00000000..2f554cef
Binary files /dev/null and b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/public/wechat.jpg differ
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/src/data/all_recipes.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/src/data/all_recipes.json
new file mode 100644
index 00000000..ad3dece9
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/src/data/all_recipes.json
@@ -0,0 +1,52878 @@
+[
+ {
+ "id": "dishes-aquatic-咖喱炒蟹",
+ "name": "咖喱炒蟹的做法",
+ "description": "# 咖喱炒蟹的做法\n\n第一次吃咖喱炒蟹是在泰国的建兴酒家中餐厅,爆肉的螃蟹挂满有蟹黄味道的咖喱,味道真的绝,喜欢吃海鲜的程序员绝对不能错过。操作简单,对沿海的程序员非常友好。\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/aquatic/咖喱炒蟹.md",
+ "image_path": null,
+ "images": [],
+ "category": "水产",
+ "difficulty": 4,
+ "tags": [
+ "水产"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "青蟹(别称:肉蟹)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 青蟹(别称:肉蟹)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "咖喱块(推介乐惠蟹黄咖喱)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 咖喱块(推介乐惠蟹黄咖喱)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "洋葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 洋葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "椰浆",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 椰浆",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生粉(别称:淀粉)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生粉(别称:淀粉)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "肉蟹",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 肉蟹 1 只(大约 300g) * 份数",
+ "notes": "量未指定"
+ },
+ {
+ "name": "咖喱块",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 咖喱块 15g(一小块)*份数",
+ "notes": "量未指定"
+ },
+ {
+ "name": "椰浆",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 椰浆 100ml*份数",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋 1 个 *份数",
+ "notes": "量未指定"
+ },
+ {
+ "name": "洋葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 洋葱 200g *份数",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大蒜 5 瓣 *份数",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "肉蟹掀盖后对半砍开,蟹钳用刀背轻轻拍裂,切口和蟹钳蘸一下生粉,不要太多。撒 5g 生粉到蟹盖中,盖住蟹黄,备用"
+ },
+ {
+ "step": 2,
+ "description": "洋葱切成洋葱碎,备用"
+ },
+ {
+ "step": 3,
+ "description": "大蒜切碎,备用"
+ },
+ {
+ "step": 4,
+ "description": "烧一壶开水,备用"
+ },
+ {
+ "step": 5,
+ "description": "起锅烧油,倒入约 20ml 食用油,等待 10 秒让油温升高"
+ },
+ {
+ "step": 6,
+ "description": "将螃蟹切口朝下,轻轻放入锅中,煎 20 秒,这一步主要是封住蟹黄,蟹肉。然后翻面,每面煎 10 秒。煎完将螃蟹取出备用"
+ },
+ {
+ "step": 7,
+ "description": "将螃蟹盖放入锅中,使用勺子舀起锅中热油泼到蟹盖中,煎封住蟹盖中的蟹黄,煎 20 秒后取出备用"
+ },
+ {
+ "step": 8,
+ "description": "不用刷锅,再倒入 10ml 食用油,大火让油温升高至轻微冒烟,将大蒜末,洋葱碎倒入,炒 10 秒钟"
+ },
+ {
+ "step": 9,
+ "description": "将咖喱块放入锅中炒化(10 秒),放入煎好的螃蟹,翻炒均匀"
+ },
+ {
+ "step": 10,
+ "description": "倒入开水 300ml,焖煮 3 分钟。"
+ },
+ {
+ "step": 11,
+ "description": "焖煮完后,倒入椰浆和蛋清,关火,关火后不断翻炒,一直到酱汁变浓稠。"
+ },
+ {
+ "step": 12,
+ "description": "出锅"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。",
+ "做法参考:[十几年澳门厨房佬教学挂汁的咖喱蟹怎么做](https://www.bilibili.com/video/BV1Nq4y1W7K9)"
+ ]
+ },
+ {
+ "id": "dishes-aquatic-微波葱姜黑鳕鱼",
+ "name": "微波葱姜黑鳕鱼的做法",
+ "description": "# 微波葱姜黑鳕鱼的做法\n\n这道菜改编自西雅图 Veil 餐厅主厨 Johnny Zhu 的母亲 Margaret Lu 的菜谱。卢女士原菜谱是使用罗非鱼来做这道菜,Johnny 改为鳕鱼,但也可以用大比目鱼鱼排,或者海鲈鱼、鳟鱼等。每种鱼的密度有差别,烹饪时间要做微调。\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/aquatic/微波葱姜黑鳕鱼.md",
+ "image_path": null,
+ "images": [],
+ "category": "水产",
+ "difficulty": 3,
+ "tags": [
+ "水产"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "黑鳕鱼,带皮",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黑鳕鱼,带皮",
+ "notes": "量未指定"
+ },
+ {
+ "name": "青葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 青葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酱油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酱油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "芝麻油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 芝麻油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "花生油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 花生油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "密封袋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 密封袋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黑鳕鱼,带皮,2 片,450g(本菜谱主角,所有调料可根据鳕鱼的实际重量进行比例调整)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黑鳕鱼,带皮,2 片,450g(本菜谱主角,所有调料可根据鳕鱼的实际重量进行比例调整)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "青葱,葱白,25g。",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 青葱,葱白,25g。",
+ "notes": "量未指定"
+ },
+ {
+ "name": "青葱,葱绿,10g。",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 青葱,葱绿,10g。",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜,13g。",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜,13g。",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒,5mL。",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒,5mL。",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酱油,25mL。",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酱油,25mL。",
+ "notes": "量未指定"
+ },
+ {
+ "name": "芝麻油,2mL。",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 芝麻油,2mL。",
+ "notes": "量未指定"
+ },
+ {
+ "name": "花生油,50mL。",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 花生油,50mL。",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "鱼片分别放入密封袋,鱼皮向下放在盘子中。"
+ },
+ {
+ "step": 2,
+ "description": "取葱白切丝 25g,姜去皮后切丝,10g,混合在一起后分成两半,分别放在袋内鱼片上。"
+ },
+ {
+ "step": 3,
+ "description": "每个袋子倒入 2.5mL 料酒。"
+ },
+ {
+ "step": 4,
+ "description": "封好密封袋,放入微波炉中,中火(800 瓦)微波至*不透明且容易散开*时(约 3.5-5 分钟),从袋中取出鱼片。"
+ },
+ {
+ "step": 5,
+ "description": "去除青葱和姜。"
+ },
+ {
+ "step": 6,
+ "description": "取酱油 25mL,芝麻油 2mL,混合均匀后平均淋在两片鱼片上。"
+ },
+ {
+ "step": 7,
+ "description": "取葱绿切细丝 10g,姜去皮后切丝 3g,混合后分成两份撒在鱼片上。"
+ },
+ {
+ "step": 8,
+ "description": "取花生油 50mL,在小锅中加热至 190℃。"
+ },
+ {
+ "step": 9,
+ "description": "将热油淋到放油葱绿的鱼片上,立刻上桌。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-aquatic-水煮鱼",
+ "name": "水煮鱼的做法",
+ "description": "# 水煮鱼的做法\n\n水煮鱼是一道做法中等难度的硬菜。巴沙鱼富含优质蛋白且脂肪含量低,配合各种时令蔬菜十分营养健康。初学者一般需要 2 小时即可完成。\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/aquatic/水煮鱼.md",
+ "image_path": null,
+ "images": [],
+ "category": "水产",
+ "difficulty": 4,
+ "tags": [
+ "水产"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "巴沙鱼",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 巴沙鱼",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蔬菜(比如土豆片/豆芽/花菜/生菜/……)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蔬菜(比如土豆片/豆芽/花菜/生菜/……)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "红油豆瓣酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 红油豆瓣酱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "藤椒油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 藤椒油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "菜籽油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 菜籽油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白胡椒粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白胡椒粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜瓣",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜瓣",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "量杯",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 量杯",
+ "notes": "量未指定"
+ },
+ {
+ "name": "厨房秤(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 厨房秤(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大不锈钢碗",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大不锈钢碗",
+ "notes": "量未指定"
+ },
+ {
+ "name": "巴沙鱼",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 巴沙鱼 500g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蔬菜(比如土豆片/豆芽/花菜/生菜/……) 可有不同搭配,推荐合计重量",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蔬菜(比如土豆片/豆芽/花菜/生菜/……) 可有不同搭配,推荐合计重量 300g 至 500g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "红油豆瓣酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 红油豆瓣酱 40g (不怕辣想多加红油就多加 10 至 20g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "豆豉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 豆豉 10g (可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "藤椒油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 藤椒油 10ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "菜籽油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 菜籽油 25ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白胡椒粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白胡椒粉 3g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大蒜 2 瓣",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 糖 2g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "准备:巴沙鱼若是从冷冻柜里取出,需要放室温自然解冻 5 小时再做切片处理。"
+ },
+ {
+ "step": 2,
+ "description": "切片:巴沙鱼撇成薄片,约 5cm 长,3cm 宽。"
+ },
+ {
+ "step": 3,
+ "description": "[腌制](../../tips/learn/学习腌.md):将切好片的巴沙鱼放入大不锈钢碗中"
+ },
+ {
+ "step": 4,
+ "description": "加入 30g 豆瓣酱,3g 盐,10ml 藤椒油,3g 白胡椒粉"
+ },
+ {
+ "step": 5,
+ "description": "用手抓匀后加入 5ml 菜籽油收尾封住口味"
+ },
+ {
+ "step": 6,
+ "description": "常温静置至少 30 分钟入味。"
+ },
+ {
+ "step": 7,
+ "description": "备菜:大蒜切成蒜末。以 300g 花菜,200g 生菜为例,将花菜与生菜洗净。"
+ },
+ {
+ "step": 8,
+ "description": "焯水与炒菜:花菜[开水锅焯水](../../tips/learn/学习焯水.md)备用;将生菜洗净晾干,炒熟备用(不用放油)。"
+ },
+ {
+ "step": 9,
+ "description": "炒豆瓣酱:热锅冷油(菜籽油 20ml),加入 10g 豆瓣酱,10g 豆豉(可选),加入蒜末,**中火**慢炒。"
+ },
+ {
+ "step": 10,
+ "description": "汆鱼片:加入 150ml 热水,水很快开后加入腌制好的鱼片,轻轻翻动让鱼片在水中散开,加入 2g 盐和 2g 糖调味(此时可根据个人口味调整盐的用量)。水再次沸腾后即可盛盘。"
+ },
+ {
+ "step": 11,
+ "description": "盛盘:先将熟的蔬菜盛至大碗中,然后将热的鱼片盛在蔬菜上面,浇上锅中剩余热汤即可!"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-aquatic-清蒸生蚝",
+ "name": "清蒸生蚝的做法",
+ "description": "# 清蒸生蚝的做法\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/aquatic/清蒸生蚝.md",
+ "image_path": null,
+ "images": [],
+ "category": "水产",
+ "difficulty": 3,
+ "tags": [
+ "水产"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "生蚝",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生蚝",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酱油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酱油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "刷子",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 刷子",
+ "notes": "量未指定"
+ },
+ {
+ "name": "饮用水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 饮用水 1 升",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生蚝",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生蚝 6 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱 3 颗",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜 6 瓣",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜 1 小块",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酱油 每个生蚝",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酱油 每个生蚝 1 ml",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "将生蚝用刷子刷干净(没有刷子用牙刷)。"
+ },
+ {
+ "step": 2,
+ "description": "蒸锅中放水,将蒸屉放上之后,将 6 个生蚝平铺在蒸屉,使用 50%功率,蒸 3 分钟。"
+ },
+ {
+ "step": 3,
+ "description": "用右手拿着湿抹布掀开烫锅盖,将每个生蚝的外壳掀开一半去掉,生蚝的凸面向下,平面向上,每个放 1 根姜丝,10g 蒜末放到生蚝上。"
+ },
+ {
+ "step": 4,
+ "description": "关上烫锅盖,100%功率蒸 3.5 分钟。"
+ },
+ {
+ "step": 5,
+ "description": "停火,用右手拿着抹布掀开烫锅盖,每个放 5ml 酱油。"
+ },
+ {
+ "step": 6,
+ "description": "盛盘。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-aquatic-红烧鱼",
+ "name": "红烧鱼的做法",
+ "description": "# 红烧鱼的做法\n\n- **WARNING** 如果没有使用过菜刀剁过肉类食物,那么并不推荐使用该菜单!!!\n- 在操作中,锋利的菜刀可能会划伤手指,请一定要小心。\n\n- 此做法代表通用红烧鱼做法,材料分为必备和可添加~\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/aquatic/红烧鱼.md",
+ "image_path": null,
+ "images": [],
+ "category": "水产",
+ "difficulty": 4,
+ "tags": [
+ "水产"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "姜、蒜瓣、干辣椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜、蒜瓣、干辣椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "油、盐、料酒、醋、酱油、白砂糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 油、盐、料酒、醋、酱油、白砂糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鱼",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鱼",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鱼 建议新手以一条中等大小的鲫鱼上手,提前划好花刀,方便成熟(不然鱼背上容易夹生)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鱼 建议新手以一条中等大小的鲫鱼上手,提前划好花刀,方便成熟(不然鱼背上容易夹生)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜丝,以正常老姜切",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜丝,以正常老姜切 2-3 片,然后片丝即可",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜瓣",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜瓣 3-4 个,拍碎或者切碎或者切片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "干辣椒(依照个人口味)2-3 个,切碎",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 干辣椒(依照个人口味)2-3 个,切碎",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香菜按照个人口味",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香菜按照个人口味",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 10g,如果辣椒辣度高,建议多一点",
+ "notes": "量未指定"
+ },
+ {
+ "name": "醋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 醋 5ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酱油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酱油 5ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白砂糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白砂糖 10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱 1-2 根,正常就撒葱花",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小米椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小米椒 1-2,不放也可以,过来人的经验,最多放 2 个,不然辣度过高要菊花残。",
+ "notes": "量未指定"
+ },
+ {
+ "name": "味精,看个人口味,不要放多,5g 即可。",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 味精,看个人口味,不要放多,5g 即可。",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蚝油,5g 即可,和味精一个道理",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蚝油,5g 即可,和味精一个道理",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "姜蒜准备好,切碎"
+ },
+ {
+ "step": 2,
+ "description": "干辣椒切碎,和姜蒜一起"
+ },
+ {
+ "step": 3,
+ "description": "加入 30-50ml 油,等待锅热..."
+ },
+ {
+ "step": 4,
+ "description": "放入**擦干水分的鱼**(不想被热油溅一身的话),然后晃动锅,用热油煎鱼,注意这过程一定要小火"
+ },
+ {
+ "step": 5,
+ "description": "将鱼翻面,重复上面油煎过程"
+ },
+ {
+ "step": 6,
+ "description": "放入姜蒜辣椒,翻炒出香味"
+ },
+ {
+ "step": 7,
+ "description": "倒入料酒,稍微多一点,此过程注意安全,会起大量油烟"
+ },
+ {
+ "step": 8,
+ "description": "倒入醋(喜欢醋可以多放一点)"
+ },
+ {
+ "step": 9,
+ "description": "然后放入白砂糖,酱油(老抽)"
+ },
+ {
+ "step": 10,
+ "description": "加入冷水,以刚好淹没鱼身为宜,然后调成中火,盖上锅盖,大概 1 分钟后将鱼翻身,继续盖上锅盖"
+ },
+ {
+ "step": 11,
+ "description": "3-4 分钟后,加入盐、小米椒、蚝油(味精、鸡精等),然后继续盖上锅盖,后续继续要翻身"
+ },
+ {
+ "step": 12,
+ "description": "当锅内汤汁收汁到鱼的脊背线上的鱼鳍下面一点点的时候(或者汤汁不多的时候),转小火,加入香菜,葱花,然后盖上锅盖 20 秒,关火"
+ },
+ {
+ "step": 13,
+ "description": "起锅"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-aquatic-红烧鱼头",
+ "name": "红烧鱼头的做法",
+ "description": "# 红烧鱼头的做法\n\n- **WARNING** 如果没有使用过菜刀剁过肉类食物,那么并不推荐使用该菜单!!!\n- 在操作中,锋利的菜刀可能会划伤手指,请一定要小心。\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/aquatic/红烧鱼头.md",
+ "image_path": null,
+ "images": [],
+ "category": "水产",
+ "difficulty": 4,
+ "tags": [
+ "水产"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "大葱、姜、大蒜、香菜、美人椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大葱、姜、大蒜、香菜、美人椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "油、盐、鸡精、生抽、老抽、陈醋、黑胡椒粉、料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 油、盐、鸡精、生抽、老抽、陈醋、黑胡椒粉、料酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "八角、干辣椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 八角、干辣椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鱼头一个",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鱼头一个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "注:市场直接贩卖的鱼头一般分为两种:白鲢、花鲢。前者价格便宜,后者价格略贵,但口感也更佳!",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 注:市场直接贩卖的鱼头一般分为两种:白鲢、花鲢。前者价格便宜,后者价格略贵,但口感也更佳!",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鱼头一个",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鱼头一个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大葱 200g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜 80g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜瓣",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜瓣 3-4 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "美人椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 美人椒 1/4 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香菜 4 棵",
+ "notes": "量未指定"
+ },
+ {
+ "name": "八角两个,干辣椒五个",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 八角两个,干辣椒五个",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "葱、姜、蒜、香菜、美人椒分别清洗干净。"
+ },
+ {
+ "step": 2,
+ "description": "干辣椒与八角稍微冲洗即可。"
+ },
+ {
+ "step": 3,
+ "description": "大葱切两半。后半段大葱(葱白处)切段,每段长度约 4cm。前半段(葱叶处)先切段,再将每段劈为四瓣。"
+ },
+ {
+ "step": 4,
+ "description": "姜切片,每片厚度约 3mm。"
+ },
+ {
+ "step": 5,
+ "description": "大蒜拍碎。"
+ },
+ {
+ "step": 6,
+ "description": "拿出两棵香菜去根,切为 1.5cm 香菜碎。"
+ },
+ {
+ "step": 7,
+ "description": "将美人椒切为厚度为 3mm 的辣椒圈。"
+ },
+ {
+ "step": 8,
+ "description": "干辣椒切四段。"
+ },
+ {
+ "step": 9,
+ "description": "注:下文所述的鱼身是购买鱼头时所附带的鱼肉。"
+ },
+ {
+ "step": 10,
+ "description": "将鱼头去鳞,清洗鱼头处未被清理干净的内脏。"
+ },
+ {
+ "step": 11,
+ "description": "剁去鱼鳍、清理鱼鳃。"
+ },
+ {
+ "step": 12,
+ "description": "将鱼头下巴与鱼身连接的地方剁开,鱼身剁块,鱼头剁成四/六瓣。"
+ },
+ {
+ "step": 13,
+ "description": "注:鱼的处理很难用文字完全表述,可以搜索鱼头处理相关视频。"
+ },
+ {
+ "step": 14,
+ "description": "将剁好的鱼头进行清洗,最好洗掉鱼块上滞留的血水。"
+ },
+ {
+ "step": 15,
+ "description": "将清洗好的鱼块放入盆中,加入 5g 盐、10g 生抽、10g 料酒。放入葱(前半段切碎的那个)、1/3 姜片。将其拌匀,静置 1-2 小时。"
+ },
+ {
+ "step": 16,
+ "description": "加入 30ml 油,等待锅热..."
+ },
+ {
+ "step": 17,
+ "description": "油热,将锅关至小火"
+ },
+ {
+ "step": 18,
+ "description": "如果不明白为何要这样做,请查看[学习炒与煎](../../tips/learn/学习炒与煎.md)中的翻炒辅料。"
+ },
+ {
+ "step": 19,
+ "description": "放入姜片,慢慢翻炒,以姜片中的大部分汁水被炒出,以金黄色为准。"
+ },
+ {
+ "step": 20,
+ "description": "放入葱段,翻炒至葱段略显发白。"
+ },
+ {
+ "step": 21,
+ "description": "放入蒜碎、八角、干辣椒,翻炒 5 秒。"
+ },
+ {
+ "step": 22,
+ "description": "将腌制好的鱼头倒入锅中,翻炒 2-3 分钟。"
+ },
+ {
+ "step": 23,
+ "description": "倒入 500ml 清水,加入 2g 盐、3g 鸡精、5g 生抽、3g 老抽、5g 料酒、2g 黑胡椒粉、3g 陈醋。"
+ },
+ {
+ "step": 24,
+ "description": "将两棵香菜放入锅中,盖上锅盖。"
+ },
+ {
+ "step": 25,
+ "description": "调至大火,将水烧开。"
+ },
+ {
+ "step": 26,
+ "description": "调至中火,慢焖入味。"
+ },
+ {
+ "step": 27,
+ "description": "当汤汁减少一半时,打开锅盖。"
+ },
+ {
+ "step": 28,
+ "description": "调至大火收汁,汤汁剩余 1/3 时,关火盛至小盆中。"
+ },
+ {
+ "step": 29,
+ "description": "注:将锅中的汤汁均匀淋到鱼头上,盛盘时可以将锅中煮的香菜放入小盆底部,这样能让成品菜好看又好吃。"
+ },
+ {
+ "step": 30,
+ "description": "将香菜放至已经盛出的鱼头上,把切好的美人椒圈放在香菜之上。"
+ },
+ {
+ "step": 31,
+ "description": "色香味俱全的红烧鱼头出炉!"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-aquatic-红烧鲤鱼",
+ "name": "红烧鲤鱼的做法",
+ "description": "# 红烧鲤鱼的做法\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/aquatic/红烧鲤鱼.md",
+ "image_path": null,
+ "images": [],
+ "category": "水产",
+ "difficulty": 4,
+ "tags": [
+ "水产"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "大葱、姜、大蒜、干辣椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大葱、姜、大蒜、干辣椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "油、盐、生抽、老抽、陈醋、蚝油、料酒、白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 油、盐、生抽、老抽、陈醋、蚝油、料酒、白糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鲤鱼、五花肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鲤鱼、五花肉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鲤鱼 (大约",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鲤鱼 (大约 2 斤)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "五花肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 五花肉 100g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大葱 200g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜 80g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜瓣",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜瓣 3-4 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "干辣椒两个",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 干辣椒两个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖 50g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "葱、姜、蒜、干辣椒分别清洗干净。"
+ },
+ {
+ "step": 2,
+ "description": "葱白处切段,每段长度约 4cm,再将每段劈为四瓣。"
+ },
+ {
+ "step": 3,
+ "description": "姜切片,每片厚度约 3mm。"
+ },
+ {
+ "step": 4,
+ "description": "一个大蒜拍碎切末,其余蒜切为二瓣。"
+ },
+ {
+ "step": 5,
+ "description": "干辣椒切四段。"
+ },
+ {
+ "step": 6,
+ "description": "五花肉切片,约 4cm*4cm。"
+ },
+ {
+ "step": 7,
+ "description": "清洗鱼。"
+ },
+ {
+ "step": 8,
+ "description": "鱼背肉厚处拉几道斜口,方便入味"
+ },
+ {
+ "step": 9,
+ "description": "锅里多倒点油,烧至 7 成热(刚刚开始冒烟),下入鱼炸 1 分钟至鱼皮稍稍变硬捞出备用(注意不要一下锅就拨弄鱼,等炸一会再拨弄、翻面),炸鱼的油倒出,锅里留一点底油"
+ },
+ {
+ "step": 10,
+ "description": "将锅里底油烧热,下入五花肉,煸出香味。"
+ },
+ {
+ "step": 11,
+ "description": "放入干辣椒、葱、姜、蒜瓣,翻炒 1 分钟。"
+ },
+ {
+ "step": 12,
+ "description": "将炸好的鱼倒入锅中。"
+ },
+ {
+ "step": 13,
+ "description": "沿锅边倒入"
+ },
+ {
+ "step": 14,
+ "description": "调至中火,将水烧开。"
+ },
+ {
+ "step": 15,
+ "description": "调至小火,慢焖入味。"
+ },
+ {
+ "step": 16,
+ "description": "15 分钟 后,打开锅盖,挑出锅里的葱、姜、蒜、干辣椒。"
+ },
+ {
+ "step": 17,
+ "description": "调至大火收汁,汤汁剩余 1/4 时,撒点蒜末,关火盛出。"
+ },
+ {
+ "step": 18,
+ "description": "红烧鲤鱼出锅!"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-aquatic-小龙虾-小龙虾",
+ "name": "小龙虾的做法",
+ "description": "# 小龙虾的做法\n\n\n\n在家里做的小龙虾,肉质细嫩,鲜嫩多汁,干净卫生。\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/aquatic/小龙虾/小龙虾.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/小龙虾/成品.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/小龙虾/成品.jpg"
+ ],
+ "category": "水产",
+ "difficulty": 4,
+ "tags": [
+ "水产"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "小龙虾",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小龙虾",
+ "notes": "量未指定"
+ },
+ {
+ "name": "油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香叶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香叶",
+ "notes": "量未指定"
+ },
+ {
+ "name": "八角",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 八角",
+ "notes": "量未指定"
+ },
+ {
+ "name": "桂皮",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 桂皮",
+ "notes": "量未指定"
+ },
+ {
+ "name": "青花椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 青花椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "花椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 花椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "子弹头辣椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 子弹头辣椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱姜蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱姜蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "郫县豆瓣",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 郫县豆瓣",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黄豆酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黄豆酱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "啤酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 啤酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小龙虾 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小龙虾 = 2 斤",
+ "notes": "量未指定"
+ },
+ {
+ "name": "油 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 油 = 70 毫升(这是平时炒菜 3 倍量)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香叶 = 两片",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香叶 = 两片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "八角 = 一个",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 八角 = 一个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "桂皮 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 桂皮 = 3 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "青花椒 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 青花椒 = 10 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "花椒 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 花椒 = 10 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "子弹头辣椒 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 子弹头辣椒 = 5 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱 = 一根大葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱 = 一根大葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜 = 30 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜 = 7 瓣大蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "郫县豆瓣 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 郫县豆瓣 = 30 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黄豆酱 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黄豆酱 = 30 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "啤酒 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 啤酒 = 500 毫升",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 = 30 毫升",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 = 10 克",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "小龙虾刷干净去虾线,葱切 2cm 葱段,姜蒜切末。"
+ },
+ {
+ "step": 2,
+ "description": "烧油,油微热, 下香叶、八角、桂皮、青花椒、花椒、子弹头辣椒。"
+ },
+ {
+ "step": 3,
+ "description": "香料出香气之后下锅葱姜蒜"
+ },
+ {
+ "step": 4,
+ "description": "葱姜蒜爆香后,加入郫县豆瓣、黄豆酱,炒出红油。"
+ },
+ {
+ "step": 5,
+ "description": "下小龙虾,翻炒至变色。"
+ },
+ {
+ "step": 6,
+ "description": "加入啤酒,等啤酒烧开后加入生抽,盐。"
+ },
+ {
+ "step": 7,
+ "description": "将小龙虾完全煮熟后出锅。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-aquatic-干煎阿根廷红虾-干煎阿根廷红虾",
+ "name": "干煎阿根廷红虾的做法",
+ "description": "# 干煎阿根廷红虾的做法\n\n\n\n平常所见到虾,只有赴“汤”蹈“火”后,才能红!阿根廷虾很任性,一红就红一辈子!跟它住在北极的亲戚,北极虾一样,天生红。\n\n阿根廷红虾,之所以这么红,是因为它生活在深海中,使得它体内含有丰富的碘、磷及珍贵的虾青素等微量元素,能够增强人体免疫力,还对心脏活动具有重要调节作用,可以减少血液中的胆固醇含量。\n\n阿根廷红虾,不仅个大肥美,虾肉白如凝脂,细腻腴滑,口感鲜嫩,味道甜香浓郁,是虾类料理界的宠儿,看着真让人垂(chao)涎(ji)欲(xiang)滴(chi),快享受这大快朵颐的欢愉吧!\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/aquatic/干煎阿根廷红虾/干煎阿根廷红虾.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/干煎阿根廷红虾/干煎阿根廷红虾.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/干煎阿根廷红虾/干煎阿根廷红虾.jpg"
+ ],
+ "category": "水产",
+ "difficulty": 3,
+ "tags": [
+ "水产"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "阿根廷红虾(选用了速冻虾)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 阿根廷红虾(选用了速冻虾)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "海盐(研磨装)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 海盐(研磨装)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黑胡椒(研磨装)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黑胡椒(研磨装)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白葡萄酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白葡萄酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香菜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "柠檬",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 柠檬",
+ "notes": "量未指定"
+ },
+ {
+ "name": "洋葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 洋葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生姜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "阿根廷红虾",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 阿根廷红虾 2-3 只",
+ "notes": "量未指定"
+ },
+ {
+ "name": "海盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 海盐 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黑胡椒(研磨装)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黑胡椒(研磨装)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白葡萄酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白葡萄酒 20ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 1ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香菜 3 片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "柠檬",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 柠檬 1 片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "洋葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 洋葱 10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生姜 10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大蒜 10g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "阿根廷红虾解冻,最好是提前 1 天从速冻取出放到冷藏里自然解冻,能更好保持风味和口感。可买已经开背去虾线的,节省了不少时间"
+ },
+ {
+ "step": 2,
+ "description": "解冻好的红虾洗净擦干备用,注意这里一定要沥干水分,赶时间可以用厨房用纸吸干水分"
+ },
+ {
+ "step": 3,
+ "description": "生姜切片,洋葱切小方块,香菜洗干净后,叶茎分离,把香菜叶切碎,大蒜压碎切成小块碎末"
+ },
+ {
+ "step": 4,
+ "description": "大火热锅,热锅后倒入两调羹橄榄油,等油温升高后,放入生姜片,洋葱块和香菜茎煸炒"
+ },
+ {
+ "step": 5,
+ "description": "约 1 分钟后取出生姜,洋葱和香菜茎,弃用"
+ },
+ {
+ "step": 6,
+ "description": "调中大火,放入红虾开始煎,注意所有虾需要单面都完整接触平底锅,煎约 2 分钟,同时给每只虾刷上一层油"
+ },
+ {
+ "step": 7,
+ "description": "待底面虾壳有微微焦黄时翻面,并撒入大蒜碎末,轻微晃动平底锅使得受热均匀"
+ },
+ {
+ "step": 8,
+ "description": "约 1 分钟后添加 20ml 白葡萄酒"
+ },
+ {
+ "step": 9,
+ "description": "再煎 1 分钟后调中小火,均匀撒上一层盐和黑胡椒"
+ },
+ {
+ "step": 10,
+ "description": "给每只虾滴上一滴生抽"
+ },
+ {
+ "step": 11,
+ "description": "撒上香菜叶,装盘"
+ },
+ {
+ "step": 12,
+ "description": "切好柠檬片,摆放到盘边即可"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-aquatic-油焖大虾-油焖大虾",
+ "name": "油焖大虾的做法",
+ "description": "# 油焖大虾的做法\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/aquatic/油焖大虾/油焖大虾.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/油焖大虾/油焖大虾.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/油焖大虾/油焖大虾.jpg"
+ ],
+ "category": "水产",
+ "difficulty": 4,
+ "tags": [
+ "水产"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "黑虎虾 or 明虾、",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黑虎虾 or 明虾、",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱、姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱、姜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒、盐、冰糖、植物油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒、盐、冰糖、植物油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "虾",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 虾 10 只",
+ "notes": "量未指定"
+ },
+ {
+ "name": "花椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 花椒 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱 50g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜 20g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黄酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黄酒 30g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 3g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冰糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冰糖 10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "植物油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 植物油",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "剪虾枪到根上,虾须虾爪都剪掉,沙包挑掉,开背虾线挑出来,洗净备用"
+ },
+ {
+ "step": 2,
+ "description": "炸料油"
+ },
+ {
+ "step": 3,
+ "description": "下油,虾摆放整齐,两面变色后轻轻摁虾头"
+ },
+ {
+ "step": 4,
+ "description": "大火烧开转小火盖盖子闷(中途不能再加汤水,不要开盖)"
+ },
+ {
+ "step": 5,
+ "description": "皮亮虾弯就可以起锅,虾摆盘"
+ },
+ {
+ "step": 6,
+ "description": "收汁(过滤后倒回锅里收浓,放葱油 ) 汤汁剩余 1/4 时。"
+ },
+ {
+ "step": 7,
+ "description": "浇汁"
+ },
+ {
+ "step": 8,
+ "description": "完成"
+ },
+ {
+ "step": 9,
+ "description": ""
+ },
+ {
+ "step": 10,
+ "description": "开吃✅"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-aquatic-混合烤鱼-烤鱼",
+ "name": "烤鱼的做法",
+ "description": "# 烤鱼的做法\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/aquatic/混合烤鱼/烤鱼.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/混合烤鱼/烤鱼.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/混合烤鱼/烤鱼.jpg"
+ ],
+ "category": "水产",
+ "difficulty": 4,
+ "tags": [
+ "水产"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "草鱼(农贸市场或者超市让店家杀掉,去除不要的器官)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 草鱼(农贸市场或者超市让店家杀掉,去除不要的器官)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白胡椒粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白胡椒粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "桂皮",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 桂皮",
+ "notes": "量未指定"
+ },
+ {
+ "name": "八角",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 八角",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香叶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香叶",
+ "notes": "量未指定"
+ },
+ {
+ "name": "青花椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 青花椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "干辣椒段",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 干辣椒段",
+ "notes": "量未指定"
+ },
+ {
+ "name": "灯笼椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 灯笼椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "火锅底料(随意)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 火锅底料(随意)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "千张",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 千张",
+ "notes": "量未指定"
+ },
+ {
+ "name": "绿豆芽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 绿豆芽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "洋葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 洋葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "豆瓣酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 豆瓣酱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "芹菜段",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 芹菜段",
+ "notes": "量未指定"
+ },
+ {
+ "name": "熟花生米",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 熟花生米",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白芝麻",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白芝麻",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香菜(放更好吃,根据个人口味可放可不放)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香菜(放更好吃,根据个人口味可放可不放)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "草鱼 大约三斤",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 草鱼 大约三斤",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大葱 半根",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大葱 半根",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 20ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒 10-15ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用盐 5-10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白胡椒粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白胡椒粉 5g-10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "桂皮 一小片",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 桂皮 一小片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "八角 两个",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 八角 两个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大蒜粒 八个",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大蒜粒 八个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香叶 两张",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香叶 两张",
+ "notes": "量未指定"
+ },
+ {
+ "name": "青花椒 一小把",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 青花椒 一小把",
+ "notes": "量未指定"
+ },
+ {
+ "name": "干辣椒段",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 干辣椒段 10 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "灯笼椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 灯笼椒 4 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "芹菜段 两根",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 芹菜段 两根",
+ "notes": "量未指定"
+ },
+ {
+ "name": "洋葱 半个",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 洋葱 半个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "千张 一张",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 千张 一张",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "草鱼(一般 3 斤 )从背部切开,两面沿着鱼的背部往下划几刀,不要划到鱼肚皮,不然不易定型"
+ },
+ {
+ "step": 2,
+ "description": "把鱼放到容器中,加入料酒,10g 白胡椒粉,5g 食盐抹匀腌制二十分钟入味。"
+ },
+ {
+ "step": 3,
+ "description": "把半根大葱切成一块一块,大蒜粒中间切开,和八角香叶桂皮放在一个容器中"
+ },
+ {
+ "step": 4,
+ "description": "干辣椒段中间一分为二切开并和灯笼椒装在一个容器中"
+ },
+ {
+ "step": 5,
+ "description": "芹菜切小段"
+ },
+ {
+ "step": 6,
+ "description": "豆芽焯水"
+ },
+ {
+ "step": 7,
+ "description": "千张焯水切成丝"
+ },
+ {
+ "step": 8,
+ "description": "洋葱切成丝。"
+ },
+ {
+ "step": 9,
+ "description": "烤制鱼"
+ },
+ {
+ "step": 10,
+ "description": "锅中撒上 20ml 食用油,等到油热后,把大葱大蒜八角香叶倒入炒香"
+ },
+ {
+ "step": 11,
+ "description": "加上一包火锅底料的一半和 15-20g 豆瓣酱,炒出红油"
+ },
+ {
+ "step": 12,
+ "description": "加入 5g 白糖,10g 食盐,5ml 生抽调味,倒入和食材齐平的清水煮开"
+ },
+ {
+ "step": 13,
+ "description": "依次下入芹菜段,豆芽,千张丝,不用煮熟,稍微烫一下后铺上洋葱丝,放上烤鱼"
+ },
+ {
+ "step": 14,
+ "description": "加入干辣椒,灯笼椒,青花椒"
+ },
+ {
+ "step": 15,
+ "description": "另一个锅烧油,油热后浇在刚加入的辣椒上面激发出香味"
+ },
+ {
+ "step": 16,
+ "description": "最后撒上熟花生米,葱花,白芝麻,香菜"
+ },
+ {
+ "step": 17,
+ "description": "煮 5-6 分钟,美味即成。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-aquatic-清蒸鲈鱼-清蒸鲈鱼",
+ "name": "清蒸鲈鱼的做法",
+ "description": "# 清蒸鲈鱼的做法\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/aquatic/清蒸鲈鱼/清蒸鲈鱼.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/清蒸鲈鱼/摆盘.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/清蒸鲈鱼/摆盘.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/清蒸鲈鱼/改刀.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/清蒸鲈鱼/清蒸鲈鱼.jpg"
+ ],
+ "category": "水产",
+ "difficulty": 3,
+ "tags": [
+ "水产"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "鲈鱼(害怕杀鱼的同学可以让店家帮忙杀)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鲈鱼(害怕杀鱼的同学可以让店家帮忙杀)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒸鱼豉油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒸鱼豉油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鲈鱼 一条",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鲈鱼 一条",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香葱 三根",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香葱 三根",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜 一块",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜 一块",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 10-15ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒸鱼豉油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒸鱼豉油 10-15ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒 10-15ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用盐 5-10g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "姜切片切丝、香葱的葱白切段,葱绿切丝,切丝后放入冷水浸泡备用。"
+ },
+ {
+ "step": 2,
+ "description": "鲈鱼处理好后洗净,用厨房纸擦干,两面分别划几刀,用盐洗掉鱼身的粘液,并用 10g 盐抹遍鱼身的内外,腌制 10 分钟以上。"
+ },
+ {
+ "step": 3,
+ "description": "补充一个鲈鱼改刀和摆盘的方法,改刀后可以让鲈鱼立起来蒸,均匀受热,同时吃起来更加方便,无需翻面。"
+ },
+ {
+ "step": 4,
+ "description": ""
+ },
+ {
+ "step": 5,
+ "description": ""
+ },
+ {
+ "step": 6,
+ "description": "鱼肚内塞上姜和葱白,鱼身也撒上姜和葱白,量为备用的一半。蒸鱼的碟子用筷子将鱼跟碟子隔开蒸"
+ },
+ {
+ "step": 7,
+ "description": "水烧热感觉到水温后放进入鱼"
+ },
+ {
+ "step": 8,
+ "description": "大火清蒸 10 分钟。"
+ },
+ {
+ "step": 9,
+ "description": "蒸好的鱼,用干净的盘子装起来并去除身上姜蒜"
+ },
+ {
+ "step": 10,
+ "description": "鱼身浇上 15ml 蒸鱼豉油"
+ },
+ {
+ "step": 11,
+ "description": "鱼身重新撒上姜和葱丝,锅内加上 10ml 食用油并烧热,将食用油淋至鱼身即可出菜"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-aquatic-白灼虾-白灼虾",
+ "name": "白灼虾的做法",
+ "description": "# 白灼虾的做法\n\n白灼虾非常适合程序员在沿海地区做,类似于清蒸鱼:简单容错、有营养、有满足感,甚至很好看。\n\n预估烹饪难度:★★",
+ "source_path": "dishes/aquatic/白灼虾/白灼虾.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/白灼虾/白灼虾.webp",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/白灼虾/白灼虾.webp"
+ ],
+ "category": "水产",
+ "difficulty": 2,
+ "tags": [
+ "水产"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "活虾",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 活虾",
+ "notes": "量未指定"
+ },
+ {
+ "name": "洋葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 洋葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酱油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酱油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "芝麻",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 芝麻",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蚝油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蚝油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香醋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香醋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "虾",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 虾 250g * 份数(建议 1-2 人份)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱 一根",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱 一根",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜 一块",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜 一块",
+ "notes": "量未指定"
+ },
+ {
+ "name": "洋葱 一头",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 洋葱 一头",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜 5-8 瓣",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 10-15ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒 20 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酱油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酱油 10-15ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "芝麻 一把",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 芝麻 一把",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香醋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香醋 10 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蚝油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蚝油 10 ml",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "洋葱切小块,姜切片,平铺平底锅。"
+ },
+ {
+ "step": 2,
+ "description": "活虾冲洗一下(去除虾线、剪刀减掉虾腿虾须子都是可选操作),控水,铺在平底锅的洋葱、姜片之上。"
+ },
+ {
+ "step": 3,
+ "description": "锅内倒入料酒,盖上锅盖,中火 1 分钟,小火 5 分钟,关火 5 分钟。"
+ },
+ {
+ "step": 4,
+ "description": "和上一步并行操作,制作蘸料:"
+ },
+ {
+ "step": 5,
+ "description": "虾出锅,用干净的盘子装好。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-aquatic-糖醋鲤鱼-糖醋鲤鱼",
+ "name": "糖醋鲤鱼的做法",
+ "description": "# 糖醋鲤鱼的做法\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/aquatic/糖醋鲤鱼/糖醋鲤鱼.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/糖醋鲤鱼/成品.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/糖醋鲤鱼/成品.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/糖醋鲤鱼/腌制.jpg"
+ ],
+ "category": "水产",
+ "difficulty": 4,
+ "tags": [
+ "水产"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "鲤鱼",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鲤鱼",
+ "notes": "量未指定"
+ },
+ {
+ "name": "番茄酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 番茄酱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白醋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白醋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "淀粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 淀粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香菜一颗",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香菜一颗",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盆(两个)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盆(两个)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "菜刀一个",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 菜刀一个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "笊篱一个、锅铲一个",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 笊篱一个、锅铲一个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鲤鱼 = 约",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鲤鱼 = 约 3 斤",
+ "notes": "量未指定"
+ },
+ {
+ "name": "清水 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 清水 = 50g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "番茄酱 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 番茄酱 = 40g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖 = 20g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白醋 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白醋 = 10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "淀粉 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 淀粉 = 10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 = 30g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大葱 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大葱 = 30g(约半颗)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜 = 30g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒 = 25g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "将鱼清洗干净,确保无鱼鳞等异物"
+ },
+ {
+ "step": 2,
+ "description": "将鱼头朝左,鱼肚朝下,右手持刀。刀竖直切下 1cm,按紧鱼身往左片 3-4cm,再将鱼片中间轻轻划一刀"
+ },
+ {
+ "step": 3,
+ "description": "将鱼放进盆里,然后将大姜切片,大葱切段(随便切切就行了,主要是需要去腥味)"
+ },
+ {
+ "step": 4,
+ "description": "用吃奶的力气将大葱大姜里的汁水挤到盆中"
+ },
+ {
+ "step": 5,
+ "description": "加入 20g 盐,25g 料酒,然后给鲤鱼搓个澡,涂抹均匀"
+ },
+ {
+ "step": 6,
+ "description": ""
+ },
+ {
+ "step": 7,
+ "description": "找个干净的盆,加入 100g 面粉、200g 淀粉、180g 水、5g 盐,用手将其搅拌均匀,面糊此时粘稠呈可拉丝状态,然后打入一个鸡蛋,再次搅匀"
+ },
+ {
+ "step": 8,
+ "description": "等待 30 分钟"
+ },
+ {
+ "step": 9,
+ "description": "将鱼放在案板上,用干毛巾将鱼身上的水擦干(这样可以更好的挂糊)"
+ },
+ {
+ "step": 10,
+ "description": "将盆冲洗干净,用干毛巾擦干"
+ },
+ {
+ "step": 11,
+ "description": "起锅烧油,加入约 1L 的油,将油温烧至 7 成热,约 200-240 度"
+ },
+ {
+ "step": 12,
+ "description": "捏起鱼的尾巴,将鱼头沉入锅底,用勺子往鱼的身上淋热油,待面糊成型后,将鱼慢慢放入锅中,拿锅铲轻轻铲起鱼的头部,然后垫上笊篱。防止底部炸糊。"
+ },
+ {
+ "step": 13,
+ "description": "准备一个盛鱼的盘子,放在锅的旁边。"
+ },
+ {
+ "step": 14,
+ "description": "用锅铲从鱼身处轻轻铲入,两个工具配合鱼翻个身。再炸两分钟,还是同样的方式(笊篱托着鱼头,锅铲托着鱼身,将鱼盛入盘中)"
+ },
+ {
+ "step": 15,
+ "description": "将锅中的油倒入擦干的盆中,放置一边,然后将锅刷干净"
+ },
+ {
+ "step": 16,
+ "description": "将 50g 清水、40g 番茄酱、20g 白糖、10g 白醋放入小碗中,搅拌均匀"
+ },
+ {
+ "step": 17,
+ "description": "再准备一个小碗加入 10g 淀粉、10g 水,搅拌成水淀粉"
+ },
+ {
+ "step": 18,
+ "description": "开大火将锅烧热,然后倒入之前准备的料汁,大火烧开,转小火"
+ },
+ {
+ "step": 19,
+ "description": "加入调好的水淀粉,边倒边搅拌,然后 20 秒后关火"
+ },
+ {
+ "step": 20,
+ "description": "将熬好的糖醋汁用勺子均匀地浇在鱼身上,可以加点香菜或葱花点缀,糖醋鲤鱼就做好了"
+ },
+ {
+ "step": 21,
+ "description": ""
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-aquatic-芥末黄油罗氏虾-芥末黄油罗氏虾",
+ "name": "芥末黄油罗氏虾的做法",
+ "description": "# 芥末黄油罗氏虾的做法\n\n\n这是一道做法简单,味道美味,具有新意的海鲜菜。\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/aquatic/芥末黄油罗氏虾/芥末黄油罗氏虾.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/芥末黄油罗氏虾/芥末黄油罗氏虾.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/芥末黄油罗氏虾/芥末黄油罗氏虾.jpg"
+ ],
+ "category": "水产",
+ "difficulty": 3,
+ "tags": [
+ "水产"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "罗氏虾",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 罗氏虾",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黄油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黄油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "芥末",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 芥末",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蚝油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蚝油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒、朗姆酒或啤酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒、朗姆酒或啤酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香菜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "罗氏虾",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 罗氏虾 1 斤多 广东市场价大概 40~45 一斤",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黄油 约",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黄油 约 20g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "芥末",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 芥末 15g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖 3g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 30g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蚝油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蚝油 30g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 3g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒、朗姆酒或啤酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒、朗姆酒或啤酒 15g 到 30g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香菜 5 条 切段",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜 5 颗 剁成蒜蓉",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "将罗氏虾剪掉头尾尖刺、触须和脚,剪刀把虾身开背,去除虾线。"
+ },
+ {
+ "step": 2,
+ "description": "提前搅拌好芥末酱汁:酱油、蚝油、芥末、盐、糖,搅拌均匀!"
+ },
+ {
+ "step": 3,
+ "description": "洗好香菜,切段备用。"
+ },
+ {
+ "step": 4,
+ "description": "罗氏虾沥掉水,锅中加入油,直接放入罗氏虾,中火,外表煎至金黄,捞出。"
+ },
+ {
+ "step": 5,
+ "description": "下入蒜蓉,大火,利用煎虾剩下的油继续煎炒蒜蓉,等到锅中白雾冒出,蒜蓉已经煎出香味,下虾和黄油,让虾充分吸收黄油香味"
+ },
+ {
+ "step": 6,
+ "description": "下入调好的酱汁,继续大火煮沸,翻炒虾,至酱汁收汁,加入酒(料酒、啤酒可以放 30g,朗姆酒味道浓郁放 15g 即可。)"
+ },
+ {
+ "step": 7,
+ "description": "在等酱汁稍微收汁,加入香菜翻炒两下,即可出锅。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-aquatic-葱油桂鱼-葱油桂鱼",
+ "name": "葱油桂鱼的做法",
+ "description": "# 葱油桂鱼的做法\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/aquatic/葱油桂鱼/葱油桂鱼.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/葱油桂鱼/10.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/葱油桂鱼/10.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/葱油桂鱼/葱油桂鱼.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/葱油桂鱼/葱油鲈鱼.jpg"
+ ],
+ "category": "水产",
+ "difficulty": 4,
+ "tags": [
+ "水产"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "桂鱼",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 桂鱼",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小米辣",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小米辣",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "植物油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 植物油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒸鱼豉油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒸鱼豉油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒸笼(含蒸锅)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒸笼(含蒸锅)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "清水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 清水",
+ "notes": "量未指定"
+ },
+ {
+ "name": "砧板",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 砧板",
+ "notes": "量未指定"
+ },
+ {
+ "name": "铁锅",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 铁锅",
+ "notes": "量未指定"
+ },
+ {
+ "name": "塑料盘或塑料盆(腌鱼用)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 塑料盘或塑料盆(腌鱼用)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "一次性手套",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 一次性手套",
+ "notes": "量未指定"
+ },
+ {
+ "name": "厨房纸",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 厨房纸",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒸鱼盘子(能平放下一条鱼即可)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒸鱼盘子(能平放下一条鱼即可)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "菜刀",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 菜刀",
+ "notes": "量未指定"
+ },
+ {
+ "name": "削皮刀",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 削皮刀",
+ "notes": "量未指定"
+ },
+ {
+ "name": "防烫盘夹(或者防烫手套)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 防烫盘夹(或者防烫手套)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "桂鱼 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 桂鱼 = 1 斤(500g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小葱 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小葱 = 1 根(长度为 30cm)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小米辣 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小米辣 = 2 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜 = 50g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒 = 25g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "植物油 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 植物油 = 15g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 = 8g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒸鱼豉油 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒸鱼豉油 = 10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "清水 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 清水 = 5L",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "去菜市场买已经处理好的鱼(自己处理的话最好不要内脏),将鱼身表面的所有鳞片刮干净"
+ },
+ {
+ "step": 2,
+ "description": "用厨房用纸将鱼肚子里的贴骨血和黑膜擦干净(帖骨血会影响口感,黑膜是鱼腥味的来源)"
+ },
+ {
+ "step": 3,
+ "description": "用菜刀在鱼身表面来回刮几次,将鱼身的黏液刮掉,进一步去除腥味,然后用清水将鱼内外冲洗干净"
+ },
+ {
+ "step": 4,
+ "description": "将鱼平放在砧板,使用厨房纸将鱼内外的水分擦干,然后鱼头朝左,尾朝右,从鱼鳃边开始,每隔 3cm 纵向划一刀,深度达到鱼的脊椎骨即可,另一面使用同样的处理方式"
+ },
+ {
+ "step": 5,
+ "description": "将鱼平放在盆中,确保盘中没有多余水分"
+ },
+ {
+ "step": 6,
+ "description": "取一块 50g 姜(鸡蛋大小),用削皮刀把表面的皮去除并洗干净,然后切成厚度为 3mm 的姜片"
+ },
+ {
+ "step": 7,
+ "description": "将小米辣洗干净、去蒂,切成厚度为 2mm 的小圆片(或切成 1mm 宽度的丝状)"
+ },
+ {
+ "step": 8,
+ "description": "将小葱洗干净,去除根须,切成 3cm 的小段,稍微粗一点的小葱,可以沿着小葱生长的方向沿中间劈开"
+ },
+ {
+ "step": 9,
+ "description": "加入 8g 盐,25g 料酒到盆中,带上一次性手套,然后对鱼进行全身按摩 1 分钟,确保鱼身每个部位都均匀涂抹了盐和料酒"
+ },
+ {
+ "step": 10,
+ "description": "按摩好鱼后,在鱼身的每一个刀口中塞入一片姜片,鱼肚子中放入 3 片姜片,腌制 10 分钟(建议不要腌制太久,否则鱼的鲜度降低)"
+ },
+ {
+ "step": 11,
+ "description": "在鱼腌制期间,在蒸锅中加入 5L 清水,烧开后,在蒸锅上放上蒸笼"
+ },
+ {
+ "step": 12,
+ "description": "鱼腌制好后,会析出水分,将多余水分和腌制用料酒、姜片倒掉,用清水冲洗干净鱼身和鱼肚,用厨房纸擦干鱼身和鱼肚"
+ },
+ {
+ "step": 13,
+ "description": "将鱼平放在蒸鱼盘中,重新在鱼身、鱼肚刀口处塞入姜片"
+ },
+ {
+ "step": 14,
+ "description": "然后将蒸鱼盘放入蒸笼中,盖上盖子,中火蒸 20 分钟"
+ },
+ {
+ "step": 15,
+ "description": "期间水蒸气会附着整个鱼和盘子上,凝结后形成鱼汤,出锅后千万不要倒掉这个汤,这个汤汁是鲜味精华"
+ },
+ {
+ "step": 16,
+ "description": "用防烫夹将蒸鱼盘夹出,在鱼身和鱼周围淋上 10g 蒸鱼豉油"
+ },
+ {
+ "step": 17,
+ "description": "然后在鱼身和周围均匀撒上小葱段和小米辣"
+ },
+ {
+ "step": 18,
+ "description": "在铁锅中倒入 15g 植物油,用中小火慢熬 5 分钟,不要用大火,否则油会挥发很快"
+ },
+ {
+ "step": 19,
+ "description": "将出锅后的热油均匀地慢慢地淋在鱼身上,鲜掉眉毛的葱油桂鱼就出炉啦!"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-aquatic-葱烧海参-葱烧海参",
+ "name": "葱烧海参的做法",
+ "description": "# 葱烧海参的做法\n\n\n\n这道菜的做法并不难,就是海参泡发是需要时间的。疫情隔离在家,干海参是过年前存的年货,正好拿出来尝试一下。\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/aquatic/葱烧海参/葱烧海参.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/葱烧海参/海参.jpeg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/葱烧海参/海参.jpeg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/葱烧海参/葱烧海参.jpeg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/葱烧海参/葱白.jpeg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/葱烧海参/酱汁.jpeg"
+ ],
+ "category": "水产",
+ "difficulty": 3,
+ "tags": [
+ "水产"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "泡发好的海参",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 泡发好的海参",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大葱葱白",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大葱葱白",
+ "notes": "量未指定"
+ },
+ {
+ "name": "泡发好的海参(北极参)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 泡发好的海参(北极参) 4 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大葱葱白",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大葱葱白 1 根大葱的葱白即可",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 20-25ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蚝油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蚝油 20g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖 2g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "淀粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 淀粉 2g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "葱白切成 1cm 的段,备用。"
+ },
+ {
+ "step": 2,
+ "description": "海参切成 1cm 的段,备用。"
+ },
+ {
+ "step": 3,
+ "description": "准备一个空碗,倒入 20g 蚝油, 10g 生抽, 2g 白糖,搅拌均匀。"
+ },
+ {
+ "step": 4,
+ "description": "另一个空碗倒入淀粉,水,制备水淀粉,勾芡用。"
+ },
+ {
+ "step": 5,
+ "description": "热锅,锅内放入 20ml - 25ml 食用油。等待 10 秒让油温升高。"
+ },
+ {
+ "step": 6,
+ "description": "放入葱白,调*小火*,注意不要让葱白变焦。大概煎 3-5 分钟即可。"
+ },
+ {
+ "step": 7,
+ "description": "用筷子夹出葱白,放入盘中备用。"
+ },
+ {
+ "step": 8,
+ "description": "倒入调好的料汁,炒香,**等待 1 - 2 分钟** 。"
+ },
+ {
+ "step": 9,
+ "description": "放入切好的海参,翻炒 1 分钟"
+ },
+ {
+ "step": 10,
+ "description": "加入 100 ml 的水, 中小火, **等待 5 分钟**"
+ },
+ {
+ "step": 11,
+ "description": "等待锅中汤汁快干的时候,加入水淀粉,加入前面取出的葱白"
+ },
+ {
+ "step": 12,
+ "description": "在外观*呈粘稠状态*后关火,盛盘 "
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-aquatic-蒜蓉虾-蒜蓉虾",
+ "name": "蒜蓉虾的做法",
+ "description": "# 蒜蓉虾的做法\n\n蒜蓉虾是广东省地方传统名菜,色香味俱全。\n\n预估烹饪难度:★★",
+ "source_path": "dishes/aquatic/蒜蓉虾/蒜蓉虾.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/蒜蓉虾/1.jpeg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/蒜蓉虾/1.jpeg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/蒜蓉虾/2.jpeg"
+ ],
+ "category": "水产",
+ "difficulty": 2,
+ "tags": [
+ "水产"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "海虾",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 海虾",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜蓉酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜蓉酱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "海虾",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 海虾 8 只",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜蓉酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜蓉酱 50 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 20 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 5 ml",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "用刀从从虾头中间切开,切到距离虾尾 1 cm"
+ },
+ {
+ "step": 2,
+ "description": "将蒜蓉酱铺在虾身中间,放在盘子中"
+ },
+ {
+ "step": 3,
+ "description": "锅中倒入热水,将盘子放入锅中,大火蒸 3 分钟"
+ },
+ {
+ "step": 4,
+ "description": "烧热油,倒入虾盘中,倒入生抽"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-aquatic-蒜香黄油虾-蒜香黄油虾",
+ "name": "蒜香黄油虾的做法",
+ "description": "# 蒜香黄油虾的做法\n\n蒜香黄油虾是一道经典的西式海鲜料理,以鲜虾为主料,配以蒜末和黄油烹制而成。口感鲜嫩,蒜香浓郁。制作简单,适合家庭日常烹饪。\n\n\n\n预估烹饪难度:★★",
+ "source_path": "dishes/aquatic/蒜香黄油虾/蒜香黄油虾.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/蒜香黄油虾/1.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/蒜香黄油虾/1.jpg"
+ ],
+ "category": "水产",
+ "difficulty": 2,
+ "tags": [
+ "水产"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "大虾(推荐黑虎虾或基围虾)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大虾(推荐黑虎虾或基围虾)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "无盐黄油(推荐安佳)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 无盐黄油(推荐安佳)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白葡萄酒(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白葡萄酒(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "柠檬",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 柠檬",
+ "notes": "量未指定"
+ },
+ {
+ "name": "平底煎锅",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 平底煎锅",
+ "notes": "量未指定"
+ },
+ {
+ "name": "厨房用夹",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 厨房用夹",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大虾",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大虾 8-10 只(约 200g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "无盐黄油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 无盐黄油 30g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大蒜 4 瓣(约 20g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白葡萄酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白葡萄酒 15ml(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "柠檬",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 柠檬 1/4 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "橄榄油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 橄榄油 10ml",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "大虾去头去壳留尾,用牙签挑去虾线,洗净后用厨房纸吸干水分"
+ },
+ {
+ "step": 2,
+ "description": "大蒜切成蒜末,备用"
+ },
+ {
+ "step": 3,
+ "description": "中火加热平底锅,放入 10ml 橄榄油"
+ },
+ {
+ "step": 4,
+ "description": "油热后放入大虾,每面煎 1-1.5 分钟至变色,取出备用"
+ },
+ {
+ "step": 5,
+ "description": "同一锅中加入黄油,融化后放入蒜末,小火炒香(约 30 秒)"
+ },
+ {
+ "step": 6,
+ "description": "如使用白葡萄酒,此时加入并煮至酒精挥发(约 1 分钟)"
+ },
+ {
+ "step": 7,
+ "description": "将虾放回锅中,与蒜香黄油酱汁翻炒均匀(约 1 分钟)"
+ },
+ {
+ "step": 8,
+ "description": "挤入柠檬汁,翻炒均匀后立即关火"
+ },
+ {
+ "step": 9,
+ "description": "装盘,淋上锅中剩余酱汁"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-aquatic-蛏抱蛋-蛏抱蛋",
+ "name": "蛏抱蛋的做法",
+ "description": "# 蛏抱蛋的做法\n\n蛏抱蛋,是流行于福建省福州地区的传统家常菜\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/aquatic/蛏抱蛋/蛏抱蛋.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/蛏抱蛋/1.jpeg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/蛏抱蛋/1.jpeg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/蛏抱蛋/2.jpeg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/蛏抱蛋/3.jpeg"
+ ],
+ "category": "水产",
+ "difficulty": 3,
+ "tags": [
+ "水产"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "蛏子",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蛏子",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "洋葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 洋葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "淀粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 淀粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡精",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡精",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蛏子",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蛏子 200 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋 2 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 100 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "洋葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 洋葱 0.25 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "淀粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 淀粉 20 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 5 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡精",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡精 5 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒 5 ml",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "烧开水,将蛏子放入水中,水煮 2 分钟后,捞上来去壳,放入大碗"
+ },
+ {
+ "step": 2,
+ "description": "往大碗中加入洋葱、生抽、料酒、鸡精、生粉后,充分搅拌"
+ },
+ {
+ "step": 3,
+ "description": "往大碗中打入 2 个 鸡蛋,继续搅拌"
+ },
+ {
+ "step": 4,
+ "description": "起锅烧油,倒入碗中蛏子,煎炸至单面金黄后,翻面继续煎炸"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-aquatic-香煎翘嘴鱼-香煎翘嘴鱼",
+ "name": "香煎翘嘴鱼的做法",
+ "description": "# 香煎翘嘴鱼的做法\n\n\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/aquatic/香煎翘嘴鱼/香煎翘嘴鱼.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/香煎翘嘴鱼/香煎翘嘴鱼.jpeg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/香煎翘嘴鱼/香煎翘嘴鱼.jpeg"
+ ],
+ "category": "水产",
+ "difficulty": 4,
+ "tags": [
+ "水产"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "翘嘴鱼(肉食性鱼类,肉细腻,口感好)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 翘嘴鱼(肉食性鱼类,肉细腻,口感好)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "青椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 青椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香菜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "豆瓣酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 豆瓣酱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "翘嘴鱼:2 斤最佳",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 翘嘴鱼:2 斤最佳",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜沫:20g",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜沫:20g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱:半根(50 克)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱:半根(50 克)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜:4 个",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜:4 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香菜:个人口味",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香菜:个人口味",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽:2ml(不太喜欢重口的可以不放)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽:2ml(不太喜欢重口的可以不放)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖:10g",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖:10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "干辣椒:4-6 个(根据个人口味选择)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 干辣椒:4-6 个(根据个人口味选择)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒:100ml",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒:100ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽:4ml",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽:4ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐:约",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐:约 50g 用于腌制鱼",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油:100ml",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油:100ml",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "鱼开背杀好(让卖鱼的杀好,千万不要剖腹杀鱼,切记是开背),清洗干净"
+ },
+ {
+ "step": 2,
+ "description": "鱼表面用盐涂抹均匀,倒入料酒约 80ml,姜末 20g,放入冰箱保鲜层进行腌制 1-2 天"
+ },
+ {
+ "step": 3,
+ "description": "取出腌制好的鱼,用绳挂起晾晒至半干(约 1-2 天,具体时间需结合气温与阳光)"
+ },
+ {
+ "step": 4,
+ "description": "食用前请将鱼用清水清洗,沥干水分(防止水遇油飞溅)"
+ },
+ {
+ "step": 5,
+ "description": "开大火将锅烧热,迅速改小火,锅中放油,尽量保持整个锅表面有油,将鱼沿锅边划入锅内(先煎鱼背面)"
+ },
+ {
+ "step": 6,
+ "description": "鱼入锅后(和翻面后),不要着急移动鱼的位置(此时容易破皮),煎约 30 秒后,尝试晃动锅"
+ },
+ {
+ "step": 7,
+ "description": "背面煎约 1 分钟后,翻面煎约 1-2 分钟,煎至两面金黄"
+ },
+ {
+ "step": 8,
+ "description": "等两面都煎好时,把鱼推向锅边一点,留点空间放入豆瓣酱炒香味,放入姜蒜,"
+ },
+ {
+ "step": 9,
+ "description": "炒出佐料香味后,加入料酒,生抽,老抽,倒入热水,水量和鱼平齐或者少点"
+ },
+ {
+ "step": 10,
+ "description": "此时改中大火,煮 5-10 分钟,后放入青椒断,白糖,鸡精,十三香,陈醋"
+ },
+ {
+ "step": 11,
+ "description": "改小火 2-5 分钟,放入葱,香菜,即可出锅"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-aquatic-鲤鱼炖白菜-鲤鱼炖白菜",
+ "name": "鲤鱼炖白菜的做法",
+ "description": "# 鲤鱼炖白菜的做法\n\n\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/aquatic/鲤鱼炖白菜/鲤鱼炖白菜.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/鲤鱼炖白菜/鲤鱼炖白菜.jpeg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/鲤鱼炖白菜/鲤鱼炖白菜.jpeg"
+ ],
+ "category": "水产",
+ "difficulty": 3,
+ "tags": [
+ "水产"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鲤鱼",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鲤鱼",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白菜心/娃娃菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白菜心/娃娃菜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "桂皮",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 桂皮",
+ "notes": "量未指定"
+ },
+ {
+ "name": "八角",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 八角",
+ "notes": "量未指定"
+ },
+ {
+ "name": "郫县豆瓣酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 郫县豆瓣酱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "干辣椒(不吃辣可以不放)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 干辣椒(不吃辣可以不放)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油:10ml",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油:10ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜:3 片",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜:3 片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜:3 瓣(切成块)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜:3 瓣(切成块)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鲤鱼:1.2 斤(清理过的)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鲤鱼:1.2 斤(清理过的)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "娃娃菜:13 片(可以多放一些,一顿就缩小了)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 娃娃菜:13 片(可以多放一些,一顿就缩小了)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐:5-8 克",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐:5-8 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽:3ml",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽:3ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽:6ml",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽:6ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "桂皮:1 块",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 桂皮:1 块",
+ "notes": "量未指定"
+ },
+ {
+ "name": "八角:3 个",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 八角:3 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "郫县豆瓣酱:20 克",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 郫县豆瓣酱:20 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "干辣椒:4-6 个(根据个人口味选择)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 干辣椒:4-6 个(根据个人口味选择)",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "鲤鱼清洗干净,改刀(在鱼身上多划几个伤口,方便入味)"
+ },
+ {
+ "step": 2,
+ "description": "娃娃菜清洗干净放入盘中备用"
+ },
+ {
+ "step": 3,
+ "description": "锅中加油,等油热放入“少盐” “姜” “蒜” “郫县豆瓣酱” “桂皮” “八角” 炒出香味"
+ },
+ {
+ "step": 4,
+ "description": "把鱼放锅里煎(3 分钟)每(30 秒)需要翻面"
+ },
+ {
+ "step": 5,
+ "description": "加入“水”(水量尽量和鱼平齐,可以少一点点) 放入 “生抽” “老抽” “娃娃菜”"
+ },
+ {
+ "step": 6,
+ "description": "大火炖 15-20 分钟,汤汁快干时添加 “盐” 即可出锅"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-aquatic-鳊鱼炖豆腐-鳊鱼炖豆腐",
+ "name": "鳊鱼炖豆腐的做法",
+ "description": "# 鳊鱼炖豆腐的做法\n\n\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/aquatic/鳊鱼炖豆腐/鳊鱼炖豆腐.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/鳊鱼炖豆腐/鳊鱼炖豆腐.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/鳊鱼炖豆腐/鳊鱼炖豆腐.jpg"
+ ],
+ "category": "水产",
+ "difficulty": 3,
+ "tags": [
+ "水产"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "鳊鱼(鱼可以让摊主帮忙处理好)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鳊鱼(鱼可以让摊主帮忙处理好)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老豆腐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老豆腐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "桂皮(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 桂皮(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冰糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冰糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "干辣椒(不吃辣可以不放)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 干辣椒(不吃辣可以不放)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "八角(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 八角(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香叶(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香叶(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "热水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 热水",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鳊鱼:550 克",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鳊鱼:550 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老豆腐:400 克",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老豆腐:400 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜:5 片",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜:5 片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱:半根(50 克)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱:半根(50 克)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜:4 个",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜:4 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽:2ml(不太喜欢重口的可以不放)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽:2ml(不太喜欢重口的可以不放)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "桂皮:1 块",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 桂皮:1 块",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冰糖:5 块",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冰糖:5 块",
+ "notes": "量未指定"
+ },
+ {
+ "name": "干辣椒:4-6 个(根据个人口味选择)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 干辣椒:4-6 个(根据个人口味选择)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒:5ml",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒:5ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽:4ml",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽:4ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐:5-8 克(根据个人口味选择)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐:5-8 克(根据个人口味选择)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "八角:1 个",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 八角:1 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香叶:1-3 片",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香叶:1-3 片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油:10ml",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油:10ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "热水:400 克",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 热水:400 克",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "鳊鱼改刀,放上姜片和料酒腌制 5-10 分钟"
+ },
+ {
+ "step": 2,
+ "description": "老豆腐切块后放入水中备用"
+ },
+ {
+ "step": 3,
+ "description": "锅中加油,可以放点盐在锅里,防止煎鱼的时候粘锅,把腌制的鱼用厨房纸擦干水分,把鱼放到锅中,两面都煎一下"
+ },
+ {
+ "step": 4,
+ "description": "等两面都煎好时,把鱼推向锅边一点,留点空间放入葱姜蒜,干辣椒,香叶,八角炒出味道"
+ },
+ {
+ "step": 5,
+ "description": "炒出佐料香味后,加入料酒,生抽,老抽,冰糖,桂皮,倒入热水,水量和鱼平齐或者少点"
+ },
+ {
+ "step": 6,
+ "description": "大火烧开后,放入老豆腐,豆腐贴在锅边,加入食盐,转小火"
+ },
+ {
+ "step": 7,
+ "description": "小火烧 10-15 分钟,然后大火收点汁,即可出锅"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-aquatic-黄油煎虾-黄油煎虾",
+ "name": "黄油煎虾的做法",
+ "description": "# 黄油煎虾的做法\n\n\n\n黄油煎虾是一道制作相对简单、风味极佳的菜式,主要耗时在于处理活虾,总耗时在一个小时内,适合初学者进行烹饪。\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/aquatic/黄油煎虾/黄油煎虾.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/黄油煎虾/黄油煎虾.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/aquatic/黄油煎虾/黄油煎虾.jpg"
+ ],
+ "category": "水产",
+ "difficulty": 3,
+ "tags": [
+ "水产"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "鲜虾(强推肉质紧实的九节虾,普通明虾也可以)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鲜虾(强推肉质紧实的九节虾,普通明虾也可以)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黄油(推荐安佳,一次用一小盒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黄油(推荐安佳,一次用一小盒 7g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黑胡椒粒(瓶磨的那种)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黑胡椒粒(瓶磨的那种)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "米酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 米酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鲜虾",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鲜虾 300g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黄油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黄油 7g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黑胡椒粒 大概",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黑胡椒粒 大概 15ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 45ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 10ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "米酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 米酒 5ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖 10ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 2.5ml",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "鲜虾摘除头部,顺带扯出虾线(这步处理不好可在下一步开背时取出虾线),使用剪刀剪开或菜刀片开虾背,沥干水分备用"
+ },
+ {
+ "step": 2,
+ "description": "调制酱汁:小碗放入上述量的全部生抽、米酒、白糖、盐搅匀备用"
+ },
+ {
+ "step": 3,
+ "description": "中大火热锅,热锅内放入食用油,等待 10 秒让油温升高"
+ },
+ {
+ "step": 4,
+ "description": "虾全部放入锅中,开始瓶磨黑胡椒,均匀地撒在虾上翻炒"
+ },
+ {
+ "step": 5,
+ "description": "虾变色后加入黄油,黄油完全融化后倒入调制酱汁,继续翻炒"
+ },
+ {
+ "step": 6,
+ "description": "大火翻炒 15 秒收汁即可装盘"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-breakfast-吐司果酱",
+ "name": "吐司果酱的做法",
+ "description": "# 吐司果酱的做法\n\n饱腹感的懒人快速营养早餐,2 分钟 搞定\n\n预估烹饪难度:★",
+ "source_path": "dishes/breakfast/吐司果酱.md",
+ "image_path": null,
+ "images": [],
+ "category": "早餐",
+ "difficulty": 1,
+ "tags": [
+ "早餐"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "新鲜吐司",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 新鲜吐司",
+ "notes": "量未指定"
+ },
+ {
+ "name": "果酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 果酱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "面包机",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 面包机",
+ "notes": "量未指定"
+ },
+ {
+ "name": "吐司两片",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 吐司两片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "果酱足够涂满一面吐司的量",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 果酱足够涂满一面吐司的量",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "将吐司放入面包机"
+ },
+ {
+ "step": 2,
+ "description": "设置好档位,时间到了会自动弹出"
+ },
+ {
+ "step": 3,
+ "description": "两分钟后吐司加热完成弹出"
+ },
+ {
+ "step": 4,
+ "description": "先取出一片吐司,涂满果酱再盖上另一片吐司即可"
+ },
+ {
+ "step": 5,
+ "description": "用餐巾纸包一下可以边走边吃也可以吃完再出门"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-breakfast-太阳蛋",
+ "name": "太阳蛋的做法",
+ "description": "# 太阳蛋的做法\n\n预估烹饪难度:★★",
+ "source_path": "dishes/breakfast/太阳蛋.md",
+ "image_path": null,
+ "images": [],
+ "category": "早餐",
+ "difficulty": 2,
+ "tags": [
+ "早餐"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "分可控火候微波炉或不可控火候微波炉(定义和分辨方式请见附加内容)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 分可控火候微波炉或不可控火候微波炉(定义和分辨方式请见附加内容)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "筷子或牙签",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 筷子或牙签",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "准备一个小碗,倒入在上一步计算好的油,撒盐,搅拌均匀。倾斜碗使油沾在碗表面。"
+ },
+ {
+ "step": 2,
+ "description": "取出一个鸡蛋,打入小碗。"
+ },
+ {
+ "step": 3,
+ "description": "蛋黄表面戳孔。牙签戳 5 个或筷子戳 1 个。"
+ },
+ {
+ "step": 4,
+ "description": "放入微波炉,中火 3 分钟。"
+ },
+ {
+ "step": 5,
+ "description": "准备一个小碗,倒入在上一步计算好的油,撒盐,搅拌均匀。倾斜碗使油沾在碗表面。"
+ },
+ {
+ "step": 6,
+ "description": "取出一个鸡蛋,打入小碗。"
+ },
+ {
+ "step": 7,
+ "description": "蛋黄表面戳孔。牙签戳 5 个或筷子戳 1 个。"
+ },
+ {
+ "step": 8,
+ "description": "放入微波炉,1 分钟。"
+ },
+ {
+ "step": 9,
+ "description": "while(太阳蛋 否 大面积成固体状) 用微波炉打(30s);"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-breakfast-完美水煮蛋",
+ "name": "完美水煮蛋的做法",
+ "description": "# 完美水煮蛋的做法\n\n\n\n科学家研发的循环水煮法,可同时达到蛋黄绵密、蛋白均匀凝固且保留最多营养素的效果。需精准控制温度与时间,难度较高。\n\n预估烹饪难度:★★★★★",
+ "source_path": "dishes/breakfast/完美水煮蛋.md",
+ "image_path": null,
+ "images": [],
+ "category": "早餐",
+ "difficulty": 5,
+ "tags": [
+ "早餐"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "新鲜鸡蛋(推荐 AA 级)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 新鲜鸡蛋(推荐 AA 级)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "100°C 沸水锅(直径≥",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 100°C 沸水锅(直径≥ 15cm)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "30°C 温水锅(直径≥",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 30°C 温水锅(直径≥ 15cm)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "定时器",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 定时器",
+ "notes": "量未指定"
+ },
+ {
+ "name": "漏勺",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 漏勺",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋 1 个(约 60g )",
+ "notes": "量未指定"
+ },
+ {
+ "name": "100°C 沸水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 100°C 沸水 1500ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "30°C 温水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 30°C 温水 1500ml",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "准备两锅水: A 锅维持 100°C 沸水, B 锅维持 30°C 温水"
+ },
+ {
+ "step": 2,
+ "description": "用漏勺将鸡蛋放入 A 锅,启动定时器"
+ },
+ {
+ "step": 3,
+ "description": "精准**每 2 分钟**将鸡蛋转移至另一锅水"
+ },
+ {
+ "step": 4,
+ "description": "重复转移操作共 16 次(总时长 32 分钟)"
+ },
+ {
+ "step": 5,
+ "description": "最后一次转移后,在 B 锅静置 30 秒"
+ },
+ {
+ "step": 6,
+ "description": "立即放入冰水( 0 摄氏度)终止加热(维持 30 秒)"
+ },
+ {
+ "step": 7,
+ "description": "剥壳时从钝端气室处开始,沿纵轴剥离蛋膜"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-breakfast-微波炉荷包蛋",
+ "name": "微波炉荷包蛋的做法",
+ "description": "# 微波炉荷包蛋的做法\n\n微波炉荷包蛋是一道简单易做且富含蛋白质的菜。只需要微波炉 120 秒内就可以完成,适合通勤社畜早餐。\n\n预估烹饪难度:★",
+ "source_path": "dishes/breakfast/微波炉荷包蛋.md",
+ "image_path": null,
+ "images": [],
+ "category": "早餐",
+ "difficulty": 1,
+ "tags": [
+ "早餐"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "芝麻油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 芝麻油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋 2 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "饮用水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 饮用水 35ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "芝麻油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 芝麻油 3ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 0.8g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "将鸡蛋打入小碗中,用筷子在所有鸡蛋黄上扎 2 个洞,避免加热弄脏微波炉"
+ },
+ {
+ "step": 2,
+ "description": "然后向碗内倒入常温饮用水"
+ },
+ {
+ "step": 3,
+ "description": "再向碗内倒入食用盐"
+ },
+ {
+ "step": 4,
+ "description": "最后加入芝麻油"
+ },
+ {
+ "step": 5,
+ "description": "将放好材料的碗放入微波炉中,高火加热 80 秒"
+ },
+ {
+ "step": 6,
+ "description": "到达设定时间后,使用抹布垫着手取出成品"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-breakfast-微波炉蛋糕",
+ "name": "微波炉蛋糕的做法",
+ "description": "# 微波炉蛋糕的做法\n\n微波炉\"叮\"蛋糕,大约需要 2 分钟 就能搞定!初学者所需时间预计延长至 20 分钟。\n\n预估烹饪难度:★",
+ "source_path": "dishes/breakfast/微波炉蛋糕.md",
+ "image_path": null,
+ "images": [],
+ "category": "早餐",
+ "difficulty": 1,
+ "tags": [
+ "早餐"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "微波炉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 微波炉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "能放进微波炉的容器",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 能放进微波炉的容器",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黄油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黄油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "面粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 面粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "泡打粉(不加吃着像饼)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 泡打粉(不加吃着像饼)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋🥚",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋🥚 1 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "面粉🍚",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 面粉🍚 15g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "泡打粉🍚",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 泡打粉🍚 2.5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白(红)糖🍬",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白(红)糖🍬 10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐🧂",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐🧂 1g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "咖啡粉☕",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 咖啡粉☕",
+ "notes": "量未指定"
+ },
+ {
+ "name": "巧克力🍫",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 巧克力🍫",
+ "notes": "量未指定"
+ },
+ {
+ "name": "麦片🍿",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 麦片🍿",
+ "notes": "量未指定"
+ },
+ {
+ "name": "牛奶🥛",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 牛奶🥛",
+ "notes": "量未指定"
+ },
+ {
+ "name": "坚果🥜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 坚果🥜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "饼干屑🍪",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 饼干屑🍪",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香蕉🍌",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香蕉🍌",
+ "notes": "量未指定"
+ },
+ {
+ "name": "非黑暗料理🍆",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 非黑暗料理🍆",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "加入以下食材,注意不要超过容器的 3/4"
+ },
+ {
+ "step": 2,
+ "description": "夸赞一下自己🥰"
+ },
+ {
+ "step": 3,
+ "description": "微波炉(高火)加热 **1分钟** (至蓬松蛋糕形态)"
+ },
+ {
+ "step": 4,
+ "description": "取出杯子(烫手啊啊啊啊↑)并拍朋友圈就可以吃了"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-breakfast-手抓饼",
+ "name": "手抓饼的做法",
+ "description": "# 手抓饼的做法\n\n预估烹饪难度:★★\n\n---",
+ "source_path": "dishes/breakfast/手抓饼.md",
+ "image_path": null,
+ "images": [],
+ "category": "早餐",
+ "difficulty": 2,
+ "tags": [
+ "早餐"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "普通面粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 普通面粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "开水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 开水",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冷水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冷水",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生菜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "火腿",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 火腿",
+ "notes": "量未指定"
+ },
+ {
+ "name": "芝士片",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 芝士片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "--",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- --",
+ "notes": "量未指定"
+ },
+ {
+ "name": "面粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 面粉 200 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "开水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 开水 100 毫升",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冷水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冷水 50 毫升",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 15 毫升",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 3 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋 1 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生菜 30 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "火腿",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 火腿 30 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "芝士片",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 芝士片 1 片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "--",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- --",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "--"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-breakfast-桂圆红枣粥",
+ "name": "桂圆红枣粥的做法",
+ "description": "# 桂圆红枣粥的做法\n\n桂圆红枣粥,甜口。补血安神,健脑益智,补养心脾。制作时间需要 70 分钟。\n\n预估烹饪难度:★★",
+ "source_path": "dishes/breakfast/桂圆红枣粥.md",
+ "image_path": null,
+ "images": [],
+ "category": "早餐",
+ "difficulty": 2,
+ "tags": [
+ "早餐"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "糯米(或大米)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 糯米(或大米)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "红枣",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 红枣",
+ "notes": "量未指定"
+ },
+ {
+ "name": "桂圆",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 桂圆",
+ "notes": "量未指定"
+ },
+ {
+ "name": "糯米",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 糯米 100g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "红枣",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 红枣 15 颗",
+ "notes": "量未指定"
+ },
+ {
+ "name": "桂圆",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 桂圆 15 颗",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "将桂圆肉扒出,用清水洗两次,放入碗中浸泡 10 分钟"
+ },
+ {
+ "step": 2,
+ "description": "红枣用清水洗两次,放入碗中浸泡 10 分钟"
+ },
+ {
+ "step": 3,
+ "description": "糯米放入电饭锅中,清水淘米两次后,加入 2000ml 水"
+ },
+ {
+ "step": 4,
+ "description": "将桂圆和红枣加入电饭锅"
+ },
+ {
+ "step": 5,
+ "description": "打开电饭锅煮饭模式,1 小时后粥成"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-breakfast-水煮玉米",
+ "name": "水煮玉米的做法",
+ "description": "# 水煮玉米的做法\n\n大约 15 分钟可以完成制作。\n\n预估烹饪难度:★★",
+ "source_path": "dishes/breakfast/水煮玉米.md",
+ "image_path": null,
+ "images": [],
+ "category": "早餐",
+ "difficulty": 2,
+ "tags": [
+ "早餐"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "新鲜玉米",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 新鲜玉米",
+ "notes": "量未指定"
+ },
+ {
+ "name": "放得下玉米的锅",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 放得下玉米的锅",
+ "notes": "量未指定"
+ },
+ {
+ "name": "水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 水",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "糖(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 糖(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "一个带皮玉米",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 一个带皮玉米",
+ "notes": "量未指定"
+ },
+ {
+ "name": "淹过玉米约半节指头的水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 淹过玉米约半节指头的水",
+ "notes": "量未指定"
+ },
+ {
+ "name": "煮玉米的时候,开始和淡盐水,差不多",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 煮玉米的时候,开始和淡盐水,差不多 2 克盐加 50ml 的水",
+ "notes": "量未指定"
+ },
+ {
+ "name": "根据口味选择加或者不加糖(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 根据口味选择加或者不加糖(可选)",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "将新鲜玉米剥去外皮,剩部分玉米皮入锅"
+ },
+ {
+ "step": 2,
+ "description": "加入淹过玉米约半节指头的水,加盐和糖"
+ },
+ {
+ "step": 3,
+ "description": "水煮开之后转至小火,加盖继续煮 15-20 分钟,玉米煮久点没事。"
+ },
+ {
+ "step": 4,
+ "description": "煮熟后沥干水分,冷却后食用。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-breakfast-溏心蛋",
+ "name": "溏心蛋的做法",
+ "description": "# 溏心蛋的做法\n\n喜欢健身的小伙伴可以在每颗鸡蛋中获得 6 克蛋白质。大约 15 分钟可以完成制作。\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/breakfast/溏心蛋.md",
+ "image_path": null,
+ "images": [],
+ "category": "早餐",
+ "difficulty": 3,
+ "tags": [
+ "早餐"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "电锅",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 电锅",
+ "notes": "量未指定"
+ },
+ {
+ "name": "水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 水",
+ "notes": "量未指定"
+ },
+ {
+ "name": "秒表(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 秒表(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋 1 颗或更多(只要您的电锅装得下,不管有几颗鸡蛋都可以)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "淹过鸡蛋约",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 淹过鸡蛋约 2 公分的冷水",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "将鸡蛋放入电锅中。鸡蛋不可互相堆叠,应皆在底部,并留有空间可以晃动"
+ },
+ {
+ "step": 2,
+ "description": "倒入淹过鸡蛋约 2 公分的冷水"
+ },
+ {
+ "step": 3,
+ "description": "开盖,使用最大功率加热至水滚起(大约 85 - 95 度,稍微滚动,不需完全沸腾)"
+ },
+ {
+ "step": 4,
+ "description": "关火,盖上盖子,让鸡蛋静置。"
+ },
+ {
+ "step": 5,
+ "description": "沥干水分,用冷水冲洗鸡蛋约 1 分钟,即可去壳食用。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-breakfast-煎饺",
+ "name": "煎饺的做法",
+ "description": "# 煎饺的做法\n\n预估烹饪难度:★★",
+ "source_path": "dishes/breakfast/煎饺.md",
+ "image_path": null,
+ "images": [],
+ "category": "早餐",
+ "difficulty": 2,
+ "tags": [
+ "早餐"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "饺子(速冻水饺)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 饺子(速冻水饺)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "饺子一包 (根据个人食量选择, 约",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 饺子一包 (根据个人食量选择, 约 10 - 15 个)",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "取出平底锅(不沾平底锅最佳)"
+ },
+ {
+ "step": 2,
+ "description": "加入 10ml - 15 ml 食用油"
+ },
+ {
+ "step": 3,
+ "description": "开火,放入饺子(尽量平均铺开,不宜堆叠)"
+ },
+ {
+ "step": 4,
+ "description": "立刻加入清水,水线没过饺子平均高度的 1/2"
+ },
+ {
+ "step": 5,
+ "description": "盖上锅盖(此时炉灶应该处于大火)"
+ },
+ {
+ "step": 6,
+ "description": "等待 8 - 10 分钟"
+ },
+ {
+ "step": 7,
+ "description": "当锅中水分仅剩 2mm 时, 转中火开始煎制"
+ },
+ {
+ "step": 8,
+ "description": "当水分全部蒸发后,摇晃平底锅使饺子受热均匀"
+ },
+ {
+ "step": 9,
+ "description": "放入黑芝麻和葱花再焖 10s"
+ },
+ {
+ "step": 10,
+ "description": "1 - 2 分钟夹出一个饺子观察底部,若出现金黄色脆皮立即取出"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-breakfast-燕麦鸡蛋饼",
+ "name": "燕麦鸡蛋饼的做法",
+ "description": "# 燕麦鸡蛋饼的做法\n\n燕麦鸡蛋饼是极具营养、便于制作、适宜快速制作的早餐。尤其适宜热爱健身的上班族。\n\n预估烹饪难度:★★",
+ "source_path": "dishes/breakfast/燕麦鸡蛋饼.md",
+ "image_path": null,
+ "images": [],
+ "category": "早餐",
+ "difficulty": 2,
+ "tags": [
+ "早餐"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "燕麦",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 燕麦",
+ "notes": "量未指定"
+ },
+ {
+ "name": "牛奶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 牛奶 50-100g,能够将燕麦搅拌粘稠即可",
+ "notes": "量未指定"
+ },
+ {
+ "name": "可根据口味选择增加",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 可根据口味选择增加 50g 蔬菜,如菠菜。",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋两个,亦可选择两个蛋清,一个蛋黄。",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋两个,亦可选择两个蛋清,一个蛋黄。",
+ "notes": "量未指定"
+ },
+ {
+ "name": "纯干燕麦片",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 纯干燕麦片 50g (大约等同一个鸡蛋的量)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "牛奶一盒 约",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 牛奶一盒 约 250ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蔬菜碎叶一把",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蔬菜碎叶一把",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "将牛奶与干燕麦混合搅拌均匀至黏稠状。"
+ },
+ {
+ "step": 2,
+ "description": "将鸡蛋搅拌均匀至颜色单一程度。"
+ },
+ {
+ "step": 3,
+ "description": "将鸡蛋液倒入燕麦牛奶中继续搅拌至黏稠、均匀。"
+ },
+ {
+ "step": 4,
+ "description": "平底锅中加入一层黄油并覆盖均匀。"
+ },
+ {
+ "step": 5,
+ "description": "下入搅拌好的食材,并摊开至饼状。"
+ },
+ {
+ "step": 6,
+ "description": "小火加热两到三分钟。如想要加入蔬菜,可以在加热过程中加入碎菜叶。"
+ },
+ {
+ "step": 7,
+ "description": "翻面继续加热两分钟。"
+ },
+ {
+ "step": 8,
+ "description": "出锅,搭配剩下的牛奶作为早餐。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-breakfast-牛奶燕麦",
+ "name": "牛奶燕麦的做法",
+ "description": "# 牛奶燕麦的做法\n\n高蛋白,粗谷物纤维,饱腹感的懒人快速营养早餐,3 分钟 搞定\n\n预估烹饪难度:★",
+ "source_path": "dishes/breakfast/牛奶燕麦.md",
+ "image_path": null,
+ "images": [],
+ "category": "早餐",
+ "difficulty": 1,
+ "tags": [
+ "早餐"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "牛奶(巴氏奶口感更好)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 牛奶(巴氏奶口感更好)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "燕麦",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 燕麦",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "🥛 牛奶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 🥛 牛奶 280ml/per",
+ "notes": "量未指定"
+ },
+ {
+ "name": "🍳 鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 🍳 鸡蛋 1 个/per",
+ "notes": "量未指定"
+ },
+ {
+ "name": "🍚 燕麦",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 🍚 燕麦 40g/per",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "将牛奶倒入早餐杯(冷的即可)"
+ },
+ {
+ "step": 2,
+ "description": "准备好 200ml 水,如果是直饮水直接加入燕麦,否则请烧开后加入燕麦"
+ },
+ {
+ "step": 3,
+ "description": "水沸后 2 分钟,燕麦煮好"
+ },
+ {
+ "step": 4,
+ "description": "煮好的燕麦捞出倒入牛奶中(尽量不要将煮燕麦的水也倒入牛奶,影响口感)"
+ },
+ {
+ "step": 5,
+ "description": "将燕麦替换为快煮燕麦"
+ },
+ {
+ "step": 6,
+ "description": "将牛奶倒入装有快煮燕麦的容器中并搅拌"
+ },
+ {
+ "step": 7,
+ "description": "将混合物放入微波炉中"
+ },
+ {
+ "step": 8,
+ "description": "中等火力微波 4 分钟"
+ },
+ {
+ "step": 9,
+ "description": "热锅,锅内放一层底油,油热后煎鸡蛋,每面煎 20s,考虑调底味(3g 椒盐,可选)"
+ },
+ {
+ "step": 10,
+ "description": "关火,装盘"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-breakfast-空气炸锅面包片",
+ "name": "空气炸锅面包片的做法",
+ "description": "# 空气炸锅面包片的做法\n\n健康饱肚子,适宜正在减脂期的程序员食用\n\n预估烹饪难度:★",
+ "source_path": "dishes/breakfast/空气炸锅面包片.md",
+ "image_path": null,
+ "images": [],
+ "category": "早餐",
+ "difficulty": 1,
+ "tags": [
+ "早餐"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "面包片",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 面包片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "空气炸锅",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 空气炸锅",
+ "notes": "量未指定"
+ },
+ {
+ "name": "面包片(两片)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 面包片(两片)",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "取出两片面包片(建议使用粗粮面包片)"
+ },
+ {
+ "step": 2,
+ "description": "将面包片**垂直**放入空气炸锅"
+ },
+ {
+ "step": 3,
+ "description": "200°C 烘烤 5 分钟"
+ },
+ {
+ "step": 4,
+ "description": "取出即可使用"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-breakfast-美式炒蛋",
+ "name": "美式炒蛋的做法",
+ "description": "# 美式炒蛋的做法\n\n美式炒蛋具有松软鲜嫩的口感,与平时的炒蛋不同,美式炒蛋中加入了少量牛奶,使得蛋花更加的细密均匀,并且营养丰富~\n\n预估烹饪难度:★★",
+ "source_path": "dishes/breakfast/美式炒蛋.md",
+ "image_path": null,
+ "images": [],
+ "category": "早餐",
+ "difficulty": 2,
+ "tags": [
+ "早餐"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "全脂牛奶/奶油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 全脂牛奶/奶油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黄油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黄油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋 3 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "全脂牛奶/奶油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 全脂牛奶/奶油 10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黄油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黄油 5 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 1 克",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "鸡蛋打入大碗中,加盐搅打至起泡,静置 15 分钟"
+ },
+ {
+ "step": 2,
+ "description": "黄油切小块入锅,倒入蛋液,开小火不断搅拌"
+ },
+ {
+ "step": 3,
+ "description": "黄油一融化,就快速翻动蛋液,将其打碎成细密状,在蛋液大体凝固前关火"
+ },
+ {
+ "step": 4,
+ "description": "加入牛奶搅拌 15 秒,至炒蛋湿润绵密,装盘"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-breakfast-茶叶蛋",
+ "name": "茶叶蛋的做法",
+ "description": "# 茶叶蛋的做法\n\n茶香浓郁,鲜香可口的高蛋白快速营养早餐,大约耗时 30 分钟。烹饪略微耗时,可以周末尝试,做一次大约够 2-3 个人吃。\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/breakfast/茶叶蛋.md",
+ "image_path": null,
+ "images": [],
+ "category": "早餐",
+ "difficulty": 3,
+ "tags": [
+ "早餐"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "八角",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 八角",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香叶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香叶",
+ "notes": "量未指定"
+ },
+ {
+ "name": "桂皮",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 桂皮",
+ "notes": "量未指定"
+ },
+ {
+ "name": "茴香",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 茴香",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冰糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冰糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "红茶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 红茶",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋 400g(约 8 颗)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "八角",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 八角 4g(约 2 颗)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香叶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香叶 0.5-1g(约 2 片)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "桂皮",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 桂皮 3g(1 小块)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "茴香",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 茴香 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冰糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冰糖 15g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "红茶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 红茶 20g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 15g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽 25g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食盐 3g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "用冷水将鸡蛋煮熟,大火大约 8 分钟(根据自家厨具决定)"
+ },
+ {
+ "step": 2,
+ "description": "鸡蛋捞出,过冷水"
+ },
+ {
+ "step": 3,
+ "description": "将鸡蛋互相碰撞,使每个鸡蛋产生裂缝"
+ },
+ {
+ "step": 4,
+ "description": "将鸡蛋下锅,放入八角,香叶,桂皮,茴香,冰糖,红茶,生抽,老抽,食盐"
+ },
+ {
+ "step": 5,
+ "description": "加水直至没过鸡蛋"
+ },
+ {
+ "step": 6,
+ "description": "大火煮开之后,转中小火煮 15 分钟"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-breakfast-蒸水蛋",
+ "name": "蒸水蛋的做法",
+ "description": "# 蒸水蛋的做法\n\n蒸水蛋(北方有些地区叫鸡蛋糕儿)都是饭店的好吃,如何自己做水滑嫩香的蒸水蛋,本教程包教包会!\n\n预估烹饪难度:★★",
+ "source_path": "dishes/breakfast/蒸水蛋.md",
+ "image_path": null,
+ "images": [],
+ "category": "早餐",
+ "difficulty": 2,
+ "tags": [
+ "早餐"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "新鲜鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 新鲜鸡蛋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "热水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 热水",
+ "notes": "量未指定"
+ },
+ {
+ "name": "锡纸或保鲜膜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 锡纸或保鲜膜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋两只",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋两只",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 2g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "热水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 热水 260ml",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "鸡蛋打入碗中,打散"
+ },
+ {
+ "step": 2,
+ "description": "取其他容器,倒入 1.5 倍(半个蛋壳为 0.5 倍水)于蛋液的温水(温度 20~30),将盐倒入水中化开"
+ },
+ {
+ "step": 3,
+ "description": "将盐水倒入鸡蛋液中,顺时针或逆时针单方向搅拌均匀,气泡之类的可以用舀出丢弃,过筛则口感更加。"
+ },
+ {
+ "step": 4,
+ "description": "使用锡纸包裹盛蛋液的碗(或用盘子盖住),置入提前带盖并加入大约 3cm 深度水的锅中"
+ },
+ {
+ "step": 5,
+ "description": "中火烧至水开,转最小的火继续蒸 4 分钟"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-breakfast-蒸花卷",
+ "name": "蒸花卷的做法",
+ "description": "# 蒸花卷的做法\n\n蒸花卷是一道简单易做的菜。能补充碳水化合物,膳食纤维。一般初学者只需要半小时即可完成。作为快手早餐,学会做之后,再也不会早上饿肚子了。\n\n预估烹饪难度:★★",
+ "source_path": "dishes/breakfast/蒸花卷.md",
+ "image_path": null,
+ "images": [],
+ "category": "早餐",
+ "difficulty": 2,
+ "tags": [
+ "早餐"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "冷冻花卷",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冷冻花卷",
+ "notes": "量未指定"
+ },
+ {
+ "name": "圆碟子",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 圆碟子",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒸架",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒸架",
+ "notes": "量未指定"
+ },
+ {
+ "name": "水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 水 400ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冷冻花卷",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冷冻花卷 5 个(女生分量 3 个即可)(可以在超市、各种买菜平台购买)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "圆碟子,直径",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 圆碟子,直径 28cm",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒸架,直径",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒸架,直径 20cm",
+ "notes": "量未指定"
+ },
+ {
+ "name": "水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 水 400ml",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "从花卷的包装袋中取出 5 个花卷"
+ },
+ {
+ "step": 2,
+ "description": "把花卷平铺在碟子上,尽量不用重叠"
+ },
+ {
+ "step": 3,
+ "description": "往锅里倒入 400ml 水,把蒸架放里面,把装花卷的碟子放在蒸架上,盖上锅盖。"
+ },
+ {
+ "step": 4,
+ "description": "开大火加热,直至水沸腾。"
+ },
+ {
+ "step": 5,
+ "description": "转中火加热 15 分钟"
+ },
+ {
+ "step": 6,
+ "description": "开盖用手感受花卷的表面温度,如果不够热,就继续盖上盖子加热,否则就可以关火出锅。"
+ },
+ {
+ "step": 7,
+ "description": "碟子取出放凉至 50 度即可食用"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-breakfast-蛋煎糍粑",
+ "name": "蛋煎糍粑的做法",
+ "description": "# 蛋煎糍粑的做法\n\n蛋煎糍粑做法很简单,不需要太多的厨艺基础~\n\n蛋煎糍粑热量高,美味+顶饿+便宜,只需十分钟就可以完成~\n\n预估烹饪难度:★★",
+ "source_path": "dishes/breakfast/蛋煎糍粑.md",
+ "image_path": null,
+ "images": [],
+ "category": "早餐",
+ "difficulty": 2,
+ "tags": [
+ "早餐"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "糍粑",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 糍粑",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖或红糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖或红糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "糍粑 两块",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 糍粑 两块",
+ "notes": "量未指定"
+ },
+ {
+ "name": "红糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 红糖 10g (建议 8g - 15g 之间)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋 1 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 10-15ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用盐 2g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "把糍粑切成长方形小块,便于后面煎"
+ },
+ {
+ "step": 2,
+ "description": "碗里打入一个鸡蛋并把鸡蛋搅碎,加入 2g 食用盐"
+ },
+ {
+ "step": 3,
+ "description": "将切好的小糍粑依此放入搅碎的鸡蛋里面,涂抹完糍粑双面为止"
+ },
+ {
+ "step": 4,
+ "description": "锅里倒入植物油 10ml ,把涂抹好的糍粑小块放进去小火慢慢煎软。"
+ },
+ {
+ "step": 5,
+ "description": "将剩下的鸡蛋液慢慢倒在糍粑表面"
+ },
+ {
+ "step": 6,
+ "description": "用筷子或者勺子为糍粑翻面,来回煎至金黄色后开吃"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-breakfast-金枪鱼酱三明治",
+ "name": "金枪鱼酱三明治的做法",
+ "description": "# 金枪鱼酱三明治的做法\n\n饱腹感很强的懒人早餐,营养很丰富,高蛋白,大概 5 分钟搞定。可以配着牛奶、咖啡等饮品一起吃。\n\n预估烹饪难度:★",
+ "source_path": "dishes/breakfast/金枪鱼酱三明治.md",
+ "image_path": null,
+ "images": [],
+ "category": "早餐",
+ "difficulty": 1,
+ "tags": [
+ "早餐"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "水浸金枪鱼罐头(不建议用油浸,会很腻)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 水浸金枪鱼罐头(不建议用油浸,会很腻)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "方形吐司片",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 方形吐司片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蛋黄酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蛋黄酱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "俄式酸黄瓜汁",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 俄式酸黄瓜汁",
+ "notes": "量未指定"
+ },
+ {
+ "name": "芝士片(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 芝士片(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "火腿片(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 火腿片(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "轻食机",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 轻食机",
+ "notes": "量未指定"
+ },
+ {
+ "name": "水浸金枪鱼",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 水浸金枪鱼 65g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "方形吐司片",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 方形吐司片 2 片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蛋黄酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蛋黄酱 50 mL",
+ "notes": "量未指定"
+ },
+ {
+ "name": "俄式酸黄瓜汁",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 俄式酸黄瓜汁 10-15mL(可根据个人口味调整)",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "将金枪鱼、蛋黄酱、俄式酸黄瓜汁倒入碗中,用勺子搅拌,保证将金枪鱼块搅碎,酱整体呈糊状,并备用"
+ },
+ {
+ "step": 2,
+ "description": "将 1 片吐司放在轻食机上"
+ },
+ {
+ "step": 3,
+ "description": "将做好的金枪鱼酱涂抹到吐司上,建议 10-15ml"
+ },
+ {
+ "step": 4,
+ "description": "将另一片方形吐司片覆盖在上面,并按压轻食机,开机"
+ },
+ {
+ "step": 5,
+ "description": "待轻食机自动停止加热,即可装盘使用"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-breakfast-鸡蛋三明治",
+ "name": "鸡蛋三明治的做法",
+ "description": "# 鸡蛋三明治的做法\n\n10 分钟的简易鸡蛋三明治 🥪\n\n预估烹饪难度:★★",
+ "source_path": "dishes/breakfast/鸡蛋三明治.md",
+ "image_path": null,
+ "images": [],
+ "category": "早餐",
+ "difficulty": 2,
+ "tags": [
+ "早餐"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "吐司",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 吐司",
+ "notes": "量未指定"
+ },
+ {
+ "name": "培根",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 培根",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黄油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黄油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蛋黄酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蛋黄酱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黑胡椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黑胡椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋 1 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "吐司",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 吐司 2 片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "培根",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 培根 2 片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黄油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黄油 10 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蛋黄酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蛋黄酱 20g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 1g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黑胡椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黑胡椒 2g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "吐司切去四边,备用"
+ },
+ {
+ "step": 2,
+ "description": "鸡蛋煮熟,捣碎"
+ },
+ {
+ "step": 3,
+ "description": "混合鸡蛋、蛋黄酱、盐、黑胡椒"
+ },
+ {
+ "step": 4,
+ "description": "锅中加入黄油,煎熟培根"
+ },
+ {
+ "step": 5,
+ "description": "组装吐司,在两片吐司间加入制作好的鸡蛋酱及培根"
+ },
+ {
+ "step": 6,
+ "description": "四边形吐司切成三角形装盘"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-breakfast-温泉蛋-温泉蛋",
+ "name": "温泉蛋的做法",
+ "description": "# 温泉蛋的做法\n\n一种传统的日式小吃,可以用于各种佐餐,注意与溏心蛋区分,溏心蛋是蛋黄不熟蛋白熟了,温泉蛋是蛋白不熟蛋黄熟了\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/breakfast/温泉蛋/温泉蛋.md",
+ "image_path": null,
+ "images": [],
+ "category": "早餐",
+ "difficulty": 3,
+ "tags": [
+ "早餐"
+ ],
+ "servings": 1,
+ "ingredients": [],
+ "steps": [
+ {
+ "step": 1,
+ "description": "在锅中盛装一定量自来水,确保水面没过约鸡蛋 3cm,水中插入温度计"
+ },
+ {
+ "step": 2,
+ "description": "开火或打开电磁炉,逐渐调整电磁炉功率或火苗大小,使得水温保持在 **70 摄氏度**"
+ },
+ {
+ "step": 3,
+ "description": "将鸡蛋放入锅中。鸡蛋不可互相堆叠,应皆在底部,并留有空间可以晃动"
+ },
+ {
+ "step": 4,
+ "description": "保持当前温度 **25 分钟**"
+ },
+ {
+ "step": 5,
+ "description": "准备一杯冰水"
+ },
+ {
+ "step": 6,
+ "description": "捞出鸡蛋,并立刻放入冰水中,**等待 3 分钟**"
+ },
+ {
+ "step": 7,
+ "description": "将鸡蛋打入小碗,完成制作"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-breakfast-苏格兰蛋-苏格兰蛋",
+ "name": "苏格兰蛋的做法",
+ "description": "\n\n\n\n# 苏格兰蛋的做法\n\n\n\n\n\n\n\n苏格兰蛋是一种用新鲜肉糜裹住鸡蛋,放入油中炸至金黄制成,这个版本比较费事,所以在此就给大家带来简易版,苏格兰蛋复杂版大家就自行查找。\n\n简易版苏格兰蛋是利用手抓饼皮包裹住芝士培根糖心蛋放入油中炸至金黄制成,大约耗时 20-30 分钟。\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/breakfast/苏格兰蛋/苏格兰蛋.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/breakfast/苏格兰蛋/egg1.png",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/breakfast/苏格兰蛋/egg1.png",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/breakfast/苏格兰蛋/egg2.png",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/breakfast/苏格兰蛋/egg3.png"
+ ],
+ "category": "早餐",
+ "difficulty": 3,
+ "tags": [
+ "早餐"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "手抓饼皮",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 手抓饼皮",
+ "notes": "量未指定"
+ },
+ {
+ "name": "芝士",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 芝士",
+ "notes": "量未指定"
+ },
+ {
+ "name": "培根",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 培根",
+ "notes": "量未指定"
+ },
+ {
+ "name": "空气炸锅或者油锅",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 空气炸锅或者油锅",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋 50g(约 1 颗)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "手抓饼",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 手抓饼 1 份-2 份(看鸡蛋大小)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "芝士片",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 芝士片 1-2 片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "培根片",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 培根片 1-2 片",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "用冷水下锅水开 3 分钟后捞出"
+ },
+ {
+ "step": 2,
+ "description": "鸡蛋捞出,放入冰水中剥壳更快速也更完整"
+ },
+ {
+ "step": 3,
+ "description": "用芝士片包裹鸡蛋"
+ },
+ {
+ "step": 4,
+ "description": "培根片包裹鸡蛋"
+ },
+ {
+ "step": 5,
+ "description": "手抓饼两端切除以矩形包裹鸡蛋"
+ },
+ {
+ "step": 6,
+ "description": "油温 6 成下锅(油面波动,有青烟,筷子插入油中周围泛起气泡即是 6 成温度) 炸制金黄即可"
+ },
+ {
+ "step": 7,
+ "description": "空气炸锅 160 度 15 分钟"
+ },
+ {
+ "step": 8,
+ "description": "切开即可食用"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-condiment-油酥",
+ "name": "油酥的做法",
+ "description": "# 油酥的做法\n\n油酥是由面粉与热油混合调制的,通常在烙饼时涂点油酥,可以使得饼子层层分明,外酥里软,口感更佳。\n\n预估烹饪难度:★★",
+ "source_path": "dishes/condiment/油酥.md",
+ "image_path": null,
+ "images": [],
+ "category": "调料",
+ "difficulty": 2,
+ "tags": [
+ "调料"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "面粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 面粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "油 = (要烙饼的张数",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 油 = (要烙饼的张数 * 10ml)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐 = (要烙饼的张数 /",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 = (要烙饼的张数 / 2)g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "面粉 = (要烙饼的张数 /",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 面粉 = (要烙饼的张数 / 0.13)g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "面粉盛小碗里,加入盐"
+ },
+ {
+ "step": 2,
+ "description": "加入 200 度的热油"
+ },
+ {
+ "step": 3,
+ "description": "用筷子将其搅拌成无固状物体的糊状。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-condiment-炸串酱料",
+ "name": "炸串酱料的做法",
+ "description": "# 炸串酱料的做法\n\n炸串酱料,号称淋袜子都好吃,新手友好,预计用时 10 分钟。\n\n预估烹饪难度:★★",
+ "source_path": "dishes/condiment/炸串酱料.md",
+ "image_path": null,
+ "images": [],
+ "category": "调料",
+ "difficulty": 2,
+ "tags": [
+ "调料"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "干辣椒面(粗细都准备)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 干辣椒面(粗细都准备)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "孜然粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 孜然粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "胡椒粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 胡椒粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "五香粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 五香粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "花椒粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 花椒粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "十三香",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 十三香",
+ "notes": "量未指定"
+ },
+ {
+ "name": "麻辣鲜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 麻辣鲜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白芝麻",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白芝麻",
+ "notes": "量未指定"
+ },
+ {
+ "name": "干辣椒面",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 干辣椒面 60 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "孜然粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 孜然粉 20 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "胡椒粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 胡椒粉 10 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "五香粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 五香粉 15 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食盐 20 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "花椒粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 花椒粉 15 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡精",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡精 8 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "十三香",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 十三香 5 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "麻辣鲜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 麻辣鲜 5 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白芝麻",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白芝麻 30 克",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "所有原料在容器内混合,搅拌均匀。"
+ },
+ {
+ "step": 2,
+ "description": "锅里烧热油,油的用量以在容器内没过所有原材料为佳。"
+ },
+ {
+ "step": 3,
+ "description": "分三次淋入热油,每次 1/3,同时搅拌。"
+ },
+ {
+ "step": 4,
+ "description": "最后放入香油 10ml,生抽 10ml,花椒油 10ml,蚝油 10ml。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-condiment-简易版炒糖色",
+ "name": "简易版炒糖色的做法",
+ "description": "# 简易版炒糖色的做法\n\n这是简易的糖色的做法。对于更为进阶的技巧和糖色更为进阶的用法,请学习[糖色的炒制](../../tips/advanced/糖色的炒制.md)。\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/condiment/简易版炒糖色.md",
+ "image_path": null,
+ "images": [],
+ "category": "调料",
+ "difficulty": 4,
+ "tags": [
+ "调料"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "糖(任选其一):",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 糖(任选其一):",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冰糖:炒出来的`糖色`色泽最为鲜艳,红亮,必须水油炒,不加水融化会很慢",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冰糖:炒出来的`糖色`色泽最为鲜艳,红亮,必须水油炒,不加水融化会很慢",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白砂糖:必须水油炒,不加水融化会很慢",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白砂糖:必须水油炒,不加水融化会很慢",
+ "notes": "量未指定"
+ },
+ {
+ "name": "绵白糖:可以不加水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 绵白糖:可以不加水",
+ "notes": "量未指定"
+ },
+ {
+ "name": "炒糖色过程火不要太大!!!电磁炉温度不够,火候过了发苦,不够发甜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 炒糖色过程火不要太大!!!电磁炉温度不够,火候过了发苦,不够发甜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "`油`:100ml",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- `油`:100ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "`开水`:500ml",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- `开水`:500ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "`糖`(这里以冰糖为例)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- `糖`(这里以冰糖为例)",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "开火,并向锅中倒入 100ml 开水"
+ },
+ {
+ "step": 2,
+ "description": "再向锅中倒入 100ml 油,与第一步间隔越短越好,此时锅为大火中火都可以,着急的话可以大火"
+ },
+ {
+ "step": 3,
+ "description": "放入冰糖(如果冰糖过于耦合,可以提前敲碎,做到耦合度越低越好)"
+ },
+ {
+ "step": 4,
+ "description": "调整火力为中火"
+ },
+ {
+ "step": 5,
+ "description": "开始搅拌"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-condiment-糖醋汁",
+ "name": "糖醋汁的做法",
+ "description": "# 糖醋汁的做法\n\n糖醋汁通常情况下由清水、白糖、白醋等制成,有些人喜欢放一些番茄酱来增添不一样的酸甜味或放一些淀粉来增加菜肴汤汁的粘性和浓度,糖醋汁可用于糖醋鱼、糖醋里脊、糖醋排骨等菜品的制作\n\n可依据糖醋汁配制的经典比例 1:2:3:4:5 来调制糖醋汁\n\n预估烹饪难度:★★",
+ "source_path": "dishes/condiment/糖醋汁.md",
+ "image_path": null,
+ "images": [],
+ "category": "调料",
+ "difficulty": 2,
+ "tags": [
+ "调料"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "清水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 清水",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白醋/米醋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白醋/米醋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "清水(50ml)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 清水(50ml)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽(40ml)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽(40ml)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖(30g)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖(30g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白醋(20ml)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白醋(20ml)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒(10ml)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒(10ml)",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "按照比例将各调料在小碗中搅拌均匀"
+ },
+ {
+ "step": 2,
+ "description": "按不同菜肴的方式处理完毕后,将配制好的糖醋汁倒入锅中"
+ },
+ {
+ "step": 3,
+ "description": "根据各菜肴的不同,烹制 5-10 分钟"
+ },
+ {
+ "step": 4,
+ "description": "大火收汁,可增加菜的浓度、香味和光泽"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-condiment-葱油",
+ "name": "葱油的做法",
+ "description": "# 葱油的做法\n\n葱油是用热油萃取以葱为主的各类香辛料得到的产物,可以用来调制肉馅,做凉拌菜,在热炒菜中作为出锅明油使用。\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/condiment/葱油.md",
+ "image_path": null,
+ "images": [],
+ "category": "调料",
+ "difficulty": 3,
+ "tags": [
+ "调料"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱(大葱小葱都可以)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱(大葱小葱都可以)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "洋葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 洋葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香菜(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香菜(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "开洋(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 开洋(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 油 200g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱 80g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜 20g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒 10ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "洋葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 洋葱 150g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "开洋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 开洋 50g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "开洋泡入 50 度温水中,加入 10ml 料酒去腥,泡 10 分钟后取出沥干水分"
+ },
+ {
+ "step": 2,
+ "description": "葱,香菜洗净,切成 5cm 长的段,擦干表面水份"
+ },
+ {
+ "step": 3,
+ "description": "洋葱切成丝,在锅里用热水煮 5 分钟,取出沥干水份"
+ },
+ {
+ "step": 4,
+ "description": "姜去皮,切成片"
+ },
+ {
+ "step": 5,
+ "description": "锅里倒入全部油,放入上述预处理好的材料,开中小火炸 20 分钟"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-condiment-蒜香酱油",
+ "name": "蒜香酱油的做法",
+ "description": "# 蒜香酱油的做法\n\n预估烹饪难度:★★",
+ "source_path": "dishes/condiment/蒜香酱油.md",
+ "image_path": null,
+ "images": [],
+ "category": "调料",
+ "difficulty": 2,
+ "tags": [
+ "调料"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "蒜头",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜头",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白芝麻",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白芝麻",
+ "notes": "量未指定"
+ },
+ {
+ "name": "花生油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 花生油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酱油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酱油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蘸料碟",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蘸料碟",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜头",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜头 2 瓣",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白芝麻",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白芝麻 5 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "花生油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 花生油 15 毫升",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酱油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酱油 30 毫升",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蘸料碟",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蘸料碟 1 个",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "拍碎蒜头"
+ },
+ {
+ "step": 2,
+ "description": "往蘸料碟中加入酱油"
+ },
+ {
+ "step": 3,
+ "description": "起锅,加入花生油,等到油温滚烫后加入拍好的蒜头,炸半分钟"
+ },
+ {
+ "step": 4,
+ "description": "半分钟后,关火,把热油倒入蘸料碟,用筷子搅拌即可"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-condiment-油泼辣子-油泼辣子",
+ "name": "油泼辣子的做法",
+ "description": "# 油泼辣子的做法\n\n\n\n\n制作耗时 10 分钟\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/condiment/油泼辣子/油泼辣子.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/condiment/油泼辣子/口水鸡+油泼辣子.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/condiment/油泼辣子/口水鸡+油泼辣子.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/condiment/油泼辣子/油泼辣子.jpg"
+ ],
+ "category": "调料",
+ "difficulty": 3,
+ "tags": [
+ "调料"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "蒜头",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜头",
+ "notes": "量未指定"
+ },
+ {
+ "name": "干辣椒面",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 干辣椒面",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "熟白芝麻",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 熟白芝麻",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小米椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小米椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "花生油(可用菜籽油替换)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 花生油(可用菜籽油替换)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "家庭小陶瓷碗",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 家庭小陶瓷碗",
+ "notes": "量未指定"
+ },
+ {
+ "name": "家庭铁勺子",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 家庭铁勺子",
+ "notes": "量未指定"
+ },
+ {
+ "name": "五香粉 (可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 五香粉 (可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "草寇(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 草寇(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小葱 (可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小葱 (可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "八角",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 八角",
+ "notes": "量未指定"
+ },
+ {
+ "name": "花椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 花椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香叶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香叶",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白芷",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白芷",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜片(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜片(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白醋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白醋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜头",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜头 1 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "干辣椒面",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 干辣椒面 100 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 5 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "熟白芝麻",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 熟白芝麻 15 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小米椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小米椒 1 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "花生油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 花生油 150 毫升 (可用菜籽油替换)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "五香粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 五香粉 10 克(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "草寇",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 草寇 1 个(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小葱 3-5 根(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 糖 30 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白醋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白醋 5 ml(大概就是小铁勺子的量)",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "拿出蒜头掰 2 个`小蒜头`去皮"
+ },
+ {
+ "step": 2,
+ "description": "拿出砧板剁碎`小蒜头`、`小米椒`"
+ },
+ {
+ "step": 3,
+ "description": "拿出碗倒入`花生油`"
+ },
+ {
+ "step": 4,
+ "description": "油热放入`其他配料`和`小葱`,等到香料变焦,捞出扔掉"
+ },
+ {
+ "step": 5,
+ "description": "拿出铁锅将碗内的油放入加热 2 分钟(菜籽油烧至冒烟)"
+ },
+ {
+ "step": 6,
+ "description": "此时是空碗"
+ },
+ {
+ "step": 7,
+ "description": "往空碗加入`干辣椒面`、`白芝麻`、`蒜末`、`小米椒`、`盐`、`五香粉`、`草寇`作为\"调料\""
+ },
+ {
+ "step": 8,
+ "description": "关火将油温冷却至 `210` 摄氏度"
+ },
+ {
+ "step": 9,
+ "description": "将锅内热油倒入碗内并用勺子搅拌即可(可以在 `165` 摄氏度时加入同样\"调料\"的碗最后进行混合进行增辣)"
+ },
+ {
+ "step": 10,
+ "description": "倒入热油稍微搅拌后放入白醋,此时会重新沸腾。继续进行搅拌,白醋增香。"
+ },
+ {
+ "step": 11,
+ "description": "油泼辣子冷却到温热放白糖和味精,白糖可以是辣味柔和,不会那么的呛口"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-condiment-草莓酱-草莓酱",
+ "name": "草莓酱的做法",
+ "description": "# 草莓酱的做法\n\n可以买那种一筐一筐卖的小草莓,主要是便宜。做成酱抹在面包上非常好吃。\n\n预估烹饪难度:★★",
+ "source_path": "dishes/condiment/草莓酱/草莓酱.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/condiment/草莓酱/做好的草莓酱.png",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/condiment/草莓酱/做好的草莓酱.png",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/condiment/草莓酱/洗好的草莓.jpeg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/condiment/草莓酱/混合好的草莓.jpeg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/condiment/草莓酱/熬煮的草莓.jpeg"
+ ],
+ "category": "调料",
+ "difficulty": 2,
+ "tags": [
+ "调料"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "草莓",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 草莓",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白砂糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白砂糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "保鲜膜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 保鲜膜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "草莓",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 草莓 1200 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖 400 克 (如需要低糖饮食,可以考虑降低到 200g)",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "草莓洗净去叶"
+ },
+ {
+ "step": 2,
+ "description": "将草莓切碎放入合适的碗中"
+ },
+ {
+ "step": 3,
+ "description": "将白糖倒入碗中与草莓搅拌均匀"
+ },
+ {
+ "step": 4,
+ "description": "碗用保鲜膜覆盖静置 1 小时"
+ },
+ {
+ "step": 5,
+ "description": "将静置的草莓和糖的混合物倒入不粘锅中开大火烧开"
+ },
+ {
+ "step": 6,
+ "description": "烧开后转小火不断搅拌直至果酱呈粘稠状关火"
+ },
+ {
+ "step": 7,
+ "description": "待草莓酱冷却后装入准备好的密封罐中"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-condiment-蔗糖糖浆-蔗糖糖浆",
+ "name": "蔗糖糖浆的做法",
+ "description": "# 蔗糖糖浆的做法\n\n将糖事先溶解好便于在配制饮料(特别是冷饮)时给饮料增甜\n\n预估烹饪难度:★",
+ "source_path": "dishes/condiment/蔗糖糖浆/蔗糖糖浆.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/condiment/蔗糖糖浆/bottle.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/condiment/蔗糖糖浆/bottle.jpg"
+ ],
+ "category": "调料",
+ "difficulty": 1,
+ "tags": [
+ "调料"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "白砂糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白砂糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 水",
+ "notes": "量未指定"
+ },
+ {
+ "name": "可密封容器(建议使用高硼硅试剂瓶,便宜)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 可密封容器(建议使用高硼硅试剂瓶,便宜)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 水 100 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白砂糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白砂糖 100 克",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-dessert-反沙芋头-反沙芋头",
+ "name": "反沙芋头的做法",
+ "description": "# 反沙芋头的做法\n\n\n\n反沙芋头是一道著名的潮汕小吃,下午茶,制作起来特别方便,~预计制作时间 20 分钟\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/dessert/反沙芋头/反沙芋头.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/dessert/反沙芋头/反沙芋头成品.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/dessert/反沙芋头/反沙芋头成品.jpg"
+ ],
+ "category": "甜品",
+ "difficulty": 3,
+ "tags": [
+ "甜品"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "荔浦芋头(电商平台购买即可,实惠新鲜)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 荔浦芋头(电商平台购买即可,实惠新鲜)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白砂糖或冰糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白砂糖或冰糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 水",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "荔浦芋头",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 荔浦芋头 200g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白砂糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白砂糖 30g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 水 15g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "芋头切长条(稍微大条一点,翻炒过程不容易烂)"
+ },
+ {
+ "step": 2,
+ "description": "加入可以没过芋头的油,等油温起来(插入筷子冒小泡即可)"
+ },
+ {
+ "step": 3,
+ "description": "放进芋头到油里,去炸到芋头浮起来,一般是微微泛黄并且可以用筷子很轻松戳洞"
+ },
+ {
+ "step": 4,
+ "description": "炸芋头的油放起来别浪费,后面炒菜啥的都能用"
+ },
+ {
+ "step": 5,
+ "description": "接下来关键的一步,把糖(30g)和水(15g)按照 2:1 比例,加热至不变色且冒小泡"
+ },
+ {
+ "step": 6,
+ "description": "倒入葱花和芋头,关火翻炒,此时等温度下来,糖就会有反沙的效果"
+ },
+ {
+ "step": 7,
+ "description": "装盘上桌!"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-dessert-咖啡椰奶冻-咖啡椰奶冻",
+ "name": "咖啡椰奶冻的做法",
+ "description": "# 咖啡椰奶冻的做法\n\n\n\n咖啡椰奶冻是一道简单易于制作的甜品 出品时间约 1 小时(不算冷藏)\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/dessert/咖啡椰奶冻/咖啡椰奶冻.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/dessert/咖啡椰奶冻/咖啡椰奶冻.png",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/dessert/咖啡椰奶冻/咖啡椰奶冻.png"
+ ],
+ "category": "甜品",
+ "difficulty": 4,
+ "tags": [
+ "甜品"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "125ml 淡奶油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 125ml 淡奶油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "250ml 椰树牌椰汁",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 250ml 椰树牌椰汁",
+ "notes": "量未指定"
+ },
+ {
+ "name": "35ml espresso 意式浓缩",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 35ml espresso 意式浓缩",
+ "notes": "量未指定"
+ },
+ {
+ "name": "50ml 椰子水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 50ml 椰子水",
+ "notes": "量未指定"
+ },
+ {
+ "name": "10g 吉利丁(gelatin)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 10g 吉利丁(gelatin)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "过滤网(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 过滤网(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "(那个...有摆盘需求的话,可以来点蓝莓 and/or 咖啡粉)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- (那个...有摆盘需求的话,可以来点蓝莓 and/or 咖啡粉)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "125ml 淡奶油(whipping cream,",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 125ml 淡奶油(whipping cream, 35% M.E)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "250ml 椰树牌椰汁",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 250ml 椰树牌椰汁",
+ "notes": "量未指定"
+ },
+ {
+ "name": "35ml espresso 意式浓缩(个人不推荐用 fruity 的咖啡豆)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 35ml espresso 意式浓缩(个人不推荐用 fruity 的咖啡豆)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "50ml 椰子水(推荐 vita coco coconut water,有条件可以敲一个椰子)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 50ml 椰子水(推荐 vita coco coconut water,有条件可以敲一个椰子)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "10g 吉利丁",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 10g 吉利丁",
+ "notes": "量未指定"
+ },
+ {
+ "name": "糖(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 糖(可选)",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "将定量淡奶油,椰树牌椰汁,espresso,椰子水混合备用。"
+ },
+ {
+ "step": 2,
+ "description": "将以上液体加热 1 分钟,温度达到 50-60 度即可。"
+ },
+ {
+ "step": 3,
+ "description": "(可选)如果格外嗜甜可以加额外的糖。"
+ },
+ {
+ "step": 4,
+ "description": "倒入吉利丁,搅拌至融化,煮 1 分钟。"
+ },
+ {
+ "step": 5,
+ "description": "(可选)过筛 (这一步可以让椰奶冻口感更佳顺畅)。"
+ },
+ {
+ "step": 6,
+ "description": "放入模具。"
+ },
+ {
+ "step": 7,
+ "description": "(可选)过滤掉表层的泡泡。这一步可以让椰奶冻口感更好,并且看着也会更棒。"
+ },
+ {
+ "step": 8,
+ "description": "放入冰箱冷藏区,等待 3 小时。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-dessert-奥利奥冰淇淋-奥利奥冰淇淋",
+ "name": "奥利奥冰淇淋的做法",
+ "description": "# 奥利奥冰淇淋的做法\n\n奥利奥冰淇淋是简单但好吃的冰淇淋,纯动物奶油不腻口,预计制作时长半小时(主要消耗在搅打奶油和去除奥利奥夹心上)。\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/dessert/奥利奥冰淇淋/奥利奥冰淇淋.md",
+ "image_path": null,
+ "images": [],
+ "category": "甜品",
+ "difficulty": 3,
+ "tags": [
+ "甜品"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "淡奶油(推荐品牌 安佳动物淡奶油)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 淡奶油(推荐品牌 安佳动物淡奶油)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "原味奥利奥",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 原味奥利奥",
+ "notes": "量未指定"
+ },
+ {
+ "name": "电动打蛋器",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 电动打蛋器",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小刀(或者可以去除夹心的工具)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小刀(或者可以去除夹心的工具)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冰淇淋模具(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冰淇淋模具(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "奥利奥",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 奥利奥 6 块",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白砂糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白砂糖 18 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "淡奶油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 淡奶油 250 毫升",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "将奥利奥拧开后去除利利(夹心),备用"
+ },
+ {
+ "step": 2,
+ "description": "用筷子将奥奥剁碎,需要有一半奥奥变成粉状,另一半的奥奥最大长度小于 0.5 厘米,备用(某宝可搜“奥利奥饼干碎”,节省时间精力^-^)"
+ },
+ {
+ "step": 3,
+ "description": "将奶油全部倒置于深容器中,并加入准备好的糖"
+ },
+ {
+ "step": 4,
+ "description": "开始用电动打蛋器高速挡 搅打至 电动打蛋器提起后下方会出现**悬挂住**的奶油( 0.5 厘米 - 1 厘米),而不是**全部**像液体一样滴下(部分滴下是正常现象)。"
+ },
+ {
+ "step": 5,
+ "description": "搅打完成后将奥奥放入奶油中,搅拌均匀直至底部有奥奥。"
+ },
+ {
+ "step": 6,
+ "description": "可选:将混合物倒入冰淇淋模具中"
+ },
+ {
+ "step": 7,
+ "description": "放置冰箱冷冻室( -18 度) 4 小时以上可取出"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-dessert-戚风蛋糕-戚风蛋糕",
+ "name": "戚风蛋糕的做法",
+ "description": "# 戚风蛋糕的做法\n\n戚风蛋糕是一道烘焙入门菜品,有一定操作难度。但成功制作后,其口感细腻绵软,令人回味。加上烘烤时间,一般初学者需要 **1.5 - 2 小时**即可完成。\n\n预估烹饪难度:★★★★★",
+ "source_path": "dishes/dessert/戚风蛋糕/戚风蛋糕.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/dessert/戚风蛋糕/DSC08606.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/dessert/戚风蛋糕/DSC08606.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/dessert/戚风蛋糕/DSC08608.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/dessert/戚风蛋糕/DSC08612.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/dessert/戚风蛋糕/DSC08618.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/dessert/戚风蛋糕/DSC08621.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/dessert/戚风蛋糕/DSC08627.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/dessert/戚风蛋糕/DSC08716.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/dessert/戚风蛋糕/IMG_0269.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/dessert/戚风蛋糕/IMG_1516.jpg"
+ ],
+ "category": "甜品",
+ "difficulty": 5,
+ "tags": [
+ "甜品"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "1 个鸡蛋(正常中等大小,约",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 1 个鸡蛋(正常中等大小,约 50g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖 16g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 8g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "牛奶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 牛奶 10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "低筋面粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 低筋面粉 17g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "6 寸:大小为",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 6 寸:大小为 3 份(即三个鸡蛋)。面积 36 个单位。",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋 3 个,白糖 50g,食用油 25g,牛奶 30g,低筋面粉 50g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "8 寸:大小为",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 8 寸:大小为 5 份(即五个鸡蛋)。面积 64 个单位。",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋 5 个,白糖 80g,食用油 40g,牛奶 50g,低筋面粉 90g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "牛奶(或水)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 牛奶(或水)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油(或黄油,但需加热软化)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油(或黄油,但需加热软化)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "低筋面粉(推荐惠宜)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 低筋面粉(推荐惠宜)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "[可选] 柠檬汁或白醋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- [可选] 柠檬汁或白醋",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "(可选) 将模具从高处落下,震出其中的热气"
+ },
+ {
+ "step": 2,
+ "description": "模具倒扣 10 分钟,使蛋糕冷却"
+ },
+ {
+ "step": 3,
+ "description": "脱模,食用"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-dessert-提拉米苏-提拉米苏",
+ "name": "提拉米苏的做法",
+ "description": "# 提拉米苏的做法\n\n\n\n提拉米苏,是意大利传统甜品。无需烤箱操作简便,烘焙新手也可以零失误获得一份美味的提拉米苏。\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/dessert/提拉米苏/提拉米苏.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/dessert/提拉米苏/提拉米苏成品.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/dessert/提拉米苏/提拉米苏成品.jpg"
+ ],
+ "category": "甜品",
+ "difficulty": 4,
+ "tags": [
+ "甜品"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "马斯卡彭芝士",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 马斯卡彭芝士",
+ "notes": "量未指定"
+ },
+ {
+ "name": "手指饼干",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 手指饼干",
+ "notes": "量未指定"
+ },
+ {
+ "name": "放凉浓缩咖啡",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 放凉浓缩咖啡",
+ "notes": "量未指定"
+ },
+ {
+ "name": "无菌鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 无菌鸡蛋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白砂糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白砂糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "可可粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 可可粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "朗姆酒(不喜欢酒的朋友可省略,可按照自己口味调节)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 朗姆酒(不喜欢酒的朋友可省略,可按照自己口味调节)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "一个装成品的容器(这里用的是玻璃乐扣)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 一个装成品的容器(这里用的是玻璃乐扣)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "打蛋器(手劲儿大的朋友也可以锻炼臂力)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 打蛋器(手劲儿大的朋友也可以锻炼臂力)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "马斯卡彭芝士",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 马斯卡彭芝士 450g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "手指饼干",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 手指饼干 1 包",
+ "notes": "量未指定"
+ },
+ {
+ "name": "放凉浓缩咖啡",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 放凉浓缩咖啡 350ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "无菌鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 无菌鸡蛋 4 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白砂糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白砂糖 50g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "可可粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 可可粉 10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "朗姆酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 朗姆酒 35ml",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "分离蛋黄蛋清"
+ },
+ {
+ "step": 2,
+ "description": "盛有蛋白的碗中加 10g 白砂糖湿性打发"
+ },
+ {
+ "step": 3,
+ "description": "盛有蛋黄的碗中将 40g 白砂糖分三次加入,搅拌至均匀"
+ },
+ {
+ "step": 4,
+ "description": "蛋黄中分三次加入马斯卡彭芝士,搅拌至均匀"
+ },
+ {
+ "step": 5,
+ "description": "蛋黄中最后加入朗姆酒,搅拌均匀"
+ },
+ {
+ "step": 6,
+ "description": "将打发好的蛋白分三次加入蛋黄芝士液中"
+ },
+ {
+ "step": 7,
+ "description": "手指饼干两面浸湿咖啡液,平铺入容器"
+ },
+ {
+ "step": 8,
+ "description": "两层芝士液两层饼干交替放入容器(这一步按照大家意愿及容器高度酌情处理)"
+ },
+ {
+ "step": 9,
+ "description": "放入冰箱冷藏四个小时(心急的小伙伴可以提早拿出来)"
+ },
+ {
+ "step": 10,
+ "description": "取出后在表面筛上可可粉,即可享用啦"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-dessert-无厨师机蜂蜜面包-无厨师机蜂蜜面包",
+ "name": "无厨师机蜂蜜面包的做法",
+ "description": "# 无厨师机蜂蜜面包的做法\n\n\n\n这个菜谱不需要厨师机,只需要等待!可以晚上的时候准备好放入冰箱,第二天再烤。口感虽然不如使用厨师机的但是还行,冰箱保存要吃的时候微波炉叮一下更好。花费时间大多在发面。\n\n预估烹饪难度:★★★★★",
+ "source_path": "dishes/dessert/无厨师机蜂蜜面包/无厨师机蜂蜜面包.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/dessert/无厨师机蜂蜜面包/无厨师机蜂蜜面包.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/dessert/无厨师机蜂蜜面包/无厨师机蜂蜜面包.jpg"
+ ],
+ "category": "甜品",
+ "difficulty": 5,
+ "tags": [
+ "甜品"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "高筋面粉:400g",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 高筋面粉:400g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "牛奶:",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 牛奶: 200g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酵母:4g",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酵母:4g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋:1 个",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋:1 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白砂糖:70g",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白砂糖:70g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐:",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐: 2g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黄油:",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黄油: 30g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蜂蜜:20g",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蜂蜜:20g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "水:",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 水: 20g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "芝麻",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 芝麻",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "制作面团:将面粉,牛奶(建议加热到 40°,本人使用微波炉 15 - 20s),酵母,鸡蛋,面粉,糖和盐混合起来。"
+ },
+ {
+ "step": 2,
+ "description": "搅拌面团,将原料混合均匀成团。"
+ },
+ {
+ "step": 3,
+ "description": "加入黄油混合。"
+ },
+ {
+ "step": 4,
+ "description": "继续搅拌 + 手揉,均匀混合。"
+ },
+ {
+ "step": 5,
+ "description": "开始发面,使用保鲜膜覆盖容器,普通气温(10 - 20°)放置 1 - 2 小时,稍长时间对效果影响不大。"
+ },
+ {
+ "step": 6,
+ "description": "明显看到面团发酵变大(2 倍)即可开始切分面团, 此时面团应该不再十分黏手。"
+ },
+ {
+ "step": 7,
+ "description": "切分面团:理想状态每一份 60g(美观),可根据喜好适当调整大小。"
+ },
+ {
+ "step": 8,
+ "description": "将每一份小面团使用擀面杖擀成舌状后卷起, 再次醒面 10 分钟左右。"
+ },
+ {
+ "step": 9,
+ "description": "再次使用擀面杖擀成舌状后卷起, 从中间切开(一个变成两个)。"
+ },
+ {
+ "step": 10,
+ "description": "再次使用擀面杖擀成舌状后卷起, 从中间切开(两个变成四个)。(此步骤可以按照自己的时间多擀/卷几次, 把握一份的大小就行)"
+ },
+ {
+ "step": 11,
+ "description": "烤盘放入油纸并倒入花生油, (每份卷好的)底部蘸水 + 面粉后放入烤盘。"
+ },
+ {
+ "step": 12,
+ "description": "再次醒发(盖上保鲜膜), 这一步可以放入冰箱, 第二天再烤。"
+ },
+ {
+ "step": 13,
+ "description": "刷上蛋液。"
+ },
+ {
+ "step": 14,
+ "description": "烤箱 180°(355°F), 18 - 20 分钟。"
+ },
+ {
+ "step": 15,
+ "description": "出炉后, 刷上蜂蜜水, 撒上芝麻。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-dessert-炸鲜奶-炸鲜奶",
+ "name": "炸鲜奶的做法",
+ "description": "# 炸鲜奶的做法\n\n\n\n炸鲜奶是一种外脆里嫩的甜点,营养价值适中,制作难度中等,预计制作时长约为 20 分钟。\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/dessert/炸鲜奶/炸鲜奶.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/dessert/炸鲜奶/炸鲜奶.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/dessert/炸鲜奶/炸鲜奶.jpg"
+ ],
+ "category": "甜品",
+ "difficulty": 3,
+ "tags": [
+ "甜品"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "牛奶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 牛奶",
+ "notes": "量未指定"
+ },
+ {
+ "name": "玉米淀粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 玉米淀粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "面包糠",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 面包糠",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "面包模具(或浅盘子)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 面包模具(或浅盘子)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "牛奶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 牛奶 250g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "玉米淀粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 玉米淀粉 30g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "面包糠",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 面包糠 100g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋 2 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖 30g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "将牛奶倒入碗中"
+ },
+ {
+ "step": 2,
+ "description": "加入玉米淀粉和白糖,搅拌均匀"
+ },
+ {
+ "step": 3,
+ "description": "将模具刷上食用油"
+ },
+ {
+ "step": 4,
+ "description": "牛奶下锅,中火烧开"
+ },
+ {
+ "step": 5,
+ "description": "烧开后转小火,边煮边搅拌"
+ },
+ {
+ "step": 6,
+ "description": "牛奶*变粘稠*后出锅,倒入模具"
+ },
+ {
+ "step": 7,
+ "description": "将模具放冰箱**冷却 1 小时**"
+ },
+ {
+ "step": 8,
+ "description": "拿出,切成大小均匀的条,随后放入碗中"
+ },
+ {
+ "step": 9,
+ "description": "向碗中倒入一半的面包糠,奶糊裹上后取出,备用"
+ },
+ {
+ "step": 10,
+ "description": "在一个新碗中打入鸡蛋,搅匀,备用"
+ },
+ {
+ "step": 11,
+ "description": "将奶糊裹上蛋液和剩余的面包糠"
+ },
+ {
+ "step": 12,
+ "description": "锅中倒入足以覆盖奶糊的油,下锅"
+ },
+ {
+ "step": 13,
+ "description": "奶糊外观*呈金黄状态*后停火,摆盘"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-dessert-烤蛋挞-烤蛋挞",
+ "name": "烤蛋挞的做法",
+ "description": "# 烤蛋挞的做法\n\n\n\n烤蛋挞是一道简单易于制作的甜品 且半成品可置于冰箱冷冻长时间保存 随吃随取 出品时间约 1 小时\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/dessert/烤蛋挞/烤蛋挞.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/dessert/烤蛋挞/烤蛋挞.png",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/dessert/烤蛋挞/烤蛋挞.png"
+ ],
+ "category": "甜品",
+ "difficulty": 4,
+ "tags": [
+ "甜品"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "蛋挞皮 品牌不限",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蛋挞皮 品牌不限",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "牛奶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 牛奶",
+ "notes": "量未指定"
+ },
+ {
+ "name": "淡奶油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 淡奶油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白砂糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白砂糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "烤箱 大小不限",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 烤箱 大小不限",
+ "notes": "量未指定"
+ },
+ {
+ "name": "克数称",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 克数称",
+ "notes": "量未指定"
+ },
+ {
+ "name": "搅拌器 包含且不限于筷子 打蛋器等工具",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 搅拌器 包含且不限于筷子 打蛋器等工具",
+ "notes": "量未指定"
+ },
+ {
+ "name": "筛网 网孔约为",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 筛网 网孔约为 1 毫米",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蛋挞皮 品牌不限 整包蛋挞皮约为",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蛋挞皮 品牌不限 整包蛋挞皮约为 30 只",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋 8 个 普通鸡蛋即可",
+ "notes": "量未指定"
+ },
+ {
+ "name": "牛奶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 牛奶 200 毫升 普通袋装牛奶即可",
+ "notes": "量未指定"
+ },
+ {
+ "name": "淡奶油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 淡奶油 450 毫升 烘焙店或超市即有售",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白砂糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白砂糖 80 克 普通砂糖即可 细砂糖更优 易于融化",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "将碗置于克数称上 称量 450 克 淡奶油(淡奶油密度在此处记为 1 )"
+ },
+ {
+ "step": 2,
+ "description": "加入 80 克白砂糖 (甜度中等 可按个人口味增减 建议范围 60-100 克)"
+ },
+ {
+ "step": 3,
+ "description": "加入 200 克牛奶 (牛奶密度在此处记为 1 )"
+ },
+ {
+ "step": 4,
+ "description": "取 8 个蛋黄加入 蛋清可留作他用"
+ },
+ {
+ "step": 5,
+ "description": "均匀搅拌所有材料直至白砂糖全部融化"
+ },
+ {
+ "step": 6,
+ "description": "使用网筛对搅拌完成的食材进行过滤 滤除鸡蛋黏膜 鸡蛋壳 未融化的白砂糖 结块的淡奶油"
+ },
+ {
+ "step": 7,
+ "description": "此时请将烤箱设置 220 摄氏度开始预热(约 10 分钟) 记得拿出烤盘"
+ },
+ {
+ "step": 8,
+ "description": "将蛋挞皮以 0.5 厘米的间隔均匀放置于烤盘中"
+ },
+ {
+ "step": 9,
+ "description": "将过滤完成的食材倒入蛋挞皮中 液面距离蛋挞皮上沿 0.5 厘米即可不宜过多"
+ },
+ {
+ "step": 10,
+ "description": "截止此步骤 半成品蛋挞的制作已经完成 可直接放入冰箱速冻 12 小时以上保存"
+ },
+ {
+ "step": 11,
+ "description": "将半成品蛋挞放入烤箱中进行烤制 温度为 200 摄氏度 时间为 25 分钟"
+ },
+ {
+ "step": 12,
+ "description": "烤制结束后即可食用"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-dessert-玛格丽特饼干-玛格丽特饼干",
+ "name": "玛格丽特饼干的做法",
+ "description": "# 玛格丽特饼干的做法\n\n\n\n玛格丽特饼干通常作为下午茶点心或伴随热饮享用,是一种经典而受欢迎的点心。它们的酥脆质地和丰富的黄油味道使它们成为许多人喜爱的饼干之一。\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/dessert/玛格丽特饼干/玛格丽特饼干.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/dessert/玛格丽特饼干/玛格丽特饼干.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/dessert/玛格丽特饼干/玛格丽特饼干.jpg"
+ ],
+ "category": "甜品",
+ "difficulty": 3,
+ "tags": [
+ "甜品"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "熟蛋黄",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 熟蛋黄",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黄油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黄油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白砂糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白砂糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "低筋面粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 低筋面粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "玉米淀粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 玉米淀粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "烤箱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 烤箱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "熟蛋黄",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 熟蛋黄 1 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黄油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黄油 50 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白砂糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白砂糖 20 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 1 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "低筋面粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 低筋面粉 50 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "玉米淀粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 玉米淀粉 50 克",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "黄油隔热水融化、将蛋黄磨碎备用。"
+ },
+ {
+ "step": 2,
+ "description": "在融化的黄油中添加糖、盐、以及碾碎的鸡蛋黄,搅拌均匀"
+ },
+ {
+ "step": 3,
+ "description": "加入低筋面粉与玉米淀粉,揉成面团"
+ },
+ {
+ "step": 4,
+ "description": "将面团均匀分割成大约 8 克重的小面团,然后将它们搓成球状。"
+ },
+ {
+ "step": 5,
+ "description": "使用大拇指轻压在每个小面团上,以形成裂纹。"
+ },
+ {
+ "step": 6,
+ "description": "预热烤箱至 150℃,将小面团放入烤箱中,烘烤 20 分钟。"
+ },
+ {
+ "step": 7,
+ "description": "微微放凉即可食用"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-dessert-红柚蛋糕-红柚蛋糕",
+ "name": "红柚蛋糕的做法",
+ "description": "# 红柚蛋糕的做法\n\n红柚蛋糕是空气炸锅基础甜点,一份适合单人食用,食材处理需要 10 分钟,烹饪需要 25 分钟。\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/dessert/红柚蛋糕/红柚蛋糕.md",
+ "image_path": null,
+ "images": [],
+ "category": "甜品",
+ "difficulty": 3,
+ "tags": [
+ "甜品"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "空气炸锅",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 空气炸锅",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "红柚果肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 红柚果肉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "面粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 面粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "锡纸盘",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 锡纸盘",
+ "notes": "量未指定"
+ },
+ {
+ "name": "油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 水",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋 2 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "面粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 面粉 80g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "红柚果肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 红柚果肉 20g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 油 15ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 水 80ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 糖 15g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "锡纸盘里打入鸡蛋 2 个, 加入红柚果肉 20g"
+ },
+ {
+ "step": 2,
+ "description": "锡纸盘中倒入 15ml 油并摇晃锡纸盘时期均匀覆盖盘底"
+ },
+ {
+ "step": 3,
+ "description": "锡纸盘中放入 10g 糖, 以及 40g 面粉和 40ml 水"
+ },
+ {
+ "step": 4,
+ "description": "用筷子顺时针方向搅拌至淡黄色糊状"
+ },
+ {
+ "step": 5,
+ "description": "锡纸盘中放入 5g 糖, 以及 40g 面粉和 40ml 水"
+ },
+ {
+ "step": 6,
+ "description": "继续用筷子搅拌至淡黄色糊状"
+ },
+ {
+ "step": 7,
+ "description": "锡纸盘放入空气炸锅的烤篮上,用 180 摄氏度烤 15 分钟"
+ },
+ {
+ "step": 8,
+ "description": "打开空气炸锅,小心取出锡纸盘,用筷子或勺子将蛋糕翻面"
+ },
+ {
+ "step": 9,
+ "description": "继续 180 摄氏度烤 8 分钟"
+ },
+ {
+ "step": 10,
+ "description": "取出即可食用"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-dessert-芋泥雪媚娘-芋泥雪媚娘",
+ "name": "芋泥雪媚娘的做法",
+ "description": "# 芋泥雪媚娘的做法\n\n\n\n芋泥雪媚娘是一道甜品,很适合做给孩子吃,无需烤箱,手残党也可以做成功~预计制作时间 2 小时。\n\n预估烹饪难度:★★★★★",
+ "source_path": "dishes/dessert/芋泥雪媚娘/芋泥雪媚娘.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/dessert/芋泥雪媚娘/芋泥雪媚娘成品.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/dessert/芋泥雪媚娘/芋泥雪媚娘成品.jpg"
+ ],
+ "category": "甜品",
+ "difficulty": 5,
+ "tags": [
+ "甜品"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "荔浦芋头(电商平台购买即可,实惠新鲜)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 荔浦芋头(电商平台购买即可,实惠新鲜)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "紫薯粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 紫薯粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "牛奶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 牛奶",
+ "notes": "量未指定"
+ },
+ {
+ "name": "糯米粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 糯米粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "玉米淀粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 玉米淀粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黄油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黄油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "淡奶油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 淡奶油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白砂糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白砂糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料理搅拌机(电动打蛋器也可以)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料理搅拌机(电动打蛋器也可以)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "筛网",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 筛网",
+ "notes": "量未指定"
+ },
+ {
+ "name": "保鲜膜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 保鲜膜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白砂糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白砂糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "荔浦芋头",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 荔浦芋头 200g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "紫薯粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 紫薯粉 3g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "牛奶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 牛奶 165g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "糯米粉 a",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 糯米粉 a 50g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "糯米粉 b",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 糯米粉 b 75g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "玉米淀粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 玉米淀粉 22g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黄油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黄油 30g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "淡奶油(推荐安佳)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 淡奶油(推荐安佳) 145g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白砂糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白砂糖 26g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "芋头切块,大火煮熟至软(40 分钟即可),全部放入料理机"
+ },
+ {
+ "step": 2,
+ "description": "向内加入 30g 牛奶,25g 淡奶油,将其打成泥状"
+ },
+ {
+ "step": 3,
+ "description": "再向内加入 3g 紫薯粉,18g 白砂糖,继续搅拌打成细腻芋泥"
+ },
+ {
+ "step": 4,
+ "description": "取出另一个碗,加入全部糯米粉 b,22g 玉米淀粉,135g 牛奶,50g 白砂糖,混匀并过筛一遍,保鲜膜盖上并扎小洞,中火蒸半个小时"
+ },
+ {
+ "step": 5,
+ "description": "在蒸的过程中,将糯米粉 a 放入平底锅小火翻炒至微微发黄(即炒熟),作为手粉备用"
+ },
+ {
+ "step": 6,
+ "description": "将中火蒸完半小时的糯米牛奶混合物(果冻状)趁热加入黄油 30g,将黄油揉至面团完全吸收,然后放冰箱冷藏一小时"
+ },
+ {
+ "step": 7,
+ "description": "取出另一只碗,加入 120g 淡奶油,8g 白砂糖,打发至有纹路,装进裱花袋备用"
+ },
+ {
+ "step": 8,
+ "description": "取出冷藏后的面团,搓揉 5 分钟,分成 30g 一个,均匀撒上 2g 手粉防粘,擀成圆形,先挤上 5g 裱花奶油,然后放上 30g 芋泥,最后将面饼像包包子一样包起来(可以减去多余的皮)"
+ },
+ {
+ "step": 9,
+ "description": "包好后再均匀撒 2g 手粉防粘"
+ },
+ {
+ "step": 10,
+ "description": "重复以上两步直至原材料用光"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-dessert-英式司康-英式司康",
+ "name": "英式司康的做法",
+ "description": "# 英式司康的做法\n\n\n\n英式司康是非常简单快手的下午茶甜品,可以搭配果酱、茶与咖啡。成品以蛋奶香气为主轴风味,糖量适中不会过于甜腻。\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/dessert/英式司康/英式司康.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/dessert/英式司康/英式司康.png",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/dessert/英式司康/英式司康.png"
+ ],
+ "category": "甜品",
+ "difficulty": 3,
+ "tags": [
+ "甜品"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "无盐黄油(推荐品牌总统)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 无盐黄油(推荐品牌总统)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "低筋面粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 低筋面粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "泡打粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 泡打粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "淡奶油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 淡奶油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "奶油奶酪(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 奶油奶酪(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "无盐黄油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 无盐黄油 40g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "低筋面粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 低筋面粉 180g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 糖 30g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 1g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "泡打粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 泡打粉 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋 1 个(约 50g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "淡奶油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 淡奶油 45g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "奶油奶酪",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 奶油奶酪 50g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "鸡蛋打散,称量出 30g 蛋液放入干净容器中,放入全量淡奶油和奶油奶酪混合均匀。如果奶酪太硬可以水浴加热至大约 40 度再混合。"
+ },
+ {
+ "step": 2,
+ "description": "将低筋面粉,盐,糖,泡打粉放入干净容器中混合均匀"
+ },
+ {
+ "step": 3,
+ "description": "黄油切成小块,放入上一步的混合物中,用手将黄油捏入混合物中,呈粗玉米粉质地"
+ },
+ {
+ "step": 4,
+ "description": "将第一步的蛋奶混合液倒入上一步得到的粉油混合物种,搅拌均接近。叠压成均匀面团"
+ },
+ {
+ "step": 5,
+ "description": "面团放到案板上,擀成 1.5cm 厚的面片,用刀或者模具分切成合适的形状"
+ },
+ {
+ "step": 6,
+ "description": "用刷子蘸取剩余的 20g 鸡蛋液,刷在司康表面"
+ },
+ {
+ "step": 7,
+ "description": "烤箱预热 180 度,烤制 27 分钟"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-dessert-草莓冰淇淋-草莓冰淇淋",
+ "name": "草莓冰淇淋的做法",
+ "description": "# 草莓冰淇淋的做法\n\n草莓冰淇淋是简单但好吃的冰淇淋,可以做很多不同的口味。这次将用当季的新鲜草莓制作美味,**不需要搅拌**的草莓冰淇淋。\n\n预估烹饪难度:★★",
+ "source_path": "dishes/dessert/草莓冰淇淋/草莓冰淇淋.md",
+ "image_path": null,
+ "images": [],
+ "category": "甜品",
+ "difficulty": 2,
+ "tags": [
+ "甜品"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "加糖炼乳",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 加糖炼乳",
+ "notes": "量未指定"
+ },
+ {
+ "name": "草莓",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 草莓",
+ "notes": "量未指定"
+ },
+ {
+ "name": "重奶油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 重奶油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香草精",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香草精",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冰淇淋模具(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冰淇淋模具(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "草莓糖浆:",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 草莓糖浆:",
+ "notes": "量未指定"
+ },
+ {
+ "name": "草莓",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 草莓 500 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白砂糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白砂糖 45 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香草精",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香草精 1 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食盐 1 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冰淇淋底料:",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冰淇淋底料:",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香草精",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香草精 5 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食盐 1 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "重奶油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 重奶油 6 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "加糖炼乳",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 加糖炼乳 400 克",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "先做草莓糖浆。把草莓洗干净,去掉顶部叶子。将草莓切成 **5mm** 的小块。保留一半切碎的草莓,稍后折叠成冰淇淋。"
+ },
+ {
+ "step": 2,
+ "description": "将另一半切碎的草莓和糖一起放入酱汁锅中。用中火搅拌和烹饪,直到草莓释放液体并在锅中形成糖浆。"
+ },
+ {
+ "step": 3,
+ "description": "让草莓在糖浆中加热,不时搅拌,直到它们分解并变形,糖浆稍微变稠。"
+ },
+ {
+ "step": 4,
+ "description": "当糖浆保持分开 **3秒钟** 时,就已经准备好了。把糖浆从火上移开,加入香草和盐搅拌。将草莓糖浆放在一边冷却。"
+ },
+ {
+ "step": 5,
+ "description": "当糖浆冷却时,准备冰淇淋基料。在碗中加入甜炼乳、浓奶油、香草精和盐。使用手动搅拌器搅打混合物,直到它变得轻盈蓬松,并形成柔软的尖峰。"
+ },
+ {
+ "step": 6,
+ "description": "将保留的切碎的新鲜草莓折叠到冰淇淋底座中。将生过的冰淇淋底座转移到冷冻安全容器中。将冷却的草莓糖浆淋在冰淇淋上,然后轻轻地将其旋入混合物中。"
+ },
+ {
+ "step": 7,
+ "description": "盖上冰淇淋并冷冻 **八小时** ,然后舀取和食用。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-dessert-酸奶意式奶冻-酸奶意式奶冻",
+ "name": "酸奶意式奶冻的做法",
+ "description": "# 酸奶意式奶冻的做法\n\n\n\n意式奶冻非常适合作为餐后甜品,可以搭配果酱、水果和香草。成品增加了原味酸奶,不会过于甜腻。\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/dessert/酸奶意式奶冻/酸奶意式奶冻.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/dessert/酸奶意式奶冻/酸奶意式奶冻.png",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/dessert/酸奶意式奶冻/酸奶意式奶冻.png"
+ ],
+ "category": "甜品",
+ "difficulty": 4,
+ "tags": [
+ "甜品"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "淡奶油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 淡奶油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "原味酸奶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 原味酸奶",
+ "notes": "量未指定"
+ },
+ {
+ "name": "吉利丁片",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 吉利丁片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "筛网",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 筛网",
+ "notes": "量未指定"
+ },
+ {
+ "name": "淡奶油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 淡奶油 200g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 糖 40g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "原味酸奶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 原味酸奶 250g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "吉利丁片",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 吉利丁片 6g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "吉利丁片剪成小片,泡入冷水中"
+ },
+ {
+ "step": 2,
+ "description": "淡奶油和糖放入锅中,加热至 60 度"
+ },
+ {
+ "step": 3,
+ "description": "关火,吉利丁从水中取出,控干水份,加入热淡奶油中,搅拌均匀"
+ },
+ {
+ "step": 4,
+ "description": "淡奶油降温至 40 度,加入原味酸奶,搅拌均匀"
+ },
+ {
+ "step": 5,
+ "description": "将上述步骤得到的混合物过两遍筛网"
+ },
+ {
+ "step": 6,
+ "description": "分装入合适的容器,放入冰箱冷藏 4 小时以上"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-dessert-雪花酥-雪花酥",
+ "name": "雪花酥的做法",
+ "description": "# 雪花酥的做法\n\n\n\n雪花酥是一个快捷简便的甜点,适合装盒送礼,制作耗时 30 分钟。\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/dessert/雪花酥/雪花酥.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/dessert/雪花酥/雪花酥成品.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/dessert/雪花酥/雪花酥成品.jpg"
+ ],
+ "category": "甜品",
+ "difficulty": 3,
+ "tags": [
+ "甜品"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "无盐黄油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 无盐黄油 20g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "棉花糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 棉花糖 75g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "全脂奶粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 全脂奶粉 40g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "混合坚果",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 混合坚果 60g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "饼干",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 饼干 75g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "无盐黄油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 无盐黄油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "棉花糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 棉花糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "全脂奶粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 全脂奶粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "混合坚果(三只松鼠每日坚果)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 混合坚果(三只松鼠每日坚果)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "饼干(非夹心型饼干,推荐小奇福或购买使用雪花酥烘焙专用小饼干)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 饼干(非夹心型饼干,推荐小奇福或购买使用雪花酥烘焙专用小饼干)",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "饼干超过一元硬币大小先切成小块"
+ },
+ {
+ "step": 2,
+ "description": "无盐黄油加入锅中,小火加热至无盐黄油完全融化"
+ },
+ {
+ "step": 3,
+ "description": "棉花糖加入锅中,使用刮刀搅拌,直至棉花糖融化并与无盐黄油均匀融合"
+ },
+ {
+ "step": 4,
+ "description": "20g 奶粉加入锅中,使用刮刀搅拌,奶粉与黄油棉花糖混合物搅拌均匀后立即关火"
+ },
+ {
+ "step": 5,
+ "description": "准备好的所有混合坚果与饼干趁热加入锅中,使用刮刀搅拌"
+ },
+ {
+ "step": 6,
+ "description": "搅拌到温度下降到手可以接触的温度后,戴上一次塑料手套,在锅中搓揉或者双手拿起拉扯,让饼干混合坚果与棉花糖黄油奶粉混合物分散均匀。"
+ },
+ {
+ "step": 7,
+ "description": "将上述步骤混合物压入模具中,边角压实,擀面杖擀平,未满的一边用手尽量压成直边"
+ },
+ {
+ "step": 8,
+ "description": "室温放凉,完全冷却后脱模,按照模具纹路切块,或切成自己喜欢的大小,撒上剩余奶粉,尽量使雪花酥每面都沾上奶粉"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-dessert-魔芋蛋糕-魔芋蛋糕",
+ "name": "魔芋蛋糕的做法",
+ "description": "# 魔芋蛋糕的做法\n\n魔芋蛋糕是一款低热量的甜点。蛋糕本身无麸质,并使用无热量的甜味剂代替白砂糖,非常适合减脂人群。加上烘烤时间,一般需要 **0.5 小时**即可完成。\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/dessert/魔芋蛋糕/魔芋蛋糕.md",
+ "image_path": null,
+ "images": [],
+ "category": "甜品",
+ "difficulty": 4,
+ "tags": [
+ "甜品"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "3 个鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 3 个鸡蛋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "赤藓糖醇",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 赤藓糖醇 50g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "可可粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 可可粉 10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "魔芋粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 魔芋粉 10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "塔塔粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 塔塔粉 1g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "赤藓糖醇",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 赤藓糖醇",
+ "notes": "量未指定"
+ },
+ {
+ "name": "可可粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 可可粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "魔芋粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 魔芋粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "塔塔粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 塔塔粉",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "在准备蛋糕糊之前,可以开始预热烤箱至 350 ℉( 150 ℃)以节省时间"
+ },
+ {
+ "step": 2,
+ "description": "从冰箱中取出新鲜的鸡蛋"
+ },
+ {
+ "step": 3,
+ "description": "准备两个容器并擦干,分别盛放蛋清与蛋黄"
+ },
+ {
+ "step": 4,
+ "description": "对盛放蛋清的容器,可稍有水珠,但**不能有任何油**;盛放蛋黄的容器不能有水珠"
+ },
+ {
+ "step": 5,
+ "description": "打蛋,手工或利用分蛋器,将蛋清与蛋黄分离到两个容器中。"
+ },
+ {
+ "step": 6,
+ "description": "分离过程中蛋黄不能破碎,**蛋清中不能混有任何蛋黄**,否则会严重影响打发。(白色系带可进入蛋清,不影响)"
+ },
+ {
+ "step": 7,
+ "description": "(注意,不使用厨房机的情况下,盛放蛋清的容器也是打蛋的容器,为避免溢出,加入全部蛋清后不要超过容器的 **1/8**)"
+ },
+ {
+ "step": 8,
+ "description": "蛋清中加入 1g 塔塔粉"
+ },
+ {
+ "step": 9,
+ "description": "打蛋器高速,打发蛋白至*粗大气泡的状态*,加入 50g 赤藓糖醇"
+ },
+ {
+ "step": 10,
+ "description": "打蛋器中低速,打发蛋白至*干性发泡*的状态(提起打蛋器头,有短小直立的尖角;倒扣容器,蛋白可粘住容器不掉下来)"
+ },
+ {
+ "step": 11,
+ "description": "此时蛋白打发程度已符合要求"
+ },
+ {
+ "step": 12,
+ "description": "打蛋器应尽量贴近容器底部,防止出现上面浮着的表层打发,底部仍然是液体的情况)"
+ },
+ {
+ "step": 13,
+ "description": "把分离的蛋黄加入打发的蛋清中,打蛋器中低速搅拌均匀"
+ },
+ {
+ "step": 14,
+ "description": "蛋清中加入 **10g 可可粉**和 **10g 魔芋粉**,先用餐刀翻拌,这是由于如果直接用打蛋器搅拌会造成粉末飞溅"
+ },
+ {
+ "step": 15,
+ "description": "翻拌手法是"
+ },
+ {
+ "step": 16,
+ "description": "先用右手拿刮刀从搅拌盆中心插入面糊底部"
+ },
+ {
+ "step": 17,
+ "description": "向 8 点钟方向刮去直到碰到盆壁,顺势舀起面糊提到空中,然后再移回盆中心将面糊放入盆内"
+ },
+ {
+ "step": 18,
+ "description": "左手握住搅拌盆从 9 点钟方向转到 7 点钟方向,刚好旋转了 60 度,就完成了 1 次循环"
+ },
+ {
+ "step": 19,
+ "description": "速度大约是 1 秒钟 2 下"
+ },
+ {
+ "step": 20,
+ "description": "此方法出自《小岛老师的蛋糕教室》。用接地气的话说就是,像炒菜一样翻炒。"
+ },
+ {
+ "step": 21,
+ "description": "翻拌结束后,打蛋器低速搅拌均匀"
+ },
+ {
+ "step": 22,
+ "description": "模具铺好烘焙纸,贴合底部与内壁"
+ },
+ {
+ "step": 23,
+ "description": "将蛋糕糊倒入模具,然后用餐刀抹平,再然后震荡几下避免大气泡"
+ },
+ {
+ "step": 24,
+ "description": "烘烤 25 分钟"
+ },
+ {
+ "step": 25,
+ "description": "烤好后,戴好隔热手套取出"
+ },
+ {
+ "step": 26,
+ "description": "(可选) 将模具从高处落下,震出其中的热气"
+ },
+ {
+ "step": 27,
+ "description": "模具倒扣 10 分钟,使蛋糕冷却"
+ },
+ {
+ "step": 28,
+ "description": "没有冷却的蛋糕立刻脱模会损伤蛋糕"
+ },
+ {
+ "step": 29,
+ "description": "此操作可能会**烫手**,注意戴好隔热手套"
+ },
+ {
+ "step": 30,
+ "description": "脱模,餐刀切块食用"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-dessert-龟苓膏-龟苓膏",
+ "name": "龟苓膏的做法",
+ "description": "# 龟苓膏的做法\n\n\n\n预估烹饪难度:★★\n\n---",
+ "source_path": "dishes/dessert/龟苓膏/龟苓膏.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/dessert/龟苓膏/龟苓膏成品.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/dessert/龟苓膏/龟苓膏成品.jpg"
+ ],
+ "category": "甜品",
+ "difficulty": 2,
+ "tags": [
+ "甜品"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "龟苓膏粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 龟苓膏粉 25 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冷水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冷水 120 毫升",
+ "notes": "量未指定"
+ },
+ {
+ "name": "开水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 开水 500 毫升",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白砂糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白砂糖 100 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小锅",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小锅",
+ "notes": "量未指定"
+ },
+ {
+ "name": "搅拌工具",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 搅拌工具",
+ "notes": "量未指定"
+ },
+ {
+ "name": "模具或碗",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 模具或碗",
+ "notes": "量未指定"
+ },
+ {
+ "name": "--",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- --",
+ "notes": "量未指定"
+ },
+ {
+ "name": "--",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- --",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "--"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-drink-B52轰炸机",
+ "name": "B52轰炸机的做法",
+ "description": "# B52轰炸机的做法\n\nB-52 是鸡尾酒中喝法比较独特的一种,要配上短吸管,餐巾纸和打火机。\n\n把酒点燃,用吸管一口气喝完,然后就能体验到先冷后热那种冰火两重天的感觉。那种感觉,只有试过才知道。\n\n用吸管适用于女士,最刺激的喝法是一口喝下,喝的时候注意尽量避免碰到杯口引起烫伤,让火在嘴里灭掉,才能喝出最好的味道。\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/drink/B52轰炸机.md",
+ "image_path": null,
+ "images": [],
+ "category": "饮品",
+ "difficulty": 3,
+ "tags": [
+ "饮品"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "甘露咖啡酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 甘露咖啡酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "爱尔兰百利甜酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 爱尔兰百利甜酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蓝天原味伏特加",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蓝天原味伏特加",
+ "notes": "量未指定"
+ },
+ {
+ "name": "吧勺",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 吧勺",
+ "notes": "量未指定"
+ },
+ {
+ "name": "利口酒杯",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 利口酒杯",
+ "notes": "量未指定"
+ },
+ {
+ "name": "打火机",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 打火机",
+ "notes": "量未指定"
+ },
+ {
+ "name": "甘露咖啡酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 甘露咖啡酒 10ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "爱尔兰百利甜酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 爱尔兰百利甜酒 10ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蓝天原味伏特加",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蓝天原味伏特加 10ml",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "在利口酒杯的最底层倒入甘露咖啡酒到 1/3 处。(10ml)"
+ },
+ {
+ "step": 2,
+ "description": "顺着吧勺缓缓倒入爱尔兰百利甜酒,也是 1/3 处 (10ml)。注意要慢,保证层次分明。(太快甜酒会和咖啡混合)"
+ },
+ {
+ "step": 3,
+ "description": "最后在上层倒入蓝天原味伏特加 (10ml)"
+ },
+ {
+ "step": 4,
+ "description": "用打火机热一下杯口"
+ },
+ {
+ "step": 5,
+ "description": "最后一步点火: 看到淡蓝色的小火苗了吗?"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-drink-Mojito莫吉托",
+ "name": "Mojito莫吉托的做法",
+ "description": "# Mojito莫吉托的做法\n\nMojito 是一种传统的古巴高球鸡尾酒。\n\n这种调酒有着相对低的酒精含量(大约 10%)。\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/drink/Mojito莫吉托.md",
+ "image_path": null,
+ "images": [],
+ "category": "饮品",
+ "difficulty": 3,
+ "tags": [
+ "饮品"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "打碎的冰块",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 打碎的冰块",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冰镇苏打水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冰镇苏打水",
+ "notes": "量未指定"
+ },
+ {
+ "name": "压汁器",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 压汁器",
+ "notes": "量未指定"
+ },
+ {
+ "name": "海波杯",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 海波杯",
+ "notes": "量未指定"
+ },
+ {
+ "name": "研杵",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 研杵",
+ "notes": "量未指定"
+ },
+ {
+ "name": "一块青柠(切成两个半块)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 一块青柠(切成两个半块)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "五珠薄荷叶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 五珠薄荷叶",
+ "notes": "量未指定"
+ },
+ {
+ "name": "糖浆",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 糖浆 20ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "金色朗姆酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 金色朗姆酒 45ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蓝天原味伏特加",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蓝天原味伏特加 10ml",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "将切成半块的青柠之一切成小块,放入海波杯,随后用研杵将其捣出汁;"
+ },
+ {
+ "step": 2,
+ "description": "用 3-4 珠薄荷叶沿着杯口涂抹,随后将其放入杯中;"
+ },
+ {
+ "step": 3,
+ "description": "加入 糖浆 20ml;"
+ },
+ {
+ "step": 4,
+ "description": "加入 金色朗姆酒 45ml;"
+ },
+ {
+ "step": 5,
+ "description": "将剩下的半块青柠压出汁水放入杯中;"
+ },
+ {
+ "step": 6,
+ "description": "轻轻搅拌,使砂糖/糖浆处于半融合状态;"
+ },
+ {
+ "step": 7,
+ "description": "将打碎的冰块放入杯中,直到占杯中 3/4;"
+ },
+ {
+ "step": 8,
+ "description": "加入冰镇苏打水直到刚好淹没碎冰;"
+ },
+ {
+ "step": 9,
+ "description": "旋转搅拌半分钟;"
+ },
+ {
+ "step": 10,
+ "description": "使用碎冰将海波杯补满;"
+ },
+ {
+ "step": 11,
+ "description": "将剩下的一株薄荷叶拍醒,插入碎冰,作装饰。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-drink-冬瓜茶",
+ "name": "冬瓜茶的做法",
+ "description": "# 冬瓜茶的做法\n\n冬瓜茶是一种清爽的传统饮料,一般初学者需要 4~5 小时完成。\n\n预估烹饪难度:★★",
+ "source_path": "dishes/drink/冬瓜茶.md",
+ "image_path": null,
+ "images": [],
+ "category": "饮品",
+ "difficulty": 2,
+ "tags": [
+ "饮品"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "冬瓜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冬瓜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冰糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冰糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "保鲜膜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 保鲜膜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "过滤网",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 过滤网",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大锅",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大锅",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冬瓜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冬瓜 1000g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冰糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冰糖 300g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-drink-可乐桶",
+ "name": "可乐桶的做法",
+ "description": "# 可乐桶的做法\n\n**饮酒有害健康,未成年人禁止饮酒**\n\n预估烹饪难度:★★",
+ "source_path": "dishes/drink/可乐桶.md",
+ "image_path": null,
+ "images": [],
+ "category": "饮品",
+ "difficulty": 2,
+ "tags": [
+ "饮品"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "波旁威士忌",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 波旁威士忌",
+ "notes": "量未指定"
+ },
+ {
+ "name": "可口可乐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 可口可乐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冰块",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冰块",
+ "notes": "量未指定"
+ },
+ {
+ "name": "柠檬(可选,提升口感用)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 柠檬(可选,提升口感用)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "手动压汁器",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 手动压汁器",
+ "notes": "量未指定"
+ },
+ {
+ "name": "威士忌",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 威士忌 100 毫升",
+ "notes": "量未指定"
+ },
+ {
+ "name": "可口可乐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 可口可乐 500 毫升",
+ "notes": "量未指定"
+ },
+ {
+ "name": "柠檬",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 柠檬 1 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冰块",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冰块 300 克",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-drink-奶茶",
+ "name": "奶茶的做法",
+ "description": "# 奶茶的做法\n\n奶茶是一种简单易做的饮料。一般初学者只需要 30 分钟即可完成。\n\n预估烹饪难度:★★",
+ "source_path": "dishes/drink/奶茶.md",
+ "image_path": null,
+ "images": [],
+ "category": "饮品",
+ "difficulty": 2,
+ "tags": [
+ "饮品"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "袋泡红茶(推荐立顿黄牌精选红茶)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 袋泡红茶(推荐立顿黄牌精选红茶)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "全脂奶粉或淡奶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 全脂奶粉或淡奶",
+ "notes": "量未指定"
+ },
+ {
+ "name": "杯子,例如带刻度的杯子,陶瓷杯或保温杯",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 杯子,例如带刻度的杯子,陶瓷杯或保温杯",
+ "notes": "量未指定"
+ },
+ {
+ "name": "袋泡红茶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 袋泡红茶 2 包(约 4g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "奶粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 奶粉 11-12g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "砂糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 砂糖 5-7g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "取袋泡红茶 2 包放入杯中,加入 180-200mL **沸水**。"
+ },
+ {
+ "step": 2,
+ "description": "**等待 20 - 30 分钟**。"
+ },
+ {
+ "step": 3,
+ "description": "称取 11-12g 奶粉和 5-7g 砂糖,分别加入前一步骤得到的液体中。"
+ },
+ {
+ "step": 4,
+ "description": "搅拌均匀即可饮用。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-drink-杨枝甘露",
+ "name": "杨枝甘露的做法",
+ "description": "# 杨枝甘露的做法\n\n没用西谷米的原因是家里没有,但是有很多的奇亚籽就拿来代替。而且奇亚籽用泡不用煮,省了很多时间!\n\n预估烹饪难度:★★",
+ "source_path": "dishes/drink/杨枝甘露.md",
+ "image_path": null,
+ "images": [],
+ "category": "饮品",
+ "difficulty": 2,
+ "tags": [
+ "饮品"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "杯子",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 杯子",
+ "notes": "量未指定"
+ },
+ {
+ "name": "水果刀",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 水果刀",
+ "notes": "量未指定"
+ },
+ {
+ "name": "牛奶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 牛奶",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冰块",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冰块",
+ "notes": "量未指定"
+ },
+ {
+ "name": "调理机/果汁机",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 调理机/果汁机",
+ "notes": "量未指定"
+ },
+ {
+ "name": "奇亚籽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 奇亚籽 24g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "牛奶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 牛奶 50ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冰块",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冰块 2 小块",
+ "notes": "量未指定"
+ },
+ {
+ "name": "芒果",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 芒果 1 粒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葡萄柚",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葡萄柚 1/2 粒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "椰奶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 椰奶 150ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "切丝芒果干 (可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 切丝芒果干 (可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "切丝柳橙干 (可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 切丝柳橙干 (可选)",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "奇亚籽泡牛奶 10 分钟。"
+ },
+ {
+ "step": 2,
+ "description": "泡籽之时,把半粒芒果、葡萄柚去皮切丁,放入杯中。"
+ },
+ {
+ "step": 3,
+ "description": "半粒芒果切小块放入调理机加冰块、椰奶打成泥。"
+ },
+ {
+ "step": 4,
+ "description": "倒入杯中,放上点缀材料(如有)。"
+ },
+ {
+ "step": 5,
+ "description": "一边享用一边写代码!!"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-drink-酸梅汤(半成品加工)",
+ "name": "酸梅汤(半成品加工)的做法",
+ "description": "# 酸梅汤(半成品加工)的做法\n\n预估烹饪难度:★",
+ "source_path": "dishes/drink/酸梅汤(半成品加工).md",
+ "image_path": null,
+ "images": [],
+ "category": "饮品",
+ "difficulty": 1,
+ "tags": [
+ "饮品"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "酸梅晶固体饮料",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酸梅晶固体饮料",
+ "notes": "量未指定"
+ },
+ {
+ "name": "方糖(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 方糖(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "北京二锅头酒(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 北京二锅头酒(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "饮用水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 饮用水 1177 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酸梅晶固体饮料",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酸梅晶固体饮料 120 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "方糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 方糖 9 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "北京二锅头酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 北京二锅头酒 48 克",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "取饮用水 1177 克。"
+ },
+ {
+ "step": 2,
+ "description": "放入酸梅晶固体饮料 60 克,使用汤匙顺时针搅拌 50 圈。"
+ },
+ {
+ "step": 3,
+ "description": "再放入剩下 60 克酸梅晶固体饮料,再次使用汤匙顺时针搅拌 50 圈。"
+ },
+ {
+ "step": 4,
+ "description": "放入 9 克的方糖,使用汤匙顺时针搅拌 100 圈。"
+ },
+ {
+ "step": 5,
+ "description": "放入北京二锅头酒 48 克,用汤匙顺时针搅拌 30 圈。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-drink-长岛冰茶",
+ "name": "长岛冰茶的做法",
+ "description": "# 长岛冰茶的做法\n\n**饮酒有害健康,未成年人禁止饮酒**\n\n预估烹饪难度:★★",
+ "source_path": "dishes/drink/长岛冰茶.md",
+ "image_path": null,
+ "images": [],
+ "category": "饮品",
+ "difficulty": 2,
+ "tags": [
+ "饮品"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "金酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 金酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "龙舌兰酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 龙舌兰酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "伏特加",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 伏特加",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白朗姆酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白朗姆酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "橙味甜酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 橙味甜酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "柠檬",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 柠檬",
+ "notes": "量未指定"
+ },
+ {
+ "name": "枫糖浆",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 枫糖浆",
+ "notes": "量未指定"
+ },
+ {
+ "name": "可乐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 可乐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冰块",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冰块",
+ "notes": "量未指定"
+ },
+ {
+ "name": "高球杯(容量",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 高球杯(容量 300ml)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "金酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 金酒 15ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "龙舌兰酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 龙舌兰酒 15ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "伏特加",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 伏特加 15ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白朗姆酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白朗姆酒 15ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "橙味甜酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 橙味甜酒 15ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "柠檬汁",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 柠檬汁 30ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "枫糖浆",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 枫糖浆 20ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "可乐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 可乐 75ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "柠檬",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 柠檬 1 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冰块",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冰块 100 克",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "柠檬对半切,挤出 30ml 柠檬汁至杯中;"
+ },
+ {
+ "step": 2,
+ "description": "依次向杯中加入:"
+ },
+ {
+ "step": 3,
+ "description": "向杯中缓慢倒入 20ml 枫糖浆,边倒边搅拌;"
+ },
+ {
+ "step": 4,
+ "description": "向杯中加入 75ml 可乐;"
+ },
+ {
+ "step": 5,
+ "description": "向杯中加入冰块直至满杯;"
+ },
+ {
+ "step": 6,
+ "description": "轻轻搅拌 20 秒;"
+ },
+ {
+ "step": 7,
+ "description": "开始享用."
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-drink-冰粉-冰粉",
+ "name": "冰粉的做法",
+ "description": "# 冰粉的做法\n\n成品1.jpg)\n成品2.jpg)\n\n石凉粉,在有些地区也叫作冰粉,是河南省信阳市浉河区的一种著名特色小吃,属于豫菜系。该菜品类似果冻,但因为是天然植物做出来的,所以比果冻更健康,配上薄荷汁、柠檬汁、红豆等调料,清凉解暑。该食物深当地人的喜爱,老少皆宜。\n\n制作方法简单,只是有些耗时,预计制作时长 3 小时(其中包含 2.5 小时静置成型时间)。\n\n预估烹饪难度:★★",
+ "source_path": "dishes/drink/冰粉/冰粉.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/drink/冰粉/石凉粉(冰粉)成品1.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/drink/冰粉/石凉粉(冰粉)成品1.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/drink/冰粉/石凉粉(冰粉)成品2.jpg"
+ ],
+ "category": "饮品",
+ "difficulty": 2,
+ "tags": [
+ "饮品"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "冰粉籽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冰粉籽 200g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "过滤豆浆渣的纱布一块",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 过滤豆浆渣的纱布一块",
+ "notes": "量未指定"
+ },
+ {
+ "name": "凉白开",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 凉白开 2000g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "薄荷汁",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 薄荷汁 10ml / 薄荷粉 10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "一次性透明塑料杯(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 一次性透明塑料杯(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "遇水发光冰块(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 遇水发光冰块(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冰粉籽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冰粉籽 200g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "凉白开",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 凉白开 2000g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "薄荷汁",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 薄荷汁 10ml / 薄荷粉 10g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "将凉白开倒入盆中;"
+ },
+ {
+ "step": 2,
+ "description": "将冰粉籽全部用纱布包起来,开口处打结"
+ },
+ {
+ "step": 3,
+ "description": "将包好的冰粉籽放入凉白开中,在凉白开中用力揉搓 6 分钟"
+ },
+ {
+ "step": 4,
+ "description": "然后将凉白开放置 2.5 小时,即可成型"
+ },
+ {
+ "step": 5,
+ "description": "随后将石凉粉用勺子装进准备好的一次性透明塑料杯中,加入 10ml 薄荷汁或者 10g 薄荷粉(柠檬汁、山楂汁、桑椹汁也可),再放入遇水发光冰块,用勺子慢慢搅拌均匀"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-drink-奇异果菠菜特调-奇异果菠菜特调",
+ "name": "奇异果菠菜特调的做法",
+ "description": "# 奇异果菠菜特调的做法\n\n预估烹饪难度:★",
+ "source_path": "dishes/drink/奇异果菠菜特调/奇异果菠菜特调.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/drink/奇异果菠菜特调/kiwi-example.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/drink/奇异果菠菜特调/kiwi-example.jpg"
+ ],
+ "category": "饮品",
+ "difficulty": 1,
+ "tags": [
+ "饮品"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "原料:",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 原料:",
+ "notes": "量未指定"
+ },
+ {
+ "name": "奇异果",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 奇异果",
+ "notes": "量未指定"
+ },
+ {
+ "name": "苹果",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 苹果",
+ "notes": "量未指定"
+ },
+ {
+ "name": "菠菜叶(",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 菠菜叶( 2-5 片)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 水",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白砂糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白砂糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "工具",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 工具",
+ "notes": "量未指定"
+ },
+ {
+ "name": "榨汁机",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 榨汁机",
+ "notes": "量未指定"
+ },
+ {
+ "name": "饮用水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 饮用水 700ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "奇异果",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 奇异果 2 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "苹果",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 苹果 1/2 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "菠菜叶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 菠菜叶 4 叶",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白砂糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白砂糖 12 克",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "将猕猴桃切成两半,每半再分四份小块"
+ },
+ {
+ "step": 2,
+ "description": "将苹果切丁"
+ },
+ {
+ "step": 3,
+ "description": "将菠菜叶去梗,只留叶子部分"
+ },
+ {
+ "step": 4,
+ "description": "将菠菜切碎"
+ },
+ {
+ "step": 5,
+ "description": "一起倒入榨汁机搅拌杯"
+ },
+ {
+ "step": 6,
+ "description": "注水"
+ },
+ {
+ "step": 7,
+ "description": "加入白砂糖"
+ },
+ {
+ "step": 8,
+ "description": "启动搅拌机,搅拌约 4 个 15 秒(每 15 秒停下看状态)"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-drink-柠檬水-柠檬水",
+ "name": "柠檬水的做法",
+ "description": "# 柠檬水的做法\n\n\n\n预估烹饪难度:★",
+ "source_path": "dishes/drink/柠檬水/柠檬水.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/drink/柠檬水/柠檬水.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/drink/柠檬水/柠檬水.jpg"
+ ],
+ "category": "饮品",
+ "difficulty": 1,
+ "tags": [
+ "饮品"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "原料",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 原料",
+ "notes": "量未指定"
+ },
+ {
+ "name": "柠檬",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 柠檬",
+ "notes": "量未指定"
+ },
+ {
+ "name": "果蜜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 果蜜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冰(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冰(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "工具",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 工具",
+ "notes": "量未指定"
+ },
+ {
+ "name": "雪克杯",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 雪克杯",
+ "notes": "量未指定"
+ },
+ {
+ "name": "柠檬",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 柠檬 40~45 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "果蜜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 果蜜 40~45 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冰几块(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冰几块(可选)",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "称 40~45 克柠檬,放入雪克杯中"
+ },
+ {
+ "step": 2,
+ "description": "雪克杯盖盖子锤大约 10 次"
+ },
+ {
+ "step": 3,
+ "description": "加入果蜜 40~45 克"
+ },
+ {
+ "step": 4,
+ "description": "补水"
+ },
+ {
+ "step": 5,
+ "description": "摇晃均匀"
+ },
+ {
+ "step": 6,
+ "description": "最后根据喜好加冰"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-drink-泰国手标红茶-泰国手标红茶",
+ "name": "泰国手标红茶的做法",
+ "description": "# 泰国手标红茶的做法\n\n\n\n泰国手标红茶是泰国街头随处可见的奶茶,味道香纯,绵密。\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/drink/泰国手标红茶/泰国手标红茶.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/drink/泰国手标红茶/泰国手标红茶.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/drink/泰国手标红茶/泰国手标红茶.jpg"
+ ],
+ "category": "饮品",
+ "difficulty": 3,
+ "tags": [
+ "饮品"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 水",
+ "notes": "量未指定"
+ },
+ {
+ "name": "茶粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 茶粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "炼乳",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 炼乳",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "牛奶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 牛奶",
+ "notes": "量未指定"
+ },
+ {
+ "name": "克称",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 克称",
+ "notes": "量未指定"
+ },
+ {
+ "name": "带刻度容器",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 带刻度容器",
+ "notes": "量未指定"
+ },
+ {
+ "name": "港式奶茶过滤袋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 港式奶茶过滤袋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "水(600cc)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 水(600cc)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "茶粉(20g)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 茶粉(20g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖(24g)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖(24g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "牛奶(18ml)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 牛奶(18ml)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "炼乳(24g)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 炼乳(24g)",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "600cc 水大火烧开"
+ },
+ {
+ "step": 2,
+ "description": "在过滤袋中装入 20g 茶粉,开水倒入过滤袋中,过滤 20 遍"
+ },
+ {
+ "step": 3,
+ "description": "使用克称量取 24g 炼乳、24g 白糖和 18ml 牛奶放入 1000ml 以上的水壶中"
+ },
+ {
+ "step": 4,
+ "description": "将过滤好的茶水倒入水壶中搅拌,直到白糖融化"
+ },
+ {
+ "step": 5,
+ "description": "将水壶放到冰箱 4 小时以上"
+ },
+ {
+ "step": 6,
+ "description": "喝前可以加 6-8 颗冰块"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-drink-海边落日-海边落日",
+ "name": "海边落日的做法",
+ "description": "# 海边落日的做法\n\n**饮酒有害健康,未成年人禁止饮酒**\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/drink/海边落日/海边落日.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/drink/海边落日/海边落日.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/drink/海边落日/海边落日.jpg"
+ ],
+ "category": "饮品",
+ "difficulty": 3,
+ "tags": [
+ "饮品"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "红石榴糖浆",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 红石榴糖浆",
+ "notes": "量未指定"
+ },
+ {
+ "name": "NFC 橙汁",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- NFC 橙汁",
+ "notes": "量未指定"
+ },
+ {
+ "name": "苏打水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 苏打水",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白朗姆",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白朗姆",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蓝橙力娇酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蓝橙力娇酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "柠檬汁",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 柠檬汁",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冰块",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冰块",
+ "notes": "量未指定"
+ },
+ {
+ "name": "柠檬",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 柠檬",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大号的玻璃杯",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大号的玻璃杯",
+ "notes": "量未指定"
+ },
+ {
+ "name": "搅拌棒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 搅拌棒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "量酒器",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 量酒器",
+ "notes": "量未指定"
+ },
+ {
+ "name": "调酒杯",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 调酒杯",
+ "notes": "量未指定"
+ },
+ {
+ "name": "吸管",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 吸管",
+ "notes": "量未指定"
+ },
+ {
+ "name": "水果刀",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 水果刀",
+ "notes": "量未指定"
+ },
+ {
+ "name": "红石榴糖浆",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 红石榴糖浆 15ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "橙汁",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 橙汁 35~50ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "苏打水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 苏打水 50ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白朗姆",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白朗姆 30ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蓝橙力娇酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蓝橙力娇酒 15ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "柠檬汁",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 柠檬汁 15ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大冰块差不多就行",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大冰块差不多就行",
+ "notes": "量未指定"
+ },
+ {
+ "name": "柠檬",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 柠檬 1 片",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-drink-百香果橙子特调-百香果橙子特调",
+ "name": "百香果橙子特调的做法",
+ "description": "# 百香果橙子特调的做法\n\n茉莉绿茶版本\n\n\n\n苏打气泡水版本\n\n\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/drink/百香果橙子特调/百香果橙子特调.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/drink/百香果橙子特调/soda-version.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/drink/百香果橙子特调/soda-version.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/drink/百香果橙子特调/tea-version.jpg"
+ ],
+ "category": "饮品",
+ "difficulty": 3,
+ "tags": [
+ "饮品"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "原料:",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 原料:",
+ "notes": "量未指定"
+ },
+ {
+ "name": "百香果",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 百香果",
+ "notes": "量未指定"
+ },
+ {
+ "name": "橙子",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 橙子",
+ "notes": "量未指定"
+ },
+ {
+ "name": "茉莉绿茶茶叶/苏打气泡水二选一",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 茉莉绿茶茶叶/苏打气泡水二选一",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白砂糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白砂糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冰块",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冰块",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蜂蜜(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蜂蜜(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "薄荷叶或其他绿叶(可选,装饰使用)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 薄荷叶或其他绿叶(可选,装饰使用)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "工具",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 工具",
+ "notes": "量未指定"
+ },
+ {
+ "name": "手动压汁器",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 手动压汁器",
+ "notes": "量未指定"
+ },
+ {
+ "name": "基于茉莉绿茶版本准备,一杯分量,约",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 基于茉莉绿茶版本准备,一杯分量,约 380 毫升",
+ "notes": "量未指定"
+ },
+ {
+ "name": "橙子",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 橙子 1 个(约 200 克,拳头大小)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "茉莉绿茶茶叶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 茉莉绿茶茶叶 3~6 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "开水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 开水 150 毫升",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冰块",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冰块 160 克以上",
+ "notes": "量未指定"
+ },
+ {
+ "name": "腌制百香果部分(因为量小不好配置,这里是两次的分量)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 腌制百香果部分(因为量小不好配置,这里是两次的分量)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "百香果",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 百香果 3 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白砂糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白砂糖 30 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蜂蜜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蜂蜜 10 克(如果没有可以用 5 克白砂糖代替)",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "百香果腌制(因为量小不好配置,这里是两次的分量)"
+ },
+ {
+ "step": 2,
+ "description": "茉莉绿茶调配(推荐比例=>茶 : 水 : 冰 = 1~2 : 50 : 30)"
+ },
+ {
+ "step": 3,
+ "description": "橙子的处理(可在泡茶期间处理)"
+ },
+ {
+ "step": 4,
+ "description": "正式调配"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-drink-砂糖椰子冰沙-砂糖椰子冰沙",
+ "name": "砂糖椰子冰沙的做法",
+ "description": "# 砂糖椰子冰沙的做法\n\n\n\n砂糖椰子冰沙是一种制作极其快速方便的饮料,若原料选择得当则口感丰富。然而制作时动静较大,适合白天在家制作以作为下午茶。\n\n预估烹饪难度:★",
+ "source_path": "dishes/drink/砂糖椰子冰沙/砂糖椰子冰沙.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/drink/砂糖椰子冰沙/砂糖椰子冰沙-1.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/drink/砂糖椰子冰沙/砂糖椰子冰沙-1.jpg"
+ ],
+ "category": "饮品",
+ "difficulty": 1,
+ "tags": [
+ "饮品"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "瓶装椰汁(瓶口较大为佳)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 瓶装椰汁(瓶口较大为佳)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "咖啡调糖(黄色粗粒)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 咖啡调糖(黄色粗粒)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "瓶装椰子汁",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 瓶装椰子汁 500ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "咖啡调糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 咖啡调糖 10g(两包太古咖啡调糖)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "坚果碎(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 坚果碎(可选)",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "500ml 瓶装椰汁倒掉 200ml,立刻拧紧瓶盖。"
+ },
+ {
+ "step": 2,
+ "description": "将这瓶椰汁放入冰箱冷冻区并冷冻 10 小时以上。"
+ },
+ {
+ "step": 3,
+ "description": "将这瓶椰汁取出,若确认瓶中椰汁已彻底冻结,则在墙角、椅背、桌角等坚硬表面上用力抽打。(请务必确认表面不会因此受到损伤)"
+ },
+ {
+ "step": 4,
+ "description": "当抽打到冻结椰汁变成冰沙状态,打开瓶盖倒出冰沙。"
+ },
+ {
+ "step": 5,
+ "description": "在冰沙表面均匀撒上咖啡调糖或坚果碎。"
+ },
+ {
+ "step": 6,
+ "description": "完成"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-drink-耙耙柑茶-耙耙柑茶",
+ "name": "耙耙柑茶的做法",
+ "description": "# 耙耙柑茶的做法\n\n\n\n预估烹饪难度:★★",
+ "source_path": "dishes/drink/耙耙柑茶/耙耙柑茶.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/drink/耙耙柑茶/citrus-tea.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/drink/耙耙柑茶/citrus-tea.jpg"
+ ],
+ "category": "饮品",
+ "difficulty": 2,
+ "tags": [
+ "饮品"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "原料:",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 原料:",
+ "notes": "量未指定"
+ },
+ {
+ "name": "耙耙柑(替换物请看附加内容)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 耙耙柑(替换物请看附加内容)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "茉莉绿茶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 茉莉绿茶",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冰块",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冰块",
+ "notes": "量未指定"
+ },
+ {
+ "name": "[蔗糖糖浆](../../condiment/蔗糖糖浆/蔗糖糖浆.md)(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- [蔗糖糖浆](../../condiment/蔗糖糖浆/蔗糖糖浆.md)(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "工具",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 工具",
+ "notes": "量未指定"
+ },
+ {
+ "name": "搅拌机",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 搅拌机",
+ "notes": "量未指定"
+ },
+ {
+ "name": "耙耙柑",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 耙耙柑 1~2 个(200 克以上)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "茉莉绿茶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 茉莉绿茶 2~4 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冰块",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冰块 60 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "1 :",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 1 : 1 蔗糖糖浆 10 克(可选)",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "茉莉绿茶调配(推荐比例=>茶 : 水 : 冰 = 1~2 : 50 : 30)"
+ },
+ {
+ "step": 2,
+ "description": "正式调配"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-drink-菠萝咖啡特调-菠萝咖啡特调",
+ "name": "菠萝咖啡特调的做法",
+ "description": "# 菠萝咖啡特调的做法\n\n\n\n菠萝咖啡特调是非常适合家庭出品的饮料,酸甜可口。\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/drink/菠萝咖啡特调/菠萝咖啡特调.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/drink/菠萝咖啡特调/菠萝咖啡特调.png",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/drink/菠萝咖啡特调/菠萝咖啡特调.png"
+ ],
+ "category": "饮品",
+ "difficulty": 3,
+ "tags": [
+ "饮品"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "咖啡液(推荐浓缩或者冷萃)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 咖啡液(推荐浓缩或者冷萃)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "菠萝汁(鲜榨或者 nfc)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 菠萝汁(鲜榨或者 nfc)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冰块",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冰块",
+ "notes": "量未指定"
+ },
+ {
+ "name": "苏打水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 苏打水",
+ "notes": "量未指定"
+ },
+ {
+ "name": "奶油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 奶油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "牛奶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 牛奶",
+ "notes": "量未指定"
+ },
+ {
+ "name": "糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "海盐(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 海盐(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "朗姆酒 (可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 朗姆酒 (可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "咖啡液",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 咖啡液 30ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "菠萝汁",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 菠萝汁 60ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冰块",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冰块 50g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "苏打水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 苏打水 30ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "奶油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 奶油 30ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "牛奶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 牛奶 10ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 糖 8g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "海盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 海盐 0.5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "朗姆酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 朗姆酒 5ml",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "杯子里依次加入冰块,咖啡液,菠萝汁和苏打水"
+ },
+ {
+ "step": 2,
+ "description": "奶油加糖打发至湿性发泡,加入朗姆酒和牛奶均匀只有流动性"
+ },
+ {
+ "step": 3,
+ "description": "在第一部混合液上方倒入奶油"
+ },
+ {
+ "step": 4,
+ "description": "奶油顶面撒上海盐"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-drink-酒酿醪糟-酒酿醪糟",
+ "name": "酒酿醪糟的做法",
+ "description": "# 酒酿(醪糟)的做法\n\n\n\n酒酿,也叫醪糟,是一道传统中式发酵甜品。成品清甜微醺,含少量酒精,具有健脾开胃、促进消化的功效。虽然制作需要一定发酵技巧,但过程简单有趣,是发酵入门好选择。预计制作时间为 2 天(不含等待时间操作仅需 1 小时左右)。\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/drink/酒酿醪糟/酒酿醪糟.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/drink/酒酿醪糟/酒酿米糕.jpeg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/drink/酒酿醪糟/酒酿米糕.jpeg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/drink/酒酿醪糟/酒酿醪糟.jpeg"
+ ],
+ "category": "饮品",
+ "difficulty": 4,
+ "tags": [
+ "饮品"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "糯米",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 糯米 800g(推荐使用圆糯米)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "安琪甜酒曲一包 (8g)(虽然按比例为每",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 安琪甜酒曲一包 (8g)(虽然按比例为每 1000g 糯米用 3g,但多放酒曲能提高成功概率)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "清水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 清水 720g + 600g(720 克用于蒸饭,后 500g 用于发酵)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒸锅(电饭煲即可)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒸锅(电饭煲即可)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "温度计(可选但推荐)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 温度计(可选但推荐)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "干净密封玻璃或陶瓷容器",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 干净密封玻璃或陶瓷容器 1 个",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "将 800g 糯米淘洗干净放入电饭煲,加入 720g 清水选择蒸饭模式"
+ },
+ {
+ "step": 2,
+ "description": "蒸熟后将米饭倒出摊凉,使用干净的工具将其摊至 30°C 左右(用温度计测量为宜,体感温热但不烫手)"
+ },
+ {
+ "step": 3,
+ "description": "将 8g 安琪甜酒曲用 20ml 温水(约 30°C)化开,均匀撒在糯米饭中,同时翻拌均匀"
+ },
+ {
+ "step": 4,
+ "description": "向糯米饭中加入 600g 清水帮助酒曲翻拌均匀。静置 2-3 分钟后发现糯米饭吸饱水分。这次加水可以让酒酿首次发酵便汤汁丰富"
+ },
+ {
+ "step": 5,
+ "description": "用擀面杖在糯米饭中央挖一个小洞(便于出酒)"
+ },
+ {
+ "step": 6,
+ "description": "将混合好的米饭装入干净容器中,轻轻压平表面,盖上盖子或保鲜膜密封好"
+ },
+ {
+ "step": 7,
+ "description": "放置于 28 ~ 32°C 环境下发酵 24 ~ 48 小时。发酵期间不可剧烈摇晃或移动"
+ },
+ {
+ "step": 8,
+ "description": "发酵成功标准为:中间凹槽有透明酒液渗出,整体略带酒香,无异味、不酸败"
+ },
+ {
+ "step": 9,
+ "description": "发酵结束后可立即冷藏保存(过程中可以加入桂花),每次食用用干净工具取出,可冷藏保存 7 ~ 10 天"
+ },
+ {
+ "step": 10,
+ "description": "可以继续二次发酵,加入适量清水增加酒酿产量(800g 水以内即可)"
+ },
+ {
+ "step": 11,
+ "description": "酒酿会一直持续发酵。如果想停止发酵,可以上锅蒸 10 分钟"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-drink-酸梅汤-酸梅汤",
+ "name": "酸梅汤的做法",
+ "description": "# 酸梅汤的做法\n\n\n\n视频演示: [链接](https://www.bilibili.com/video/BV1164y1F7hv/)\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/drink/酸梅汤/酸梅汤.md",
+ "image_path": null,
+ "images": [],
+ "category": "饮品",
+ "difficulty": 4,
+ "tags": [
+ "饮品"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 水",
+ "notes": "量未指定"
+ },
+ {
+ "name": "乌枣",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 乌枣",
+ "notes": "量未指定"
+ },
+ {
+ "name": "乌梅",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 乌梅",
+ "notes": "量未指定"
+ },
+ {
+ "name": "山楂片(生)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 山楂片(生)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黄冰糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黄冰糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "甘草",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 甘草",
+ "notes": "量未指定"
+ },
+ {
+ "name": "陈皮",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 陈皮",
+ "notes": "量未指定"
+ },
+ {
+ "name": "红豆蔻",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 红豆蔻",
+ "notes": "量未指定"
+ },
+ {
+ "name": "干桂花",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 干桂花",
+ "notes": "量未指定"
+ },
+ {
+ "name": "两升水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 两升水",
+ "notes": "量未指定"
+ },
+ {
+ "name": "乌枣",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 乌枣 25 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "乌梅",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 乌梅 25g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黄冰糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黄冰糖 100 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "山楂片",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 山楂片 30 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "甘草",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 甘草 2 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "陈皮",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 陈皮 4 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "红豆蔻",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 红豆蔻 1 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "干桂花",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 干桂花 3 克",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "冲洗材料(干桂花和冰糖除外), 1.5 升水常温浸泡两小时以上(干桂花和冰糖除外)"
+ },
+ {
+ "step": 2,
+ "description": "开中大火煮沸,盖盖,转小火煮 40 分钟,为头煎"
+ },
+ {
+ "step": 3,
+ "description": "将冰糖放入盆内,再将沥好用材的头汤趁热倒入,搅拌至冰糖融化。"
+ },
+ {
+ "step": 4,
+ "description": "药材重新装回锅内再 600 毫升的水,开大火煮沸,盖盖,转中火,再煮 20 分钟为二煎"
+ },
+ {
+ "step": 5,
+ "description": "最后将二煎和冰糖水趁热混合为成品。在成品 60-70℃加入干桂花(不要超过 80℃)加盖晾凉再放入冰箱冷藏 3 小时以上。"
+ },
+ {
+ "step": 6,
+ "description": "饮用时记得将干桂花沥出。如饮茶般细啜,冰凉振齿,酸醒人、甜适度,滋味丰满而悠长"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-drink-金汤力-金汤力",
+ "name": "金汤力的做法",
+ "description": "# 金汤力的做法\n\n**饮酒有害健康,未成年人禁止饮酒**\n\n预估烹饪难度:★★",
+ "source_path": "dishes/drink/金汤力/金汤力.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/drink/金汤力/gin-tonic.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/drink/金汤力/gin-tonic.jpg"
+ ],
+ "category": "饮品",
+ "difficulty": 2,
+ "tags": [
+ "饮品"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "金酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 金酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "汤力水气泡水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 汤力水气泡水",
+ "notes": "量未指定"
+ },
+ {
+ "name": "柠檬",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 柠檬",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冰块",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冰块",
+ "notes": "量未指定"
+ },
+ {
+ "name": "新鲜绿叶(可选,装饰用)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 新鲜绿叶(可选,装饰用)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "手动压汁器",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 手动压汁器",
+ "notes": "量未指定"
+ },
+ {
+ "name": "金酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 金酒 30~40 毫升",
+ "notes": "量未指定"
+ },
+ {
+ "name": "汤力水气泡水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 汤力水气泡水 1 罐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "柠檬",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 柠檬 1 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冰块",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冰块 100 克",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-drink-金菲士-金菲士",
+ "name": "金菲士的做法",
+ "description": "# 金菲士的做法\n\n**饮酒有害健康,未成年人禁止饮酒**\n\n预估烹饪难度:★★",
+ "source_path": "dishes/drink/金菲士/金菲士.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/drink/金菲士/gin-fizz.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/drink/金菲士/gin-fizz.jpg"
+ ],
+ "category": "饮品",
+ "difficulty": 2,
+ "tags": [
+ "饮品"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "金酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 金酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "苏打气泡水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 苏打气泡水",
+ "notes": "量未指定"
+ },
+ {
+ "name": "柠檬",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 柠檬",
+ "notes": "量未指定"
+ },
+ {
+ "name": "[蔗糖糖浆](../../condiment/蔗糖糖浆/蔗糖糖浆.md)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- [蔗糖糖浆](../../condiment/蔗糖糖浆/蔗糖糖浆.md)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冰块",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冰块",
+ "notes": "量未指定"
+ },
+ {
+ "name": "新鲜绿叶(可选,装饰用)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 新鲜绿叶(可选,装饰用)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "手动压汁器",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 手动压汁器",
+ "notes": "量未指定"
+ },
+ {
+ "name": "雪克瓶(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 雪克瓶(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "金酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 金酒 30~40 毫升",
+ "notes": "量未指定"
+ },
+ {
+ "name": "苏打气泡水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 苏打气泡水 1 罐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "柠檬",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 柠檬 1 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "1 :",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 1 : 1 蔗糖糖浆 30~40 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冰块",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冰块 100 克",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-乡村啤酒鸭",
+ "name": "乡村啤酒鸭的做法",
+ "description": "# 乡村啤酒鸭的做法\n\n\n\n将鸭肉与啤酒一同炖煮成菜,使滋补的鸭肉味道更加浓厚,鸭肉不仅入口鲜香,还带有一股啤酒清香。一般初学者只需要 1 小时即可完成。\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/meat_dish/乡村啤酒鸭.md",
+ "image_path": null,
+ "images": [],
+ "category": "荤菜",
+ "difficulty": 4,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "鸭肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸭肉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "啤酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 啤酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "32 厘米以上的炒锅一个(锅太小难炒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 32 厘米以上的炒锅一个(锅太小难炒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "青椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 青椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "红椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 红椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生姜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小米椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小米椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜苗",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜苗",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "草果",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 草果",
+ "notes": "量未指定"
+ },
+ {
+ "name": "桂皮",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 桂皮",
+ "notes": "量未指定"
+ },
+ {
+ "name": "八角",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 八角",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香叶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香叶",
+ "notes": "量未指定"
+ },
+ {
+ "name": "干辣椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 干辣椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸭肉(半只,1 kg,让市场老板剁成小块)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸭肉(半只,1 kg,让市场老板剁成小块)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "啤酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 啤酒 1000 ml (可以买 500 ml 的罐装啤酒两瓶)",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "鸭肉清洗一遍放进锅中"
+ },
+ {
+ "step": 2,
+ "description": "加清水淹没鸭肉"
+ },
+ {
+ "step": 3,
+ "description": "加 20 ml 料酒"
+ },
+ {
+ "step": 4,
+ "description": "加备用的 1 根 大葱"
+ },
+ {
+ "step": 5,
+ "description": "加生姜 ,拍散的 2 厘米"
+ },
+ {
+ "step": 6,
+ "description": "开火烧滚"
+ },
+ {
+ "step": 7,
+ "description": "捞出浮沫"
+ },
+ {
+ "step": 8,
+ "description": "鸭肉捞出,清水洗干净备用"
+ },
+ {
+ "step": 9,
+ "description": "锅清洗感觉烧热,加 60ml 的花生油"
+ },
+ {
+ "step": 10,
+ "description": "油温到 60 度的时候,加一把花椒( 30 颗)"
+ },
+ {
+ "step": 11,
+ "description": "加鸭肉翻炒 4 分钟"
+ },
+ {
+ "step": 12,
+ "description": "加入 1000 ml 的啤酒"
+ },
+ {
+ "step": 13,
+ "description": "烧鸭肉 30 分钟"
+ },
+ {
+ "step": 14,
+ "description": "出锅"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-冷吃兔",
+ "name": "冷吃兔的做法",
+ "description": "# 冷吃兔的做法\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/meat_dish/冷吃兔.md",
+ "image_path": null,
+ "images": [],
+ "category": "荤菜",
+ "difficulty": 4,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "兔肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 兔肉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "味精",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 味精",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蚝油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蚝油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小葱/大葱/洋葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小葱/大葱/洋葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "干辣椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 干辣椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "青花椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 青花椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "八角",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 八角",
+ "notes": "量未指定"
+ },
+ {
+ "name": "桂皮",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 桂皮",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香叶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香叶",
+ "notes": "量未指定"
+ },
+ {
+ "name": "山奈",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 山奈",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白蔻",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白蔻",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小茴香",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小茴香",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白芝麻",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白芝麻",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐量 = 兔肉斤数",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐量 = 兔肉斤数 * 2 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "味精量 = 兔肉斤数",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 味精量 = 兔肉斤数 * 1 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蚝油量 = 兔肉斤数",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蚝油量 = 兔肉斤数 * 5 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒量 = 兔肉斤数",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒量 = 兔肉斤数 * 10 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "油量 = 兔肉斤数",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 油量 = 兔肉斤数 * 0.9 ~ 1 升",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜量 = 兔肉斤数",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜量 = 兔肉斤数 * 二分之一头蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜量 = 蒜量",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜量 = 蒜量",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小葱/大葱/洋葱总量 = 兔肉斤数",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小葱/大葱/洋葱总量 = 兔肉斤数 * 15 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "干辣椒量 = 辣椒段的总体积等于兔肉的总体积",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 干辣椒量 = 辣椒段的总体积等于兔肉的总体积",
+ "notes": "量未指定"
+ },
+ {
+ "name": "青花椒量 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 青花椒量 = 3 斤兔肉对应吃饭用的小碗,一整碗花椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "八角量 = 兔肉斤数",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 八角量 = 兔肉斤数 * 1 粒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "桂皮量 = 兔肉斤数",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 桂皮量 = 兔肉斤数 * 大拇指长短的一块",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香叶量 = 兔肉斤数",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香叶量 = 兔肉斤数 * 5 片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "山奈量 = 兔肉斤数",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 山奈量 = 兔肉斤数 * 黄豆大小的一块",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白蔻量 = 兔肉斤数",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白蔻量 = 兔肉斤数 * 2 颗",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小茴香量 = 兔肉斤数",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小茴香量 = 兔肉斤数 * 15 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白芝麻量 = 兔肉斤数",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白芝麻量 = 兔肉斤数 * 25 克",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-可乐鸡翅",
+ "name": "可乐鸡翅的做法",
+ "description": "# 可乐鸡翅的做法\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/meat_dish/可乐鸡翅.md",
+ "image_path": null,
+ "images": [],
+ "category": "荤菜",
+ "difficulty": 3,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "鸡翅中",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡翅中",
+ "notes": "量未指定"
+ },
+ {
+ "name": "可乐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 可乐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生姜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒或啤酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒或啤酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡翅",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡翅 10 ~ 12 只",
+ "notes": "量未指定"
+ },
+ {
+ "name": "可乐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 可乐 500ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖 10 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 15 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽 3 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 2 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生姜 2 片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒 20 毫升",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小葱挽成结",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小葱挽成结",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "鸡翅入锅,倒入冷水淹没。放生姜 1 片和料酒 10 ~ 20 毫升。大火煮开( 大约 2 分钟 )后,撇去浮沫,沥出水分"
+ },
+ {
+ "step": 2,
+ "description": "捞出鸡翅,可用刀将两边各划上两口改刀。生抽约 10 克腌制鸡翅 10 分钟(生抽能完全包裹鸡翅表面入味就行)"
+ },
+ {
+ "step": 3,
+ "description": "锅重新小火起油,先将剩余姜片爆香,然后下入腌好的鸡翅。将鸡翅煎至金黄翻面(直到两面金黄),用炒菜勺子翻动一下鸡翅,与姜片一起翻炒 4~5 下(目的是防止鸡翅和姜片粘黏)。"
+ },
+ {
+ "step": 4,
+ "description": "鸡翅金黄,倒入可乐没过鸡翅,开大火将锅中可乐煮沸,然后撇去漂浮的黑色浮沫(包含血水)。此时加入葱结。"
+ },
+ {
+ "step": 5,
+ "description": "调味:加入食用盐 2 克,白糖 10 克,生抽 3 克调味(可以适当用老抽调底色,3 克)。"
+ },
+ {
+ "step": 6,
+ "description": "等到葱结变黄,和姜片一起捞出,转中火继续慢煮可乐鸡翅。"
+ },
+ {
+ "step": 7,
+ "description": "等到可乐呈现挂丝状态,关小火让汁牢牢挂在鸡翅上。出锅,装盘。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-咕噜肉",
+ "name": "咕噜肉的做法",
+ "description": "# 咕噜肉的做法\n\n咕噜肉是非常下饭的菜肴,只需一道就可以吃得津津有味,大人小孩都爱吃。而这次做的是简易版菠萝咕噜肉,利用简单的材料就可以在家做出特有风味的咕噜肉 。\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/meat_dish/咕噜肉.md",
+ "image_path": null,
+ "images": [],
+ "category": "荤菜",
+ "difficulty": 4,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "梅头猪肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 梅头猪肉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "青椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 青椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "罐头菠萝片",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 罐头菠萝片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "茄汁",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 茄汁",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白醋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白醋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜蓉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜蓉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白砂糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白砂糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "1 汤匙 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 1 汤匙 = 15ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "1 茶匙 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 1 茶匙 = 5ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "梅头猪肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 梅头猪肉 100g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "青椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 青椒 25g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "罐头菠萝片",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 罐头菠萝片 75g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 1/4 茶匙",
+ "notes": "量未指定"
+ },
+ {
+ "name": "茄汁",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 茄汁 4 汤匙",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白醋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白醋 2 茶匙",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜蓉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜蓉 1 汤匙",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 1/2 茶匙",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生粉 2 1/2 茶匙",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白砂糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白砂糖 2 汤匙",
+ "notes": "量未指定"
+ },
+ {
+ "name": "水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 水 200 毫升",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "将梅头猪肉(100 克)洗净,然后用厨房纸抹干水份,将猪肉切成比想要的成品小一圈的大小。"
+ },
+ {
+ "step": 2,
+ "description": "用盐(1/2 茶匙)腌制梅头猪肉 20 分钟。"
+ },
+ {
+ "step": 3,
+ "description": "将青椒(25 克)切碎。"
+ },
+ {
+ "step": 4,
+ "description": "将菠萝片(75 克)切件。"
+ },
+ {
+ "step": 5,
+ "description": "在碗中加入茄汁(4 汤匙)﹑白醋(2 茶匙)﹑蒜蓉(1 汤匙)﹑生抽(½ 茶匙)﹑生粉(2½ 茶匙)﹑白砂糖(2 汤匙)﹑盐(¼ 茶匙)和水(200 毫升),拌匀成酱汁。"
+ },
+ {
+ "step": 6,
+ "description": "将梅头猪肉粒沾上生粉(6 汤匙)。"
+ },
+ {
+ "step": 7,
+ "description": "加入油(500 毫升)中火加热。"
+ },
+ {
+ "step": 8,
+ "description": "将梅头猪肉粒放至锅里中火炸 5 分钟,然后盛起。"
+ },
+ {
+ "step": 9,
+ "description": "加入梅头猪肉粒,再大火翻炸 1 分钟。"
+ },
+ {
+ "step": 10,
+ "description": "加入油(1 茶匙)和酱汁,中火加热 3 分钟。"
+ },
+ {
+ "step": 11,
+ "description": "加入青椒和菠萝,大火加热 2 分钟。"
+ },
+ {
+ "step": 12,
+ "description": "将已炸好的梅头猪肉粒与酱汁拌匀即可。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-商芝肉",
+ "name": "商芝肉的做法",
+ "description": "# 商芝肉的做法\n\n此菜色泽红润,质地软嫩,肥而不腻,有浓郁的商芝香味,是陕西省商县特有的风味菜。因商芝属于陕西特产,此菜原料获取难度较大,不易制作。\n\n预估烹饪难度:★★★★★",
+ "source_path": "dishes/meat_dish/商芝肉.md",
+ "image_path": null,
+ "images": [],
+ "category": "荤菜",
+ "difficulty": 5,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "带皮猪五花肉(去骨)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 带皮猪五花肉(去骨)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "商芝(又名紫萁,属蕨类,嫩叶可食)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 商芝(又名紫萁,属蕨类,嫩叶可食)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "八角",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 八角",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蜂蜜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蜂蜜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "醋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 醋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "味精",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 味精",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酱油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酱油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "摊鸡蛋皮",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 摊鸡蛋皮",
+ "notes": "量未指定"
+ },
+ {
+ "name": "精盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 精盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡汤",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡汤",
+ "notes": "量未指定"
+ },
+ {
+ "name": "芝麻油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 芝麻油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "熟猪油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 熟猪油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "带皮猪五花肉:",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 带皮猪五花肉: 500 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "商芝:",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 商芝: 50 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱:",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱: 10 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜:",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜: 2 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "八角:",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 八角: 3 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蜂蜜:",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蜂蜜: 15 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "醋:",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 醋: 5 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒:",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒: 15 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "味精:",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 味精: 1.5 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酱油:",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酱油: 10 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "摊鸡蛋皮:一张约",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 摊鸡蛋皮:一张约 15 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "精盐:",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 精盐: 1 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡汤:",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡汤: 200 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "芝麻油:",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 芝麻油: 10 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "熟猪油:",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 熟猪油: 2000 克(消耗约 60 克)",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "将肉刮洗干净,入煮锅煮至六成熟(变色为白),捞出趁热用蜂蜜、醋涂抹肉皮。"
+ },
+ {
+ "step": 2,
+ "description": "炒锅内放入熟猪油,用旺火烧至八成熟(约 200 度,油表有大量青烟,油状平静),将肉块皮朝下投入,炸至呈金红色时,捞入凉肉煮锅(之前煮完的煮锅)中泡软,放在案板上,切成三寸(10 cm)长、两分(0.6 cm)厚的片,仍然皮朝下,整齐装入蒸碗内。"
+ },
+ {
+ "step": 3,
+ "description": "将 5 克大葱切成 2.4 cm 长的段,5 克切成 2.4 cm 长的斜形片。姜去皮洗净,1.5 克切成片,5 克切成末,摊的鸡蛋皮切成 2.4 cm 长的等腰三角形片。"
+ },
+ {
+ "step": 4,
+ "description": "商芝入沸水锅中煮软捞出,去除老茎、杂质,淘洗干净,切成 3 cm 长的段,放入碗中,加酱油(5 克)、精盐(1 克)、熟猪油(10 克)拌匀,盖在肉片上,另将鸡汤(100 克)放入一小碗中,加酱油(5 克)、精盐(0.5 克)、料酒(15 克)搅匀,浇入蒸碗,再放入姜片、葱段、八角上笼用旺火蒸约半小时后,转用小火继续蒸约一小时三十分钟,熟烂后取出,拣去姜、葱、八角,倒、过滤原汁,将肉扣入汤盘。"
+ },
+ {
+ "step": 5,
+ "description": "炒锅内,放入鸡汤(100 克),加入原汁,用旺火烧沸,下入姜末、葱片、味精后搅匀,投入摊鸡蛋皮,淋芝麻油,浇入汤盘即成。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-孜然牛肉",
+ "name": "孜然牛肉的做法",
+ "description": "# 孜然牛肉的做法\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/meat_dish/孜然牛肉.md",
+ "image_path": null,
+ "images": [],
+ "category": "荤菜",
+ "difficulty": 3,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "牛柳或牛肩肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 牛柳或牛肩肉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "青椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 青椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "孜然(颗粒>粉)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 孜然(颗粒>粉)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小米椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小米椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽酱油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽酱油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "淀粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 淀粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "捣药罐(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 捣药罐(可选)",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "首先将小米椒切碎,和孜然粒一起放入捣药罐捣碎成颗粒,这样更入味。如果时间紧张可跳过捣碎步骤"
+ },
+ {
+ "step": 2,
+ "description": "青椒切头去籽(喜欢辣的可不去),切成丝。葱切段。"
+ },
+ {
+ "step": 3,
+ "description": "牛肉提前解冻,过一边水洗干净,晾干或用厨用纸吸干,将牛肉顺着纹理切成片"
+ },
+ {
+ "step": 4,
+ "description": "然后进行腌肉,加入生抽,淀粉,油,均匀搅拌,静止 30 分钟。腌肉方法也可参考[学习腌](../../tips/learn/学习腌.md)"
+ },
+ {
+ "step": 5,
+ "description": "热锅下油,放入葱,爆出香味后放入腌好的牛肉煸炒"
+ },
+ {
+ "step": 6,
+ "description": "牛肉变色后均匀放入孜然辣椒颗粒并炒熟"
+ },
+ {
+ "step": 7,
+ "description": "然后下入青椒丝,断生后放盐"
+ },
+ {
+ "step": 8,
+ "description": "大🔥炒 1 分钟后关火再翻炒 30 秒保证受热均匀即可出锅"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-小炒肉",
+ "name": "小炒肉的做法",
+ "description": "# 小炒肉的做法\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/meat_dish/小炒肉.md",
+ "image_path": null,
+ "images": [],
+ "category": "荤菜",
+ "difficulty": 3,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "五花肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 五花肉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "朝天椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 朝天椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小米椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小米椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "豆豉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 豆豉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "豆瓣酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 豆瓣酱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "淀粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 淀粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "五花肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 五花肉 500g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "朝天椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 朝天椒 4 条",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小米椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小米椒 4 颗",
+ "notes": "量未指定"
+ },
+ {
+ "name": "豆豉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 豆豉 10g,根据个人口味加减 ±5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "豆瓣酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 豆瓣酱 10g,根据个人口味加减 ±5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽 10ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "淀粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 淀粉 10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 1-2g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱 0.5-1 根",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜 2 瓣",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 15ml",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "五花肉切片"
+ },
+ {
+ "step": 2,
+ "description": "把肉放入器皿内,加入淀粉、老抽、盐搅拌腌制半小时"
+ },
+ {
+ "step": 3,
+ "description": "葱切段"
+ },
+ {
+ "step": 4,
+ "description": "小米椒、朝天椒斜刀切好"
+ },
+ {
+ "step": 5,
+ "description": "热锅、倒油"
+ },
+ {
+ "step": 6,
+ "description": "油热后加入五花肉煸炒。炒至变色后盛出来"
+ },
+ {
+ "step": 7,
+ "description": "向锅中加蒜,煸出香味,加入豆豉,翻炒均匀"
+ },
+ {
+ "step": 8,
+ "description": "加入豆瓣酱翻炒均匀"
+ },
+ {
+ "step": 9,
+ "description": "加入炒好的五花肉继续的翻炒均匀"
+ },
+ {
+ "step": 10,
+ "description": "加入小米椒、朝天椒、葱段翻炒 40 秒"
+ },
+ {
+ "step": 11,
+ "description": "出锅。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-小米辣炒肉",
+ "name": "小米辣炒肉的做法",
+ "description": "# 小米辣炒肉的做法\n\n⚠️注意:不建议清淡饮食的尝试。\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/meat_dish/小米辣炒肉.md",
+ "image_path": null,
+ "images": [],
+ "category": "荤菜",
+ "difficulty": 3,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "小米辣",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小米辣",
+ "notes": "量未指定"
+ },
+ {
+ "name": "花生油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 花生油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "五花肉/瘦肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 五花肉/瘦肉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蚝油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蚝油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "豆瓣酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 豆瓣酱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡精",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡精",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小米椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小米椒 20 个,根据个人口味加减",
+ "notes": "量未指定"
+ },
+ {
+ "name": "花生油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 花生油 20ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "五花肉/瘦肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 五花肉/瘦肉 200g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 1-2g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 10ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蚝油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蚝油 10ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜蒜 50g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "豆瓣酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 豆瓣酱 10g,根据个人口味加减",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡精",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡精 10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖 5g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "将小米辣洗净,斜刀切大一点"
+ },
+ {
+ "step": 2,
+ "description": "肉的话,想切丝切丝,想切片切片,倒入调料(生抽、蚝油、盐)腌制 5 分钟"
+ },
+ {
+ "step": 3,
+ "description": "热锅倒油,先把肉炒好盛起"
+ },
+ {
+ "step": 4,
+ "description": "姜蒜爆香,倒入豆瓣酱翻炒,到入切好的小米辣,再倒入瘦肉,翻炒一下,放点生抽、鸡精、盐、糖翻炒"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-小酥肉",
+ "name": "小酥肉的做法",
+ "description": "# 小酥肉的做法\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/meat_dish/小酥肉.md",
+ "image_path": null,
+ "images": [],
+ "category": "荤菜",
+ "difficulty": 3,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "去皮猪肉(根据喜好选择肥瘦)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 去皮猪肉(根据喜好选择肥瘦)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "植物油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 植物油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老姜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "十三香",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 十三香",
+ "notes": "量未指定"
+ },
+ {
+ "name": "胡椒粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 胡椒粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "味精",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 味精",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡精",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡精",
+ "notes": "量未指定"
+ },
+ {
+ "name": "花椒碎",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 花椒碎",
+ "notes": "量未指定"
+ },
+ {
+ "name": "花椒粒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 花椒粒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "面粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 面粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "红薯淀粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 红薯淀粉",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "老姜切丝,小葱不用切。"
+ },
+ {
+ "step": 2,
+ "description": "根据计算公式倒入料酒、清水。"
+ },
+ {
+ "step": 3,
+ "description": "用手捏揉 5 分钟,使姜葱的味道充分溶解在水中。"
+ },
+ {
+ "step": 4,
+ "description": "将猪肉去皮洗净"
+ },
+ {
+ "step": 5,
+ "description": "切成长度 8~10 厘米,厚度 1.5 厘米的肉条。"
+ },
+ {
+ "step": 6,
+ "description": "根据上面的计算公式加入盐,十三香、胡椒粉、味精、鸡精、花椒碎、花椒粒、生抽。"
+ },
+ {
+ "step": 7,
+ "description": "倒入前面制作好的葱姜水"
+ },
+ {
+ "step": 8,
+ "description": "抓匀并且充分揉制 10 分钟,直至肉吸收所有水分并且变得粘手。"
+ },
+ {
+ "step": 9,
+ "description": "封上保鲜膜放冷藏室静置 30 分钟。"
+ },
+ {
+ "step": 10,
+ "description": "将面粉、红薯粉倒入腌制好的肉中,加入鸡蛋清。"
+ },
+ {
+ "step": 11,
+ "description": "充分揉制 15 分钟。"
+ },
+ {
+ "step": 12,
+ "description": "锅中倒入植物油,根据锅大小控制油量,油面高度 3 厘米以上。"
+ },
+ {
+ "step": 13,
+ "description": "大火将温加热至 150° 后,转小火保持温度。"
+ },
+ {
+ "step": 14,
+ "description": "将裹好粉的肉条用筷子夹入油锅中,捋成自己喜欢的形状,炸 3~5 分钟定型。目测颜色微黄,用锅铲翻动感受倒略微有些硬了就可以。具体时间受肉块大小、油温、裹粉程度影响。"
+ },
+ {
+ "step": 15,
+ "description": "捞出沥油。"
+ },
+ {
+ "step": 16,
+ "description": "将油温升至 180° 放入初炸好的肉条,炸至金黄色即可捞出。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-尖椒炒牛肉",
+ "name": "尖椒炒牛肉的做法",
+ "description": "# 尖椒炒牛肉的做法\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/meat_dish/尖椒炒牛肉.md",
+ "image_path": null,
+ "images": [],
+ "category": "荤菜",
+ "difficulty": 3,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "牛肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 牛肉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱、姜、蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱、姜、蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "尖椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 尖椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酱油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酱油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 糖",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "蒜剁成蒜泥"
+ },
+ {
+ "step": 2,
+ "description": "葱切段"
+ },
+ {
+ "step": 3,
+ "description": "姜切成姜片"
+ },
+ {
+ "step": 4,
+ "description": "尖椒切成段"
+ },
+ {
+ "step": 5,
+ "description": "牛肉放入碗中"
+ },
+ {
+ "step": 6,
+ "description": "加姜、盐、酱油、糖进行腌制 30-40 分钟"
+ },
+ {
+ "step": 7,
+ "description": "腌制完姜可以去掉"
+ },
+ {
+ "step": 8,
+ "description": "冷油下锅,待油变热至偶有气泡"
+ },
+ {
+ "step": 9,
+ "description": "加入蒜泥"
+ },
+ {
+ "step": 10,
+ "description": "蒜泥变金黄后加入尖椒"
+ },
+ {
+ "step": 11,
+ "description": "待尖椒表皮微皱,加入腌制好的牛肉翻炒"
+ },
+ {
+ "step": 12,
+ "description": "翻炒变熟之前加入葱,继续翻炒"
+ },
+ {
+ "step": 13,
+ "description": "翻炒至牛肉变熟,关火出锅"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-山西过油肉",
+ "name": "山西过油肉的做法",
+ "description": "# 山西过油肉的做法\n\n过油肉是山西传统名菜,有很多年历史,基本家家都会做。\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/meat_dish/山西过油肉.md",
+ "image_path": null,
+ "images": [],
+ "category": "荤菜",
+ "difficulty": 4,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "猪里脊",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 猪里脊",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜苔",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜苔",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酱油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酱油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱姜蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱姜蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "淀粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 淀粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "木耳",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 木耳",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱头",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱头",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "陈醋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 陈醋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "花椒粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 花椒粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡精",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡精",
+ "notes": "量未指定"
+ },
+ {
+ "name": "猪里脊",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 猪里脊 150 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜苔",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜苔 6 根",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 300ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱姜蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱姜蒜 50g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 20ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食盐 10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋 1 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "淀粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 淀粉 10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "木耳",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 木耳 20g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱头",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱头 100g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "其他调料",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 其他调料 20g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "木耳提前泡发好,如果着急可以用热水泡发。"
+ },
+ {
+ "step": 2,
+ "description": "猪里脊切片放入碗中,加 20ml 生抽、料酒、花椒粉,打入一个鸡蛋,拿自己的小手搅拌均匀,加入淀粉(建议红薯淀粉)拌匀,倒入 300ml 食用油封浆,腌制 15 分钟。"
+ },
+ {
+ "step": 3,
+ "description": "蒜苔切段大约 3cm,葱头切菱形块备用。"
+ },
+ {
+ "step": 4,
+ "description": "起锅烧油油要多一点,油温五成热,下入腌制好的肉片,将肉片打散,捞出控油备用。"
+ },
+ {
+ "step": 5,
+ "description": "将锅中多余油倒出,留 10ml 油炒菜,油温七成热"
+ },
+ {
+ "step": 6,
+ "description": "下入葱姜蒜爆香,先下蒜苔炒至断生,再下入木耳葱头,加入生抽,花椒粉,翻炒几下将之前炸好的肉片下入翻炒"
+ },
+ {
+ "step": 7,
+ "description": "加 10g 的盐,起锅前加 10ml 的醋和鸡精,起锅装盘。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-带把肘子",
+ "name": "带把肘子的做法",
+ "description": "# 带把肘子的做法\n\n肘肉酥烂不腻,肘皮胶粘,香醇味美,辅佐以葱段,甜面酱,别有一番风味,因脚爪形似把柄,故得其名,是陕西省大荔县名菜。营养价值丰富,但制作难度较高。\n\n预估烹饪难度:★★★★★",
+ "source_path": "dishes/meat_dish/带把肘子.md",
+ "image_path": null,
+ "images": [],
+ "category": "荤菜",
+ "difficulty": 5,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "带脚、爪猪前肘: 一个(大约二斤五两 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 带脚、爪猪前肘: 一个(大约二斤五两 = 1250 克)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "红豆腐乳:",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 红豆腐乳: 1 块 = 10 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "甜面酱:",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 甜面酱: 150 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "精盐:",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 精盐: 15 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "红酱油:",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 红酱油: 35 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白酱油:",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白酱油: 25 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒:",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒: 25 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜片:",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜片: 50 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜末:",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜末: 10 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "八角:",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 八角: 3 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "桂皮:",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 桂皮: 5 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱:",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱: 200 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "带脚、爪猪前肘",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 带脚、爪猪前肘",
+ "notes": "量未指定"
+ },
+ {
+ "name": "红豆腐乳",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 红豆腐乳",
+ "notes": "量未指定"
+ },
+ {
+ "name": "甜面酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 甜面酱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "精盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 精盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "红酱油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 红酱油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白酱油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白酱油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜片",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜末",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜末",
+ "notes": "量未指定"
+ },
+ {
+ "name": "八角",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 八角",
+ "notes": "量未指定"
+ },
+ {
+ "name": "桂皮",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 桂皮",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "将肘子刮洗干净,肘头朝外、肘把(脚爪)朝里、肘皮朝下放在案板上。"
+ },
+ {
+ "step": 2,
+ "description": "用刀在正中由肘头向肘把沿着腿骨将皮剖开,剔去腿骨两边的肉(三面离肉),底部骨与肉相连,使骨头露出,然后将两节腿骨由中间用刀背(还是用斧头吧)砸断。"
+ },
+ {
+ "step": 3,
+ "description": "肘子放入煮锅煮至七成熟捞出(外观正常,内部淡红色),用干净抹布擦干水,趁热用红酱油涂抹肉皮。"
+ },
+ {
+ "step": 4,
+ "description": "取蒸锅一个,锅底放入八角、桂皮,先将肘把的关节处用手掰断,不伤外皮,再将肘皮朝下装进蒸锅内,装锅时根据肘子体型,将肘把贴住锅边窝着装进锅内,成为圆形。"
+ },
+ {
+ "step": 5,
+ "description": "撒入精盐,用消过毒的干净纱布盖在肉上,再将甜面酱(50 克)、葱(75 克)、红豆腐乳、红酱油、白酱油、姜、蒜等在纱布上抹开,用旺火蒸大约三小时(以蒸烂为准)。"
+ },
+ {
+ "step": 6,
+ "description": "蒸完取出,揭去纱布,扣入盘中,拣去八角,上桌时另带葱段和甜面酱小碟(或将甜面酱抹到肘面上,另带葱段小碟亦可)。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-意式烤鸡",
+ "name": "意式烤鸡的做法",
+ "description": "# 意式烤鸡的做法\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/meat_dish/意式烤鸡.md",
+ "image_path": null,
+ "images": [],
+ "category": "荤菜",
+ "difficulty": 3,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "鸡腿肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡腿肉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黑胡椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黑胡椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "橄榄油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 橄榄油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "柠檬汁",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 柠檬汁",
+ "notes": "量未指定"
+ },
+ {
+ "name": "欧芹",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 欧芹",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡腿肉用量通常来说为",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡腿肉用量通常来说为 1-2 个/人",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "鸡腿肉涂上盐、黑胡椒、橄榄油和蒜末"
+ },
+ {
+ "step": 2,
+ "description": "放入预热至 180 度的烤箱中,烤 30-40 分钟或至熟"
+ },
+ {
+ "step": 3,
+ "description": "欧芹切成碎末备用"
+ },
+ {
+ "step": 4,
+ "description": "柠檬挤出汁备用"
+ },
+ {
+ "step": 5,
+ "description": "烤好的鸡肉取出,淋上柠檬汁"
+ },
+ {
+ "step": 6,
+ "description": "撒上欧芹碎即可"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-杀猪菜",
+ "name": "杀猪菜的做法",
+ "description": "# 杀猪菜的做法\n\n杀猪菜的做法 (荤菜)\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/meat_dish/杀猪菜.md",
+ "image_path": null,
+ "images": [],
+ "category": "荤菜",
+ "difficulty": 4,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "血肠",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 血肠",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酸菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酸菜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "排骨",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 排骨",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "辣椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 辣椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香叶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香叶",
+ "notes": "量未指定"
+ },
+ {
+ "name": "八角",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 八角",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱结",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱结",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "菜籽油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 菜籽油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "血肠",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 血肠 200 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酸菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酸菜 500 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "排骨",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 排骨 400 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒 10 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜瓣",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜瓣 5 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜粉 5 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "干辣椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 干辣椒 5 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香叶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香叶 2 片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "八角",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 八角 1 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱结",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱结 1 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香油 10 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "菜籽油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 菜籽油 10 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 5 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蘸料:辣椒油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蘸料:辣椒油 5 克、生抽 10 克、蒜蓉 5 克、香油 2 克。",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "血肠用牙签多扎一些小孔,然后放水中小火煮十分钟,不要让水烧开,保持 80 度,否则血肠非常容易爆开。"
+ },
+ {
+ "step": 2,
+ "description": "煮好的血肠切块备用。"
+ },
+ {
+ "step": 3,
+ "description": "排骨放料酒焯水,控干水分备用。"
+ },
+ {
+ "step": 4,
+ "description": "锅内放入菜籽油,放蒜瓣,干辣椒,姜粉炒香。"
+ },
+ {
+ "step": 5,
+ "description": "放入排骨翻炒至表面金黄。"
+ },
+ {
+ "step": 6,
+ "description": "酸菜洗净拧干水分,放入锅中,加入香油翻炒,香油可以更好的去除酸味而且让酸菜更香,大火翻炒二分钟。"
+ },
+ {
+ "step": 7,
+ "description": "加入 600 毫升热水。"
+ },
+ {
+ "step": 8,
+ "description": "转入电压力锅,加香叶,八角,葱结,盐。"
+ },
+ {
+ "step": 9,
+ "description": "浓香模式压 40 分钟。"
+ },
+ {
+ "step": 10,
+ "description": "到时间后放气开盖。加入血肠和枸杞,盖上锅盖焖二分钟即可,血肠是熟的,不需再加热。"
+ },
+ {
+ "step": 11,
+ "description": "倒入盆中,按照上表调制蘸料,即可开吃。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-椒盐排条",
+ "name": "椒盐排条的做法",
+ "description": "# 椒盐排条的做法\n\n椒盐排条是道非常经典的本帮菜,咸、香,也容易制作。\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/meat_dish/椒盐排条.md",
+ "image_path": null,
+ "images": [],
+ "category": "荤菜",
+ "difficulty": 4,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "大排",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大排",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "椒盐粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 椒盐粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱姜水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱姜水",
+ "notes": "量未指定"
+ },
+ {
+ "name": "面粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 面粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "淀粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 淀粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "吉士粉(增色增香,没有可以不放)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 吉士粉(增色增香,没有可以不放)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 水",
+ "notes": "量未指定"
+ },
+ {
+ "name": "油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大排",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大排 4 块(大约 360 克)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋 1 个 50 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 1 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "椒盐粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 椒盐粉 10 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱姜水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱姜水 100 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "面粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 面粉 80 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "淀粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 淀粉 80 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "吉士粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 吉士粉 2-3 g(增色增香,没有可以不放)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 水 10 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 油 10 g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "大排洗干净,剔骨,用刀面拍松,切成厚片,再改成粗条。"
+ },
+ {
+ "step": 2,
+ "description": "加入椒盐粉,搅匀,待到出胶质了**分次**加入葱姜水,放入冰箱腌制 20 分钟。"
+ },
+ {
+ "step": 3,
+ "description": "制作炸糊。放入 80 g 面粉,20 g 淀粉(注意是 20 g 淀粉,剩下 60 g 备用),2 - 3 g 吉士粉,盐 1 g。"
+ },
+ {
+ "step": 4,
+ "description": "打入一个鸡蛋,搅拌,再分次加入水 100 g ,再加 10 g 油,反复搅拌。直到炸糊完全调开,略粘稠即可。"
+ },
+ {
+ "step": 5,
+ "description": "取出剩余的 60 g 淀粉,取出排条,裹上一层淀粉,再裹上面糊。"
+ },
+ {
+ "step": 6,
+ "description": "锅中加入油,能没过食材即可,加热到大约 150 ℃ - 160 ℃ 。下入排条炸成浅金黄色后捞出。刚下入排条时可能会有粘连,不要动。待排条定型后可用筷子翻动,即可分开。"
+ },
+ {
+ "step": 7,
+ "description": "待油温再次升高到 150 ℃ - 160 ℃ 时,下入排条复炸至金黄色后捞出。"
+ },
+ {
+ "step": 8,
+ "description": "撒上椒盐粉,搅拌均匀后出锅。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。",
+ "做法参考:[椒盐排条的做法](https://www.bilibili.com/video/BV14s4y1c76H)"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-水煮肉片",
+ "name": "水煮肉片的做法",
+ "description": "# 水煮肉片的做法\n\n水煮肉片麻辣鲜香,适合干饭,但是做法稍微有点麻烦。难度主要在肉滑嫩,初学者一般需要 1 - 2 小时完成。干饭人,一切都值~\n\n预估烹饪难度:★★★★★",
+ "source_path": "dishes/meat_dish/水煮肉片.md",
+ "image_path": null,
+ "images": [],
+ "category": "荤菜",
+ "difficulty": 5,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "猪里脊肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 猪里脊肉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "胡椒粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 胡椒粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽酱油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽酱油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋清",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋清",
+ "notes": "量未指定"
+ },
+ {
+ "name": "土豆淀粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 土豆淀粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "植物油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 植物油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "豆芽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 豆芽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "凤尾",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 凤尾",
+ "notes": "量未指定"
+ },
+ {
+ "name": "芹菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 芹菜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜苗",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜苗",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生姜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "红泡椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 红泡椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "青花椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 青花椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "干辣椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 干辣椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "红油豆瓣",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 红油豆瓣",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡精",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡精",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白砂糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白砂糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "菜籽油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 菜籽油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小葱 2 根",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生姜 10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大蒜 20g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "红泡椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 红泡椒 20g(根据受辣程度选择 0-40 g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜苗",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜苗 2 根",
+ "notes": "量未指定"
+ },
+ {
+ "name": "芹菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 芹菜 3 根",
+ "notes": "量未指定"
+ },
+ {
+ "name": "红油瓣酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 红油瓣酱 5ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡精",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡精 1.5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽酱油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽酱油 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用盐 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "胡椒粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 胡椒粉 2g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒 3g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋清",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋清 1 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "土豆淀粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 土豆淀粉 7g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "植物油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 植物油 280g(根据情况选择,想吃重油就多加 100g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "菜籽油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 菜籽油 200g(根据情况选择,想吃重油就多加 100g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "绿豆芽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 绿豆芽 100g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "凤尾",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 凤尾 1 根",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白砂糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白砂糖 1g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小米辣干辣椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小米辣干辣椒 20g(根据受辣程度选择 0-40g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "青花椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 青花椒 5g(根据情况选择,想吃麻就多 5g)",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "里脊肉改刀成小块,再切成 2 毫米薄片(可根据自己的口感改进),放入碗中,加入清水清洗两遍来去除血水和杂质,捞出挤干水分备用。"
+ },
+ {
+ "step": 2,
+ "description": "碗中加入食用盐 1.5g,胡椒粉 1g,生抽酱油 5g,料酒 3g,然后朝着一个方向搅拌 2 分钟,使其入味。"
+ },
+ {
+ "step": 3,
+ "description": "另外准备一个碗,加入一个鸡蛋清,加入 7g 土豆淀粉,一个方向搅拌均匀,倒入肉中"
+ },
+ {
+ "step": 4,
+ "description": "绿豆芽 100g,凤尾 1 根(改刀成小条),芹菜 3 根切成小段,蒜苗 2 根拍散切成小段。"
+ },
+ {
+ "step": 5,
+ "description": "大蒜 20g 剁碎,生姜小块剁碎,红泡椒 20g 剁碎。"
+ },
+ {
+ "step": 6,
+ "description": "小米辣干辣椒 15g,青花椒 3g,锅内加入油滑锅,油稍许热了将多余的倒出备用留 50g 底油,下入干辣椒、花椒,开小火炒香,切记不要炒糊(颜色要变黑即可),倒出在菜板上剁细。"
+ },
+ {
+ "step": 7,
+ "description": "锅烧热,放入 100g 植物油烧至 6 成热,加入 2g 青花椒、干辣椒爆香,配菜下锅,加入 1g 食用盐,炒至断生,盛入碗中垫底备用。"
+ },
+ {
+ "step": 8,
+ "description": "锅洗干净,加入 150g 植物油烧至 6 成热,加入制作好的姜蒜红泡椒,爆香后加入豆瓣 10g,开小火把豆瓣爆香炒出红油即可。"
+ },
+ {
+ "step": 9,
+ "description": "加入 800 毫升清水(根据实际情况选择),大火烧开,转小火调味,加入食用盐 2.5g,鸡精 1.5g,1g 白砂糖提鲜,1g 胡椒粉,5g 水淀粉(根据实际情况选择)将汤汁收浓稠一点。"
+ },
+ {
+ "step": 10,
+ "description": "汤汁开后,开小火将腌制好的肉片分开依次下锅,然后开中火将肉片烫熟,用锅铲轻轻推动一下避免粘连,待汤汁烧开,肉片熟后捞出放入碗中配菜上,再将原汤倒入(不超过菜品)。"
+ },
+ {
+ "step": 11,
+ "description": "碗中均匀撒上刀口辣椒、蒜蓉和葱花。"
+ },
+ {
+ "step": 12,
+ "description": "锅洗干净,加入 200g 菜籽油,烧至 7 成热,然后一次性均匀泼在碗中肉片上(注意安全),美味完成。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-洋葱炒猪肉",
+ "name": "洋葱炒猪肉的做法",
+ "description": "# 洋葱炒猪肉的做法\n\n咸中带甜,简单上手,一不小心可能让人多吃一碗饭。一般只需 15 分钟即可完成。\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/meat_dish/洋葱炒猪肉.md",
+ "image_path": null,
+ "images": [],
+ "category": "荤菜",
+ "difficulty": 3,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "洋葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 洋葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "猪肉片",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 猪肉片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蕃茄酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蕃茄酱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "麻油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 麻油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "洋葱 一颗 (是主角,喜欢吃洋葱可以多半颗~一颗)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 洋葱 一颗 (是主角,喜欢吃洋葱可以多半颗~一颗)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "猪肉 (250g)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 猪肉 (250g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜头 (3 瓣)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜头 (3 瓣)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油 (15ml)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 (15ml)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黑胡椒 (1.25g)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黑胡椒 (1.25g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酱油 (30ml)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酱油 (30ml)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "糖 (15g)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 糖 (15g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "麻油 (5ml)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 麻油 (5ml)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "番茄酱 (15ml)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 番茄酱 (15ml)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒 (15ml)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒 (15ml)",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "洋葱切片,猪肉,蒜头拍碎,以及混合上述调味料备用"
+ },
+ {
+ "step": 2,
+ "description": "炒锅内倒入 1 大匙食用油(等待 10 秒让油温升高),倒入猪肉"
+ },
+ {
+ "step": 3,
+ "description": "炒至变色后下蒜头炒香盛起备用"
+ },
+ {
+ "step": 4,
+ "description": "原锅下洋葱翻炒 3~4 分钟后加入调味料炒匀"
+ },
+ {
+ "step": 5,
+ "description": "下刚盛起备用的猪肉翻炒至猪肉熟后"
+ },
+ {
+ "step": 6,
+ "description": "待猪肉熟后再翻炒 1、2 分钟即可起锅"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-烤鸡翅",
+ "name": "烤鸡翅的做法",
+ "description": "# 烤鸡翅的做法\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/meat_dish/烤鸡翅.md",
+ "image_path": null,
+ "images": [],
+ "category": "荤菜",
+ "difficulty": 3,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "鸡翅中",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡翅中",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黑胡椒粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黑胡椒粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酱油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酱油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "烤箱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 烤箱",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "鸡翅放入碗中"
+ },
+ {
+ "step": 2,
+ "description": "加盐、黑胡椒粉、酱油、料酒进行腌制 30-40 分钟"
+ },
+ {
+ "step": 3,
+ "description": "将烤箱预热至 200℃"
+ },
+ {
+ "step": 4,
+ "description": "将腌制好的鸡翅均匀地放在烤盘上"
+ },
+ {
+ "step": 5,
+ "description": "将烤盘放入烤箱中层,烤 15-20 分钟"
+ },
+ {
+ "step": 6,
+ "description": "取出烤盘,将鸡翅翻面,再烤 15-20 分钟,直到熟透"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-猪肉烩酸菜",
+ "name": "猪肉烩酸菜的做法",
+ "description": "# 猪肉烩酸菜的做法\n\n猪肉烩酸菜是一道北方名菜,简单易做。富含蛋白质。一般初学者需要 3 小时完成。\n\n预估烹饪难度:★★★★★",
+ "source_path": "dishes/meat_dish/猪肉烩酸菜.md",
+ "image_path": null,
+ "images": [],
+ "category": "荤菜",
+ "difficulty": 5,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "猪五花肉或猪肉排骨",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 猪五花肉或猪肉排骨",
+ "notes": "量未指定"
+ },
+ {
+ "name": "东北酸菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 东北酸菜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽酱油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽酱油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "五香粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 五香粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大料",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大料",
+ "notes": "量未指定"
+ },
+ {
+ "name": "猪排骨或者五花肉(总共)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 猪排骨或者五花肉(总共) 1500 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "东北酸菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 东北酸菜 1000 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大葱 1 根",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜 100 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜 4 瓣",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 10 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽酱油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽酱油 15 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "五香粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 五香粉 10 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒 20 毫升",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大料",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大料 2 颗",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "大葱切段;生姜 50 克切段, 50 克切末;大蒜切末,备用。"
+ },
+ {
+ "step": 2,
+ "description": "全部酸菜切丝,用水冲洗 2 ~ 3 遍备用。"
+ },
+ {
+ "step": 3,
+ "description": "排骨和五花肉入锅,倒入冷水淹没。放入全部葱段, 50 克生姜段和料酒 20 毫升。大火煮开后,等待 5 分钟。关火,将排骨和五花肉捞出,冷水冲洗掉浮沫,备用"
+ },
+ {
+ "step": 4,
+ "description": "煮好的五花肉切片或者切块,备用。"
+ },
+ {
+ "step": 5,
+ "description": "将之前的锅洗干净,并且擦干(不然加入油会崩出来)。"
+ },
+ {
+ "step": 6,
+ "description": "锅中加入油,开中火,放入姜蒜末爆香,放入五花肉和排骨。将五花肉和排骨煎至金黄,倒入 10 克五香粉和 15 克 生抽酱油,用铲子翻动 1 ~ 2 分钟。"
+ },
+ {
+ "step": 7,
+ "description": "将冲洗好的酸菜丝加入锅中,翻炒 3 分钟。"
+ },
+ {
+ "step": 8,
+ "description": "倒入纯净水至刚好没过食材,加入 2 颗大料,转大火,直到锅中水沸腾。转中火,盖锅盖焖煮。"
+ },
+ {
+ "step": 9,
+ "description": "等待 1.5 ~ 2 小时,直至五花肉软烂 (可以用筷子轻松扎穿)"
+ },
+ {
+ "step": 10,
+ "description": "掀开锅盖,开大火收汤,翻动锅中食材直至锅中剩余水分只覆盖锅底,转小火,准备调味。"
+ },
+ {
+ "step": 11,
+ "description": "调味:加入食用盐 10 克,搅拌均匀。"
+ },
+ {
+ "step": 12,
+ "description": "关火,出锅。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-甜辣烤全翅",
+ "name": "甜辣烤全翅的做法",
+ "description": "# 甜辣烤全翅的做法\n\n本甜辣烤全翅使用空气炸锅烹饪并仅使用家中常见调料,低油脂并且不需要成品烧烤酱,一份适合单人食用,食材处理需要 15 分钟,腌制需要 120 分钟, 烹饪需要 50 分钟。\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/meat_dish/甜辣烤全翅.md",
+ "image_path": null,
+ "images": [],
+ "category": "荤菜",
+ "difficulty": 3,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "空气炸锅",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 空气炸锅",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡全翅",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡全翅",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "胡椒粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 胡椒粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "甜椒粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 甜椒粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "辣椒粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 辣椒粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蚝油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蚝油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 水",
+ "notes": "量未指定"
+ },
+ {
+ "name": "油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "锡纸盘",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 锡纸盘",
+ "notes": "量未指定"
+ },
+ {
+ "name": "保鲜膜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 保鲜膜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡全翅",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡全翅 4 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 45ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽 15ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜粉 10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "胡椒粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 胡椒粉 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 糖 10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "甜椒粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 甜椒粉 10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "辣椒粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 辣椒粉 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蚝油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蚝油 15ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 水 20ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 油 10ml",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "将 4 个新鲜鸡全翅取出,在翅中两根骨头之间用刀划开表皮,正反面各一刀"
+ },
+ {
+ "step": 2,
+ "description": "将 4 个鸡全翅放入碗中,加入生抽 45ml , 老抽 15ml , 蒜粉 10g , 胡椒粉 5g , 糖 10g , 甜椒粉 10g ,辣椒粉 5g , 蚝油 15ml , 水 20ml 以及油 10ml"
+ },
+ {
+ "step": 3,
+ "description": "用勺子将酱汁均匀的抹在鸡全翅上,尤其是翅中的刀口处,大约花费 3 分钟"
+ },
+ {
+ "step": 4,
+ "description": "用保鲜膜盖住防油腌制中鸡全翅的碗,放入冰箱冷藏格静置 120 分钟"
+ },
+ {
+ "step": 5,
+ "description": "取出鸡全翅,锡纸盘中放入鸡全翅 4 个,将碗中残余酱料均匀倒在鸡全翅上"
+ },
+ {
+ "step": 6,
+ "description": "锡纸盘放入空气炸锅的烤篮上,用 200 摄氏度烤 25 分钟"
+ },
+ {
+ "step": 7,
+ "description": "打开空气炸锅,小心取出锡纸盘,将鸡全翅翻面"
+ },
+ {
+ "step": 8,
+ "description": "继续 200 摄氏度烤 25 分钟"
+ },
+ {
+ "step": 9,
+ "description": "取出即可食用"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-番茄红酱",
+ "name": "番茄红酱的做法",
+ "description": "# 番茄红酱的做法\n\n番茄红酱香浓可口,营养丰富,咱很喜欢。可以作为薄饼、意面~~热干面~~等主食的百搭酱料。有些繁琐,适合有烹饪经验的人尝试。一次吃不完也没有关系,可以冷冻后随时拿出来加热哦。(但是千万要记得吃)\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/meat_dish/番茄红酱.md",
+ "image_path": null,
+ "images": [],
+ "category": "荤菜",
+ "difficulty": 4,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "碎牛肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 碎牛肉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜瓣",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜瓣",
+ "notes": "量未指定"
+ },
+ {
+ "name": "胡萝卜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 胡萝卜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "芹菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 芹菜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "洋葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 洋葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "橄榄油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 橄榄油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "胡椒粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 胡椒粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "番茄酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 番茄酱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "牛奶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 牛奶",
+ "notes": "量未指定"
+ },
+ {
+ "name": "干罗勒或百里香(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 干罗勒或百里香(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "碎牛肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 碎牛肉 500g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜瓣",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜瓣 2 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "胡萝卜 半根",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 胡萝卜 半根",
+ "notes": "量未指定"
+ },
+ {
+ "name": "芹菜 一根",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 芹菜 一根",
+ "notes": "量未指定"
+ },
+ {
+ "name": "洋葱 半个",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 洋葱 半个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "橄榄油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 橄榄油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 糖 2g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食盐 10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黑胡椒粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黑胡椒粉 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "番茄酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 番茄酱 300g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "牛奶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 牛奶 300ml",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "将胡萝卜、芹菜、洋葱切碎,蒜瓣切片。"
+ },
+ {
+ "step": 2,
+ "description": "加入 10ml 橄榄油,热油下锅蔬菜,大火翻炒开始略微变色后盛出。"
+ },
+ {
+ "step": 3,
+ "description": "锅内加油 10ml,加蒜翻炒 10 秒,加入碎牛肉、糖、盐、胡椒粉和香料将牛肉炒脆(有颗粒感)。"
+ },
+ {
+ "step": 4,
+ "description": "加入炒好的蔬菜们和番茄酱继续翻炒,搅拌均匀。"
+ },
+ {
+ "step": 5,
+ "description": "分多次缓缓倒入牛奶,中小火煮 30 分钟,完成。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-白菜猪肉炖粉条",
+ "name": "白菜猪肉炖粉条的做法",
+ "description": "# 白菜猪肉炖粉条的做法\n\n白菜猪肉炖粉条是一道简单易做的菜。这是一道传统的东北家常菜,以做法简单、味道上乘的特点,在广大东北人民群众中备受喜爱。\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/meat_dish/白菜猪肉炖粉条.md",
+ "image_path": null,
+ "images": [],
+ "category": "荤菜",
+ "difficulty": 3,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "五花肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 五花肉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白菜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "土豆干粉条",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 土豆干粉条",
+ "notes": "量未指定"
+ },
+ {
+ "name": "十三香",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 十三香",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡精",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡精",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "五花肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 五花肉 300g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大白菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大白菜 500g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "土豆干粉条",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 土豆干粉条 50g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "十三香",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 十三香 10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡精",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡精 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用盐 15g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽 5ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 5ml",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "锅内烧水,水开后放入干粉条,煮 5 分钟后同水一起倒出容器中,盖上盖子继续浸泡泡 备用(第一步先做这个,期间可以进行以下步骤)"
+ },
+ {
+ "step": 2,
+ "description": "五花肉切 3mm 的肉片,备用"
+ },
+ {
+ "step": 3,
+ "description": "大白菜嫩叶与白菜帮子分开切成 2 份菜片,备用"
+ },
+ {
+ "step": 4,
+ "description": "热锅,锅内放入 10ml - 15ml 食用油。等待 10 秒让油温升高"
+ },
+ {
+ "step": 5,
+ "description": "放入五花肉,保持翻炒至肉变色"
+ },
+ {
+ "step": 6,
+ "description": "加入老抽,炒 **1 分钟**,给肉上色"
+ },
+ {
+ "step": 7,
+ "description": "加入白菜帮子,加入食用盐、生抽,炒一分钟(如果粘锅,烹入 10ml 水)"
+ },
+ {
+ "step": 8,
+ "description": "加水没过所有食材,加入鸡精 ,十三香,沸腾后,将火调小然后**等待 20 分钟**"
+ },
+ {
+ "step": 9,
+ "description": "粉条滤水切成小段放入碗中 备用"
+ },
+ {
+ "step": 10,
+ "description": "加入白菜嫩叶,炒匀后将粉条放在菜上方,加盖再煮 **5 分钟**"
+ },
+ {
+ "step": 11,
+ "description": "尝味、关火,收汁"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-粉蒸肉",
+ "name": "粉蒸肉的做法",
+ "description": "# 粉蒸肉的做法\n\n粉蒸肉是一道经典的中式蒸菜,香味浓郁,口感软糯,营养丰富。适合家庭聚餐或节日宴客。此菜适合有一定烹饪经验的人士制作,预计从准备到完成约需 90 分钟。\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/meat_dish/粉蒸肉.md",
+ "image_path": null,
+ "images": [],
+ "category": "荤菜",
+ "difficulty": 4,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "五花肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 五花肉 500g(肥瘦相间)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒸肉米粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒸肉米粉 100g(推荐使用李锦记或自制)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 15ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽 10ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒 15ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "郫县豆瓣酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 郫县豆瓣酱 10g(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜末",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜末 10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜末",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜末 10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白砂糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白砂糖 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "土豆",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 土豆 300g(或南瓜 300g,作为垫底食材)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "清水(蒸锅用)2000ml",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 清水(蒸锅用)2000ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "五花肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 五花肉 250g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒸肉米粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒸肉米粉 50g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 7.5ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽 5ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒 7.5ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "郫县豆瓣酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 郫县豆瓣酱 5g(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜末",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜末 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜末",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜末 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白砂糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白砂糖 2.5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "土豆或南瓜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 土豆或南瓜 150g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒸锅用水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒸锅用水 1000ml",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "将五花肉洗净,切成长约 5cm、宽约 3cm、厚度约 0.5cm 的肉片"
+ },
+ {
+ "step": 2,
+ "description": "将姜、蒜切成颗粒直径不大于 1mm 的细末"
+ },
+ {
+ "step": 3,
+ "description": "取一大碗,放入切好的五花肉、15ml 生抽、10ml 老抽、15ml 料酒、10g 郫县豆瓣酱、10g 姜末、10g 蒜末、5g 白砂糖"
+ },
+ {
+ "step": 4,
+ "description": "用筷子搅拌均匀后,盖上保鲜膜,室温(20°C - 25°C)静置腌制 30 分钟"
+ },
+ {
+ "step": 5,
+ "description": "腌制完成后,加入 100g 蒸肉米粉,继续翻拌 2 分钟,确保每片肉都均匀裹粉"
+ },
+ {
+ "step": 6,
+ "description": "土豆去皮,切片厚度控制在 0.8cm,片面积约为 5cm x 5cm,重量控制在 300g"
+ },
+ {
+ "step": 7,
+ "description": "在直径 20cm 的深碗底部铺满土豆片,尽量无重叠"
+ },
+ {
+ "step": 8,
+ "description": "将拌好粉的五花肉均匀铺在土豆片上,压实"
+ },
+ {
+ "step": 9,
+ "description": "蒸锅中加入 2000ml 清水,开火加热至水面持续冒泡(100°C)"
+ },
+ {
+ "step": 10,
+ "description": "将装好食材的碗放入蒸锅内,盖好锅盖"
+ },
+ {
+ "step": 11,
+ "description": "保持中火蒸 60 分钟(火力保持在可持续沸腾的程度,约 600W 热功率)"
+ },
+ {
+ "step": 12,
+ "description": "时间结束后,用筷子插入肉块中央,若能轻松穿透并无明显阻力,则表明蒸熟"
+ },
+ {
+ "step": 13,
+ "description": "若未达到此状态,则继续加热 10 - 15 分钟,直至肉质软烂,油脂渗出"
+ },
+ {
+ "step": 14,
+ "description": "取出盛盘,即可食用"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-糖醋里脊",
+ "name": "糖醋里脊的做法",
+ "description": "# 糖醋里脊的做法\n\n糖醋里脊是中国经典传统名菜之一,该菜品以猪里脊肉为主材,配以面粉、淀粉、醋等佐料,酸甜可口,让人食欲大开;该菜品在陕菜、豫菜、浙菜、鲁菜、川菜、淮扬菜、粤菜、闽菜里均有此菜。\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/meat_dish/糖醋里脊.md",
+ "image_path": null,
+ "images": [],
+ "category": "荤菜",
+ "difficulty": 4,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "里脊肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 里脊肉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "醋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 醋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "淀粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 淀粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蚝油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蚝油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "番茄酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 番茄酱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白胡椒粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白胡椒粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "里脊肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 里脊肉 500g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "醋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 醋 10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖 30g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "淀粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 淀粉 50g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋 50g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 10ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒 20g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蚝油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蚝油 10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "番茄酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 番茄酱 30ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白胡椒粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白胡椒粉 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食盐 10g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "腌肉:将猪里脊肉先切厚片,用刀背拍一拍,把肉拍松一点。切成一个手指头粗的条,加料酒,生抽,蚝油,食盐,白胡椒粉,一个鸡蛋,将肉用手抓匀,腌制 20 分钟以上。"
+ },
+ {
+ "step": 2,
+ "description": "调酱:番茄酱+10g 醋+30g 白糖+150ml 清水,搅拌至糖融化,备用。"
+ },
+ {
+ "step": 3,
+ "description": "裹粉:先把粉全部裹好再来炸,这样在炸的时候就不会手忙脚乱。准备一个大碗,里面放淀粉,把每一根肉条都满满裹上淀粉。"
+ },
+ {
+ "step": 4,
+ "description": "炸制:油温 160 摄氏度下里脊,可以拿一个干筷子放在油里面试一下,周围冒小泡就可以下锅。"
+ },
+ {
+ "step": 5,
+ "description": "炸到表面微黄可以捞出,全程中火。然后等油温升高到 200 摄氏度,把里脊倒进去重新炸一次,只需 40 秒,表皮就会很脆,马上捞出。"
+ },
+ {
+ "step": 6,
+ "description": "裹酱:另外拿一个锅,锅里放底油,把调好的酱汁倒进去,煮到冒泡,把炸好的里脊放进去,翻炒,让每一根都裹上酱汁。"
+ },
+ {
+ "step": 7,
+ "description": "下炸好的里脊肉翻炒,关火盛出。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-肉饼炖蛋",
+ "name": "肉饼炖蛋的做法",
+ "description": "# 肉饼炖蛋的做法\n\n肉饼炖蛋是一道传统的中国家常菜,也是一道非常受欢迎的下饭菜。初学者只需要 20 分钟即可完成。\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/meat_dish/肉饼炖蛋.md",
+ "image_path": null,
+ "images": [],
+ "category": "荤菜",
+ "difficulty": 3,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "猪肉末",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 猪肉末",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白胡椒粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白胡椒粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "芝麻香油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 芝麻香油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "猪肉末",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 猪肉末 300g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋 2 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒 10ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 20ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白胡椒粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白胡椒粉 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "芝麻香油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 芝麻香油 10-15ml",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "在碗中加入猪肉末、料酒、生抽、白胡椒粉、鸡蛋和芝麻香油,搅拌均匀。"
+ },
+ {
+ "step": 2,
+ "description": "将调好味的猪肉末铺在盘子里,肉末中间用勺子挖一个洞,往洞中打入 1 个鸡蛋。"
+ },
+ {
+ "step": 3,
+ "description": "锅中加水至 1/4 高度,水烧开后,将盘子放入锅中,盖上锅盖,蒸 15 分钟。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-萝卜炖羊排",
+ "name": "萝卜炖羊排的做法",
+ "description": "# 萝卜炖羊排的做法\n\n萝卜炖羊排是一道常见家常菜,老少皆宜。一般初学者只需要最多 2 小时即可完成。\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/meat_dish/萝卜炖羊排.md",
+ "image_path": null,
+ "images": [],
+ "category": "荤菜",
+ "difficulty": 4,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "羊排",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 羊排",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白萝卜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白萝卜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "花椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 花椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白芷(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白芷(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒或者黄酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒或者黄酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冰糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冰糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 水",
+ "notes": "量未指定"
+ },
+ {
+ "name": "羊排",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 羊排 400g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白萝卜一根",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白萝卜一根",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大葱一根",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大葱一根",
+ "notes": "量未指定"
+ },
+ {
+ "name": "花椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 花椒 10 粒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜 10g ,一般买一头姜,从中切大约 4 片即可",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒或者黄酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒或者黄酒 30ml-40ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用盐 10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冰糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冰糖 2-4 粒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "水:没过食材的量,需要",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 水:没过食材的量,需要 1000ml",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "萝卜去皮、滚刀切成 3-5cm 的大块,备用"
+ },
+ {
+ "step": 2,
+ "description": "羊排在购买时可以让卖家切好,因为家用刀一般难以切动,备用"
+ },
+ {
+ "step": 3,
+ "description": "羊肉冷水下锅,加入一半的料酒,一半的葱姜,煮 10 分钟去掉血腥,(可选)把焯的过程中出现的血沫子可以用勺子盛出来"
+ },
+ {
+ "step": 4,
+ "description": "另起一锅冷水,放入切好的白萝卜,放入一半的冰糖,等水开后煮 5 分钟去掉白萝卜的辣味"
+ },
+ {
+ "step": 5,
+ "description": "盛出来焯好的羊排,放入高压锅中,加水没过所有食材后再增加大约 300ml 的水"
+ },
+ {
+ "step": 6,
+ "description": "将剩余的葱姜料酒,花椒,冰糖,白芷(可选),盐放入锅中,盖锅等待上汽后计时,中火炖大约 15 分钟。"
+ },
+ {
+ "step": 7,
+ "description": "关火,等待高压锅放气完毕,开盖,加入之前焯好的萝卜,调味,加入 3-10g 的食盐或者水,品尝汤的咸淡,"
+ },
+ {
+ "step": 8,
+ "description": "再开火,中火,高压锅上汽再炖 10 分钟,普通锅盖盖再炖 20 分钟"
+ },
+ {
+ "step": 9,
+ "description": "关火,盛盘"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-蒜苔炒肉末",
+ "name": "蒜苔炒肉末的做法",
+ "description": "# 蒜苔炒肉末的做法\n\n蒜苔炒肉末是一道简单易做的菜。这是一道北方家常菜,以做法简单、味道上乘的特点,在广大北方人民群众中备受喜爱。\n\n预估烹饪难度:★★",
+ "source_path": "dishes/meat_dish/蒜苔炒肉末.md",
+ "image_path": null,
+ "images": [],
+ "category": "荤菜",
+ "difficulty": 2,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "五花肉薄片",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 五花肉薄片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜苔",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜苔",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜瓣",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜瓣",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜苔",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜苔 1 扎(每扎蒜苔约 190g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "五花肉薄片",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 五花肉薄片 4 片(约 20g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 10ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜瓣",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜瓣 2 瓣",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 15ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食盐 2g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "蒜苔切成 5cm 小段,备用"
+ },
+ {
+ "step": 2,
+ "description": "五花肉切成 5mm * 5cm 丝状,备用"
+ },
+ {
+ "step": 3,
+ "description": "蒜瓣拍碎切成末,备用"
+ },
+ {
+ "step": 4,
+ "description": "热锅,锅内放入 10ml 食用油。等待 10 秒让油温升高"
+ },
+ {
+ "step": 5,
+ "description": "放入蒜末,中火翻炒 **10 秒** 将蒜末炒出香味"
+ },
+ {
+ "step": 6,
+ "description": "放入五花肉和 5ml 生抽,中火翻炒 **30 秒** 将肉炒熟并上色"
+ },
+ {
+ "step": 7,
+ "description": "将蒜苔放入锅内并加入 10ml 生抽,翻炒 **30 秒**"
+ },
+ {
+ "step": 8,
+ "description": "锅内加入 20g 水,中火翻炒 **5 分钟** 将蒜苔炒至稍稍变软"
+ },
+ {
+ "step": 9,
+ "description": "最后加入 2g 食盐,中火翻炒 **30 秒**,即可出锅装盘"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-虎皮肘子",
+ "name": "虎皮肘子的做法",
+ "description": "# 虎皮肘子的做法\n\n虎皮肘子是一道传统名菜,以猪肘为主料,通过先烧再炸后炖三个步骤使肘子皮呈现出虎皮状。肘子皮软烂入味,肥而不腻,瘦肉松软可口。这道菜是逢年过节让老辈子闭嘴猛炫的不二之选,可谓是救命法宝。\n\n预估烹饪难度:★★★★★",
+ "source_path": "dishes/meat_dish/虎皮肘子.md",
+ "image_path": null,
+ "images": [],
+ "category": "荤菜",
+ "difficulty": 5,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "猪前肘",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 猪前肘",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用植物油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用植物油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冰糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冰糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白醋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白醋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香叶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香叶",
+ "notes": "量未指定"
+ },
+ {
+ "name": "肉桂皮",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 肉桂皮",
+ "notes": "量未指定"
+ },
+ {
+ "name": "豆蔻",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 豆蔻",
+ "notes": "量未指定"
+ },
+ {
+ "name": "花椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 花椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大料",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大料",
+ "notes": "量未指定"
+ },
+ {
+ "name": "淀粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 淀粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 水",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "1 汤匙 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 1 汤匙 = 15ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "1 茶匙 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 1 茶匙 = 5ml",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "猪肘解冻后水泡 1 小时去除血水。"
+ },
+ {
+ "step": 2,
+ "description": "如有火焰喷枪,则使用火焰喷枪灼烧**猪肘皮**表面至**棕黑色**以去除猪毛,破坏汗腺。注意不要长时间炙烤同一个位置以避免烧焦,当猪肘皮几乎完全呈现棕黑色时则停止灼烧。"
+ },
+ {
+ "step": 3,
+ "description": "如无火焰喷枪,将铁锅烧至 200 以上,将猪肘直接放入锅内,用锅铲或筷子使猪肘皮充分接触铁锅表面,当猪肘皮与铁锅接触位置呈现出棕色时,更换位置继续烫猪肘皮,直到整个猪肘被充分烫过。注意再次过程中注意铁锅温度,不要使铁锅红热。"
+ },
+ {
+ "step": 4,
+ "description": "使用清洁球在水中刷洗猪肘,将其表面烧焦的部分去除。刷洗结束后,猪肘再次呈现出未被灼烧前的状态。"
+ },
+ {
+ "step": 5,
+ "description": "将猪肘置于铁锅中,加尽量多的冷水,具体视铁锅深度与猪肘大小而定,在保证可以拿得动铁锅及其内容物的情况下,能浸没猪肘 3/4 以上为最佳。"
+ },
+ {
+ "step": 6,
+ "description": "取 1 棵葱的葱白,分成 3 段,放入锅中。"
+ },
+ {
+ "step": 7,
+ "description": "取 3 粒蒜,分别用刀身拍扁,放入锅中。"
+ },
+ {
+ "step": 8,
+ "description": "取 3 克姜,放入锅中。"
+ },
+ {
+ "step": 9,
+ "description": "将 2 汤匙料酒加入锅中。"
+ },
+ {
+ "step": 10,
+ "description": "锅中水烧开后,等待五分钟,随后将猪肘取出,捡出锅中所有配料,更换容器保留所有肉汤备用。"
+ },
+ {
+ "step": 11,
+ "description": "向锅中加入冷油,以之前水量为参考,能浸没猪肘 3/5 以上为佳,开中火加热。"
+ },
+ {
+ "step": 12,
+ "description": "当[油温](tips/advanced/油温判断技巧.md)达到五成时,转为小火,放入猪肘油炸。"
+ },
+ {
+ "step": 13,
+ "description": "在油炸过程中烹饪者应注意人身安全。"
+ },
+ {
+ "step": 14,
+ "description": "在油炸过程中,使用锅铲或其他耐高温厨具将锅中的油均匀淋到猪肘未被浸没的部分,如果条件允许应以 3 分钟的间隔翻转猪肘,使其油炸均匀。"
+ },
+ {
+ "step": 15,
+ "description": "油炸过程持续大约 20 分钟,当观察到猪肘皮已经全部呈现出浅棕色,而瘦肉部分已经微焦,则可捞出备用。"
+ },
+ {
+ "step": 16,
+ "description": "炸制完后的油可用于制作其他油炸类食物,但注意不要使用太多次。"
+ },
+ {
+ "step": 17,
+ "description": "[炒糖色](../../tips/advanced/糖色的炒制.md)200ml 备用。"
+ },
+ {
+ "step": 18,
+ "description": "将猪肘加入高压锅内,加入所有肉汤、糖色、香叶、肉桂皮、豆蔻、花椒、大料、老抽、生抽、白醋。如果喜欢甜口,可以再额外加入 2-3 克冰糖。"
+ },
+ {
+ "step": 19,
+ "description": "取 1 棵葱的葱白,分成 3 段,放入锅中。"
+ },
+ {
+ "step": 20,
+ "description": "取 3 粒蒜,分别用刀身拍扁,放入锅中。"
+ },
+ {
+ "step": 21,
+ "description": "取 3 克姜,放入锅中。"
+ },
+ {
+ "step": 22,
+ "description": "盖上锅盖,加压炖煮 40 分钟。"
+ },
+ {
+ "step": 23,
+ "description": "在炖煮期间调制水淀粉。取碗 1 个,加入 1 汤匙淀粉,100ml 水,搅拌使其成为白色悬浊液"
+ },
+ {
+ "step": 24,
+ "description": "炖煮时间结束后,打开高压锅锅盖,捡出锅中所有的配料,只保留猪肘。"
+ },
+ {
+ "step": 25,
+ "description": "将高压锅中剩余的肉汤转移至铁锅内,猪肘转移至盘子或盆内"
+ },
+ {
+ "step": 26,
+ "description": "将铁锅置于灶台上,开大火。在收汁过程中可以用筷子头蘸取锅内汤汁判断咸淡,并根据口味添加盐。注意,汤汁多的时候味道会比汤汁少的时候味道更淡,加入盐时需要考虑这一点。"
+ },
+ {
+ "step": 27,
+ "description": "当肉汤沸腾时,注意观察剩余肉汤余量"
+ },
+ {
+ "step": 28,
+ "description": "当剩余肉汤少于原肉汤体积的 1/2 时,再次搅拌之前调制好的水淀粉,并加入一半"
+ },
+ {
+ "step": 29,
+ "description": "等待肉汤沸腾,加入剩下的一半"
+ },
+ {
+ "step": 30,
+ "description": "等待肉汤沸腾,沸腾后等待 1-2 分钟关火,此时锅内的肉汤呈红棕色粘稠状"
+ },
+ {
+ "step": 31,
+ "description": "用汤匙舀起肉汤均匀地淋在猪肘上,尽量使猪肘的每一处都淋到汤汁。如果在猪肘被完全淋到前汤汁已经用完则可直接上桌,否则剩余汤汁不需要再淋,可直接上桌。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-蚂蚁上树",
+ "name": "蚂蚁上树的做法",
+ "description": "# 蚂蚁上树的做法\n\n蚂蚁上树是一道经典的川菜,主要材料为粉丝和肉末。它咸香微辣、入味透彻,粉丝软滑爽口,肉末细嫩鲜香。全程只需 20 分钟,是非常适合家庭操作的一道菜。\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/meat_dish/蚂蚁上树.md",
+ "image_path": null,
+ "images": [],
+ "category": "荤菜",
+ "difficulty": 3,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "红薯粉丝",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 红薯粉丝",
+ "notes": "量未指定"
+ },
+ {
+ "name": "猪肉末(或牛肉末)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 猪肉末(或牛肉末)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "郫县豆瓣酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 郫县豆瓣酱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜末、姜末",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜末、姜末",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小葱(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小葱(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "红薯粉丝",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 红薯粉丝 80g(干重)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "猪肉末",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 猪肉末 150g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "郫县豆瓣酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 郫县豆瓣酱 15g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 10ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽 5ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 10ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜末",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜末 10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜末",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜末 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "清水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 清水 300ml(用于煮粉丝)",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "红薯粉丝提前泡软,泡水时间为 20 分钟,备用"
+ },
+ {
+ "step": 2,
+ "description": "将蒜、姜分别剁碎,备用"
+ },
+ {
+ "step": 3,
+ "description": "锅烧热,加入 10ml 食用油,加入蒜末、姜末炒香"
+ },
+ {
+ "step": 4,
+ "description": "加入猪肉末翻炒至**肉色发白且微微出油**"
+ },
+ {
+ "step": 5,
+ "description": "加入郫县豆瓣酱,炒至**红油析出**"
+ },
+ {
+ "step": 6,
+ "description": "加入生抽和老抽,翻炒均匀"
+ },
+ {
+ "step": 7,
+ "description": "倒入 300ml 清水,煮沸"
+ },
+ {
+ "step": 8,
+ "description": "放入泡软沥干的粉丝,用筷子轻轻拨动防止粘连"
+ },
+ {
+ "step": 9,
+ "description": "中小火煮约 5 分钟,直至粉丝**完全吸收汤汁**、呈现微微收干状态"
+ },
+ {
+ "step": 10,
+ "description": "依据口味可撒入小葱末,关火装盘"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-辣椒炒肉",
+ "name": "辣椒炒肉的做法",
+ "description": "# 辣椒炒肉的做法\n\n⚠️注意:本道菜需要一定料理基础,不推荐新手尝试。\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/meat_dish/辣椒炒肉.md",
+ "image_path": null,
+ "images": [],
+ "category": "荤菜",
+ "difficulty": 3,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "青椒(吃辣的话推荐用杭椒,螺丝椒,不吃辣的用尖椒,甜椒)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 青椒(吃辣的话推荐用杭椒,螺丝椒,不吃辣的用尖椒,甜椒)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "猪瘦肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 猪瘦肉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蚝油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蚝油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酱油(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酱油(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "豆豉(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 豆豉(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "青椒的数量 = 份数",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 青椒的数量 = 份数 * 3 个。",
+ "notes": "量未指定"
+ },
+ {
+ "name": "肉量 = 份数",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 肉量 = 份数 * 200g。",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐量 = 份数",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐量 = 份数 * 3g。",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽 = 份数",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 = 份数 * 3ml。",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蚝油 = 份数",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蚝油 = 份数 * 3ml。",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大蒜 = 份数",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大蒜 = 份数 * 5g。",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生姜 = 份数",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生姜 = 份数 * 5g。",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酱油 = 份数",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酱油 = 份数 * 2ml。",
+ "notes": "量未指定"
+ },
+ {
+ "name": "豆豉 = 份数",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 豆豉 = 份数 * 3g。",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "将`青椒`洗净,去除`青椒把`以及`青椒籽`,再用`滚刀手法`切好备用。"
+ },
+ {
+ "step": 2,
+ "description": "`大蒜`用刀拍一下,再横切成`蒜瓣`,`生姜`切碎成`姜末`。"
+ },
+ {
+ "step": 3,
+ "description": "将`猪瘦肉`切成`肉片`(顺着猪肉的纹理切,即刀和肉的纹理呈水平线,出来的肉片,纹路呈“川”字)。"
+ },
+ {
+ "step": 4,
+ "description": "将切好的`猪肉`洗净,放入空碗,再加入计算好的`生抽`、`蚝油`、`盐`搅拌均匀,腌制 10 分钟。"
+ },
+ {
+ "step": 5,
+ "description": "热锅,不用倒油,把`切好的青椒`放入锅中,大火干煸至虎皮状后,再加 2g`盐`继续翻炒 1 分钟 后捞起。"
+ },
+ {
+ "step": 6,
+ "description": "不用洗锅,大火热锅,加入份数 * 8ml`油`,等待 30s,加入`蒜瓣`、`姜末`翻炒 15s。"
+ },
+ {
+ "step": 7,
+ "description": "加入腌制好的`猪肉`倒入锅内翻炒 2 分钟,再加入干煸过的`青椒`翻炒 1 分钟。"
+ },
+ {
+ "step": 8,
+ "description": "根据个人口味喜好加入`豆豉`,最后加入`酱油`,继续翻炒 30s。"
+ },
+ {
+ "step": 9,
+ "description": "出锅,盛盘。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-香干肉丝",
+ "name": "香干肉丝的做法",
+ "description": "# 香干肉丝的做法\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/meat_dish/香干肉丝.md",
+ "image_path": null,
+ "images": [],
+ "category": "荤菜",
+ "difficulty": 3,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "猪里脊(可以买超市切好且称重好的肉丝)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 猪里脊(可以买超市切好且称重好的肉丝)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香干",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香干",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "淀粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 淀粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "青椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 青椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡精",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡精",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香干 = 份数",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香干 = 份数 * 75g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "青椒的数量 = 份数",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 青椒的数量 = 份数 * 5 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "肉量 = 份数",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 肉量 = 份数 * 100g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐量 = 份数",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐量 = 份数 * 3g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽 = 份数",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 = 份数 * 5ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "淀粉 = 份数",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 淀粉 = 份数 * 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大蒜 = 份数",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大蒜 = 份数 * 5g(大约 3 个蒜瓣)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡精 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡精 = 2g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "`肉丝`(没有肉丝,自己切)用生抽(3ml),生粉混合均匀待用。"
+ },
+ {
+ "step": 2,
+ "description": "将`青椒`洗净,再用`滚刀手法`切好备用。"
+ },
+ {
+ "step": 3,
+ "description": "`大蒜`横切成片,`香干`切丝。"
+ },
+ {
+ "step": 4,
+ "description": "`淀粉`与水(10ml)混合,搅拌均匀。"
+ },
+ {
+ "step": 5,
+ "description": "干净锅 15ml 油,不用等油热就倒入肉丝慢慢划散,肉丝熟了,立马捞出,留油到锅里。"
+ },
+ {
+ "step": 6,
+ "description": "将蒜片和香干放入锅中,加入 2ml 生抽,翻炒均匀。"
+ },
+ {
+ "step": 7,
+ "description": "2-3 分钟,看火大小,将青椒丝放入锅中混合,翻炒。"
+ },
+ {
+ "step": 8,
+ "description": "1 分钟后,放入肉丝混合。"
+ },
+ {
+ "step": 9,
+ "description": "倒入淀粉与水的混合物勾芡,加入盐 3g,鸡精 2g,翻炒 2-3 分钟出锅。"
+ },
+ {
+ "step": 10,
+ "description": "成品。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-鱼香肉丝",
+ "name": "鱼香肉丝的做法",
+ "description": "# 鱼香肉丝的做法\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/meat_dish/鱼香肉丝.md",
+ "image_path": null,
+ "images": [],
+ "category": "荤菜",
+ "difficulty": 4,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "里脊肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 里脊肉 200g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "胡萝卜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 胡萝卜 100g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "青椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 青椒 100g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "木耳(干)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 木耳(干) 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 10ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒 5ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蛋清",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蛋清 1 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "淀粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 淀粉 10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "醋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 醋 15ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖 10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜 20g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱 20g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜 2 瓣",
+ "notes": "量未指定"
+ },
+ {
+ "name": "豆瓣酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 豆瓣酱 15g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "制作`腌料`:将下列原料混合:"
+ },
+ {
+ "step": 2,
+ "description": "生抽 5ml"
+ },
+ {
+ "step": 3,
+ "description": "料酒 5ml"
+ },
+ {
+ "step": 4,
+ "description": "淀粉 5g"
+ },
+ {
+ "step": 5,
+ "description": "水 20ml"
+ },
+ {
+ "step": 6,
+ "description": "蛋清 1 个"
+ },
+ {
+ "step": 7,
+ "description": "制作`香汁`:将下列原料混合:"
+ },
+ {
+ "step": 8,
+ "description": "生抽 5ml"
+ },
+ {
+ "step": 9,
+ "description": "醋 15ml"
+ },
+ {
+ "step": 10,
+ "description": "白糖 10 克"
+ },
+ {
+ "step": 11,
+ "description": "盐 1 克"
+ },
+ {
+ "step": 12,
+ "description": "淀粉 5g"
+ },
+ {
+ "step": 13,
+ "description": "水 20ml"
+ },
+ {
+ "step": 14,
+ "description": "用`腌料`腌制里脊肉 15-30 分钟。注意将肉抓匀。"
+ },
+ {
+ "step": 15,
+ "description": "干木耳泡 4 个小时,洗净,切成小块。"
+ },
+ {
+ "step": 16,
+ "description": "青椒洗净,去蒂,切成丝。"
+ },
+ {
+ "step": 17,
+ "description": "胡萝卜洗净,切成丝,将胡萝卜丝[焯水](../../tips/learn/学习焯水.md)。"
+ },
+ {
+ "step": 18,
+ "description": "姜、蒜切沫。"
+ },
+ {
+ "step": 19,
+ "description": "葱切成 5mm 的小段。"
+ },
+ {
+ "step": 20,
+ "description": "将锅烧热,加入 15ml 油。"
+ },
+ {
+ "step": 21,
+ "description": "向锅内倒入准备好的腌肉,快速滑散至变白,盛出备用。"
+ },
+ {
+ "step": 22,
+ "description": "将锅烧热,加入 5ml 油。"
+ },
+ {
+ "step": 23,
+ "description": "倒入全部`葱`、`姜`、`蒜`、`豆瓣酱`。"
+ },
+ {
+ "step": 24,
+ "description": "倒入全部`胡萝卜`,翻炒 20s 后,放入青椒和木耳,翻炒 2 分钟。"
+ },
+ {
+ "step": 25,
+ "description": "倒入`炒过的肉`。快速翻炒均匀。注意不要炒超过 20 秒。"
+ },
+ {
+ "step": 26,
+ "description": "倒入`香汁`。快速翻炒均匀。注意不要炒超过 15 秒。"
+ },
+ {
+ "step": 27,
+ "description": "关火,盛盘。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-麻辣香锅",
+ "name": "麻辣香锅的做法",
+ "description": "# 麻辣香锅的做法\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/meat_dish/麻辣香锅.md",
+ "image_path": null,
+ "images": [],
+ "category": "荤菜",
+ "difficulty": 3,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "青菜(油菜、油麦菜、菠菜)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 青菜(油菜、油麦菜、菠菜)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "无骨肉(猪肉、牛肉、鸡肉、鱼丸、火腿肠)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 无骨肉(猪肉、牛肉、鸡肉、鱼丸、火腿肠)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "干豆腐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 干豆腐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "北京麻辣方便面",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 北京麻辣方便面",
+ "notes": "量未指定"
+ },
+ {
+ "name": "干辣椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 干辣椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "青菜共需",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 青菜共需 455 克,其中油菜、油麦菜、菠菜的比例按自己喜好分配即可",
+ "notes": "量未指定"
+ },
+ {
+ "name": "无骨肉共需",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 无骨肉共需 430 克,其中猪肉、牛肉、鸡肉、鱼丸、火腿肠的比例按自己喜好分配即可",
+ "notes": "量未指定"
+ },
+ {
+ "name": "干豆腐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 干豆腐 152 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "北京麻辣方便面",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 北京麻辣方便面 1 袋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "干辣椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 干辣椒 5 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "麻辣香锅调料",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 麻辣香锅调料 110 克",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-黄焖鸡",
+ "name": "黄焖鸡的做法",
+ "description": "# 黄焖鸡的做法\n\n黄焖鸡是一道十分下饭的美食,食材平平无奇又十分容易烹制,一学就会。\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/meat_dish/黄焖鸡.md",
+ "image_path": null,
+ "images": [],
+ "category": "荤菜",
+ "difficulty": 3,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "鸡腿",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡腿",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香菇(干香菇最佳)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香菇(干香菇最佳)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "青椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 青椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生姜片",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生姜片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "干辣椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 干辣椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白胡椒粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白胡椒粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酱油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酱油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "味精",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 味精",
+ "notes": "量未指定"
+ },
+ {
+ "name": "土豆(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 土豆(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡腿 = 两只",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡腿 = 两只",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香菇(干香菇最佳)=",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香菇(干香菇最佳)= 5 朵",
+ "notes": "量未指定"
+ },
+ {
+ "name": "青椒 = 两个",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 青椒 = 两个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生姜片 = 两片",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生姜片 = 两片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "干辣椒 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 干辣椒 = 5,6 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 = 10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒 = 10ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白胡椒粉 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白胡椒粉 = 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖 = 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酱油 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酱油 = 5ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "土豆 = 一个(可选,可使汤汁更粘稠)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 土豆 = 一个(可选,可使汤汁更粘稠)",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "鸡腿洗净,剁成**4cm**大小的块"
+ },
+ {
+ "step": 2,
+ "description": "生姜切片、干辣椒切成**小圈**"
+ },
+ {
+ "step": 3,
+ "description": "香菇切片,青椒切成细长的**马蹄状**,若为干香菇,洗净灰尘后泡一晚上并留香菇水备用"
+ },
+ {
+ "step": 4,
+ "description": "若有土豆,切为与鸡肉大小类似的**滚刀块**"
+ },
+ {
+ "step": 5,
+ "description": "炒糖色:锅里倒入底油,冷油时放入白糖(**有一定难度,新手可跳至鸡肉炒制并使用老抽替代**)"
+ },
+ {
+ "step": 6,
+ "description": "小火慢慢加热,待油温逐渐升高,白糖开始融化并变成较深的棕色(期间要不断搅拌,防止糊锅)"
+ },
+ {
+ "step": 7,
+ "description": "迅速倒入鸡块,转大火,快速翻炒!烹入料酒,继续翻炒片刻"
+ },
+ {
+ "step": 8,
+ "description": "将生姜片和干辣椒倒入"
+ },
+ {
+ "step": 9,
+ "description": "放入酱油,炒匀"
+ },
+ {
+ "step": 10,
+ "description": "倒入香菇水或清水,以能淹住鸡肉为准"
+ },
+ {
+ "step": 11,
+ "description": "倒入香菇片,白胡椒粉,盐,土豆"
+ },
+ {
+ "step": 12,
+ "description": "翻炒均匀后,盖上锅盖焖煮,转中小火**15——20分钟**,有条件可以转至砂锅"
+ },
+ {
+ "step": 13,
+ "description": "待鸡肉软烂,汤汁浓稠后(汤汁不要收的太干),最后放入青椒"
+ },
+ {
+ "step": 14,
+ "description": "放入味精,兜炒均匀后,关火!青椒基本断生即可,不要炒时间久了"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-黄瓜炒肉",
+ "name": "黄瓜炒肉的做法",
+ "description": "# 黄瓜炒肉的做法\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/meat_dish/黄瓜炒肉.md",
+ "image_path": null,
+ "images": [],
+ "category": "荤菜",
+ "difficulty": 3,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "黄瓜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黄瓜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "猪瘦肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 猪瘦肉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小米辣",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小米辣",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黄瓜 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黄瓜 = 100 克 * 份数",
+ "notes": "量未指定"
+ },
+ {
+ "name": "猪肉 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 猪肉 = 50 克 * 份数",
+ "notes": "量未指定"
+ },
+ {
+ "name": "油量 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 油量 = 50 克 * 份数",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐量 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐量 = 10 克 * 份数",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酱油 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酱油 = 5 克 * 份数",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜瓣 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜瓣 = 2 瓣 * 份数",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小米辣 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小米辣 = 1 根 * 份数",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "将猪瘦肉切片,放入碗中,倒入食用油 10 克,生抽,搅拌均匀,腌制 10 分钟"
+ },
+ {
+ "step": 2,
+ "description": "将黄瓜切去 5 厘米的头尾,剩余部分斜着切成 0.5 厘米的薄片"
+ },
+ {
+ "step": 3,
+ "description": "将黄瓜倒入碗中,撒上盐 8 克,搅拌均匀,腌制 5 分钟"
+ },
+ {
+ "step": 4,
+ "description": "将蒜瓣去皮,压扁,切成蒜末备用"
+ },
+ {
+ "step": 5,
+ "description": "将小米辣去丁切分成均匀 0.5 厘米的段状"
+ },
+ {
+ "step": 6,
+ "description": "热锅,倒油 40 克,等油温到冒烟,放入蒜蓉小米辣翻炒 5 次"
+ },
+ {
+ "step": 7,
+ "description": "放入腌制好的猪瘦肉,翻炒至肉熟变色"
+ },
+ {
+ "step": 8,
+ "description": "放入黄瓜,加入盐 2 克,大火翻炒均匀半分钟,出锅"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-农家一碗香-农家一碗香",
+ "name": "农家一碗香的做法",
+ "description": "# 农家一碗香的做法\n\n\n\n农家一碗香,是一道地道的湖南菜,里面主要食材有青椒、鸡蛋和猪肉。味道咸香下饭,而且这道菜烹饪简单,不需要特别的处理。\n\n农家一碗香是一道中等难度的菜品。预计备菜 7 分钟,烹饪 10 分钟,总计 17 分钟。\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/meat_dish/农家一碗香/农家一碗香.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/农家一碗香/农家一碗香成品.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/农家一碗香/农家一碗香成品.jpg"
+ ],
+ "category": "荤菜",
+ "difficulty": 3,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [],
+ "steps": [],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-冬瓜酿肉-冬瓜酿肉",
+ "name": "冬瓜酿肉的做法",
+ "description": "# 冬瓜酿肉的做法\n\n\n\n荤素搭配,鲜嫩爽滑,做法简单。一般 30 分钟。\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/meat_dish/冬瓜酿肉/冬瓜酿肉.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/冬瓜酿肉/冬瓜形状.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/冬瓜酿肉/冬瓜形状.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/冬瓜酿肉/冬瓜酿肉成品.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/冬瓜酿肉/卷肉.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/冬瓜酿肉/打鸡蛋.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/冬瓜酿肉/摆盘.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/冬瓜酿肉/腌制好的冬瓜.jpg"
+ ],
+ "category": "荤菜",
+ "difficulty": 4,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "冬瓜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冬瓜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "猪肉末",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 猪肉末",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱姜末",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱姜末",
+ "notes": "量未指定"
+ },
+ {
+ "name": "胡椒粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 胡椒粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "淀粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 淀粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "猪肉末",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 猪肉末 300g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋 1 个(可选,不习惯的人可能会有点腥)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冬瓜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冬瓜 200g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱花(一根,约",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱花(一根,约 20g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "胡椒粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 胡椒粉 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 10ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "淀粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 淀粉 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "水淀粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 水淀粉 25g(淀粉 25g,水 50ml)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱姜末(姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱姜末(姜 3-4 片约 30g, 取上面一根葱花中的葱白部分即可)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 20g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "冬瓜去皮,切成 25cm 长 3cm 厚的片"
+ },
+ {
+ "step": 2,
+ "description": "将切好的冬瓜放入碗中,放入 15g 盐,将冬瓜抹匀,放置 10 分钟"
+ },
+ {
+ "step": 3,
+ "description": "放置冬瓜的同时,换个碗放入肉末,葱姜末, 5g 盐,淀粉 5g,胡椒粉,生抽,胡椒粉"
+ },
+ {
+ "step": 4,
+ "description": "使用筷子在肉末中进行顺时针搅拌,搅拌到食材颜色没有明显对比(约 2 分钟)"
+ },
+ {
+ "step": 5,
+ "description": "将腌制好的冬瓜(会变软)使用清水洗三遍"
+ },
+ {
+ "step": 6,
+ "description": "拿出 1 片冬瓜片卷起来,并把肉塞进去"
+ },
+ {
+ "step": 7,
+ "description": "放入碟子中摆到碟子的边缘"
+ },
+ {
+ "step": 8,
+ "description": "打入 1 个鸡蛋到中间圆圈处"
+ },
+ {
+ "step": 9,
+ "description": "放入普通铁锅中水烧开后,蒸 15 分钟,盖上锅盖"
+ },
+ {
+ "step": 10,
+ "description": "开盖,取出蒸好的冬瓜酿肉"
+ },
+ {
+ "step": 11,
+ "description": "将冬瓜酿肉碟子的水倒入锅中,放入水淀粉,加入 50ml 清水倒入锅中烧开"
+ },
+ {
+ "step": 12,
+ "description": "淋到冬瓜酿肉上"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-凉拌鸡丝-凉拌鸡丝",
+ "name": "凉拌鸡丝的做法",
+ "description": "# 凉拌鸡丝的做法\n\n\n隔离期间的一道快手菜,少油低卡,制作简单,预计制作时间 30 分钟\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/meat_dish/凉拌鸡丝/凉拌鸡丝.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/凉拌鸡丝/凉拌鸡丝.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/凉拌鸡丝/凉拌鸡丝.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/凉拌鸡丝/凉拌鸡丝_撕.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/凉拌鸡丝/凉拌鸡丝_焯水.jpg"
+ ],
+ "category": "荤菜",
+ "difficulty": 3,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "鸡胸肉(常温冷冻均可)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡胸肉(常温冷冻均可)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "麻油(花椒油)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 麻油(花椒油)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香醋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香醋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "凉白开水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 凉白开水",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡胸肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡胸肉 200 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "麻油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 麻油 5 毫升",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 4 毫升",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香醋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香醋 4 毫升",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖 3 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 2 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜 20 克",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "姜切片,备用"
+ },
+ {
+ "step": 2,
+ "description": "锅中倒入 4 升水"
+ },
+ {
+ "step": 3,
+ "description": "加入鸡胸肉、姜片"
+ },
+ {
+ "step": 4,
+ "description": "倒入 20 毫升料酒"
+ },
+ {
+ "step": 5,
+ "description": "开大火不盖盖将水烧开"
+ },
+ {
+ "step": 6,
+ "description": "水开后转中火,用勺子将浮沫捞出"
+ },
+ {
+ "step": 7,
+ "description": "继续煮 **5-7** 分钟,如果是非冷冻肉煮 5 分钟,冷冻肉煮 7 分钟"
+ },
+ {
+ "step": 8,
+ "description": "鸡胸肉大小会影响成熟时间,用筷子插入鸡胸肉,如果能轻松插入,代表鸡肉熟了。如果不熟需延长煮制时间"
+ },
+ {
+ "step": 9,
+ "description": "用凉白开水冲泡鸡胸肉,使鸡胸肉降至室温"
+ },
+ {
+ "step": 10,
+ "description": "顺着鸡胸肉纹理将鸡胸肉撕成细丝"
+ },
+ {
+ "step": 11,
+ "description": "准备一个碗"
+ },
+ {
+ "step": 12,
+ "description": "碗中加入准备好的麻油、生抽、香醋、白糖、盐"
+ },
+ {
+ "step": 13,
+ "description": "搅拌料汁,使糖和盐尽量溶化"
+ },
+ {
+ "step": 14,
+ "description": "将料汁倒入鸡丝中,搅拌均匀"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-卤菜-卤菜",
+ "name": "卤菜的做法",
+ "description": "# 卤菜的做法\n\n卤菜是一道经典的中式卤味料理,富含蛋白质和多种维生素。肉质鲜嫩多汁,香气四溢,入味程度可根据浸泡时间自行调整。这道菜适合作为凉菜、下酒菜或搭配主食食用,卤水还可多次使用,越陈越香。\n本教程以卤牛肉为例,其他肉类同理。\n\n\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/meat_dish/卤菜/卤菜.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/卤菜/卤水.jpeg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/卤菜/卤水.jpeg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/卤菜/卤牛肉.jpeg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/卤菜/牛肉面.jpeg"
+ ],
+ "category": "荤菜",
+ "difficulty": 3,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "卤料包(超市即可购买)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 卤料包(超市即可购买)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黄豆酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黄豆酱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "豆瓣酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 豆瓣酱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蚝油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蚝油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "南腐乳",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 南腐乳",
+ "notes": "量未指定"
+ },
+ {
+ "name": "洋葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 洋葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生姜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "干辣椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 干辣椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖(最好是黄冰糖,用于熬糖色)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖(最好是黄冰糖,用于熬糖色)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "啤酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 啤酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "牛腱子(或其他肉类)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 牛腱子(或其他肉类)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "高压锅",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 高压锅",
+ "notes": "量未指定"
+ },
+ {
+ "name": "滤网",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 滤网",
+ "notes": "量未指定"
+ },
+ {
+ "name": "卤料包",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 卤料包 1 包(约 10g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黄豆酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黄豆酱 15ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "豆瓣酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 豆瓣酱 15ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蚝油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蚝油 15ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "南腐乳",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 南腐乳 15ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "洋葱 半个(约",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 洋葱 半个(约 100g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生姜 30g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大蒜 40g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "干辣椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 干辣椒 10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 120ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽 60ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 10-15g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖 30g(用于熬糖色)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "啤酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 啤酒 1 罐(330ml)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "牛腱子",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 牛腱子 500g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "清水 足量(需要没过所有肉类)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 清水 足量(需要没过所有肉类)",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "牛腱子提前浸泡在冷水中 3 小时以上,去除血水"
+ },
+ {
+ "step": 2,
+ "description": "准备糖色:锅中加入 30g 白糖,小火加热至糖完全融化并呈现棕褐色,加入 150ml 热水,搅拌均匀备用"
+ },
+ {
+ "step": 3,
+ "description": "将洋葱切块,生姜和大蒜拍碎,干辣椒掰断备用"
+ },
+ {
+ "step": 4,
+ "description": "在锅中加入足量的水,放入卤料包、洋葱、生姜、大蒜、干辣椒,大火烧开"
+ },
+ {
+ "step": 5,
+ "description": "加入黄豆酱、豆瓣酱、蚝油和南腐乳各 15ml,搅拌均匀"
+ },
+ {
+ "step": 6,
+ "description": "倒入准备好的糖色,混合均匀"
+ },
+ {
+ "step": 7,
+ "description": "加入生抽 120ml 和老抽 60ml,搅拌均匀"
+ },
+ {
+ "step": 8,
+ "description": "加入 10-15g 盐调味"
+ },
+ {
+ "step": 9,
+ "description": "倒入 1 罐啤酒(330ml),再次烧开"
+ },
+ {
+ "step": 10,
+ "description": "牛腱子放入锅中焯水 2-3 分钟,捞出并用热水冲洗干净表面的浮沫"
+ },
+ {
+ "step": 11,
+ "description": "将焯水后的牛腱子放入已烧开的卤水中,确保卤水没过所有肉类"
+ },
+ {
+ "step": 12,
+ "description": "盖上高压锅盖,上汽后继续烹饪 25-30 分钟"
+ },
+ {
+ "step": 13,
+ "description": "烹饪完成后,不要开盖保温,自然冷却并浸泡一晚上(这样会更入味)"
+ },
+ {
+ "step": 14,
+ "description": "将卤好的肉取出放入冰箱冷藏,使其成型"
+ },
+ {
+ "step": 15,
+ "description": "食用前取出切片,可直接食用或凉拌"
+ },
+ {
+ "step": 16,
+ "description": "卤水重复使用方法:每次卤完肉后,将卤水过滤,去除所有固体内容物,重新烧开杀菌,冷却后可冷藏或冷冻保存。使用时按原配方比例重新添加调味料。(加水量根据卤水使用情况而定)"
+ },
+ {
+ "step": 17,
+ "description": "卤水保存得当可以使用很长时间,且越老越香。"
+ },
+ {
+ "step": 18,
+ "description": "**重要提示**:如卤制素菜,必须另取一部分卤水,单独使用,并且卤制完素菜的卤水不可重复使用。"
+ },
+ {
+ "step": 19,
+ "description": "将蒜末、葱花、白芝麻、辣椒粉按 1:1:1:1 的比例混合,依个人口味加小米辣,热植物油中加入少量芝麻油或藤椒油,分次泼在调料上,再加入生抽、醋、蚝油各 10ml,5ml 糖,味精/鸡精,最后 15ml 卤汤混合均匀。"
+ },
+ {
+ "step": 20,
+ "description": "凉拌时可搭配拍黄瓜、木耳、油炸花生米、香菜等配菜。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-口水鸡-口水鸡",
+ "name": "口水鸡的做法",
+ "description": "# 口水鸡的做法\n\n\n\n口水鸡(凉菜),炎炎夏日里,热菜难以入口,但又嗜肉如命,\n除了口水鸡,实在想不出更好的适合在夏天吃的肉菜了。\n被红油包裹的鸡肉,红艳鲜亮,冰爽 Q 弹,鲜美而不腻。夏日米饭杀手当之无愧!\n(注:口水鸡做法多样,欢迎补充)\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/meat_dish/口水鸡/口水鸡.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/口水鸡/口水鸡.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/口水鸡/口水鸡.jpg"
+ ],
+ "category": "荤菜",
+ "difficulty": 3,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "半只鸡",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 半只鸡",
+ "notes": "量未指定"
+ },
+ {
+ "name": "辣椒粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 辣椒粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "花椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 花椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "花生",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 花生",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱姜蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱姜蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "花椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 花椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "醋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 醋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "味精",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 味精",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 20ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡 半只(500g)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡 半只(500g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "辣椒粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 辣椒粉 20g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "花椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 花椒 30 颗(20g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "花生",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 花生 10 颗(30g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小葱 2 颗(50g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜 1 小块(20g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜 2 个 (10g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 5ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "醋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 醋 5ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "味精",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 味精 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "花椒粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 花椒粉 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香菜 5g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "姜切片,1 颗小葱,15 颗花椒备用"
+ },
+ {
+ "step": 2,
+ "description": "鸡肉洗干净,放入锅中,清水没过鸡肉,放入姜片、小葱和花椒,开大火烧开。"
+ },
+ {
+ "step": 3,
+ "description": "大火烧开后,转中小火 20 分钟关火"
+ },
+ {
+ "step": 4,
+ "description": "取出鸡肉,放入冰水中,直至冰凉"
+ },
+ {
+ "step": 5,
+ "description": "取出鸡肉,切块摆盘子中,备用"
+ },
+ {
+ "step": 6,
+ "description": "小火把锅烧热,导入花生,烘烤至表皮爆裂。(注意随时翻动,不要糊了)"
+ },
+ {
+ "step": 7,
+ "description": "一颗葱切成段,蒜拍成末,花椒 15 颗,花生去皮切碎。"
+ },
+ {
+ "step": 8,
+ "description": "锅内导入油烧热后,放入葱段,花椒和一半蒜末,炒香"
+ },
+ {
+ "step": 9,
+ "description": "炒至油温 8 成热,关火,滤出热油"
+ },
+ {
+ "step": 10,
+ "description": "将热油倒入放辣椒粉的碗中,搅拌,并滤出红油"
+ },
+ {
+ "step": 11,
+ "description": "红油中放入剩余蒜末、生抽、醋、盐、味精、糖、香油、花椒粉。拌匀放凉"
+ },
+ {
+ "step": 12,
+ "description": "在鸡肉上撒上花生碎,把红油淋到切好的鸡肉上,撒上香菜。成盘"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-台式卤肉饭-台式卤肉饭",
+ "name": "台式卤肉饭的做法",
+ "description": "# 台式卤肉饭的做法\n\n糖和脂肪是人类快乐的源泉,富含这二者的台式卤肉饭每一口都能带来直击灵魂的满足感。\n\n本文提供一种操作简单但风味不减的台式卤肉饭做法,预计制作时间 1.5 小时(0.5 小时操作,1 小时炖煮)。\n\n厨房小白可上手。\n\n预估烹饪难度:★★★★★",
+ "source_path": "dishes/meat_dish/台式卤肉饭/台式卤肉饭.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/台式卤肉饭/1.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/台式卤肉饭/1.jpg"
+ ],
+ "category": "荤菜",
+ "difficulty": 5,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "红葱头(火葱)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 红葱头(火葱)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "带皮五花肉 (可用猪绞肉代替)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 带皮五花肉 (可用猪绞肉代替)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽酱油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽酱油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "米酒(可用料酒代替)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 米酒(可用料酒代替)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香叶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香叶",
+ "notes": "量未指定"
+ },
+ {
+ "name": "八角",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 八角",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冰糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冰糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白胡椒粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白胡椒粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "五香粉(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 五香粉(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "米饭",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 米饭",
+ "notes": "量未指定"
+ },
+ {
+ "name": "红葱头",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 红葱头 25 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "带皮五花肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 带皮五花肉 500g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋 4 个(可任意修改鸡蛋个数)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 15 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽酱油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽酱油 75 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "米酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 米酒 10 + 25 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大蒜 25 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香叶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香叶 2 片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "八角",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 八角 1 颗",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冰糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冰糖 20 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白胡椒粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白胡椒粉 6 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "五香粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 五香粉 6 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "米饭 (根据个人食量决定)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 米饭 (根据个人食量决定)",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "带皮五花肉切成 $0.7cm(长)\\times 0.7cm(宽) \\times 2.5cm(高)$ 的细长条"
+ },
+ {
+ "step": 2,
+ "description": "红葱头、大蒜切末备用"
+ },
+ {
+ "step": 3,
+ "description": "鸡蛋煮熟剥壳,并用刀划破蛋白(便于入味),备用。"
+ },
+ {
+ "step": 4,
+ "description": "**大火**热锅,锅内放入 15 ml 食用油,让油滑满锅底即可。"
+ },
+ {
+ "step": 5,
+ "description": "放入五花肉条,翻炒至肉色稍微变白,沿锅边淋入米酒 10ml 。继续翻炒至五花肉不再出油。"
+ },
+ {
+ "step": 6,
+ "description": "将切好的红葱头加入锅中,翻炒 1 分钟爆出油葱香味。"
+ },
+ {
+ "step": 7,
+ "description": "将切好的红葱头加入锅中,翻炒 30 秒。"
+ },
+ {
+ "step": 8,
+ "description": "把猪肉推到旁边,放入冰糖加热到融化冒泡变成焦糖,再把猪肉一起翻拌,让焦糖均匀附着。"
+ },
+ {
+ "step": 9,
+ "description": "加入生抽炒出香气。"
+ },
+ {
+ "step": 10,
+ "description": "呛入米酒 25 ml ,水加到淹过猪肉,加入白胡椒粉、五香粉、八角、香叶、水煮蛋,沸腾后转小火卤 1 小时。"
+ },
+ {
+ "step": 11,
+ "description": "1 小时后,开大火收汁直到酱汁浓稠,呈现有光泽的琥珀色,即完成。"
+ },
+ {
+ "step": 12,
+ "description": "炖煮结束后,乘一碗米饭,将软烂的卤肉浇在米饭上,并加上卤蛋,开始享用。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-咖喱肥牛-咖喱肥牛",
+ "name": "咖喱肥牛的做法",
+ "description": "# 咖喱肥牛的做法\n\n\n\n咖喱肥牛美味营养并且下饭,吃多了炒炸菜后再吃个咖喱肥牛相当美滋滋。\n\n适合在家吃或者做成便当带去公司吃(微波炉加热也不会有太大味道~)。\n\n并且所需材料少,容易购买,新手一般 40 分钟即可出锅。\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/meat_dish/咖喱肥牛/咖喱肥牛.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/咖喱肥牛/咖喱肥牛.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/咖喱肥牛/咖喱肥牛.jpg"
+ ],
+ "category": "荤菜",
+ "difficulty": 4,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "香叶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香叶",
+ "notes": "量未指定"
+ },
+ {
+ "name": "纯牛奶(推荐卫岗鲜奶)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 纯牛奶(推荐卫岗鲜奶)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "洋葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 洋葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "胡萝卜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 胡萝卜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "土豆",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 土豆",
+ "notes": "量未指定"
+ },
+ {
+ "name": "肥牛卷",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 肥牛卷",
+ "notes": "量未指定"
+ },
+ {
+ "name": "咖喱块",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 咖喱块",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香叶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香叶 1 片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "纯牛奶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 纯牛奶 50ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "洋葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 洋葱 100g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "胡萝卜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 胡萝卜 150g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "土豆",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 土豆 200g (挺饱腹的,酌情添加)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "肥牛卷",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 肥牛卷 300g (喜欢吃肉就多来点)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "咖喱块",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 咖喱块 2 块 (大约 100g)",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "洋葱切成条状、胡萝卜以及土豆切成块状,备用"
+ },
+ {
+ "step": 2,
+ "description": "烧一锅开水,水沸时将肥牛卷下锅,捞出血沫后放在一边沥水,备用"
+ },
+ {
+ "step": 3,
+ "description": "热锅,锅内放入 10ml - 15ml 食用油,**等待 10 秒让油温升高**"
+ },
+ {
+ "step": 4,
+ "description": "放入洋葱,翻炒至洋葱变软变透明"
+ },
+ {
+ "step": 5,
+ "description": "放入土豆以及胡萝卜**翻炒 2 分钟**"
+ },
+ {
+ "step": 6,
+ "description": "加入冷水至淹没所有食材即可"
+ },
+ {
+ "step": 7,
+ "description": "将香叶、咖喱块投入锅中,盖上锅盖,**待水沸腾后将火调小然后等待直至土豆块以及胡萝卜块炖至软烂(可用筷子确认)**"
+ },
+ {
+ "step": 8,
+ "description": "加入肥牛卷以及牛奶,盖上锅盖再小火煮 2-3 分钟即可出锅(用勺子搅拌食材,注意力度,避免肥牛卷破碎)"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-啤酒鸭-啤酒鸭",
+ "name": "啤酒鸭的做法",
+ "description": "# 啤酒鸭的做法\n\n\n\n啤酒鸭不仅入口鲜香,还带有一股啤酒清香。肉久吃不腻,汤久涮而不淡。风味独特,具有热而不浮,香而不腻的独特口味让人赞口不绝。一般初学者需要 1 小时即可完成。\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/meat_dish/啤酒鸭/啤酒鸭.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/啤酒鸭/啤酒鸭.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/啤酒鸭/啤酒鸭.jpg"
+ ],
+ "category": "荤菜",
+ "difficulty": 4,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "鸭肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸭肉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "啤酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 啤酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽酱油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽酱油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽酱油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽酱油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冰糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冰糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "干辣椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 干辣椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡精",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡精",
+ "notes": "量未指定"
+ },
+ {
+ "name": "丁香",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 丁香",
+ "notes": "量未指定"
+ },
+ {
+ "name": "八角",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 八角",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香叶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香叶",
+ "notes": "量未指定"
+ },
+ {
+ "name": "豆瓣酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 豆瓣酱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸭肉半只(约",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸭肉半只(约 1kg)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "啤酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 啤酒 800ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽酱油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽酱油 10-15ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽酱油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽酱油 5-10ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜 5 片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜头",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜头 12 瓣",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冰糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冰糖 10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "干辣椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 干辣椒 5 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒 30ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 8g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡精",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡精 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "丁香",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 丁香 4 颗",
+ "notes": "量未指定"
+ },
+ {
+ "name": "八角",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 八角 3 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香叶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香叶 3 片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "豆瓣酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 豆瓣酱 20g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "把鸭子切成 3 cm 小块,鸭肉冷水下锅,加姜片、料酒,焯一遍水,盛出沥干水分,备用。"
+ },
+ {
+ "step": 2,
+ "description": "炒锅烧热,放入约 100ml 食用油,大火待油烧开,鸭肉入锅翻炒至上色。"
+ },
+ {
+ "step": 3,
+ "description": "待鸭肉完全变色(肉眼可见泛白),将鸭肉拨到锅的一边,倒入豆瓣酱和糖,小火翻炒出香味和糖色。"
+ },
+ {
+ "step": 4,
+ "description": "加入丁香、八角、香叶、干辣椒、生抽、老抽、蒜,翻炒出香味。"
+ },
+ {
+ "step": 5,
+ "description": "倒入啤酒,没过鸭肉,加入盐、鸡精,然后中火将鸭子烧 30 分钟(牙口不好的话可以再多烧 5 分钟)。"
+ },
+ {
+ "step": 6,
+ "description": "出锅盛盘,上桌食用。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-回锅肉-回锅肉",
+ "name": "回锅肉的做法",
+ "description": "# 回锅肉的做法\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/meat_dish/回锅肉/回锅肉.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/回锅肉/1.jpeg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/回锅肉/1.jpeg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/回锅肉/2.jpeg"
+ ],
+ "category": "荤菜",
+ "difficulty": 4,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "五花肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 五花肉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生姜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "青红椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 青红椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜苗",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜苗",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "豆瓣酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 豆瓣酱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽酱油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽酱油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "味精",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 味精",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小葱 2 棵",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生姜 10-40g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "青红椒(根据受辣程度选择,",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 青红椒(根据受辣程度选择, 0-30g)*注:不建议使用肉厚的菜椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜苗",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜苗 1 把",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒 5ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "豆瓣酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 豆瓣酱 10ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "味精",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 味精 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 5ml",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "锅烧热,用手将五花肉紧紧压在锅上炙皮"
+ },
+ {
+ "step": 2,
+ "description": "用钢丝球把皮刷干净,至黑色部分碳化部分被完全去除,不刷干净会有苦味"
+ },
+ {
+ "step": 3,
+ "description": "将五花肉放入锅中,放入能淹没五花肉的冷水,放入生姜片、料酒和小葱(取 2 棵小葱打结)"
+ },
+ {
+ "step": 4,
+ "description": "开大火煮,水开后撇去浮沫,继续煮 15 分钟,煮至瘦肉部分可以用筷子轻松刺穿"
+ },
+ {
+ "step": 5,
+ "description": "青红椒切圈"
+ },
+ {
+ "step": 6,
+ "description": "蒜苗切段"
+ },
+ {
+ "step": 7,
+ "description": "生姜切小薄片"
+ },
+ {
+ "step": 8,
+ "description": "将 5ml 豆瓣酱和 5ml 生抽提前混合"
+ },
+ {
+ "step": 9,
+ "description": "将煮熟的五花肉捞出放入冷水晾凉"
+ },
+ {
+ "step": 10,
+ "description": "擦干五花肉的水,切成上肥下瘦的 2mm 的薄片(切厚了口感不好,而且很油)"
+ },
+ {
+ "step": 11,
+ "description": "选用冰冻五花肉常量放置 0.5 小时 或者鲜五花肉放冰箱冷藏 1 个小时,切成 2-5 mm 薄片"
+ },
+ {
+ "step": 12,
+ "description": "开中火,辣椒放过锅中干煸 30-45 秒后取出"
+ },
+ {
+ "step": 13,
+ "description": "锅烧热,放入一层底油滑锅,放入姜片煸炒 15 秒"
+ },
+ {
+ "step": 14,
+ "description": "倒入五花肉,间隔 10 S 翻炒一次,待五花肉出现焦黄色(翻炒时间越久五花肉口感越硬)"
+ },
+ {
+ "step": 15,
+ "description": "倒入之前干煸过的辣椒、10ml 豆瓣酱,生抽调味,继续翻炒 60 秒"
+ },
+ {
+ "step": 16,
+ "description": "倒入切成段的蒜苗翻炒 10 秒"
+ },
+ {
+ "step": 17,
+ "description": "出锅"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-土豆炖排骨-土豆炖排骨",
+ "name": "土豆炖排骨的做法",
+ "description": "# 土豆炖排骨的做法\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/meat_dish/土豆炖排骨/土豆炖排骨.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/土豆炖排骨/排骨1.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/土豆炖排骨/排骨1.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/土豆炖排骨/排骨2.jpg"
+ ],
+ "category": "荤菜",
+ "difficulty": 3,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "肋排",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 肋排",
+ "notes": "量未指定"
+ },
+ {
+ "name": "土豆",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 土豆",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "干辣椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 干辣椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "八角",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 八角",
+ "notes": "量未指定"
+ },
+ {
+ "name": "花椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 花椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "桂皮",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 桂皮",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蚝油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蚝油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黄豆酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黄豆酱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "肋排 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 肋排 = 750g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "土豆 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 土豆 = 300g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜 = 30g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小葱 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小葱 = 25g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒 = 25g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖 = 10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "干辣椒 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 干辣椒 = 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "八角 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 八角 = 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "花椒 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 花椒 = 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "桂皮 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 桂皮 = 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 = 10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽 = 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蚝油 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蚝油 = 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黄豆酱 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黄豆酱 = 5g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "土豆两个滚刀切片,姜片切片"
+ },
+ {
+ "step": 2,
+ "description": "排骨 750g 冷水下锅,加入姜片、葱段、料酒焯水 2 分钟,焯干水后捞出清洗干净(一定要用热水,不能用冷水)"
+ },
+ {
+ "step": 3,
+ "description": "热锅凉油,将白糖倒入锅中,翻炒至融化为焦糖色"
+ },
+ {
+ "step": 4,
+ "description": "加入排骨煎至两面金黄,让排骨裹满焦糖"
+ },
+ {
+ "step": 5,
+ "description": "加入干辣椒、八角、花椒、桂皮、姜片(建议买超市的香料包)、10ml 生抽、5ml 老抽、5ml 料酒、5ml 蚝油、5ml 黄豆酱"
+ },
+ {
+ "step": 6,
+ "description": "大火翻炒均匀后加入 700ml 开水,大火烧开后转小火焖煮 1 小时"
+ },
+ {
+ "step": 7,
+ "description": "最后加入土豆煮 10 分钟就可以出锅啦(喜欢吃青红椒的也可以按自己喜好加入)"
+ },
+ {
+ "step": 8,
+ "description": ""
+ },
+ {
+ "step": 9,
+ "description": ""
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-奶酪培根通心粉-奶酪培根通心粉",
+ "name": "奶酪培根通心粉的做法",
+ "description": "# 奶酪培根通心粉的做法\n\n\n\n\n这是一道美味的奶酪培根通心粉(Mac and Cheese),适合四人享用。它结合了浓郁的奶酪和香脆的培根,简单易做,是一道受欢迎的美式家常菜。\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/meat_dish/奶酪培根通心粉/奶酪培根通心粉.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/奶酪培根通心粉/onepot.png",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/奶酪培根通心粉/onepot.png",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/奶酪培根通心粉/oven.jpg"
+ ],
+ "category": "荤菜",
+ "difficulty": 3,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "通心粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 通心粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "奶酪",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 奶酪",
+ "notes": "量未指定"
+ },
+ {
+ "name": "肉类",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 肉类",
+ "notes": "量未指定"
+ },
+ {
+ "name": "洋葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 洋葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黄油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黄油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "面粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 面粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "牛奶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 牛奶",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "通心粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 通心粉 100-125g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "奶酪",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 奶酪 40-55g,若要烘烤额外准备 25g, 条状",
+ "notes": "量未指定"
+ },
+ {
+ "name": "培根或其他肉类",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 培根或其他肉类 100-125g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "洋葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 洋葱 25g-40g 切成碎",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黄油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黄油 15g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "面粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 面粉 10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "牛奶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 牛奶 125ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大蒜半瓣,切碎",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大蒜半瓣,切碎",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "奶酪要磨成碎末"
+ },
+ {
+ "step": 2,
+ "description": "洋葱切成条状"
+ },
+ {
+ "step": 3,
+ "description": "通心粉用微咸的水煮 6 分钟"
+ },
+ {
+ "step": 4,
+ "description": "**中火**"
+ },
+ {
+ "step": 5,
+ "description": "锅中放入黄油,等待融化"
+ },
+ {
+ "step": 6,
+ "description": "加入洋葱"
+ },
+ {
+ "step": 7,
+ "description": "洋葱软化后加入大蒜"
+ },
+ {
+ "step": 8,
+ "description": "大蒜香味出来后,加入肉类,等待 5 秒"
+ },
+ {
+ "step": 9,
+ "description": "**小火**"
+ },
+ {
+ "step": 10,
+ "description": "分四次加入牛奶,每次搅拌 5 秒后再加下一次"
+ },
+ {
+ "step": 11,
+ "description": "加入面粉并充分搅拌"
+ },
+ {
+ "step": 12,
+ "description": "加入奶酪并搅拌均匀"
+ },
+ {
+ "step": 13,
+ "description": "将通心粉和奶酪搅拌"
+ },
+ {
+ "step": 14,
+ "description": "如果不打算烘烤,可以直接吃了"
+ },
+ {
+ "step": 15,
+ "description": "**烘烤:**"
+ },
+ {
+ "step": 16,
+ "description": "预热烤箱至 180°C"
+ },
+ {
+ "step": 17,
+ "description": "将额外的 50g 芝士铺在通心粉之上"
+ },
+ {
+ "step": 18,
+ "description": "等待烤箱预热至 180°C 后,将通心粉放入"
+ },
+ {
+ "step": 19,
+ "description": "烤至表面金黄,约 24 分钟"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-姜炒鸡-姜炒鸡",
+ "name": "姜炒鸡的做法",
+ "description": "# 姜炒鸡的做法\n\n\n\n姜炒鸡是一道湖南口味菜,下饭五颗星,食材平平无奇又十分容易烹制,一学就会。\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/meat_dish/姜炒鸡/姜炒鸡.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/姜炒鸡/姜炒鸡.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/姜炒鸡/姜炒鸡.jpg"
+ ],
+ "category": "荤菜",
+ "difficulty": 3,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "鸡",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生姜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "啤酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 啤酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小米椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小米椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "美人辣",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 美人辣",
+ "notes": "量未指定"
+ },
+ {
+ "name": "泡椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 泡椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡 = 半只(土鸡最好,预计",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡 = 半只(土鸡最好,预计 650g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 = 50ml(茶油最好,没有就用菜籽油)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生姜 = 半斤 (250g)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生姜 = 半斤 (250g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "啤酒 = 半瓶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 啤酒 = 半瓶 250ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 = 20ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽 = 10ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 = 3g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小米椒 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小米椒 = 0-5 个 (0-50g)(根据辣口味调整)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "美人辣 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 美人辣 = 0-5 个 (0-50g)(没有可以用小米椒代替)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "泡椒 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 泡椒 = 5 个 (50g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大蒜 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大蒜 = 3 头 (50g)",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "鸡尽量剁成 1cm 的小块,洗净后滤干,再放生抽腌和料酒腌制 30 分钟"
+ },
+ {
+ "step": 2,
+ "description": "大先热锅到微微冒烟,放入食用油,等 5 秒"
+ },
+ {
+ "step": 3,
+ "description": "下入姜片后转中火炒 30 秒,"
+ },
+ {
+ "step": 4,
+ "description": "下入鸡块翻炒 3 分钟,炒干水分,炒出鸡油"
+ },
+ {
+ "step": 5,
+ "description": "放入各种剁碎的辣椒和大蒜子,加盐和老抽继续翻炒 30 秒"
+ },
+ {
+ "step": 6,
+ "description": "倒入啤酒,中小火焖 2 分钟"
+ },
+ {
+ "step": 7,
+ "description": "大火收汁盛盘"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-姜葱捞鸡-姜葱捞鸡",
+ "name": "姜葱捞鸡的做法",
+ "description": "# 姜葱捞鸡的做法\n\n嫩滑爆汁,白饭杀手,简单易做,\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/meat_dish/姜葱捞鸡/姜葱捞鸡.md",
+ "image_path": null,
+ "images": [],
+ "category": "荤菜",
+ "difficulty": 3,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "鸡腿肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡腿肉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐焗鸡粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐焗鸡粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱,姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱,姜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡腿",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡腿 4 个, 约 400g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐焗鸡粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐焗鸡粉 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜 50g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱 1 根",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 糖 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 油 35ml",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "四个鸡腿清洗干净,放入碗中"
+ },
+ {
+ "step": 2,
+ "description": "碗中加入盐焗鸡粉和 5ml 油,搅拌均匀"
+ },
+ {
+ "step": 3,
+ "description": "让鸡腿静置腌制 15 分钟, 同时准备蒸锅并把水煮开"
+ },
+ {
+ "step": 4,
+ "description": "鸡腿腌制完成后, 放入水开后的蒸锅中,蒸制 20 分钟"
+ },
+ {
+ "step": 5,
+ "description": "将姜根据个人口味切成 1)姜蓉或 2)姜丝或 3)姜粒"
+ },
+ {
+ "step": 6,
+ "description": "将葱切成 0.5cm 小段"
+ },
+ {
+ "step": 7,
+ "description": "将葱姜放入蘸料碗,并加入盐和糖"
+ },
+ {
+ "step": 8,
+ "description": "将剩余的油倒入另一个锅中加热至六至七层热"
+ },
+ {
+ "step": 9,
+ "description": "将热油淋入葱姜碗中"
+ },
+ {
+ "step": 10,
+ "description": "鸡腿蒸好后将其撕碎成鸡丝,不需要特别细,大概 1cm 粗就可以"
+ },
+ {
+ "step": 11,
+ "description": "姜葱姜油淋在鸡丝上,搅拌均匀即可"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。",
+ "做法参考:[白饭杀手来啦 #姜葱捞鸡-哔哩哔哩](https://b23.tv/2trBdqJ)"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-宫保鸡丁-宫保鸡丁",
+ "name": "宫保鸡丁的做法",
+ "description": "# 宫保鸡丁的做法\n\n老派川菜的简单做法分享\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/meat_dish/宫保鸡丁/宫保鸡丁.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/宫保鸡丁/宫保鸡丁.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/宫保鸡丁/宫保鸡丁.jpg"
+ ],
+ "category": "荤菜",
+ "difficulty": 4,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "手枪腿(或者鸡胸脯肉)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 手枪腿(或者鸡胸脯肉)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "干辣椒(或者二荆条)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 干辣椒(或者二荆条)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "熟花生",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 熟花生",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽酱油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽酱油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香醋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香醋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡精",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡精",
+ "notes": "量未指定"
+ },
+ {
+ "name": "淀粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 淀粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "植物油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 植物油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "芝麻油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 芝麻油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "必须配料",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 必须配料",
+ "notes": "量未指定"
+ },
+ {
+ "name": "手枪腿(或者鸡胸脯肉) =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 手枪腿(或者鸡胸脯肉) = 1 支(约 350g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大葱 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大葱 = 1 根(约 180g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "熟花生 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 熟花生 = 150g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜片 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜片 = 10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "干辣椒(或者二荆条) =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 干辣椒(或者二荆条) = 10g(若选择二荆条,则需要大约 4 支)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽酱油 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽酱油 = 10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖 = 2g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 = 2g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "植物油 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 植物油 = 20g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "淀粉 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 淀粉 = 15g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒 = 15g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "进阶配料",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 进阶配料",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽酱油 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽酱油 = 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "花椒 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 花椒 = 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香醋 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香醋 = 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡精 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡精 = 2g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "芝麻油 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 芝麻油 = 10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "淀粉(用以勾芡) =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 淀粉(用以勾芡) = 10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "豆瓣酱 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 豆瓣酱 = 10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "可选配料",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 可选配料",
+ "notes": "量未指定"
+ },
+ {
+ "name": "莴笋 = 约",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 莴笋 = 约 250g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "油泼辣子 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 油泼辣子 = 5g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "手枪腿用剪刀去骨,鸡肉面用刀背拍打一遍,切条后切至 1.5cm 见方肉丁;泡于清水 10 分钟,捞出控干备用(若是鸡胸脯肉,则可以直接进行切丁以及之后的动作)"
+ },
+ {
+ "step": 2,
+ "description": "取大葱葱绿与姜片 5g 于碗中,倒入 50g 开水备用;葱白切 1.5cm 圆粒备用;取花生放入微波炉高火 5 分钟焙干备用"
+ },
+ {
+ "step": 3,
+ "description": "鸡丁中加入盐 2g,老抽酱油 5g,料酒 15g,淀粉 15g 搅拌均匀,至微微发干;缓慢加入部分葱姜水,搅拌鸡丁至粘手;保鲜膜密封,放入冰箱腌制 1 小时"
+ },
+ {
+ "step": 4,
+ "description": "干辣椒切段;起锅,大火烧热转小火;放入干辣椒焙干至微微发糊,捞起备用;花椒焙干至有香味,捞起备用"
+ },
+ {
+ "step": 5,
+ "description": "转大火,倒入 20g 植物油,7 成热(竹筷子起泡)下入鸡丁,煎至上面开始发白,用锅铲翻面,煎 30s 后翻炒均匀"
+ },
+ {
+ "step": 6,
+ "description": "下入葱粒翻炒,加入余下葱姜水不够 100g 再加一点清水(务必是热水);盖上锅盖,转中小火焖 2 分钟;"
+ },
+ {
+ "step": 7,
+ "description": "转大火,下入熟花生,干辣椒和花椒;加入鸡精 2g,香醋 5g,白糖 2g,翻炒均匀;"
+ },
+ {
+ "step": 8,
+ "description": "淀粉 10g 加 50g 清水调成水淀粉,加入锅中,翻炒均匀,收汁到自己想要的浓度"
+ },
+ {
+ "step": 9,
+ "description": "关火,淋入芝麻油 10g,即可出锅"
+ },
+ {
+ "step": 10,
+ "description": "莴笋去皮切至 1cm 见方的小块,备用;"
+ },
+ {
+ "step": 11,
+ "description": "二荆条切成 1cm 长段;"
+ },
+ {
+ "step": 12,
+ "description": "手枪腿用剪刀去骨,鸡肉面用刀背拍打一遍,切条后切至 1.5cm 见方肉丁;泡于清水 10 分钟,捞出控干备用(若是鸡胸脯肉,则可以直接进行切丁以及之后的动作);"
+ },
+ {
+ "step": 13,
+ "description": "取大葱葱绿与姜片 5g 于碗中,倒入 50g 开水备用;葱白切 1.5cm 圆粒备用"
+ },
+ {
+ "step": 14,
+ "description": "鸡丁中加入盐 2g,老抽酱油 5g,料酒 15g,淀粉 15g 搅拌均匀,至微微发干;缓慢加入部分葱姜水,搅拌鸡丁至粘手;保鲜膜密封,放入冰箱腌制 1 小时"
+ },
+ {
+ "step": 15,
+ "description": "转中火,倒入 20g 植物油,放入生花生翻炒至其表面微微焦糊,捞起花生但是油留在锅内;"
+ },
+ {
+ "step": 16,
+ "description": "继续加热,7 成热(竹筷子起泡)下入鸡丁,放入豆瓣酱,翻炒大概 1 分钟;"
+ },
+ {
+ "step": 17,
+ "description": "加入备好的莴笋丁,继续翻炒 1 分钟;"
+ },
+ {
+ "step": 18,
+ "description": "下入葱粒翻炒,加入余下葱姜水不够 100g 再加一点清水(务必是热水);加入二荆条段;盖上锅盖,转中小火焖 2 分钟;"
+ },
+ {
+ "step": 19,
+ "description": "转大火,下入先前捞起来备用的花生,花椒;加入鸡精 2g,香醋 5g,白糖 2g,翻炒均匀;"
+ },
+ {
+ "step": 20,
+ "description": "淀粉 10g 加 50g 清水调成水淀粉,加入锅中,翻炒均匀,收汁到自己想要的浓度"
+ },
+ {
+ "step": 21,
+ "description": "关火,淋入芝麻油 10g 与油泼辣子 5g 再翻炒 10s,即可出锅"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-小炒鸡肝-小炒鸡肝",
+ "name": "小炒鸡肝的做法",
+ "description": "# 小炒鸡肝的做法\n\n\n\n一道稍微麻烦的菜。\n\n适合喜欢吃肝的人,也可以用其他的动物的肝,但是鸡肝会更好吃。\n\n需要初学者具有一定的焯水技巧。\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/meat_dish/小炒鸡肝/小炒鸡肝.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/小炒鸡肝/成品.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/小炒鸡肝/成品.jpg"
+ ],
+ "category": "荤菜",
+ "difficulty": 3,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "生鸡肝",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生鸡肝",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜苗(蒜苗指的是:大蒜幼苗发育到一定时期的青苗。有些地方叫做青蒜,特别说明一下。)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜苗(蒜苗指的是:大蒜幼苗发育到一定时期的青苗。有些地方叫做青蒜,特别说明一下。)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大葱、姜、料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大葱、姜、料酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用盐、鸡精(味精)、五香粉(十三香)、胡椒粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用盐、鸡精(味精)、五香粉(十三香)、胡椒粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "烧烤料或孜然粉(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 烧烤料或孜然粉(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "芝麻(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 芝麻(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生鸡肝",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生鸡肝 5 个(约 800g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜苗(约",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜苗(约 200g,喜欢吃的可以多放)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大葱 150g(分成两份,一份切段 100g,一份切片 50g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜 120g(分成两份,一份切片 70g,一份切丁 50g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒 30ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用盐 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡精",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡精 5g(也可以是味精)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "五香粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 五香粉 5g(十三香)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "胡椒粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 胡椒粉 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "烧烤料或孜然粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 烧烤料或孜然粉 10g(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "芝麻",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 芝麻 5g (可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 30ml",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "鸡肝清洗,备用"
+ },
+ {
+ "step": 2,
+ "description": "蒜苗清洗,切段,备用"
+ },
+ {
+ "step": 3,
+ "description": "大葱清洗,取 100g 切段,取 50g 切片,备用"
+ },
+ {
+ "step": 4,
+ "description": "姜清晰,取 70g 切片, 取 50g 切丁,备用"
+ },
+ {
+ "step": 5,
+ "description": "第一步:焯水"
+ },
+ {
+ "step": 6,
+ "description": "第二步:炒制"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-小炒黄牛肉-小炒黄牛肉",
+ "name": "小炒黄牛肉的做法",
+ "description": "# 小炒黄牛肉的做法\n\n\n\n小炒黄牛肉是一道简单易做的湘菜。口味十分劲爆爽口。一般初学者只需要 1 小时即可完成\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/meat_dish/小炒黄牛肉/小炒黄牛肉.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/小炒黄牛肉/小炒黄牛肉.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/小炒黄牛肉/小炒黄牛肉.jpg"
+ ],
+ "category": "荤菜",
+ "difficulty": 4,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "牛里脊",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 牛里脊",
+ "notes": "量未指定"
+ },
+ {
+ "name": "芹菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 芹菜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小米椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小米椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "野山椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 野山椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香菜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "牛里脊",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 牛里脊 400g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "芹菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 芹菜 200g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小米椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小米椒 30g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "野山椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 野山椒 30g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香菜 30g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 15ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酱油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酱油 6ml",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "牛里脊切成不超过 3cm 宽,3mm 厚的薄片,倒入 6ml 酱油,用手抓匀备用"
+ },
+ {
+ "step": 2,
+ "description": "芹菜切成不超过 5cm 的小段,备用"
+ },
+ {
+ "step": 3,
+ "description": "小米椒切成丝状,备用"
+ },
+ {
+ "step": 4,
+ "description": "野山椒切成颗粒,备用"
+ },
+ {
+ "step": 5,
+ "description": "香菜切成成不超过 3cm 的小段,备用"
+ },
+ {
+ "step": 6,
+ "description": "热锅,锅内放入 15ml 食用油,大火等待 30 秒让油温升高"
+ },
+ {
+ "step": 7,
+ "description": "放入小米椒和野山椒爆香"
+ },
+ {
+ "step": 8,
+ "description": "放入牛里脊和芹菜,然后**大火翻炒 1 分钟**"
+ },
+ {
+ "step": 9,
+ "description": "关火,撒上香菜,盛盘"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-尖叫牛蛙-尖叫牛蛙",
+ "name": "尖叫牛蛙的做法",
+ "description": "# 尖叫牛蛙的做法\n\n\n\n尖叫牛蛙是一道容易完成的菜。一般初学者只需要 1-2 小时即可完成。该菜品味道鲜美之外,还具有开胃功效,非常适宜食欲不佳的时候做,老少皆宜。(能吃辣最好)\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/meat_dish/尖叫牛蛙/尖叫牛蛙.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/尖叫牛蛙/尖叫牛蛙.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/尖叫牛蛙/尖叫牛蛙.jpg"
+ ],
+ "category": "荤菜",
+ "difficulty": 4,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "牛蛙肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 牛蛙肉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "泡姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 泡姜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "泡椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 泡椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "野山椒(也可以用干红辣椒替代)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 野山椒(也可以用干红辣椒替代)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "青红辣椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 青红辣椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "豆瓣酱(推荐郫县豆瓣)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 豆瓣酱(推荐郫县豆瓣)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐巴",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐巴",
+ "notes": "量未指定"
+ },
+ {
+ "name": "胡椒粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 胡椒粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生粉(干淀粉也可)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生粉(干淀粉也可)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "啤酒(推荐雪花)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 啤酒(推荐雪花)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "藤椒油(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 藤椒油(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "猪油(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 猪油(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱花",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱花",
+ "notes": "量未指定"
+ },
+ {
+ "name": "牛蛙肉块",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 牛蛙肉块 800g (买 3 斤活蛙,建议挑小个头的,肉质鲜嫩)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "泡姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 泡姜 20-30 克 (看个人口味,喜欢重口一点可以多放)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "泡椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 泡椒 5-10 克 (看个人吃辣的承受能力,不能吃辣的建议减半为 2.5 克,微微辣)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "野山椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 野山椒 10 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "青红辣椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 青红辣椒 20 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大蒜 30-50 克 (可以根据口味偏好调整,最好不低于 30 克)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "豆瓣酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 豆瓣酱 20-30 克 (重口选 30,想吃清淡点 20 克。PS 这菜不可能太清淡,哈哈)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐巴",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐巴 15 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "胡椒粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 胡椒粉 10 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "啤酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 啤酒 400-500ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒 10ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "藤椒油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 藤椒油 5-10ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生粉 30 克 (干淀粉可以平替)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "猪油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 猪油 20ml(没有猪油也可,用食用油替代)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 200ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱花",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱花 5 克",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "牛蛙肉洗净后控干水分,加入 10 克以上的盐巴和 50ml 以上的啤酒,用手抓 5 分钟,去除牛蛙肉的腥味"
+ },
+ {
+ "step": 2,
+ "description": "然后对着清水冲洗,直至不再流出血水和杂质,控干水分,放到合适的器皿中,准备腌制"
+ },
+ {
+ "step": 3,
+ "description": "加入 5 克盐,30 克生粉,10ml 料酒,5 克胡椒粉,用手抓均匀,腌制 5-10 分钟"
+ },
+ {
+ "step": 4,
+ "description": "将泡姜 泡椒 野山椒 切丝或者片(根据自己刀工选择),青红辣椒切成圈圈 大蒜拨开即可"
+ },
+ {
+ "step": 5,
+ "description": "起锅烧热,加入 200ml 食用油(锅底比较平的可以再加 100ml),烧至 6 成油温(有小气泡出现),将腌制好的牛蛙倒入,快速过油炸制,10 秒钟后捞出(不能超时太多,否则会导致蛙肉老柴)"
+ },
+ {
+ "step": 6,
+ "description": "捞出蛙肉后,控油,并将锅中的热油倒出到碗中,保留 30ml,加入 20ml 猪油(如果没有,则在锅中保留总共 50ml 食用油)"
+ },
+ {
+ "step": 7,
+ "description": "待油温 6 成热,加入泡姜、泡椒、野山椒、大蒜,超出香味,加入豆瓣酱 20 克,中火翻炒至出红油(时间控制在 30 秒),倒入 400ml 啤酒,"
+ },
+ {
+ "step": 8,
+ "description": "然后倒入炸过的牛蛙肉,用勺子推着翻,不要用力搅拌,加入 5 克胡椒粉,加入 5ml 藤椒油,中火慢焖 3 分钟"
+ },
+ {
+ "step": 9,
+ "description": "加大火力,大火收汁半分钟,加入青红辣椒圈,再煮 10 秒准备起锅"
+ },
+ {
+ "step": 10,
+ "description": "盛到盆里,撒上葱花,可以开动了!"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-巴基斯坦牛肉咖喱-巴基斯坦牛肉咖喱",
+ "name": "巴基斯坦牛肉咖喱的做法",
+ "description": "# 巴基斯坦牛肉咖喱的做法\n\n\n\nAchar gosht(巴基斯坦牛肉咖喱)是一道来自巴基斯坦的特色咖喱菜品。这道菜融合了咖喱的香浓和牛肉的软糯口感,风味独特,偏辣口。它富含优质蛋白质和多种维生素,营养价值丰富。制作过程需要 2.5 小时,步骤并不复杂,是一道适合在周末慢慢烹饪的美味佳肴。\n\n预估烹饪难度:★★★★★",
+ "source_path": "dishes/meat_dish/巴基斯坦牛肉咖喱/巴基斯坦牛肉咖喱.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/巴基斯坦牛肉咖喱/倒入番茄蓉.png",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/巴基斯坦牛肉咖喱/倒入番茄蓉.png",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/巴基斯坦牛肉咖喱/巴基斯坦牛肉咖喱.png",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/巴基斯坦牛肉咖喱/油.png",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/巴基斯坦牛肉咖喱/牛肉.png",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/巴基斯坦牛肉咖喱/番茄蓉.png",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/巴基斯坦牛肉咖喱/红.png"
+ ],
+ "category": "荤菜",
+ "difficulty": 5,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "普通的炒锅",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 普通的炒锅",
+ "notes": "量未指定"
+ },
+ {
+ "name": "电饭煲/电炖锅",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 电饭煲/电炖锅",
+ "notes": "量未指定"
+ },
+ {
+ "name": "Masala 粉(品牌可选 Shan)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- Masala 粉(品牌可选 Shan)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "牛肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 牛肉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "番茄",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 番茄",
+ "notes": "量未指定"
+ },
+ {
+ "name": "螺丝椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 螺丝椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "原味酸奶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 原味酸奶",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "番茄🍅",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 番茄🍅 4 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "螺丝椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 螺丝椒 2 个(大个的)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "原味酸奶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 原味酸奶 1 盒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "Masala 粉一包",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- Masala 粉一包 50g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜粉 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜粉 5g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "全部螺丝椒切成段状,备用"
+ },
+ {
+ "step": 2,
+ "description": "全部番茄打成番茄蓉,备用"
+ },
+ {
+ "step": 3,
+ "description": "牛肉切成 2cm 的小块,洗净备用"
+ },
+ {
+ "step": 4,
+ "description": "炒锅中倒入一层油(用来防止番茄蓉沸腾蒸发)"
+ },
+ {
+ "step": 5,
+ "description": "倒入番茄蓉,持续搅拌 2-3 分钟,等待它越变越红"
+ },
+ {
+ "step": 6,
+ "description": "加入 5g 蒜粉,5g 姜粉和 1 包 50g 的 Masala 粉,搅拌均匀"
+ },
+ {
+ "step": 7,
+ "description": "加入牛肉和螺丝椒段,搅拌均匀"
+ },
+ {
+ "step": 8,
+ "description": "加入 1 盒酸奶(为了让整个酱汁变得粘稠),搅拌均匀"
+ },
+ {
+ "step": 9,
+ "description": "将整锅材料转移到电饭煲/电炖锅,并加入 250 ml 的水,开启炖肉/慢炖档,设定时间 2-3 个小时"
+ },
+ {
+ "step": 10,
+ "description": "等待完成,开锅检查牛肉软糯,就可以吃了"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-干煸仔鸡-干煸仔鸡",
+ "name": "干煸仔鸡的做法",
+ "description": "# 干煸仔鸡的做法\n\n\n\n干煸仔鸡是一道甜辣口味的川菜,是北京大学食堂赵春月厨师长研发的美食,广受师生喜爱。赵厨师长已将菜谱公开,方便大家自己动手制作,疫情居家下饭必备!\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/meat_dish/干煸仔鸡/干煸仔鸡.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/干煸仔鸡/干煸仔鸡成品.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/干煸仔鸡/干煸仔鸡成品.jpg"
+ ],
+ "category": "荤菜",
+ "difficulty": 4,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "鸡腿",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡腿",
+ "notes": "量未指定"
+ },
+ {
+ "name": "土豆",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 土豆",
+ "notes": "量未指定"
+ },
+ {
+ "name": "青椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 青椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "胡椒粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 胡椒粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "淀粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 淀粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "郫县红油豆瓣酱(注意区分,不是那种棕黄色的豆瓣酱)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 郫县红油豆瓣酱(注意区分,不是那种棕黄色的豆瓣酱)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "花椒碎",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 花椒碎",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡腿肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡腿肉 400g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "土豆",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 土豆 200g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "青椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 青椒 60g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜瓣",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜瓣 10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 3g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡精",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡精 3g (可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "胡椒粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 胡椒粉 2g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽 2g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "淀粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 淀粉 20g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "郫县红油豆瓣酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 郫县红油豆瓣酱 40g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖 30g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "花椒碎",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 花椒碎 2g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "鸡腿去骨(如使用鸡腿排可忽略此步骤),鸡腿肉用刀背砸一砸,切成 2cm 的块。"
+ },
+ {
+ "step": 2,
+ "description": "鸡腿肉中加入盐、鸡精(可选)、胡椒粉、生抽、老抽、料酒,抓拌至粘手时加入淀粉拌匀,加入食用油防止粘连,腌制 30 分钟。"
+ },
+ {
+ "step": 3,
+ "description": "土豆去皮,切成 2cm 的块,沸水煮 5 分钟后捞出,控干水分,防止油炸时爆锅。"
+ },
+ {
+ "step": 4,
+ "description": "青椒去籽,切成 2cm 小片,放在笊篱中备用。"
+ },
+ {
+ "step": 5,
+ "description": "锅中加入宽油(根据锅的形状,能没过食材即可),油温烧至 180℃ 时,下入土豆块炸 3 分钟后捞出。"
+ },
+ {
+ "step": 6,
+ "description": "待油温再次升高到 180℃时,下入鸡块炸 2 分钟后捞出。"
+ },
+ {
+ "step": 7,
+ "description": "待油温再次升高到 180℃时,下入鸡块复炸 1 分钟后捞出。"
+ },
+ {
+ "step": 8,
+ "description": "待油温再次升高到 180℃时,下入土豆块复炸 1 分钟后,将锅中的油和土豆块经过笊篱过滤倒出,让笊篱上的青椒片断生。"
+ },
+ {
+ "step": 9,
+ "description": "锅中加入 5ml 食用油,小火煸炒蒜瓣至发黄,下入红油豆瓣酱煸炒出香,下入白糖炒融化,下入花椒碎,加 40ml 清水,不停搅拌至酱汁粘稠。"
+ },
+ {
+ "step": 10,
+ "description": "下入炸好的鸡块、土豆块、青椒片,搅拌均匀后出锅。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-广式萝卜牛腩-广式萝卜牛腩",
+ "name": "广式萝卜牛腩的做法",
+ "description": "# 广式萝卜牛腩的做法\n\n\n\n广式萝卜牛腩营养丰富,味道鲜美,汤汁浓郁、孩子食欲好了,成绩也好了。\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/meat_dish/广式萝卜牛腩/广式萝卜牛腩.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/广式萝卜牛腩/广式萝卜牛腩.webp",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/广式萝卜牛腩/广式萝卜牛腩.webp"
+ ],
+ "category": "荤菜",
+ "difficulty": 4,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "牛腩",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 牛腩",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白萝卜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白萝卜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜片",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜瓣",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜瓣",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "八角",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 八角",
+ "notes": "量未指定"
+ },
+ {
+ "name": "桂皮",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 桂皮",
+ "notes": "量未指定"
+ },
+ {
+ "name": "干辣椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 干辣椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香叶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香叶",
+ "notes": "量未指定"
+ },
+ {
+ "name": "南乳",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 南乳",
+ "notes": "量未指定"
+ },
+ {
+ "name": "柱侯酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 柱侯酱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蚝油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蚝油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酱油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酱油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冰糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冰糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "牛腩",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 牛腩 500g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白萝卜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白萝卜 1 根",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜片",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜片 8 片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜瓣",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜瓣 5 片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱结 一把",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱结 一把",
+ "notes": "量未指定"
+ },
+ {
+ "name": "八角",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 八角 2 片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "桂皮",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 桂皮 1 小块",
+ "notes": "量未指定"
+ },
+ {
+ "name": "干辣椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 干辣椒 2 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香叶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香叶 2 片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "南乳",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 南乳 1 块",
+ "notes": "量未指定"
+ },
+ {
+ "name": "柱侯酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 柱侯酱 30g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蚝油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蚝油 15g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酱油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酱油 15g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冰糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冰糖 10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽 15g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "萝卜滚到切块备用"
+ },
+ {
+ "step": 2,
+ "description": "牛腩整块焯水,加入 2 片姜和一把葱结,等水开之后煮 5-10 分钟,然后捞出切件"
+ },
+ {
+ "step": 3,
+ "description": "将牛腩切块,切成自己喜欢的大小(牛腩焯过水,待会儿焖的时候基本不会缩水了,大块的会焖相对就一点的时间)"
+ },
+ {
+ "step": 4,
+ "description": "准备焖牛腩的酱料,将南乳、柱侯酱、酱油、蚝油、糖、盐按上面的量调和(冰糖刚刚没了换成了白糖)"
+ },
+ {
+ "step": 5,
+ "description": "热锅下油,将姜蒜爆香,放入牛腩,炒至干身,加入调好的酱料,炒香,如果喜欢色泽浓一点的可以加一点老抽润色一下"
+ },
+ {
+ "step": 6,
+ "description": "调料充分混合之后倒入热水"
+ },
+ {
+ "step": 7,
+ "description": "将牛腩换到汤锅中,放入桂皮、八角、香叶和干辣椒,焖大概 2 个小时"
+ },
+ {
+ "step": 8,
+ "description": "牛腩焖到半软之后加入白萝卜继续焖 30 分钟"
+ },
+ {
+ "step": 9,
+ "description": "等到萝卜焖软之后就完成,一锅浓香的萝卜牛腩就完成了"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-徽派红烧肉-徽派红烧肉",
+ "name": "徽派红烧肉的做法",
+ "description": "# 徽派红烧肉的做法\n\n徽式红烧肉是一道由五花肉等食材制成的菜肴。\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/meat_dish/徽派红烧肉/徽派红烧肉.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/徽派红烧肉/1.jpeg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/徽派红烧肉/1.jpeg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/徽派红烧肉/2.jpeg"
+ ],
+ "category": "荤菜",
+ "difficulty": 4,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "五花肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 五花肉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白砂糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白砂糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蚝油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蚝油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜片",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜头",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜头",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "五香粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 五香粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "五花肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 五花肉 300 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白砂糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白砂糖 100 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 200 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 10 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蚝油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蚝油 5 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽 5 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜片",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜片 2 片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜头",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜头 3 颗",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒 100 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱 1 根",
+ "notes": "量未指定"
+ },
+ {
+ "name": "五香粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 五香粉 10 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 10 g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "五花肉切块,每块 2-3 cm 大小"
+ },
+ {
+ "step": 2,
+ "description": "锅中加入 150 ml 食用油,倒入五花肉,煎炸 2 分钟 后,加入盐,翻炒五花肉,2 分钟 后出锅"
+ },
+ {
+ "step": 3,
+ "description": "锅中加入 50 ml 食用油,倒入白砂糖,翻炒到咖啡色"
+ },
+ {
+ "step": 4,
+ "description": "倒入五花肉,翻炒 30 S ,加入姜片、蒜头后翻炒 30 S"
+ },
+ {
+ "step": 5,
+ "description": "加入料酒,五香粉、葱,加入水没过五花肉,盖上锅盖煮 10 分钟"
+ },
+ {
+ "step": 6,
+ "description": "加入生抽、老抽、蚝油,中火煮 20 分钟"
+ },
+ {
+ "step": 7,
+ "description": "开锅,大火烧汁,端盘"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-新疆大盘鸡-新疆大盘鸡",
+ "name": "新疆大盘鸡的做法",
+ "description": "# 新疆大盘鸡的做法\n\n\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/meat_dish/新疆大盘鸡/新疆大盘鸡.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/新疆大盘鸡/大盘鸡.jpeg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/新疆大盘鸡/大盘鸡.jpeg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/新疆大盘鸡/大盘鸡皮带面.jpeg"
+ ],
+ "category": "荤菜",
+ "difficulty": 4,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "花椒,香叶,香果,干线椒,大蒜,大葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 花椒,香叶,香果,干线椒,大蒜,大葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "油,盐,生抽,蚝油,料酒(可拿啤酒),白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 油,盐,生抽,蚝油,料酒(可拿啤酒),白糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡肉(鸡腿肉最好),土豆,菜椒和甜椒(可以不用,加上配色好看)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡肉(鸡腿肉最好),土豆,菜椒和甜椒(可以不用,加上配色好看)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "两个火枪腿的鸡肉(这大约是",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 两个火枪腿的鸡肉(这大约是 1kg )",
+ "notes": "量未指定"
+ },
+ {
+ "name": "土豆",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 土豆 2 个适中大小:750g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大葱 100g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "菜椒甜椒各一个,各",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 菜椒甜椒各一个,各 50g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖 20g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "干线椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 干线椒 5 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大蒜 4 瓣",
+ "notes": "量未指定"
+ },
+ {
+ "name": "油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 油 50g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "肉先剁好,块状,用清水+盐浸泡 5 分钟,去除血水,去腥,然后空干水"
+ },
+ {
+ "step": 2,
+ "description": "葱蒜辣椒土豆等洗干净,土豆削皮"
+ },
+ {
+ "step": 3,
+ "description": "葱白切长段,长度 4cm 一段,菜椒和线椒切块状"
+ },
+ {
+ "step": 4,
+ "description": "土豆切成滚刀土豆,即切一刀动滚动一下,一块土豆大概有 4cm*4cm 大小即可"
+ },
+ {
+ "step": 5,
+ "description": "炒糖色:先将油加入锅中,然后将白砂糖放入,用锅铲来回搅拌,将糖炒化,然后炒出焦黄色,此时将空干水的鸡肉倒入锅中翻炒,进行上色"
+ },
+ {
+ "step": 6,
+ "description": "放入花椒,香叶,香果,干线椒等进行翻炒"
+ },
+ {
+ "step": 7,
+ "description": "放入 5g 盐,生抽 7ml,蚝油 10g ,料酒 100g,倒入 1 升清水,料酒可以用啤酒代替"
+ },
+ {
+ "step": 8,
+ "description": "调至中火,将水烧开,调制中小火慢炖入味"
+ },
+ {
+ "step": 9,
+ "description": "当水收至鸡肉即将露出时,将土豆放在锅表面:注意不要翻动土豆,就盖在表面,不然翻到下面容易粘锅,继续盖锅盖炖,炖一会后将大葱,菜椒和甜椒放入,继续炖。"
+ },
+ {
+ "step": 10,
+ "description": "炖到汁收的差不多时可以进行翻面,将土豆与汤汁相吸,最后关火盛出。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-无骨鸡爪-无骨鸡爪",
+ "name": "无骨鸡爪的做法",
+ "description": "# 无骨鸡爪的做法\n\n\n**图片里的颜色比较浅,家里人爱吃酱油少的**\n\n这是一道做法简单但消耗体力和耐力的无骨鸡爪,酸辣开胃,Q 弹爽口,第一次做的话总耗时 8 个小时 15 分钟。\n\n预估烹饪难度:★★★★★",
+ "source_path": "dishes/meat_dish/无骨鸡爪/无骨鸡爪.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/无骨鸡爪/无骨鸡爪.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/无骨鸡爪/无骨鸡爪.jpg"
+ ],
+ "category": "荤菜",
+ "difficulty": 5,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "鸡爪",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡爪",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小米辣",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小米辣",
+ "notes": "量未指定"
+ },
+ {
+ "name": "洋葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 洋葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蚝油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蚝油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黑醋(推荐陈醋)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黑醋(推荐陈醋)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "花椒油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 花椒油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香菜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "柠檬",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 柠檬",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡爪",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡爪 1kg",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜 4 片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒 65g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大葱 3 段(5cm 一段)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大蒜 10 瓣",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小米辣",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小米辣 4 个少辣,6 个中辣,12 个大辣(推荐大辣)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "洋葱 (半个)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 洋葱 (半个)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 75g = 15g * 5",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蚝油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蚝油 30g = 15g * 2",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黑醋(推荐陈醋)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黑醋(推荐陈醋) 50g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖 10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 3g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "花椒油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 花椒油 10ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香菜 3 颗",
+ "notes": "量未指定"
+ },
+ {
+ "name": "柠檬",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 柠檬 2 颗(以 1 颗为单位来调整酸度)",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "用剪刀 / 刀 把鸡爪上的指甲的部分全部剪掉 **包括指甲下面的肉和骨头,让它一点指甲都不剩**"
+ },
+ {
+ "step": 2,
+ "description": "用水把他们洗干净,放一边"
+ },
+ {
+ "step": 3,
+ "description": "把`鸡爪`放入大锅中,准备去腥味"
+ },
+ {
+ "step": 4,
+ "description": "`大葱`,`料酒`,`姜` 全放进去"
+ },
+ {
+ "step": 5,
+ "description": "加水没过`鸡爪`"
+ },
+ {
+ "step": 6,
+ "description": "大火煮开 **中途可以把浮末捞起来**"
+ },
+ {
+ "step": 7,
+ "description": "水开**100度,沸腾**后等 10 分钟"
+ },
+ {
+ "step": 8,
+ "description": "关火,捞出来,把水沥干,洗干净,放入盆里"
+ },
+ {
+ "step": 9,
+ "description": "放入冰箱,**冷冻层** 20 分钟"
+ },
+ {
+ "step": 10,
+ "description": "把全部放入不是冷冻层的冰箱,然后分批**10个一批**拿出来去骨"
+ },
+ {
+ "step": 11,
+ "description": "从手指(鸡爪的)最前端开始,每只手指都要用刀划开**划到它的手背部分**"
+ },
+ {
+ "step": 12,
+ "description": "再从手背部用刀分划开至整个手臂"
+ },
+ {
+ "step": 13,
+ "description": "把每只手指关节处都掰一掰**按手指出声音时那种**"
+ },
+ {
+ "step": 14,
+ "description": "按着它的手指最前端,往里推,每只手指都一样,先推到中间手掌手背部分"
+ },
+ {
+ "step": 15,
+ "description": "每只手指皮脱离后,从手掌开始往手臂部分推直到整个脱下来了"
+ },
+ {
+ "step": 16,
+ "description": "放入碗中,备用"
+ },
+ {
+ "step": 17,
+ "description": "`小米辣` 切均匀小颗"
+ },
+ {
+ "step": 18,
+ "description": "`大蒜`,`洋葱`,`香菜`切碎"
+ },
+ {
+ "step": 19,
+ "description": "`柠檬`对半切开,把柠檬汁挤入鸡爪的容器里"
+ },
+ {
+ "step": 20,
+ "description": "把`全部`调料倒入装鸡爪的容器,`小米辣`,`大蒜`,`洋葱`和`香菜`也放进去"
+ },
+ {
+ "step": 21,
+ "description": "抓拌均匀"
+ },
+ {
+ "step": 22,
+ "description": "放入冰箱一个晚上(6 个小时)"
+ },
+ {
+ "step": 23,
+ "description": "调配好后全部放入准备好的鸡爪"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-枝竹羊腩煲-枝竹羊腩煲",
+ "name": "枝竹羊腩煲的做法",
+ "description": "# 枝竹羊腩煲的做法\n\n枝竹羊腩煲是一份老少皆宜,适合冬季暖胃的美食。 此道菜肥而不腻,搭配米饭堪称一绝。一般初学者需 2 个半小时即可完成。\n\n预估烹饪难度:★★★★★",
+ "source_path": "dishes/meat_dish/枝竹羊腩煲/枝竹羊腩煲.md",
+ "image_path": null,
+ "images": [],
+ "category": "荤菜",
+ "difficulty": 5,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "羊腩",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 羊腩",
+ "notes": "量未指定"
+ },
+ {
+ "name": "腐竹",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 腐竹",
+ "notes": "量未指定"
+ },
+ {
+ "name": "柱侯酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 柱侯酱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "腐乳",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 腐乳",
+ "notes": "量未指定"
+ },
+ {
+ "name": "南乳",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 南乳",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蚝油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蚝油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "清水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 清水",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冰糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冰糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱段",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱段",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜片",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香菇",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香菇",
+ "notes": "量未指定"
+ },
+ {
+ "name": "洋葱或红葱头",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 洋葱或红葱头",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜瓣",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜瓣",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香叶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香叶",
+ "notes": "量未指定"
+ },
+ {
+ "name": "八角",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 八角",
+ "notes": "量未指定"
+ },
+ {
+ "name": "桂皮",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 桂皮",
+ "notes": "量未指定"
+ },
+ {
+ "name": "其余配菜例如马蹄、土豆或者萝卜可依据个人喜好自行添加",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 其余配菜例如马蹄、土豆或者萝卜可依据个人喜好自行添加",
+ "notes": "量未指定"
+ },
+ {
+ "name": "羊腩",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 羊腩 500g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "炸腐竹",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 炸腐竹 30g-50g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "柱侯酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 柱侯酱 30g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "腐乳",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 腐乳 40g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "南乳",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 南乳 35g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽 5ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "辣椒油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 辣椒油 5ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "清水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 清水 500ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冰糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冰糖 20g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "砂糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 砂糖 10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小葱 5 根",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜片",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜片 6-8 片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香菇",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香菇 7-8 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "洋葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 洋葱 1 个或红葱头 4 - 5 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜瓣",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜瓣 7-8 瓣",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香叶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香叶 1 片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "八角",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 八角 4-5 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "桂皮",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 桂皮 10g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "准备工作: 香菇提前浸泡 2 - 3 小时至变软。腐竹提前浸泡 30 分钟至变软"
+ },
+ {
+ "step": 2,
+ "description": "准备酱汁 1: 南乳、柱侯酱、20g 腐乳、老抽放入同个小碗中搅拌均匀"
+ },
+ {
+ "step": 3,
+ "description": "准备酱汁 2: 20g 腐乳、砂糖、辣椒油放入同个小碗搅拌均匀"
+ },
+ {
+ "step": 4,
+ "description": "泡好的香菇去除根部"
+ },
+ {
+ "step": 5,
+ "description": "泡好的腐竹切成 5cm 的小段,挤干水分"
+ },
+ {
+ "step": 6,
+ "description": "洋葱去皮切丝。也可以用去皮红葱头进行替代,口味更佳。"
+ },
+ {
+ "step": 7,
+ "description": "小葱切成大约 5cm 的葱段"
+ },
+ {
+ "step": 8,
+ "description": "羊腩冷水下锅,放入 2 - 3 片姜片,倒入凉水,大火煮至水滚后关火"
+ },
+ {
+ "step": 9,
+ "description": "捞出羊腩,放入准备好的冷水盆中放凉,使其更有嚼劲"
+ },
+ {
+ "step": 10,
+ "description": "锅烧热后放入冷油,放入 4 - 5 片姜片、洋葱/红葱头、葱白段、7 - 8 瓣蒜瓣进行爆香"
+ },
+ {
+ "step": 11,
+ "description": "放入冷却好的羊腩,用筷子搅拌大约 2 - 5 分钟直至出现金黄色"
+ },
+ {
+ "step": 12,
+ "description": "放入调好的酱汁 1 ,翻炒大约 2 分钟至颜色均匀"
+ },
+ {
+ "step": 13,
+ "description": "倒入清水至刚好没过食材"
+ },
+ {
+ "step": 14,
+ "description": "放入香菇、冰糖、香叶、八角、桂皮"
+ },
+ {
+ "step": 15,
+ "description": "加盖转小火炖 90 分钟"
+ },
+ {
+ "step": 16,
+ "description": "开盖加入腐竹,加盖转中火煮 20 分钟"
+ },
+ {
+ "step": 17,
+ "description": "开盖加入酱汁 2 搅拌均匀"
+ },
+ {
+ "step": 18,
+ "description": "关火,出锅前加入葱绿段或香菜"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-柱候牛腩-柱候牛腩",
+ "name": "柱候牛腩的做法",
+ "description": "# 柱候牛腩的做法\n\n\n\n\n肉香味美,色泽诱人,滋补强壮,口感甚佳,令人垂涎的广式菜肴。有高压锅只需 1 个小时,否则需要炖煮 3 个小时。\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/meat_dish/柱候牛腩/柱候牛腩.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/柱候牛腩/土豆切片.jpeg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/柱候牛腩/土豆切片.jpeg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/柱候牛腩/柱候牛腩.jpeg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/柱候牛腩/柱候牛腩配米饭.jpeg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/柱候牛腩/牛腩入锅.jpeg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/柱候牛腩/牛腩切块.jpeg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/柱候牛腩/牛腩此时可开始炖煮.jpeg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/柱候牛腩/牛腩焯水.jpeg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/柱候牛腩/牛腩部位.jpeg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/柱候牛腩/碗1.jpeg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/柱候牛腩/碗2.jpeg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/柱候牛腩/碗3.jpeg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/柱候牛腩/碗4.jpeg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/柱候牛腩/碗5.jpeg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/柱候牛腩/碗6.jpeg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/柱候牛腩/碗7.jpeg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/柱候牛腩/过滤汤汁.jpeg"
+ ],
+ "category": "荤菜",
+ "difficulty": 4,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "炖煮锅,高压锅(可选,但极度推荐!)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 炖煮锅,高压锅(可选,但极度推荐!)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "牛腩(首选坑腩,带筋的部位)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 牛腩(首选坑腩,带筋的部位)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "柱候酱(核心酱),郫县豆瓣酱,南腐乳,叉烧酱(可选),蚝油, 老抽,生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 柱候酱(核心酱),郫县豆瓣酱,南腐乳,叉烧酱(可选),蚝油, 老抽,生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "花雕酒,白酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 花雕酒,白酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香叶, 花椒,八角,干辣椒,丁香,甘草,干辣椒,小米辣(可选),姜,蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香叶, 花椒,八角,干辣椒,丁香,甘草,干辣椒,小米辣(可选),姜,蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "牛腩",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 牛腩 500-600g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜 30g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜半头",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜半头",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小米辣",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小米辣 1 条(可根据口味调整用量)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香叶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香叶 2 片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "花椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 花椒 0.5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "八角",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 八角 2 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "干辣椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 干辣椒 3 个(可根据口味调整用量)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "丁香",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 丁香 3 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "甘草",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 甘草 2 片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "南腐乳",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 南腐乳 2 块",
+ "notes": "量未指定"
+ },
+ {
+ "name": "郫县豆瓣酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 郫县豆瓣酱 15g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冰糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冰糖 10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "花雕酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 花雕酒 10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白酒 20g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "柱候酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 柱候酱 50g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蚝油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蚝油 20g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 60g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "叉烧酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 叉烧酱 20g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "先把辅料备好:"
+ },
+ {
+ "step": 2,
+ "description": "牛肉不用切,直接冷水下锅,开大火焯水,水沸腾时将牛肉捞出"
+ },
+ {
+ "step": 3,
+ "description": "冲洗牛肉表面的杂质后,切成 4cm\\*4cm\\*4cm 的大块,控干水后放入碗中备用"
+ },
+ {
+ "step": 4,
+ "description": "大火,热锅下油,把碗 1(姜、蒜、小米辣)倒入锅中,炒香"
+ },
+ {
+ "step": 5,
+ "description": "中小火,倒入碗 2(香料),翻炒均匀,大概 30 秒"
+ },
+ {
+ "step": 6,
+ "description": "中小火,放入碗 3(南乳),用锅铲把南乳压碎"
+ },
+ {
+ "step": 7,
+ "description": "中小火,放入碗 4(豆瓣酱),翻炒均匀,大概 30 秒"
+ },
+ {
+ "step": 8,
+ "description": "中小火,放入碗 5(冰糖),炒至融化"
+ },
+ {
+ "step": 9,
+ "description": "中小火,下入牛腩,炒至牛肉上色"
+ },
+ {
+ "step": 10,
+ "description": "大火,沿锅边淋入碗 6(酒),快速翻炒,炒至牛肉表面略微焦褐"
+ },
+ {
+ "step": 11,
+ "description": "倒入碗 7(酱料),快速翻炒,留意底层汁水,炒至不停冒小气泡,汤汁略微浓稠"
+ },
+ {
+ "step": 12,
+ "description": "将锅内全部食材转移至另一个炖煮锅或高压锅,加水淹过食材"
+ },
+ {
+ "step": 13,
+ "description": "根据使用的锅来选择炖肉的时间:"
+ },
+ {
+ "step": 14,
+ "description": "时间到后开盖调味,如果不够咸加盐或生抽(少量加,不断尝味道,直到合适),不够甜则同理加糖"
+ },
+ {
+ "step": 15,
+ "description": "调好味道后便可以把牛腩先捞出"
+ },
+ {
+ "step": 16,
+ "description": "如果要吃萝卜土豆,则削皮切成 2cm 厚片倒入锅中煮 10 - 15 分钟(或煮至想要吃的口感),如果是高压锅则在加压煮 5 分钟"
+ },
+ {
+ "step": 17,
+ "description": "煮好后捞出萝卜土豆和牛腩放一起"
+ },
+ {
+ "step": 18,
+ "description": "把汤汁过滤淋入碗中"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。",
+ "做法参考:[柱候牛腩+茅根马蹄竹蔗水教程](https://www.bilibili.com/video/BV12C4y1W7ox)"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-梅菜扣肉-梅菜扣肉",
+ "name": "梅菜扣肉的做法",
+ "description": "# 梅菜扣肉的做法\n\n梅菜扣肉造型别致、大方得体,颜色酱红油亮,汤汁黏稠鲜美,扣肉肥而不腻,食之软烂醇香。\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/meat_dish/梅菜扣肉/梅菜扣肉.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/梅菜扣肉/1.jpeg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/梅菜扣肉/1.jpeg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/梅菜扣肉/2.jpeg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/梅菜扣肉/3.jpeg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/梅菜扣肉/4.jpeg"
+ ],
+ "category": "荤菜",
+ "difficulty": 4,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "五花肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 五花肉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "梅菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 梅菜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "五香粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 五香粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白砂糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白砂糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小米椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小米椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜末",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜末",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡精",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡精",
+ "notes": "量未指定"
+ },
+ {
+ "name": "五花肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 五花肉 200 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "梅菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 梅菜 30 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "五香粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 五香粉 2 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 300 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白砂糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白砂糖 5 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽 30 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 20 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小米椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小米椒 1 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜末",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜末 10 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用盐 2 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡精",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡精 2 g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "梅菜放到清水中,浸泡 1 小时"
+ },
+ {
+ "step": 2,
+ "description": "锅中倒入 50 ml 食用油,将整个五花肉猪皮朝下,放到锅中 1 分钟 ,取出挂掉猪皮 【可选】"
+ },
+ {
+ "step": 3,
+ "description": "锅中加入开水,放入五花肉,大火煮 20 分钟 (筷子可以插进五花肉),取出五花肉"
+ },
+ {
+ "step": 4,
+ "description": "在五花肉表面涂抹均匀老抽、五香粉、白砂糖,放置 15 分钟"
+ },
+ {
+ "step": 5,
+ "description": "起锅烧油,加入五花肉,中火油炸直至两面金黄色(3-5 分钟)"
+ },
+ {
+ "step": 6,
+ "description": "起锅烧油,倒入梅菜,加上小米椒、蒜蓉、鸡精、食用盐后翻炒,直至炒干梅干菜水分"
+ },
+ {
+ "step": 7,
+ "description": "五花肉切片(后端 0.5-1 cm),放在大碗中,散上梅干菜"
+ },
+ {
+ "step": 8,
+ "description": "中火蒸 45 分钟"
+ },
+ {
+ "step": 9,
+ "description": "拿个盘子倒盖在五花肉大碗中,将五花肉倒在盘子中"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-水煮牛肉-水煮牛肉",
+ "name": "水煮牛肉的做法",
+ "description": "# 水煮牛肉的做法\n\n\n\n麻辣鲜香\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/meat_dish/水煮牛肉/水煮牛肉.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/水煮牛肉/sznr1.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/水煮牛肉/sznr1.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/水煮牛肉/sznr10.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/水煮牛肉/sznr11.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/水煮牛肉/sznr12.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/水煮牛肉/sznr2.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/水煮牛肉/sznr3.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/水煮牛肉/sznr4.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/水煮牛肉/sznr5.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/水煮牛肉/sznr6.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/水煮牛肉/sznr7.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/水煮牛肉/sznr8.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/水煮牛肉/sznr9.jpg"
+ ],
+ "category": "荤菜",
+ "difficulty": 3,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "牛肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 牛肉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "豆芽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 豆芽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香菜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "豆瓣酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 豆瓣酱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "淀粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 淀粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "干辣椒粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 干辣椒粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "红辣椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 红辣椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蚝油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蚝油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "牛肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 牛肉 300g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "豆芽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 豆芽 100g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋 1 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香菜 5 根",
+ "notes": "量未指定"
+ },
+ {
+ "name": "豆瓣酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 豆瓣酱 10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒 10ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "淀粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 淀粉 15g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "干辣椒粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 干辣椒粉 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜 20g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜 3 瓣",
+ "notes": "量未指定"
+ },
+ {
+ "name": "红辣椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 红辣椒 1 根",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蚝油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蚝油 8g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "牛肉洗干净切片。"
+ },
+ {
+ "step": 2,
+ "description": "加入 15g 姜丝,1 个鸡蛋,15g 淀粉,8g 蚝油,10ml 料酒搅拌均匀,腌制 15 分钟。"
+ },
+ {
+ "step": 3,
+ "description": "香菜洗干净切好。"
+ },
+ {
+ "step": 4,
+ "description": "锅里倒油,加入豆瓣酱,5g 姜丝,蒜片。"
+ },
+ {
+ "step": 5,
+ "description": "倒入开水,煮成红汤。"
+ },
+ {
+ "step": 6,
+ "description": "豆芽洗干净去掉尾须,放进开水里焯熟。"
+ },
+ {
+ "step": 7,
+ "description": "将豆芽铺入碗底。"
+ },
+ {
+ "step": 8,
+ "description": "将牛肉片一片一片的放进红汤中,煮熟以后捞出。"
+ },
+ {
+ "step": 9,
+ "description": "将牛肉铺在豆芽上,撒上香菜梗。"
+ },
+ {
+ "step": 10,
+ "description": "撒上香菜叶,辣椒粉,辣椒圈。"
+ },
+ {
+ "step": 11,
+ "description": "另起锅烧热油,将热油淋在菜上面,就完成了。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。",
+ "做法参考:[水煮牛肉的详细步骤](https://www.zhms.cn/recipe/blrqm.html?source=2)"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-清蒸鳜鱼-清蒸鳜鱼",
+ "name": "清蒸鳜鱼的做法",
+ "description": "# 清蒸鳜鱼的做法\n\n\n\n鳜鱼可以称的上淡水鱼之王,味道鲜美,所谓高端的食材只需要最简单的烹饪方式,清蒸最能体现鳜鱼的鲜美。\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/meat_dish/清蒸鳜鱼/清蒸鳜鱼.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/清蒸鳜鱼/清蒸鳜鱼成品图.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/清蒸鳜鱼/清蒸鳜鱼成品图.jpg"
+ ],
+ "category": "荤菜",
+ "difficulty": 3,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "鳜鱼",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鳜鱼",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "红辣椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 红辣椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜片",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鳜鱼",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鳜鱼 500g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大葱 1 节",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小葱 2 根",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 30g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "红辣椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 红辣椒 1 颗",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜片",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜片 3 片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 20ml",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-湖南家常红烧肉-湖南家常红烧肉",
+ "name": "湖南家常红烧肉的做法",
+ "description": "# 湖南家常红烧肉的做法\n\n\n\n湖南家常红烧肉,入口软糯,肥而不腻\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/meat_dish/湖南家常红烧肉/湖南家常红烧肉.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/湖南家常红烧肉/湖南家常红烧肉.jpeg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/湖南家常红烧肉/湖南家常红烧肉.jpeg"
+ ],
+ "category": "荤菜",
+ "difficulty": 3,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "带皮五花肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 带皮五花肉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "干小米椒🌶(根据个人情况而定)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 干小米椒🌶(根据个人情况而定)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香叶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香叶",
+ "notes": "量未指定"
+ },
+ {
+ "name": "桂皮",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 桂皮",
+ "notes": "量未指定"
+ },
+ {
+ "name": "八角",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 八角",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冰糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冰糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冰糖(锁油上色)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冰糖(锁油上色)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "开水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 开水",
+ "notes": "量未指定"
+ },
+ {
+ "name": "五花肉:500g",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 五花肉:500g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油:10g",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油:10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香叶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香叶 5 片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜 3 片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "桂皮",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 桂皮 1 小块",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冰糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冰糖 6 颗",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒 20 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 5 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽 2 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 2 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "八角",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 八角 3 颗",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "带皮五花肉洗净冷水下锅,加入姜片 2~3 片去腥味,煮到沸腾捞出冷水冲净白沫"
+ },
+ {
+ "step": 2,
+ "description": "五花肉切块,尺寸 1.5cm*1.5cm 块状大小"
+ },
+ {
+ "step": 3,
+ "description": "热锅加入油,加入冰糖小火搅拌至焦糖色即可,加入切好的五花肉,中火翻炒上色"
+ },
+ {
+ "step": 4,
+ "description": "加入备好的姜片、八角、桂皮、生抽、老抽、料酒、干小米椒、盐,小火翻炒 1 分钟,加开水没过肉"
+ },
+ {
+ "step": 5,
+ "description": "加盖中火煮沸,转小火慢顿 30 分钟,慢炖期间,间隔 10 分钟搅拌一次防止粘锅"
+ },
+ {
+ "step": 6,
+ "description": "小火慢炖汤汁剩三分之一的时,调成中火收汁出锅。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-湘祁米夫鸭-湘祁米夫鸭",
+ "name": "湘祁米夫鸭的做法",
+ "description": "# 湘祁米夫鸭的做法\n\n\n\n湖南两祁地区特色菜品,逢年过节家家桌上有。鸭肉被米粉子包裹,入口咸香回味悠长可解乡愁。\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/meat_dish/湘祁米夫鸭/湘祁米夫鸭.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/湘祁米夫鸭/step①:准备米粉.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/湘祁米夫鸭/step①:准备米粉.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/湘祁米夫鸭/step②:煸炒鸭子.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/湘祁米夫鸭/step③:米粉裹鸭.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/湘祁米夫鸭/step④:高压锅蒸煮.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/湘祁米夫鸭/湘祁米夫鸭.jpg"
+ ],
+ "category": "荤菜",
+ "difficulty": 4,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "鸭子(必须新鲜现杀的)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸭子(必须新鲜现杀的)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "糯米粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 糯米粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "粘米粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 粘米粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒸肉粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒸肉粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "细辣椒粉(吃辣则加)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 细辣椒粉(吃辣则加)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白胡椒粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白胡椒粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "五花肉(可加可不加)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 五花肉(可加可不加)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "开水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 开水",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸭子:1000g",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸭子:1000g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "糯米粉:100g",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 糯米粉:100g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "粘米粉:",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 粘米粉: 300g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒸肉粉:",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒸肉粉: 50g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "细辣椒粉:",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 细辣椒粉: 50g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白胡椒粉:5g",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白胡椒粉:5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "五花肉:50g",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 五花肉:50g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜蒜:20g",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜蒜:20g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐:10g",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐:10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油:10g",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油:10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "开水:100g",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 开水:100g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "将糯米粉、粘米粉、蒸肉粉、细辣椒粉、5 克盐、白胡椒粉倒一起搅匀"
+ },
+ {
+ "step": 2,
+ "description": "鸭子让热心摊主剁成蒸煮块,姜切片,蒜子剥皮,五花肉切片即可"
+ },
+ {
+ "step": 3,
+ "description": "热锅凉油煸炒五花肉出油,再加食用油烧热,下入鸭子煸炒"
+ },
+ {
+ "step": 4,
+ "description": "鸭子煸炒到表皮焦变色,下入姜蒜和盐继续煸炒香味"
+ },
+ {
+ "step": 5,
+ "description": "关小火倒入米粉翻炒,鸭肉均匀裹满米粉子,加入开水,少量多次的加,边加边翻炒"
+ },
+ {
+ "step": 6,
+ "description": "翻炒鸭肉和米粉有湿感,铲出入碗中,高压锅放水蒸 20-25 分钟"
+ },
+ {
+ "step": 7,
+ "description": "出锅前撒点葱花即可享用了"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-牛排-牛排",
+ "name": "牛排的做法",
+ "description": "# 牛排的做法\n\n\n\n牛排是一种广受欢迎的西式肉类料理,富含蛋白质、油脂和铁、锌等矿物质。牛排的烹饪过程通过灵活的烹饪手法(如煎、烤、慢煮、熟成)控制牛排的熟度,从三分熟(中心为粉红色)到全熟(完全熟透)可选。高温烹饪能形成焦香外壳,搭配盐、大蒜、黄油、香料可以得到丰富的风味。牛排烹饪的入门较为简单,但精通困难。本文主要介绍最简单的煎牛排,全部烹饪过程耗时 15-30 分钟。图示为 5 分熟的牛小排(short ribs)。\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/meat_dish/牛排/牛排.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/牛排/牛排.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/牛排/牛排.jpg"
+ ],
+ "category": "荤菜",
+ "difficulty": 4,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "平底锅(有条件的推荐铸铁平底锅)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 平底锅(有条件的推荐铸铁平底锅)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "锡箔纸(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 锡箔纸(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "厨房纸(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 厨房纸(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "汤匙",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 汤匙",
+ "notes": "量未指定"
+ },
+ {
+ "name": "橄榄油(推荐特级初榨橄榄油)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 橄榄油(推荐特级初榨橄榄油)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黄油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黄油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐(推荐大颗粒海盐)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐(推荐大颗粒海盐)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黑胡椒粉(推荐粗颗粒现磨黑胡椒)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黑胡椒粉(推荐粗颗粒现磨黑胡椒)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香料(可选,推荐迷迭香或者百里香,尽量使用新鲜的植物枝条而不是这些香料磨成的粉)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香料(可选,推荐迷迭香或者百里香,尽量使用新鲜的植物枝条而不是这些香料磨成的粉)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "预制牛排酱汁(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 预制牛排酱汁(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "配菜( 可选,按喜好准备,这里推荐芦笋,口蘑,小番茄,小土豆,选其中的",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 配菜( 可选,按喜好准备,这里推荐芦笋,口蘑,小番茄,小土豆,选其中的 1-2 种即可)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "牛排",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 牛排 450-500g(两片牛排)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黑胡椒粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黑胡椒粉 2g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大蒜 1 个(约 25-30g,实际用量约为 5-10g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "橄榄油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 橄榄油 10-15ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黄油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黄油 20-25g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "口蘑",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 口蘑 5-10 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小土豆",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小土豆 5-10 个(每个约 20g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小番茄",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小番茄 5-10 个(每个约 15g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "百里香",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 百里香 2g(若使用新鲜百里香枝条,取 3-6 根,每根约 10cm 长即可)",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-猪皮冻-猪皮冻",
+ "name": "猪皮冻的做法",
+ "description": "# 猪皮冻的做法\n\n\n\n猪皮冻是一道简单易做的菜。北方人年夜饭餐桌上的“常青树”,晶莹剔透的外表,滑嫩 Q 弹的口感,是不折不扣的超级下酒菜。\n\n猪皮美容养颜,难度稍高,预计制作时长 24 小时。\n\n预估烹饪难度:★★★★★",
+ "source_path": "dishes/meat_dish/猪皮冻/猪皮冻.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/猪皮冻/猪皮冻.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/猪皮冻/猪皮冻.jpg"
+ ],
+ "category": "荤菜",
+ "difficulty": 5,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "猪皮",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 猪皮",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大料、花椒、白芷、桂皮、丁香、香叶、小茴香",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大料、花椒、白芷、桂皮、丁香、香叶、小茴香",
+ "notes": "量未指定"
+ },
+ {
+ "name": "主料:猪皮",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 主料:猪皮 1kg、水 4kg",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香料包:八角",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香料包:八角 10g、花椒 5g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "将猪皮,剁成不超过 10cm 小块,用清水浸泡 12 小时,然后冷水下锅,加入姜 10g、料酒 50ml 后,汆烫 5-10 分钟,捞出放入冷水中"
+ },
+ {
+ "step": 2,
+ "description": "将焯过水的猪皮,放到案板上,将里面的白肉部分全部剔除,然后再切成成不超过 3mm 的长条,放入盆中"
+ },
+ {
+ "step": 3,
+ "description": "加入白醋 20g,盐 5g,用力搓洗 3 分钟,再用清水洗净,这时的猪皮已经基本没什么腥味"
+ },
+ {
+ "step": 4,
+ "description": "锅内加入 4kg 水,放入猪皮,葱 10g,姜片 10g,八角 10g,花椒 5g,大火烧开后,小火煲煮 90 分钟至猪皮软烂"
+ },
+ {
+ "step": 5,
+ "description": "再加入盐 8g、味精 10g、鸡精 15g、生抽 50ml、老抽 20ml 调味后,倒入盘中,将葱姜,八角拣出,晾凉至果冻状"
+ },
+ {
+ "step": 6,
+ "description": "放冰箱冷藏即可,食用时,切成小块或者厚片"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-瘦肉土豆片-瘦肉土豆片",
+ "name": "瘦肉土豆片的做法",
+ "description": "# 瘦肉土豆片的做法\n\n\n\n瘦肉土豆片是一道简单易做的菜。小炒家常菜,方便快速,适合上班族用于带饭必备小炒菜。一般初学者只需要 1 小时即可完成。\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/meat_dish/瘦肉土豆片/瘦肉土豆片.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/瘦肉土豆片/瘦肉土豆片.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/瘦肉土豆片/瘦肉土豆片.jpg"
+ ],
+ "category": "荤菜",
+ "difficulty": 3,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "纯瘦肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 纯瘦肉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "土豆",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 土豆",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜苗",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜苗",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生粉(玉米淀粉或其他淀粉)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生粉(玉米淀粉或其他淀粉)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "纯瘦肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 纯瘦肉 200g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "土豆",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 土豆 200g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜苗",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜苗 2 根(约 20 g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生粉 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽 3g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用盐 2g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "土豆去皮、对半切开,再切成约 2mm 的薄片,备用"
+ },
+ {
+ "step": 2,
+ "description": "蒜苗洗净,切成约 1cm 的段,备用"
+ },
+ {
+ "step": 3,
+ "description": "瘦肉洗净切成约 2mm 的薄片,放入碗中,加入 5g 生粉、5g 生抽、3g 老抽腌制十分钟,备用"
+ },
+ {
+ "step": 4,
+ "description": "瘦肉腌制时,烧一锅开水,将土豆片放入锅中,焯水,约 5 分钟"
+ },
+ {
+ "step": 5,
+ "description": "热锅,锅内放入 10ml - 15ml 食用油。等待 10 秒让油温升高"
+ },
+ {
+ "step": 6,
+ "description": "放入瘦肉,翻炒至变色,倒入蒜苗一起炒,蒜苗炒约 20 秒"
+ },
+ {
+ "step": 7,
+ "description": "放入土豆,保持翻炒,加入 2g 食用盐、5g 生抽,"
+ },
+ {
+ "step": 8,
+ "description": "炒约 3 分钟,盛盘"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-糖醋排骨-糖醋排骨",
+ "name": "糖醋排骨的做法",
+ "description": "# 糖醋排骨的做法\n\n糖醋排骨是一道具有代表性的传统名菜,以其独特的酸甜口味深受大众喜爱。本菜谱在保留原有风味的基础上,对用料绑定、火候控制以及操作细节作了优化,旨在提高菜谱的可迁移性和可执行性。\n\n\n\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/meat_dish/糖醋排骨/糖醋排骨.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/糖醋排骨/1.jpeg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/糖醋排骨/1.jpeg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/糖醋排骨/2.jpeg"
+ ],
+ "category": "荤菜",
+ "difficulty": 4,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "排骨",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 排骨",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白砂糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白砂糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蚝油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蚝油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡精",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡精",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜片",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "芝麻",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 芝麻",
+ "notes": "量未指定"
+ },
+ {
+ "name": "番茄酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 番茄酱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香醋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香醋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "五香粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 五香粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "排骨",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 排骨 300 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白砂糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白砂糖 30 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 5 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蚝油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蚝油 5 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽 5 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡精",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡精 2 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜片",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜片 2 片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "芝麻",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 芝麻 2 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "番茄酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 番茄酱 10 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香醋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香醋 5 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "五香粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 五香粉 2 g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "排骨与姜片放入冷水中,大火加热至水沸腾、出现大量泡沫后转中火,待水持续沸腾时再转小火焯水 2–3 分钟,捞出备用。"
+ },
+ {
+ "step": 2,
+ "description": "用开水反复清洗排骨 2–3 遍,确保彻底去除血沫。"
+ },
+ {
+ "step": 3,
+ "description": "在锅中倒入足够量的食用油进行深炸(油量依据锅具大小而定,建议约 300 ml 供一般家庭使用),待油温升至约 170°C 后,下排骨炸制 3–5 分钟,直至表面略呈金黄色,捞出控油。"
+ },
+ {
+ "step": 4,
+ "description": "另取干净锅,置于小火上加热 50 ml 热水,加入白砂糖 30 g,轻轻搅拌直至糖完全溶解,并略呈淡黄色。此步骤的重点在于观察糖溶解情况,无需过分依赖颜色变化。"
+ },
+ {
+ "step": 5,
+ "description": "将炸好的排骨倒入炒制糖水的锅中,迅速翻炒 30 秒后,依次加入香醋 5 ml、生抽 5 ml、蚝油 5 ml、鸡精 2 g、番茄酱 10 g、五香粉 2 g,再次翻炒 30 秒,使调料均匀裹覆排骨,然后加入开水至刚好没过排骨。"
+ },
+ {
+ "step": 6,
+ "description": "用大火将锅中液体煮沸后,加入老抽 5 ml 进行上色,并快速收汁;若排骨块较大,可转小火焖煮 5–10 分钟以便更好地入味,切勿采用中火长时间炖煮 20 分钟,以免损伤口感。"
+ },
+ {
+ "step": 7,
+ "description": "起锅装盘,撒上芝麻 2 g,即可享用。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-红烧猪蹄-红烧猪蹄",
+ "name": "红烧猪蹄的做法",
+ "description": "# 红烧猪蹄的做法\n\n\n\n红烧猪蹄营养丰富,味道香,汤汁浓郁、下饭强。\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/meat_dish/红烧猪蹄/红烧猪蹄.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/红烧猪蹄/红烧猪蹄.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/红烧猪蹄/红烧猪蹄.jpg"
+ ],
+ "category": "荤菜",
+ "difficulty": 4,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "猪蹄",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 猪蹄",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香叶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香叶",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "桂皮",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 桂皮",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冰糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冰糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "八角",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 八角",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "猪蹄:2~3 根",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 猪蹄:2~3 根",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油:30ml",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油:30ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香叶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香叶 2 片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜 5 片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱半根",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱半根",
+ "notes": "量未指定"
+ },
+ {
+ "name": "桂皮",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 桂皮 1 块",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冰糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冰糖 7-8 粒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒 30 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 20 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽 20 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 8 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "八角",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 八角 4 个",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "冷水锅中放入热心摊主剁好的猪蹄,加入 20 ml 料酒与葱姜,煮 15 分钟去掉血腥"
+ },
+ {
+ "step": 2,
+ "description": "热锅冷油,倒入 30ml 食用油,放入 7-8 粒冰糖,开小火,熬成糖色,期间用锅铲把冰糖压碎,大概熬 2 分钟"
+ },
+ {
+ "step": 3,
+ "description": "熬成糖色后,放入焯过水的猪蹄,继续小火,翻炒猪蹄,直至所有猪蹄两面微黄"
+ },
+ {
+ "step": 4,
+ "description": "加入香叶 2 片、桂皮 1 块、八角 4 个、生抽 20 ml、老抽 20 ml、料酒 10 ml、姜 3 片、盐 8 克,转中火、继续翻炒 1 分钟"
+ },
+ {
+ "step": 5,
+ "description": "加入开水或者冷水,水需要没过猪蹄,盖上锅盖,大火烧开,烧开之后关火"
+ },
+ {
+ "step": 6,
+ "description": "把锅内的食材全部倒入高压锅中,高压锅中需要 15 分钟(如果同学没有高压锅,可放在锅中大火转小火熬制即可)"
+ },
+ {
+ "step": 7,
+ "description": "15 分钟之后,把高压锅的食材倒入炒锅中,开大火收汁,此时可用筷子尝下味道、淡的话可以加 2~3g 盐"
+ },
+ {
+ "step": 8,
+ "description": "大火收汁时长根据锅中的水来算,一般 30 秒即可,多留点也无碍、红烧猪蹄汤也是很下饭的"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-红烧肉-南派红烧肉",
+ "name": "南派红烧肉的做法",
+ "description": "# 南派红烧肉的做法\n\n这份红烧肉教程是一道新手不败的菜谱。配着米饭好吃的停不下来,香糯无敌棒色泽诱人肥而不腻\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/meat_dish/红烧肉/南派红烧肉.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/红烧肉/000.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/红烧肉/000.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/红烧肉/001.jpg"
+ ],
+ "category": "荤菜",
+ "difficulty": 4,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "注:如果有可能,请尽量把刀磨的锋利一些。",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 注:如果有可能,请尽量把刀磨的锋利一些。",
+ "notes": "量未指定"
+ },
+ {
+ "name": "工具:`锅`(砂锅为宜,铝锅其次,高压锅也可以,最好不要铁锅、铜锅)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 工具:`锅`(砂锅为宜,铝锅其次,高压锅也可以,最好不要铁锅、铜锅)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "主料:`五花肉`",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 主料:`五花肉`",
+ "notes": "量未指定"
+ },
+ {
+ "name": "猪五花肉:约",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 猪五花肉:约 2 斤",
+ "notes": "量未指定"
+ },
+ {
+ "name": "油:100-150ml,色拉油、猪油、花生油都可以",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 油:100-150ml,色拉油、猪油、花生油都可以",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜:",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜: 6 片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冰糖:约",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冰糖:约 15 块",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白砂糖:30g",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白砂糖:30g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽:15ml",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽:15ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒:20ml",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒:20ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "凉水:没过食材的量即可,看锅大小准备",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 凉水:没过食材的量即可,看锅大小准备",
+ "notes": "量未指定"
+ },
+ {
+ "name": "开水:没过食材的量即可,看锅大小准备",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 开水:没过食材的量即可,看锅大小准备",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香叶:4 片",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香叶:4 片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "八角:3 个",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 八角:3 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐:2-3g",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐:2-3g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "花椒:10g",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 花椒:10g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "`猪五花肉`切大块(约 4.5cm )"
+ },
+ {
+ "step": 2,
+ "description": "`生姜`切片(每片厚度约 3mm )"
+ },
+ {
+ "step": 3,
+ "description": "`开水`烧开"
+ },
+ {
+ "step": 4,
+ "description": "`凉水`自来水即可"
+ },
+ {
+ "step": 5,
+ "description": "`小葱`小葱白色的部分`葱白`切成小段(小葱最佳,大葱也可以)"
+ },
+ {
+ "step": 6,
+ "description": "`蒜`中间切开,不要拍扁,否则难以捞出以至最后`收汁`时影响味道"
+ },
+ {
+ "step": 7,
+ "description": "建议先拿出来一半葱姜,再将剩下的`生姜、葱白、蒜、花椒、八角、香叶`提前放入一个碗中备用"
+ },
+ {
+ "step": 8,
+ "description": "凉水锅中放入切好的五花肉,加入料酒与 2/5 葱姜,煮 15 分钟去掉血腥,捞出来后洗干净;"
+ },
+ {
+ "step": 9,
+ "description": "炒[糖色](./../../condiment/简易版炒糖色.md),注意采用其中提到的操作 2 来制作糖色。"
+ },
+ {
+ "step": 10,
+ "description": "将准备好的`生姜、葱白、蒜、花椒、八角、香叶`还有`五花肉`倒入锅中`大火`翻炒,期间加入至闻到香味,倒入开水至没过全部肉炖煮 50 分钟-60 分钟"
+ },
+ {
+ "step": 11,
+ "description": "加入 10ml 料酒;"
+ },
+ {
+ "step": 12,
+ "description": "盖上锅盖煮至沸腾后,每隔 25 分钟打开盖子将浮在表面的油和沫捞出;"
+ },
+ {
+ "step": 13,
+ "description": "当水的高度减至肉最高的高度与锅底高度的 3/5 时,转中火,并捞出除肉和水以外的所有辅料,开始收汁;"
+ },
+ {
+ "step": 14,
+ "description": "打开锅盖,待汤汁快没有的时粘稠状出锅(切记不可收干);"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-红烧肉-简易红烧肉",
+ "name": "简易红烧肉的做法",
+ "description": "# 简易红烧肉的做法\n\n这份红烧肉教程是一道新手不败的菜谱。配着米饭好吃的停不下来,香糯无敌棒色泽诱人肥而不腻。建议搭配米饭食用。\n\n\n\n\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/meat_dish/红烧肉/简易红烧肉.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/红烧肉/000.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/红烧肉/000.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/红烧肉/001.jpg"
+ ],
+ "category": "荤菜",
+ "difficulty": 3,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "注:如果有可能,请尽量把刀磨的锋利一些。",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 注:如果有可能,请尽量把刀磨的锋利一些。",
+ "notes": "量未指定"
+ },
+ {
+ "name": "主料:`大肉`、`鸡蛋`(可选)、`豆皮`(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 主料:`大肉`、`鸡蛋`(可选)、`豆皮`(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "猪五花肉:约",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 猪五花肉:约 3~4 斤",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜:",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜: 6 片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冰糖:15 克(约",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冰糖:15 克(约 7 块)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽:10ml",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽:10ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽:15ml",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽:15ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒:5ml",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒:5ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "开水:没过食材的量,需要",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 开水:没过食材的量,需要 600ml-900ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香叶:3 片",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香叶:3 片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "八角:2 个",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 八角:2 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鹌鹑蛋(可选,没有鹌鹑蛋,可以用同等重量的鸡蛋代替):0-2 个",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鹌鹑蛋(可选,没有鹌鹑蛋,可以用同等重量的鸡蛋代替):0-2 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "豆皮(可选):0-80g",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 豆皮(可选):0-80g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐:2-3g",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐:2-3g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "`猪五花肉`切大块(约 4.5cm ,冷冻半小时至一小时更好切)"
+ },
+ {
+ "step": 2,
+ "description": "`豆皮`切 2cm 的宽度"
+ },
+ {
+ "step": 3,
+ "description": "`生姜`切片(每片厚度约 3mm )"
+ },
+ {
+ "step": 4,
+ "description": "`水`烧开"
+ },
+ {
+ "step": 5,
+ "description": "`鹌鹑蛋`煮熟并用`叉子`/`牙签`扎孔(尽量多些好入味)"
+ },
+ {
+ "step": 6,
+ "description": "`大葱`大葱白色的部分`葱白`"
+ },
+ {
+ "step": 7,
+ "description": "冷水锅中放入切好的`猪五花肉`,加入料酒与葱姜,煮 15 分钟去掉血腥"
+ },
+ {
+ "step": 8,
+ "description": "锅中放入两片`生姜`提味"
+ },
+ {
+ "step": 9,
+ "description": "开中小火后直接加入`五花肉`,不需要放入食用油,每块`五花肉`六个面都煎一下,煎至出油即可"
+ },
+ {
+ "step": 10,
+ "description": "将煎出的油倒出备用,并将`五花肉`推至一边,加入 15g `冰糖`,翻炒至`冰糖`融化;"
+ },
+ {
+ "step": 11,
+ "description": "融化后将五花肉与冰糖炒至融合上色,加入"
+ },
+ {
+ "step": 12,
+ "description": "加入`烧好的开水`炖煮 40 分钟(刀工差的同学切的过大请自觉延长炖煮时间),并放入"
+ },
+ {
+ "step": 13,
+ "description": "盖上锅盖煮至沸腾后,加入煮好扎好孔的`鹌鹑蛋`和`豆皮`,开中小火,等待 40 分钟。(中途可适当翻搅防止粘锅);"
+ },
+ {
+ "step": 14,
+ "description": "打开锅盖,待汤汁快没有的时候开大火收汁(切记不可收干);"
+ },
+ {
+ "step": 15,
+ "description": "加入 2-3g `盐`,翻炒一下,就可以出锅了。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-羊排焖面-羊排焖面",
+ "name": "羊排焖面的做法",
+ "description": "# 羊排焖面的做法\n\n\n羊排焖面是一道硬菜,适合聚会时候大展身手。缺点就是有点花时间,优点就是好吃,而且一道菜补齐人体所需的三大营养物质。\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/meat_dish/羊排焖面/羊排焖面.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/羊排焖面/羊排焖面.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/羊排焖面/羊排焖面.jpg"
+ ],
+ "category": "荤菜",
+ "difficulty": 4,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "带皮羊排肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 带皮羊排肉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "青椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 青椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "甜椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 甜椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "青椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 青椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "花椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 花椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "干辣椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 干辣椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生姜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白砂糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白砂糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "带皮羊排",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 带皮羊排 500g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "青椒,甜椒 各",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 青椒,甜椒 各 2 个",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "羊肉冷水下锅焯水,水开了之后把血沫撇掉,捞出羊肉。"
+ },
+ {
+ "step": 2,
+ "description": "切好生姜( 4 片),放入干辣椒与花椒在碗里备用。"
+ },
+ {
+ "step": 3,
+ "description": "在炒锅加入油。(多一点也没关系)"
+ },
+ {
+ "step": 4,
+ "description": "油热之后,放入白砂糖,给羊肉炒出焦糖色。"
+ },
+ {
+ "step": 5,
+ "description": "羊肉水份炒干之后,放入盐、老抽,以及备好的调味料。"
+ },
+ {
+ "step": 6,
+ "description": "加入清水没过羊肉,大火煮沸之后,让其继续煮 10 分钟,之后小火炖煮 30 分钟。"
+ },
+ {
+ "step": 7,
+ "description": "在此期间,可以和面。和面的量以及操作方法在附加内容里讲解 *(注 1)。"
+ },
+ {
+ "step": 8,
+ "description": "放入青椒,甜椒,大葱,以及面皮进行翻炒。"
+ },
+ {
+ "step": 9,
+ "description": "翻炒均匀之后,即可出锅。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-老妈蹄花-老妈蹄花",
+ "name": "老妈蹄花的做法",
+ "description": "# 老妈蹄花的做法\n\n\n\n红烧猪蹄营养丰富,口感细腻,软烂脱骨,配上酸辣汁简直太香!\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/meat_dish/老妈蹄花/老妈蹄花.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/老妈蹄花/result1.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/老妈蹄花/result1.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/老妈蹄花/result2.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/老妈蹄花/result3.jpg"
+ ],
+ "category": "荤菜",
+ "difficulty": 4,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "猪蹄(尽量选择猪前蹄:肉多筋多骨头少)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 猪蹄(尽量选择猪前蹄:肉多筋多骨头少)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白芷",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白芷",
+ "notes": "量未指定"
+ },
+ {
+ "name": "当归(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 当归(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡精",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡精",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小米椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小米椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白胡椒粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白胡椒粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香醋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香醋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "花椒油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 花椒油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "油泼辣子(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 油泼辣子(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白芸豆(没有可用海带)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白芸豆(没有可用海带)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "猪蹄:3 根",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 猪蹄:3 根",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白芸豆:200g",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白芸豆:200g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "当归:2g",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 当归:2g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白胡椒粉:5g",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白胡椒粉:5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜片:30g",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜片:30g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "当归:2g",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 当归:2g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜末:8g",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜末:8g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡精:2g",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡精:2g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽:25g",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽:25g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱花:10g",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱花:10g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "200g 白芸豆提前一晚清水浸泡备用"
+ },
+ {
+ "step": 2,
+ "description": "准备猪前蹄,买菜的时候让师傅从中间劈开,用喷火枪去除毛囊,拿回家清洗"
+ },
+ {
+ "step": 3,
+ "description": "冷水锅中加入猪蹄、大葱段、姜片、料酒,焯水十分钟,撇去浮沫,捞出洗干净备用"
+ },
+ {
+ "step": 4,
+ "description": "高压锅中放入猪蹄、当归、白芷、白胡椒粉、姜片,上汽后压三十分钟,放入白芸豆,再压十分钟,这个时候如果汤底是奶白色,那么恭喜是正确的(如果中途需要加水,只能加热水)"
+ },
+ {
+ "step": 5,
+ "description": "揭盖后加入盐、鸡精、葱花调味"
+ },
+ {
+ "step": 6,
+ "description": "调制灵魂汁子:放入葱、蒜、小米椒,白胡椒粉、生抽、香醋、油泼辣子、花椒油、猪蹄原汤"
+ },
+ {
+ "step": 7,
+ "description": "灵魂汁子,浇给"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-老式锅包肉-老式锅包肉",
+ "name": "老式锅包肉的做法",
+ "description": "# 老式锅包肉的做法\n\n锅包肉是东北名菜,创始于光绪年间哈尔滨道台府厨师郑兴文之手。老式锅包肉的酸味来源于白醋汁,口味酸甜酥脆。\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/meat_dish/老式锅包肉/老式锅包肉.md",
+ "image_path": null,
+ "images": [],
+ "category": "荤菜",
+ "difficulty": 4,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "猪通脊肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 猪通脊肉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "胡萝卜(可无)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 胡萝卜(可无)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香菜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白醋(建议使用",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白醋(建议使用 9 度的醋,这样才会有较为突出的老式锅包肉特有的醋香)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "味精",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 味精",
+ "notes": "量未指定"
+ },
+ {
+ "name": "土豆淀粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 土豆淀粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "中筋面粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 中筋面粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小苏打",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小苏打",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白熟芝麻(可无)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白熟芝麻(可无)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "猪通脊肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 猪通脊肉 300g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大葱 50g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜 30g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜 3-4 瓣",
+ "notes": "量未指定"
+ },
+ {
+ "name": "胡萝卜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 胡萝卜 10g(可无)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香菜 10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白熟芝麻",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白熟芝麻 5g(可无)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白醋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白醋 40g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖 40g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒 20ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 8g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "味精",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 味精 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "米醋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 米醋 5ml(可无)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "土豆淀粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 土豆淀粉 210g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "中筋面粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 中筋面粉 70g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小苏打",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小苏打 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 1000ml(用于炸制)",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-芥末罗氏虾-芥末罗氏虾",
+ "name": "芥末罗氏虾的做法",
+ "description": "# 芥末罗氏虾的做法\n\n\n\n本菜品可替换成任意虾种类,包括但不限于基围虾、花虾、黑虎虾等。鲜香入味、芥末风味十足、吃完吮指,且操作十分简单。\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/meat_dish/芥末罗氏虾/芥末罗氏虾.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/芥末罗氏虾/芥末罗氏虾成品.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/芥末罗氏虾/芥末罗氏虾成品.jpg"
+ ],
+ "category": "荤菜",
+ "difficulty": 3,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "罗氏虾",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 罗氏虾",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "青芥末",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 青芥末",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "胡椒粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 胡椒粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蚝油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蚝油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小米辣(不吃辣可不放或替换成红菜椒)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小米辣(不吃辣可不放或替换成红菜椒)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黄油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黄油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "罗氏虾",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 罗氏虾 250g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜 1-2 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "青芥末",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 青芥末 20g,用于碗汁",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生粉 10g,用于碗汁",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 30g,用于碗汁",
+ "notes": "量未指定"
+ },
+ {
+ "name": "胡椒粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 胡椒粉 5g,用于碗汁",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖 3g,用于碗汁",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蚝油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蚝油 15g,用于碗汁",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 3g,用于碗汁",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小米辣",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小米辣 1-2 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黄油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黄油 20g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 80ml",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-茭白炒肉-茭白炒肉",
+ "name": "茭白炒肉的做法",
+ "description": "# 茭白炒肉的做法\n\n茭白味道鲜美,有一定营养价值\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/meat_dish/茭白炒肉/茭白炒肉.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/茭白炒肉/1.jpeg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/茭白炒肉/1.jpeg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/茭白炒肉/2.jpeg"
+ ],
+ "category": "荤菜",
+ "difficulty": 3,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "茭白",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 茭白",
+ "notes": "量未指定"
+ },
+ {
+ "name": "瘦肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 瘦肉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "淀粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 淀粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡精",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡精",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "茭白",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 茭白 2 根",
+ "notes": "量未指定"
+ },
+ {
+ "name": "瘦肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 瘦肉 100 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "淀粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 淀粉 15 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 30 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡精",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡精 5 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜 1 片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜 1 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒 5 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 2 g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "茭白切片,每片厚度 0.5 cm"
+ },
+ {
+ "step": 2,
+ "description": "瘦肉切条,厚度 0.3-0.5 cm,加入料酒、生粉、盐、水搅拌"
+ },
+ {
+ "step": 3,
+ "description": "姜切片、蒜头剁碎"
+ },
+ {
+ "step": 4,
+ "description": "起锅水烧开,放入茭白,水煮 60-90 S 后取出沥干"
+ },
+ {
+ "step": 5,
+ "description": "起锅,倒入 15 ml 油,倒入瘦肉,反复翻炒 60 S 取出"
+ },
+ {
+ "step": 6,
+ "description": "起锅,倒入 15 ml 油,倒入姜、蒜翻炒 30S,加入茭白继续翻炒 30 S"
+ },
+ {
+ "step": 7,
+ "description": "继续加入瘦肉翻炒 60 S,加入 20 ml 水,加入盐、鸡精后翻炒 60S 出锅"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-荔枝肉-荔枝肉",
+ "name": "荔枝肉的做法",
+ "description": "# 荔枝肉的做法\n\n荔枝肉独具闽菜特点,味道酸甜可口。是福州地区比较常见的一道菜肴\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/meat_dish/荔枝肉/荔枝肉.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/荔枝肉/1.jpeg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/荔枝肉/1.jpeg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/荔枝肉/2.jpeg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/荔枝肉/3.jpeg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/荔枝肉/4.jpeg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/荔枝肉/5.jpeg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/荔枝肉/6.jpeg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/荔枝肉/7.jpeg"
+ ],
+ "category": "荤菜",
+ "difficulty": 4,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "瘦肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 瘦肉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "凤梨",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 凤梨",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白砂糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白砂糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "淀粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 淀粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡精",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡精",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜末",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜末",
+ "notes": "量未指定"
+ },
+ {
+ "name": "芝麻",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 芝麻",
+ "notes": "量未指定"
+ },
+ {
+ "name": "番茄酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 番茄酱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香醋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香醋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鲜香菇",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鲜香菇 2 朵",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蟹味菇",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蟹味菇 30 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "瘦肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 瘦肉 150 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "凤梨",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 凤梨 100 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋 1 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 500 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白砂糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白砂糖 5 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "淀粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 淀粉 100 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 5 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡精",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡精 5 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜末",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜末 5 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "芝麻",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 芝麻 2 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "番茄酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 番茄酱 20 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香醋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香醋 2 ml",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "瘦肉切块(每块 2-3 cm ),放入大碗中,加入鸡蛋 1 个 、生粉 50 g 、生抽 3 ml 、鸡精 2 g"
+ },
+ {
+ "step": 2,
+ "description": "充分搅拌,直至生粉包裹住瘦肉块(太稀则继续加生粉,太干则加水),然后加入 5 ml 油,在充分搅拌后备用"
+ },
+ {
+ "step": 3,
+ "description": "在准备一个碗,加入番茄酱、鸡精 3 g 、生抽 2 ml 、姜末、白砂糖、生粉 10 g \\香醋、凉水 200 ml ,充分搅拌后备用"
+ },
+ {
+ "step": 4,
+ "description": "切一个凤梨, 准备 6 个 (每个 1.5-2 cm)凤梨块"
+ },
+ {
+ "step": 5,
+ "description": "起锅烧油,倒入 500 ml 油,一直烧油直到听到油炸声"
+ },
+ {
+ "step": 6,
+ "description": "将瘦肉一个一个放入锅中(切记不可以整碗倒入),保证每个肉不要粘在一起"
+ },
+ {
+ "step": 7,
+ "description": "全部放入瘦肉后,每 30 S 用勺子来回两面翻转瘦肉块,直至瘦肉块表面金黄"
+ },
+ {
+ "step": 8,
+ "description": "取出瘦肉,一分钟后倒入油锅中继续炸,直至瘦肉块表面出现焦黄后,取出放入大碗备用"
+ },
+ {
+ "step": 9,
+ "description": "起锅,倒入汤汁,30 S 后倒入瘦肉块、凤梨块,充分翻炒后 出锅"
+ },
+ {
+ "step": 10,
+ "description": "摆上芝麻"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-荷兰豆炒腊肠-荷兰豆炒腊肠",
+ "name": "荷兰豆炒腊肠的做法",
+ "description": "# 荷兰豆炒腊肠的做法\n\n\n\n荷兰豆炒腊肠是一道营养丰富,口感清爽,有利于开胃助食,增进食欲的美味菜肴。荷兰豆中富含人体所需的各种营养物质,尤其是含有优质蛋白质,可以提高机体的抗病能力和康复能力。\n\n预估烹饪难度:★★",
+ "source_path": "dishes/meat_dish/荷兰豆炒腊肠/荷兰豆炒腊肠.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/荷兰豆炒腊肠/1.png",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/荷兰豆炒腊肠/1.png",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/荷兰豆炒腊肠/2.png"
+ ],
+ "category": "荤菜",
+ "difficulty": 2,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "荷兰豆",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 荷兰豆",
+ "notes": "量未指定"
+ },
+ {
+ "name": "腊肠",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 腊肠",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "荷兰豆大约",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 荷兰豆大约 50 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "腊肠约",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 腊肠约 100 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 10ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 10ml",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "把荷兰豆去掉蒂,有时间的也可以同时把尾部去掉"
+ },
+ {
+ "step": 2,
+ "description": "买腊肠之前可以问老板是生的还是熟的,如果是生的,需要提前蒸一下,如果是熟的可以直接使用"
+ },
+ {
+ "step": 3,
+ "description": "把荷兰豆清洗一下,然后焯一下水,大概 45s,荷兰豆焯至变色即可,捞出过凉水备用"
+ },
+ {
+ "step": 4,
+ "description": "热锅,锅内放入大约 10ml 食用油。等待 10 秒让油温升高"
+ },
+ {
+ "step": 5,
+ "description": "放入腊肠,保持翻炒至腊肠*微微卷边*,注意这里一定要**保持小火**,小到不能小的那种,不然容易糊"
+ },
+ {
+ "step": 6,
+ "description": "放入荷兰豆,转为中大火,翻炒 30s 放入生抽,接着再翻炒 20-30s 即可"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-血浆鸭-血浆鸭",
+ "name": "血浆鸭的做法",
+ "description": "# 血浆鸭的做法\n\n.jpg)\n\n.jpg)\n\n血浆鸭是湖南武冈特色传统名菜,香、脆可口,由于醋血的作用,不仅鸭骨酥而脆,就是姜和辣椒也变得不辣而甜净。一般初学者只需要 2 小时就可以完成。\n\n预估烹饪难度:★★★★★",
+ "source_path": "dishes/meat_dish/血浆鸭/血浆鸭.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/血浆鸭/血浆鸭(微辣).jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/血浆鸭/血浆鸭(微辣).jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/血浆鸭/血浆鸭(特辣).jpg"
+ ],
+ "category": "荤菜",
+ "difficulty": 5,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "鲜仔鸭肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鲜仔鸭肉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鲜鸭血(宰杀鸭子时加醋接鸭血,用筷子顺时针搅拌防凝固)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鲜鸭血(宰杀鸭子时加醋接鸭血,用筷子顺时针搅拌防凝固)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜仔",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜仔",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "辣椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 辣椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酒(或者白酒、啤酒、米酒皆可)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酒(或者白酒、啤酒、米酒皆可)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡精",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡精",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鲜仔鸭肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鲜仔鸭肉 2000g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鲜鸭血",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鲜鸭血 250ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜 6 片 (根据个人吃辣喜好程度可多放 1-3 片姜)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜仔",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜仔 6 瓣",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香葱 2 根,切好备用",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酒(任选其一):",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酒(任选其一):",
+ "notes": "量未指定"
+ },
+ {
+ "name": "高度白酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 高度白酒 50ml + 水 150ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "啤酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 啤酒 200ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "米酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 米酒 200ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 10ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒 30ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 8g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡精",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡精 5g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "鲜仔鸭肉切成约 3cm 小块,加料酒、姜片,去除血水。"
+ },
+ {
+ "step": 2,
+ "description": "炒锅烧热,放入约 100ml 食用油,大火待油烧开,放入腌制好的鲜鸭肉,不断翻炒。"
+ },
+ {
+ "step": 3,
+ "description": "待鸭肉完全变色(肉眼可见泛白),放入酒,再加入 200ml 开水,刚好淹没鸭肉即可,盖上锅盖中火煮 15 分钟。"
+ },
+ {
+ "step": 4,
+ "description": "水开之后,打开锅盖放入姜蒜,翻炒一遍,盖上锅盖持续加热 10 分钟。"
+ },
+ {
+ "step": 5,
+ "description": "打开锅盖放入辣椒,不断翻炒,待至肉眼可见辣椒炒软,放入鲜鸭血,此时需要不断翻炒,确保每块鸭肉和每片辣椒都有鸭血的浸润(此乃血浆鸭的精髓)。"
+ },
+ {
+ "step": 6,
+ "description": "翻炒至肉眼可见鸭血均为黑色,加入盐,鸡精,香葱,(喜欢食用山胡椒油的朋友也可以在此时放入 3-6 滴山胡椒油)再次翻炒一到二次即可。"
+ },
+ {
+ "step": 7,
+ "description": "出锅盛盘,上桌食用。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-西红柿土豆炖牛肉-西红柿土豆炖牛肉",
+ "name": "西红柿土豆炖牛肉的做法",
+ "description": "# 西红柿土豆炖牛肉的做法\n\n\n\n西红柿土豆炖牛肉(腩)的特点就是还挺好吃,牛肉是优质蛋白,换成牛腩更好吃。\n\n难度基本没有,90 岁老奶奶拄拐杖都能做。\n\n预计制作总时常 1~1.5h。炖的时间:做的时间≈3:1\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/meat_dish/西红柿土豆炖牛肉/西红柿土豆炖牛肉.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/西红柿土豆炖牛肉/abaaba_1.png",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/西红柿土豆炖牛肉/abaaba_1.png"
+ ],
+ "category": "荤菜",
+ "difficulty": 4,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "牛肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 牛肉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小料",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小料",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "花椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 花椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "八角",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 八角",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香叶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香叶",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖 or 冰糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖 or 冰糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酱油(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酱油(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黑胡椒粉(或白胡椒粉)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黑胡椒粉(或白胡椒粉)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "土豆",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 土豆",
+ "notes": "量未指定"
+ },
+ {
+ "name": "西红柿",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 西红柿",
+ "notes": "量未指定"
+ },
+ {
+ "name": "洋葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 洋葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "牛肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 牛肉 500-700g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小料",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小料",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱一根,姜四片,料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱一根,姜四片,料酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "花椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 花椒 3g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "八角一个(半)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 八角一个(半)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香叶两片",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香叶两片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 油 15ml (若用牛腩可根据喜好减少为 10ml)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "调味品",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 调味品",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖 or 冰糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖 or 冰糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酱油(千禾酿造生抽无添加),老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酱油(千禾酿造生抽无添加),老抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黑胡椒粉(白的也行)2g",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黑胡椒粉(白的也行)2g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "土豆两三个(看喜好,锅能盛了为准)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 土豆两三个(看喜好,锅能盛了为准)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "西红柿拳头大小中等个头两三个",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 西红柿拳头大小中等个头两三个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "比拳头大一点的洋葱一个",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 比拳头大一点的洋葱一个",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "备菜:"
+ },
+ {
+ "step": 2,
+ "description": "制作"
+ },
+ {
+ "step": 3,
+ "description": "炖煮"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-西红柿牛腩-西红柿牛腩",
+ "name": "西红柿牛腩的做法",
+ "description": "# 西红柿牛腩的做法\n\n西红柿牛腩汤汁浓厚酸甜可口,牛肉软绵醇香,搭配米饭绝配。一般初学者需要 90 分钟完成。\n\n预估烹饪难度:★★★★★",
+ "source_path": "dishes/meat_dish/西红柿牛腩/西红柿牛腩.md",
+ "image_path": null,
+ "images": [],
+ "category": "荤菜",
+ "difficulty": 5,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "西红柿",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 西红柿",
+ "notes": "量未指定"
+ },
+ {
+ "name": "牛腩",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 牛腩",
+ "notes": "量未指定"
+ },
+ {
+ "name": "燃气灶(西红柿去皮用)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 燃气灶(西红柿去皮用)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "高压锅/砂锅/普通铝锅(铁锅)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 高压锅/砂锅/普通铝锅(铁锅)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "2cm 两段葱段、两片姜片,葱花、姜各",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 2cm 两段葱段、两片姜片,葱花、姜各 10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽、白胡椒粉,白糖,料/黄酒,八角三小片",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽、白胡椒粉,白糖,料/黄酒,八角三小片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "牛腩(挑选肥瘦相间的口感比较好)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 牛腩(挑选肥瘦相间的口感比较好)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "西红柿",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 西红柿 3-4 个(每个约 200g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "牛腩",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 牛腩 500g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 20-30ml",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "牛腩切条、切块成长宽高均 2cm ,冷水下锅,开锅煮制 2 分钟去除血水,捞出冲洗干净"
+ },
+ {
+ "step": 2,
+ "description": "另起锅 2L 水烧开,加入 2cm 两段葱段、两片姜片、八角、料/黄酒 5-10ml,放入焯好的牛肉,盖盖炖制(砂锅 1 小时,高压锅炖肉模式 45 分钟),筷子能轻松插透就证明炖好了"
+ },
+ {
+ "step": 3,
+ "description": "西红柿去皮:西红柿头部滑十字至腰线,筷子/刀叉从果蒂捅入,煤气灶小火,一边转动一边烤,及时拿下来查看,起皮后撕下来,切块。越小越好"
+ },
+ {
+ "step": 4,
+ "description": "起锅烧油,油温 7 成热,葱、姜各 10g,番茄下锅,炒透炒出番茄红色,加入煮好的牛腩和原汤,原汤刚刚没过牛肉即可"
+ },
+ {
+ "step": 5,
+ "description": "根据个人口味放入盐、糖、生抽调味盖盖"
+ },
+ {
+ "step": 6,
+ "description": "开锅后大火继续炒制 3-5 分钟"
+ },
+ {
+ "step": 7,
+ "description": "待番茄汁呈中等粘稠程度后关火,散入葱花,盛盘"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-豆豉鲮鱼油麦菜-豆豉鲮鱼油麦菜",
+ "name": "豆豉鲮鱼油麦菜的做法",
+ "description": "# 豆豉鲮鱼油麦菜的做法\n\n\n\n豆豉鲮鱼油麦菜是一到十分常见的菜,材料简单,操作方便,鲮鱼咸香,非常下饭。\n\n预估烹饪难度:★★",
+ "source_path": "dishes/meat_dish/豆豉鲮鱼油麦菜/豆豉鲮鱼油麦菜.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/豆豉鲮鱼油麦菜/豆豉鲮鱼油麦菜成品.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/豆豉鲮鱼油麦菜/豆豉鲮鱼油麦菜成品.jpg"
+ ],
+ "category": "荤菜",
+ "difficulty": 2,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "油麦菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 油麦菜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "甘竹牌鲮鱼罐头",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 甘竹牌鲮鱼罐头",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "油麦菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 油麦菜 500g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鲮鱼罐头",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鲮鱼罐头 1 罐(250g 上下)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜 4 瓣",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 20g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖 3g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 15ml",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-豉汁蒸白鱔-豉汁蒸白鱔食谱",
+ "name": "豉汁蒸白鱔食谱的做法",
+ "description": "# 豉汁蒸白鱔的做法\n\n\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/meat_dish/豉汁蒸白鱔/豉汁蒸白鱔食谱.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/豉汁蒸白鱔/豉汁蒸白鱔.jpeg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/豉汁蒸白鱔/豉汁蒸白鱔.jpeg"
+ ],
+ "category": "荤菜",
+ "difficulty": 3,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "白鱔(白鳝)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白鱔(白鳝)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "豆豉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 豆豉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜头",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜头",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "麻油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 麻油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生粉(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生粉(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "红椒(可选,装饰用)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 红椒(可选,装饰用)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白鱔",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白鱔 250g (约一条小白鱔,已去内脏并切成段)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "豆豉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 豆豉 1 汤匙",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜头",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜头 2 瓣 (剁碎)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜 3 片 (切丝)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱 1 根 (切段或丝)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 1.5 汤匙",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽 0.5 茶匙",
+ "notes": "量未指定"
+ },
+ {
+ "name": "糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 糖 0.5 茶匙",
+ "notes": "量未指定"
+ },
+ {
+ "name": "麻油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 麻油 0.5 茶匙",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生粉 1 茶匙 (可选,用于腌制)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 水 1 汤匙 (调酱汁)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "红椒 少许 (切丝,装饰用)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 红椒 少许 (切丝,装饰用)",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-贵州辣子鸡-贵州辣子鸡",
+ "name": "贵州辣子鸡的做法",
+ "description": "# 贵州辣子鸡的做法\n\n\n\n贵州人对吃鸡的执恋\n\n* 过节日,吃鸡\n* 过生日,吃鸡\n* 生病,吃鸡\n* 有客人,吃鸡\n* 家人团聚,吃鸡\n* 不知道吃什么,那就吃鸡\n\n贵州辣子鸡多种配菜,香辣可口,香糯软烂\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/meat_dish/贵州辣子鸡/贵州辣子鸡.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/贵州辣子鸡/贵州辣子鸡.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/贵州辣子鸡/贵州辣子鸡.jpg"
+ ],
+ "category": "荤菜",
+ "difficulty": 4,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "农村玉米鸡",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 农村玉米鸡",
+ "notes": "量未指定"
+ },
+ {
+ "name": "土豆",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 土豆",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香叶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香叶",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "花椒 or 麻椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 花椒 or 麻椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "糍粑辣椒(花溪党武的辣椒,遵义的子弹头,条子椒,大方的皱椒混合之后打碎的辣椒)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 糍粑辣椒(花溪党武的辣椒,遵义的子弹头,条子椒,大方的皱椒混合之后打碎的辣椒)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "豆瓣酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 豆瓣酱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "啤酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 啤酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜苗",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜苗",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酒糟",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酒糟",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡三到四个人的量是四斤,人多可以依次累加",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡三到四个人的量是四斤,人多可以依次累加",
+ "notes": "量未指定"
+ },
+ {
+ "name": "啤酒半瓶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 啤酒半瓶",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜手指头大小两个",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜手指头大小两个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "糍粑辣椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 糍粑辣椒 500g,会是拳头大小两坨",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜苗三根",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜苗三根",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香叶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香叶 2 片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大蒜两个",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大蒜两个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "土豆两个",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 土豆两个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "菜籽油两斤,开始炸鸡会用很多",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 菜籽油两斤,开始炸鸡会用很多",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽 20 ml",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "在锅中加入和锅一半高度的油,将切成长条的土豆先炸至表面金黄然后捞出备用,等油温上至烤手时候将切好的鸡块放入锅中炸,并放入切好的生姜片和花椒"
+ },
+ {
+ "step": 2,
+ "description": "刚开始炸鸡的时候,油是浑浊的,因为鸡块里面有水的原因,等到油炸至清澈,鸡块就炸好了,然后捞出备用"
+ },
+ {
+ "step": 3,
+ "description": "现在锅里面的油可以捞三分之一出来,现在用不到这么多的油"
+ },
+ {
+ "step": 4,
+ "description": "将锅中剩余的油加热,加入糍粑辣椒,豆瓣酱,生姜片,炒出红油状,将炸好的鸡块翻炒均匀"
+ },
+ {
+ "step": 5,
+ "description": "等到鸡块都上色,加入老抽,倒入啤酒,啤酒一定要盖过鸡块,加上香叶盖上盖,闷十分钟,期间间隔翻炒"
+ },
+ {
+ "step": 6,
+ "description": "然后加入土豆条,大蒜(不用切,一颗一颗的最好),然后再闷 20 分钟"
+ },
+ {
+ "step": 7,
+ "description": "最后加入酒糟翻炒均匀再加入切好的蒜苗,就可以出锅了"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-酱排骨-酱排骨",
+ "name": "酱排骨的做法",
+ "description": "# 酱排骨的做法\n\n酱排骨其色泽酱红,肉质酥烂,骨香浓郁,汁浓味鲜,咸中带甜。\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/meat_dish/酱排骨/酱排骨.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/酱排骨/1.jpeg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/酱排骨/1.jpeg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/酱排骨/2.jpeg"
+ ],
+ "category": "荤菜",
+ "difficulty": 4,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "排骨",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 排骨",
+ "notes": "量未指定"
+ },
+ {
+ "name": "五香粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 五香粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白砂糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白砂糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蚝油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蚝油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小米椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小米椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "排骨",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 排骨 300 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "五香粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 五香粉 20 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒 10 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 30 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白砂糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白砂糖 15 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽 10 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 10 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蚝油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蚝油 5 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小米椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小米椒 1 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜 2 粒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜 2 片",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "起锅烧热水,放入排骨、姜片、料酒,煮开后用勺子舀去白色油沫,2-3 分钟后出锅"
+ },
+ {
+ "step": 2,
+ "description": "冷水清洗排骨,清洗 2-3 遍"
+ },
+ {
+ "step": 3,
+ "description": "小火起锅,加入食用油,加入白砂糖 ,轻轻搅拌到糖水变成黄色"
+ },
+ {
+ "step": 4,
+ "description": "倒入排骨翻炒 30 S 后,加入生抽、蚝油、五香粉、蒜、小米椒后翻炒 30 S 后,加入清水没过排骨"
+ },
+ {
+ "step": 5,
+ "description": "大火煮 30 分钟,加入老抽上色,再煮 10 分钟"
+ },
+ {
+ "step": 6,
+ "description": "起锅摆盘"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-酱牛肉-酱牛肉",
+ "name": "酱牛肉的做法",
+ "description": "# 酱牛肉的做法\n\n\n\n家常酱牛肉营养丰富,味道香,不论是当作主食还是佐餐都很棒。一般初学者只需要 3 小时即可完成。\n\n预估烹饪难度:★★★★★",
+ "source_path": "dishes/meat_dish/酱牛肉/酱牛肉.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/酱牛肉/酱牛肉.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/酱牛肉/酱牛肉.jpg"
+ ],
+ "category": "荤菜",
+ "difficulty": 5,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "牛肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 牛肉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香叶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香叶",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "桂皮",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 桂皮",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冰糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冰糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "花椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 花椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "八角",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 八角",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黄豆酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黄豆酱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "牛肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 牛肉 2000 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香叶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香叶 1 片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜 3 片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱半根",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱半根",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽 15ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "桂皮",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 桂皮 1 块",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冰糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冰糖 7-8 粒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "花椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 花椒 15 粒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒 30ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 15ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 8 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "八角",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 八角 4 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黄豆酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黄豆酱 15ml",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "牛肉浸泡 4-6 小时,加料酒、姜片,去除血水"
+ },
+ {
+ "step": 2,
+ "description": "牛肉切成 8cm,不超过 10cm 的肉块"
+ },
+ {
+ "step": 3,
+ "description": "牛肉放入锅中,加入冷水至水面没过牛肉,开锅至水沸腾开始计时,3 分钟后停火,捞出牛肉,用温水洗净"
+ },
+ {
+ "step": 4,
+ "description": "将洗净后的牛肉放入砂锅或炖锅,加水没过牛肉,开大火,放入除盐之外的其他配料。"
+ },
+ {
+ "step": 5,
+ "description": "水开之后,大火转为小火,持续加热 90 分钟,加盐"
+ },
+ {
+ "step": 6,
+ "description": "加盐后,继续小火 90 分钟(注:每 30 分钟确认水位线,要求至少达到牛肉面高度的 80%)"
+ },
+ {
+ "step": 7,
+ "description": "加热 180 分钟后,捞出牛肉,自然冷却,切片"
+ },
+ {
+ "step": 8,
+ "description": "上桌食用,其他牛肉建议不切片冷藏。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-醉排骨-醉排骨",
+ "name": "醉排骨的做法",
+ "description": "# 醉排骨的做法\n\n醉排骨是福建省福州市特色传统名菜\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/meat_dish/醉排骨/醉排骨.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/醉排骨/1.jpeg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/醉排骨/1.jpeg"
+ ],
+ "category": "荤菜",
+ "difficulty": 4,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "排骨",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 排骨",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白砂糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白砂糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鱼露",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鱼露",
+ "notes": "量未指定"
+ },
+ {
+ "name": "芝麻",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 芝麻",
+ "notes": "量未指定"
+ },
+ {
+ "name": "番茄酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 番茄酱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香醋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香醋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜头",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜头",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "地瓜粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 地瓜粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋黄",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋黄",
+ "notes": "量未指定"
+ },
+ {
+ "name": "排骨",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 排骨 200 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白砂糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白砂糖 10 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 500 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鱼露",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鱼露 5 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "芝麻",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 芝麻 5 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "番茄酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 番茄酱 5 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香醋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香醋 5 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜头",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜头 2 粒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱 1 根",
+ "notes": "量未指定"
+ },
+ {
+ "name": "地瓜粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 地瓜粉 30 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋黄",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋黄 1 个",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "排骨中加入 5 g 地瓜粉和水进行搅拌,清洗 2-3 遍后放入大碗备用"
+ },
+ {
+ "step": 2,
+ "description": "排骨中加入鱼露、地瓜粉、鸡蛋黄 充分搅拌"
+ },
+ {
+ "step": 3,
+ "description": "将排骨一个一个放入锅中(切记不可以整碗倒入),保证每个不要粘在一起"
+ },
+ {
+ "step": 4,
+ "description": "全部放入后,每 30 S 用勺子来回两面翻转瘦肉块,直至排骨表面金黄"
+ },
+ {
+ "step": 5,
+ "description": "取出排骨,一分钟后倒入油锅中继续炸,直至瘦肉块表面出现焦黄后,取出放入大碗备用"
+ },
+ {
+ "step": 6,
+ "description": "准备一个小碗,加入蒜末、香醋、白砂糖、鱼露、番茄酱、葱花、芝麻搅拌均匀,倒入 5 ml 热油"
+ },
+ {
+ "step": 7,
+ "description": "将汤汁浇灌入排骨,在充分搅拌后倒入盘中"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-青椒土豆炒肉-青椒土豆炒肉",
+ "name": "青椒土豆炒肉的做法",
+ "description": "# 青椒土豆炒肉的做法\n\n\n\n青椒土豆炒肉是一道荤素搭配的简单炒菜。一般初学者只需要 1 小时即可完成。贼下饭~\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/meat_dish/青椒土豆炒肉/青椒土豆炒肉.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/青椒土豆炒肉/青椒土豆炒肉.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/青椒土豆炒肉/青椒土豆炒肉.jpg"
+ ],
+ "category": "荤菜",
+ "difficulty": 3,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "青椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 青椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "土豆",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 土豆",
+ "notes": "量未指定"
+ },
+ {
+ "name": "猪肉(五花肉)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 猪肉(五花肉)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酱油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酱油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "土豆淀粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 土豆淀粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "青椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 青椒 2 个(共约 200g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "土豆",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 土豆 2 个(共约 300g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "猪肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 猪肉 200g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱 1 根(约 10g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜 1 块(约 5g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜 3 瓣(约 12g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 7g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酱油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酱油 6-10ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 10-15ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "土豆淀粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 土豆淀粉 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 水 15g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "青椒去除根蒂切小块,土豆去皮切 2mm 薄片,猪肉切 4mm 薄片,葱横纵切 3mm 小段,姜蒜去皮拍散剁碎末;土豆淀粉加入约 15g 水搅拌均匀至水淀粉。"
+ },
+ {
+ "step": 2,
+ "description": "起锅烧油,加热至 7 成热放入猪肉片,缓缓翻滚炒至去肉红色,加入约 3ml 酱油,翻炒肉片均匀上色,放入约 2g 盐。"
+ },
+ {
+ "step": 3,
+ "description": "转 5 成油温,加入葱姜蒜炒 5 秒,然后加入土豆片,转 7 成油温均匀翻炒,加入加入约 5ml 酱油和 2g 盐,炒至土豆断生,表面轻微焦黄。"
+ },
+ {
+ "step": 4,
+ "description": "转 8 成油温加入青椒,大火煸炒出锅气(有白烟冒出),反复均匀翻炒 1 分钟上色,最后在锅周围倒入水淀粉转 4 成火勾芡。"
+ },
+ {
+ "step": 5,
+ "description": "在外观*呈粘稠状态*后关火,盛盘"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-香干芹菜炒肉-香干芹菜炒肉",
+ "name": "香干芹菜炒肉的做法",
+ "description": "# 香干芹菜炒肉的做法\n\n\n\n香干芹菜炒肉是一道非常简单的家常菜小炒,据说多吃芹菜对于高血压有很好的缓解作用,加上香干和猪肉一起翻炒,还是很美味的。一般初学者只需要 30 分钟(含配菜时间)即可完成。\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/meat_dish/香干芹菜炒肉/香干芹菜炒肉.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/香干芹菜炒肉/香干芹菜炒肉.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/香干芹菜炒肉/香干芹菜炒肉.jpg"
+ ],
+ "category": "荤菜",
+ "difficulty": 3,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "豆干",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 豆干",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香芹/芹菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香芹/芹菜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "猪肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 猪肉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "辣椒:青椒或者红椒都可以",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 辣椒:青椒或者红椒都可以",
+ "notes": "量未指定"
+ },
+ {
+ "name": "花椒:可选",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 花椒:可选",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡精:可选",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡精:可选",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蚝油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蚝油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "豆干:150g",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 豆干:150g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香芹:4 根",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香芹:4 根",
+ "notes": "量未指定"
+ },
+ {
+ "name": "猪肉:200g",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 猪肉:200g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜头:2 瓣",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜头:2 瓣",
+ "notes": "量未指定"
+ },
+ {
+ "name": "辣椒:4 个",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 辣椒:4 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "花椒:6 粒(不喜欢可以不放,或者放花椒水)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 花椒:6 粒(不喜欢可以不放,或者放花椒水)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐:5g",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐:5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡精:3g",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡精:3g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽:8ml",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽:8ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蚝油:5ml",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蚝油:5ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油:10-15ml",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油:10-15ml",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "芹菜去叶切段、切成不超过 4cm 的条状,备用"
+ },
+ {
+ "step": 2,
+ "description": "香干切条,宽约小拇指,备用"
+ },
+ {
+ "step": 3,
+ "description": "蒜头切片或者剁成蒜泥都行,备用"
+ },
+ {
+ "step": 4,
+ "description": "辣椒切圈或者斜切成条都行,备用"
+ },
+ {
+ "step": 5,
+ "description": "热锅,锅内放入 10ml - 15ml 食用油。等待 10 秒让油温升高"
+ },
+ {
+ "step": 6,
+ "description": "放入花椒、大蒜爆香(可以吃姜的也可以额外放入一些姜片/姜丝)"
+ },
+ {
+ "step": 7,
+ "description": "加入猪肉炒至变色,再加入 8ml 老抽上色翻炒均匀(有豆瓣酱的,可以放入 3ml 豆瓣酱一起翻炒)"
+ },
+ {
+ "step": 8,
+ "description": "加入香干翻炒均匀(大约 2 分钟)"
+ },
+ {
+ "step": 9,
+ "description": "加入辣椒翻炒均匀(大约 1-2 分钟)"
+ },
+ {
+ "step": 10,
+ "description": "加入芹菜,放入 5g 盐翻炒 1 分钟"
+ },
+ {
+ "step": 11,
+ "description": "加入 3g 鸡精、5ml 蚝油翻炒均匀,即可出锅"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-香煎五花肉-香煎五花肉",
+ "name": "香煎五花肉的做法",
+ "description": "# 香煎五花肉的做法\n\n\n\n香煎五花肉一道简单易上手的菜。五花肉肥而不腻,生菜叶脆爽健康。稍微有下厨经验的人半小时便可制作完毕。\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/meat_dish/香煎五花肉/香煎五花肉.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/香煎五花肉/香煎五花肉.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/香煎五花肉/香煎五花肉.jpg"
+ ],
+ "category": "荤菜",
+ "difficulty": 3,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "五花肉条(推荐长宽高为",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 五花肉条(推荐长宽高为 20cm\\*6cm\\*5cm)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生菜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酱油,盐,味精,料酒,姜蒜,油,豆瓣酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酱油,盐,味精,料酒,姜蒜,油,豆瓣酱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "五花肉条(推荐长宽高为",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 五花肉条(推荐长宽高为 20cm\\*6cm\\*5cm)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生菜一朵",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生菜一朵",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 5ml",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "将五花肉条沿长边切片,每片厚 1mm-1.5mm,备用"
+ },
+ {
+ "step": 2,
+ "description": "将切好的五花肉放置碗中,依次加入 8g 酱油,1g 盐,1g 味精,10g 料酒,两片姜,两朵拍扁的大蒜腌制 10 分钟"
+ },
+ {
+ "step": 3,
+ "description": "将生菜叶直接用手扒下来,洗干净,备用"
+ },
+ {
+ "step": 4,
+ "description": "热锅,倒入 5ml 食用油。油轻微冒烟后下入五花肉。单面煎制焦黄色后翻面,另一边同理。"
+ },
+ {
+ "step": 5,
+ "description": "五花肉出锅后,装盘。"
+ },
+ {
+ "step": 6,
+ "description": "将豆瓣酱抹到菜叶上,卷着五花肉即可食用"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-香菇滑鸡-香菇滑鸡",
+ "name": "香菇滑鸡的做法",
+ "description": "# 香菇滑鸡的做法\n\n\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/meat_dish/香菇滑鸡/香菇滑鸡.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/香菇滑鸡/香菇滑鸡.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/香菇滑鸡/香菇滑鸡.jpg"
+ ],
+ "category": "荤菜",
+ "difficulty": 3,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "大鸡腿",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大鸡腿",
+ "notes": "量未指定"
+ },
+ {
+ "name": "干香菇",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 干香菇",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大鸡腿",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大鸡腿 2 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "干香菇",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 干香菇 5 粒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜 2 片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱 2 颗",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜 2 瓣",
+ "notes": "量未指定"
+ },
+ {
+ "name": "温水(30-40 ℃)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 温水(30-40 ℃) 150ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒 15ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 30ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 1.5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽 15ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 糖 15ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香油 5ml",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "温水泡发干香菇"
+ },
+ {
+ "step": 2,
+ "description": "姜切小块,葱切段,蒜对半切小粒"
+ },
+ {
+ "step": 3,
+ "description": "鸡腿去骨(不去骨也可,只是略影响程序员吃饭的效率而已),切成小块"
+ },
+ {
+ "step": 4,
+ "description": "泡发的香菇一分为四,香菇水留着备用"
+ },
+ {
+ "step": 5,
+ "description": "鸡腿肉焯水 1 分钟,去除血沫和杂质"
+ },
+ {
+ "step": 6,
+ "description": "鸡腿肉中加料酒 15ml、生抽 15ml、盐 1.5g、老抽 15ml,抓匀"
+ },
+ {
+ "step": 7,
+ "description": "油温 3 成,下入鸡腿肉煸炒,等鸡腿肉金黄后盛出备用"
+ },
+ {
+ "step": 8,
+ "description": "锅留底油,下入葱、姜、蒜炒香,香菇入锅,大火翻匀"
+ },
+ {
+ "step": 9,
+ "description": "等待 20 秒会有香菇香味从锅中飘出,此时下入煸炒过的鸡腿肉,下入香菇水(全部,**本程序员认为的灵魂操作**)、糖 15ml、生抽 30ml"
+ },
+ {
+ "step": 10,
+ "description": "转中火不盖盖,咕嘟 2 分钟收浓汤汁,淋入香油 5ml,撒上葱花后即可关火、装盘"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-香辣鸡爪煲-香辣鸡爪煲",
+ "name": "香辣鸡爪煲的做法",
+ "description": "# 香辣鸡爪煲的做法\n\n\n\n香辣鸡爪煲口感 Q 弹,香辣浓郁,回味无穷。\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/meat_dish/香辣鸡爪煲/香辣鸡爪煲.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/香辣鸡爪煲/result1.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/香辣鸡爪煲/result1.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/香辣鸡爪煲/result2.jpg"
+ ],
+ "category": "荤菜",
+ "difficulty": 4,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "鸡爪",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡爪",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香叶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香叶",
+ "notes": "量未指定"
+ },
+ {
+ "name": "八角",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 八角",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小米椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小米椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "辣椒面(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 辣椒面(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蚝油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蚝油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "五香粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 五香粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡精",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡精",
+ "notes": "量未指定"
+ },
+ {
+ "name": "一斤鸡爪",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 一斤鸡爪",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香叶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香叶 3 片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "八角三个",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 八角三个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小米椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小米椒 6 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜末",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜末 10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜末",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜末 10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小葱 2 根",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蚝油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蚝油 3g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "给鸡爪剪去指甲。如果买的鸡爪只有脚掌部分,对半切开即可。 如果是整只鸡爪,需要去骨。清水洗干净。"
+ },
+ {
+ "step": 2,
+ "description": "鸡爪冷水下锅,葱姜料酒焯水,水开,撇去浮沫。"
+ },
+ {
+ "step": 3,
+ "description": "加入香叶、八角、生抽、老抽,盖盖小火慢煮三十分钟。"
+ },
+ {
+ "step": 4,
+ "description": "捞出鸡爪,留一碗鸡汤备用。"
+ },
+ {
+ "step": 5,
+ "description": "起锅烧油,用小火炒香姜末、蒜末、小米椒,能吃辣再放点辣椒面。加入生抽、老抽、蚝油、五香粉、盐,炒出酱香味。"
+ },
+ {
+ "step": 6,
+ "description": "放入鸡爪,放一点盐调味,翻炒一两分钟,再倒入鸡汤,边炒边搅动。"
+ },
+ {
+ "step": 7,
+ "description": "放入鸡精提鲜,撒入葱段搅拌均匀即可出锅。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-鱼香茄子-鱼香茄子",
+ "name": "鱼香茄子的做法",
+ "description": "# 鱼香茄子的做法\n\n\n\n这个菜真的超级下饭,当个干饭王吧。\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/meat_dish/鱼香茄子/鱼香茄子.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/鱼香茄子/yxqz1.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/鱼香茄子/yxqz1.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/鱼香茄子/yxqz2.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/鱼香茄子/yxqz3.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/鱼香茄子/yxqz4.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/鱼香茄子/yxqz5.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/鱼香茄子/yxqz6.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/鱼香茄子/yxqz7.jpg"
+ ],
+ "category": "荤菜",
+ "difficulty": 3,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "茄子",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 茄子",
+ "notes": "量未指定"
+ },
+ {
+ "name": "肉末",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 肉末",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "味精",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 味精",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "醋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 醋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "水淀粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 水淀粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "豆瓣酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 豆瓣酱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "茄子",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 茄子 2 根",
+ "notes": "量未指定"
+ },
+ {
+ "name": "肉末",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 肉末 20g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 3-5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 糖 5-10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "味精",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 味精 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 10ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽 5ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "醋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 醋 10ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "水淀粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 水淀粉 100ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "豆瓣酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 豆瓣酱 20-30g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小葱、姜、蒜、小米辣 (根据自己口味)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小葱、姜、蒜、小米辣 (根据自己口味)",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "将茄子切成条。"
+ },
+ {
+ "step": 2,
+ "description": "将肉切成肉沫,葱姜蒜切碎、小米椒切丁。"
+ },
+ {
+ "step": 3,
+ "description": "调鱼香汁:碗中放入盐、味精、糖、生抽、老抽、醋、水淀粉搅拌均匀。"
+ },
+ {
+ "step": 4,
+ "description": "锅中倒入 300ml 油,开小火(小火容易掌控),等油温七成热(小火大约 40 秒,有烟冒出)放入茄子炸两分钟,当茄子边缘微黄就捞出。多出的油可以盛出以后炒菜用。"
+ },
+ {
+ "step": 5,
+ "description": "锅中留 15-30ml 油,倒入肉沫炒至颜色变白就盛出来。"
+ },
+ {
+ "step": 6,
+ "description": "锅中倒入 15-30ml 油,放入豆瓣酱、葱白、姜、蒜炒香,然后倒入肉沫翻炒均匀。"
+ },
+ {
+ "step": 7,
+ "description": "加入 80-150ml 清水(水面预计茄子八成高度为准),倒入茄子、倒入料汁,爆炒入味收汁。最后放入葱翻炒均匀,就可以起锅了。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。",
+ "做法参考:[鱼香茄子详细步骤](https://www.zhms.cn/recipe/kbbrl.html?source=2)"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-麻婆豆腐-麻婆豆腐",
+ "name": "麻婆豆腐的做法",
+ "description": "# 麻婆豆腐的做法\n\n\n\n这是参考麻婆豆腐创作的一道菜。富含有铁、钙、磷、镁等人体必需的多种微量元素,最重要的是非常下饭哦~\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/meat_dish/麻婆豆腐/麻婆豆腐.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/麻婆豆腐/1.jpeg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/麻婆豆腐/1.jpeg"
+ ],
+ "category": "荤菜",
+ "difficulty": 3,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "内脂豆腐(推荐清美)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 内脂豆腐(推荐清美)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "水果刀",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 水果刀",
+ "notes": "量未指定"
+ },
+ {
+ "name": "咸鸭蛋(推荐留夫鸭的,这个是灵魂)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 咸鸭蛋(推荐留夫鸭的,这个是灵魂)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "五花肉(超市的肉糜也行)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 五花肉(超市的肉糜也行)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生姜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小米椒(不吃辣的可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小米椒(不吃辣的可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香辣酱(推荐广乐的)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香辣酱(推荐广乐的)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "花椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 花椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酱油(味极鲜酱油)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酱油(味极鲜酱油)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "1 盒内脂豆腐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 1 盒内脂豆腐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "1 枚咸鸭蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 1 枚咸鸭蛋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "20-30g 五花肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 20-30g 五花肉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "两瓣大蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 两瓣大蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "2 片生姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 2 片生姜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "5 根小米辣",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 5 根小米辣",
+ "notes": "量未指定"
+ },
+ {
+ "name": "5g 蒜蓉辣酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 5g 蒜蓉辣酱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "20 颗花椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 20 颗花椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "3g 食盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 3g 食盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "10g 酱油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 10g 酱油",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "大蒜和生姜切碎,备用"
+ },
+ {
+ "step": 2,
+ "description": "小米辣切成辣椒圈,备用"
+ },
+ {
+ "step": 3,
+ "description": "五花肉切成肉糜(本来就是买的肉糜的跳过)"
+ },
+ {
+ "step": 4,
+ "description": "肉糜中加入一半的食盐和味极鲜酱油,搅拌均匀,备用"
+ },
+ {
+ "step": 5,
+ "description": "鸭蛋用菜刀竖着对半切开(注意安全),去除蛋黄(一定要去除,不然会腥),剩下的蛋白捣碎成大约 2 mm * 2 mm 大小,不用太碎,备用"
+ },
+ {
+ "step": 6,
+ "description": "打开豆腐包装,用水果刀将在盒子中的豆腐划成大约 2.5 cm * 3 cm 大小,备用"
+ },
+ {
+ "step": 7,
+ "description": "热锅,锅内放入 10ml - 15ml 食用油。等待 10 秒让油温升高"
+ },
+ {
+ "step": 8,
+ "description": "调成小火,放入大蒜、生姜、辣椒圈、花椒、咸鸭蛋、蒜蓉辣酱翻炒 20 秒,炒出香味"
+ },
+ {
+ "step": 9,
+ "description": "调成中火,放入肉糜,翻炒大约 1 分钟,肉炒变色"
+ },
+ {
+ "step": 10,
+ "description": "调成小火,放入豆腐,将剩下的食盐、味极鲜酱油酱油均匀的洒在豆腐上"
+ },
+ {
+ "step": 11,
+ "description": "从锅边倒入开水(不然豆腐容易破),没过豆腐即可"
+ },
+ {
+ "step": 12,
+ "description": "开大火,水沸腾后立马转入中火,等待大约 10 分钟"
+ },
+ {
+ "step": 13,
+ "description": "等到水只剩 1/5 并且豆腐表面已经入色,关火,盛盘"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-黑椒牛柳-黑椒牛柳",
+ "name": "黑椒牛柳的做法",
+ "description": "# 黑椒牛柳的做法\n\n\n\n黑椒牛柳是一道简单易做的菜。蔬菜与肉类均衡,富含蛋白质,口味适合大多数人。一般初学者只需要 1 小时以内即可完成。\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/meat_dish/黑椒牛柳/黑椒牛柳.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/黑椒牛柳/黑椒牛柳.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/黑椒牛柳/黑椒牛柳.jpg"
+ ],
+ "category": "荤菜",
+ "difficulty": 4,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "牛肉(可以用牛里脊肉或者牛排肉)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 牛肉(可以用牛里脊肉或者牛排肉)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "洋葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 洋葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "菜椒(红/黄椒)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 菜椒(红/黄椒)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "淀粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 淀粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黑胡椒(粉)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黑胡椒(粉)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黑椒(腌料)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黑椒(腌料)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "花生油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 花生油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "牛肉量 = 份数",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 牛肉量 = 份数 * 100 克 (视就餐者胃容量和锅容量酌情增减)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "洋葱量 = 份数",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 洋葱量 = 份数 * 1/12 个(即 3 人时约切 1/4 )",
+ "notes": "量未指定"
+ },
+ {
+ "name": "菜椒量 = 份数",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 菜椒量 = 份数 * 1/12 个(即 3 人时约切 1/4 )",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐量 = 份数",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐量 = 份数 * 1 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "淀粉 = 份数",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 淀粉 = 份数 * 3 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黑椒腌料 = 参照所购商品的说明按比例腌制",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黑椒腌料 = 参照所购商品的说明按比例腌制",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黑胡椒粉 = 份数",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黑胡椒粉 = 份数 * 1 克(实际上是随便撒)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "花生油 = 份数",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 花生油 = 份数 * 10ml (实际上油量是依据菜量变动的,如对牛肉的量有增减请按对应比例变动)",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "将牛肉切条,长度最好控制在 8 厘米以下,厚度约 5-10 毫米,宽度约 1 厘米(要求不严格)"
+ },
+ {
+ "step": 2,
+ "description": "利用腌料腌制牛肉,混合均匀后静置,用量与时间请参照商品说明,可以延长不能缩短。"
+ },
+ {
+ "step": 3,
+ "description": "如果使用液态腌料,可以在腌制结束前三分钟撒一层黑胡椒粉,然后再加入淀粉,再次混合均匀后静置 20 分钟。"
+ },
+ {
+ "step": 4,
+ "description": "开火,热锅,加入花生油。"
+ },
+ {
+ "step": 5,
+ "description": "当能看到锅里的油冒出一丝烟时,放入牛肉,翻炒。"
+ },
+ {
+ "step": 6,
+ "description": "开中火偏大,翻炒 2 分钟至牛肉外表变色(即不出现明显血色,有血色部分说明翻炒不到位)(此处应小心油滴溅射)。"
+ },
+ {
+ "step": 7,
+ "description": "放入洋葱和菜椒,翻炒 2 分钟。"
+ },
+ {
+ "step": 8,
+ "description": "加入盐,再次撒一份黑胡椒粉,翻炒 30 秒,搅拌均匀。"
+ },
+ {
+ "step": 9,
+ "description": "观察洋葱已经变软即可关火,出锅,盛盘。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-meat_dish-黔式腊肠娃娃菜-黔式腊肠娃娃菜",
+ "name": "黔式腊肠娃娃菜的做法",
+ "description": "# 黔式腊肠娃娃菜的做法\n\n\n\n黔式腊肠娃娃菜不需要掌握火候,也无需调料,非常适合懒癌的菜。制作时间 15 分钟,口味近似于川菜、湘菜,却是西南菜系中鲜见的不辣菜式,咸鲜可口、南北皆宜。\n\n预估烹饪难度:★",
+ "source_path": "dishes/meat_dish/黔式腊肠娃娃菜/黔式腊肠娃娃菜.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/黔式腊肠娃娃菜/黔式腊肠娃娃菜.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/meat_dish/黔式腊肠娃娃菜/黔式腊肠娃娃菜.jpg"
+ ],
+ "category": "荤菜",
+ "difficulty": 1,
+ "tags": [
+ "荤菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "黔式腊肠",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黔式腊肠",
+ "notes": "量未指定"
+ },
+ {
+ "name": "娃娃菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 娃娃菜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黔式腊肠",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黔式腊肠 200g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "娃娃菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 娃娃菜 300g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 水 750ml",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "锅内放入 750ml 水,开火加热至沸腾"
+ },
+ {
+ "step": 2,
+ "description": "放入腊肠,计时 13 分钟"
+ },
+ {
+ "step": 3,
+ "description": "放入娃娃菜,计时 2 分钟"
+ },
+ {
+ "step": 4,
+ "description": "关火,夹出腊肠及娃娃菜"
+ },
+ {
+ "step": 5,
+ "description": "娃娃菜切段、腊肠切片,装盘"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-semi-finished-凉皮",
+ "name": "凉皮的做法",
+ "description": "# 凉皮的做法\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/semi-finished/凉皮.md",
+ "image_path": null,
+ "images": [],
+ "category": "半成品加工",
+ "difficulty": 3,
+ "tags": [
+ "半成品加工"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "凉皮、面筋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 凉皮、面筋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐、鸡精、蚝油、生抽、老抽、香油、香醋、芝麻酱(原味芝麻酱最佳)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐、鸡精、蚝油、生抽、老抽、香油、香醋、芝麻酱(原味芝麻酱最佳)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黄瓜、大蒜、绿豆芽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黄瓜、大蒜、绿豆芽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盆、碗、盘子、蒜臼",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盆、碗、盘子、蒜臼",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黄瓜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黄瓜 100g/人、绿豆芽 50g/人。",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "锅中加入 500ml 水。煮沸。"
+ },
+ {
+ "step": 2,
+ "description": "将绿豆芽放入锅中,大火煮 60 秒。豆芽捞出,过凉水,放入盘中备用。"
+ },
+ {
+ "step": 3,
+ "description": "黄瓜切丝放入盘中备用"
+ },
+ {
+ "step": 4,
+ "description": "将 10g 蒜瓣剥皮、放入蒜臼中加入 1g 盐。锤成蒜泥,加入 10g 自来水。放置备用。"
+ },
+ {
+ "step": 5,
+ "description": "注:超市购买来的凉皮表面一般会有食用油,可以使用自来水清洗。面筋同样。"
+ },
+ {
+ "step": 6,
+ "description": "注:清洗面筋之后,请用手将面筋中的大量水分挤出(不需过于用力)。"
+ },
+ {
+ "step": 7,
+ "description": "准备小碗,加入 3g 盐、2g 鸡精、5g 生抽、1g 老抽、1g 香油、2g 蚝油、香醋 5g、(盐、香醋均可根据个人口味酌量添加,以上数据只是大众口味)。"
+ },
+ {
+ "step": 8,
+ "description": "以上调料加入 25-35g 温水(据个人咸淡程度),使用筷子将其拌匀、溶解。静置一旁冷却。"
+ },
+ {
+ "step": 9,
+ "description": "注:以下计量均为一人份,如果有 n 人,请自觉将计量乘以 n"
+ },
+ {
+ "step": 10,
+ "description": "拿出小碗,将准备好的芝麻酱放入其中。"
+ },
+ {
+ "step": 11,
+ "description": "加入 4g 盐、3g 鸡精、5g 生抽、1g 老抽、3g 蚝油。"
+ },
+ {
+ "step": 12,
+ "description": "使用筷子将其调料与芝麻酱拌匀。"
+ },
+ {
+ "step": 13,
+ "description": "加入 10g 清水将其拌匀。"
+ },
+ {
+ "step": 14,
+ "description": "上一步骤重复 2、3 次(次数根据个人对芝麻酱的浓稠程度而定)。"
+ },
+ {
+ "step": 15,
+ "description": "拿出之前准备好的小盆,加入之前准备好的凉皮。"
+ },
+ {
+ "step": 16,
+ "description": "倒入盐水,使用筷子将其拌匀。随之盛入小碗(盐水一并倒入碗中)。"
+ },
+ {
+ "step": 17,
+ "description": "豆芽放置凉皮上、面筋随后放上。"
+ },
+ {
+ "step": 18,
+ "description": "将调配好的芝麻酱从面筋上方倒下。"
+ },
+ {
+ "step": 19,
+ "description": "撒上黄瓜丝。"
+ },
+ {
+ "step": 20,
+ "description": "如有喜爱可以加入辣椒油。"
+ },
+ {
+ "step": 21,
+ "description": "色香味俱全的家常凉皮出炉!"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-semi-finished-半成品意面",
+ "name": "半成品意面的做法",
+ "description": "# 半成品意面的做法\n\n意大利面🍝和中国面条口感上的区别主要是因为它是由小麦品种中最硬质的杜兰(durum)磨粉制成的。\n\n预估烹饪难度:★",
+ "source_path": "dishes/semi-finished/半成品意面.md",
+ "image_path": null,
+ "images": [],
+ "category": "半成品加工",
+ "difficulty": 1,
+ "tags": [
+ "半成品加工"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "1 袋 半成品意大利面(推荐品牌圃美多)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 1 袋 半成品意大利面(推荐品牌圃美多)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "50 ml 清水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 50 ml 清水",
+ "notes": "量未指定"
+ },
+ {
+ "name": "平底锅 或 微波炉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 平底锅 或 微波炉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "2 人",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 2 人 1 顿 520g(以半成品为准)",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "热锅"
+ },
+ {
+ "step": 2,
+ "description": "将 50 ml 清水倒入平底锅"
+ },
+ {
+ "step": 3,
+ "description": "将面条放入,炒 1 分钟"
+ },
+ {
+ "step": 4,
+ "description": "将酱料倒入,翻炒 1 分钟"
+ },
+ {
+ "step": 5,
+ "description": "装盘即可"
+ },
+ {
+ "step": 6,
+ "description": "将面条放入「可用于微波炉加热」的盘子中"
+ },
+ {
+ "step": 7,
+ "description": "将附带的酱料倒在面条上"
+ },
+ {
+ "step": 8,
+ "description": "倒入 50 ml 清水"
+ },
+ {
+ "step": 9,
+ "description": "700W 加热 2 分钟"
+ },
+ {
+ "step": 10,
+ "description": "取出拌匀即可"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-semi-finished-牛油火锅底料",
+ "name": "牛油火锅底料的做法",
+ "description": "# 牛油火锅底料的做法\n\n重庆火锅又称毛肚火锅或麻辣火锅,是中国传统饮食方式之一。\n\n其起源于明末清初的重庆嘉陵江畔,该菜式也是朝天门等码头船工纤夫的粗放餐饮方式。\n\n其主要原料是牛毛肚、猪黄喉、鸭肠、牛血旺等。\n\n一般初学者只需要 1 小时即可完成。\n\n预估烹饪难度:★★★★★",
+ "source_path": "dishes/semi-finished/牛油火锅底料.md",
+ "image_path": null,
+ "images": [],
+ "category": "半成品加工",
+ "difficulty": 5,
+ "tags": [
+ "半成品加工"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "每份原料可制作",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 每份原料可制作 7.5 kg 火锅底料/火锅老油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "牛油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 牛油 4500 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "(色拉油 或 菜籽油)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- (色拉油 或 菜籽油) 1000 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "纯猪油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 纯猪油 500 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "豆瓣(郫县)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 豆瓣(郫县) 1000 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "糍粑辣椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 糍粑辣椒 3000 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老姜(切片)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老姜(切片) 250 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大葱(切段)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大葱(切段) 100 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "洋葱(切丝)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 洋葱(切丝) 100 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大蒜(切片)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大蒜(切片) 200 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "豆鼓(剁碎)(永川)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 豆鼓(剁碎)(永川) 10 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "豆母子",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 豆母子 140 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "红花椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 红花椒 150 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老油 ? 颗粒香料",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老油 ? 颗粒香料 100 g : 整形香料 150 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "麦芽粉(肉香)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 麦芽粉(肉香) 12.5 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白酒(52%VOL)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白酒(52%VOL) 150 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老油 ?? 干辣椒面",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老油 ?? 干辣椒面 15 g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "锅置旺火(大火)放入牛油烧至 八成热(240±10°C) 时放入 `老姜、大葱、洋葱、大蒜 (各100g)`,炸干(吸尽异味(牛油腥味))后捞出扔掉。"
+ },
+ {
+ "step": 2,
+ "description": "放入 `(色拉油 || 菜籽油)、纯猪油`,等待锅中油温下降到 五成热(150±10°C) 时放入 `糍粑辣椒` 持续翻炒 5-8 分钟。"
+ },
+ {
+ "step": 3,
+ "description": "放入 `豆瓣` 炒散,转用 **中小火** 慢炒至料渣略发白翻砂(发出沙沙声)。"
+ },
+ {
+ "step": 4,
+ "description": "油在外观呈现樱桃红时放入 `姜片(150g)、大蒜(100g)` 炒香,大约 15 秒。"
+ },
+ {
+ "step": 5,
+ "description": "放入 `豆鼓、豆母子` 炒香,放入 `红花椒、小茴香` 炒香。"
+ },
+ {
+ "step": 6,
+ "description": "(老油) 此刻放入 颗粒香料"
+ },
+ {
+ "step": 7,
+ "description": "放入 `麦芽粉` 炒散,放入 `白酒` 炒散。"
+ },
+ {
+ "step": 8,
+ "description": "起锅装入容器中,静置于温度低的环境(10-20) 5 天后再使用效果最佳。"
+ },
+ {
+ "step": 9,
+ "description": "起锅装入容器中放入 `干辣椒面` 搅匀置放 24 小时,等待制作 **老油**。"
+ },
+ {
+ "step": 10,
+ "description": "将底料倒入锅中,加入 3/5 的开水用大火烧开 (底料:2/5 开水:3/5)。"
+ },
+ {
+ "step": 11,
+ "description": "烧开后表面会出现泡沫,将泡沫撇净。"
+ },
+ {
+ "step": 12,
+ "description": "转用 **中小火** 慢熬出味(约 25-30 分钟),过滤去渣。"
+ },
+ {
+ "step": 13,
+ "description": "等待容器中 **油水分离** 后,将表面的 **油** 撇净(将油打出来) 装入另外的容器。"
+ },
+ {
+ "step": 14,
+ "description": "将上一步所 **撇** 出来的 **油** 重新倒入 **净锅** 中,直至 **炼干** 油中水分起锅装入容器即为 **火锅老油**。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-semi-finished-速冻水饺",
+ "name": "速冻水饺的做法",
+ "description": "# 速冻水饺的做法\n\n饺子是一种源自中国的一种以面皮包馅、形如半月或元宝形的食物。饺子是在农历新年和冬至等节日的重要食品。通常由碎肉和蔬菜馅料包裹成一片薄生面团后包好密封。而饺子的缺点在于难以制作。不妨选择购买速冻水饺来快速在家里吃上热气腾腾的饺子。\n\n预估烹饪难度:★",
+ "source_path": "dishes/semi-finished/速冻水饺.md",
+ "image_path": null,
+ "images": [],
+ "category": "半成品加工",
+ "difficulty": 1,
+ "tags": [
+ "半成品加工"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "未过期的一袋速冻水饺",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 未过期的一袋速冻水饺",
+ "notes": "量未指定"
+ },
+ {
+ "name": "一般一个人可以食用",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 一般一个人可以食用 7~10 个水饺",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "中火,将水倒入锅中,静候水煮沸。"
+ },
+ {
+ "step": 2,
+ "description": "将饺子倒入锅中。"
+ },
+ {
+ "step": 3,
+ "description": "倒入锅前可以适当用水过一下。"
+ },
+ {
+ "step": 4,
+ "description": "倒入饺子后,可以用炒菜勺子或铲子搅水,但要注意不要铲到饺子上,以避免粘锅上撕破皮或互相粘连造成粘连处夹生。"
+ },
+ {
+ "step": 5,
+ "description": "频率不需要太高,平均每 `30` 秒摇 `3` 秒,饺子浮起后不需要再做此步。"
+ },
+ {
+ "step": 6,
+ "description": "饺子浮起及水再次煮沸后,用炒菜勺子盛起一个饺子观察,如果面皮有夹生可用炒菜勺子舀入 80ml 凉水,将水降温,然后继续煮至沸腾,此间重复此观察、搅拌操作,最多加两次水就能全熟。"
+ },
+ {
+ "step": 7,
+ "description": "所有饺子浮起后(下饺子后约 8 分钟)用铲子或漏勺把饺子铲入盘或碗中,装盘后即可食用。"
+ },
+ {
+ "step": 8,
+ "description": "吃完饺子后,等锅内水温降低,将水倒掉并用洗洁精及时刷锅,不然过段时间锅内煮过的面粉会在锅壁形成黏糊糊的物质。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-semi-finished-速冻馄饨",
+ "name": "速冻馄饨的做法",
+ "description": "# 速冻馄饨的做法\n\n馄饨是一种起源于中国的一种民间传统面食,[饺子](./速冻水饺.md)由其分化而出,有皮薄馅嫩、汤清味鲜的特点。\n\n预估烹饪难度:★★",
+ "source_path": "dishes/semi-finished/速冻馄饨.md",
+ "image_path": null,
+ "images": [],
+ "category": "半成品加工",
+ "difficulty": 2,
+ "tags": [
+ "半成品加工"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "未过期的一袋速冻馄饨(自带调味料包更佳)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 未过期的一袋速冻馄饨(自带调味料包更佳)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "电饭煲(推荐品牌小米智能电饭煲)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 电饭煲(推荐品牌小米智能电饭煲)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐(速冻馄饨无调味料包时)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐(速冻馄饨无调味料包时)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡精(速冻馄饨无调味料包时)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡精(速冻馄饨无调味料包时)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "胡椒粉(速冻馄饨无调味料包时)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 胡椒粉(速冻馄饨无调味料包时)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香油(速冻馄饨无调味料包时)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香油(速冻馄饨无调味料包时)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香菜 1 根(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "一般一个人一顿可以食用",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 一般一个人一顿可以食用 12~20 个馄饨",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "将水倒入电饭煲中,按炖或煮的模式运行 35 分钟,此时揭开电饭煲应看到水为沸腾状态。"
+ },
+ {
+ "step": 2,
+ "description": "将速冻馄饨小心放入水中,注意不要烫伤。"
+ },
+ {
+ "step": 3,
+ "description": "放入电饭煲前可以适当用水过一下。"
+ },
+ {
+ "step": 4,
+ "description": "如果馄饨有调料包,此时可一并加入水中。"
+ },
+ {
+ "step": 5,
+ "description": "盖上电饭煲,按同样炖或煮的模式运行 20 分钟。"
+ },
+ {
+ "step": 6,
+ "description": "将所有馄饨连同能没过所有馄饨的水一同盛入碗中。"
+ },
+ {
+ "step": 7,
+ "description": "如果此前没有加入调料包,此时可按自身口味轻重加入盐、鸡精、胡椒粉、香油调味。"
+ },
+ {
+ "step": 8,
+ "description": "也可撒上 5~8 片香菜叶佐味(仅适用于对香菜味道不敏感的人)。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-semi-finished-懒人蛋挞-懒人蛋挞",
+ "name": "懒人蛋挞的做法",
+ "description": "# 懒人蛋挞的做法\n\n\n\n蛋挞是一道常见的可口甜品,通常而言制作蛋挞是需要调和蛋挞液和制作蛋挞皮的,这个过程比较复杂和耗时,但是网购半成品恰恰解决解决以上的难题,初学者只需大约 40 分就可以完成。从今往后只要家里有烤箱,就可以化身烘焙达人,帮家人烤蛋挞!\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/semi-finished/懒人蛋挞/懒人蛋挞.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/semi-finished/懒人蛋挞/懒人蛋挞.png",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/semi-finished/懒人蛋挞/懒人蛋挞.png"
+ ],
+ "category": "半成品加工",
+ "difficulty": 3,
+ "tags": [
+ "半成品加工"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "需要烤箱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 需要烤箱 1 个(有上下火功能的最佳,也可以没有)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "隔热手套",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 隔热手套 1 双",
+ "notes": "量未指定"
+ },
+ {
+ "name": "网购蛋挞液",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 网购蛋挞液 1 盒,蛋挞皮 1 盒(附近的大超市也可以,比如家乐福、沃尔玛等等)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蛋挞皮",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蛋挞皮 1 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蛋挞液约",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蛋挞液约 10ml,到达挞皮的 4/5 最佳",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "烤箱 200 度,预热 10 分钟"
+ },
+ {
+ "step": 2,
+ "description": "在烤盘上放上蛋挞皮,蛋挞皮中倒入蛋挞液约 10ml,具体分量需要看蛋挞皮大小,通常倒入 4/5 即可"
+ },
+ {
+ "step": 3,
+ "description": "将烤盘放入烤箱内,上下火 190 度,烤 10 - 20 分。如果想快速烤出蛋挞液上的焦褐斑点,需要上火更高一些,通常是 200 - 210 度"
+ },
+ {
+ "step": 4,
+ "description": "蛋挞液烤出焦褐斑点,蛋挞皮完全蓬松冒油即可"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-semi-finished-炸薯条-炸薯条",
+ "name": "炸薯条的做法",
+ "description": "# 炸薯条的做法\n\n\n\n薯条🍟是一种土豆🥔\\马铃薯🥔\\洋芋🥔切成条状之后再油炸而成的快餐食物(在有的国家可能不算快餐),非常适合。相较于油炸,空气炸锅可能会更加易于避免崩溃和实现异步非阻塞。相较于自己动手切土豆再洗去淀粉并喷上油,使用半成品薯条可能会显著减少热量摄入前的热量消耗,四舍五入就是会显著减少热量摄入~~前的热量消耗~~。\n\n预估烹饪难度:★★",
+ "source_path": "dishes/semi-finished/炸薯条/炸薯条.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/semi-finished/炸薯条/炸薯条.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/semi-finished/炸薯条/炸薯条.jpg"
+ ],
+ "category": "半成品加工",
+ "difficulty": 2,
+ "tags": [
+ "半成品加工"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "1 袋半成品薯条(推荐品牌麦肯)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 1 袋半成品薯条(推荐品牌麦肯)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "1 个空气炸锅(喜欢脆的切忌小牌子)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 1 个空气炸锅(喜欢脆的切忌小牌子)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "作为主食,1 人",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 作为主食,1 人 1 顿 400g(以半成品为准)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "作为小食,1 人",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 作为小食,1 人 1 顿 1/4 主食质量+-50g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "开封大分量半成品薯条注意开口要小,可以有效减少长久储藏下薯条表面结霜。"
+ },
+ {
+ "step": 2,
+ "description": "插电,200℃预热 5 分钟。"
+ },
+ {
+ "step": 3,
+ "description": "预热的目的是为了确保放入食材的时候锅内温度已经处于烹饪所需温度。"
+ },
+ {
+ "step": 4,
+ "description": "注意,预热完再拿出薯条,不应等薯条软化后再炸制。"
+ },
+ {
+ "step": 5,
+ "description": "取出薯条放入空气炸锅,200℃20 分钟。"
+ },
+ {
+ "step": 6,
+ "description": "取出薯条的时候注意半成品薯条已经有油,所以要异步去做客户端内刀斯林的话需要使用夹持工具。"
+ },
+ {
+ "step": 7,
+ "description": "5~10 分钟时可以拿出锅体晃动使薯条受热均匀也防止粘连。"
+ },
+ {
+ "step": 8,
+ "description": "10 分钟~15 分钟时,拿出锅体,往已经干了的薯条表面喷 1 层面积为薯条表面积 2/3 的油。"
+ },
+ {
+ "step": 9,
+ "description": "喜欢脆薯条的,取出后拿着锅体跳舞让空气经过薯条表面后装盘;喜欢软薯条的直接装盘。配合蘸酱或浇上酱汁更佳。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-semi-finished-空气炸锅羊排-空气炸锅羊排",
+ "name": "空气炸锅羊排的做法",
+ "description": "# 空气炸锅羊排的做法\n\n\n\n空气炸锅羊排超级懒人版,味道尚可,主要看羊排的品质。\n\n- 烹饪总时长:40 分钟(准备 5 分钟+腌制 20 分钟+下锅 15 分钟)\n- 实际操作时间:10 分钟\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/semi-finished/空气炸锅羊排/空气炸锅羊排.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/semi-finished/空气炸锅羊排/羊排.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/semi-finished/空气炸锅羊排/羊排.jpg"
+ ],
+ "category": "半成品加工",
+ "difficulty": 3,
+ "tags": [
+ "半成品加工"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "必备:黑椒混合牛排调味料(懒)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 必备:黑椒混合牛排调味料(懒)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "必备:蒜蓉酱(推荐川娃子的,同样是因为懒)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 必备:蒜蓉酱(推荐川娃子的,同样是因为懒)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "必备:厨房纸",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 必备:厨房纸",
+ "notes": "量未指定"
+ },
+ {
+ "name": "可选:黄油(JD 买小盒装的,一片一小盒)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 可选:黄油(JD 买小盒装的,一片一小盒)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "可选:烧烤料",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 可选:烧烤料",
+ "notes": "量未指定"
+ },
+ {
+ "name": "可选:罗勒碎",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 可选:罗勒碎",
+ "notes": "量未指定"
+ },
+ {
+ "name": "可选:空气炸锅烤架(用烤架油比较少,底下更容易熟,洗起来麻烦。不用的话比较入味。看个人选择啦)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 可选:空气炸锅烤架(用烤架油比较少,底下更容易熟,洗起来麻烦。不用的话比较入味。看个人选择啦)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "羊排",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 羊排 1 片约 160g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黑椒混合牛排调味料",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黑椒混合牛排调味料 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜蓉酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜蓉酱 20g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黄油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黄油 1 小盒 10g 或 烧烤料 20g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "羊排放入碗中清水洗净血水"
+ },
+ {
+ "step": 2,
+ "description": "羊排用厨房纸吸干水分,双面抹上黑椒混合调味料、蒜蓉酱,静置腌制 20 分钟"
+ },
+ {
+ "step": 3,
+ "description": "锡纸碗放上烤架,羊排放在烤架上,撒上罗勒碎,黄油或烧烤料放在羊排上,空气炸锅 180° 10 分钟"
+ },
+ {
+ "step": 4,
+ "description": "羊排翻面,撒上罗勒碎,黄油(从锡纸碗里舀上来)或烧烤料放在羊排上,空气炸锅 180° 5 分钟(可以视个人喜好加一点时间,这里写的是不会焦的时间)"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-semi-finished-空气炸锅鸡翅中-空气炸锅鸡翅中",
+ "name": "空气炸锅鸡翅中的做法",
+ "description": "# 空气炸锅鸡翅中的做法\n\n\n\n\n空气炸锅做鸡翅中方便,这样自带油脂的食物味道很好,比 KFC 的好吃,吃完不**用洗碗洗锅**。\n\n- 烹饪时长:40 分钟(准备 3 分钟+解冻 20 分钟+下锅 17 分钟)\n- 实际操作时间:5 分钟\n\n预估烹饪难度:★★",
+ "source_path": "dishes/semi-finished/空气炸锅鸡翅中/空气炸锅鸡翅中.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/semi-finished/空气炸锅鸡翅中/鸡翅中_0.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/semi-finished/空气炸锅鸡翅中/鸡翅中_0.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/semi-finished/空气炸锅鸡翅中/鸡翅中_1.jpg"
+ ],
+ "category": "半成品加工",
+ "difficulty": 2,
+ "tags": [
+ "半成品加工"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "可选:罗勒碎(撒上去纯粹为了好看)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 可选:罗勒碎(撒上去纯粹为了好看)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "可选:云南单山蘸水(代替烧烤料)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 可选:云南单山蘸水(代替烧烤料)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡翅中",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡翅中 6 个(泰森奥尔良鸡翅中,其他品牌例如圣农嘟嘟翅可能会大一些,请自行根据食量斟酌)",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "鸡翅从冰箱拿出来,鸡翼面朝下放入锡纸烤盘,撒上罗勒碎,盖上保鲜膜自然解冻 20 分钟"
+ },
+ {
+ "step": 2,
+ "description": "撒上罗勒碎,空气炸锅 200°C,10 分钟"
+ },
+ {
+ "step": 3,
+ "description": "翻面,撒上罗勒碎,空气炸锅 200°C,7 分钟"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-semi-finished-速冻汤圆-速冻汤圆",
+ "name": "速冻汤圆的做法",
+ "description": "# 速冻汤圆的做法\n\n\n\n速冻汤圆是一道简单易做的菜。一般初学者只需要 6 分钟即可完成。\n\n预估烹饪难度:★",
+ "source_path": "dishes/semi-finished/速冻汤圆/速冻汤圆.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/semi-finished/速冻汤圆/速冻汤圆.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/semi-finished/速冻汤圆/速冻汤圆.jpg"
+ ],
+ "category": "半成品加工",
+ "difficulty": 1,
+ "tags": [
+ "半成品加工"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "速冻汤圆",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 速冻汤圆",
+ "notes": "量未指定"
+ },
+ {
+ "name": "微波炉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 微波炉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "速冻汤圆:11 个。数量取决于碗的大小。保证放入的汤圆最高不超过碗高度 -",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 速冻汤圆:11 个。数量取决于碗的大小。保证放入的汤圆最高不超过碗高度 - 5mm。",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "取出速冻汤圆,放入碗中。"
+ },
+ {
+ "step": 2,
+ "description": "倒入开水,直至浸没汤圆。"
+ },
+ {
+ "step": 3,
+ "description": "微波炉高火 4 分钟。"
+ },
+ {
+ "step": 4,
+ "description": "假如汤圆均已吸水膨胀,则已熟。"
+ },
+ {
+ "step": 5,
+ "description": "如果没熟,再加热 1 分钟。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-soup-奶油蘑菇汤",
+ "name": "奶油蘑菇汤的做法",
+ "description": "# 奶油蘑菇汤的做法\n\n预估烹饪难度:★\n\n---",
+ "source_path": "dishes/soup/奶油蘑菇汤.md",
+ "image_path": null,
+ "images": [],
+ "category": "汤",
+ "difficulty": 1,
+ "tags": [
+ "汤"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "白蘑菇",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白蘑菇",
+ "notes": "量未指定"
+ },
+ {
+ "name": "洋葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 洋葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黄油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黄油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "面粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 面粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "牛奶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 牛奶",
+ "notes": "量未指定"
+ },
+ {
+ "name": "淡奶油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 淡奶油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黑胡椒碎",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黑胡椒碎",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "清水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 清水",
+ "notes": "量未指定"
+ },
+ {
+ "name": "--",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- --",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白蘑菇",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白蘑菇 200 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "洋葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 洋葱 50 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黄油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黄油 15 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "面粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 面粉 10 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "牛奶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 牛奶 200 毫升",
+ "notes": "量未指定"
+ },
+ {
+ "name": "淡奶油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 淡奶油 30 毫升",
+ "notes": "量未指定"
+ },
+ {
+ "name": "清水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 清水 100 毫升",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 2 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黑胡椒碎",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黑胡椒碎 1 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "--",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- --",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "--"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-soup-小米粥",
+ "name": "小米粥的做法",
+ "description": "# 小米粥的做法\n\n小米含有多种维生素、氨基酸、脂肪和碳水化合物,营养价值较高,每 100 克小米含蛋白质 9.7 克、脂肪 3.5 克,都不低于稻、麦。\n\n一般粮食中不含有的胡萝卜素,而小米每 100 克含量 0.12 毫克,维生素 B1 的含量位居所有粮食之首。\n\n小米含糖也很高,每 100 克含糖 72.8 克,产热量比大米高许多。另外,小米也富含维生素 B1,B2 等\n\n预估烹饪难度:★★",
+ "source_path": "dishes/soup/小米粥.md",
+ "image_path": null,
+ "images": [],
+ "category": "汤",
+ "difficulty": 2,
+ "tags": [
+ "汤"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "小米",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小米",
+ "notes": "量未指定"
+ },
+ {
+ "name": "水(山泉水最佳)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 水(山泉水最佳)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小米",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小米 100 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "水(山泉水最佳)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 水(山泉水最佳) 2000 克",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "小米 100 克,放入碗中,用水轻淘一遍(用手搅拌一下,将水倒掉,只是去掉外面的浮灰,不可搓洗!!!)"
+ },
+ {
+ "step": 2,
+ "description": "水烧开,务必烧开!!!"
+ },
+ {
+ "step": 3,
+ "description": "水烧开沸腾时,将小米倒入锅内。(很容易被忽视的一个很重要的环节)"
+ },
+ {
+ "step": 4,
+ "description": "搅拌使得小米不会粘连锅底,继续用大火熬 6-10 分钟,注意用中间穿插搅拌几次。"
+ },
+ {
+ "step": 5,
+ "description": "改中火、文火熬 15-20 分钟,锅盖要错开一条缝,千万不能让小米油溜掉哟,中间继续搅拌几次,不要糊锅底"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-soup-生汆丸子汤",
+ "name": "生汆丸子汤的做法",
+ "description": "# 生汆丸子汤的做法\n\n生汆丸子汤,吃的就是一个鲜、嫩、弹。\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/soup/生汆丸子汤.md",
+ "image_path": null,
+ "images": [],
+ "category": "汤",
+ "difficulty": 4,
+ "tags": [
+ "汤"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "盐量 = 猪肉斤数",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐量 = 猪肉斤数 * 6 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "胡椒粉量 = 猪肉斤数",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 胡椒粉量 = 猪肉斤数 * 2 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "土豆淀粉 = 多少人的用量",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 土豆淀粉 = 多少人的用量 * 40 克,本教程以一人用量算",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "肉改刀切开,肥瘦三七分"
+ },
+ {
+ "step": 2,
+ "description": "上刀剁一剁,用刀背砸一砸,把肉筋打开打松疏"
+ },
+ {
+ "step": 3,
+ "description": "剁一剁,砸一砸,剁成肉末,要想好吃得自己剁,机器打的太黏糊了"
+ },
+ {
+ "step": 4,
+ "description": "每斤肉,6 克盐,1 克胡椒粉"
+ },
+ {
+ "step": 5,
+ "description": "上手抓匀"
+ },
+ {
+ "step": 6,
+ "description": "葱姜花椒水分次加,边加边搅,用手揉匀,让肉吸饱水。每斤肉末 80 克葱姜花椒水"
+ },
+ {
+ "step": 7,
+ "description": "放入鸡蛋清,继续顺着一个方向搅"
+ },
+ {
+ "step": 8,
+ "description": "加入 40 克土豆淀粉,搅匀"
+ },
+ {
+ "step": 9,
+ "description": "加入熟豆油,这是为了保持其嫩滑弹的状态"
+ },
+ {
+ "step": 10,
+ "description": "起锅烧水,烧开,改小火,似开非开的样子"
+ },
+ {
+ "step": 11,
+ "description": "上手,挤丸子,"
+ },
+ {
+ "step": 12,
+ "description": "全部漂起来,用小火煮 1 分钟"
+ },
+ {
+ "step": 13,
+ "description": "粉丝放碗底"
+ },
+ {
+ "step": 14,
+ "description": "加木耳,黄花,小香葱并用盐、胡椒粉、鸡粉打底调味"
+ },
+ {
+ "step": 15,
+ "description": "连汤带丸子冲如碗中"
+ },
+ {
+ "step": 16,
+ "description": "淋 3-5 滴香油"
+ },
+ {
+ "step": 17,
+ "description": "加一小颗香菜"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-soup-番茄牛肉蛋花汤",
+ "name": "番茄牛肉蛋花汤的做法",
+ "description": "# 番茄牛肉蛋花汤的做法\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/soup/番茄牛肉蛋花汤.md",
+ "image_path": null,
+ "images": [],
+ "category": "汤",
+ "difficulty": 3,
+ "tags": [
+ "汤"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "牛肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 牛肉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "番茄",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 番茄",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱、姜、蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱、姜、蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "胡椒粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 胡椒粉",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "牛肉切成薄片"
+ },
+ {
+ "step": 2,
+ "description": "番茄切成小块"
+ },
+ {
+ "step": 3,
+ "description": "葱切成葱花"
+ },
+ {
+ "step": 4,
+ "description": "姜切成姜片"
+ },
+ {
+ "step": 5,
+ "description": "蒜剁成蒜泥"
+ },
+ {
+ "step": 6,
+ "description": "牛肉放入碗中"
+ },
+ {
+ "step": 7,
+ "description": "加盐、胡椒粉腌制 15-20 分钟"
+ },
+ {
+ "step": 8,
+ "description": "加水煮开"
+ },
+ {
+ "step": 9,
+ "description": "加入姜片和牛肉片,煮至牛肉变色"
+ },
+ {
+ "step": 10,
+ "description": "加入番茄块,煮至番茄变软"
+ },
+ {
+ "step": 11,
+ "description": "打散鸡蛋液,缓慢地倒入锅中,用筷子搅拌形成蛋花"
+ },
+ {
+ "step": 12,
+ "description": "加入盐和胡椒粉调味"
+ },
+ {
+ "step": 13,
+ "description": "最后加入葱花,即可出锅"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-soup-皮蛋瘦肉粥",
+ "name": "皮蛋瘦肉粥的做法",
+ "description": "# 皮蛋瘦肉粥的做法\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/soup/皮蛋瘦肉粥.md",
+ "image_path": null,
+ "images": [],
+ "category": "汤",
+ "difficulty": 3,
+ "tags": [
+ "汤"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "饮用水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 饮用水",
+ "notes": "量未指定"
+ },
+ {
+ "name": "皮蛋(松花蛋)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 皮蛋(松花蛋)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "瘦肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 瘦肉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大米",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大米",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香菜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生菜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生姜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酱油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酱油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蚝油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蚝油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "胡椒粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 胡椒粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "电饭锅",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 电饭锅",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小碗若干",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小碗若干",
+ "notes": "量未指定"
+ },
+ {
+ "name": "饮用水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 饮用水 1 升",
+ "notes": "量未指定"
+ },
+ {
+ "name": "皮蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 皮蛋 2 颗",
+ "notes": "量未指定"
+ },
+ {
+ "name": "瘦肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 瘦肉 100g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大米",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大米 150ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小葱 1 棵",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香菜 1 棵",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生菜 4 叶",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生姜 1 拇指块",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酱油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酱油 5 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蚝油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蚝油 5 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 2g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "胡椒粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 胡椒粉 1g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 10ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小葱 - 洗净 - 去除根部 - 去除枯黄枯黑无法食用部分 - 切碎 - 放入小碗备用",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小葱 - 洗净 - 去除根部 - 去除枯黄枯黑无法食用部分 - 切碎 - 放入小碗备用",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香菜 - 洗净 - 去除根部 - 去除枯黄枯黑无法食用部分 - 切碎 - 放入小碗备用",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香菜 - 洗净 - 去除根部 - 去除枯黄枯黑无法食用部分 - 切碎 - 放入小碗备用",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生菜 - 洗净 - 去除根部 - 去除枯黄枯黑无法食用部分 - 切碎 - 放入小碗备用",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生菜 - 洗净 - 去除根部 - 去除枯黄枯黑无法食用部分 - 切碎 - 放入小碗备用",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "大米 - 洗净 - 放入电饭锅内胆 - 加入 1 升 饮用水"
+ },
+ {
+ "step": 2,
+ "description": "瘦肉 - 洗净 - 简易晾去水分 - 加入 10ml 食用油 - 揉搓均匀 - 放入电饭锅内胆"
+ },
+ {
+ "step": 3,
+ "description": "皮蛋 - 去壳 - 洗净 - 对半切开 - 分离蛋白蛋黄 - 蛋白简单切碎块 - 蛋黄揉碎 - 放入电饭锅内胆"
+ },
+ {
+ "step": 4,
+ "description": "生姜 - 洗净 - 削皮 - 去除枯黄枯黑无法食用部分 - 切丝 - 放入电饭锅内胆"
+ },
+ {
+ "step": 5,
+ "description": "小葱 - 洗净 - 去除根部 - 去除枯黄枯黑无法食用部分 - 切碎 - 放入小碗备用"
+ },
+ {
+ "step": 6,
+ "description": "香菜 - 洗净 - 去除根部 - 去除枯黄枯黑无法食用部分 - 切碎 - 放入小碗备用"
+ },
+ {
+ "step": 7,
+ "description": "生菜 - 洗净 - 去除根部 - 去除枯黄枯黑无法食用部分 - 切碎 - 放入小碗备用"
+ },
+ {
+ "step": 8,
+ "description": "酱油 + 蚝油 + 盐 + 胡椒粉 - 搅拌均匀 - 放入小碗备用"
+ },
+ {
+ "step": 9,
+ "description": "主料 - 使用电饭锅煮粥模式煮熟"
+ },
+ {
+ "step": 10,
+ "description": "配料 - 待主料煮熟后,生菜单独过一次热水,并与其余配料一同开盖加入主料中搅拌均匀"
+ },
+ {
+ "step": 11,
+ "description": "酱料 - 待主料煮熟后,与其余配料一同开盖加入主料中搅拌均匀"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-soup-米粥",
+ "name": "米粥的做法",
+ "description": "# 米粥的做法\n\n大米粥是一道以大米和水作為主要原料經大火煮沸熬製而成的美食,老少皆宜,米粥具有補脾、和胃、清肺功效。\n\n预估烹饪难度:★★",
+ "source_path": "dishes/soup/米粥.md",
+ "image_path": null,
+ "images": [],
+ "category": "汤",
+ "difficulty": 2,
+ "tags": [
+ "汤"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "米",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 米",
+ "notes": "量未指定"
+ },
+ {
+ "name": "水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 水",
+ "notes": "量未指定"
+ },
+ {
+ "name": "植物油(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 植物油(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "一般一个人可以食用",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 一般一个人可以食用 60ml-110ml 的米。",
+ "notes": "量未指定"
+ },
+ {
+ "name": "水的体积是米饭的体积的",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 水的体积是米饭的体积的 9-12 倍。",
+ "notes": "量未指定"
+ },
+ {
+ "name": "一碗容量是",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 一碗容量是 500ml。",
+ "notes": "量未指定"
+ },
+ {
+ "name": "中断大火加热的最晚时间 T1:1.5 分钟/500ml",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 中断大火加热的最晚时间 T1:1.5 分钟/500ml * 水体积",
+ "notes": "量未指定"
+ },
+ {
+ "name": "米粥能够食用的最早时间 Tr:10 分钟/500ml",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 米粥能够食用的最早时间 Tr:10 分钟/500ml * 水体积",
+ "notes": "量未指定"
+ },
+ {
+ "name": "油的质量 Mo:生米体积 /",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 油的质量 Mo:生米体积 / 10",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冷藏时间 Tc = 生米体积 /10 ml/分钟。",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冷藏时间 Tc = 生米体积 /10 ml/分钟。",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "(可选)将 Mo ml 的油与洗净的米混合,*尽量确保完全混合,即每粒米上至少都沾上少量油*"
+ },
+ {
+ "step": 2,
+ "description": "(可选)将 米-油混合物品冷藏保存,冷藏时间 Tc。"
+ },
+ {
+ "step": 3,
+ "description": "将米和水加入锅中。"
+ },
+ {
+ "step": 4,
+ "description": "开大火,加热到 T1。"
+ },
+ {
+ "step": 5,
+ "description": "在 T1 之前将火关小。**如果忘记此步骤,水可能会漫出而熄灭火焰。非常危险!**"
+ },
+ {
+ "step": 6,
+ "description": "加热到 Tr。在 Tr 时关闭火源。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-soup-紫菜蛋花汤",
+ "name": "紫菜蛋花汤的做法",
+ "description": "# 紫菜蛋花汤的做法\n\n预估烹饪难度:★★",
+ "source_path": "dishes/soup/紫菜蛋花汤.md",
+ "image_path": null,
+ "images": [],
+ "category": "汤",
+ "difficulty": 2,
+ "tags": [
+ "汤"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "紫菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 紫菜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱花",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱花",
+ "notes": "量未指定"
+ },
+ {
+ "name": "水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 水",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "虾仁(个人口味,可加可不加)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 虾仁(个人口味,可加可不加)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "10g 的干紫菜(喜欢紫菜的可以多放些)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 10g 的干紫菜(喜欢紫菜的可以多放些)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "两个鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 两个鸡蛋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 2 克",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "干紫菜用清水泡 15 分钟,捞起沥干水份备用。"
+ },
+ {
+ "step": 2,
+ "description": "热锅,倒入 1.5 升清水、5ml 油、2g 盐。待水开后放入紫菜。"
+ },
+ {
+ "step": 3,
+ "description": "紫菜烧开后 3 分钟,将打好的蛋液徐徐倒入锅内,30 秒既可起锅。"
+ },
+ {
+ "step": 4,
+ "description": "撒上葱花,转小火 20 秒。"
+ },
+ {
+ "step": 5,
+ "description": "关火,出锅前放入几滴香油,也有的会放入一点虾皮,味道也不错。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-soup-罗宋汤",
+ "name": "罗宋汤的做法",
+ "description": "# 罗宋汤的做法\n\n罗宋汤是一道源自俄罗斯甜菜汤的汤品,在传入上海后有了本土化的做法。其制作较为简单,初学者只需要 2-3 小时即可完成。\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/soup/罗宋汤.md",
+ "image_path": null,
+ "images": [],
+ "category": "汤",
+ "difficulty": 4,
+ "tags": [
+ "汤"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "蔬菜高汤(欧芹、胡萝卜、洋葱三件套)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蔬菜高汤(欧芹、胡萝卜、洋葱三件套)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "牛肉高汤(可用〇汤宝代替)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 牛肉高汤(可用〇汤宝代替)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "牛肉(可选牛腩肉或牛尾肉)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 牛肉(可选牛腩肉或牛尾肉)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "番茄(番茄膏、番茄罐头)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 番茄(番茄膏、番茄罐头)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "牛肉高汤",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 牛肉高汤 500 mL",
+ "notes": "量未指定"
+ },
+ {
+ "name": "牛肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 牛肉 250 g (可选用牛腩肉或牛尾肉)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "番茄罐头",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 番茄罐头 2 罐 (可用番茄替代、但风味欠佳)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "番茄膏",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 番茄膏 5 g (增加番茄风味)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "马铃薯",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 马铃薯 400 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "洋葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 洋葱 100 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "胡萝卜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 胡萝卜 100 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "欧芹",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 欧芹 100 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "包菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 包菜 200 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "红肠",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 红肠 100 - 200 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "橄榄油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 橄榄油 5 mL (橄榄油用于蔬菜的烹制,可以用植物油代替)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "植物油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 植物油 5 mL (植物油用于牛肉的烹制,不能用橄榄油代替)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 18 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黑胡椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黑胡椒 3 g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "洋葱、胡萝卜、欧芹切 1cm 见方小丁"
+ },
+ {
+ "step": 2,
+ "description": "红肠、马铃薯切 2cm 块"
+ },
+ {
+ "step": 3,
+ "description": "包菜去梗后,手撕至 2cm 片"
+ },
+ {
+ "step": 4,
+ "description": "牛肉撒盐 3 g 、黑胡椒 3 g 腌制 5 分钟"
+ },
+ {
+ "step": 5,
+ "description": "平底锅烧热,加入植物油"
+ },
+ {
+ "step": 6,
+ "description": "煎制牛肉,直至表面**焦黄色**(可以带生,千万别糊了),取出备用。"
+ },
+ {
+ "step": 7,
+ "description": "汤锅烧热,加入橄榄油、洋葱丁、胡萝卜丁、欧芹丁"
+ },
+ {
+ "step": 8,
+ "description": "炒至**洋葱透明**,加入番茄膏、番茄罐头"
+ },
+ {
+ "step": 9,
+ "description": "加入牛肉、马铃薯丁,翻炒均匀"
+ },
+ {
+ "step": 10,
+ "description": "加水没过食材,中火烹制 1 小时"
+ },
+ {
+ "step": 11,
+ "description": "开锅加入包菜丁、红肠丁,搅拌均匀"
+ },
+ {
+ "step": 12,
+ "description": "中火烹制半小时"
+ },
+ {
+ "step": 13,
+ "description": "开盖加入剩余 15 g 盐,混合均匀后盛盘"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-soup-腊八粥",
+ "name": "腊八粥的做法",
+ "description": "# 腊八粥的做法\n\n> 无论盛在哪里的腊八粥,自然会熬煮过去。一年的酸甜苦辣涩。—— 迷迭香《腊八粥》\n\n腊八粥,又称七宝五味粥、佛粥、大家饭等,是一种由多样食材熬制而成的粥。主要富含碳水化合物、磷镁元素和各类维生素等,不仅可以补充日常的能量,其中的莲子还有养心安神的作用,适合工作压力大的人食用。除去食材准备时间,一般只需要 3 小时即可完成。\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/soup/腊八粥.md",
+ "image_path": null,
+ "images": [],
+ "category": "汤",
+ "difficulty": 4,
+ "tags": [
+ "汤"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "饮用水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 饮用水",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大米",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大米",
+ "notes": "量未指定"
+ },
+ {
+ "name": "糯米",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 糯米",
+ "notes": "量未指定"
+ },
+ {
+ "name": "花生",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 花生",
+ "notes": "量未指定"
+ },
+ {
+ "name": "红豆",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 红豆",
+ "notes": "量未指定"
+ },
+ {
+ "name": "红枣",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 红枣",
+ "notes": "量未指定"
+ },
+ {
+ "name": "粥锅(普通锅容易糊底,有条件可选择高压锅)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 粥锅(普通锅容易糊底,有条件可选择高压锅)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "中号玻璃碗(或其他中号不锈钢容器)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 中号玻璃碗(或其他中号不锈钢容器)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小碗若干",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小碗若干",
+ "notes": "量未指定"
+ },
+ {
+ "name": "饮用水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 饮用水 1 L",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大米",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大米 50 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "糯米",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 糯米 50 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "薏米",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 薏米 50 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黑米",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黑米 50 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小米",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小米 50 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "莲子",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 莲子 25 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "绿豆",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 绿豆 25 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "红豆",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 红豆 25 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "花生",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 花生 25 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黄豆",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黄豆 25 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "豌豆",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 豌豆 25 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "红枣",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 红枣 25 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "桂圆",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 桂圆 25 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "栗子",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 栗子 25 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "去壳核桃",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 去壳核桃 25 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葡萄干",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葡萄干 25 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "红腰豆",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 红腰豆 25 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冰糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冰糖 10~25 g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "提前洗净好绿豆、红豆、花生、黄豆、豌豆、红腰豆,并用干净的玻璃碗盛放好,注入 3/4 玻璃碗大小的饮用水,浸泡一夜(或最少 8 小时)。"
+ },
+ {
+ "step": 2,
+ "description": "提前洗净好大米、糯米、薏米、黑米、小米、莲子,并用干净的玻璃碗盛放好,注入 3/4 玻璃碗大小的饮用水,浸泡 3 小时。"
+ },
+ {
+ "step": 3,
+ "description": "将步骤 1 中准备好的盛有绿豆、红豆、花生、黄豆、豌豆、红腰豆的玻璃碗中的水分分离倒出,其余原料倒入粥锅中,加入 1 升饮用水(或漫过食材 1 拇指块),大火煮沸,煮沸后合上锅盖,小火煮 30 分钟。"
+ },
+ {
+ "step": 4,
+ "description": "将步骤 2 中准备好的盛有大米、糯米、薏米、黑米、小米、莲子的玻璃碗中的水分分离倒出,其余原料继续倒入粥锅中,合上锅盖,小火煮 60 分钟。"
+ },
+ {
+ "step": 5,
+ "description": "洗净好红枣、桂圆、栗子、核桃、葡萄干(其中红枣切成小片)、冰糖,倒入锅中,合上锅盖,小火煮 60 分钟。"
+ },
+ {
+ "step": 6,
+ "description": "确认煮出的粥粘稠后即可关火、盛盘、食用。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-soup-西红柿鸡蛋汤",
+ "name": "西红柿鸡蛋汤的做法",
+ "description": "# 西红柿鸡蛋汤的做法\n\n预估烹饪难度:★★",
+ "source_path": "dishes/soup/西红柿鸡蛋汤.md",
+ "image_path": null,
+ "images": [],
+ "category": "汤",
+ "difficulty": 2,
+ "tags": [
+ "汤"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "西红柿",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 西红柿",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "味素",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 味素",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱、姜、蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱、姜、蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "西红柿",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 西红柿 1 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋 1-2 个(依照自己的口味而定,喜欢吃鸡蛋就放 2 个,一般就放 1 个)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香油 2 滴",
+ "notes": "量未指定"
+ },
+ {
+ "name": "味素",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 味素 5 克(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 15 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱、姜、蒜共",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱、姜、蒜共 15 克",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-soup-金针菇汤",
+ "name": "金针菇汤的做法",
+ "description": "# 金针菇汤的做法\n\n预估烹饪难度:★★",
+ "source_path": "dishes/soup/金针菇汤.md",
+ "image_path": null,
+ "images": [],
+ "category": "汤",
+ "difficulty": 2,
+ "tags": [
+ "汤"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "金针菇",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 金针菇",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋(如需要)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋(如需要)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "金针菇",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 金针菇 400-500 克 (市场里面售卖的一袋即可)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食盐 15 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "味精",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 味精 5 克",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-soup-陈皮排骨汤",
+ "name": "陈皮排骨汤的做法",
+ "description": "# 陈皮排骨汤的做法\n\n新鲜的排骨除了拿来烧或者炖之外,还可以用来煲汤,搭配广东陈皮煲出来的汤非常养生,对脾胃、肺及咽喉都有一定的滋补功效,熬夜党必备。\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/soup/陈皮排骨汤.md",
+ "image_path": null,
+ "images": [],
+ "category": "汤",
+ "difficulty": 4,
+ "tags": [
+ "汤"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "排骨",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 排骨",
+ "notes": "量未指定"
+ },
+ {
+ "name": "陈皮",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 陈皮",
+ "notes": "量未指定"
+ },
+ {
+ "name": "西洋参",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 西洋参",
+ "notes": "量未指定"
+ },
+ {
+ "name": "石斛",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 石斛",
+ "notes": "量未指定"
+ },
+ {
+ "name": "玉竹",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 玉竹",
+ "notes": "量未指定"
+ },
+ {
+ "name": "麦冬",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 麦冬",
+ "notes": "量未指定"
+ },
+ {
+ "name": "煲汤盅",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 煲汤盅",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "排骨,用猪骨也替代也可,4-5 块",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 排骨,用猪骨也替代也可,4-5 块",
+ "notes": "量未指定"
+ },
+ {
+ "name": "陈皮(一般选用",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 陈皮(一般选用 8-20 年制),一般 1 块陈皮是 3 瓣,取 1 瓣即可",
+ "notes": "量未指定"
+ },
+ {
+ "name": "西洋参(又名花旗参),9 片",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 西洋参(又名花旗参),9 片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "石斛,",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 石斛, 6 颗",
+ "notes": "量未指定"
+ },
+ {
+ "name": "玉竹,",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 玉竹, 5 片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "麦冬,",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 麦冬, 7 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "煲汤盅,按",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 煲汤盅,按 1 人份",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食盐 ,5g",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食盐 ,5g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "排骨用热水过一边,去血水"
+ },
+ {
+ "step": 2,
+ "description": "陈皮、麦冬、玉竹、石斛和西洋参,冲洗干净即可"
+ },
+ {
+ "step": 3,
+ "description": "煲汤盅洗干净"
+ },
+ {
+ "step": 4,
+ "description": "打开煲汤盅,先放入排骨在底部,然后依次放入陈皮、麦冬、玉竹、石斛和西洋参"
+ },
+ {
+ "step": 5,
+ "description": "加入热水进煲汤盅,水不宜太满"
+ },
+ {
+ "step": 6,
+ "description": "煲汤容器加入水,炖煮 1.5 小时即可"
+ },
+ {
+ "step": 7,
+ "description": "加入食盐,趁热饮用"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-soup-勾芡香菇汤-勾芡香菇汤",
+ "name": "勾芡香菇汤的做法",
+ "description": "# 勾芡香菇汤的做法\n\n鲜香菇除了拿来和肉炒外,其实拿来做浓浓的勾芡汤也是非常可口的。\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/soup/勾芡香菇汤/勾芡香菇汤.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/soup/勾芡香菇汤/1.jpeg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/soup/勾芡香菇汤/1.jpeg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/soup/勾芡香菇汤/2.jpeg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/soup/勾芡香菇汤/3.jpeg"
+ ],
+ "category": "汤",
+ "difficulty": 3,
+ "tags": [
+ "汤"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "香菇",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香菇",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡精",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡精",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鲜香菇",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鲜香菇 2 朵",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香葱 0.5 根",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡精",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡精 3 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 10 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用盐 3 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "开水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 开水 350 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生粉 10 g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "香菇切片(每片厚度 0.5-1 cm,厚点相对薄点更有嚼劲),放入大碗中,倒入 2g 食用盐 浸泡 15 分钟"
+ },
+ {
+ "step": 2,
+ "description": "生粉倒入小碗中,加入 50ml 水,搅拌生粉直至融化没有颗粒(即水淀粉)"
+ },
+ {
+ "step": 3,
+ "description": "倒掉碗中的盐水,适当去掉香菇本身的水分(方便下一步煎炸)【可选】"
+ },
+ {
+ "step": 4,
+ "description": "小火,倒入油,待油开始冒小泡(小火 30s ,看每个锅的功率),倒入香菇,每面煎 10s 【可选】"
+ },
+ {
+ "step": 5,
+ "description": "倒入开水 300ml ,调中火再煮 3-5 分钟"
+ },
+ {
+ "step": 6,
+ "description": "倒入水淀粉,适当搅拌锅中汤汁后,加入 3g 盐、3 g ,最后撒上葱花出锅"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-soup-排骨苦瓜汤-排骨苦瓜汤",
+ "name": "排骨苦瓜汤的做法",
+ "description": "# 排骨苦瓜汤的做法\n\n排骨苦瓜汤是一道味道鲜美且容易烹饪的汤。不过汤的烹饪时间都较长,一般来说最好提前 4 个小时开始进行准备。\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/soup/排骨苦瓜汤/排骨苦瓜汤.md",
+ "image_path": null,
+ "images": [],
+ "category": "汤",
+ "difficulty": 4,
+ "tags": [
+ "汤"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "电压力锅(可以极大简化烹饪过程和时间)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 电压力锅(可以极大简化烹饪过程和时间)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "砂锅(相比于炒锅更适合炖汤)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 砂锅(相比于炒锅更适合炖汤)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "排骨",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 排骨",
+ "notes": "量未指定"
+ },
+ {
+ "name": "苦瓜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 苦瓜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "虾皮",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 虾皮",
+ "notes": "量未指定"
+ },
+ {
+ "name": "排骨",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 排骨 250g 到 500g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "苦瓜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 苦瓜 100g 到 200g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "虾皮",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 虾皮 5g 到 15g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生姜 5~10g(可选,用于焯水时去腥)",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "排骨洗净,切到约 4cm ±2cm * 3 ± 2cm 的小块(如没有剁排骨的工具,可以求助摊主)"
+ },
+ {
+ "step": 2,
+ "description": "炒锅倒入冷水 700ml 和排骨一起加热至煮沸,关火捞出排骨"
+ },
+ {
+ "step": 3,
+ "description": "苦瓜中间切为两半,清除干净内部的种子和苦瓜瓤,切为 0.5 ± 0.3 cm 的苦瓜条,洗净"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-soup-昂刺鱼豆腐汤-昂刺鱼豆腐汤",
+ "name": "昂刺鱼豆腐汤的做法",
+ "description": "# 昂刺鱼豆腐汤的做法\n\n- 昂刺鱼/沙光鱼 豆腐汤 刺少 肉嫩 营养丰盛、适合任何年龄的小伙伴\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/soup/昂刺鱼豆腐汤/昂刺鱼豆腐汤.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/soup/昂刺鱼豆腐汤/昂刺鱼豆腐汤01.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/soup/昂刺鱼豆腐汤/昂刺鱼豆腐汤01.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/soup/昂刺鱼豆腐汤/昂刺鱼豆腐汤02.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/soup/昂刺鱼豆腐汤/沙光鱼豆腐汤.jpg"
+ ],
+ "category": "汤",
+ "difficulty": 3,
+ "tags": [
+ "汤"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "昂刺鱼或者沙光鱼",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 昂刺鱼或者沙光鱼",
+ "notes": "量未指定"
+ },
+ {
+ "name": "豆腐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 豆腐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "胡椒粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 胡椒粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "昂刺鱼或者沙光鱼 一条",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 昂刺鱼或者沙光鱼 一条",
+ "notes": "量未指定"
+ },
+ {
+ "name": "豆腐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 豆腐 100 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香葱 一根",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香葱 一根",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜 一块",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜 一块",
+ "notes": "量未指定"
+ },
+ {
+ "name": "胡椒粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 胡椒粉 3-5 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 15 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用盐 10-15 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "开水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 开水 1L",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "鱼处理好后洗净,(特别注意肚内的血丝、不洗干净会有腥味),放入大碗中,倒入料酒、10g 姜片、5g 盐,腌制 15 分钟"
+ },
+ {
+ "step": 2,
+ "description": "豆腐切块,放入凉水浸泡 5 分钟,捞出备用"
+ },
+ {
+ "step": 3,
+ "description": "煎鱼前,先用生姜片擦一下锅防止粘锅,倒入油(油量为 15ml * 鱼的条数 ),烧热后放入鱼煎 2~3 分钟,期间需要晃动一下鱼,防止粘底,且需要翻一次身"
+ },
+ {
+ "step": 4,
+ "description": "待鱼全部煎好之后,倒入开水、5ml 料酒、姜片,小火转至大火,盖上锅盖、大火煮 10 分钟(水要稍微多一些,后面会蒸发掉一些)"
+ },
+ {
+ "step": 5,
+ "description": "见汤变白后倒入准备好的豆腐,调中火再煮 5 分钟,加入 10g 盐、3g 胡椒粉调味,最后撒上葱花出锅"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-soup-朱雀汤-朱雀汤",
+ "name": "朱雀汤的做法",
+ "description": "# 朱雀汤的做法\n\n预估烹饪难度:★",
+ "source_path": "dishes/soup/朱雀汤/朱雀汤.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/soup/朱雀汤/朱雀汤.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/soup/朱雀汤/朱雀汤.jpg"
+ ],
+ "category": "汤",
+ "difficulty": 1,
+ "tags": [
+ "汤"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香油(芝麻油)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香油(芝麻油)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 水",
+ "notes": "量未指定"
+ },
+ {
+ "name": "一个鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 一个鸡蛋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "500ml 水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 500ml 水",
+ "notes": "量未指定"
+ },
+ {
+ "name": "20 克白糖(根据个人口味调整)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 20 克白糖(根据个人口味调整)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "2ml 香油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 2ml 香油",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "鸡蛋在碗中打散,再倒入香油。"
+ },
+ {
+ "step": 2,
+ "description": "水烧开后,在沸腾状态下快速倒入盛有鸡蛋的碗中。"
+ },
+ {
+ "step": 3,
+ "description": "放入白糖。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-soup-玉米排骨汤-玉米排骨汤",
+ "name": "玉米排骨汤的做法",
+ "description": "# 玉米排骨汤的做法\n\n新鲜的排骨除了拿来烧或者炖之外,还可以用来煲汤,搭配玉米和胡萝卜煲出来的汤非常鲜美。\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/soup/玉米排骨汤/玉米排骨汤.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/soup/玉米排骨汤/玉米排骨汤.jpeg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/soup/玉米排骨汤/玉米排骨汤.jpeg"
+ ],
+ "category": "汤",
+ "difficulty": 3,
+ "tags": [
+ "汤"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "排骨",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 排骨",
+ "notes": "量未指定"
+ },
+ {
+ "name": "玉米",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 玉米",
+ "notes": "量未指定"
+ },
+ {
+ "name": "胡箩卜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 胡箩卜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生姜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "醋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 醋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黑胡椒粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黑胡椒粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "砂锅(没有的话,用铁锅也行)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 砂锅(没有的话,用铁锅也行)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "排骨",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 排骨 500-800g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "玉米一根(喜欢吃玉米的可以多一根)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 玉米一根(喜欢吃玉米的可以多一根)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "胡萝卜一根(喜欢吃胡箩卜的可以多一根)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 胡萝卜一根(喜欢吃胡箩卜的可以多一根)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大葱小半根",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大葱小半根",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香葱一根",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香葱一根",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 10 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黑胡椒粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黑胡椒粉 4g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒 10ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "醋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 醋 10ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用盐 10-15g(根据最后汤剩余的量而定)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "开水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 开水 1000 ml",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "大葱切成 3-4cm 的大段,用刀背拍一下"
+ },
+ {
+ "step": 2,
+ "description": "玉米剁成小块"
+ },
+ {
+ "step": 3,
+ "description": "胡箩卜切成滚刀块"
+ },
+ {
+ "step": 4,
+ "description": "生姜去皮切大片"
+ },
+ {
+ "step": 5,
+ "description": "新鲜的排骨砍成小块"
+ },
+ {
+ "step": 6,
+ "description": "排骨凉水下锅,放入大葱、生姜、料酒开始焯水,大火烧开,撇去浮沫,捞出排骨,沥干水分"
+ },
+ {
+ "step": 7,
+ "description": "热锅凉油,切大片的生姜和排骨一起下锅煸炒,待排骨表面微微焦黄,放入醋(可加速肉质软烂),继续煸炒一分钟"
+ },
+ {
+ "step": 8,
+ "description": "冲入开水,一次给足,之后就不要再加了,大火烧开"
+ },
+ {
+ "step": 9,
+ "description": "先下入玉米,放入胡椒粉,盖盖小火炖二十分钟,然后放入胡萝卜,盖盖继续小火炖四十分钟"
+ },
+ {
+ "step": 10,
+ "description": "调味很简单,出锅前三分钟,除了盐什么都不用放,最后撒上一把小葱花即可"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-soup-羊肉汤-羊肉汤",
+ "name": "羊肉汤的做法",
+ "description": "# 羊肉汤的做法\n\n\n\n羊肉汤/羊肉汤简单易,有抵御寒冷、温润养胃、开胃健脾的功效,富含钙、铁、蛋白质等营养物质。\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/soup/羊肉汤/羊肉汤.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/soup/羊肉汤/羊肉汤.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/soup/羊肉汤/羊肉汤.jpg"
+ ],
+ "category": "汤",
+ "difficulty": 3,
+ "tags": [
+ "汤"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "羊肉或羊杂",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 羊肉或羊杂",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白胡椒粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白胡椒粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "孜然粉(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 孜然粉(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香菜(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香菜(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "羊肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 羊肉 300g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 10ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒 20ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大葱 50g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "开水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 开水 1000ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白胡椒粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白胡椒粉 1g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用盐 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "孜然粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 孜然粉 1g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香菜 20g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "羊肉切成长 5cm 宽 0.5cm 的块"
+ },
+ {
+ "step": 2,
+ "description": "大葱切成小段"
+ },
+ {
+ "step": 3,
+ "description": "羊肉放入锅中,加入 1000ml 常温水,加入料酒、大葱"
+ },
+ {
+ "step": 4,
+ "description": "煮沸 2 分钟后,捞出羊肉,使用常温水洗净,沥干水分"
+ },
+ {
+ "step": 5,
+ "description": "热锅加入食用油,加入羊肉,翻炒 2 分钟至羊肉表面微黄"
+ },
+ {
+ "step": 6,
+ "description": "加入开水,开到大火档位"
+ },
+ {
+ "step": 7,
+ "description": "5 分钟后,加入白胡椒粉、盐,继续煮沸 5 分钟"
+ },
+ {
+ "step": 8,
+ "description": "出锅之后,加入香菜、孜然粉,搅拌均匀"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-soup-菌菇炖乳鸽-菌菇炖乳鸽",
+ "name": "菌菇炖乳鸽的做法",
+ "description": "# 菌菇炖乳鸽的做法\n\n- 菌菇炖乳鸽 汤鲜、肉嫩、营养丰富\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/soup/菌菇炖乳鸽/菌菇炖乳鸽.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/soup/菌菇炖乳鸽/菌菇炖乳鸽.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/soup/菌菇炖乳鸽/菌菇炖乳鸽.jpg"
+ ],
+ "category": "汤",
+ "difficulty": 4,
+ "tags": [
+ "汤"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "乳鸽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 乳鸽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "菌菇",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 菌菇",
+ "notes": "量未指定"
+ },
+ {
+ "name": "玉米",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 玉米",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "瓦罐或者高压锅",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 瓦罐或者高压锅",
+ "notes": "量未指定"
+ },
+ {
+ "name": "乳鸽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 乳鸽 300 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "菌菇",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 菌菇 100 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "玉米",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 玉米 200 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜 30 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒 15 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用盐 10 g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "冷水洗干净热心摊主处理好的乳鸽"
+ },
+ {
+ "step": 2,
+ "description": "冷水锅中放入洗干净的乳鸽,加入 15ml 料酒与姜,水煮开即可捞出乳鸽,要不然会丢失营养"
+ },
+ {
+ "step": 3,
+ "description": "把乳鸽放到高压缩或者瓦罐中、倒入的水要没过乳鸽,放入生姜 20 g,玉米 200 g、菌菇 100 g"
+ },
+ {
+ "step": 4,
+ "description": "时间到了,盛到碗中,加入 3~5g 盐 即可"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-soup-银耳莲子粥-银耳莲子粥",
+ "name": "银耳莲子粥的做法",
+ "description": "# 银耳莲子粥的做法\n\n\n\n银耳莲子粥是一道营养非常丰富的粥。口味偏甜,具有养心安神的功效。\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/soup/银耳莲子粥/银耳莲子粥.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/soup/银耳莲子粥/银耳莲子粥.png",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/soup/银耳莲子粥/银耳莲子粥.png"
+ ],
+ "category": "汤",
+ "difficulty": 4,
+ "tags": [
+ "汤"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "银耳",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 银耳",
+ "notes": "量未指定"
+ },
+ {
+ "name": "去心莲子",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 去心莲子",
+ "notes": "量未指定"
+ },
+ {
+ "name": "红枣",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 红枣",
+ "notes": "量未指定"
+ },
+ {
+ "name": "枸杞(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 枸杞(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冰糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冰糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "银耳",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 银耳 60g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "去心莲子",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 去心莲子 20g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "红枣",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 红枣 6g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "枸杞",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 枸杞 5-6g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冰糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冰糖 10-20g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "把银耳、莲子用清水浸泡 2 个小时,红枣浸泡 10 - 20 分钟,枸杞洗净,备用"
+ },
+ {
+ "step": 2,
+ "description": "在锅中倒入 600ml 水,烧开后依次放入银耳、莲子、红枣"
+ },
+ {
+ "step": 3,
+ "description": "等待水再次烧开后,盖上锅盖,转至中火继续熬"
+ },
+ {
+ "step": 4,
+ "description": "熬到大约 1 小时后,放入 5g - 10g 冰糖和 5g - 6g 枸杞,转至小火熬"
+ },
+ {
+ "step": 5,
+ "description": "小火继续熬 30 分钟,此时银耳开始呈现粘稠状态"
+ },
+ {
+ "step": 6,
+ "description": "再次放入 5g - 10g 冰糖,用勺子搅拌 5 - 10 分钟"
+ },
+ {
+ "step": 7,
+ "description": "关火,用勺子盛出"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-soup-陈皮排骨汤-陈皮排骨汤",
+ "name": "陈皮排骨汤的做法",
+ "description": "# 陈皮排骨汤的做法\n\n新鲜的排骨除了拿来烧或者炖之外,还可以用来煲汤,搭配广东陈皮煲出来的汤非常养生,对脾胃、肺及咽喉都有一定的滋补功效,熬夜党必备。\n\n\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/soup/陈皮排骨汤/陈皮排骨汤.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/soup/陈皮排骨汤/陈皮排骨汤.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/soup/陈皮排骨汤/陈皮排骨汤.jpg"
+ ],
+ "category": "汤",
+ "difficulty": 3,
+ "tags": [
+ "汤"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "排骨",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 排骨",
+ "notes": "量未指定"
+ },
+ {
+ "name": "陈皮",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 陈皮",
+ "notes": "量未指定"
+ },
+ {
+ "name": "西洋参",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 西洋参",
+ "notes": "量未指定"
+ },
+ {
+ "name": "石斛",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 石斛",
+ "notes": "量未指定"
+ },
+ {
+ "name": "玉竹",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 玉竹",
+ "notes": "量未指定"
+ },
+ {
+ "name": "麦冬",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 麦冬",
+ "notes": "量未指定"
+ },
+ {
+ "name": "煲汤盅",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 煲汤盅",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "排骨,用猪骨也替代也可,4-5 块",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 排骨,用猪骨也替代也可,4-5 块",
+ "notes": "量未指定"
+ },
+ {
+ "name": "陈皮(一般选用",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 陈皮(一般选用 8-20 年制),一般 1 块陈皮是 3 瓣,取 1 瓣即可",
+ "notes": "量未指定"
+ },
+ {
+ "name": "西洋参(又名花旗参),9 片",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 西洋参(又名花旗参),9 片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "石斛,",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 石斛, 6 颗",
+ "notes": "量未指定"
+ },
+ {
+ "name": "玉竹,",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 玉竹, 5 片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "麦冬,",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 麦冬, 7 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "煲汤盅,按",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 煲汤盅,按 1 人份",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食盐 ,5g",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食盐 ,5g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "排骨用热水过一边,去血水"
+ },
+ {
+ "step": 2,
+ "description": "陈皮、麦冬、玉竹、石斛和西洋参,冲洗干净即可"
+ },
+ {
+ "step": 3,
+ "description": "煲汤盅洗干净"
+ },
+ {
+ "step": 4,
+ "description": "打开煲汤盅,先放入排骨在底部,然后依次放入陈皮、麦冬、玉竹、石斛和西洋参"
+ },
+ {
+ "step": 5,
+ "description": "加入热水进煲汤盅,水不宜太满"
+ },
+ {
+ "step": 6,
+ "description": "煲汤容器加入水,炖煮 1.5 小时即可"
+ },
+ {
+ "step": 7,
+ "description": "加入食盐,趁热饮用"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-staple-手工水饺",
+ "name": "手工水饺的做法",
+ "description": "# 手工水饺的做法\n\n饺子是一道非常好吃的主食之一。饱肚且易于根据自己口味进行调味,适合在 US 的同学吃不到水饺解馋。一般初学者需要 3 小时完成,难度较大\n\n预估烹饪难度:★★★★★",
+ "source_path": "dishes/staple/手工水饺.md",
+ "image_path": null,
+ "images": [],
+ "category": "主食",
+ "difficulty": 5,
+ "tags": [
+ "主食"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "擀面杖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 擀面杖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "面粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 面粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冷水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冷水",
+ "notes": "量未指定"
+ },
+ {
+ "name": "直径",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 直径 30cm 以上的盆",
+ "notes": "量未指定"
+ },
+ {
+ "name": "芝麻香油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 芝麻香油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "单人,约",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 单人,约 20 只",
+ "notes": "量未指定"
+ },
+ {
+ "name": "面粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 面粉 200g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冷水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冷水 150ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "芝麻香油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 芝麻香油 2-3ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "瘦肉末",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 瘦肉末 250g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "肥肉末",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 肥肉末 20g #不喜可不加",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜 3g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱 15g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 3g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蚝油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蚝油 2ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香油 2ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 2ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋 1 个",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "盆中加入所有面粉"
+ },
+ {
+ "step": 2,
+ "description": "加入芝麻香油"
+ },
+ {
+ "step": 3,
+ "description": "面粉中央挖小洞"
+ },
+ {
+ "step": 4,
+ "description": "分 4-5 次加入水,并搅和,当出现碎末状的稍微干燥面团时"
+ },
+ {
+ "step": 5,
+ "description": "取消加水,用手将面团压实"
+ },
+ {
+ "step": 6,
+ "description": "面团压实至可把盆周围的面粉纳入即可,此步骤为面光盆光"
+ },
+ {
+ "step": 7,
+ "description": "将面团置于桌上,盆倒扣于桌上,环境温度为 25 度,使面团醒发约 45 分钟"
+ },
+ {
+ "step": 8,
+ "description": "醒发完成后,将面团搓成条状,合成一团,再次搓成条,重复 3 次"
+ },
+ {
+ "step": 9,
+ "description": "擀成条状,切成 20 份均匀大小面团,并搓成直径约 3-3.5cm 的球状"
+ },
+ {
+ "step": 10,
+ "description": "压扁面团,在手上,桌上,擀面杖上,及面团上撒上面粉,此步骤防止面团发粘"
+ },
+ {
+ "step": 11,
+ "description": "用擀面杖将面团擀平,约 8cm 直径,厚约 2mm,中间略微比四周厚 1mm"
+ },
+ {
+ "step": 12,
+ "description": "猪肉去皮,保留部分肥肉,切成小块"
+ },
+ {
+ "step": 13,
+ "description": "菜刀(建议两把)将猪肉剁成肉沫,放入碗中"
+ },
+ {
+ "step": 14,
+ "description": "葱、姜切成末,放入肉碗中搅拌均匀"
+ },
+ {
+ "step": 15,
+ "description": "韭菜洗净,切短至 3mm 以下长度"
+ },
+ {
+ "step": 16,
+ "description": "韭菜和肉沫混合,加入蚝油、生抽、香油各 2ml,加入一个鸡蛋的蛋清,用手混合搅拌均匀"
+ },
+ {
+ "step": 17,
+ "description": "放置 30 分钟即可开始包饺子"
+ },
+ {
+ "step": 18,
+ "description": "左手上放面皮,放饺子馅一面尽量不要粘到面粉,防止无法合拢"
+ },
+ {
+ "step": 19,
+ "description": "右手用筷子夹约面皮 1/2 直径的馅"
+ },
+ {
+ "step": 20,
+ "description": "沿饺子皮圆周进行合拢,捏实,个人吃无需捏花,饺子皮不漏即可"
+ },
+ {
+ "step": 21,
+ "description": "使用可放下 20 只饺子的锅,或分批量煮"
+ },
+ {
+ "step": 22,
+ "description": "烧水,水约 3/4 锅的高度"
+ },
+ {
+ "step": 23,
+ "description": "大火烧开水后放入饺子,调至中火"
+ },
+ {
+ "step": 24,
+ "description": "第一次放入饺子,且水冒泡后,锅边加入 50ml 冷水(重复此步骤两次)"
+ },
+ {
+ "step": 25,
+ "description": "第三次水开后加入冷水 50ml,水开后调至小火等 60s 即可出锅"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-staple-汤面",
+ "name": "汤面的做法",
+ "description": "# 汤面的做法\n\n汤面是许多人喜爱的基础主食,根据个人喜好加入任何自己喜欢的食材,营养全面,固液兼具,材料易得,做法简单,有手就行。\n\n预估烹饪难度:★★",
+ "source_path": "dishes/staple/汤面.md",
+ "image_path": null,
+ "images": [],
+ "category": "主食",
+ "difficulty": 2,
+ "tags": [
+ "主食"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "菜类材料:建议荤素搭配,选择自己喜欢的食材洗干净即可。例如:",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 菜类材料:建议荤素搭配,选择自己喜欢的食材洗干净即可。例如:",
+ "notes": "量未指定"
+ },
+ {
+ "name": "牛羊鱼虾等肉类(生熟皆可)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 牛羊鱼虾等肉类(生熟皆可)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋鸭蛋鹅蛋鸵鸟蛋等蛋类",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋鸭蛋鹅蛋鸵鸟蛋等蛋类",
+ "notes": "量未指定"
+ },
+ {
+ "name": "豆块豆筋豆腐皮等豆制品类",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 豆块豆筋豆腐皮等豆制品类",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生菜菠菜油麦菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生菜菠菜油麦菜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "青椒番茄胡萝卜等蔬菜类。",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 青椒番茄胡萝卜等蔬菜类。",
+ "notes": "量未指定"
+ },
+ {
+ "name": "面类材料:单人一个方便面大小的量,可以在",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 面类材料:单人一个方便面大小的量,可以在 70-230g 之间选择。",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冷水: 加入能浸没面的量,一般在",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冷水: 加入能浸没面的量,一般在 200 - 400 ml 之间选择",
+ "notes": "量未指定"
+ },
+ {
+ "name": "菜类:体积大约和面类相当",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 菜类:体积大约和面类相当",
+ "notes": "量未指定"
+ },
+ {
+ "name": "其中青菜体积可忽略",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 其中青菜体积可忽略",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "先将菜类材料切成边长不超过 4cm 的块状,便于煮熟"
+ },
+ {
+ "step": 2,
+ "description": "如有生肉,则先放入冷水中,盖上锅盖,煮沸腾,先捞出上层血沫,再关火,捞出半熟的肉备用"
+ },
+ {
+ "step": 3,
+ "description": "先大火将水加热至沸腾,后调至中火"
+ },
+ {
+ "step": 4,
+ "description": "将较难煮熟的食材放入锅中(比如半熟肉类、香菇类、等最先放入锅中)。为保证煮熟,可在沸腾后计时 10 分钟,特别难熟的大块食材可追加 5 分钟。"
+ },
+ {
+ "step": 5,
+ "description": "将面食放入锅中,适当搅拌确保面和汤充分接触,使液面保持轻微沸腾,煮 5 分钟。加入面后液面易产生白色泡沫,可适当抬起锅盖通气或者撤下锅盖。"
+ },
+ {
+ "step": 6,
+ "description": "将易于煮熟的食材如青菜类放入锅中,适当搅拌以充分浸没,煮 2-5 分钟"
+ },
+ {
+ "step": 7,
+ "description": "关火,随后加入盐、胡椒粉、香油等自己喜欢的调味料,适当搅拌即可出锅食用"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-staple-炒年糕",
+ "name": "炒年糕的做法",
+ "description": "# 炒年糕的做法\n\n闽南风味的炒年糕是一道非常好吃的主食。它制作过程简单,原料获取方便,适合海外朋友满足口腹之欲。初学者需要 30 分钟完成,难度较小。\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/staple/炒年糕.md",
+ "image_path": null,
+ "images": [],
+ "category": "主食",
+ "difficulty": 3,
+ "tags": [
+ "主食"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "年糕/白粿 (形状不限)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 年糕/白粿 (形状不限)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "调味料: 酱油,盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 调味料: 酱油,盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "(可选):鸡蛋,青菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- (可选):鸡蛋,青菜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "年糕",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 年糕 250 g * 份数",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小葱 2 根 * 份数",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 50 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酱油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酱油 15 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 1-2g * 份数,按口味喜好。",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "锅中加水烧开,煮熟年糕,碗中加水确保年糕不会粘连,捞起年糕备用。"
+ },
+ {
+ "step": 2,
+ "description": "小葱切葱花(将葱白和葱叶分开),青菜切小段备用。"
+ },
+ {
+ "step": 3,
+ "description": "(可选) 制作炒蛋,见[西红柿炒蛋](https://github.com/Anduin2017/HowToCook/blob/master/dishes/vegetable_dish/%E8%A5%BF%E7%BA%A2%E6%9F%BF%E7%82%92%E9%B8%A1%E8%9B%8B.md)。"
+ },
+ {
+ "step": 4,
+ "description": "热锅,加入 30ml 食用油。"
+ },
+ {
+ "step": 5,
+ "description": "将葱白倒入锅中,直至大部分葱白变成焦黄色且发出香味,倒出葱油备用。"
+ },
+ {
+ "step": 6,
+ "description": "重新热锅,加入 20ml 食用油。"
+ },
+ {
+ "step": 7,
+ "description": "加入所有辅料(鸡蛋,青菜等),翻炒均匀。"
+ },
+ {
+ "step": 8,
+ "description": "将年糕的水倒掉,向锅中加入年糕。"
+ },
+ {
+ "step": 9,
+ "description": "加入酱油和盐,翻炒均匀。"
+ },
+ {
+ "step": 10,
+ "description": "关火,加入葱油,翻炒均匀,乘盘。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-staple-炒方便面",
+ "name": "炒方便面的做法",
+ "description": "# 炒方便面的做法\n\n这是在探究了传统煮方便面的改良方向之后,进行的一次最成功的尝试。它能够让方便面的美味程度提升很大程度,简单好做。开始炒吧!\n\n预估烹饪难度:★★",
+ "source_path": "dishes/staple/炒方便面.md",
+ "image_path": null,
+ "images": [],
+ "category": "主食",
+ "difficulty": 2,
+ "tags": [
+ "主食"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "方便面",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 方便面",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "火腿肠(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 火腿肠(可选)",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "将火腿肠撕开包装,切成宽度 1cm 的小块。"
+ },
+ {
+ "step": 2,
+ "description": "向煮锅中加入 300 ml 水。煮沸。"
+ },
+ {
+ "step": 3,
+ "description": "加入方便面面饼,煮 45 秒。煮的过程中将其挑动,把面条打散。"
+ },
+ {
+ "step": 4,
+ "description": "面条打散后立刻关火。"
+ },
+ {
+ "step": 5,
+ "description": "将面汤和面分离。用凉水冲一下面条。"
+ },
+ {
+ "step": 6,
+ "description": "准备一个小碗,将方便面的调料包挤进去。"
+ },
+ {
+ "step": 7,
+ "description": "挤进去所有菜包"
+ },
+ {
+ "step": 8,
+ "description": "挤进去所有酱包"
+ },
+ {
+ "step": 9,
+ "description": "挤进去 50% - 80% 的粉包。(全部粉包都挤进去会很咸)"
+ },
+ {
+ "step": 10,
+ "description": "将上一步的面汤取出 80ml,加入小碗,搅匀,得到调料碗。"
+ },
+ {
+ "step": 11,
+ "description": "取出计算好的数量的鸡蛋,打入一个小碗。"
+ },
+ {
+ "step": 12,
+ "description": "每个鸡蛋加入 2g 盐。搅拌均匀。"
+ },
+ {
+ "step": 13,
+ "description": "热锅 20s,加入份数 * 8ml 油。"
+ },
+ {
+ "step": 14,
+ "description": "加入刚刚准备好的一碗鸡蛋。翻炒大约 20s 至鸡蛋形成固态即可。"
+ },
+ {
+ "step": 15,
+ "description": "将煎鸡蛋取出暂存。"
+ },
+ {
+ "step": 16,
+ "description": "热锅 20s,增加锅内的油到份数 * 10ml。"
+ },
+ {
+ "step": 17,
+ "description": "加入第一步处理的火腿肠。翻炒 10 秒。"
+ },
+ {
+ "step": 18,
+ "description": "加入第二步的面。翻炒 30 秒。"
+ },
+ {
+ "step": 19,
+ "description": "加入第三步的调料碗。翻炒 30 秒。"
+ },
+ {
+ "step": 20,
+ "description": "加入第四步的煎鸡蛋。翻炒 30 秒。"
+ },
+ {
+ "step": 21,
+ "description": "关火盛盘即可。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-staple-炒河粉",
+ "name": "炒河粉的做法",
+ "description": "# 炒河粉的做法\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/staple/炒河粉.md",
+ "image_path": null,
+ "images": [],
+ "category": "主食",
+ "difficulty": 4,
+ "tags": [
+ "主食"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "炒河粉、猪肉/牛肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 炒河粉、猪肉/牛肉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "炒料:盐、味精、老抽、生抽、孜然粉(或直接用河粉料)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 炒料:盐、味精、老抽、生抽、孜然粉(或直接用河粉料)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "其他调味料:胡椒粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 其他调味料:胡椒粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黄瓜、面筋块、绿豆芽、鸡蛋、蒜瓣、小葱、淀粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黄瓜、面筋块、绿豆芽、鸡蛋、蒜瓣、小葱、淀粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盆、盘子",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盆、盘子",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黄瓜丝",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黄瓜丝 30g/人、面筋块 30g/人、绿豆芽 30g/人、打碎的鸡蛋 1 个/人。",
+ "notes": "量未指定"
+ },
+ {
+ "name": "拍碎的蒜瓣",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 拍碎的蒜瓣 2 个/人、小葱 1 根/人",
+ "notes": "量未指定"
+ },
+ {
+ "name": "河粉料可按",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 河粉料可按 20g/人添加,若自行准备炒料可 10g 盐+2g 味精+3g 孜然粉。",
+ "notes": "量未指定"
+ },
+ {
+ "name": "淀粉可准备每",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 淀粉可准备每 100g 肉+5g 淀粉比例准备。",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽/生抽,分别为每",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽/生抽,分别为每 250g 河粉 10ml/15ml。",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "小葱切碎(葱白和葱叶分开)、蒜瓣拍碎,丢案板上备用。"
+ },
+ {
+ "step": 2,
+ "description": "打碎鸡蛋,捞一点蛋清到一只碗中,剩下的丢入另一只碗中备用。"
+ },
+ {
+ "step": 3,
+ "description": "将绿豆芽放入锅中,大火煮 60 秒。豆芽捞出,过凉水,放入盘中备用。"
+ },
+ {
+ "step": 4,
+ "description": "黄瓜切丝放入盘中备用,可和豆芽丢一起。"
+ },
+ {
+ "step": 5,
+ "description": "处理面筋,单独丢一个盘中。"
+ },
+ {
+ "step": 6,
+ "description": "肉切细条状,加入淀粉与刚刚碗中的鸡蛋清、胡椒粉,顺时针拌匀。"
+ },
+ {
+ "step": 7,
+ "description": "注:超市购买来的凉皮表面一般会有食用油,可以使用自来水清洗。面筋同样。"
+ },
+ {
+ "step": 8,
+ "description": "注:清洗面筋之后,请用手将面筋中的大量水分挤出(不需过于用力)。"
+ },
+ {
+ "step": 9,
+ "description": "加入食用油,锅热倒出。"
+ },
+ {
+ "step": 10,
+ "description": "倒入处理好的肉,翻炒均匀至变色,倒入碗中备用。"
+ },
+ {
+ "step": 11,
+ "description": "趁锅热,加入 20g 食用油(高血压人群可降低用量),倒入葱白、蒜爆炒出香。"
+ },
+ {
+ "step": 12,
+ "description": "加入河粉,淋入老抽提色,翻炒均匀后再加入河粉炒料,继续翻炒。"
+ },
+ {
+ "step": 13,
+ "description": "河粉即将透明时,放入炒制好的肉丝与面筋,并加入生抽提鲜,简单翻炒两次。"
+ },
+ {
+ "step": 14,
+ "description": "加入豆芽与黄瓜丝,翻炒至河粉完全透明。"
+ },
+ {
+ "step": 15,
+ "description": "关火!"
+ },
+ {
+ "step": 16,
+ "description": "撒入葱叶点缀,把锅端起。"
+ },
+ {
+ "step": 17,
+ "description": "倒入盘中,开始干饭。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-staple-炒馍",
+ "name": "炒馍的做法",
+ "description": "# 炒馍的做法\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/staple/炒馍.md",
+ "image_path": null,
+ "images": [],
+ "category": "主食",
+ "difficulty": 3,
+ "tags": [
+ "主食"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "馒头(隔天略硬更好)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 馒头(隔天略硬更好)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "孜然粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 孜然粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "五香粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 五香粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "馒头",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 馒头 2 个(隔天略硬更好)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 3g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 油 20ml(花生油或芝麻油更好)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "孜然粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 孜然粉 3g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "辣椒粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 辣椒粉 3g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "五香粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 五香粉 3g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小葱 2 棵",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋 (可选,2 个)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋 (可选,2 个)",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "将馒头切成小块或小片。"
+ },
+ {
+ "step": 2,
+ "description": "选有鸡蛋的话将鸡蛋打进碗里,打散(可加盐和五香粉各 1g 或不加,等炒的过程中加)。"
+ },
+ {
+ "step": 3,
+ "description": "鸡蛋浇在馒头上,拌匀,鸡蛋不宜过多。"
+ },
+ {
+ "step": 4,
+ "description": "大火热锅,倒入食用油(不锈钢锅怕伤锅的话可以先倒油,烧至油热也可也可)"
+ },
+ {
+ "step": 5,
+ "description": "将馍丁放进去翻炒,翻炒均匀。"
+ },
+ {
+ "step": 6,
+ "description": "将火调小,炒至馍丁呈金黄色。"
+ },
+ {
+ "step": 7,
+ "description": "放入盐,胡椒粉,五香粉。"
+ },
+ {
+ "step": 8,
+ "description": "最后将葱花放入一起翻炒几下。"
+ },
+ {
+ "step": 9,
+ "description": "关火出锅。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-staple-炸酱面",
+ "name": "炸酱面的做法",
+ "description": "# 炸酱面的做法\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/staple/炸酱面.md",
+ "image_path": null,
+ "images": [],
+ "category": "主食",
+ "difficulty": 3,
+ "tags": [
+ "主食"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "肉丁/肉末",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 肉丁/肉末",
+ "notes": "量未指定"
+ },
+ {
+ "name": "面条(挂面或普通面条)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 面条(挂面或普通面条)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "菜码(根据个人喜好选择,通常",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 菜码(根据个人喜好选择,通常 4-10 种,可选择黄瓜、白菜、萝卜等)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "豆瓣酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 豆瓣酱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "甜面酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 甜面酱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "肉丁/肉末",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 肉丁/肉末 150g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "如果",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 如果 *面条* 选择了*挂面*:150g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱 15g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "菜码 总量",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 菜码 总量 35g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "豆瓣酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 豆瓣酱 20g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "甜面酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 甜面酱 20g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-staple-热干面",
+ "name": "热干面的做法",
+ "description": "# 热干面的做法\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/staple/热干面.md",
+ "image_path": null,
+ "images": [],
+ "category": "主食",
+ "difficulty": 3,
+ "tags": [
+ "主食"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "热干面特有的碱水面",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 热干面特有的碱水面",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酸豆角",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酸豆角",
+ "notes": "量未指定"
+ },
+ {
+ "name": "肉末",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 肉末",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜水",
+ "notes": "量未指定"
+ },
+ {
+ "name": "肉汤汁",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 肉汤汁",
+ "notes": "量未指定"
+ },
+ {
+ "name": "萝卜干",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 萝卜干",
+ "notes": "量未指定"
+ },
+ {
+ "name": "芝麻酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 芝麻酱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "辣椒油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 辣椒油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "胡椒粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 胡椒粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酱油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酱油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡精",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡精",
+ "notes": "量未指定"
+ },
+ {
+ "name": "热干面特有的碱水面 (250g)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 热干面特有的碱水面 (250g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小葱 (10g)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小葱 (10g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酸豆角 (20g)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酸豆角 (20g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "肉末 (30g)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 肉末 (30g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜水 (30ml)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜水 (30ml)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "肉汤汁 (30ml)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 肉汤汁 (30ml)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "萝卜干 (50g)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 萝卜干 (50g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "芝麻酱 (40ml)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 芝麻酱 (40ml)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "辣椒油 (0-10ml)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 辣椒油 (0-10ml)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "胡椒粉(0-10g)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 胡椒粉(0-10g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酱油(5ml)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酱油(5ml)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食盐(3g)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食盐(3g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡精(0-3g)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡精(0-3g)",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "水煮沸,并加入碱水面,焯烫 25 秒钟捞起"
+ },
+ {
+ "step": 2,
+ "description": "撒上食盐、鸡精和胡椒粉"
+ },
+ {
+ "step": 3,
+ "description": "芝麻酱用 90ml 水稀释,搅匀,然后加入"
+ },
+ {
+ "step": 4,
+ "description": "加入 5ml 酱油,加入 30ml 肉汤汁和蒜水"
+ },
+ {
+ "step": 5,
+ "description": "加入萝卜干,肉末,酸豆角,葱花"
+ },
+ {
+ "step": 6,
+ "description": "拌均匀后开吃"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-staple-煮泡面加蛋",
+ "name": "煮泡面加蛋的做法",
+ "description": "# 煮泡面加蛋的做法\n\n煮泡面加蛋是能满足于各种人群的生存基本需求的重要主食,其材料方便易得,做法简单易上手且制作周期极短。\n\n预估烹饪难度:★",
+ "source_path": "dishes/staple/煮泡面加蛋.md",
+ "image_path": null,
+ "images": [],
+ "category": "主食",
+ "difficulty": 1,
+ "tags": [
+ "主食"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "泡面",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 泡面",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 水",
+ "notes": "量未指定"
+ },
+ {
+ "name": "单人,能支撑一个成年人不饥饿状态约",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 单人,能支撑一个成年人不饥饿状态约 3 至 4 小时。",
+ "notes": "量未指定"
+ },
+ {
+ "name": "泡面",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 泡面 1 包",
+ "notes": "量未指定"
+ },
+ {
+ "name": "水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 水 550ml-1000ml,根据锅的情况。以能完整将泡面浸入其中为准。",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋 1 个",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "先将水加热至沸腾(火候不做严格要求,使用热水会更快)"
+ },
+ {
+ "step": 2,
+ "description": "将取出的面饼放入锅中"
+ },
+ {
+ "step": 3,
+ "description": "将泡面里附带的佐料放入锅中"
+ },
+ {
+ "step": 4,
+ "description": "取出筷子轻微拨动泡面,使佐料充分溶解,面饼充分浸泡受热"
+ },
+ {
+ "step": 5,
+ "description": "盖上锅盖等待约 1 分钟至锅内水再次沸腾"
+ },
+ {
+ "step": 6,
+ "description": "去壳鸡蛋,加入锅中"
+ },
+ {
+ "step": 7,
+ "description": "等待约 3 至 4 分钟,即可"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-staple-老干妈拌面",
+ "name": "老干妈拌面的做法",
+ "description": "# 老干妈拌面的做法\n\n预估烹饪难度:★",
+ "source_path": "dishes/staple/老干妈拌面.md",
+ "image_path": null,
+ "images": [],
+ "category": "主食",
+ "difficulty": 1,
+ "tags": [
+ "主食"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "面",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 面",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老干妈",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老干妈",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酱油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酱油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 水 1 升",
+ "notes": "量未指定"
+ },
+ {
+ "name": "面量",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 面量 120 克 * 份数",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老干妈",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老干妈 15ml * 份数",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酱油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酱油 5ml * 份数",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "将水倒入锅中并煮沸"
+ },
+ {
+ "step": 2,
+ "description": "将面均匀放入锅中"
+ },
+ {
+ "step": 3,
+ "description": "在煮的过程注意搅拌,避免面粘成一坨"
+ },
+ {
+ "step": 4,
+ "description": "当用筷子挑起一根面且该面能自然地从筷子上滑落时再等 30 秒关火"
+ },
+ {
+ "step": 5,
+ "description": "将面夹入碗中"
+ },
+ {
+ "step": 6,
+ "description": "按照上面的计量放入老干妈和酱油"
+ },
+ {
+ "step": 7,
+ "description": "用筷子将碗里的面、老干妈、酱油拌均匀"
+ },
+ {
+ "step": 8,
+ "description": "吃"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-staple-肉蛋盖饭",
+ "name": "肉蛋盖饭的做法",
+ "description": "# 肉蛋盖饭的做法\n\n肉蛋盖饭适合于单人简易晚餐,烹饪大约需要十五分钟。\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/staple/肉蛋盖饭.md",
+ "image_path": null,
+ "images": [],
+ "category": "主食",
+ "difficulty": 3,
+ "tags": [
+ "主食"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "米饭",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 米饭",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "肉馅",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 肉馅",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "醋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 醋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "红葱油(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 红葱油(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "米饭",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 米饭 240g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋 4 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "肉馅",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 肉馅 300g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽 10ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 25ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "醋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 醋 20ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "红葱油可选",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 红葱油可选 10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱 10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 油 30ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 糖 15g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "煮好米饭,通常使用买米赠送的量杯,一杯米 240g"
+ },
+ {
+ "step": 2,
+ "description": "锅中放油 30ml"
+ },
+ {
+ "step": 3,
+ "description": "放入肉馅,调中火煎至两面微焦"
+ },
+ {
+ "step": 4,
+ "description": "将鸡蛋打入锅中,不要打散,盖上锅盖"
+ },
+ {
+ "step": 5,
+ "description": "调一个碗汁,碗中放入计算中的对应数量的老抽,生抽,醋,糖,红葱油,搅拌均匀"
+ },
+ {
+ "step": 6,
+ "description": "打开锅盖,将碗汁倒入锅中,等待三分钟"
+ },
+ {
+ "step": 7,
+ "description": "关火,将肉蛋盖到米饭上"
+ },
+ {
+ "step": 8,
+ "description": "安全检查,开始食用盖饭"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-staple-葱油拌面",
+ "name": "葱油拌面的做法",
+ "description": "# 葱油拌面的做法\n\n葱油拌面是一道经典的上海家常面点。做法简单,以其独特的葱油香味而闻名。富含碳水化合物和脂肪,能够快速补充能量。一般初学者只需要 20 分钟即可完成。是一道非常适合加班后的简单晚餐选择。\n\n预估烹饪难度:★★",
+ "source_path": "dishes/staple/葱油拌面.md",
+ "image_path": null,
+ "images": [],
+ "category": "主食",
+ "difficulty": 2,
+ "tags": [
+ "主食"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "干面条",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 干面条",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小葱 100 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 100 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 60 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽 20 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖 15 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "干面条",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 干面条 80 g (约相当于 150 g 湿面条)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱油酱汁",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱油酱汁 15 ml",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "将 小葱 洗净,切成长段(约 5-7 cm)。葱白和葱绿可以分开。"
+ },
+ {
+ "step": 2,
+ "description": "锅中加入 100 ml 食用油,中火烧热。先放入葱白段,煸炒至微黄。"
+ },
+ {
+ "step": 3,
+ "description": "加入葱绿段,转小火,继续煸炒。"
+ },
+ {
+ "step": 4,
+ "description": "保持小火,耐心煸炒约 **15-20 分钟**,直至葱段变得焦黄酥脆。"
+ },
+ {
+ "step": 5,
+ "description": "将焦黄的葱段捞出(葱油保留在锅中)。"
+ },
+ {
+ "step": 6,
+ "description": "在锅中的葱油中,加入 60 ml 生抽,20 ml 老抽,15 g 白糖。小火加热并搅拌,约 **1 分钟**,至糖溶解,酱汁混合均匀。立即关火。将制作好的葱油酱汁倒入容器中,放凉后密封保存。"
+ },
+ {
+ "step": 7,
+ "description": "取 80 g 干面条。"
+ },
+ {
+ "step": 8,
+ "description": "锅中加入 1000 ml 饮用水,大火烧开。"
+ },
+ {
+ "step": 9,
+ "description": "放入 面条,根据面条包装说明,煮至熟透(通常 **3-8 分钟**,以包装说明为准)。"
+ },
+ {
+ "step": 10,
+ "description": "将 煮好的 面条 捞出,沥干水分,放入碗中。"
+ },
+ {
+ "step": 11,
+ "description": "在装有 面条 的碗中,加入 15 ml 之前做好的 葱油酱汁。"
+ },
+ {
+ "step": 12,
+ "description": "可以加入之前炸好的葱段(可选)。"
+ },
+ {
+ "step": 13,
+ "description": "用 筷子 快速搅拌均匀,即可食用。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-staple-蒸卤面",
+ "name": "蒸卤面的做法",
+ "description": "# 蒸卤面的做法\n\n蒸卤面是一道豫南的非常经典的家常菜,荤素搭档,简单易学。一般初学者只需要一个小时即可完成。\n\nNOTE:本次标准为豫南口味,可能和其他地方不太一样,食无标准,兼容并包,好吃即可。\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/staple/蒸卤面.md",
+ "image_path": null,
+ "images": [],
+ "category": "主食",
+ "difficulty": 4,
+ "tags": [
+ "主食"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "猪五花肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 猪五花肉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "芹菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 芹菜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱,姜,蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱,姜,蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油(花生油最佳)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油(花生油最佳)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽,老抽,料酒,盐,五香粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽,老抽,料酒,盐,五香粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒸锅,需带笼屉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒸锅,需带笼屉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "炒锅",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 炒锅",
+ "notes": "量未指定"
+ },
+ {
+ "name": "花椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 花椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "干红椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 干红椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "青椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 青椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "芹菜 两根中等大小的芹菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 芹菜 两根中等大小的芹菜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "五花肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 五花肉 350g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "面条",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 面条 500g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大葱 10cm",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大蒜 5 瓣",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜片",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜片 20g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "青椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 青椒 2 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "干红椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 干红椒 3 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "花椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 花椒 20 粒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "五香粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 五香粉 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 15ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽 10ml",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "猪肉去皮,切成 `2 cm * 6 cm * 0.5 cm` 薄片备用"
+ },
+ {
+ "step": 2,
+ "description": "芹菜去叶,去掉根部 2cm,然后从中对半切开,切成 2cm 段备用"
+ },
+ {
+ "step": 3,
+ "description": "大蒜去皮切粒备用,葱切 0.2cm 薄片备用,姜切细丝备用"
+ },
+ {
+ "step": 4,
+ "description": "炒锅烧热至冒烟后,倒入 3ml 食用油滑锅后倒出底油"
+ },
+ {
+ "step": 5,
+ "description": "重新加入食用油,加入肉片,葱姜蒜,干红椒,炒 1 分钟,注意不停匀速翻炒"
+ },
+ {
+ "step": 6,
+ "description": "加入料酒,生抽,老抽,再翻炒 1 分钟"
+ },
+ {
+ "step": 7,
+ "description": "续入 500ml 热水。盖上锅盖炖煮 3 分钟"
+ },
+ {
+ "step": 8,
+ "description": "将芹菜,青椒倒入锅中,加入盐,五香粉调味,盖上锅盖继续炖煮煮 3 分钟 后关火"
+ },
+ {
+ "step": 9,
+ "description": "蒸锅加入 1000ml 水,烧开上汽后,将面条摊平在笼屉上放入锅中,蒸 15 分钟"
+ },
+ {
+ "step": 10,
+ "description": "面条蒸熟后取出,用筷子和无情铁手扒拉散开在案板上,室温冷却"
+ },
+ {
+ "step": 11,
+ "description": "将面条放入菜锅中搅拌,搅拌方式为一手持筷,一手持锅铲将菜翻至面条上面,以面条以全部均匀上色为搅拌完成标准"
+ },
+ {
+ "step": 12,
+ "description": "将搅拌后的面条再次放在整屉上,再次蒸 10 分钟 关火"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-staple-蛋包饭",
+ "name": "蛋包饭的做法",
+ "description": "# 蛋包饭的做法\n\n蛋包饭是一道日式经典家常菜,由炒饭和嫩滑鸡蛋组成,口感丰富,色香味俱全。富含蛋白质、碳水和维生素,是非常适合早餐或正餐的选择。预估制作时间为 25 分钟。\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/staple/蛋包饭.md",
+ "image_path": null,
+ "images": [],
+ "category": "主食",
+ "difficulty": 3,
+ "tags": [
+ "主食"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "鸡蛋(建议使用土鸡蛋,口感更香)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋(建议使用土鸡蛋,口感更香)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "洋葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 洋葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "胡萝卜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 胡萝卜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "玉米粒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 玉米粒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "青豆(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 青豆(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "火腿肠或鸡胸肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 火腿肠或鸡胸肉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "米饭",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 米饭",
+ "notes": "量未指定"
+ },
+ {
+ "name": "番茄酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 番茄酱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油(建议使用植物油)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油(建议使用植物油)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "牛奶(可选,让蛋皮更嫩)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 牛奶(可选,让蛋皮更嫩)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋 2 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "洋葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 洋葱 30g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "胡萝卜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 胡萝卜 30g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "火腿肠或鸡胸肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 火腿肠或鸡胸肉 50g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "玉米粒和青豆总共",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 玉米粒和青豆总共 30g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "米饭",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 米饭 200g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "番茄酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 番茄酱 20ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 15ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "牛奶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 牛奶 10ml(与鸡蛋混合)",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "洋葱、胡萝卜、火腿肠或鸡胸肉切成小丁,备用"
+ },
+ {
+ "step": 2,
+ "description": "热锅,锅中倒入 10ml 食用油,等待 10 秒加热"
+ },
+ {
+ "step": 3,
+ "description": "先放入洋葱丁翻炒 1 分钟,出香味后加入胡萝卜、玉米粒、青豆继续翻炒 2 分钟"
+ },
+ {
+ "step": 4,
+ "description": "加入火腿肠或鸡胸肉丁,炒至变色"
+ },
+ {
+ "step": 5,
+ "description": "加入米饭炒散后,加入番茄酱 20ml,翻炒均匀,炒饭完成,盛出备用"
+ },
+ {
+ "step": 6,
+ "description": "鸡蛋打散,加入 10ml 牛奶搅匀"
+ },
+ {
+ "step": 7,
+ "description": "锅中放入 5ml 食用油,倒入蛋液,轻晃锅底让蛋液均匀铺满锅面"
+ },
+ {
+ "step": 8,
+ "description": "用小火加热,待蛋液表面半熟状态时,将炒饭放入蛋液中央"
+ },
+ {
+ "step": 9,
+ "description": "用铲子将蛋皮折叠包住米饭,形成椭圆形状"
+ },
+ {
+ "step": 10,
+ "description": "用锅铲轻轻推至盘中,整理外形,可在表面挤上少量番茄酱装饰"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-staple-蛋炒饭",
+ "name": "蛋炒饭的做法",
+ "description": "# 蛋炒饭的做法\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/staple/蛋炒饭.md",
+ "image_path": null,
+ "images": [],
+ "category": "主食",
+ "difficulty": 3,
+ "tags": [
+ "主食"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "冷饭",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冷饭",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "火腿",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 火腿",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黄瓜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黄瓜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "胡萝卜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 胡萝卜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "胡椒粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 胡椒粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "灯影牛肉丝/午餐肉/腊肠/卤肉...等熟肉(备选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 灯影牛肉丝/午餐肉/腊肠/卤肉...等熟肉(备选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冷饭(份数*500ml)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冷饭(份数*500ml)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋 (份数*1.5 //",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋 (份数*1.5 // 1 向下取整)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "火腿(份数*2 个)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 火腿(份数*2 个)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黄瓜(可选,份数*30g)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黄瓜(可选,份数*30g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "胡萝卜(可选,份数*30g)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 胡萝卜(可选,份数*30g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "油(份数*12ml)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 油(份数*12ml)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐(份数\\*4g - 份数*6g)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐(份数\\*4g - 份数*6g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "胡椒粉(份数*8g)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 胡椒粉(份数*8g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香葱(份数*1 颗)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香葱(份数*1 颗)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽(份数*10ml)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽(份数*10ml)",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "米饭提前用铲子铲成小块"
+ },
+ {
+ "step": 2,
+ "description": "火腿肠、胡萝卜、黄瓜等根据需求切片或者块状"
+ },
+ {
+ "step": 3,
+ "description": "如果家里有熟肉 准备好味道更佳"
+ },
+ {
+ "step": 4,
+ "description": "将蛋白,蛋黄分开,分别打入一个大碗里,各自搅匀。注意,不要在这一步加盐。"
+ },
+ {
+ "step": 5,
+ "description": "大火热锅,待锅里冒烟放入食用油,放入蛋白,待主体凝固后盛出备用。"
+ },
+ {
+ "step": 6,
+ "description": "如果油够,则直接放入蛋黄,如果油不够则放入食用油并等其升温到大火热锅"
+ },
+ {
+ "step": 7,
+ "description": "待主体凝固后,将火调至中小火,倒入火腿肠、熟肉,胡萝卜、黄瓜等备料、翻炒 10 秒钟(到爆香)"
+ },
+ {
+ "step": 8,
+ "description": "重新倒入蛋白,翻炒 5s 钟,迅速倒入米饭大火翻炒,为的就是每一粒饭都裹上鸡蛋。"
+ },
+ {
+ "step": 9,
+ "description": "翻炒过程中将米饭的块状捣碎、这一步过程会比较长、待米饭全部捣碎再翻炒均匀即可"
+ },
+ {
+ "step": 10,
+ "description": "调至小火、加盐、胡椒粉、生抽"
+ },
+ {
+ "step": 11,
+ "description": "进一步翻炒均匀,能看到一些米饭在锅里有“跳起来”的时候其实就已经差不多了"
+ },
+ {
+ "step": 12,
+ "description": "最后倒入香葱再翻炒 10s"
+ },
+ {
+ "step": 13,
+ "description": "关火、盛入碗中"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-staple-螺蛳粉",
+ "name": "螺蛳粉的做法",
+ "description": "# 螺蛳粉的做法\n\n正宗的螺蛳粉是不臭的!\n\n预估烹饪难度:★",
+ "source_path": "dishes/staple/螺蛳粉.md",
+ "image_path": null,
+ "images": [],
+ "category": "主食",
+ "difficulty": 1,
+ "tags": [
+ "主食"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "根据个人经验,一包袋装螺蛳粉足够一人一餐食(虽然看着很大包)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 根据个人经验,一包袋装螺蛳粉足够一人一餐食(虽然看着很大包)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 水 1L",
+ "notes": "量未指定"
+ },
+ {
+ "name": "袋装螺蛳粉一包,其中应该包含:",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 袋装螺蛳粉一包,其中应该包含:",
+ "notes": "量未指定"
+ },
+ {
+ "name": "米粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 米粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "螺蛳肉包(可能放在配料包中)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 螺蛳肉包(可能放在配料包中)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "汤料包",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 汤料包",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酸笋包、花生包、豆皮包、木耳包等配料包",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酸笋包、花生包、豆皮包、木耳包等配料包",
+ "notes": "量未指定"
+ },
+ {
+ "name": "醋包、辣椒油等调味包",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 醋包、辣椒油等调味包",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "锅中加水,将水烧开"
+ },
+ {
+ "step": 2,
+ "description": "下米粉,煮 3-5 分钟,期间用筷子搅拌,防止米粉粘在一起"
+ },
+ {
+ "step": 3,
+ "description": "下汤料包,按个人口味添加"
+ },
+ {
+ "step": 4,
+ "description": "下一部分配料包,如木耳,花生,螺蛳(这部分配料需要煮一会才入味)"
+ },
+ {
+ "step": 5,
+ "description": "下调味包,按个人口味添加"
+ },
+ {
+ "step": 6,
+ "description": "搅拌后捞出,放入碗中"
+ },
+ {
+ "step": 7,
+ "description": "下剩下的配料包,如酸笋,豆皮(这部分配料不适合被汤泡太久)"
+ },
+ {
+ "step": 8,
+ "description": "享用美食"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-staple-酸辣蕨根粉",
+ "name": "酸辣蕨根粉的做法",
+ "description": "# 酸辣蕨根粉的做法\n\n酸辣蕨根粉是一道适合初学者的简单易做的凉菜,可做主食,以酸辣口为主,预计 10 分钟可做完。\n\n预估烹饪难度:★★",
+ "source_path": "dishes/staple/酸辣蕨根粉.md",
+ "image_path": null,
+ "images": [],
+ "category": "主食",
+ "difficulty": 2,
+ "tags": [
+ "主食"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "蕨根粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蕨根粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "油泼辣子",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 油泼辣子",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酱油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酱油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香醋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香醋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小米辣(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小米辣(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "一口有点深度的锅",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 一口有点深度的锅",
+ "notes": "量未指定"
+ },
+ {
+ "name": "如果觉得酱料较为清淡,可以加入",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 如果觉得酱料较为清淡,可以加入 2 至 5 克盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "如果想要酱料鲜一些,可以加入",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 如果想要酱料鲜一些,可以加入 2 克糖",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "锅中加入约为深度 3/5 的水,烧开"
+ },
+ {
+ "step": 2,
+ "description": "水沸腾后加入蕨根粉,中小火煮 8 分钟"
+ },
+ {
+ "step": 3,
+ "description": "出锅"
+ },
+ {
+ "step": 4,
+ "description": "根据配比,加入酱油、醋、油泼辣子"
+ },
+ {
+ "step": 5,
+ "description": "用筷子蘸取,尝一口"
+ },
+ {
+ "step": 6,
+ "description": "如果觉得此时酱油味稍浓,加入准备好的盐"
+ },
+ {
+ "step": 7,
+ "description": "如果觉得此时不够鲜,加入准备好的糖"
+ },
+ {
+ "step": 8,
+ "description": "充分搅拌至大部分颗粒状调料溶解"
+ },
+ {
+ "step": 9,
+ "description": "取一个碗"
+ },
+ {
+ "step": 10,
+ "description": "加入上一步调制的酱料"
+ },
+ {
+ "step": 11,
+ "description": "将蕨根粉过冷水后放入酱料中"
+ },
+ {
+ "step": 12,
+ "description": "充分搅拌"
+ },
+ {
+ "step": 13,
+ "description": "将准备的葱、蒜、小米辣切碎后撒在粉上"
+ },
+ {
+ "step": 14,
+ "description": "完成啦(。・∀・)ノ゙"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-staple-醪糟小汤圆",
+ "name": "醪糟小汤圆的做法",
+ "description": "# 醪糟小汤圆的做法\n\n预估烹饪难度:★★",
+ "source_path": "dishes/staple/醪糟小汤圆.md",
+ "image_path": null,
+ "images": [],
+ "category": "主食",
+ "difficulty": 2,
+ "tags": [
+ "主食"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "小汤圆",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小汤圆",
+ "notes": "量未指定"
+ },
+ {
+ "name": "醪糟",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 醪糟",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "枸杞(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 枸杞(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 水 300 毫升 * 份数",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小汤圆",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小汤圆 250 克 * 份数",
+ "notes": "量未指定"
+ },
+ {
+ "name": "醪糟",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 醪糟 50 克 * 份数",
+ "notes": "量未指定"
+ },
+ {
+ "name": "枸杞",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 枸杞 5 颗 * 份数",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "将水倒入锅中并煮沸"
+ },
+ {
+ "step": 2,
+ "description": "放入小汤圆煮 8 分钟"
+ },
+ {
+ "step": 3,
+ "description": "放入醪糟和枸杞再煮 2 分钟"
+ },
+ {
+ "step": 4,
+ "description": "盛入碗中根据个人口味加入白糖并搅拌均匀"
+ },
+ {
+ "step": 5,
+ "description": "吃"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-staple-韭菜盒子",
+ "name": "韭菜盒子的做法",
+ "description": "# 韭菜盒子的做法\n\n韭菜盒子是一道美味的传统小吃,外皮酥脆,内馅鲜香,富含维生素和蛋白质。制作简单,适合午餐,预计制作时长约 2.5 小时。\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/staple/韭菜盒子.md",
+ "image_path": null,
+ "images": [],
+ "category": "主食",
+ "difficulty": 3,
+ "tags": [
+ "主食"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "韭菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 韭菜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "虾仁",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 虾仁",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "面粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 面粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "韭菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 韭菜 500g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "虾仁",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 虾仁 100g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋 3 枚",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香油 10ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "面粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 面粉 250g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "将面粉放入大碗中,加入水,搅拌成光滑的面团,静置 30 分钟。"
+ },
+ {
+ "step": 2,
+ "description": "韭菜洗净切碎,加入打散的鸡蛋、5g 盐,搅拌均匀。"
+ },
+ {
+ "step": 3,
+ "description": "将面团分成小剂子,擀成薄圆饼,包入韭菜、虾仁、鸡蛋液。"
+ },
+ {
+ "step": 4,
+ "description": "热锅,加入食用油,放入包好的韭菜盒子,煎至两面金黄,约 3-4 分钟。"
+ },
+ {
+ "step": 5,
+ "description": "盛盘,稍凉后即可享用。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-staple-麻油拌面",
+ "name": "麻油拌面的做法",
+ "description": "# 麻油拌面的做法\n\n省吃俭用懒人的菜:麻油拌面:想必大家都会有节约开销的时刻吧,附上个人耐吃又省钱的食谱。不需要太多的步骤简单的煮,捞,吃。\n\n- 单身的朋友懒惰出门,又不想花钱,简简单单就一餐。\n- 非单身的朋友想存钱,让女友花钱,简简单单就一餐。\n\n预估烹饪难度:★",
+ "source_path": "dishes/staple/麻油拌面.md",
+ "image_path": null,
+ "images": [],
+ "category": "主食",
+ "difficulty": 1,
+ "tags": [
+ "主食"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "风干快熟面/任何牌子的快熟面(不需要调味料)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 风干快熟面/任何牌子的快熟面(不需要调味料)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "麻油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 麻油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "胡椒粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 胡椒粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 水 1 升",
+ "notes": "量未指定"
+ },
+ {
+ "name": "快熟面",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 快熟面 1 块",
+ "notes": "量未指定"
+ },
+ {
+ "name": "麻油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 麻油 15ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽 10 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 30 克(可选,这 30g 盐不会被全部食用)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "胡椒粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 胡椒粉 10 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 5 克(可选)",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "将水倒入锅中并煮沸 (喜欢吃 q 弹面的同学,可在水里加入 30 克盐,用盐水煮出来的面会比较 q 弹)"
+ },
+ {
+ "step": 2,
+ "description": "将快熟面放入锅中 3 分钟(也可参考当下品牌快熟面的烹饪时间)"
+ },
+ {
+ "step": 3,
+ "description": "当面开始散了可以开始搅拌,让面受热均匀"
+ },
+ {
+ "step": 4,
+ "description": "将水滤干把面倒入碗中"
+ },
+ {
+ "step": 5,
+ "description": "按照上面的计量放入麻油,老抽,胡椒粉,生抽(可选)"
+ },
+ {
+ "step": 6,
+ "description": "筷子搅拌均匀"
+ },
+ {
+ "step": 7,
+ "description": "一道简单即省钱的懒人麻油拌面就完成啦"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-staple-麻辣减脂荞麦面",
+ "name": "麻辣减脂荞麦面的做法",
+ "description": "# 麻辣减脂荞麦面的做法\n\n麻辣减脂荞麦面做法非常简单,不需要任何厨艺基础。\n一份 298 千卡,美味+便宜+减脂,只需要 20 分钟就可以完成。\n\n预估烹饪难度:★★",
+ "source_path": "dishes/staple/麻辣减脂荞麦面.md",
+ "image_path": null,
+ "images": [],
+ "category": "主食",
+ "difficulty": 2,
+ "tags": [
+ "主食"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "调味料:火锅底料、花生酱、全脂牛奶、生抽、辣椒油、醋、花椒油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 调味料:火锅底料、花生酱、全脂牛奶、生抽、辣椒油、醋、花椒油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "原料:半干荞麦面、娃娃菜、生菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 原料:半干荞麦面、娃娃菜、生菜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "洗菜盆、直径",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 洗菜盆、直径 18cm 的小锅",
+ "notes": "量未指定"
+ },
+ {
+ "name": "半干荞麦面",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 半干荞麦面 100g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "娃娃菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 娃娃菜 8 片(共 150g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生菜 6 片(共 80g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "火锅底料",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 火锅底料 25g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "花生酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 花生酱 15g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "全脂牛奶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 全脂牛奶 150ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 6ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "辣椒油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 辣椒油 10ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "醋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 醋 20ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "花椒油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 花椒油 10ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 水 500ml",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "娃娃菜、生菜洗好,备用"
+ },
+ {
+ "step": 2,
+ "description": "锅内倒入 500ml 水,开大火,将荞麦面和娃娃菜放进去,等待水沸腾"
+ },
+ {
+ "step": 3,
+ "description": "水沸腾后,转小火,加入火锅底料、花生酱、牛奶、生抽、辣椒油,水开后煮 5 分钟"
+ },
+ {
+ "step": 4,
+ "description": "加入生菜,再煮 2 分钟"
+ },
+ {
+ "step": 5,
+ "description": "加入醋、花椒油,关火,直接端着小锅开吃。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-staple-中式馅饼-中式馅饼",
+ "name": "中式馅饼的做法",
+ "description": "# 中式馅饼的做法\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/staple/中式馅饼/中式馅饼.md",
+ "image_path": null,
+ "images": [],
+ "category": "主食",
+ "difficulty": 4,
+ "tags": [
+ "主食"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "面粉(非自发粉)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 面粉(非自发粉)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "肉沫",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 肉沫",
+ "notes": "量未指定"
+ },
+ {
+ "name": "油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酱油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酱油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "风味调料(如鸡粉、孜然、椒盐,可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 风味调料(如鸡粉、孜然、椒盐,可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜头",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜头",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "胡萝卜(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 胡萝卜(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "平底锅",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 平底锅",
+ "notes": "量未指定"
+ },
+ {
+ "name": "炒锅(可以使用同一个平底锅替代)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 炒锅(可以使用同一个平底锅替代)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "面粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 面粉 200g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "肉沫",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 肉沫 50g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 油 30ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 3g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 糖 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生粉 10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酱油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酱油 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "风味调料",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 风味调料 3g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜头",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜头 2 瓣",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大葱 1/4 根(靠叶部分)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋 (可选,1 个)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋 (可选,1 个)",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "取肉沫(解冻),加入 1/2 所有上述调料(油、盐、糖、酱油、风味调料)和全部的生粉,搅拌均匀,腌制 30 分钟。"
+ },
+ {
+ "step": 2,
+ "description": "将面粉加入碗中,加入鸡蛋,加入剩下 1/2 所有上述调料,加入相当于面粉 1/2 的水(使得面粉相对粘稠但可以流动),搅拌均匀。"
+ },
+ {
+ "step": 3,
+ "description": "蒜头切为蒜末。"
+ },
+ {
+ "step": 4,
+ "description": "大葱切段。"
+ },
+ {
+ "step": 5,
+ "description": "胡萝卜切末(作为馅料用,所以要求尽量细碎,可用乱刀)"
+ },
+ {
+ "step": 6,
+ "description": "热锅冷油,宽油起锅。"
+ },
+ {
+ "step": 7,
+ "description": "待油烧热后,放入蒜末爆香。"
+ },
+ {
+ "step": 8,
+ "description": "加入腌制的肉沫,翻炒,直至断生。"
+ },
+ {
+ "step": 9,
+ "description": "将胡萝卜末加入肉沫中一同翻炒,直至油被染为金黄色(这是为了萃取胡萝卜的风味)。"
+ },
+ {
+ "step": 10,
+ "description": "关火。冷却 2 分钟。"
+ },
+ {
+ "step": 11,
+ "description": "将炒好的肉沫倒入生面糊中,搅匀。"
+ },
+ {
+ "step": 12,
+ "description": "重新开火,平底锅铺底油。"
+ },
+ {
+ "step": 13,
+ "description": "调至小火,将面糊倒入锅中均匀铺满。保证厚度不要过高。可以端起锅,让面糊流过锅底来完成这一操作。"
+ },
+ {
+ "step": 14,
+ "description": "在饼的表面尚为液态时,撒上大葱段。"
+ },
+ {
+ "step": 15,
+ "description": "保持小火,直到底面凝固。"
+ },
+ {
+ "step": 16,
+ "description": "将饼翻面,继续小火煎烤,直至另一侧凝固。"
+ },
+ {
+ "step": 17,
+ "description": "之后,每一面再额外煎 20 秒。"
+ },
+ {
+ "step": 18,
+ "description": "关火出锅。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-staple-凉粉-凉粉",
+ "name": "凉粉的做法",
+ "description": "# 凉粉的做法\n\n\n\n伤心凉粉吃了不会让人伤心的哦!\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/staple/凉粉/凉粉.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/凉粉/lf1.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/凉粉/lf1.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/凉粉/lf10.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/凉粉/lf11.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/凉粉/lf2.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/凉粉/lf3.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/凉粉/lf4.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/凉粉/lf5.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/凉粉/lf6.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/凉粉/lf7.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/凉粉/lf8.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/凉粉/lf9.jpg"
+ ],
+ "category": "主食",
+ "difficulty": 3,
+ "tags": [
+ "主食"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "豌豆淀粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 豌豆淀粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小米辣",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小米辣",
+ "notes": "量未指定"
+ },
+ {
+ "name": "辣椒粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 辣椒粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酱油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酱油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "醋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 醋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡精",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡精",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "花生碎",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 花生碎",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香菜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "豌豆淀粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 豌豆淀粉 100g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大蒜 3 瓣",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小米辣",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小米辣 3 颗",
+ "notes": "量未指定"
+ },
+ {
+ "name": "辣椒粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 辣椒粉 10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酱油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酱油 10ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "醋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 醋 10ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖 3ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡精",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡精 3g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 3g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "花生碎",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 花生碎 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香菜 5g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "准备食材。"
+ },
+ {
+ "step": 2,
+ "description": "把豌豆淀粉和水各 100 克混合搅拌。"
+ },
+ {
+ "step": 3,
+ "description": "往锅中倒入 600g 水,大火煮开后转为小火。"
+ },
+ {
+ "step": 4,
+ "description": "倒入淀粉水,边倒边不断的搅拌,搅拌到浓稠且色泽均匀。"
+ },
+ {
+ "step": 5,
+ "description": "找一个容器,在容器中刷一层薄薄的食用油。"
+ },
+ {
+ "step": 6,
+ "description": "将煮好的淀粉倒入容器中冷藏 2-4 小时。"
+ },
+ {
+ "step": 7,
+ "description": "冷藏后取出,脱模,切条。"
+ },
+ {
+ "step": 8,
+ "description": "大蒜和小米辣剁成沫,放上 10g 辣椒粉,5g 花生碎,热油搅拌均匀。"
+ },
+ {
+ "step": 9,
+ "description": "再加入 10ml 酱油,10ml 醋,5g 白糖,3g 鸡精,3g 盐搅拌均匀。"
+ },
+ {
+ "step": 10,
+ "description": "将调味料倒在凉粉上,然后撒上香菜即可。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。",
+ "做法参考:[制作凉粉的详细步骤](https://www.zhms.cn/recipe/mzvyy.html?source=2)"
+ ]
+ },
+ {
+ "id": "dishes-staple-基础牛奶面包-基础牛奶面包",
+ "name": "基础牛奶面包的做法",
+ "description": "# 基础牛奶面包的做法\n\n\n\n面包是常见的主食。普通面包需要经过长时间的发酵及和面。但本食谱尽量简化了制作步骤,方便新手上手,并尽量保证其风味。当然,要求更高的也可以查阅其的面包食谱。\n\n本食谱**需要的额外的工具较多**,会在后面的章节详细介绍。\n\n本食谱面向**烘焙新手**,难度**中**,预计制作制作时长 **200 分钟**。\n\n预估烹饪难度:★★★★★",
+ "source_path": "dishes/staple/基础牛奶面包/基础牛奶面包.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/基础牛奶面包/1-1成品.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/基础牛奶面包/1-1成品.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/基础牛奶面包/2-1设备简介1.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/基础牛奶面包/2-2设备简介2.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/基础牛奶面包/4-1酵头1.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/基础牛奶面包/4-2酵头2.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/基础牛奶面包/4-3酵头3.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/基础牛奶面包/4-4此时的面团.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/基础牛奶面包/4-5转移到容器内.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/基础牛奶面包/4-6成品面包.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/基础牛奶面包/5-1成品.jpg"
+ ],
+ "category": "主食",
+ "difficulty": 5,
+ "tags": [
+ "主食"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "**酵头**",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- **酵头**",
+ "notes": "量未指定"
+ },
+ {
+ "name": "面粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 面粉 1 cup (盛一杯后用勺子挂掉多余的部分,不要晃动)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "30 ℃ 温水(以不烫手为宜)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 30 ℃ 温水(以不烫手为宜) 1 cup",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酵母",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酵母 2 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 2 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "**面团**",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- **面团**",
+ "notes": "量未指定"
+ },
+ {
+ "name": "面粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 面粉 2½ cup",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋 1 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "糖或糖浆 ⅛ cup",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 糖或糖浆 ⅛ cup",
+ "notes": "量未指定"
+ },
+ {
+ "name": "奶制品 混合后共 ¼ cup (奶粉需要和水混合。)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 奶制品 混合后共 ¼ cup (奶粉需要和水混合。)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黄油或玉米油 ⅛ cup",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黄油或玉米油 ⅛ cup",
+ "notes": "量未指定"
+ },
+ {
+ "name": "谷朊粉 ¼ ~ ½ cup (可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 谷朊粉 ¼ ~ ½ cup (可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香草精",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香草精 3 g (可选)",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "*发酵失败了?看看这里:**"
+ },
+ {
+ "step": 2,
+ "description": "*如何制作“永久的”酵头?**"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-staple-微波炉腊肠煲仔饭-微波炉腊肠煲仔饭",
+ "name": "微波炉腊肠煲仔饭的做法",
+ "description": "# 微波炉腊肠煲仔饭的做法\n\n\n\n程序员以单身汉居多 🐶,做再多的菜也会有一个人吃不完的烦恼,因此一份简单的腊肠煲仔饭则刚刚好。\n\n使用微波炉烹制仅需 `15 分钟` ,既营养又美味,这是一道简单且细腻的主食,给 TA 露上一手吧。\n\n预估烹饪难度:★★",
+ "source_path": "dishes/staple/微波炉腊肠煲仔饭/微波炉腊肠煲仔饭.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/微波炉腊肠煲仔饭/微波炉腊肠煲仔饭.png",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/微波炉腊肠煲仔饭/微波炉腊肠煲仔饭.png"
+ ],
+ "category": "主食",
+ "difficulty": 2,
+ "tags": [
+ "主食"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "工具",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 工具",
+ "notes": "量未指定"
+ },
+ {
+ "name": "微波炉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 微波炉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "2 个大碗(推荐微波炉专用碗)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 2 个大碗(推荐微波炉专用碗)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "1 个小碗",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 1 个小碗",
+ "notes": "量未指定"
+ },
+ {
+ "name": "原料",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 原料",
+ "notes": "量未指定"
+ },
+ {
+ "name": "米",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 米 200 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "腊肠",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 腊肠 1 根",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋 1 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "红萝卜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 红萝卜 1 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 油 15 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 10 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香葱 1 颗",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "将米淘洗干净后倒入 `饭碗` 内,加入 400ml 的水,**盖上盖**"
+ },
+ {
+ "step": 2,
+ "description": "放入微波炉,高火,`6` 分钟,煮饭途中准备原料"
+ },
+ {
+ "step": 3,
+ "description": "6 分钟后,用毛巾或隔热手套取出碗,可以看见米饭已经八分熟"
+ },
+ {
+ "step": 4,
+ "description": "在米饭上摆入切片的腊肠,继续高火 `2` 分钟"
+ },
+ {
+ "step": 5,
+ "description": "取出腊肠饭,放入 `青菜碗`,高火 `4-5` 分钟"
+ },
+ {
+ "step": 6,
+ "description": "在腊肠饭上摆好青菜,磕入鸡蛋,看个人喜好继续高火 `40-60` 秒"
+ },
+ {
+ "step": 7,
+ "description": "取出腊肠饭,此时已经基本完成。"
+ },
+ {
+ "step": 8,
+ "description": "将 `小碗` 放入,继续高火 `30` 秒"
+ },
+ {
+ "step": 9,
+ "description": "在腊肠饭上淋上叮热的生抽,撒上葱花即可"
+ },
+ {
+ "step": 10,
+ "description": "多余的青菜可以沾着酱油吃"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-staple-意式肉酱面-意式肉酱面",
+ "name": "意式肉酱面的做法",
+ "description": "# 意式肉酱面的做法\n\n\n\n意式肉酱面是一道非常容易做的菜,做得熟练的话,可以在 15 分钟内完成,从此告别方便面\n\n预估烹饪难度:★",
+ "source_path": "dishes/staple/意式肉酱面/意式肉酱面.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/意式肉酱面/final.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/意式肉酱面/final.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/意式肉酱面/sauce.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/意式肉酱面/spaghetti.jpg"
+ ],
+ "category": "主食",
+ "difficulty": 1,
+ "tags": [
+ "主食"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "意大利面",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 意大利面",
+ "notes": "量未指定"
+ },
+ {
+ "name": "意大利面酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 意大利面酱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "肉沫",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 肉沫",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白洋葱(紫洋葱也可以)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白洋葱(紫洋葱也可以)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "意大利面",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 意大利面 180 克(可以根据食量上下浮动)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "肉沫",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 肉沫 80 克(可以根据食量上下浮动)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "洋葱大半个 (大约",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 洋葱大半个 (大约 150 克,通常是肉的两倍重)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "意大利面酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 意大利面酱 300 克(可以看情况上下浮动)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 10-15ml",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "锅中加水,烧开后放入意面(等待 6 - 12 分钟)"
+ },
+ {
+ "step": 2,
+ "description": "在烧水的时候可以进行下面这些步骤,但请注意煮面的时间"
+ },
+ {
+ "step": 3,
+ "description": "洋葱切成小丁"
+ },
+ {
+ "step": 4,
+ "description": "空锅中倒油,中火下入洋葱碎"
+ },
+ {
+ "step": 5,
+ "description": "时刻搅拌,注意不要让洋葱烧糊,直到洋葱变成半透明状"
+ },
+ {
+ "step": 6,
+ "description": "下入肉沫,继续搅拌(搅散),直到肉末变成棕色"
+ },
+ {
+ "step": 7,
+ "description": "加入意大利面酱,稍微搅拌一下即可"
+ },
+ {
+ "step": 8,
+ "description": "把煮好的意大利面沥干水分并倒入肉酱中搅拌均匀即可(或者直接把做好的肉酱倒在意面上也行)"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-staple-扬州炒饭-扬州炒饭",
+ "name": "扬州炒饭的做法",
+ "description": "# 扬州炒饭的做法\n\n扬州炒饭是蛋炒饭的升级版,制作时间较长,但是制作步骤简单\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/staple/扬州炒饭/扬州炒饭.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/扬州炒饭/veg.png",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/扬州炒饭/veg.png"
+ ],
+ "category": "主食",
+ "difficulty": 4,
+ "tags": [
+ "主食"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "冷饭(干一点的为佳)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冷饭(干一点的为佳)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冷冻去皮基围虾",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冷冻去皮基围虾",
+ "notes": "量未指定"
+ },
+ {
+ "name": "午餐肉罐头",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 午餐肉罐头",
+ "notes": "量未指定"
+ },
+ {
+ "name": "青豆",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 青豆",
+ "notes": "量未指定"
+ },
+ {
+ "name": "胡萝卜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 胡萝卜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "玉米粒(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 玉米粒(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冷饭",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冷饭 500g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋 2-3 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冷冻去头去皮基围虾",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冷冻去头去皮基围虾 10-15 只",
+ "notes": "量未指定"
+ },
+ {
+ "name": "午餐肉罐头",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 午餐肉罐头 150g(推荐上海梅林的火腿午餐肉罐头,340g 每罐,一次用半罐)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "青豆",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 青豆 30g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "胡萝卜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 胡萝卜 30g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "玉米粒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 玉米粒 30g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱 1 根",
+ "notes": "量未指定"
+ },
+ {
+ "name": "油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 油 30-40ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 12-15g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "胡萝卜切丁 0.2cm*0.2cm*0.2cm,备用"
+ },
+ {
+ "step": 2,
+ "description": "午餐肉切丁 0.2cm*0.2cm*0.2cm,备用"
+ },
+ {
+ "step": 3,
+ "description": "葱分别取葱白和葱绿,各切成 0.25-0.5cm 的小段,分开备用"
+ },
+ {
+ "step": 4,
+ "description": "在碗中打入鸡蛋液,均匀搅拌,备用"
+ },
+ {
+ "step": 5,
+ "description": "将胡萝卜,青豆,玉米粒煮熟捞出,备用(水别倒)"
+ },
+ {
+ "step": 6,
+ "description": "将虾煮熟,捞出备用(水可以倒了)"
+ },
+ {
+ "step": 7,
+ "description": "热锅热油,可以参考[学习炒与煎](../../../tips/learn/学习炒与煎.md)中的热锅双油"
+ },
+ {
+ "step": 8,
+ "description": "鸡蛋凝固后立刻捞出,备用"
+ },
+ {
+ "step": 9,
+ "description": "将午餐肉,青豆,胡萝卜,玉米粒,虾倒入锅中翻炒 1-2 分钟,装盘备用"
+ },
+ {
+ "step": 10,
+ "description": "水冲一下锅,将杂物冲干净,保证锅内干净(可以有油但是不能有杂质)"
+ },
+ {
+ "step": 11,
+ "description": "热锅热油(10ml),将葱白放入爆香"
+ },
+ {
+ "step": 12,
+ "description": "调至小火(如果油温过高可以关火 1-2 分钟),放入米饭,用铲子快速砸击米饭并翻炒,保证米饭均匀沾到油且粒粒分明"
+ },
+ {
+ "step": 13,
+ "description": "倒入鸡蛋,继续砸击,使鸡蛋碎开并与米饭充分混合"
+ },
+ {
+ "step": 14,
+ "description": "转大火,倒入其他所有备用配料,快速翻炒 1-2 分钟"
+ },
+ {
+ "step": 15,
+ "description": "撒入盐,并翻炒至充分混合"
+ },
+ {
+ "step": 16,
+ "description": "撒入葱绿,翻炒 1 分钟"
+ },
+ {
+ "step": 17,
+ "description": "关火,装盘"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-staple-披萨饼皮-披萨饼皮",
+ "name": "披萨饼皮的做法",
+ "description": "# 披萨饼皮的做法\n\n\n\n披萨制作总体来说比较简单,稍微有点麻烦也是争议最多的就是披萨饼皮,做好了披萨饼皮喜欢吃什么口味的披萨,直接把准备好的食材放上去烤熟就好,所以这里重点说一下披萨饼皮如何制作。\n\n本教程中的饼皮是属于软面团低温隔夜发酵\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/staple/披萨饼皮/披萨饼皮.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/披萨饼皮/001.jpeg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/披萨饼皮/001.jpeg"
+ ],
+ "category": "主食",
+ "difficulty": 4,
+ "tags": [
+ "主食"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "*原料**",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- *原料**",
+ "notes": "量未指定"
+ },
+ {
+ "name": "中筋面粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 中筋面粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "水(温水)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 水(温水)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "安琪干酵母粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 安琪干酵母粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "橄榄油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 橄榄油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白砂糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白砂糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "*工具**",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- *工具**",
+ "notes": "量未指定"
+ },
+ {
+ "name": "烤箱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 烤箱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "烘焙油纸",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 烘焙油纸",
+ "notes": "量未指定"
+ },
+ {
+ "name": "披萨石(有更好,没有普通烤盘也可以)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 披萨石(有更好,没有普通烤盘也可以)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "擀面杖(非必需)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 擀面杖(非必需)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "面粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 面粉 125g x 4= 500g,",
+ "notes": "量未指定"
+ },
+ {
+ "name": "水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 水 70 x 5 = 350g,",
+ "notes": "量未指定"
+ },
+ {
+ "name": "橄榄油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 橄榄油 7 x 5 =35g,",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酵母粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酵母粉 1 x 5 = 5g,",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 0.6 x 5 = 3g,",
+ "notes": "量未指定"
+ },
+ {
+ "name": "糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 糖 0.6 x 5 = 3g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "用准备好的温水把酵母粉化开,稍微搅拌小就好,备用"
+ },
+ {
+ "step": 2,
+ "description": "取准备好的面粉,依次添加盐、橄榄油、白砂糖"
+ },
+ {
+ "step": 3,
+ "description": "准备混合水和面粉,边加水边搅拌直至水全部加完"
+ },
+ {
+ "step": 4,
+ "description": "搅拌至看不到干米粉为止"
+ },
+ {
+ "step": 5,
+ "description": "用差不多三倍大面团的容器装好,密封,冰箱冷藏(4 度) **等待 8~12 小时,一般晚上做第二天就可以用**"
+ },
+ {
+ "step": 6,
+ "description": "观察面团醒发完毕 **差不多是原始大小大约两倍算醒发完毕**"
+ },
+ {
+ "step": 7,
+ "description": "取醒发好的面团,均匀分成四份,分别用保鲜膜盖好,备用"
+ },
+ {
+ "step": 8,
+ "description": "案板撒稍微多一点的干面粉,准备开始揉面"
+ },
+ {
+ "step": 9,
+ "description": "因为是比较湿的面团,所以粘上干面粉后才没那么粘手,不用揉太多次,面团表面稍微光滑一点就可以了"
+ },
+ {
+ "step": 10,
+ "description": "用手拉扯,或者擀面杖擀平,也不一定非得擀圆,只要厚度均匀,烤箱放得进去就好"
+ },
+ {
+ "step": 11,
+ "description": "铺好油纸,放上饼皮,依照个人口味,把准备好的食材放上去,撒上芝士碎"
+ },
+ {
+ "step": 12,
+ "description": "水果烤箱上 180 度,下 220 度,16 分钟即可"
+ },
+ {
+ "step": 13,
+ "description": "肉蔬菜烤箱上 200 度,下 230 度,18 分钟即可"
+ },
+ {
+ "step": 14,
+ "description": "挤上沙拉酱或者其他自己喜欢的酱即可享用~"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-staple-日式咖喱饭-日式咖喱饭",
+ "name": "日式咖喱饭的做法",
+ "description": "# 日式咖喱饭的做法\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/staple/日式咖喱饭/日式咖喱饭.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/日式咖喱饭/成品.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/日式咖喱饭/成品.jpg"
+ ],
+ "category": "主食",
+ "difficulty": 4,
+ "tags": [
+ "主食"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "洋葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 洋葱 2 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "土豆",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 土豆 2 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "胡萝卜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 胡萝卜 1 根",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜头",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜头 2~3 瓣",
+ "notes": "量未指定"
+ },
+ {
+ "name": "肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 肉 2 斤",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "胡萝卜去头尾,去皮,滚刀切"
+ },
+ {
+ "step": 2,
+ "description": "洋葱剥去外层去芯,切成月牙状"
+ },
+ {
+ "step": 3,
+ "description": "土豆去皮、切大块"
+ },
+ {
+ "step": 4,
+ "description": "肉切块状"
+ },
+ {
+ "step": 5,
+ "description": "剥蒜拍平切碎"
+ },
+ {
+ "step": 6,
+ "description": "咖喱块切碎,增加接触面积加速溶解"
+ },
+ {
+ "step": 7,
+ "description": "热油锅放入蒜和肉,**快速翻炒**至肉*表面变白*"
+ },
+ {
+ "step": 8,
+ "description": "加入胡萝卜,**快速翻炒**至均匀受热"
+ },
+ {
+ "step": 9,
+ "description": "加入洋葱,**快速翻炒**至洋葱*变透明状*"
+ },
+ {
+ "step": 10,
+ "description": "加入土豆,保持翻炒至土豆*变软*(可以用筷子确认)"
+ },
+ {
+ "step": 11,
+ "description": "加水没过所有食材,沸腾后**等待 15 分钟**"
+ },
+ {
+ "step": 12,
+ "description": "关火,加咖喱并搅拌"
+ },
+ {
+ "step": 13,
+ "description": "等待咖喱融化后再开火,缓慢**搅拌 10 分钟**,防止糊锅"
+ },
+ {
+ "step": 14,
+ "description": "在外观*呈粘稠状态*关火结束制作"
+ },
+ {
+ "step": 15,
+ "description": "微波炉:单人份高火 2-3 分钟"
+ },
+ {
+ "step": 16,
+ "description": "锅:需额外加 50ml 水,加热时保持搅拌"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-staple-日式肥牛丼饭-日式肥牛丼饭",
+ "name": "日式肥牛丼饭的做法",
+ "description": "# 日式肥牛丼饭的做法\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/staple/日式肥牛丼饭/日式肥牛丼饭.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/日式肥牛丼饭/成品.png",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/日式肥牛丼饭/成品.png"
+ ],
+ "category": "主食",
+ "difficulty": 4,
+ "tags": [
+ "主食"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "洋葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 洋葱 1 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "肥牛",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 肥牛 250 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱 1~2 根",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白芝麻",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白芝麻 5 克",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "洋葱剥去外层去芯,切成月牙状"
+ },
+ {
+ "step": 2,
+ "description": "葱洗净切成 0.5cm 的小段"
+ },
+ {
+ "step": 3,
+ "description": "热锅直接放入白芝麻,**前后晃动锅体**使芝麻均匀受热至*略呈金黄色*"
+ },
+ {
+ "step": 4,
+ "description": "肥牛焯水 1 分钟后捞出"
+ },
+ {
+ "step": 5,
+ "description": "将 40g `味淋`(或 30g `料酒`),30g `酱油`,20g `耗油`,5g `糖`,5g `老抽`(可选,用于调色),在碗中搅拌混合成`调料`(该步骤可直接将碗放在电子秤上进行)"
+ },
+ {
+ "step": 6,
+ "description": "热油锅放入洋葱,**快速翻炒**至洋葱*变透明状*"
+ },
+ {
+ "step": 7,
+ "description": "关小火,加入 250g 水(或出汁),开回大火加热**等待 3 分钟**"
+ },
+ {
+ "step": 8,
+ "description": "加入牛肉和`调料`"
+ },
+ {
+ "step": 9,
+ "description": "**不断翻动**所有食材 **10 分钟**,防止食材粘锅"
+ },
+ {
+ "step": 10,
+ "description": "关火"
+ },
+ {
+ "step": 11,
+ "description": "盛装肥牛丼至[米饭](../米饭/电饭煲蒸米饭.md)上(注意要把汁水淋一些在饭上)"
+ },
+ {
+ "step": 12,
+ "description": "撒上葱花和白芝麻,制作完成。"
+ },
+ {
+ "step": 13,
+ "description": "微波炉:单人份高火 2-3 分钟"
+ },
+ {
+ "step": 14,
+ "description": "锅:需额外加 50ml 水,加热时需**不断翻动**"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-staple-河南蒸面条-河南蒸面条",
+ "name": "河南蒸面条的做法",
+ "description": "# 河南蒸面条的做法\n\n\n\n河南蒸面条是一道在河南坊间流行的小吃,也可以用家里的挂面制作。\n\n简单来讲,是先将挂面裹油放入蒸笼蒸熟,再加蔬菜配以调料炒,最后二次蒸制,以达到入味劲道的效果。\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/staple/河南蒸面条/河南蒸面条.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/河南蒸面条/河南蒸面条.png",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/河南蒸面条/河南蒸面条.png"
+ ],
+ "category": "主食",
+ "difficulty": 4,
+ "tags": [
+ "主食"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "挂面 (推荐圆的)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 挂面 (推荐圆的)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "五花肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 五花肉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜薹",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜薹",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱 + 姜 + 蒜 + 料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱 + 姜 + 蒜 + 料酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐 + 鸡精 + 十三香",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 + 鸡精 + 十三香",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽 + 老抽 + 蚝油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 + 老抽 + 蚝油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "麻油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 麻油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "油 + 锅 + 菜刀 + 铲子",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 油 + 锅 + 菜刀 + 铲子",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒸篦子",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒸篦子",
+ "notes": "量未指定"
+ },
+ {
+ "name": "额外的盆",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 额外的盆",
+ "notes": "量未指定"
+ },
+ {
+ "name": "挂面",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 挂面 300g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "五花肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 五花肉 350g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜薹",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜薹 150g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 10-15ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 15ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽 10ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蚝油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蚝油 5ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 2g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡精",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡精 2g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "十三香",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 十三香 1g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱 10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜 10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒 5ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "麻油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 麻油 5ml",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "起锅加 7 成水,水开,上蒸篦子"
+ },
+ {
+ "step": 2,
+ "description": "将挂面,均匀铺开放置,淋 5ml 油并抹匀,蒸 15 分钟"
+ },
+ {
+ "step": 3,
+ "description": "将挂面和蒸篦子取出,放置一边,并倒掉锅中的水"
+ },
+ {
+ "step": 4,
+ "description": "五花肉,切成 2mm 厚度的肉片"
+ },
+ {
+ "step": 5,
+ "description": "蒜薹,切成 3cm 段"
+ },
+ {
+ "step": 6,
+ "description": "葱,切成 0.2cm 薄片"
+ },
+ {
+ "step": 7,
+ "description": "姜,切成 1mm x 1mm x 3cm 的细丝"
+ },
+ {
+ "step": 8,
+ "description": "蒜,放在砧板上拍碎,切成 1mm 的粒度"
+ },
+ {
+ "step": 9,
+ "description": "起锅,烧干水分,加 3ml 食用油"
+ },
+ {
+ "step": 10,
+ "description": "手持锅柄,摇晃锅,使食用油充分挂满锅的 2/3"
+ },
+ {
+ "step": 11,
+ "description": "中火,加入肉片,翻炒 1 分钟"
+ },
+ {
+ "step": 12,
+ "description": "加入葱姜蒜,料酒,继续翻炒 1 分钟"
+ },
+ {
+ "step": 13,
+ "description": "将蒜薹段,放入锅中,翻炒 1 分钟"
+ },
+ {
+ "step": 14,
+ "description": "开始调味,加入老抽、生抽、蚝油、盐、鸡精、十三香,翻炒 1 分钟"
+ },
+ {
+ "step": 15,
+ "description": "加入 500ML 水,没过蔬菜,炖煮 1 分钟"
+ },
+ {
+ "step": 16,
+ "description": "将蒸好的挂面放入,不断搅拌 3 分钟,待挂面全部均匀上色,关火"
+ },
+ {
+ "step": 17,
+ "description": "将搅拌好的挂面和菜,全部倒入额外的盆中"
+ },
+ {
+ "step": 18,
+ "description": "起锅,加冷水 7 成,放上蒸篦子,将拌好的面条和菜,均匀的铺在上面"
+ },
+ {
+ "step": 19,
+ "description": "水开后,大火烧 15 分钟,出锅"
+ },
+ {
+ "step": 20,
+ "description": "淋上 10g 的麻油,即可食用"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-staple-火腿饭团-火腿饭团",
+ "name": "火腿饭团的做法",
+ "description": "# 火腿饭团的做法\n\n\n好吃!富含碳水和蛋白质还有维生素。有手就行的制作难度,预计制作时间 1 h 。\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/staple/火腿饭团/火腿饭团.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/火腿饭团/饭团.png",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/火腿饭团/饭团.png"
+ ],
+ "category": "主食",
+ "difficulty": 4,
+ "tags": [
+ "主食"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "火腿",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 火腿",
+ "notes": "量未指定"
+ },
+ {
+ "name": "米饭",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 米饭",
+ "notes": "量未指定"
+ },
+ {
+ "name": "水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 水",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冷冻青豆(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冷冻青豆(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冷冻玉米粒(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冷冻玉米粒(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "海苔碎(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 海苔碎(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "喜欢的沙拉酱(推荐日式 mayo!)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 喜欢的沙拉酱(推荐日式 mayo!)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "火腿(100g)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 火腿(100g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "米饭(125g)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 米饭(125g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "水(90ml)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 水(90ml)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冷冻青豆(30g)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冷冻青豆(30g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冷冻玉米粒(30g)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冷冻玉米粒(30g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "海苔碎(10g)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 海苔碎(10g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "喜欢的沙拉酱(20g)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 喜欢的沙拉酱(20g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 10-15ml",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "将米饭和水放到电饭锅里,点击米饭模式,等待完成"
+ },
+ {
+ "step": 2,
+ "description": "冷冻玉米粒和青豆放到锅里,加水没过所有食材,沸腾后静待 2 分钟后,捞出。"
+ },
+ {
+ "step": 3,
+ "description": "火腿切成 1cm 的方块"
+ },
+ {
+ "step": 4,
+ "description": "与此同时,加入 10ml 食用油,加入火腿翻炒至火腿上色"
+ },
+ {
+ "step": 5,
+ "description": "将米饭,火腿,海苔碎,青豆,玉米粒,沙拉酱放入碗中,混合均匀即可"
+ },
+ {
+ "step": 6,
+ "description": "装盘(如果有的话)"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-staple-炒凉粉-炒凉粉",
+ "name": "炒凉粉的做法",
+ "description": "# 炒凉粉的做法\n\n\n\n炒凉粉是一道流行于山西、陕西地区的一道特色小吃,入口滑嫩,老少皆宜。\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/staple/炒凉粉/炒凉粉.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/炒凉粉/chaoliangfen.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/炒凉粉/chaoliangfen.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/炒凉粉/炒凉粉成品.jpg"
+ ],
+ "category": "主食",
+ "difficulty": 3,
+ "tags": [
+ "主食"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "凉粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 凉粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "玉米油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 玉米油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "豆瓣酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 豆瓣酱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "十三香",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 十三香",
+ "notes": "量未指定"
+ },
+ {
+ "name": "中粗辣椒面",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 中粗辣椒面",
+ "notes": "量未指定"
+ },
+ {
+ "name": "矿泉水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 矿泉水",
+ "notes": "量未指定"
+ },
+ {
+ "name": "凉粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 凉粉 500g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "玉米油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 玉米油 10ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜末",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜末 10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香葱 15g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "豆瓣酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 豆瓣酱 15g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 20ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽 10ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食盐 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "十三香",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 十三香 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "中粗辣椒面",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 中粗辣椒面 15g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "矿泉水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 矿泉水 20ml",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "凉粉改刀切麻将块大小"
+ },
+ {
+ "step": 2,
+ "description": "开小火,起锅烧油,锅烧微热后,下入蒜末爆香后加入豆瓣酱炒出红油"
+ },
+ {
+ "step": 3,
+ "description": "将凉粉块下入锅中,翻炒 10 秒"
+ },
+ {
+ "step": 4,
+ "description": "加入生抽提味,老抽上色,翻炒均匀后加入辣椒面继续翻炒均匀"
+ },
+ {
+ "step": 5,
+ "description": "加入食盐、十三香继续翻炒 10 秒"
+ },
+ {
+ "step": 6,
+ "description": "加入准备好的矿泉水,再次翻炒 10 秒,待汤汁浓稠后,关火出锅装盘"
+ },
+ {
+ "step": 7,
+ "description": "撒上葱花即可完成"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-staple-炒意大利面-炒意大利面",
+ "name": "炒意大利面的做法",
+ "description": "# 炒意大利面的做法\n\n\n\n这是一道软糯爽口的意大利面的做法,非常简单,用时大概 30 分钟。\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/staple/炒意大利面/炒意大利面.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/炒意大利面/a.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/炒意大利面/a.jpg"
+ ],
+ "category": "主食",
+ "difficulty": 3,
+ "tags": [
+ "主食"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "意大利面",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 意大利面",
+ "notes": "量未指定"
+ },
+ {
+ "name": "肥牛片",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 肥牛片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "番茄酱 / 黑胡椒酱(选其一即可)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 番茄酱 / 黑胡椒酱(选其一即可)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "菜籽油(其他植物油也可)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 菜籽油(其他植物油也可)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "意大利面",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 意大利面 50 克 / 人",
+ "notes": "量未指定"
+ },
+ {
+ "name": "肥牛",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 肥牛 5 片 / 人",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 5ml / 50 克意面",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "加入 250 克水 / 人"
+ },
+ {
+ "step": 2,
+ "description": "待水烧开,下入面条,中火煮 15 - 20 分钟(这个面通常比较硬,捞起来之前最好尝一下,中心如果有一点硬,需要继续煮)"
+ },
+ {
+ "step": 3,
+ "description": "捞出面条,盛入盘中备用"
+ },
+ {
+ "step": 4,
+ "description": "热锅倒入食用油,待油温中热,下入面条翻炒一分钟(如果太干,加入少量水)"
+ },
+ {
+ "step": 5,
+ "description": "放入 10 克番茄酱、肥牛、加入 2g 食盐,继续翻炒一分钟"
+ },
+ {
+ "step": 6,
+ "description": "起锅"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-staple-烙饼-烙饼",
+ "name": "烙饼的做法",
+ "description": "# 烙饼的做法\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/staple/烙饼/烙饼.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/烙饼/成品.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/烙饼/成品.jpg"
+ ],
+ "category": "主食",
+ "difficulty": 4,
+ "tags": [
+ "主食"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "面粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 面粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "电饼铛",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 电饼铛",
+ "notes": "量未指定"
+ },
+ {
+ "name": "面粉 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 面粉 = 400g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "热水 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 热水 = 130ml(80 度)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冷水 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冷水 = 130ml",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "将 400g 面粉倒入盆中,一半用凉水和面,一半用热水和面,搅拌成面絮,用手揉成团。用保鲜膜封起来,醒面 40 分钟"
+ },
+ {
+ "step": 2,
+ "description": "离醒面完成时间还有 10 分钟时,请查看[小技巧](../../condiment/油酥.md)中的油酥做法(热油酥效果更好)"
+ },
+ {
+ "step": 3,
+ "description": "醒好的面不用揉,稍微摁一下,用一横刀一竖刀将其分成四份。"
+ },
+ {
+ "step": 4,
+ "description": "搓圆,擀开,擀成与电饼铛大小差不多的饼,取 1/4 的油酥,将饼表面涂抹均匀"
+ },
+ {
+ "step": 5,
+ "description": "沿饼的半径切开,从外圈将其卷成圆锥形,然后将圆锥尾部捏好,防止油酥外漏。"
+ },
+ {
+ "step": 6,
+ "description": "按压面饼圆锥尖的地方,将其压扁,然后再次擀成与电饼铛大小差不多的面饼(厚度约为 3mm)"
+ },
+ {
+ "step": 7,
+ "description": "将电饼铛预热,涂上凉油(热锅凉油),将擀好的饼放入电饼铛中,将饼的上方也刷点油,涂抹均匀(锁住水分),盖上盖子"
+ },
+ {
+ "step": 8,
+ "description": "大火烙一分钟,打开盖子,将饼翻个面再烙一分钟"
+ },
+ {
+ "step": 9,
+ "description": "重复以上动作,完成饼的烙制"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-staple-烧饼-芝麻烧饼",
+ "name": "芝麻烧饼的做法",
+ "description": "# 芝麻烧饼的做法\n\n\n芝麻烧饼,外酥里软,简单易做。\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/staple/烧饼/芝麻烧饼.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/烧饼/芝麻烧饼.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/烧饼/芝麻烧饼.jpg"
+ ],
+ "category": "主食",
+ "difficulty": 3,
+ "tags": [
+ "主食"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "面粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 面粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酵母粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酵母粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "十三香",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 十三香",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "温水(",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 温水( 40℃ )",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "面团:300 克面粉,3 克酵母粉,3 克白糖,180 克温水,20 克食用油,醒面 10 分钟"
+ },
+ {
+ "step": 2,
+ "description": "油酥:小碗放 30 克面粉,2 克盐,4 克十三香,20 克食用油,拌匀后,静置"
+ },
+ {
+ "step": 3,
+ "description": "做饼:面擀成长方形,抹上调好的油酥,从一头卷起,切成 7 个面剂子,对折,用虎口收拢即可,先沾水再沾白芝麻,擀成小圆饼"
+ },
+ {
+ "step": 4,
+ "description": "烙饼:将电饼铛预热,倒入凉油(锅底铺满油),将擀好的饼放入电饼铛中,将饼的上方也刷点油,涂抹均匀盖上盖子,选大饼档,听到叮的一声出锅即可"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-staple-电饭煲三文鱼炊饭-电饭煲三文鱼炊饭",
+ "name": "电饭煲三文鱼炊饭的做法",
+ "description": "# 电饭煲三文鱼炊饭的做法\n\n\n\n预估烹饪难度:★★",
+ "source_path": "dishes/staple/电饭煲三文鱼炊饭/电饭煲三文鱼炊饭.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/电饭煲三文鱼炊饭/电饭煲三文鱼炊饭.webp",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/电饭煲三文鱼炊饭/电饭煲三文鱼炊饭.webp"
+ ],
+ "category": "主食",
+ "difficulty": 2,
+ "tags": [
+ "主食"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "有盐牛油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 有盐牛油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "三文鱼",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 三文鱼",
+ "notes": "量未指定"
+ },
+ {
+ "name": "米",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 米",
+ "notes": "量未指定"
+ },
+ {
+ "name": "粟米(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 粟米(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "金菇(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 金菇(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冬菇(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冬菇(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "米",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 米 1 杯 / 人",
+ "notes": "量未指定"
+ },
+ {
+ "name": "三文鱼",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 三文鱼 300g / 人",
+ "notes": "量未指定"
+ },
+ {
+ "name": "水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 水",
+ "notes": "量未指定"
+ },
+ {
+ "name": "牛油一汤匙 / 人",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 牛油一汤匙 / 人",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "三文鱼去鳞,去骨"
+ },
+ {
+ "step": 2,
+ "description": "金菇、冬菇切碎"
+ },
+ {
+ "step": 3,
+ "description": "洗米三次"
+ },
+ {
+ "step": 4,
+ "description": "把三文鱼、米、牛油放入电饭煲"
+ },
+ {
+ "step": 5,
+ "description": "想口感浓厚一点,可以加多一汤匙牛油"
+ },
+ {
+ "step": 6,
+ "description": "根据电饭煲的刻度放水"
+ },
+ {
+ "step": 7,
+ "description": "把电饭煲調較至煲飯模式,等待大約 30 - 45 分鐘"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-staple-空气炸锅照烧鸡饭-空气炸锅照烧鸡饭",
+ "name": "空气炸锅照烧鸡饭的做法",
+ "description": "# 空气炸锅照烧鸡饭的做法\n\n\n\n空气炸锅照烧鸡饭是一道简单易做的菜。是一道既便利又便宜的美食,而且在品尝美味的同时,新手也能完全掌握!\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/staple/空气炸锅照烧鸡饭/空气炸锅照烧鸡饭.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/空气炸锅照烧鸡饭/空气炸锅照烧鸡饭.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/空气炸锅照烧鸡饭/空气炸锅照烧鸡饭.jpg"
+ ],
+ "category": "主食",
+ "difficulty": 4,
+ "tags": [
+ "主食"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "丽滋饼干(Ritz crackers)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 丽滋饼干(Ritz crackers)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酱油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酱油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "糖(白沙糖)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 糖(白沙糖)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡肉 900g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酱油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酱油 100-125ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 糖 60-65g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白醋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白醋 30-35ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "丽滋饼干(咸味曲奇可替代)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 丽滋饼干(咸味曲奇可替代) 16 个(48g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋 2 个",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "将酱油、糖和醋混合在一起,搅匀料汁备用"
+ },
+ {
+ "step": 2,
+ "description": "另一个碗中加入鸡肉、鸡蛋、1/2 料汁和压碎的丽滋饼干。搅拌均匀"
+ },
+ {
+ "step": 3,
+ "description": "空气炸锅用箔纸碗铺底,加入肉饼混合物,将剩余的料汁均匀的倒在上面"
+ },
+ {
+ "step": 4,
+ "description": "**350°** 炸**40 分钟**。最好在米饭上食用"
+ },
+ {
+ "step": 5,
+ "description": "在外观*呈金黄酥脆*后出锅,切块盛盘"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-staple-米饭-煮锅蒸米饭",
+ "name": "煮锅蒸米饭的做法",
+ "description": "# 煮锅蒸米饭的做法\n\n预估烹饪难度:★★",
+ "source_path": "dishes/staple/米饭/煮锅蒸米饭.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/米饭/rice_regularPot.jpeg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/米饭/rice_regularPot.jpeg"
+ ],
+ "category": "主食",
+ "difficulty": 2,
+ "tags": [
+ "主食"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "北方大米",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 北方大米",
+ "notes": "量未指定"
+ },
+ {
+ "name": "水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 水",
+ "notes": "量未指定"
+ },
+ {
+ "name": "厚底煮锅+严丝合缝的锅盖(制作过程中不会有大量蒸汽泄漏)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 厚底煮锅+严丝合缝的锅盖(制作过程中不会有大量蒸汽泄漏)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "米:100ml-200ml/人",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 米:100ml-200ml/人",
+ "notes": "量未指定"
+ },
+ {
+ "name": "水:米的体积的",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 水:米的体积的 2 倍",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "清洗大米"
+ },
+ {
+ "step": 2,
+ "description": "将米和水加入煮锅"
+ },
+ {
+ "step": 3,
+ "description": "大火煮至水沸腾"
+ },
+ {
+ "step": 4,
+ "description": "**搅拌底部防止粘黏**"
+ },
+ {
+ "step": 5,
+ "description": "盖上锅盖,转**小火**加热 10-15 分钟(根据对软糯程度的喜好),中途切勿打开锅盖"
+ },
+ {
+ "step": 6,
+ "description": "关火,静置 5 分钟"
+ },
+ {
+ "step": 7,
+ "description": "Enjoy :)"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-staple-米饭-电饭煲蒸米饭",
+ "name": "电饭煲蒸米饭的做法",
+ "description": "# 电饭煲蒸米饭的做法\n\n预估烹饪难度:★",
+ "source_path": "dishes/staple/米饭/电饭煲蒸米饭.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/米饭/rice_regularPot.jpeg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/米饭/rice_regularPot.jpeg"
+ ],
+ "category": "主食",
+ "difficulty": 1,
+ "tags": [
+ "主食"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "电饭煲",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 电饭煲",
+ "notes": "量未指定"
+ },
+ {
+ "name": "江南米或北方大米",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 江南米或北方大米",
+ "notes": "量未指定"
+ },
+ {
+ "name": "水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 水",
+ "notes": "量未指定"
+ },
+ {
+ "name": "一般一个人可以食用",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 一般一个人可以食用 100ml-200ml 的米。",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "清洗米"
+ },
+ {
+ "step": 2,
+ "description": "将米和水一起加入电饭煲中。"
+ },
+ {
+ "step": 3,
+ "description": "连接电饭煲电源,进入加热模式。等待大约 30 分钟。"
+ },
+ {
+ "step": 4,
+ "description": "待电饭煲自动进入保温模式后。"
+ },
+ {
+ "step": 5,
+ "description": "将米在电饭煲中闷 10-15 分钟。"
+ },
+ {
+ "step": 6,
+ "description": "盛出米。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-staple-老友猪肉粉-老友猪肉粉",
+ "name": "老友猪肉粉的做法",
+ "description": "# 老友猪肉粉的做法\n\n\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/staple/老友猪肉粉/老友猪肉粉.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/老友猪肉粉/老友猪肉粉.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/老友猪肉粉/老友猪肉粉.jpg"
+ ],
+ "category": "主食",
+ "difficulty": 3,
+ "tags": [
+ "主食"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "米粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 米粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "猪肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 猪肉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酸笋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酸笋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "剁椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 剁椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "豆豉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 豆豉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "米醋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 米醋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "胡椒粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 胡椒粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "米粉(250g 记得",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 米粉(250g 记得 50 度的温水泡半小时)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "猪肉(50g)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 猪肉(50g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酸笋(50g)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酸笋(50g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "剁椒(15g 辣椒剁完后, 个人需求适当放。 )",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 剁椒(15g 辣椒剁完后, 个人需求适当放。 )",
+ "notes": "量未指定"
+ },
+ {
+ "name": "豆豉(30g)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 豆豉(30g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大蒜(10g)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大蒜(10g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒(10-20ml)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒(10-20ml)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽(15ml)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽(15ml)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖(5g 如果不喜欢糖,可以考虑不放)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖(5g 如果不喜欢糖,可以考虑不放)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "米醋(5ml)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 米醋(5ml)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐(5ml)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐(5ml)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "油(15ml)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 油(15ml)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生粉(15ml)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生粉(15ml)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "胡椒粉(10ml)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 胡椒粉(10ml)",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "全部猪肉用料酒、盐、生抽、生粉、胡椒粉倒在一个碗里调味,备用"
+ },
+ {
+ "step": 2,
+ "description": "热锅不放油,下全部酸笋把水份炒干,炒干的酸笋中间留点空间"
+ },
+ {
+ "step": 3,
+ "description": "放入 10ml - 15ml 食用油与全部大蒜、 剁椒、 豆豉到炒干的酸笋中间到炒干的酸笋中间,全部推到中间炒出香味"
+ },
+ {
+ "step": 4,
+ "description": "放入全部调味好的猪肉,持续放入 10ml 生抽炒一分钟"
+ },
+ {
+ "step": 5,
+ "description": "放入 5ml 米醋、 10ml 生抽、450ml 清水一起煮开"
+ },
+ {
+ "step": 6,
+ "description": "水煮开后,放入温水泡好的米粉,继续煮 3 分钟就可以盛盘"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-staple-茄子肉煎饼-茄子肉煎饼",
+ "name": "茄子肉煎饼的做法",
+ "description": "# 茄子肉煎饼的做法\n\n\n\n茄子肉煎饼是一道简单易做的饼类主食。\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/staple/茄子肉煎饼/茄子肉煎饼.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/茄子肉煎饼/1茄片肉片.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/茄子肉煎饼/1茄片肉片.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/茄子肉煎饼/2米粉250g.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/茄子肉煎饼/3米粉面粉鸡蛋.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/茄子肉煎饼/4混合.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/茄子肉煎饼/5起锅烧油.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/茄子肉煎饼/6开始煎.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/茄子肉煎饼/7撒盐准备起锅.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/茄子肉煎饼/茄子肉煎饼.jpg"
+ ],
+ "category": "主食",
+ "difficulty": 3,
+ "tags": [
+ "主食"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "米粉(指用大米研磨成的粉)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 米粉(指用大米研磨成的粉)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小麦粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小麦粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "煮熟的腊肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 煮熟的腊肉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "茄子(买长条状的,越圆越好)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 茄子(买长条状的,越圆越好)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "米粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 米粉 250g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "面粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 面粉 50g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋 1 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "煮熟的腊肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 煮熟的腊肉 100g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "茄子",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 茄子 1 根(约 10-15cm 长)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 10-15ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食盐 1-2g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "将茄子去皮后切成片,将腊肉切成片,备用"
+ },
+ {
+ "step": 2,
+ "description": "依次向盆中加入 250g 米粉(大米研磨成的粉)、50g 面粉和 1 个鸡蛋"
+ },
+ {
+ "step": 3,
+ "description": "边用筷子搅拌,边加入清水(**清水用于调节粘稠度**),使米粉、面粉、鸡蛋混合成面糊,当面糊能够附着在茄片、肉片上而不掉落时停止加水,而后将所有茄片和肉片放入面糊中,用面糊充分包裹"
+ },
+ {
+ "step": 4,
+ "description": "平底锅加入食用油**10-30ml**,开小火"
+ },
+ {
+ "step": 5,
+ "description": "用筷子或勺子把裹了面糊的茄片、肉片放入锅中,先煎至两面金黄,再煎**3-6分钟**(**煎的过程中,食用油会变少,可再添加食用油**)"
+ },
+ {
+ "step": 6,
+ "description": "撒盐,翻炒均匀,起锅装盘"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-staple-西红柿鸡蛋挂面-西红柿鸡蛋挂面",
+ "name": "西红柿鸡蛋挂面的做法",
+ "description": "# 西红柿鸡蛋挂面的做法\n\n挂面太多怎么办?只煮个白水面味道难以下咽怎么办?简单的食材煮个美味的面条怎么操作?\n西红柿鸡蛋挂面只需简单的食材,快速的操作,不多的厨具,解决**不想麻烦**、**挂面太多**、**食材简单**的所有烦恼\n此处更要鸣谢 my mother 的在线指导:v:\n简单好做,开始吧!\n制作时间:20 分钟\n\n预估烹饪难度:★★",
+ "source_path": "dishes/staple/西红柿鸡蛋挂面/西红柿鸡蛋挂面.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/西红柿鸡蛋挂面/food.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/西红柿鸡蛋挂面/food.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/西红柿鸡蛋挂面/fryEgg.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/西红柿鸡蛋挂面/pretreatFood.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/西红柿鸡蛋挂面/tomato.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/西红柿鸡蛋挂面/tomatoNoodle.jpg"
+ ],
+ "category": "主食",
+ "difficulty": 2,
+ "tags": [
+ "主食"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "挂面或者鲜面条也行",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 挂面或者鲜面条也行",
+ "notes": "量未指定"
+ },
+ {
+ "name": "西红柿一个",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 西红柿一个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酱油、蚝油或者鸡精",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酱油、蚝油或者鸡精",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白砂糖(中和西红柿的酸味,西红柿如果不酸就不用加)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白砂糖(中和西红柿的酸味,西红柿如果不酸就不用加)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "青椒(非线椒)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 青椒(非线椒)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "挂面",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 挂面 1 把(根据食量来)50-100g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "西红柿",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 西红柿 1 个大概 200g 吧。",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋 1~2 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蚝油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蚝油 5g 或鸡精 3g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白砂糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白砂糖 2g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酱油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酱油 5-8g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香油 5g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "小葱洗净并切成葱花"
+ },
+ {
+ "step": 2,
+ "description": "西红柿切块儿,如果不太会切建议先百度一下~"
+ },
+ {
+ "step": 3,
+ "description": "青椒切成菱形块"
+ },
+ {
+ "step": 4,
+ "description": "生鸡蛋打入一个小碗并打散,如果鸡蛋有点腥味可以加 2g 白醋去腥"
+ },
+ {
+ "step": 5,
+ "description": "起锅烧热,倒入 15~20g 食用油,鸡蛋炒嫩滑就得多一点油,同时为后面煸炒西红柿留一些底油"
+ },
+ {
+ "step": 6,
+ "description": "待油温到七成热时(手掌隔大概 10cm,能感觉到热),倒入蛋液快速划散"
+ },
+ {
+ "step": 7,
+ "description": "鸡蛋滑到凝固后,一点不会有蛋液了后倒入小碗备用,此处留一些底油"
+ },
+ {
+ "step": 8,
+ "description": "锅中留底油后先加入葱白、蒜末炒香"
+ },
+ {
+ "step": 9,
+ "description": "加入西红柿块、青椒,待西红柿炒出一点汁水"
+ },
+ {
+ "step": 10,
+ "description": "此时速速加入 5g 酱油和 2g 白砂糖"
+ },
+ {
+ "step": 11,
+ "description": "翻炒十几秒后加入一碗清水(刚刚好即将没过西红柿即可)"
+ },
+ {
+ "step": 12,
+ "description": "煮沸后加入炒好的鸡蛋,加入蚝油 5g 或者 2g 鸡精用于提鲜"
+ },
+ {
+ "step": 13,
+ "description": "中小火收汁,期间要搅拌防止粘锅,收汁到下图后加一点葱花(剩下的葱绿部分)和香油(不加也可以),臊子制作完成"
+ },
+ {
+ "step": 14,
+ "description": "可以不用洗锅,直接加清水 500ml"
+ },
+ {
+ "step": 15,
+ "description": "煮沸加入挂面,挂面煮软后加入 100ml 清水"
+ },
+ {
+ "step": 16,
+ "description": "再次煮沸后,若面条飘起来了,再加入 100ml 清水"
+ },
+ {
+ "step": 17,
+ "description": "煮沸后看面条两侧是否呈透明状,透明状则熟了"
+ },
+ {
+ "step": 18,
+ "description": "捞面到臊子碗中,拌面即可啦~"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-staple-豆角焖面-豆角焖面",
+ "name": "豆角焖面的做法",
+ "description": "# 豆角焖面的做法\n\n豆角焖面是一道懒人美食,操作简单,方便美味。\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/staple/豆角焖面/豆角焖面.md",
+ "image_path": null,
+ "images": [],
+ "category": "主食",
+ "difficulty": 3,
+ "tags": [
+ "主食"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "鲜面条(韭叶 or 二细<解释见最下方 关于面条粗细区别一栏)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鲜面条(韭叶 or 二细<解释见最下方 关于面条粗细区别一栏)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "肉(最好为五花肉)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 肉(最好为五花肉)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "豆角",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 豆角",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡精",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡精",
+ "notes": "量未指定"
+ },
+ {
+ "name": "耗油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 耗油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "味精",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 味精",
+ "notes": "量未指定"
+ },
+ {
+ "name": "十三香",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 十三香",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜头",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜头",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "热水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 热水",
+ "notes": "量未指定"
+ },
+ {
+ "name": "菜刀",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 菜刀",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鲜面条",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鲜面条 300g。",
+ "notes": "量未指定"
+ },
+ {
+ "name": "肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 肉 100g。",
+ "notes": "量未指定"
+ },
+ {
+ "name": "豆角",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 豆角 150g。",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱 10g。",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜 5g。",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜 10g。",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "将豆角切成 5cm - 6cm 的小段。"
+ },
+ {
+ "step": 2,
+ "description": "将葱切成 1cm - 2cm 小段。"
+ },
+ {
+ "step": 3,
+ "description": "将姜切成 1mm x 1mm x 3cm 的长条"
+ },
+ {
+ "step": 4,
+ "description": "将蒜放在砧板上拍碎,切成 1mm 的粒度。"
+ },
+ {
+ "step": 5,
+ "description": "将五花肉切成 2mm 厚度的肉片。"
+ },
+ {
+ "step": 6,
+ "description": "首先将锅烧热,烧去锅内全部水汽,手放过内距离锅底 10cm 处,感觉明显有些许烤手。"
+ },
+ {
+ "step": 7,
+ "description": "加入上述定量的食用油,手持锅柄,离灶 5cm 处,摇晃锅,使食用油充分挂满锅的三分之二(自下而上)。"
+ },
+ {
+ "step": 8,
+ "description": "放入全部的姜和全部的葱段,翻炒爆香 5 秒(注意!此时有油飞溅的危险,建议带上手套或做好防护措施)。"
+ },
+ {
+ "step": 9,
+ "description": "放入全部的肉片,放入以后不着急饭锅,静置 5 秒后,再翻炒,使所有的肉都裹满食用油。"
+ },
+ {
+ "step": 10,
+ "description": "不断翻炒肉片,待到全部肉片都已经变色,沿锅边均匀淋如准备好的生抽,翻炒均匀。"
+ },
+ {
+ "step": 11,
+ "description": "依次加入准备好的盐、老抽、耗油、十三香、鸡精以及全部准备好的豆角,翻炒 2 分钟。"
+ },
+ {
+ "step": 12,
+ "description": "加入准备好的热水。"
+ },
+ {
+ "step": 13,
+ "description": "水开使用勺子舀出锅内 2 分之一菜汤(注意!不要将菜舀出)。"
+ },
+ {
+ "step": 14,
+ "description": "将所有面条平铺在菜的上方。"
+ },
+ {
+ "step": 15,
+ "description": "盖上锅盖,中火焖 5 分钟。"
+ },
+ {
+ "step": 16,
+ "description": "打开锅盖,将舀出的菜汤使用勺子,以每次一勺的量,均匀撒在面条上。"
+ },
+ {
+ "step": 17,
+ "description": "盖上锅盖,中火焖 3 分钟。"
+ },
+ {
+ "step": 18,
+ "description": "打开锅盖,将所有的蒜、味精均匀撒入。"
+ },
+ {
+ "step": 19,
+ "description": "使用筷子不断翻炒,将菜与肉均匀搅拌。"
+ },
+ {
+ "step": 20,
+ "description": "关火"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-staple-酱拌荞麦面-酱拌荞麦面",
+ "name": "酱拌荞麦面的做法",
+ "description": "# 酱拌荞麦面的做法\n\n酱拌荞麦面营养健康、酸甜可口\n\n预估烹饪难度:★★",
+ "source_path": "dishes/staple/酱拌荞麦面/酱拌荞麦面.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/酱拌荞麦面/1.jpeg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/酱拌荞麦面/1.jpeg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/酱拌荞麦面/2.jpeg"
+ ],
+ "category": "主食",
+ "difficulty": 2,
+ "tags": [
+ "主食"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "荞麦面",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 荞麦面",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黄瓜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黄瓜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "红萝卜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 红萝卜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老干妈",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老干妈",
+ "notes": "量未指定"
+ },
+ {
+ "name": "荞麦面",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 荞麦面 100 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黄瓜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黄瓜 0.5 根",
+ "notes": "量未指定"
+ },
+ {
+ "name": "红萝卜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 红萝卜 0.5 根",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老干妈",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老干妈 20 ml",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "荞麦面下冷水煮熟,8-10 分钟 后捞出沥干备用"
+ },
+ {
+ "step": 2,
+ "description": "黄瓜、萝卜 切成小条"
+ },
+ {
+ "step": 3,
+ "description": "将荞麦面、黄瓜、萝卜放入盘子,放上老干妈,搅拌"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-staple-韩式拌饭-韩式拌饭",
+ "name": "韩式拌饭的做法",
+ "description": "# 韩式拌饭的做法\n\n\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/staple/韩式拌饭/韩式拌饭.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/韩式拌饭/韩式拌饭.png",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/韩式拌饭/韩式拌饭.png"
+ ],
+ "category": "主食",
+ "difficulty": 3,
+ "tags": [
+ "主食"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "米饭",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 米饭",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "火锅牛肉卷",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 火锅牛肉卷",
+ "notes": "量未指定"
+ },
+ {
+ "name": "豆芽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 豆芽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蘑菇",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蘑菇",
+ "notes": "量未指定"
+ },
+ {
+ "name": "胡萝卜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 胡萝卜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "西葫芦",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 西葫芦",
+ "notes": "量未指定"
+ },
+ {
+ "name": "韩式辣酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 韩式辣酱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "雪碧",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 雪碧",
+ "notes": "量未指定"
+ },
+ {
+ "name": "芝麻",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 芝麻",
+ "notes": "量未指定"
+ },
+ {
+ "name": "芝麻油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 芝麻油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "米饭",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 米饭 1 碗 (400g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋 1 颗",
+ "notes": "量未指定"
+ },
+ {
+ "name": "火锅牛肉卷",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 火锅牛肉卷 6 卷 60g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "豆芽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 豆芽 1 把 80g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蘑菇",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蘑菇 50g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "胡萝卜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 胡萝卜 1/4 根",
+ "notes": "量未指定"
+ },
+ {
+ "name": "西葫芦",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 西葫芦 50g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "韩式辣酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 韩式辣酱 25ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "雪碧",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 雪碧 2 瓶盖, 20ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "芝麻",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 芝麻 10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "芝麻油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 芝麻油 20ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 15ml",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "蔬菜清洗 切丝 放锅中翻炒 食材变软 便可称出"
+ },
+ {
+ "step": 2,
+ "description": "煮水 等沸腾时 焯牛肉卷 只需煮熟 大概三分钟即可捞出"
+ },
+ {
+ "step": 3,
+ "description": "煎[溏心蛋](../../breakfast/溏心蛋.md)"
+ },
+ {
+ "step": 4,
+ "description": "将[米饭](../../staple/米饭/电饭煲蒸米饭.md)放在一个碗里 然后倒扣在大碗"
+ },
+ {
+ "step": 5,
+ "description": "将准备好的蔬菜和肉卷依次绕圈放在米饭上面 将煎蛋放中间"
+ },
+ {
+ "step": 6,
+ "description": "备酱汁"
+ },
+ {
+ "step": 7,
+ "description": "将备好的酱汁倒在摆好盘的碗中"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-staple-鲣鱼海苔玉米饭-鲣鱼海苔玉米饭",
+ "name": "鲣鱼海苔玉米饭的做法",
+ "description": "# 鲣鱼海苔玉米饭的做法\n\n\n\n空气炸锅羊排超级懒人版,味道尚可,主要看羊排的品质。\n\n- 烹饪总时长:40 分钟(准备 3 分钟+煮饭 40 分钟+拌饭 2 分钟)\n- 实际操作时间:5 分钟\n\n预估烹饪难度:★★",
+ "source_path": "dishes/staple/鲣鱼海苔玉米饭/鲣鱼海苔玉米饭.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/鲣鱼海苔玉米饭/米饭.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/staple/鲣鱼海苔玉米饭/米饭.jpg"
+ ],
+ "category": "主食",
+ "difficulty": 2,
+ "tags": [
+ "主食"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "必备:鲣鱼海苔碎(JD 和淘宝都有,可以搜索:日式拌饭料)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 必备:鲣鱼海苔碎(JD 和淘宝都有,可以搜索:日式拌饭料)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "必备:玉米粒(淘宝搜索:玉米粒 即食)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 必备:玉米粒(淘宝搜索:玉米粒 即食)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鲣鱼海苔碎",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鲣鱼海苔碎 20g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "玉米粒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 玉米粒 80g/袋",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "盛好米饭,放入玉米粒拌好"
+ },
+ {
+ "step": 2,
+ "description": "放入鲣鱼海苔碎"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-vegetable_dish-凉拌油麦菜",
+ "name": "凉拌油麦菜的做法",
+ "description": "# 凉拌油麦菜的做法\n\n预估烹饪难度:★",
+ "source_path": "dishes/vegetable_dish/凉拌油麦菜.md",
+ "image_path": null,
+ "images": [],
+ "category": "素菜",
+ "difficulty": 1,
+ "tags": [
+ "素菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "油麦菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 油麦菜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "芝麻酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 芝麻酱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酱油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酱油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "醋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 醋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蚝油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蚝油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "1 颗 油麦菜(约",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 1 颗 油麦菜(约 200g) * 份数",
+ "notes": "量未指定"
+ },
+ {
+ "name": "15ml 醋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 15ml 醋 * 份数",
+ "notes": "量未指定"
+ },
+ {
+ "name": "5ml 酱油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 5ml 酱油 * 份数",
+ "notes": "量未指定"
+ },
+ {
+ "name": "10ml 芝麻酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 10ml 芝麻酱 * 份数",
+ "notes": "量未指定"
+ },
+ {
+ "name": "5ml 香油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 5ml 香油 * 份数",
+ "notes": "量未指定"
+ },
+ {
+ "name": "5g 糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 5g 糖 * 份数",
+ "notes": "量未指定"
+ },
+ {
+ "name": "10ml 蚝油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 10ml 蚝油 * 份数",
+ "notes": "量未指定"
+ },
+ {
+ "name": "两**头**蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 两**头**蒜 * 份数",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "蒜拍碎切末"
+ },
+ {
+ "step": 2,
+ "description": "醋,酱油,芝麻酱,香油,糖,蚝油,蒜末放到碗里搅拌均匀"
+ },
+ {
+ "step": 3,
+ "description": "油麦菜切段,每段不超过 4cm"
+ },
+ {
+ "step": 4,
+ "description": "油麦菜放到一个大点的盆里,倒入上述碗中酱料,充分搅拌均匀."
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-vegetable_dish-凉拌豆腐",
+ "name": "凉拌豆腐的做法",
+ "description": "# 凉拌豆腐的做法\n\n凉拌豆腐是一道清爽可口的家常凉菜。富含植物蛋白和钙质,低脂健康,非常适合夏季食用或作为日常佐餐。制作过程简单快捷,一般初学者只需要 10 分钟即可完成。\n\n预估烹饪难度:★★",
+ "source_path": "dishes/vegetable_dish/凉拌豆腐.md",
+ "image_path": null,
+ "images": [],
+ "category": "素菜",
+ "difficulty": 2,
+ "tags": [
+ "素菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "豆腐 (推荐选用北豆腐或老豆腐)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 豆腐 (推荐选用北豆腐或老豆腐)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "醋(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 醋(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "辣椒油(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 辣椒油(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "豆腐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 豆腐 250 g (约 1 块常见大小的豆腐)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小葱 10 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大蒜 2-3 瓣",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 15 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香油 5 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "醋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 醋 5 ml(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖 2 g(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "辣椒油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 辣椒油 5 ml(可选)",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "将 豆腐 切成 2 cm 见方的小块,备用。"
+ },
+ {
+ "step": 2,
+ "description": "锅中加入 500 ml 饮用水,大火烧开。"
+ },
+ {
+ "step": 3,
+ "description": "放入 豆腐 块,煮 **1-2 分钟**,以去除豆腥味并使豆腐口感更紧实。"
+ },
+ {
+ "step": 4,
+ "description": "将 煮好的 豆腐 块捞出,沥干水分,放入碗中,备用。"
+ },
+ {
+ "step": 5,
+ "description": "将 小葱 洗净,切成葱花,备用。"
+ },
+ {
+ "step": 6,
+ "description": "将 大蒜 去皮,切成蒜末,备用。"
+ },
+ {
+ "step": 7,
+ "description": "在一个干净的小碗中,加入 15 ml 生抽,5 ml 香油,5 ml 醋(可选),2 g 白糖(可选)。"
+ },
+ {
+ "step": 8,
+ "description": "加入切好的 大蒜末。"
+ },
+ {
+ "step": 9,
+ "description": "搅拌均匀,使 白糖 充分溶解,酱汁混合均匀。"
+ },
+ {
+ "step": 10,
+ "description": "将制作好的酱汁均匀淋在 豆腐 块上。"
+ },
+ {
+ "step": 11,
+ "description": "撒上切好的 小葱花。"
+ },
+ {
+ "step": 12,
+ "description": "根据个人喜好,淋上 5 ml 辣椒油(可选)。"
+ },
+ {
+ "step": 13,
+ "description": "用 筷子 或勺子轻轻拌匀,即可食用。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-vegetable_dish-凉拌金针菇",
+ "name": "凉拌金针菇的做法",
+ "description": "# 凉拌金针菇的做法\n\n凉拌金针菇是一道简单快捷的开胃凉菜。口感脆嫩爽滑,富含膳食纤维和多种维生素。制作过程无需复杂的烹饪技巧,非常适合新手和忙碌时快速准备。一般初学者只需要 10 分钟即可完成。\n\n预估烹饪难度:★★",
+ "source_path": "dishes/vegetable_dish/凉拌金针菇.md",
+ "image_path": null,
+ "images": [],
+ "category": "素菜",
+ "difficulty": 2,
+ "tags": [
+ "素菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "金针菇",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 金针菇",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "醋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 醋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香油(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香油(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "辣椒油(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 辣椒油(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "金针菇",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 金针菇 150 g (约 1 小包)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小葱 5 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大蒜 2 瓣",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 15 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "醋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 醋 10 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖 3 g(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香油 5 ml(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "辣椒油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 辣椒油 5 ml(可选)",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "将 金针菇 根部切除,用清水冲洗干净,备用。"
+ },
+ {
+ "step": 2,
+ "description": "将 小葱 洗净,切成葱花,备用。"
+ },
+ {
+ "step": 3,
+ "description": "将 大蒜 去皮,切成蒜末,备用。"
+ },
+ {
+ "step": 4,
+ "description": "锅中加入 1000 ml 饮用水,大火烧开。"
+ },
+ {
+ "step": 5,
+ "description": "放入 金针菇,煮 **1-2 分钟**,至金针菇变软。"
+ },
+ {
+ "step": 6,
+ "description": "将 煮好的 金针菇 捞出,沥干水分,放入一个较大的碗中,备用。"
+ },
+ {
+ "step": 7,
+ "description": "在另一个干净的小碗中,加入 15 ml 生抽,10 ml 醋,3 g 白糖(可选),5 ml 香油(可选)。"
+ },
+ {
+ "step": 8,
+ "description": "加入切好的 大蒜末。"
+ },
+ {
+ "step": 9,
+ "description": "搅拌均匀,使 白糖 充分溶解,酱汁混合均匀。"
+ },
+ {
+ "step": 10,
+ "description": "将制作好的酱汁均匀淋在 金针菇 上。"
+ },
+ {
+ "step": 11,
+ "description": "撒上切好的 小葱花。"
+ },
+ {
+ "step": 12,
+ "description": "根据个人喜好,淋上 5 ml 辣椒油(可选)。"
+ },
+ {
+ "step": 13,
+ "description": "用 筷子 轻轻拌匀,即可食用。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-vegetable_dish-凉拌黄瓜",
+ "name": "凉拌黄瓜的做法",
+ "description": "# 凉拌黄瓜的做法\n\n预估烹饪难度:★",
+ "source_path": "dishes/vegetable_dish/凉拌黄瓜.md",
+ "image_path": null,
+ "images": [],
+ "category": "素菜",
+ "difficulty": 1,
+ "tags": [
+ "素菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "黄瓜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黄瓜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "醋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 醋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酱油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酱油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黄瓜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黄瓜 200 克 * 份数",
+ "notes": "量未指定"
+ },
+ {
+ "name": "醋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 醋 7.5 ml + 4 ml * 份数",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酱油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酱油 5 ml + 2.5 ml * 份数",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜 3 瓣 * 份数",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 0.4 克 + 0.2 克 * 份数",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香油 5 ml + 2 ml * 份数",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蚝油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蚝油 5 ml",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "用菜刀将黄瓜拍扁,再剁成长 3 厘米的碎块"
+ },
+ {
+ "step": 2,
+ "description": "将碎黄瓜装入碗中"
+ },
+ {
+ "step": 3,
+ "description": "将蒜拍碎切成碎末"
+ },
+ {
+ "step": 4,
+ "description": "将醋,酱油,盐,蚝油和蒜依次倒入碗中搅拌均匀并腌制 15 分钟"
+ },
+ {
+ "step": 5,
+ "description": "将香油倒入碗中并均匀搅拌"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-vegetable_dish-地三鲜",
+ "name": "地三鲜的做法",
+ "description": "# 地三鲜的做法\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/vegetable_dish/地三鲜.md",
+ "image_path": null,
+ "images": [],
+ "category": "素菜",
+ "difficulty": 3,
+ "tags": [
+ "素菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "茄子",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 茄子",
+ "notes": "量未指定"
+ },
+ {
+ "name": "土豆",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 土豆",
+ "notes": "量未指定"
+ },
+ {
+ "name": "尖椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 尖椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "豆瓣酱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 豆瓣酱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "淀粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 淀粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "茄子",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 茄子 100g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "土豆",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 土豆 150g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "尖椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 尖椒 3 - 4 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "淀粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 淀粉 20g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "土豆洗净、去皮。茄子、尖椒洗净。"
+ },
+ {
+ "step": 2,
+ "description": "葱 3g 切 0.5cm 小段。蒜 10g 剁碎。姜 10g 切沫。"
+ },
+ {
+ "step": 3,
+ "description": "茄子、土豆、尖椒均切成 15g 的小块。"
+ },
+ {
+ "step": 4,
+ "description": "热锅,加入 25ml 油。"
+ },
+ {
+ "step": 5,
+ "description": "加入土豆,煎炸大约 3 分钟,待其到大约 8 分熟,以显示金黄色为准。"
+ },
+ {
+ "step": 6,
+ "description": "捞出土豆,留下油。"
+ },
+ {
+ "step": 7,
+ "description": "加入茄子,煎炸大约 40 秒,待其到大约 7 分熟,以显示金黄色为准。"
+ },
+ {
+ "step": 8,
+ "description": "如果锅内已经没有流动的油,可以考虑补充 15ml 油。"
+ },
+ {
+ "step": 9,
+ "description": "放入葱 3g。姜 10g。"
+ },
+ {
+ "step": 10,
+ "description": "放入豆瓣酱 20ml。"
+ },
+ {
+ "step": 11,
+ "description": "放入生抽 10ml。"
+ },
+ {
+ "step": 12,
+ "description": "放入盐 8g。"
+ },
+ {
+ "step": 13,
+ "description": "放入糖 10g。"
+ },
+ {
+ "step": 14,
+ "description": "放入之前处理的土豆。"
+ },
+ {
+ "step": 15,
+ "description": "放入尖椒。"
+ },
+ {
+ "step": 16,
+ "description": "翻炒 1 分钟。"
+ },
+ {
+ "step": 17,
+ "description": "放入蒜 10g"
+ },
+ {
+ "step": 18,
+ "description": "加入 200ml 水和 20g 淀粉。"
+ },
+ {
+ "step": 19,
+ "description": "待水开后,汤减少一半时,关火盛盘。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-vegetable_dish-松仁玉米",
+ "name": "松仁玉米的做法",
+ "description": "# 松仁玉米的做法\n\n松仁玉米是一道色香味俱全的家常菜,口感甜嫩清爽,松仁香脆,老少皆宜。\n\n预估烹饪难度:★★",
+ "source_path": "dishes/vegetable_dish/松仁玉米.md",
+ "image_path": null,
+ "images": [],
+ "category": "素菜",
+ "difficulty": 2,
+ "tags": [
+ "素菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "玉米粒(建议使用甜玉米)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 玉米粒(建议使用甜玉米)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "熟松子仁",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 熟松子仁",
+ "notes": "量未指定"
+ },
+ {
+ "name": "胡萝卜(可选,增加色彩)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 胡萝卜(可选,增加色彩)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白砂糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白砂糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "淀粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 淀粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 水",
+ "notes": "量未指定"
+ },
+ {
+ "name": "玉米粒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 玉米粒 200 g(可用罐头甜玉米,也可自备煮熟)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "熟松子仁",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 熟松子仁 30 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "胡萝卜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 胡萝卜 50 g(切小丁,可省略)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 15 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白砂糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白砂糖 10 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 1 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "淀粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 淀粉 5 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 水 20 ml(用于调淀粉水)",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "玉米粒和胡萝卜丁提前焯水 1 分钟,捞出沥干备用"
+ },
+ {
+ "step": 2,
+ "description": "热锅凉油,放入胡萝卜丁略炒,再加入玉米粒翻炒"
+ },
+ {
+ "step": 3,
+ "description": "加入白砂糖和盐,炒匀"
+ },
+ {
+ "step": 4,
+ "description": "混合水与淀粉成水淀粉,倒入锅中快速翻炒使汤汁略稠"
+ },
+ {
+ "step": 5,
+ "description": "加入熟松仁翻炒均匀"
+ },
+ {
+ "step": 6,
+ "description": "出锅装盘"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-vegetable_dish-水油焖蔬菜",
+ "name": "水油焖蔬菜的做法",
+ "description": "# 水油焖蔬菜的做法\n\n水油焖蔬菜中添加了油,这提升了口感,并且可提升脂溶性维生素的摄入。相比生吃蔬菜,好处更多。\n\n预估烹饪难度:★★",
+ "source_path": "dishes/vegetable_dish/水油焖蔬菜.md",
+ "image_path": null,
+ "images": [],
+ "category": "素菜",
+ "difficulty": 2,
+ "tags": [
+ "素菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蚝油(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蚝油(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "叶菜类蔬菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 叶菜类蔬菜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "叶菜类蔬菜:300g ~",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 叶菜类蔬菜:300g ~ 500g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "洗净蔬菜"
+ },
+ {
+ "step": 2,
+ "description": "锅中加入 150ml 水,并烧开。(水不需要能完全没过蔬菜)"
+ },
+ {
+ "step": 3,
+ "description": "加入 3g 盐"
+ },
+ {
+ "step": 4,
+ "description": "(可选)加入 3ml 蚝油"
+ },
+ {
+ "step": 5,
+ "description": "加入 2ml 食用油"
+ },
+ {
+ "step": 6,
+ "description": "下菜, 翻拌一下,然后盖上锅盖焖 1 分钟"
+ },
+ {
+ "step": 7,
+ "description": "盛盘"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-vegetable_dish-油醋爆蛋",
+ "name": "油醋爆蛋的做法",
+ "description": "# 油醋爆蛋的做法\n\n油醋爆蛋是十分简单但是色香味一绝的一道菜,属于湘菜。制作十分简单,大约十分钟左右。\n\n预估烹饪难度:★★",
+ "source_path": "dishes/vegetable_dish/油醋爆蛋.md",
+ "image_path": null,
+ "images": [],
+ "category": "素菜",
+ "difficulty": 2,
+ "tags": [
+ "素菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小米辣",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小米辣",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香醋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香醋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蚝油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蚝油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "咖喱块",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 咖喱块 115g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "土豆",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 土豆 2 个(每个土豆大约重 120g,共约 240g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 10-15ml",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "鸡蛋不需打散,直接打入碗中备用"
+ },
+ {
+ "step": 2,
+ "description": "香葱切 3cm 长小段即可"
+ },
+ {
+ "step": 3,
+ "description": "蒜瓣和小米辣放入打蒜器,打成沫"
+ },
+ {
+ "step": 4,
+ "description": "将香醋、生抽、蚝油、白糖、水加入小碗,搅拌均匀作为糖醋料汁"
+ },
+ {
+ "step": 5,
+ "description": "油热倒入鸡蛋,等鸡蛋凝固之后铲成大块,倒入蒜沫、小米辣沫、倒入糖醋料汁"
+ },
+ {
+ "step": 6,
+ "description": "大火收汁、快出锅时加入葱段即可"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-vegetable_dish-清炒花菜",
+ "name": "清炒花菜的做法",
+ "description": "# 清炒花菜的做法\n\n清炒花菜是一道常见的家常素菜。富含维生素 C 和膳食纤维,口感脆嫩。做法简单,是一道快速上手的炒菜。一般初学者只需要 15 分钟即可完成。\n\n预估烹饪难度:★★",
+ "source_path": "dishes/vegetable_dish/清炒花菜.md",
+ "image_path": null,
+ "images": [],
+ "category": "素菜",
+ "difficulty": 2,
+ "tags": [
+ "素菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "花菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 花菜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "花菜 约",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 花菜 约 300 g (约 1/2 中等大小的花菜)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大蒜 2-3 瓣",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 3 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 15 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "饮用水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 饮用水 50 ml (用于炒制过程)",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "将 花菜 洗净,用刀或手掰成小朵,粗茎部分可以切片,备用。"
+ },
+ {
+ "step": 2,
+ "description": "将 大蒜 去皮,切成蒜片,备用。"
+ },
+ {
+ "step": 3,
+ "description": "锅中加入 1000 ml 饮用水,大火烧开。"
+ },
+ {
+ "step": 4,
+ "description": "放入 花菜 朵,煮 **2-3 分钟**,至花菜颜色变浅,口感稍微软化。"
+ },
+ {
+ "step": 5,
+ "description": "将 煮好的 花菜 捞出,沥干水分,备用。"
+ },
+ {
+ "step": 6,
+ "description": "热锅,加入 15 ml 食用油,大火烧热。"
+ },
+ {
+ "step": 7,
+ "description": "放入 蒜片,快速煸炒出香味。"
+ },
+ {
+ "step": 8,
+ "description": "放入 焯好水的 花菜 朵,转中大火,快速翻炒约 **2 分钟**,使花菜均匀受热。"
+ },
+ {
+ "step": 9,
+ "description": "加入 3 g 盐,继续翻炒均匀。"
+ },
+ {
+ "step": 10,
+ "description": "沿锅边淋入 50 ml 饮用水,盖上锅盖,焖 **1 分钟**,帮助花菜完全熟透入味。"
+ },
+ {
+ "step": 11,
+ "description": "开盖,快速翻炒均匀,即可出锅。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-vegetable_dish-清蒸南瓜",
+ "name": "清蒸南瓜的做法",
+ "description": "# 清蒸南瓜的做法\n\n清蒸南瓜是一道制作极其简单的家常甜点或主食。它最大程度地保留了南瓜本身的天然甜味和营养,口感软糯。是健康饮食的不错选择。一般初学者只需要 15-20 分钟即可完成(主要为蒸的时间)。\n\n预估烹饪难度:★",
+ "source_path": "dishes/vegetable_dish/清蒸南瓜.md",
+ "image_path": null,
+ "images": [],
+ "category": "素菜",
+ "difficulty": 1,
+ "tags": [
+ "素菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "南瓜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 南瓜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒸锅",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒸锅",
+ "notes": "量未指定"
+ },
+ {
+ "name": "南瓜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 南瓜 300 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "饮用水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 饮用水 1000 ml (用于蒸锅)",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "将 南瓜 外皮洗净,去除瓜瓤和籽。"
+ },
+ {
+ "step": 2,
+ "description": "将 南瓜 切成厚度大约 2 cm 的片,备用。"
+ },
+ {
+ "step": 3,
+ "description": "在 蒸锅 的锅中加入 1000 ml 饮用水。"
+ },
+ {
+ "step": 4,
+ "description": "将切好的 南瓜 片均匀摆放在盘中。"
+ },
+ {
+ "step": 5,
+ "description": "待蒸锅中的水烧开后,将装有 南瓜 的盘子放入蒸锅中。"
+ },
+ {
+ "step": 6,
+ "description": "盖上锅盖,保持大火蒸 **15-20 分钟**,直至南瓜变软,可以用筷子轻松穿透。"
+ },
+ {
+ "step": 7,
+ "description": "关火,小心取出盘子。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-vegetable_dish-炒茄子",
+ "name": "炒茄子的做法",
+ "description": "# 炒茄子的做法\n\n家常炒茄子,简单易学,原料不复杂,其中可选项有无皆可。(但是八角强烈推荐)\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/vegetable_dish/炒茄子.md",
+ "image_path": null,
+ "images": [],
+ "category": "素菜",
+ "difficulty": 3,
+ "tags": [
+ "素菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "茄子",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 茄子",
+ "notes": "量未指定"
+ },
+ {
+ "name": "八角(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 八角(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "虾皮(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 虾皮(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香葱(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香葱(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酱油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酱油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "菜籽油或花生油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 菜籽油或花生油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "茄子数量 = 份数",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 茄子数量 = 份数 * 1.8 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "八角 = 份数",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 八角 = 份数 * 1 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "虾皮 = 份数",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 虾皮 = 份数 * 正常男子手抓半把",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香葱 = 份数",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香葱 = 份数 * 2 颗",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酱油 = 份数",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酱油 = 份数 * 40 ml",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "将茄子洗净,一刀分为两段(竖切)。每段的茄子切菱形块,将切好的茄子放入碗中待命。"
+ },
+ {
+ "step": 2,
+ "description": "将香葱洗净,并切成葱花放到案板上待命。"
+ },
+ {
+ "step": 3,
+ "description": "切好八角,放到案板上待命。"
+ },
+ {
+ "step": 4,
+ "description": "开火热锅,直至锅内没有水。"
+ },
+ {
+ "step": 5,
+ "description": "往锅内倒食用油,没过锅底的两倍(油可以多加,但不可少加)。"
+ },
+ {
+ "step": 6,
+ "description": "热油约 6 成熟,放入八角、虾皮、香葱这三种可选性材料。"
+ },
+ {
+ "step": 7,
+ "description": "如果没有八角等可选材料,热油至 9 成熟。"
+ },
+ {
+ "step": 8,
+ "description": "待锅内的油到 9 成熟,将碗中的茄子倒入锅内用锅铲进行翻炒。"
+ },
+ {
+ "step": 9,
+ "description": "翻炒约 40 秒,将锅铲悬空,与锅平行,把酱油倒入锅铲内。一人约 2.5 锅铲(酱油可以少加,但不可多加,会咸)"
+ },
+ {
+ "step": 10,
+ "description": "继续进行翻炒。"
+ },
+ {
+ "step": 11,
+ "description": "等到锅内所有茄子变色且变软时捞出。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-vegetable_dish-炒青菜",
+ "name": "炒青菜的做法",
+ "description": "# 炒青菜的做法\n\n制作简单方便。预计 10 分钟即可完成。\n\n预估烹饪难度:★★",
+ "source_path": "dishes/vegetable_dish/炒青菜.md",
+ "image_path": null,
+ "images": [],
+ "category": "素菜",
+ "difficulty": 2,
+ "tags": [
+ "素菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "青菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 青菜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "青菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 青菜 100g * 份数",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 10-15ml(覆盖锅底即可)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食盐 2g * 份数",
+ "notes": "量未指定"
+ },
+ {
+ "name": "饮用水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 饮用水 70ml * 份数",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖 5g * 份数",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "青菜掰成小瓣,用清水洗净,备用。"
+ },
+ {
+ "step": 2,
+ "description": "中火或大火热锅后,锅内放入 10-15ml 食用油。再等待 30 秒让油温升高。"
+ },
+ {
+ "step": 3,
+ "description": "将准备好的青菜倒入锅中,翻炒至青菜变软(约 1 分钟)。"
+ },
+ {
+ "step": 4,
+ "description": "倒入计算好的清水,水位应当完全浸润或即将没过青菜,加入食盐 (2g * 份数),继续翻炒约 1 分钟。"
+ },
+ {
+ "step": 5,
+ "description": "最后加入白糖小火加热 2 分钟,加热时盖上锅盖。"
+ },
+ {
+ "step": 6,
+ "description": "盛盘。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-vegetable_dish-素炒豆角",
+ "name": "素炒豆角的做法",
+ "description": "# 素炒豆角的做法\n\n巨下饭的家常菜\n\n预估烹饪难度:★★",
+ "source_path": "dishes/vegetable_dish/素炒豆角.md",
+ "image_path": null,
+ "images": [],
+ "category": "素菜",
+ "difficulty": 2,
+ "tags": [
+ "素菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "豆角",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 豆角",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小米椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小米椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "耗油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 耗油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "豆角",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 豆角 250g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小米椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小米椒 2 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱 3 圈",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜 2 颗",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 6ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽 2ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "耗油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 耗油 6ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 6g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "葱切花,蒜切沫,备用。"
+ },
+ {
+ "step": 2,
+ "description": "生抽、老抽、耗油、盐混合调料汁,备用。"
+ },
+ {
+ "step": 3,
+ "description": "小米椒切圈,备用。"
+ },
+ {
+ "step": 4,
+ "description": "豆角去筋,45° 斜切*4-10cm*小段,备用。"
+ },
+ {
+ "step": 5,
+ "description": "起锅烧油(10ml - 15ml),冒烟后放入葱、小米椒,翻炒至闻到香味;"
+ },
+ {
+ "step": 6,
+ "description": "加入豆角,翻炒*30s*,"
+ },
+ {
+ "step": 7,
+ "description": "加入料汁,开大火翻炒*2分钟*"
+ },
+ {
+ "step": 8,
+ "description": "倒入 150ml 水"
+ },
+ {
+ "step": 9,
+ "description": "转中小火,盖上锅盖焖制 8-10 分钟"
+ },
+ {
+ "step": 10,
+ "description": "加入蒜切沫,出锅。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-vegetable_dish-红烧茄子",
+ "name": "红烧茄子的做法",
+ "description": "# 红烧茄子的做法\n\n预估烹饪难度:★★★★",
+ "source_path": "dishes/vegetable_dish/红烧茄子.md",
+ "image_path": null,
+ "images": [],
+ "category": "素菜",
+ "difficulty": 4,
+ "tags": [
+ "素菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "大蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "青辣椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 青辣椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "洋葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 洋葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "西红柿",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 西红柿",
+ "notes": "量未指定"
+ },
+ {
+ "name": "青茄子",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 青茄子",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酱油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酱油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "面粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 面粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "淀粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 淀粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "青茄子的数量 = 份数",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 青茄子的数量 = 份数 * 0.7 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "青辣椒 = 份数",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 青辣椒 = 份数 * 0.5 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "洋葱 = 份数",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 洋葱 = 份数 * 0.3 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "西红柿 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 西红柿 = 1 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大葱 = 半颗",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大葱 = 半颗",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大蒜 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大蒜 = 3 瓣",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋 = 1 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "面粉 = 青茄子数量",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 面粉 = 青茄子数量 * 150 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "淀粉 = 面粉 /",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 淀粉 = 面粉 / 4 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酱油 = 茄子数量",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酱油 = 茄子数量 * 7 克(向上取整)",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-vegetable_dish-脆皮豆腐",
+ "name": "脆皮豆腐的做法",
+ "description": "# 脆皮豆腐的做法\n\n浓郁的酱汁裹满豆腐,吃一口就停不下来,别提有多好吃。\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/vegetable_dish/脆皮豆腐.md",
+ "image_path": null,
+ "images": [],
+ "category": "素菜",
+ "difficulty": 3,
+ "tags": [
+ "素菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "老豆腐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老豆腐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蚝油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蚝油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "玉米淀粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 玉米淀粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "平底锅",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 平底锅",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老豆腐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老豆腐 1 块 (市场买 1.25 格 * 人)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋 2 只 * 老豆腐块数",
+ "notes": "量未指定"
+ },
+ {
+ "name": "玉米淀粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 玉米淀粉 50 g * 老豆腐块数",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 20 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蚝油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蚝油 10 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽 5 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖 10 g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "鸡蛋搅拌形成蛋液放置备用"
+ },
+ {
+ "step": 2,
+ "description": "配置酱料 (20 g 生抽+10 g 蚝油+5 g 老抽+10 g 白糖+10 g 玉米淀粉+200 ml 清水)"
+ },
+ {
+ "step": 3,
+ "description": "老豆腐切片 (个人建议,仅供参考 人 * 5 片,厚度 1.2 cm)"
+ },
+ {
+ "step": 4,
+ "description": "玉米淀粉倒入盘中,将老豆腐片粘上淀粉后,粘上蛋液,放置一旁"
+ },
+ {
+ "step": 5,
+ "description": "热锅,锅内放入 18ml 食用油。等待 10 秒让油温升高"
+ },
+ {
+ "step": 6,
+ "description": "将粘上蛋液的老豆腐片均匀放入锅中,铺好锅底,小火煎至金黄翻面"
+ },
+ {
+ "step": 7,
+ "description": "待两面煎至金黄后,倒入酱料,让每块豆腐都沐浴在酱料中,大火 3 分钟至酱汁浓稠"
+ },
+ {
+ "step": 8,
+ "description": "关火"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-vegetable_dish-茄子炖土豆",
+ "name": "茄子炖土豆的做法",
+ "description": "# 茄子炖土豆的做法\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/vegetable_dish/茄子炖土豆.md",
+ "image_path": null,
+ "images": [],
+ "category": "素菜",
+ "difficulty": 3,
+ "tags": [
+ "素菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "茄子",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 茄子",
+ "notes": "量未指定"
+ },
+ {
+ "name": "土豆",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 土豆",
+ "notes": "量未指定"
+ },
+ {
+ "name": "肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 肉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "辣椒(是青辣椒,而**不是辣椒面或辣椒油**)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 辣椒(是青辣椒,而**不是辣椒面或辣椒油**)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酱油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酱油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "茄子的数量 = 份数",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 茄子的数量 = 份数 * 1 个 (每个茄子约 150g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "土豆数量 = 份数",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 土豆数量 = 份数 * 1 个(每个土豆约 150g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "肉量 = 份数",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 肉量 = 份数 * 180 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酱油量 = 份数",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酱油量 = 份数 * 15 毫升",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐量 = 份数",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐量 = 份数 * 5 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "辣椒量 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 辣椒量 = 50 克(调味,所以无论多少人都放这些。)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜量 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜量 = 3 瓣(调味,所以无论多少人都放这些。注意是里面的小瓣 3 瓣,而**不是3整头**)",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-vegetable_dish-葱煎豆腐",
+ "name": "葱煎豆腐的做法",
+ "description": "# 葱煎豆腐的做法\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/vegetable_dish/葱煎豆腐.md",
+ "image_path": null,
+ "images": [],
+ "category": "素菜",
+ "difficulty": 3,
+ "tags": [
+ "素菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "白豆腐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白豆腐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "青辣椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 青辣椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡精",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡精",
+ "notes": "量未指定"
+ },
+ {
+ "name": "平底锅",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 平底锅",
+ "notes": "量未指定"
+ },
+ {
+ "name": "辣椒的数量 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 辣椒的数量 = 1.5 只/三人。",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱的数量 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱的数量 = 2 根/三人。",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐量 = 份数",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐量 = 份数 * 3g。",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡精量 = 份数",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡精量 = 份数 * 1.5g。",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "豆腐洗净。切约 5 mm 厚度,置于碟中。"
+ },
+ {
+ "step": 2,
+ "description": "葱洗净,除去根部,切成葱花,备用。"
+ },
+ {
+ "step": 3,
+ "description": "辣椒洗净,切开,去籽,切成 1cm * 1cm 状,备用、"
+ },
+ {
+ "step": 4,
+ "description": "热锅,加入份数 * 9ml 油。"
+ },
+ {
+ "step": 5,
+ "description": "油入锅后,使其均匀布于锅底,均匀放入豆腐,小火煎至金黄翻面。"
+ },
+ {
+ "step": 6,
+ "description": "待两面金黄,盛入碟中备用。"
+ },
+ {
+ "step": 7,
+ "description": "补油至覆盖锅底,倒入辣椒大火翻炒,并铲碾 3 分钟。"
+ },
+ {
+ "step": 8,
+ "description": "倒入豆腐,翻炒,加入盐与鸡精,中火翻炒 1 分钟后倒入 10 ML 水,大火收汁。"
+ },
+ {
+ "step": 9,
+ "description": "出锅前撒上之前计算好的葱花,起锅盛盘。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-vegetable_dish-蒜蓉西兰花",
+ "name": "蒜蓉西兰花的做法",
+ "description": "# 蒜蓉西兰花的做法\n\n预估烹饪难度:★★",
+ "source_path": "dishes/vegetable_dish/蒜蓉西兰花.md",
+ "image_path": null,
+ "images": [],
+ "category": "素菜",
+ "difficulty": 2,
+ "tags": [
+ "素菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "西兰花",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 西兰花 1 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大蒜 3-4 瓣",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蚝油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蚝油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "西兰花 约",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 西兰花 约 200 g (约 1/2 中等大小的西兰花)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大蒜 3-4 瓣",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 10 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蚝油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蚝油 5 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖 2 g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "将 西兰花 切成小朵,清洗干净。"
+ },
+ {
+ "step": 2,
+ "description": "将 大蒜 去皮,切成蒜末,备用。"
+ },
+ {
+ "step": 3,
+ "description": "锅中加入 1000 ml 饮用水,大火烧开。"
+ },
+ {
+ "step": 4,
+ "description": "放入 西兰花,保持大火 **煮 2-3 分钟**,至 西兰花 颜色变翠绿,口感变软。"
+ },
+ {
+ "step": 5,
+ "description": "将 煮好的 西兰花 捞出,沥干水分,摆入盘中,备用。"
+ },
+ {
+ "step": 6,
+ "description": "热锅,加入 10 ml 食用油。油温升高后,放入 大蒜末,小火煸炒出香味。"
+ },
+ {
+ "step": 7,
+ "description": "加入 10 ml 生抽,5 ml 蚝油,2 g 白糖,加入 30 ml 饮用水。"
+ },
+ {
+ "step": 8,
+ "description": "将锅中汤汁烧开。"
+ },
+ {
+ "step": 9,
+ "description": "将烧好的蒜蓉汁 均匀淋在盘中的 西兰花 上。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-vegetable_dish-蒲烧茄子",
+ "name": "蒲烧茄子的做法",
+ "description": "# 蒲烧茄子的做法\n\n众所皆知,茄子🍆和土豆🥔是两种荤菜。这一道蒲烧茄子,从外观上之于鳗鱼正如`土豆炖.*`中的生姜之于土豆。\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/vegetable_dish/蒲烧茄子.md",
+ "image_path": null,
+ "images": [],
+ "category": "素菜",
+ "difficulty": 3,
+ "tags": [
+ "素菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "茄子",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 茄子",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒲烧汁",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒲烧汁",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蜂蜜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蜂蜜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 水",
+ "notes": "量未指定"
+ },
+ {
+ "name": "根据锅的类型决策不同量的油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 根据锅的类型决策不同量的油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "不粘锅:油汇聚成滴后要散布在茄子的面积",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 不粘锅:油汇聚成滴后要散布在茄子的面积",
+ "notes": "量未指定"
+ },
+ {
+ "name": "铁锅:摊开后油可以刚好覆盖锅底",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 铁锅:摊开后油可以刚好覆盖锅底",
+ "notes": "量未指定"
+ },
+ {
+ "name": "1 个长的上小下大的茄子(注意不要使用浙茄和圆茄)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 1 个长的上小下大的茄子(注意不要使用浙茄和圆茄)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "1 份蒲烧汁",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 1 份蒲烧汁",
+ "notes": "量未指定"
+ },
+ {
+ "name": "20 ml 蜂蜜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 20 ml 蜂蜜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "15 ml 白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 15 ml 白糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "40 ml 生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 40 ml 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "10 ml 老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 10 ml 老抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "20 ml 料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 20 ml 料酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "100 ml 水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 100 ml 水",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "茄子削皮、横着切成两段"
+ },
+ {
+ "step": 2,
+ "description": "蒸 5 分钟"
+ },
+ {
+ "step": 3,
+ "description": "纵向切开,不要切断,在两边切面各划 2~3 刀至可以摊平"
+ },
+ {
+ "step": 4,
+ "description": "煎至两面金黄"
+ },
+ {
+ "step": 5,
+ "description": "往茄子上浇蒲烧汁至没过 1/2 茄子高度"
+ },
+ {
+ "step": 6,
+ "description": "煎至背面上色,翻面"
+ },
+ {
+ "step": 7,
+ "description": "把剩下的蒲烧汁浇在茄子上"
+ },
+ {
+ "step": 8,
+ "description": "出锅,一份茄子烧蒲烧汁就烧好了"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-vegetable_dish-蚝油生菜",
+ "name": "蚝油生菜的做法",
+ "description": "# 蚝油生菜的做法\n\n预估烹饪难度:★★",
+ "source_path": "dishes/vegetable_dish/蚝油生菜.md",
+ "image_path": null,
+ "images": [],
+ "category": "素菜",
+ "difficulty": 2,
+ "tags": [
+ "素菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "生菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生菜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蚝油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蚝油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生菜 1 棵( 200 g ± 50 )",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蚝油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蚝油 6-8 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大蒜 4-5 瓣(做成蒜泥或切碎)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 6 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 0.5 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖 1 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 5-8 ml",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "生菜洗净并去掉烂菜叶。"
+ },
+ {
+ "step": 2,
+ "description": "热锅,先放 1 L 清水(凉),然后在锅内放入 2 ml - 3 ml 食用油和 0.5 g 盐,等待锅中的水煮沸。"
+ },
+ {
+ "step": 3,
+ "description": "水沸后,放入生菜,将**每一片**生菜叶都焯水 10 s。"
+ },
+ {
+ "step": 4,
+ "description": "捞出生菜,控干水份,摆盘 。"
+ },
+ {
+ "step": 5,
+ "description": "调汁:将生抽 10 ml 、蚝油 6-8 ml 、盐 0.5 g 、 白糖 1 g 放入碗中调匀,并加入 10-15 ml 清水(凉)搅拌均匀。"
+ },
+ {
+ "step": 6,
+ "description": "再开火,热锅,放入食用油 5-8 ml,油热放入蒜泥。"
+ },
+ {
+ "step": 7,
+ "description": "等待有蒜香飘出,倒入调好的汁,煮沸即可,立马关火。"
+ },
+ {
+ "step": 8,
+ "description": "将锅中的汤汁均匀地**浇**在生菜上。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-vegetable_dish-西红柿炒鸡蛋",
+ "name": "西红柿炒鸡蛋的做法",
+ "description": "# 西红柿炒鸡蛋的做法\n\n西红柿炒蛋是中国家常几乎最常见的一道菜肴。它的原材料易于搜集,制作步骤也较为简单,所以非常适合新厨师上手,是很多人学习做菜时做的第一道菜。\n\n预估烹饪难度:★★",
+ "source_path": "dishes/vegetable_dish/西红柿炒鸡蛋.md",
+ "image_path": null,
+ "images": [],
+ "category": "素菜",
+ "difficulty": 2,
+ "tags": [
+ "素菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "西红柿",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 西红柿",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "糖(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 糖(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱花(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱花(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "西红柿 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 西红柿 = 1 个(约 180g) * 份数",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋 = 1.5 个 * 份数,向上取整",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 = 4ml * 鸡蛋/个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 = 1.5-2g * 份数",
+ "notes": "量未指定"
+ },
+ {
+ "name": "糖 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 糖 = 0-2g * 份数",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱花 =",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱花 = 0-10g * 份数",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "西红柿洗净"
+ },
+ {
+ "step": 2,
+ "description": "可选:去掉西红柿的外表皮"
+ },
+ {
+ "step": 3,
+ "description": "西红柿去蒂,切成边长不超过 4cm 的小块,即为 `西红柿块`"
+ },
+ {
+ "step": 4,
+ "description": "将鸡蛋打入碗中,加入 `1g * 份数` 的盐,搅匀,即为 `鸡蛋液`"
+ },
+ {
+ "step": 5,
+ "description": "热锅,加入食用油"
+ },
+ {
+ "step": 6,
+ "description": "油热后,倒入 `鸡蛋液`。翻炒至鸡蛋结为固体且颜色微微发黄,即为 `半熟鸡蛋`"
+ },
+ {
+ "step": 7,
+ "description": "关火。将 `半熟鸡蛋` 盛盘,重新开火"
+ },
+ {
+ "step": 8,
+ "description": "加入 `西红柿块`,锅铲拍打并翻炒 20 秒,或至西红柿软烂"
+ },
+ {
+ "step": 9,
+ "description": "向锅中加入 `半熟鸡蛋`,翻炒均匀"
+ },
+ {
+ "step": 10,
+ "description": "加入剩余的盐、糖(可选,如果倾向于甜味版本)、葱花(可选),翻炒均匀"
+ },
+ {
+ "step": 11,
+ "description": "关火,盛盘"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-vegetable_dish-酸辣土豆丝",
+ "name": "酸辣土豆丝的做法",
+ "description": "# 酸辣土豆丝的做法\n\n酸辣土豆丝是一道简单易做的菜。色泽光亮,酸辣可口。辅料辣椒富含维生素 C。该菜用料简单,好学易做\n\n预估烹饪难度:★★",
+ "source_path": "dishes/vegetable_dish/酸辣土豆丝.md",
+ "image_path": null,
+ "images": [],
+ "category": "素菜",
+ "difficulty": 2,
+ "tags": [
+ "素菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "土豆",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 土豆",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "青椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 青椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "红椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 红椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "干辣椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 干辣椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "陈醋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 陈醋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "土豆",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 土豆 240g(越细越长更好)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大蒜 4 瓣",
+ "notes": "量未指定"
+ },
+ {
+ "name": "青椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 青椒 0.5 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "红椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 红椒 0.5 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "干辣椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 干辣椒 3 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱 1 根",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 5ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "陈醋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 陈醋 10ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 2g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 10-15ml",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "土豆去皮、切丝(或用刨丝器)。"
+ },
+ {
+ "step": 2,
+ "description": "切好的土豆丝用清水清洗,去除多余的淀粉,然后对土豆丝焯水 10 秒。沥干,备用。"
+ },
+ {
+ "step": 3,
+ "description": "葱蒜干辣椒切小块,青红椒切丝。"
+ },
+ {
+ "step": 4,
+ "description": "热锅,小火热油爆香蒜和干辣椒。"
+ },
+ {
+ "step": 5,
+ "description": "加入青红椒翻炒几下,加入土豆丝翻炒至变色。"
+ },
+ {
+ "step": 6,
+ "description": "加 5ml 生抽,10ml 陈醋,蒜末,最后加入盐翻炒均匀即可。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-vegetable_dish-金针菇日本豆腐煲",
+ "name": "金针菇日本豆腐煲的做法",
+ "description": "# 金针菇日本豆腐煲的做法\n\n金针菇日本豆腐煲是一道容易上手的日常料理。\n\n预估烹饪难度:★★",
+ "source_path": "dishes/vegetable_dish/金针菇日本豆腐煲.md",
+ "image_path": null,
+ "images": [],
+ "category": "素菜",
+ "difficulty": 2,
+ "tags": [
+ "素菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "金针菇",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 金针菇",
+ "notes": "量未指定"
+ },
+ {
+ "name": "日本豆腐(玉子豆腐)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 日本豆腐(玉子豆腐)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小米椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小米椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蚝油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蚝油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "金针菇",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 金针菇 1-2 把",
+ "notes": "量未指定"
+ },
+ {
+ "name": "豆腐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 豆腐 2 袋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小米椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小米椒 3-5 根,切碎",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜 2-3 瓣",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 15ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蚝油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蚝油 5ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽 3ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 糖 3g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 10-15ml",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "豆腐切片,小火煎到两面金黄出锅备用。"
+ },
+ {
+ "step": 2,
+ "description": "切蒜成蒜末;将生抽,蚝油,老抽,糖,100ml 水调汁备用。"
+ },
+ {
+ "step": 3,
+ "description": "热锅放油,油热放小米椒、蒜末爆香,先放金针菇,炒软,把煎好的豆腐平铺在金针菇上,倒入#2 配好的料汁,焖 5 分钟,大火收汁。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-vegetable_dish-陕北熬豆角",
+ "name": "陕北熬豆角的做法",
+ "description": "# 陕北熬豆角的做法\n\n陕北熬豆角是一种对初学者极其友善的菜,因其制作方式使用`熬`的方式,食材可多可少,可有可无,几乎不存在失败的可能性。\n\n预估烹饪难度:★★",
+ "source_path": "dishes/vegetable_dish/陕北熬豆角.md",
+ "image_path": null,
+ "images": [],
+ "category": "素菜",
+ "difficulty": 2,
+ "tags": [
+ "素菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "豆角",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 豆角",
+ "notes": "量未指定"
+ },
+ {
+ "name": "土豆",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 土豆",
+ "notes": "量未指定"
+ },
+ {
+ "name": "西红柿",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 西红柿",
+ "notes": "量未指定"
+ },
+ {
+ "name": "螺丝椒(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 螺丝椒(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "五香粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 五香粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蚝油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蚝油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "豆角",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 豆角 300g * 2 人",
+ "notes": "量未指定"
+ },
+ {
+ "name": "土豆",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 土豆 1 个 * 2 人",
+ "notes": "量未指定"
+ },
+ {
+ "name": "西红柿",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 西红柿 1 个 * 2 人",
+ "notes": "量未指定"
+ },
+ {
+ "name": "螺丝椒(可选)2 个",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 螺丝椒(可选)2 个 * 2 人",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 6g * 2 人",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 6ml * 2 人",
+ "notes": "量未指定"
+ },
+ {
+ "name": "五香粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 五香粉 3g * 2 人",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蚝油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蚝油 6ml * 2 人",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱 3 圈",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜 2g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜 2 颗",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香菜碎(可选)根据口味加",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香菜碎(可选)根据口味加",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "葱切花,蒜切沫,姜切丝,备用。"
+ },
+ {
+ "step": 2,
+ "description": "豆角去筋,切*2-10cm*小段,备用。"
+ },
+ {
+ "step": 3,
+ "description": "土豆去皮,切*1cm³*小块,备用。"
+ },
+ {
+ "step": 4,
+ "description": "西红柿去皮,切*1cm³*小块,备用。"
+ },
+ {
+ "step": 5,
+ "description": "辣椒去仔,切*0.15cm 宽*条,备用。"
+ },
+ {
+ "step": 6,
+ "description": "起锅烧油(10ml - 15ml),冒烟后放入葱姜蒜,翻炒至闻到葱姜蒜香味;"
+ },
+ {
+ "step": 7,
+ "description": "加入豆角,翻炒至变色(青绿色变为翠绿色);"
+ },
+ {
+ "step": 8,
+ "description": "加入土豆块,翻炒 30s;"
+ },
+ {
+ "step": 9,
+ "description": "加入热水(水面刚刚漫过菜),盖上锅盖熬至土豆*变软*(可以用筷子确认);"
+ },
+ {
+ "step": 10,
+ "description": "加入西红柿块,加入盐,生抽,蚝油,五香粉,辣椒,熬至西红柿成汁(注意搅拌,防止糊锅);"
+ },
+ {
+ "step": 11,
+ "description": "加入香菜碎,出锅。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-vegetable_dish-雷椒皮蛋",
+ "name": "雷椒皮蛋的做法",
+ "description": "# 雷椒皮蛋的做法\n\n雷椒皮蛋是一个非常简单的下饭凉菜,这道菜操作比较简单,且食材常见, 最终成品卖相不会很好看,但是是夏天下饭的神器之一\n\n预估烹饪难度:★★",
+ "source_path": "dishes/vegetable_dish/雷椒皮蛋.md",
+ "image_path": null,
+ "images": [],
+ "category": "素菜",
+ "difficulty": 2,
+ "tags": [
+ "素菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "皮蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 皮蛋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "长条青椒(有些叫线椒,后面介绍以“青椒”代替)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 长条青椒(有些叫线椒,后面介绍以“青椒”代替)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小米辣(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小米辣(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "陈醋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 陈醋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "深一点的小铁盆",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 深一点的小铁盆",
+ "notes": "量未指定"
+ },
+ {
+ "name": "皮蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 皮蛋 2 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "青椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 青椒 4 根 (长 10-15cm,宽 2-4cm)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱 (大约",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱 (大约 10cm 即可,葱绿最佳)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜 3-4 瓣",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 10-20ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 15-20ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "陈醋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 陈醋 15-20ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖 6-10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香油 5-7ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小米辣",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小米辣 3-4 颗",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "青椒清洗,去除根部,侧面切开,去除内部的子后在案板压平,备用(一定要去除青椒子,否则会在锅里炸开)"
+ },
+ {
+ "step": 2,
+ "description": "葱切成半厘米小段,备用"
+ },
+ {
+ "step": 3,
+ "description": "蒜去皮,切成碎末,备用"
+ },
+ {
+ "step": 4,
+ "description": "皮蛋去皮,备用"
+ },
+ {
+ "step": 5,
+ "description": "小米辣,切成 5-10mm 的小段,备用"
+ },
+ {
+ "step": 6,
+ "description": "热锅,锅内放入 10ml - 20ml 食用油"
+ },
+ {
+ "step": 7,
+ "description": "放入全部青椒,改小火保持锅子温度,煎至青椒变软(可以用筷子试一下,插入即透即可)"
+ },
+ {
+ "step": 8,
+ "description": "关火,将皮蛋和青椒放入小铁盆中"
+ },
+ {
+ "step": 9,
+ "description": "方法 1: 有擀面杖且砸东西不会吵到邻居:用擀面杖的一头在小盆中砸击皮蛋和青椒,至皮蛋与青椒混合(选项)"
+ },
+ {
+ "step": 10,
+ "description": "方法 2:将青椒用手撕开,撕成大约半厘米的条状,用叉子将皮蛋压碎(选项)"
+ },
+ {
+ "step": 11,
+ "description": "小米辣"
+ },
+ {
+ "step": 12,
+ "description": "倒入生抽,陈醋,白糖,香油,以及其他未使用的备用食材"
+ },
+ {
+ "step": 13,
+ "description": "均匀搅拌"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-vegetable_dish-鸡蛋火腿炒黄瓜",
+ "name": "鸡蛋火腿炒黄瓜的做法",
+ "description": "# 鸡蛋火腿炒黄瓜的做法\n\n预估烹饪难度:★★",
+ "source_path": "dishes/vegetable_dish/鸡蛋火腿炒黄瓜.md",
+ "image_path": null,
+ "images": [],
+ "category": "素菜",
+ "difficulty": 2,
+ "tags": [
+ "素菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "黄瓜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黄瓜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "火腿肠",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 火腿肠",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "红尖椒(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 红尖椒(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "黄瓜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 黄瓜 1 根(约 200g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋 2 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "火腿肠",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 火腿肠 1 根(约 40g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "红尖椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 红尖椒 1 个(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 10ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 3ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 2g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "黄瓜洗净,切半圆形片,备用"
+ },
+ {
+ "step": 2,
+ "description": "火腿切半圆形片,备用"
+ },
+ {
+ "step": 3,
+ "description": "红尖椒(可选)切碎,备用"
+ },
+ {
+ "step": 4,
+ "description": "将鸡蛋打入碗中,搅匀,即为 `鸡蛋液`"
+ },
+ {
+ "step": 5,
+ "description": "热锅里倒 5ml 食用油"
+ },
+ {
+ "step": 6,
+ "description": "油热后转小火,倒入打散的`鸡蛋液`,用筷子划散,翻炒至鸡蛋结为固体且颜色微微发黄,即为 `半熟鸡蛋`,盛出备用"
+ },
+ {
+ "step": 7,
+ "description": "**不用洗锅**,往锅内倒入 5ml 食用油,倒入黄瓜片大火**翻炒 1 分钟**"
+ },
+ {
+ "step": 8,
+ "description": "把 `半熟鸡蛋` 倒入锅中,调入 2g 盐、3ml 生抽,立刻倒入火腿片和辣椒碎(可选)翻炒均匀"
+ },
+ {
+ "step": 9,
+ "description": "关火,盛盘"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-vegetable_dish-上汤娃娃菜-上汤娃娃菜",
+ "name": "上汤娃娃菜的做法",
+ "description": "# 上汤娃娃菜的做法\n\n上汤娃娃菜的做法 (素菜、减肥餐)\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/vegetable_dish/上汤娃娃菜/上汤娃娃菜.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/上汤娃娃菜/上汤娃娃菜.png",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/上汤娃娃菜/上汤娃娃菜.png"
+ ],
+ "category": "素菜",
+ "difficulty": 3,
+ "tags": [
+ "素菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "娃娃菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 娃娃菜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "皮蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 皮蛋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "午餐肉(火腿肠)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 午餐肉(火腿肠)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "淀粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 淀粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "娃娃菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 娃娃菜 700g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "金针菇",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 金针菇 10g(看个人喜好, 不喜欢 see you tomorrow 的就不放 😂)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "皮蛋 一个(没有也可以不放)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 皮蛋 一个(没有也可以不放)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "午餐肉(火腿肠都可以替代)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 午餐肉(火腿肠都可以替代)",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "娃娃菜洗净, 竖着切开切成段。"
+ },
+ {
+ "step": 2,
+ "description": "葱 3g 切 小段。蒜 10g 切片。姜 10g 切小片。"
+ },
+ {
+ "step": 3,
+ "description": "皮蛋切成丁, 火腿肠或者午餐肉切成丁(1cm 大小的丁)"
+ },
+ {
+ "step": 4,
+ "description": "金针菇洗净撕开"
+ },
+ {
+ "step": 5,
+ "description": "烧热水娃娃菜放进去十秒钟出一下水捞出。"
+ },
+ {
+ "step": 6,
+ "description": "热锅凉油, 加热锅倒入油过一遍就倒出来, 重新倒入一点油。"
+ },
+ {
+ "step": 7,
+ "description": "调至小火加入葱姜蒜,煎炒出香味即可。"
+ },
+ {
+ "step": 8,
+ "description": "加入适 300g 清水(水量没过娃娃菜即可), 放入娃娃菜, 金针菇, 午餐肉"
+ },
+ {
+ "step": 9,
+ "description": "加入调味料蚝油、糖、盐、味精烧开。"
+ },
+ {
+ "step": 10,
+ "description": "煮 3 分钟, 煮开后开始装盘, 盛出娃娃菜后皮蛋放在上面把汤汁浇上去就可以了"
+ },
+ {
+ "step": 11,
+ "description": ""
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-vegetable_dish-凉拌木耳-凉拌木耳",
+ "name": "凉拌木耳的做法",
+ "description": "# 凉拌木耳的做法\n\n凉拌木耳,由于发放物资中有很多干货,木耳是较为健康的食物。且凉拌木耳的烹饪方式也相对简单。\n\n预估烹饪难度:★★",
+ "source_path": "dishes/vegetable_dish/凉拌木耳/凉拌木耳.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/凉拌木耳/1.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/凉拌木耳/1.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/凉拌木耳/10.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/凉拌木耳/2.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/凉拌木耳/3.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/凉拌木耳/4.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/凉拌木耳/5.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/凉拌木耳/6.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/凉拌木耳/7.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/凉拌木耳/8.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/凉拌木耳/9.jpg"
+ ],
+ "category": "素菜",
+ "difficulty": 2,
+ "tags": [
+ "素菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "干木耳 (湿木耳也可,但不能太久之前泡发的,必须是新鲜的湿木耳)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 干木耳 (湿木耳也可,但不能太久之前泡发的,必须是新鲜的湿木耳)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜瓣",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜瓣",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小米辣",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小米辣",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "醋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 醋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "芥末 (可以不用)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 芥末 (可以不用)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "干木耳:",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 干木耳: 20g / 湿木耳: 120g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜瓣:",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜瓣: 2-3 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小米辣:",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小米辣: 2 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐:",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐: 2 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "糖:",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 糖: 5-10g(依个人口味)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽:",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽: 15ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "醋:",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 醋: 15ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香油:",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香油: 5ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "芥末: (约",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 芥末: (约 2cm)",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "泡发干木耳, 水量约为 400ml, 泡发约 45 分钟。 (湿木耳跳过此步骤)"
+ },
+ {
+ "step": 2,
+ "description": "将泡发好的木耳, 进行去根处理(如图 4, 5, 6), 并彻底洗净。"
+ },
+ {
+ "step": 3,
+ "description": "起锅烧水,水开后放入木耳, 大火煮 1.5-2 分钟。"
+ },
+ {
+ "step": 4,
+ "description": "将蒜瓣、小米辣切碎放入碗中 (可选取中大碗), 并依次加入盐、糖、生抽、醋、香油、芥末, 用量如上。"
+ },
+ {
+ "step": 5,
+ "description": "木耳盛出后沥水, 放入上一步碗中。"
+ },
+ {
+ "step": 6,
+ "description": "搅拌充分,端盘。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-vegetable_dish-凉拌莴笋-凉拌莴笋",
+ "name": "凉拌莴笋的做法",
+ "description": "# 凉拌莴笋的做法\n\n凉拌莴笋,开胃小菜\n\n预估烹饪难度:★★",
+ "source_path": "dishes/vegetable_dish/凉拌莴笋/凉拌莴笋.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/凉拌莴笋/1.jpeg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/凉拌莴笋/1.jpeg"
+ ],
+ "category": "素菜",
+ "difficulty": 2,
+ "tags": [
+ "素菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "莴笋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 莴笋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "萝卜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 萝卜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小米辣",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小米辣",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜头",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜头",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "莴笋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 莴笋 1 根",
+ "notes": "量未指定"
+ },
+ {
+ "name": "萝卜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 萝卜 0.25 根",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小米辣",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小米辣 2 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜 1 片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜头",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜头 2 粒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 5 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 25 ml",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "莴笋削皮,切小条。萝卜切条,一起放入大碗,加入盐搅拌,放置 10 分钟"
+ },
+ {
+ "step": 2,
+ "description": "放置后的莴笋用水清洗 1-2 遍"
+ },
+ {
+ "step": 3,
+ "description": "起锅烧水,放入莴笋,水煮 1 分钟 捞出,沥干水分,放入大碗"
+ },
+ {
+ "step": 4,
+ "description": "起锅烧油,放入姜片、蒜粒、小米椒煸炒 30-45 S ,倒入莴笋中"
+ },
+ {
+ "step": 5,
+ "description": "搅拌充分,端盘"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-vegetable_dish-包菜炒鸡蛋粉丝-包菜炒鸡蛋粉丝",
+ "name": "包菜炒鸡蛋粉丝的做法",
+ "description": "# 包菜炒鸡蛋粉丝的做法\n\n包菜炒鸡蛋粉丝,是中国的一道日常生活中所熟知的菜品\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/vegetable_dish/包菜炒鸡蛋粉丝/包菜炒鸡蛋粉丝.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/包菜炒鸡蛋粉丝/1.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/包菜炒鸡蛋粉丝/1.jpg"
+ ],
+ "category": "素菜",
+ "difficulty": 3,
+ "tags": [
+ "素菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "包菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 包菜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "粉丝",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 粉丝",
+ "notes": "量未指定"
+ },
+ {
+ "name": "胡萝卜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 胡萝卜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "菜籽油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 菜籽油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐、生抽、老抽、蚝油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐、生抽、老抽、蚝油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱、蒜、干辣椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱、蒜、干辣椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "包菜 半 颗",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 包菜 半 颗",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋 2 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "粉丝",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 粉丝 1 把",
+ "notes": "量未指定"
+ },
+ {
+ "name": "胡萝卜 半 根",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 胡萝卜 半 根",
+ "notes": "量未指定"
+ },
+ {
+ "name": "菜籽油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 菜籽油 20 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 2 g、生抽 15 ml、老抽 10 ml、蚝油 10 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱 半 根、蒜瓣",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱 半 根、蒜瓣 2 片、干辣椒 5 根",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "胡萝卜、包菜切丝备用"
+ },
+ {
+ "step": 2,
+ "description": "粉丝先用冷水浸泡 1 小时,然后将粉丝放入锅中,加入开水烧至粉丝烫软捞出备用"
+ },
+ {
+ "step": 3,
+ "description": "鸡蛋打入碗中,加入盐后搅拌 15 秒"
+ },
+ {
+ "step": 4,
+ "description": "葱、蒜、辣椒切成小粒备用"
+ },
+ {
+ "step": 5,
+ "description": "起锅烧油,倒入鸡蛋,打散炒熟盛出"
+ },
+ {
+ "step": 6,
+ "description": "再倒入油,放入葱、蒜、干辣椒翻炒 8 秒"
+ },
+ {
+ "step": 7,
+ "description": "下胡萝卜、包菜丝儿翻炒 30 秒"
+ },
+ {
+ "step": 8,
+ "description": "放入粉丝"
+ },
+ {
+ "step": 9,
+ "description": "放调料,生抽 15 ml,老抽 10 ml,蚝油 10 ml,盐 2 克"
+ },
+ {
+ "step": 10,
+ "description": "放入之前炒好的鸡蛋,翻炒约 15 秒"
+ },
+ {
+ "step": 11,
+ "description": "出锅摆盘"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-vegetable_dish-小炒藕丁-小炒藕丁",
+ "name": "小炒藕丁的做法",
+ "description": "# 小炒藕丁的做法\n\n\n\n小炒藕丁是一道简单易做的菜,莲藕营养丰富,非常适合素食。预计制作时长 20 分钟\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/vegetable_dish/小炒藕丁/小炒藕丁.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/小炒藕丁/小炒藕丁.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/小炒藕丁/小炒藕丁.jpg"
+ ],
+ "category": "素菜",
+ "difficulty": 3,
+ "tags": [
+ "素菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "大葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小米辣",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小米辣",
+ "notes": "量未指定"
+ },
+ {
+ "name": "莲藕",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 莲藕",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "耗油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 耗油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大葱 1 段",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小米辣",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小米辣 1-2 根 (看个人吃辣程度)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "莲藕",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 莲藕 1 段",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 30 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽 15 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "耗油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 耗油 15 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 10-15ml",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "大葱、小米辣切小段,备用"
+ },
+ {
+ "step": 2,
+ "description": "莲藕去皮,切成不超过 3cm 的小块,放入水中备用(防止氧化发黑)"
+ },
+ {
+ "step": 3,
+ "description": "取炒锅,锅内放入 500ml 凉水,煮沸"
+ },
+ {
+ "step": 4,
+ "description": "将藕丁下入沸水中,焯水 2 分钟后,取出放入盘中备用"
+ },
+ {
+ "step": 5,
+ "description": "将锅中水倒掉后,将锅加热干燥,加入 10-15 ml 食用油"
+ },
+ {
+ "step": 6,
+ "description": "待油温升高后,下入葱花,小米辣爆香"
+ },
+ {
+ "step": 7,
+ "description": "将处理好的藕丁下入锅中,大火翻炒"
+ },
+ {
+ "step": 8,
+ "description": "加入生抽、老抽、耗油"
+ },
+ {
+ "step": 9,
+ "description": "翻炒 2 分钟即可出锅"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-vegetable_dish-干锅花菜-干锅花菜",
+ "name": "干锅花菜的做法",
+ "description": "# 干锅花菜的做法\n\n\n\n干锅花菜是湘菜常见的一道菜。\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/vegetable_dish/干锅花菜/干锅花菜.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/干锅花菜/干锅花菜.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/干锅花菜/干锅花菜.jpg"
+ ],
+ "category": "素菜",
+ "difficulty": 3,
+ "tags": [
+ "素菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "花菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 花菜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "五花肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 五花肉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "辣椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 辣椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "花菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 花菜 400 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "五花肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 五花肉 100 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "辣椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 辣椒 1-2 根",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 10 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜瓣",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜瓣 3-4 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 2 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 油 10 ml",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "花菜朵朝下,没入淡盐水中浸泡 20 分钟。然后洗净用小刀拆成小朵"
+ },
+ {
+ "step": 2,
+ "description": "入开水锅中焯水 1 分钟,捞出立即用冷水冲淋至完全凉透,沥水备用"
+ },
+ {
+ "step": 3,
+ "description": "五花肉切成薄片,大蒜白色切下用刀背拍扁,小红辣椒切成段"
+ },
+ {
+ "step": 4,
+ "description": "锅烧热放油,油热下大葱白爆香"
+ },
+ {
+ "step": 5,
+ "description": "下五花肉片入锅,用中火煸炒至表面全部变色,继续煸炒一会儿,把肥肉部分的油份逼出一部分"
+ },
+ {
+ "step": 6,
+ "description": "倒入红辣椒段和花菜,翻炒几下"
+ },
+ {
+ "step": 7,
+ "description": "加入 10 ml 生抽"
+ },
+ {
+ "step": 8,
+ "description": "再加入 5 g 白糖,转大火不断翻炒 1 分钟"
+ },
+ {
+ "step": 9,
+ "description": "把大蒜叶部分切成段,放入锅中,翻炒几下后,关火盖上盖子焖 1 分钟即可"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-vegetable_dish-手撕包菜-手撕包菜",
+ "name": "手撕包菜的做法",
+ "description": "# 手撕包菜的做法\n\n手撕包菜是一道色香味俱全的汉族名菜,属于湘菜系\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/vegetable_dish/手撕包菜/手撕包菜.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/手撕包菜/1.jpeg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/手撕包菜/1.jpeg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/手撕包菜/2.jpeg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/手撕包菜/3.jpeg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/手撕包菜/4.jpeg"
+ ],
+ "category": "素菜",
+ "difficulty": 3,
+ "tags": [
+ "素菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "包菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 包菜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "五花肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 五花肉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小米辣",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小米辣",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香醋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香醋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡精",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡精",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜头",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜头",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜苗",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜苗",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "包菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 包菜 1 颗",
+ "notes": "量未指定"
+ },
+ {
+ "name": "五花肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 五花肉 200 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小米辣",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小米辣 2 根",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 60 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒 5 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 5 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香醋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香醋 5 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡精",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡精 2 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜 2 片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜头",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜头 2 粒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜苗",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜苗 0.5 根",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 5 g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "包菜对半切开,去掉中间白色部分【参见图一】"
+ },
+ {
+ "step": 2,
+ "description": "手撕包菜,碗中放入 2 g 盐,清洗包菜并沥干备用【参见图二】"
+ },
+ {
+ "step": 3,
+ "description": "姜片、蒜头、小米辣、蒜苗处理后备用【参见图三】"
+ },
+ {
+ "step": 4,
+ "description": "五花肉切片,清水清洗后备用"
+ },
+ {
+ "step": 5,
+ "description": "锅中加入 30 ml 食用油,倒入包菜翻炒,大火翻炒 1 分钟 后,加入 3 g 盐 ,继续翻炒 2 分钟 后取出备用"
+ },
+ {
+ "step": 6,
+ "description": "锅中加入 30 ml 食用油,倒入五花肉,大火翻炒 1 分钟"
+ },
+ {
+ "step": 7,
+ "description": "倒入姜片等材料,翻炒 1 分钟"
+ },
+ {
+ "step": 8,
+ "description": "倒入包菜翻炒后,加入 香醋、料酒、鸡精、料酒,大火继续翻炒,2 分钟 后出锅"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-vegetable_dish-拔丝土豆-拔丝土豆",
+ "name": "拔丝土豆的做法",
+ "description": "# 拔丝土豆的做法\n\n拔丝土豆是一道色香味俱全的特色名菜,属于鲁菜系\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/vegetable_dish/拔丝土豆/拔丝土豆.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/拔丝土豆/1.jpeg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/拔丝土豆/1.jpeg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/拔丝土豆/2.jpeg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/拔丝土豆/3.jpeg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/拔丝土豆/4.jpeg"
+ ],
+ "category": "素菜",
+ "difficulty": 3,
+ "tags": [
+ "素菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "土豆",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 土豆",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "淀粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 淀粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白砂糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白砂糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 水",
+ "notes": "量未指定"
+ },
+ {
+ "name": "芝麻",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 芝麻",
+ "notes": "量未指定"
+ },
+ {
+ "name": "土豆",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 土豆 2 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 300 ml (土豆能浮在油上面即可)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "淀粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 淀粉 30 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白砂糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白砂糖 120 g (要多放糖,这是为了在土豆上面裹上一层厚厚的糖浆,从而产生拔丝的效果)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 水 100 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "芝麻",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 芝麻 5 g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "土豆去皮,切均匀的小块。放入淀粉(不加水)搅拌,使得淀粉覆盖土豆表面"
+ },
+ {
+ "step": 2,
+ "description": "起锅烧油,放入土豆块,缓缓翻滚煎炸 5-7 分钟 ,直至筷子可以插进土豆"
+ },
+ {
+ "step": 3,
+ "description": "取出土豆,放入大碗备用"
+ },
+ {
+ "step": 4,
+ "description": "锅中加入水、白砂糖,沿着一个方向慢慢搅动白砂糖,直到白砂糖颜色变成褐色"
+ },
+ {
+ "step": 5,
+ "description": "重新倒入土豆,翻炒 30 S 后 出锅"
+ },
+ {
+ "step": 6,
+ "description": "土豆盛盘,散上芝麻"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-vegetable_dish-椒盐玉米-椒盐玉米",
+ "name": "椒盐玉米的做法",
+ "description": "# 椒盐玉米的做法\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/vegetable_dish/椒盐玉米/椒盐玉米.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/椒盐玉米/椒盐玉米.jpeg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/椒盐玉米/椒盐玉米.jpeg"
+ ],
+ "category": "素菜",
+ "difficulty": 3,
+ "tags": [
+ "素菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "玉米粒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 玉米粒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "椒盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 椒盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "芝麻粒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 芝麻粒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "淀粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 淀粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "两个塑料簸箕",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 两个塑料簸箕",
+ "notes": "量未指定"
+ },
+ {
+ "name": "若干吸油纸",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 若干吸油纸",
+ "notes": "量未指定"
+ },
+ {
+ "name": "玉米粒(袋装)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 玉米粒(袋装) 350g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "淀粉(在锅里完全能盖住玉米粒表面为准,在",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 淀粉(在锅里完全能盖住玉米粒表面为准,在 40 - 70g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "椒盐粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 椒盐粉 10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "芝麻粒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 芝麻粒 10g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "玉米粒都是剥好的,直接解冻即可,温水泡 15 分钟或者灶上开水煮 5 分钟。"
+ },
+ {
+ "step": 2,
+ "description": "拿出一个簸箕,将其假设为 BoxA,垫上吸油纸,倒进解冻好的玉米粒。"
+ },
+ {
+ "step": 3,
+ "description": "shaking shaking shaking! - 直到吸油纸全部变湿为止。"
+ },
+ {
+ "step": 4,
+ "description": "拿出第二个簸箕 BoxB,垫上吸油纸,将 BoxA 的玉米粒全部倒入 BoxB 中。"
+ },
+ {
+ "step": 5,
+ "description": "shaking shaking shaking! - 直到吸油纸全部变湿为止。"
+ },
+ {
+ "step": 6,
+ "description": "重复上述操作多次,直到玉米表面没有明显可见的水滴但保持湿润的状态。"
+ },
+ {
+ "step": 7,
+ "description": "倒入大量淀粉,能够完全盖住玉米粒。"
+ },
+ {
+ "step": 8,
+ "description": "shaking shaking shaking! - 直到淀粉裹住了玉米粒"
+ },
+ {
+ "step": 9,
+ "description": "开灶 - 放锅 - 倒入油 尽量铺满锅底 但不要太多。"
+ },
+ {
+ "step": 10,
+ "description": "油热 8 成,倒入裹上了淀粉的玉米粒。"
+ },
+ {
+ "step": 11,
+ "description": "中火先煎 30s,不要翻炒,不然淀粉会掉。"
+ },
+ {
+ "step": 12,
+ "description": "轻微翻炒 3 分钟即可出锅。"
+ },
+ {
+ "step": 13,
+ "description": "最重要的一步:撒上 3g 椒盐,撒上芝麻粒!"
+ },
+ {
+ "step": 14,
+ "description": "香喷喷的”椒盐玉米“就做好了"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-vegetable_dish-榄菜肉末四季豆-榄菜肉末四季豆",
+ "name": "榄菜肉末四季豆的做法",
+ "description": "# 榄菜肉末四季豆的做法\n\n\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/vegetable_dish/榄菜肉末四季豆/榄菜肉末四季豆.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/榄菜肉末四季豆/榄菜肉末四季豆.JPG",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/榄菜肉末四季豆/榄菜肉末四季豆.JPG"
+ ],
+ "category": "素菜",
+ "difficulty": 3,
+ "tags": [
+ "素菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "四季豆",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 四季豆",
+ "notes": "量未指定"
+ },
+ {
+ "name": "五花肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 五花肉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "橄榄菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 橄榄菜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小米辣(不吃辣可以不放)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小米辣(不吃辣可以不放)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "四季豆",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 四季豆 220g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "五花肉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 五花肉 100g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "橄榄菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 橄榄菜 20g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大蒜 10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小米辣",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小米辣 10g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "将四季豆洗净,并把筋撕干净,然后切成大小均匀的颗粒备用。"
+ },
+ {
+ "step": 2,
+ "description": "将大蒜拍碎剁成蒜末备用。"
+ },
+ {
+ "step": 3,
+ "description": "将小米辣切成大小均匀的颗粒备用。"
+ },
+ {
+ "step": 4,
+ "description": "将五花肉去皮,然后剁成肉末备用。"
+ },
+ {
+ "step": 5,
+ "description": "将锅烧热,然后加入 20ml 油滑锅,锅滑好之后将热油倒出,然后加入 10ml 冷油,这就是传说中热锅冷油,这么做主要是防止肉末粘锅。"
+ },
+ {
+ "step": 6,
+ "description": "如果家里没有晾油瓶的话,也可以不用滑锅,放入油之后,直接加入肉末开始煸炒,小火煸炒两分钟,炒出猪油。"
+ },
+ {
+ "step": 7,
+ "description": "肉末炒香之后加入蒜末,橄榄菜和小米辣,炒出香味。"
+ },
+ {
+ "step": 8,
+ "description": "加入四季豆开中火煸炒,四季豆至少要炒 5 分钟,一定要保证四季豆**熟透**,否则可能会食物中毒。"
+ },
+ {
+ "step": 9,
+ "description": "四季豆炒熟后加入 2ml 酱油从锅边淋入,然后加入 2g 盐、1g 鸡精、1g 胡椒粉和 0.5g 糖。"
+ },
+ {
+ "step": 10,
+ "description": "将调料翻炒均匀。"
+ },
+ {
+ "step": 11,
+ "description": "出锅,装盘。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-vegetable_dish-洋葱炒鸡蛋-洋葱炒鸡蛋",
+ "name": "洋葱炒鸡蛋的做法",
+ "description": "# 洋葱炒鸡蛋的做法\n\n洋葱炒鸡蛋,是中国的一道日常生活中所熟知的菜品\n\n预估烹饪难度:★★",
+ "source_path": "dishes/vegetable_dish/洋葱炒鸡蛋/洋葱炒鸡蛋.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/洋葱炒鸡蛋/1.jpeg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/洋葱炒鸡蛋/1.jpeg"
+ ],
+ "category": "素菜",
+ "difficulty": 2,
+ "tags": [
+ "素菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "洋葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 洋葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋 2 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "洋葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 洋葱 50 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 50 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 2 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱 半 根",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱 半 根",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒 2 ml",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "鸡蛋打入大碗中,加入洋葱片、盐后搅拌 60 S"
+ },
+ {
+ "step": 2,
+ "description": "起锅烧油,倒入鸡蛋,一面煎炸 30-45 S ,翻面继续翻炒,反复 2-3 分钟 后散上料酒出锅"
+ },
+ {
+ "step": 3,
+ "description": "鸡蛋装盘,散上葱花"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-vegetable_dish-炒滑蛋-炒滑蛋",
+ "name": "炒滑蛋的做法",
+ "description": "# 炒滑蛋的做法\n\n\n\n炒滑蛋是一道简单易做的菜。一般初学者只需要 5 分钟即可完成。\n\n预估烹饪难度:★",
+ "source_path": "dishes/vegetable_dish/炒滑蛋/炒滑蛋.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/炒滑蛋/炒滑蛋.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/炒滑蛋/炒滑蛋.jpg"
+ ],
+ "category": "素菜",
+ "difficulty": 1,
+ "tags": [
+ "素菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "鸡蛋(最好是无菌蛋)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋(最好是无菌蛋)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "牛奶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 牛奶",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋 4 颗",
+ "notes": "量未指定"
+ },
+ {
+ "name": "牛奶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 牛奶 30ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 10ml",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "鸡蛋加入牛奶以及 5ml 食用油搅拌均匀,备用"
+ },
+ {
+ "step": 2,
+ "description": "大火烧热平底锅约 30s, 加入 5ml 食用油"
+ },
+ {
+ "step": 3,
+ "description": "烧 30s 转小火, 并且放入搅拌好的鸡蛋"
+ },
+ {
+ "step": 4,
+ "description": "在锅中静置 5 秒后,用锅铲将蛋液从边缘缓慢推向中间"
+ },
+ {
+ "step": 5,
+ "description": "翻炒至鸡蛋大致凝固后关火,装盘"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-vegetable_dish-烤茄子-烤茄子",
+ "name": "烤茄子的做法",
+ "description": "# 烤茄子的做法\n\n非常简单方便,而且香极了\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/vegetable_dish/烤茄子/烤茄子.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/烤茄子/烤茄子.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/烤茄子/烤茄子.jpg"
+ ],
+ "category": "素菜",
+ "difficulty": 3,
+ "tags": [
+ "素菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "茄子",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 茄子",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酱油(生抽)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酱油(生抽)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜蓉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜蓉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "辣椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 辣椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "孜然",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 孜然",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "茄子",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 茄子 1 个 (大约 200g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 20-30 毫升",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酱油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酱油 4-6 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小米椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小米椒 1 个 (大约 20g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜蓉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜蓉 3-4 瓣",
+ "notes": "量未指定"
+ },
+ {
+ "name": "孜然",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 孜然 1-3 克",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用盐 0.5-2 克",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "将酱油、孜然、食用盐、蒜蓉和切碎的小米椒置于碗中,均匀搅拌备用"
+ },
+ {
+ "step": 2,
+ "description": "茄子洗净,用纸巾擦干表面的水分"
+ },
+ {
+ "step": 3,
+ "description": "用叉子在茄子的一侧扎 4-8 下"
+ },
+ {
+ "step": 4,
+ "description": "使用 15-25ml 的食用油涂满茄子表面"
+ },
+ {
+ "step": 5,
+ "description": "将烤箱温度设置为 200℃ (打开烤箱风扇 大火),预热 2 分钟"
+ },
+ {
+ "step": 6,
+ "description": "将茄子放入烤箱中层或者上层,烤制 12-15 分钟 (茄子表面有褶皱,且能按压 0.3-0.5cm 的深度即可)"
+ },
+ {
+ "step": 7,
+ "description": "取出茄子,用刀茄子上竖着划一个口子。口子居中,上下距 1-1.5cm"
+ },
+ {
+ "step": 8,
+ "description": "用小刀或者叉子伸入口子,竖着切割茄子内部"
+ },
+ {
+ "step": 9,
+ "description": "将口子微微掰开,倒入第一步准备的酱料"
+ },
+ {
+ "step": 10,
+ "description": "再次将茄子放入烤箱,将烤箱温度设置为 200℃ ,烤制 4-7 分钟"
+ },
+ {
+ "step": 11,
+ "description": "取出,关闭烤箱电源"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-vegetable_dish-白灼菜心-白灼菜心",
+ "name": "白灼菜心的做法",
+ "description": "# 白灼菜心的做法\n\n\n\n\n\n> 没有拍照,上图是网图,不过做出来都差不多啦\n\n白灼菜心是经典粤菜,白灼是粤菜的一种烹饪技法,用煮沸的水或汤将生的食物烫熟,称为白灼。这种烹饪手法能保持原有的鲜味,粤菜常用此法烹制虾和蔬菜。\n\n总之吧,减肥或者是**快速解决绿叶菜的绝佳方式**。\n\n预估烹饪难度:★★",
+ "source_path": "dishes/vegetable_dish/白灼菜心/白灼菜心.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/白灼菜心/白灼菜心.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/白灼菜心/白灼菜心.jpg"
+ ],
+ "category": "素菜",
+ "difficulty": 2,
+ "tags": [
+ "素菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "新鲜菜心",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 新鲜菜心",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽、蚝油、盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽、蚝油、盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大蒜、小米辣",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大蒜、小米辣",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "新鲜菜心",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 新鲜菜心 250g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 10g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "调一个灵魂料汁儿",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 调一个灵魂料汁儿",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蚝油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蚝油 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐、糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐、糖 5g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大蒜四五瓣、小米辣一两根",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大蒜四五瓣、小米辣一两根",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-vegetable_dish-糖拌西红柿-糖拌西红柿",
+ "name": "糖拌西红柿的做法",
+ "description": "# 糖拌西红柿的做法\n\n\n\n新鲜可口,制作简便,营养价值高,适合夏季食用,家庭餐桌上的一道美味凉菜。西红柿含有大量的维生素 C, 做法简单 几分钟就可完成。\n\n预估烹饪难度:★★",
+ "source_path": "dishes/vegetable_dish/糖拌西红柿/糖拌西红柿.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/糖拌西红柿/火山飘雪.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/糖拌西红柿/火山飘雪.jpg"
+ ],
+ "category": "素菜",
+ "difficulty": 2,
+ "tags": [
+ "素菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "西红柿",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 西红柿",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白砂糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白砂糖",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冰箱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冰箱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "西红柿",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 西红柿 2 个(每个西红柿约 100g,共 200g)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白砂糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白砂糖 20g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "用刀将西红柿皮米字型划开"
+ },
+ {
+ "step": 2,
+ "description": "用筷子插入西红柿的菊花,在燃气上转动烤 10 秒(或用开水冲 30 秒),直到西红柿皮卷边"
+ },
+ {
+ "step": 3,
+ "description": "把西红柿的衣服脱光"
+ },
+ {
+ "step": 4,
+ "description": "再西红柿大卸八块(沿纹路切可以更多的留汁水),去掉头部根蒂部,备用"
+ },
+ {
+ "step": 5,
+ "description": "全部切好后,将西红柿在盘子中均匀码一层"
+ },
+ {
+ "step": 6,
+ "description": "撒上白糖,重复上面一步直到全部西红柿放完"
+ },
+ {
+ "step": 7,
+ "description": "放入冰箱冷藏 10 分钟"
+ },
+ {
+ "step": 8,
+ "description": "一盘糖拌西红柿就好了,营养美味,酸甜爽口,夏日解暑又解腻"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-vegetable_dish-红烧冬瓜-红烧冬瓜",
+ "name": "红烧冬瓜的做法",
+ "description": "# 红烧冬瓜的做法\n\n红烧冬瓜是一道具有色泽红亮,香鲜味美、营养价值丰富的家常菜\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/vegetable_dish/红烧冬瓜/红烧冬瓜.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/红烧冬瓜/1.jpeg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/红烧冬瓜/1.jpeg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/红烧冬瓜/2.jpeg"
+ ],
+ "category": "素菜",
+ "difficulty": 3,
+ "tags": [
+ "素菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "冬瓜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冬瓜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "淀粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 淀粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡精",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡精",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜末",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜末",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蚝油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蚝油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "冬瓜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 冬瓜 300 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 50 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒 2 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "淀粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 淀粉 10 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 10 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "老抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 老抽 15 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡精",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡精 3 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香葱 0.5 根",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜末",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜末 1 粒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蚝油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蚝油 15 ml",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "冬瓜去皮,切 边长不超过 2cm 小块"
+ },
+ {
+ "step": 2,
+ "description": "起锅烧油,放入冬瓜,缓缓翻滚煎炸 2 分钟 ,直至冬瓜表面泛金黄色"
+ },
+ {
+ "step": 3,
+ "description": "取出冬瓜,放入大碗备用"
+ },
+ {
+ "step": 4,
+ "description": "利用锅中的剩余油,依次放入姜末、生抽、蚝油,翻炒 15 S"
+ },
+ {
+ "step": 5,
+ "description": "重新倒入冬瓜,翻炒 30 S 后,加入开水,水要没过冬瓜表面,大火煮 10 分钟"
+ },
+ {
+ "step": 6,
+ "description": "加入老抽上色,继续煮,直至冬瓜软糯(筷子可以轻松插近冬瓜)"
+ },
+ {
+ "step": 7,
+ "description": "加入鸡精、料酒、香葱翻炒后 30 S, 取出冬瓜到大碗中"
+ },
+ {
+ "step": 8,
+ "description": "锅中剩余汤汁保留,倒入水淀粉,煮开后汤汁浇灌在冬瓜表面"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-vegetable_dish-芹菜拌茶树菇-芹菜拌茶树菇",
+ "name": "芹菜拌茶树菇的做法",
+ "description": "# 芹菜拌茶树菇的做法\n\n\n\n\n芹菜拌茶树菇是一道简单易做的凉拌菜。富含多种人体所需的维生素和矿物质。一般初学者只需要 30 分钟即可完成。\n\n预估烹饪难度:★★",
+ "source_path": "dishes/vegetable_dish/芹菜拌茶树菇/芹菜拌茶树菇.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/芹菜拌茶树菇/芹菜拌茶树菇.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/芹菜拌茶树菇/芹菜拌茶树菇.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/芹菜拌茶树菇/闽星茶树菇.jpg"
+ ],
+ "category": "素菜",
+ "difficulty": 2,
+ "tags": [
+ "素菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "闽星茶树菇",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 闽星茶树菇",
+ "notes": "量未指定"
+ },
+ {
+ "name": "芹菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 芹菜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蚝油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蚝油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "味极鲜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 味极鲜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "闽星茶树菇",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 闽星茶树菇 1 瓶",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香油 5ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蚝油 大约",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蚝油 大约 7ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "味极鲜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 味极鲜 3ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用盐 大约",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用盐 大约 2g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "(如果是芹菜苗这一步略过)用热水壶烧一壶热水,备用"
+ },
+ {
+ "step": 2,
+ "description": "新鲜的芹菜苗或者芹菜摘去黄叶清洗,备用"
+ },
+ {
+ "step": 3,
+ "description": "(如果是芹菜苗这一步略过)将芹菜摘去叶子单独放一个盆中,将芹菜茎用刀划成 2-3 毫米宽的芹菜条备用,这一步的目的是让芹菜断生的更快更均匀,吃起来更脆更爽口"
+ },
+ {
+ "step": 4,
+ "description": "芹菜苗切成 4cm 的芹菜段,备用"
+ },
+ {
+ "step": 5,
+ "description": "(如果是芹菜苗这一步略过)起锅开火,将热水壶的开水倒入锅中待水起泡沸腾"
+ },
+ {
+ "step": 6,
+ "description": "(如果是芹菜苗这一步略过)将切好的芹菜条放入锅中焯水,大约 20 秒放入芹菜叶,5 秒后关火全部捞出过凉水,备用"
+ },
+ {
+ "step": 7,
+ "description": "将盆中焯好的芹菜或者芹菜苗撒上准备好的食盐,香油,耗油和味极鲜搅拌均匀"
+ },
+ {
+ "step": 8,
+ "description": "将茶树菇倒入盆中搅拌均匀"
+ },
+ {
+ "step": 9,
+ "description": "装盘"
+ },
+ {
+ "step": 10,
+ "description": "开吃"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-vegetable_dish-莴笋叶煎饼-莴笋叶煎饼",
+ "name": "莴笋叶煎饼的做法",
+ "description": "# 莴笋叶煎饼的做法\n\n莴笋叶煎饼营养、好吃\n\n预估烹饪难度:★★",
+ "source_path": "dishes/vegetable_dish/莴笋叶煎饼/莴笋叶煎饼.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/莴笋叶煎饼/1.jpeg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/莴笋叶煎饼/1.jpeg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/莴笋叶煎饼/2.jpeg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/莴笋叶煎饼/3.jpeg"
+ ],
+ "category": "素菜",
+ "difficulty": 2,
+ "tags": [
+ "素菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "莴笋叶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 莴笋叶",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "淀粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 淀粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡精",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡精",
+ "notes": "量未指定"
+ },
+ {
+ "name": "莴笋叶",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 莴笋叶 50 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋 2 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 30 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 5 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "淀粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 淀粉 15 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡精",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡精 2 g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "莴笋叶剁碎,加入鸡蛋、生粉、生抽、鸡精搅拌均匀备用"
+ },
+ {
+ "step": 2,
+ "description": "起锅烧油,倒入莴笋叶浆汁,均匀平铺在锅面上"
+ },
+ {
+ "step": 3,
+ "description": "第一面炸 120 S 后,翻面再炸 60 S 后出锅"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-vegetable_dish-菠菜炒鸡蛋-菠菜炒鸡蛋",
+ "name": "菠菜炒鸡蛋的做法",
+ "description": "# 菠菜炒鸡蛋的做法\n\n这道菜难度系数简单,营养丰富。\n\n\n\n预估烹饪难度:★★",
+ "source_path": "dishes/vegetable_dish/菠菜炒鸡蛋/菠菜炒鸡蛋.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/菠菜炒鸡蛋/菠菜炒鸡蛋.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/菠菜炒鸡蛋/菠菜炒鸡蛋.jpg"
+ ],
+ "category": "素菜",
+ "difficulty": 2,
+ "tags": [
+ "素菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "菠菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 菠菜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "菠菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 菠菜 350g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋 2 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 15ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用盐 5g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "菠菜去根,洗净,放在篮子里,焯水"
+ },
+ {
+ "step": 2,
+ "description": "将鸡蛋打入碗中,搅匀"
+ },
+ {
+ "step": 3,
+ "description": "热锅,加入 10ml 油"
+ },
+ {
+ "step": 4,
+ "description": "油热后,倒入鸡蛋液,中火翻炒 15 秒,先煎成蛋饼,然后再用锅铲切成小块"
+ },
+ {
+ "step": 5,
+ "description": "关火,将鸡蛋块 盛到盘子中,不要洗锅"
+ },
+ {
+ "step": 6,
+ "description": "重新开火,倒入 5ml 油,油热后,放入菠菜,大火 翻炒 15 秒后,倒入鸡蛋块,翻炒均匀"
+ },
+ {
+ "step": 7,
+ "description": "加入 5g 盐、100ml 饮用水,大火 翻炒 10 秒"
+ },
+ {
+ "step": 8,
+ "description": "关火,盛盘"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-vegetable_dish-蒜蓉空心菜-蒜蓉空心菜",
+ "name": "蒜蓉空心菜的做法",
+ "description": "# 蒜蓉空心菜的做法\n\n背景:\n\n曾经去学校附近的川菜馆吃过蒜蓉空心菜,之后就一直很喜欢吃。\n\n预估烹饪难度:★★",
+ "source_path": "dishes/vegetable_dish/蒜蓉空心菜/蒜蓉空心菜.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/蒜蓉空心菜/1.JPG",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/蒜蓉空心菜/1.JPG"
+ ],
+ "category": "素菜",
+ "difficulty": 2,
+ "tags": [
+ "素菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "空心菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 空心菜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒜末",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒜末",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "筷子",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 筷子",
+ "notes": "量未指定"
+ },
+ {
+ "name": "铲子",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 铲子",
+ "notes": "量未指定"
+ },
+ {
+ "name": "新鲜空心菜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 新鲜空心菜 250 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大蒜半个,切碎为蒜末",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大蒜半个,切碎为蒜末",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 45 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 2 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖 3 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 8 ml",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "空心菜洗净,去掉烂叶或者老梗,均匀切成 2 段或者 3 段(防止过长不好炒)"
+ },
+ {
+ "step": 2,
+ "description": "锅里先倒少量油,烧至微微冒烟,此时拿起锅将国内的热油向四周浸润,让油均匀覆盖锅底,然后再倒入剩余的油([热锅凉油法](https://cook.aiursoft.cn/tips/learn/%E5%AD%A6%E4%B9%A0%E7%82%92%E4%B8%8E%E7%85%8E/?h=%E7%83%AD%E9%94%85#_5))。"
+ },
+ {
+ "step": 3,
+ "description": "放入蒜末,小火炒 10 到 15 秒煸香"
+ },
+ {
+ "step": 4,
+ "description": "尽快均匀地放入空心菜,**开大火**,左手拿铲子,右手拿筷子,配合将空心菜不停翻动,**直至软化变绿**。"
+ },
+ {
+ "step": 5,
+ "description": "接着不需使用筷子,而是使用铲子快速翻炒已软化的空心菜 15 - 20 秒,使之受热更均匀,撒入盐 2 g ,白糖 3 g,生抽 8 ml。"
+ },
+ {
+ "step": 6,
+ "description": "继续大火翻炒 10 秒,即可出锅。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-vegetable_dish-虎皮青椒-虎皮青椒",
+ "name": "虎皮青椒的做法",
+ "description": "# 虎皮青椒的做法\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/vegetable_dish/虎皮青椒/虎皮青椒.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/虎皮青椒/虎皮青椒.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/虎皮青椒/虎皮青椒.jpg"
+ ],
+ "category": "素菜",
+ "difficulty": 3,
+ "tags": [
+ "素菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "青椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 青椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大蒜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖(灵魂)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖(灵魂)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "醋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 醋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "砵或者有一定深度的碗",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 砵或者有一定深度的碗",
+ "notes": "量未指定"
+ },
+ {
+ "name": "青椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 青椒 5 个,长度在 10-15cm 的最为合适",
+ "notes": "量未指定"
+ },
+ {
+ "name": "大蒜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 大蒜 2-3 瓣",
+ "notes": "量未指定"
+ },
+ {
+ "name": "油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 油 20ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白糖",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白糖 15g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 15ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香醋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香醋 15ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 4g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "去掉青椒蒂,用自来水冲洗干净。"
+ },
+ {
+ "step": 2,
+ "description": "青椒切长片,平均一个青椒纵向切成 3-4 片即可。"
+ },
+ {
+ "step": 3,
+ "description": "大蒜去皮,切成碎末,体积在 2mm x 2mm x 2mm 即可。"
+ },
+ {
+ "step": 4,
+ "description": "`调料 1`:拿一个小碗倒入 20ml 油,将大蒜末放入其中。"
+ },
+ {
+ "step": 5,
+ "description": "`调料 2`:白糖、生抽、醋、盐全部倒入砵(碗)等容器,搅拌。"
+ },
+ {
+ "step": 6,
+ "description": "将 `调料 1` 倒入锅中,开火加热 5 成放入青椒,青椒片不要叠在一起,单独成片放置锅中。"
+ },
+ {
+ "step": 7,
+ "description": "用锅铲不停的按压青椒,合适的时候翻面。"
+ },
+ {
+ "step": 8,
+ "description": "翻炒约 2 分钟,待青椒表皮出现褶皱时,倒入 `调料 2`。"
+ },
+ {
+ "step": 9,
+ "description": "加大火候继续翻炒 30s 后即可出锅盛入盘中。"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-vegetable_dish-蚝油三鲜菇-蚝油三鲜菇",
+ "name": "蚝油三鲜菇的做法",
+ "description": "# 蚝油三鲜菇的做法\n\n几分钟就能做出的蚝油蘑菇,滑嫩入味鲜美可口,别提多好吃了。\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/vegetable_dish/蚝油三鲜菇/蚝油三鲜菇.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/蚝油三鲜菇/1.jpeg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/蚝油三鲜菇/1.jpeg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/蚝油三鲜菇/2.jpeg"
+ ],
+ "category": "素菜",
+ "difficulty": 3,
+ "tags": [
+ "素菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "香菇",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香菇",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蟹味菇",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蟹味菇",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白玉菇",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白玉菇",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小米辣",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小米辣",
+ "notes": "量未指定"
+ },
+ {
+ "name": "菜椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 菜椒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "淀粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 淀粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡精",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡精",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜末",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜末",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蚝油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蚝油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "西蓝花",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 西蓝花",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鲜香菇",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鲜香菇 2 朵",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蟹味菇",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蟹味菇 30 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白玉菇",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白玉菇 30 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "小米辣",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 小米辣 1 根",
+ "notes": "量未指定"
+ },
+ {
+ "name": "菜椒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 菜椒 0.5 颗",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 10 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用盐 5 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "料酒",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 料酒 2 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "淀粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 淀粉 10 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 10 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡精",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡精 3 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香葱 0.5 根",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜末",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜末 1 粒",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蚝油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蚝油 5 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "开水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 开水 350 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "西蓝花",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 西蓝花 100 g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "蟹味菇、白玉菇 去掉根部泥土,掰散菌朵"
+ },
+ {
+ "step": 2,
+ "description": "香菇切片(每片厚度 0.5-1 cm,厚点相对薄点更有嚼劲)"
+ },
+ {
+ "step": 3,
+ "description": "生粉倒入小碗中,加入 50ml 水,搅拌生粉直至融化没有颗粒(即水淀粉)备用"
+ },
+ {
+ "step": 4,
+ "description": "水开,放入西蓝花,清水煮 3 分钟,放入碗中备用"
+ },
+ {
+ "step": 5,
+ "description": "洗锅烧开水,加入 5 g 食用盐,倒入蟹味菇、白玉菇、香菇,水煮 1 分钟"
+ },
+ {
+ "step": 6,
+ "description": "1 分钟后,捞出沥干水分"
+ },
+ {
+ "step": 7,
+ "description": "起锅烧油,待油开始冒小泡,放入姜末、小米辣、菜椒 煸炒 30 S"
+ },
+ {
+ "step": 8,
+ "description": "倒入三鲜菇,然后依次倒入生抽、蚝油、鸡精,翻炒均匀后,倒入水淀粉"
+ },
+ {
+ "step": 9,
+ "description": "中火烧干汁,加入料酒、葱花 出锅"
+ },
+ {
+ "step": 10,
+ "description": "摆上西蓝花"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-vegetable_dish-西红柿豆腐汤羹-西红柿豆腐汤羹",
+ "name": "西红柿豆腐汤羹的做法",
+ "description": "# 西红柿豆腐汤羹的做法\n\n西红柿豆腐汤羹是一道很清淡美味的汤羹\n\n预估烹饪难度:★★",
+ "source_path": "dishes/vegetable_dish/西红柿豆腐汤羹/西红柿豆腐汤羹.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/西红柿豆腐汤羹/1.jpeg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/西红柿豆腐汤羹/1.jpeg"
+ ],
+ "category": "素菜",
+ "difficulty": 2,
+ "tags": [
+ "素菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "西红柿",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 西红柿",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "豆腐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 豆腐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "淀粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 淀粉",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡精",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡精",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香葱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "开水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 开水",
+ "notes": "量未指定"
+ },
+ {
+ "name": "西红柿",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 西红柿 1 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋 1 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "豆腐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 豆腐 100 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 5 ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 2 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "淀粉",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 淀粉 5 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡精",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡精 2 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香葱 0.5 根",
+ "notes": "量未指定"
+ },
+ {
+ "name": "姜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 姜 1 片",
+ "notes": "量未指定"
+ },
+ {
+ "name": "开水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 开水 350 ml",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "西红柿切成小丁、鸡蛋打入碗中搅拌、豆腐切块备用"
+ },
+ {
+ "step": 2,
+ "description": "起锅烧油,放入姜片 5 S 后倒入入西红柿翻炒 30 S"
+ },
+ {
+ "step": 3,
+ "description": "锅中加入开水,汤水烧开,60 S 后到入鸡蛋液、豆腐块"
+ },
+ {
+ "step": 4,
+ "description": "汤水重新烧开后,加入水淀粉,沿一个方向搅拌 2 圈"
+ },
+ {
+ "step": 5,
+ "description": "加入鸡精、盐、香葱,30 S 后起锅"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-vegetable_dish-西葫芦炒鸡蛋-西葫芦炒鸡蛋",
+ "name": "西葫芦炒鸡蛋的做法",
+ "description": "# 西葫芦炒鸡蛋的做法\n\n\n\n西葫芦炒鸡蛋是一道简单易做的家常菜。简单易购的食材,好吃又下饭。\n\n预估烹饪难度:★★",
+ "source_path": "dishes/vegetable_dish/西葫芦炒鸡蛋/西葫芦炒鸡蛋.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/西葫芦炒鸡蛋/西葫芦炒鸡蛋.jpeg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/西葫芦炒鸡蛋/西葫芦炒鸡蛋.jpeg"
+ ],
+ "category": "素菜",
+ "difficulty": 2,
+ "tags": [
+ "素菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "西葫芦",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 西葫芦",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "西红柿(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 西红柿(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "西葫芦",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 西葫芦 500g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "西红柿",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 西红柿 100g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋 3 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 10-20ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用盐 6g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "西红柿洗净,切成小块,备用"
+ },
+ {
+ "step": 2,
+ "description": "西葫芦洗净,切成边长约为 4cm 的菱形,备用"
+ },
+ {
+ "step": 3,
+ "description": "打三个鸡蛋到碗里,打散搅匀,备用"
+ },
+ {
+ "step": 4,
+ "description": "热锅,锅内放入 5ml - 10ml 食用油"
+ },
+ {
+ "step": 5,
+ "description": "倒入鸡蛋,保持翻炒至鸡蛋成固体,用锅铲分成小块后盛到碗里,备用"
+ },
+ {
+ "step": 6,
+ "description": "锅内放入 5ml - 10ml 食用油,倒入西红柿,炒至变软"
+ },
+ {
+ "step": 7,
+ "description": "倒入西葫芦一起翻炒均匀,放入 6g 食用盐,将火调小然后**等待 4 - 5 分钟**"
+ },
+ {
+ "step": 8,
+ "description": "倒入备用的鸡蛋,中火翻炒 15 秒"
+ },
+ {
+ "step": 9,
+ "description": "关火,盛盘"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-vegetable_dish-话梅煮毛豆-话梅煮毛豆",
+ "name": "话梅煮毛豆的做法",
+ "description": "# 话梅煮毛豆的做法\n\n酸甜可口、营养价值高的一种简易美食\n\n预估烹饪难度:★★",
+ "source_path": "dishes/vegetable_dish/话梅煮毛豆/话梅煮毛豆.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/话梅煮毛豆/1.jpeg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/话梅煮毛豆/1.jpeg"
+ ],
+ "category": "素菜",
+ "difficulty": 2,
+ "tags": [
+ "素菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "毛豆",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 毛豆",
+ "notes": "量未指定"
+ },
+ {
+ "name": "话梅",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 话梅",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "毛豆",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 毛豆 300 g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "话梅",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 话梅 6 颗",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用盐 2 g",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "清水加入食用盐,毛豆浸泡 15 分钟"
+ },
+ {
+ "step": 2,
+ "description": "加入开水,倒入毛豆、话梅,水煮 20-30 分钟"
+ },
+ {
+ "step": 3,
+ "description": "起锅开吃"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-vegetable_dish-鸡蛋羹-微波炉鸡蛋羹",
+ "name": "微波炉鸡蛋羹的做法",
+ "description": "# 微波炉鸡蛋羹的做法\n\n\n微波炉鸡蛋羹是一个简单易制作的菜。非常适合夜间突然饿了的时候充当夜宵,快捷简单。\n\n预估烹饪难度:★★",
+ "source_path": "dishes/vegetable_dish/鸡蛋羹/微波炉鸡蛋羹.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/鸡蛋羹/微波炉鸡蛋羹.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/鸡蛋羹/微波炉鸡蛋羹.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/鸡蛋羹/鸡蛋羹.jpg"
+ ],
+ "category": "素菜",
+ "difficulty": 2,
+ "tags": [
+ "素菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 水",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋 2 个 * 份数",
+ "notes": "量未指定"
+ },
+ {
+ "name": "水",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 水 200ml * 份数",
+ "notes": "量未指定"
+ },
+ {
+ "name": "虾皮",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 虾皮 10 个 * 份数(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "葱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 葱 5g *份数(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 盐 3g * 份数",
+ "notes": "量未指定"
+ },
+ {
+ "name": "酱油(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 酱油(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "芝麻油(香油)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 芝麻油(香油) 1ml(可选)",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "将鸡蛋打入可使用微波炉加热的陶瓷碗中,使用筷子将其打散。"
+ },
+ {
+ "step": 2,
+ "description": "加入水和盐,搅拌均匀。"
+ },
+ {
+ "step": 3,
+ "description": "将虾皮放入碗中,搅拌均匀,保证所有虾皮不会堆积在一起。"
+ },
+ {
+ "step": 4,
+ "description": "葱切碎至边长 0.6±3mm 状,放入碗中搅拌均匀。"
+ },
+ {
+ "step": 5,
+ "description": "将此碗及内容物放入微波炉中,容器表面覆盖保鲜膜或以可微波瓷盘加盖(注意:不得密封,必须留有涨缩量)加热 2 分钟(500W)。"
+ },
+ {
+ "step": 6,
+ "description": "小心地取下保鲜膜或其他覆盖物,然后继续加热 2 分钟。"
+ },
+ {
+ "step": 7,
+ "description": "若微波炉不带旋转式加热盘,将碗缓慢的水平旋转 180 度,以确保内容物加热均匀。"
+ },
+ {
+ "step": 8,
+ "description": "放入芝麻油。"
+ },
+ {
+ "step": 9,
+ "description": "小心地从微波炉中拿出碗(真的很烫)。"
+ },
+ {
+ "step": 10,
+ "description": "如果选择放入酱油,则确保酱油在鸡蛋羹表面流动后能以最薄的形式沾满鸡蛋羹表面即可。"
+ },
+ {
+ "step": 11,
+ "description": "开心的享受鸡蛋羹"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-vegetable_dish-鸡蛋羹-蒸箱鸡蛋羹",
+ "name": "蒸箱鸡蛋羹的做法",
+ "description": "# 蒸箱鸡蛋羹的做法\n\n蒸箱鸡蛋羹,是一道简单快捷易做的菜,制作时长约为 15 分钟。适用于有家庭蒸箱的厨师。\n\n预估烹饪难度:★★★",
+ "source_path": "dishes/vegetable_dish/鸡蛋羹/蒸箱鸡蛋羹.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/鸡蛋羹/微波炉鸡蛋羹.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/鸡蛋羹/微波炉鸡蛋羹.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/鸡蛋羹/鸡蛋羹.jpg"
+ ],
+ "category": "素菜",
+ "difficulty": 3,
+ "tags": [
+ "素菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "蒸箱",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 蒸箱",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋 1 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用盐 1g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用油 5ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽 / 味极鲜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 / 味极鲜 6ml(可选调味)",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "一个鸡蛋放入碗中打散"
+ },
+ {
+ "step": 2,
+ "description": "向碗中加入鸡蛋体积 1.0-1.5 倍 60 度纯净水,并且搅拌均匀"
+ },
+ {
+ "step": 3,
+ "description": "加入食用盐 1g"
+ },
+ {
+ "step": 4,
+ "description": "加入食用油 5ml"
+ },
+ {
+ "step": 5,
+ "description": "过滤蛋液,去掉蛋液中的浮沫(可选,不过滤蒸出来的蛋会有气泡导致不好看)"
+ },
+ {
+ "step": 6,
+ "description": "确认蒸箱的水源已经补充至足够(若不确定,可把水槽补满)"
+ },
+ {
+ "step": 7,
+ "description": "将已经完全搅拌均匀的鸡蛋液碗放入蒸箱"
+ },
+ {
+ "step": 8,
+ "description": "调节至**100摄氏度**蒸 **10 分钟**"
+ },
+ {
+ "step": 9,
+ "description": "打开蒸箱 (注意:蒸箱在开启时会有蒸气瞬间喷出,注意缓慢开启)"
+ },
+ {
+ "step": 10,
+ "description": "出锅(可加入生抽调味)"
+ },
+ {
+ "step": 11,
+ "description": "享用"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ },
+ {
+ "id": "dishes-vegetable_dish-鸡蛋羹-鸡蛋羹",
+ "name": "鸡蛋羹的做法",
+ "description": "# 鸡蛋羹的做法\n\n\n\n鸡蛋羹,又称水蒸蛋,不需要准备复杂的食材,是一道简单快捷易做的菜,当早餐或是正餐都可,制作时长约为 15 分钟。\n\n预估烹饪难度:★★",
+ "source_path": "dishes/vegetable_dish/鸡蛋羹/鸡蛋羹.md",
+ "image_path": "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/鸡蛋羹/微波炉鸡蛋羹.jpg",
+ "images": [
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/鸡蛋羹/微波炉鸡蛋羹.jpg",
+ "https://media.githubusercontent.com/media/worryzyy/HowToCook/mcp/dishes/vegetable_dish/鸡蛋羹/鸡蛋羹.jpg"
+ ],
+ "category": "素菜",
+ "difficulty": 2,
+ "tags": [
+ "素菜"
+ ],
+ "servings": 1,
+ "ingredients": [
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用盐",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香油",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香油",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽 / 味极鲜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 / 味极鲜",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白醋(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白醋(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "藤椒油(可选)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 藤椒油(可选)",
+ "notes": "量未指定"
+ },
+ {
+ "name": "鸡蛋",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 鸡蛋 2 个",
+ "notes": "量未指定"
+ },
+ {
+ "name": "食用盐",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 食用盐 3g",
+ "notes": "量未指定"
+ },
+ {
+ "name": "香油(或芝麻油)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 香油(或芝麻油) 2-4ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "生抽 / 味极鲜",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 生抽 / 味极鲜 8ml",
+ "notes": "量未指定"
+ },
+ {
+ "name": "白醋(或料酒)",
+ "quantity": null,
+ "unit": null,
+ "text_quantity": "- 白醋(或料酒) 2ml(可选)",
+ "notes": "量未指定"
+ }
+ ],
+ "steps": [
+ {
+ "step": 1,
+ "description": "两个鸡蛋放入碗中打散"
+ },
+ {
+ "step": 2,
+ "description": "加入食用盐 3g"
+ },
+ {
+ "step": 3,
+ "description": "加入 2ml 白醋,去除鸡蛋的腥味(可选)"
+ },
+ {
+ "step": 4,
+ "description": "向碗中加入鸡蛋体积 1-1.5 倍的 70 度纯净水,并且搅拌均匀"
+ },
+ {
+ "step": 5,
+ "description": "过滤蛋液,去掉蛋液中的浮沫(可选,不过滤蒸出来的蛋会有气泡导致不好看)"
+ },
+ {
+ "step": 6,
+ "description": "向任意一口锅中加入 50ml 清水,水烧开后,放入盛有鸡蛋液的碗"
+ },
+ {
+ "step": 7,
+ "description": "蒸煮步骤(二选一)"
+ },
+ {
+ "step": 8,
+ "description": "如何判断已经熟了?"
+ },
+ {
+ "step": 9,
+ "description": "出锅"
+ },
+ {
+ "step": 10,
+ "description": "加入香油和生抽即可享用"
+ }
+ ],
+ "prep_time_minutes": null,
+ "cook_time_minutes": null,
+ "total_time_minutes": null,
+ "additional_notes": [
+ "如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"
+ ]
+ }
+]
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/src/data/recipes.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/src/data/recipes.ts
new file mode 100644
index 00000000..a5036d99
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/src/data/recipes.ts
@@ -0,0 +1,36 @@
+import { Recipe } from '../types/index.js'
+import localRecipes from './all_recipes.json' with { type: 'json' };
+
+// 远程菜谱JSON文件URL
+const RECIPES_URL = 'https://weilei.site/all_recipes.json'
+
+/**
+ * 从远程获取菜谱,如果失败则使用本地备份
+ */
+export async function fetchRecipes(): Promise {
+ try {
+ const response = await fetch(RECIPES_URL)
+ if (!response.ok) {
+ throw new Error(`HTTP error! Status: ${response.status}`)
+ }
+ const data = await response.json()
+ return data as Recipe[]
+ } catch (error) {
+ console.error('获取远程菜谱数据失败,将使用本地备份数据')
+ // localRecipes 已经是 JSON 转好的对象
+ return localRecipes as Recipe[]
+ }
+}
+
+/**
+ * 获取所有分类
+ */
+export function getAllCategories(recipes: Recipe[]): string[] {
+ const categories = new Set()
+ recipes.forEach((recipe) => {
+ if (recipe.category) {
+ categories.add(recipe.category)
+ }
+ })
+ return Array.from(categories)
+}
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/src/index.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/src/index.ts
new file mode 100644
index 00000000..3dadf61b
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/src/index.ts
@@ -0,0 +1,213 @@
+#!/usr/bin/env node
+
+import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
+import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
+import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
+import { Command } from 'commander';
+import { createServer } from 'http';
+import { fetchRecipes, getAllCategories } from "./data/recipes.js";
+import { registerGetAllRecipesTool } from "./tools/getAllRecipes.js";
+import { registerGetRecipeByIdTool } from "./tools/getRecipeById.js";
+import { registerGetRecipesByCategoryTool } from "./tools/getRecipesByCategory.js";
+import { registerRecommendMealsTool } from "./tools/recommendMeals.js";
+import { registerWhatToEatTool } from "./tools/whatToEat.js";
+import { Recipe } from './types/index.js';
+
+// 全局变量存储数据
+let recipes: Recipe[] = [];
+let categories: string[] = [];
+
+// 命令行参数处理
+const program = new Command()
+ .option("--transport ", "transport type", "stdio")
+ .option("--port ", "port for HTTP/SSE transport", "3000")
+ .parse(process.argv);
+
+const cliOptions = program.opts<{
+ transport: string;
+ port: string;
+}>();
+
+const allowedTransports = ["stdio", "http", "sse"];
+if (!allowedTransports.includes(cliOptions.transport)) {
+ console.error(
+ `Invalid --transport value: '${cliOptions.transport}'. Must be one of: stdio, http, sse.`
+ );
+ process.exit(1);
+}
+
+const TRANSPORT_TYPE = (cliOptions.transport || "stdio") as "stdio" | "http" | "sse";
+const PORT = parseInt(cliOptions.port, 10);
+// SSE transports
+const sseTransports: Record = {};
+// 创建MCP服务器实例
+function createServerInstance(): McpServer {
+ const server = new McpServer({
+ name: 'howtocook-mcp',
+ version: '0.1.1',
+ }, {
+ capabilities: {
+ logging: {},
+ },
+ });
+
+ // 注册所有工具
+ registerGetAllRecipesTool(server, recipes);
+ registerGetRecipesByCategoryTool(server, recipes, categories);
+ registerRecommendMealsTool(server, recipes);
+ registerWhatToEatTool(server, recipes);
+ registerGetRecipeByIdTool(server, recipes);
+
+ return server;
+}
+
+// 加载菜谱数据
+async function loadRecipeData() {
+ try {
+ recipes = await fetchRecipes();
+ categories = getAllCategories(recipes);
+ console.error(`📚 已加载 ${recipes.length} 个菜谱`);
+ } catch (error) {
+ console.error('加载菜谱数据失败:', error);
+ recipes = [];
+ categories = [];
+ throw error;
+ }
+}
+
+// 启动服务的主函数
+async function main() {
+ // 加载菜谱数据
+ await loadRecipeData();
+
+ if (TRANSPORT_TYPE === "http" || TRANSPORT_TYPE === "sse") {
+ const httpServer = createServer(async (req, res) => {
+ const url = new URL(req.url || "", `http://${req.headers.host}`).pathname;
+
+ // 设置 CORS 头
+ res.setHeader("Access-Control-Allow-Origin", "*");
+ res.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,DELETE");
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, MCP-Session-Id, mcp-session-id");
+
+ // 处理预检请求
+ if (req.method === "OPTIONS") {
+ res.writeHead(200);
+ res.end();
+ return;
+ }
+
+ try {
+ // 为每个请求创建新的服务器实例
+ const requestServer = createServerInstance();
+
+ if (url === "/mcp") {
+ const transport = new StreamableHTTPServerTransport({
+ sessionIdGenerator: undefined,
+ });
+ await requestServer.connect(transport);
+ await transport.handleRequest(req, res);
+ }else if (url === "/sse" && req.method === "GET") {
+ // Create new SSE transport for GET request
+ const sseTransport = new SSEServerTransport("/messages", res);
+ // Store the transport by session ID
+ sseTransports[sseTransport.sessionId] = sseTransport;
+ // Clean up transport when connection closes
+ res.on("close", () => {
+ delete sseTransports[sseTransport.sessionId];
+ });
+ await requestServer.connect(sseTransport);
+ } else if (url === "/messages" && req.method === "POST") {
+ // Get session ID from query parameters
+ const sessionId =
+ new URL(req.url || "", `http://${req.headers.host}`).searchParams.get("sessionId") ??
+ "";
+
+ if (!sessionId) {
+ res.writeHead(400);
+ res.end("Missing sessionId parameter");
+ return;
+ }
+
+ // Get existing transport for this session
+ const sseTransport = sseTransports[sessionId];
+ if (!sseTransport) {
+ res.writeHead(400);
+ res.end(`No transport found for sessionId: ${sessionId}`);
+ return;
+ }
+
+ // Handle the POST message with the existing transport
+ await sseTransport.handlePostMessage(req, res);
+ }
+ else if (url === "/health") {
+ res.writeHead(200, { "Content-Type": "application/json" });
+ res.end(JSON.stringify({ status: "ok", transport: TRANSPORT_TYPE }));
+ } else if (url === "/info") {
+ res.writeHead(200, { "Content-Type": "application/json" });
+ res.end(JSON.stringify({
+ name: "HowToCook MCP Server",
+ version: "0.1.1",
+ transport: TRANSPORT_TYPE,
+ endpoints: {
+ mcp: "/mcp",
+ sse: "/sse",
+ health: "/health",
+ info: "/info"
+ },
+ recipeCount: recipes.length
+ }));
+ } else {
+ res.writeHead(404);
+ res.end("Not found");
+ }
+ } catch (error) {
+ console.error("处理请求时出错:", error);
+ if (!res.headersSent) {
+ res.writeHead(500);
+ res.end("Internal Server Error");
+ }
+ }
+ });
+
+ httpServer.listen(PORT, () => {
+ console.error(`🚀 HowToCook MCP ${TRANSPORT_TYPE.toUpperCase()} 服务器启动成功`);
+ if(TRANSPORT_TYPE === "http"){
+ console.error(`🔗 MCP 端点: http://localhost:${PORT}/mcp`);
+ }else if(TRANSPORT_TYPE === "sse"){
+ console.error(`🔗 MCP 端点: http://localhost:${PORT}/sse`);
+ }
+ console.error(`💡 健康检查: http://localhost:${PORT}/health`);
+ console.error(`ℹ️ 服务器信息: http://localhost:${PORT}/info`);
+ });
+ } else {
+ // stdio 模式
+ const server = createServerInstance();
+ const transport = new StdioServerTransport();
+ try {
+ await server.connect(transport);
+ console.error('HowToCook MCP STDIO 服务器启动成功');
+ } catch (error) {
+ console.error('服务器启动失败:', error);
+ process.exit(1);
+ }
+ }
+}
+
+// 优雅关闭
+process.on('SIGINT', async () => {
+ console.error('\n正在关闭服务器...');
+ process.exit(0);
+});
+
+process.on('SIGTERM', async () => {
+ console.error('\n收到终止信号,正在关闭服务器...');
+ process.exit(0);
+});
+
+// 启动服务器
+main().catch((error) => {
+ console.error('启动服务器失败:', error);
+ process.exit(1);
+});
+
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/src/tools/getAllRecipes.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/src/tools/getAllRecipes.ts
new file mode 100644
index 00000000..3842ed3e
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/src/tools/getAllRecipes.ts
@@ -0,0 +1,27 @@
+import { z } from "zod";
+import { Recipe } from "../types/index.js";
+import { simplifyRecipeNameOnly } from "../utils/recipeUtils.js";
+import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+
+export function registerGetAllRecipesTool(server: McpServer, recipes: Recipe[]) {
+ server.tool(
+ "mcp_howtocook_getAllRecipes",
+ "获取所有菜谱",
+ {
+ 'no_param': z.string().optional()
+ .describe('无参数')
+ },
+ async () => {
+ // 返回更简化版的菜谱数据,只包含name和description
+ const simplifiedRecipes = recipes.map(simplifyRecipeNameOnly);
+ return {
+ content: [
+ {
+ type: "text",
+ text: JSON.stringify(simplifiedRecipes, null, 2),
+ },
+ ],
+ };
+ }
+ );
+}
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/src/tools/getRecipeById.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/src/tools/getRecipeById.ts
new file mode 100644
index 00000000..ee106c12
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/src/tools/getRecipeById.ts
@@ -0,0 +1,80 @@
+import { z } from "zod";
+import { Recipe } from "../types/index.js";
+import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+
+export function registerGetRecipeByIdTool(server: McpServer, recipes: Recipe[]) {
+ server.tool(
+ "mcp_howtocook_getRecipeById",
+ "根据菜谱名称或ID查询指定菜谱的完整详情,包括食材、步骤等",
+ {
+ query: z.string().describe('菜谱名称或ID,支持模糊匹配菜谱名称')
+ },
+ async ({ query }: { query: string }) => {
+ // 首先尝试精确匹配ID
+ let foundRecipe = recipes.find(recipe => recipe.id === query);
+
+ // 如果没有找到,尝试精确匹配名称
+ if (!foundRecipe) {
+ foundRecipe = recipes.find(recipe => recipe.name === query);
+ }
+
+ // 如果还没有找到,尝试模糊匹配名称
+ if (!foundRecipe) {
+ foundRecipe = recipes.find(recipe =>
+ recipe.name.toLowerCase().includes(query.toLowerCase())
+ );
+ }
+
+ // 如果仍然没有找到,返回所有可能的匹配项(最多5个)
+ if (!foundRecipe) {
+ const possibleMatches = recipes.filter(recipe =>
+ recipe.name.toLowerCase().includes(query.toLowerCase()) ||
+ recipe.description.toLowerCase().includes(query.toLowerCase())
+ ).slice(0, 5);
+
+ if (possibleMatches.length === 0) {
+ return {
+ content: [
+ {
+ type: "text",
+ text: JSON.stringify({
+ error: "未找到匹配的菜谱",
+ query: query,
+ suggestion: "请检查菜谱名称是否正确,或尝试使用关键词搜索"
+ }, null, 2),
+ },
+ ],
+ };
+ }
+
+ return {
+ content: [
+ {
+ type: "text",
+ text: JSON.stringify({
+ message: "未找到精确匹配,以下是可能的匹配项:",
+ query: query,
+ possibleMatches: possibleMatches.map(recipe => ({
+ id: recipe.id,
+ name: recipe.name,
+ description: recipe.description,
+ category: recipe.category
+ }))
+ }, null, 2),
+ },
+ ],
+ };
+ }
+
+ // 返回找到的完整菜谱信息
+ return {
+ content: [
+ {
+ type: "text",
+ text: JSON.stringify(foundRecipe, null, 2),
+ },
+ ],
+ };
+ }
+ );
+}
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/src/tools/getRecipesByCategory.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/src/tools/getRecipesByCategory.ts
new file mode 100644
index 00000000..88ac8c85
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/src/tools/getRecipesByCategory.ts
@@ -0,0 +1,28 @@
+import { z } from "zod";
+import { Recipe } from "../types/index.js";
+import { simplifyRecipe } from "../utils/recipeUtils.js";
+import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+
+export function registerGetRecipesByCategoryTool(server: McpServer, recipes: Recipe[], categories: string[]) {
+ server.tool(
+ "mcp_howtocook_getRecipesByCategory",
+ `根据分类查询菜谱,可选分类有: ${categories.join(', ')}`,
+ {
+ category: z.enum(categories as [string, ...string[]])
+ .describe('菜谱分类名称,如水产、早餐、荤菜、主食等')
+ },
+ async ({ category }: { category: string }) => {
+ const filteredRecipes = recipes.filter((recipe) => recipe.category === category);
+ // 返回简化版的菜谱数据
+ const simplifiedRecipes = filteredRecipes.map(simplifyRecipe);
+ return {
+ content: [
+ {
+ type: "text",
+ text: JSON.stringify(simplifiedRecipes, null, 2),
+ },
+ ],
+ };
+ }
+ );
+}
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/src/tools/recommendMeals.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/src/tools/recommendMeals.ts
new file mode 100644
index 00000000..c65a4a62
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/src/tools/recommendMeals.ts
@@ -0,0 +1,235 @@
+import { z } from "zod";
+import { Recipe, MealPlan, SimpleRecipe, DayPlan } from "../types/index.js";
+import { simplifyRecipe, processRecipeIngredients, categorizeIngredients } from "../utils/recipeUtils.js";
+import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+
+export function registerRecommendMealsTool(server: McpServer, recipes: Recipe[]) {
+ server.tool(
+ "mcp_howtocook_recommendMeals",
+ "根据用户的忌口、过敏原、人数智能推荐菜谱,创建一周的膳食计划以及大致的购物清单",
+ {
+ allergies: z.array(z.string()).optional()
+ .describe('过敏原列表,如["大蒜", "虾"]'),
+ avoidItems: z.array(z.string()).optional()
+ .describe('忌口食材列表,如["葱", "姜"]'),
+ peopleCount: z.number().int().min(1).max(10)
+ .describe('用餐人数,1-10之间的整数')
+ },
+ async ({ allergies = [], avoidItems = [], peopleCount }: {
+ allergies?: string[],
+ avoidItems?: string[],
+ peopleCount: number
+ }) => {
+ // 过滤掉含有忌口和过敏原的菜谱
+ const filteredRecipes = recipes.filter((recipe) => {
+ // 检查是否包含过敏原或忌口食材
+ const hasAllergiesOrAvoidItems = recipe.ingredients?.some((ingredient) => {
+ const name = ingredient.name?.toLowerCase() || '';
+ return allergies.some(allergy => name.includes(allergy.toLowerCase())) ||
+ avoidItems.some(item => name.includes(item.toLowerCase()));
+ });
+
+ return !hasAllergiesOrAvoidItems;
+ });
+
+ // 将菜谱按分类分组
+ const recipesByCategory: Record = {};
+ const targetCategories = ['水产', '早餐', '荤菜', '主食'];
+
+ filteredRecipes.forEach((recipe) => {
+ if (targetCategories.includes(recipe.category)) {
+ if (!recipesByCategory[recipe.category]) {
+ recipesByCategory[recipe.category] = [];
+ }
+ recipesByCategory[recipe.category].push(recipe);
+ }
+ });
+
+ // 创建每周膳食计划
+ const mealPlan: MealPlan = {
+ weekdays: [],
+ weekend: [],
+ groceryList: {
+ ingredients: [],
+ shoppingPlan: {
+ fresh: [],
+ pantry: [],
+ spices: [],
+ others: []
+ }
+ }
+ };
+
+ // 用于跟踪已经选择的菜谱,以便后续处理食材信息
+ const selectedRecipes: Recipe[] = [];
+
+ // 周一至周五
+ for (let i = 0; i < 5; i++) {
+ const dayPlan: DayPlan = {
+ day: ['周一', '周二', '周三', '周四', '周五'][i],
+ breakfast: [],
+ lunch: [],
+ dinner: []
+ };
+
+ // 早餐 - 根据人数推荐1-2个早餐菜单
+ const breakfastCount = Math.max(1, Math.ceil(peopleCount / 5));
+ for (let j = 0; j < breakfastCount && recipesByCategory['早餐'] && recipesByCategory['早餐'].length > 0; j++) {
+ const breakfastIndex = Math.floor(Math.random() * recipesByCategory['早餐'].length);
+ const selectedRecipe = recipesByCategory['早餐'][breakfastIndex];
+ selectedRecipes.push(selectedRecipe);
+ dayPlan.breakfast.push(simplifyRecipe(selectedRecipe));
+ // 避免重复,从候选列表中移除
+ recipesByCategory['早餐'] = recipesByCategory['早餐'].filter((_, idx) => idx !== breakfastIndex);
+ }
+
+ // 午餐和晚餐的菜谱数量,根据人数确定
+ const mealCount = Math.max(2, Math.ceil(peopleCount / 3));
+
+ // 午餐
+ for (let j = 0; j < mealCount; j++) {
+ // 随机选择菜系:主食、水产、蔬菜、荤菜等
+ const categories = ['主食', '水产', '荤菜', '素菜', '甜品'];
+ let selectedCategory = categories[Math.floor(Math.random() * categories.length)];
+
+ // 如果该分类没有菜谱或已用完,尝试其他分类
+ while (!recipesByCategory[selectedCategory] || recipesByCategory[selectedCategory].length === 0) {
+ selectedCategory = categories[Math.floor(Math.random() * categories.length)];
+ if (categories.every(cat => !recipesByCategory[cat] || recipesByCategory[cat].length === 0)) {
+ break; // 所有分类都没有可用菜谱,退出循环
+ }
+ }
+
+ if (recipesByCategory[selectedCategory] && recipesByCategory[selectedCategory].length > 0) {
+ const index = Math.floor(Math.random() * recipesByCategory[selectedCategory].length);
+ const selectedRecipe = recipesByCategory[selectedCategory][index];
+ selectedRecipes.push(selectedRecipe);
+ dayPlan.lunch.push(simplifyRecipe(selectedRecipe));
+ // 避免重复,从候选列表中移除
+ recipesByCategory[selectedCategory] = recipesByCategory[selectedCategory].filter((_, idx) => idx !== index);
+ }
+ }
+
+ // 晚餐
+ for (let j = 0; j < mealCount; j++) {
+ // 随机选择菜系,与午餐类似但可添加汤羹
+ const categories = ['主食', '水产', '荤菜', '素菜', '甜品', '汤羹'];
+ let selectedCategory = categories[Math.floor(Math.random() * categories.length)];
+
+ // 如果该分类没有菜谱或已用完,尝试其他分类
+ while (!recipesByCategory[selectedCategory] || recipesByCategory[selectedCategory].length === 0) {
+ selectedCategory = categories[Math.floor(Math.random() * categories.length)];
+ if (categories.every(cat => !recipesByCategory[cat] || recipesByCategory[cat].length === 0)) {
+ break; // 所有分类都没有可用菜谱,退出循环
+ }
+ }
+
+ if (recipesByCategory[selectedCategory] && recipesByCategory[selectedCategory].length > 0) {
+ const index = Math.floor(Math.random() * recipesByCategory[selectedCategory].length);
+ const selectedRecipe = recipesByCategory[selectedCategory][index];
+ selectedRecipes.push(selectedRecipe);
+ dayPlan.dinner.push(simplifyRecipe(selectedRecipe));
+ // 避免重复,从候选列表中移除
+ recipesByCategory[selectedCategory] = recipesByCategory[selectedCategory].filter((_, idx) => idx !== index);
+ }
+ }
+
+ mealPlan.weekdays.push(dayPlan);
+ }
+
+ // 周六和周日
+ for (let i = 0; i < 2; i++) {
+ const dayPlan: DayPlan = {
+ day: ['周六', '周日'][i],
+ breakfast: [],
+ lunch: [],
+ dinner: []
+ };
+
+ // 早餐 - 根据人数推荐菜品,至少2个菜品,随人数增加
+ const breakfastCount = Math.max(2, Math.ceil(peopleCount / 3));
+ for (let j = 0; j < breakfastCount && recipesByCategory['早餐'] && recipesByCategory['早餐'].length > 0; j++) {
+ const breakfastIndex = Math.floor(Math.random() * recipesByCategory['早餐'].length);
+ const selectedRecipe = recipesByCategory['早餐'][breakfastIndex];
+ selectedRecipes.push(selectedRecipe);
+ dayPlan.breakfast.push(simplifyRecipe(selectedRecipe));
+ recipesByCategory['早餐'] = recipesByCategory['早餐'].filter((_, idx) => idx !== breakfastIndex);
+ }
+
+ // 计算工作日的基础菜品数量
+ const weekdayMealCount = Math.max(2, Math.ceil(peopleCount / 3));
+ // 周末菜品数量:比工作日多1-2个菜,随人数增加
+ const weekendAddition = peopleCount <= 4 ? 1 : 2; // 4人以下多1个菜,4人以上多2个菜
+ const mealCount = weekdayMealCount + weekendAddition;
+
+ const getMeals = (count: number): SimpleRecipe[] => {
+ const result: SimpleRecipe[] = [];
+ const categories = ['荤菜', '水产'];
+
+ // 尽量平均分配不同分类的菜品
+ for (let j = 0; j < count; j++) {
+ const category = categories[j % categories.length];
+ if (recipesByCategory[category] && recipesByCategory[category].length > 0) {
+ const index = Math.floor(Math.random() * recipesByCategory[category].length);
+ const selectedRecipe = recipesByCategory[category][index];
+ selectedRecipes.push(selectedRecipe);
+ result.push(simplifyRecipe(selectedRecipe));
+ recipesByCategory[category] = recipesByCategory[category].filter((_, idx) => idx !== index);
+ } else if (recipesByCategory['主食'] && recipesByCategory['主食'].length > 0) {
+ // 如果没有足够的荤菜或水产,使用主食
+ const index = Math.floor(Math.random() * recipesByCategory['主食'].length);
+ const selectedRecipe = recipesByCategory['主食'][index];
+ selectedRecipes.push(selectedRecipe);
+ result.push(simplifyRecipe(selectedRecipe));
+ recipesByCategory['主食'] = recipesByCategory['主食'].filter((_, idx) => idx !== index);
+ }
+ }
+
+ return result;
+ };
+
+ dayPlan.lunch = getMeals(mealCount);
+ dayPlan.dinner = getMeals(mealCount);
+
+ mealPlan.weekend.push(dayPlan);
+ }
+
+ // 统计食材清单,收集所有菜谱的所有食材
+ const ingredientMap = new Map();
+
+ // 处理所有菜谱
+ selectedRecipes.forEach(recipe => processRecipeIngredients(recipe, ingredientMap));
+
+ // 整理食材清单
+ for (const [name, info] of ingredientMap.entries()) {
+ mealPlan.groceryList.ingredients.push({
+ name,
+ totalQuantity: info.totalQuantity,
+ unit: info.unit,
+ recipeCount: info.recipeCount,
+ recipes: info.recipes
+ });
+ }
+
+ // 对食材按使用频率排序
+ mealPlan.groceryList.ingredients.sort((a, b) => b.recipeCount - a.recipeCount);
+
+ // 生成购物计划,根据食材类型进行分类
+ categorizeIngredients(mealPlan.groceryList.ingredients, mealPlan.groceryList.shoppingPlan);
+
+ return {
+ content: [
+ {
+ type: "text",
+ text: JSON.stringify(mealPlan, null, 2),
+ },
+ ],
+ };
+ }
+ );
+}
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/src/tools/whatToEat.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/src/tools/whatToEat.ts
new file mode 100644
index 00000000..0ff8f38f
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/src/tools/whatToEat.ts
@@ -0,0 +1,113 @@
+import { z } from "zod";
+import { Recipe, DishRecommendation } from "../types/index.js";
+import { simplifyRecipe } from "../utils/recipeUtils.js";
+import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+
+export function registerWhatToEatTool(server: McpServer, recipes: Recipe[]) {
+ server.tool(
+ "mcp_howtocook_whatToEat",
+ "不知道吃什么?根据人数直接推荐适合的菜品组合",
+ {
+ peopleCount: z.number().int().min(1).max(10)
+ .describe('用餐人数,1-10之间的整数,会根据人数推荐合适数量的菜品')
+ },
+ async ({ peopleCount }: { peopleCount: number }) => {
+ // 根据人数计算荤素菜数量
+ const vegetableCount = Math.floor((peopleCount + 1) / 2);
+ const meatCount = Math.ceil((peopleCount + 1) / 2);
+
+ // 获取所有荤菜
+ let meatDishes = recipes.filter((recipe) =>
+ recipe.category === '荤菜' || recipe.category === '水产'
+ );
+
+ // 获取其他可能的菜品(当做素菜)
+ let vegetableDishes = recipes.filter((recipe) =>
+ recipe.category !== '荤菜' && recipe.category !== '水产' &&
+ recipe.category !== '早餐' && recipe.category !== '主食'
+ );
+
+ // 特别处理:如果人数超过8人,增加鱼类荤菜
+ let recommendedDishes: Recipe[] = [];
+ let fishDish: Recipe | null = null;
+
+ if (peopleCount > 8) {
+ const fishDishes = recipes.filter((recipe) => recipe.category === '水产');
+ if (fishDishes.length > 0) {
+ fishDish = fishDishes[Math.floor(Math.random() * fishDishes.length)];
+ recommendedDishes.push(fishDish);
+ }
+ }
+
+ // 打乱肉类优先级顺序,增加随机性
+ const meatTypes = ['猪肉', '鸡肉', '牛肉', '羊肉', '鸭肉', '鱼肉'];
+ // 使用 Fisher-Yates 洗牌算法打乱数组
+ for (let i = meatTypes.length - 1; i > 0; i--) {
+ const j = Math.floor(Math.random() * (i + 1));
+ [meatTypes[i], meatTypes[j]] = [meatTypes[j], meatTypes[i]];
+ }
+
+ const selectedMeatDishes: Recipe[] = [];
+
+ // 需要选择的荤菜数量
+ const remainingMeatCount = fishDish ? meatCount - 1 : meatCount;
+
+ // 尝试按照随机化的肉类优先级选择荤菜
+ for (const meatType of meatTypes) {
+ if (selectedMeatDishes.length >= remainingMeatCount) break;
+
+ const meatTypeOptions = meatDishes.filter((dish) => {
+ // 检查菜品的材料是否包含这种肉类
+ return dish.ingredients?.some((ingredient) => {
+ const name = ingredient.name?.toLowerCase() || '';
+ return name.includes(meatType.toLowerCase());
+ });
+ });
+
+ if (meatTypeOptions.length > 0) {
+ // 随机选择一道这种肉类的菜
+ const selected = meatTypeOptions[Math.floor(Math.random() * meatTypeOptions.length)];
+ selectedMeatDishes.push(selected);
+ // 从可选列表中移除,避免重复选择
+ meatDishes = meatDishes.filter((dish) => dish.id !== selected.id);
+ }
+ }
+
+ // 如果通过肉类筛选的荤菜不够,随机选择剩余的
+ while (selectedMeatDishes.length < remainingMeatCount && meatDishes.length > 0) {
+ const randomIndex = Math.floor(Math.random() * meatDishes.length);
+ selectedMeatDishes.push(meatDishes[randomIndex]);
+ meatDishes.splice(randomIndex, 1);
+ }
+
+ // 随机选择素菜
+ const selectedVegetableDishes: Recipe[] = [];
+ while (selectedVegetableDishes.length < vegetableCount && vegetableDishes.length > 0) {
+ const randomIndex = Math.floor(Math.random() * vegetableDishes.length);
+ selectedVegetableDishes.push(vegetableDishes[randomIndex]);
+ vegetableDishes.splice(randomIndex, 1);
+ }
+
+ // 合并推荐菜单
+ recommendedDishes = recommendedDishes.concat(selectedMeatDishes, selectedVegetableDishes);
+
+ // 构建推荐结果
+ const recommendationDetails: DishRecommendation = {
+ peopleCount,
+ meatDishCount: selectedMeatDishes.length + (fishDish ? 1 : 0),
+ vegetableDishCount: selectedVegetableDishes.length,
+ dishes: recommendedDishes.map(simplifyRecipe),
+ message: `为${peopleCount}人推荐的菜品,包含${selectedMeatDishes.length + (fishDish ? 1 : 0)}个荤菜和${selectedVegetableDishes.length}个素菜。`
+ };
+
+ return {
+ content: [
+ {
+ type: "text",
+ text: JSON.stringify(recommendationDetails, null, 2),
+ },
+ ],
+ };
+ }
+ );
+}
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/src/types/index.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/src/types/index.ts
new file mode 100644
index 00000000..decbc89f
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/src/types/index.ts
@@ -0,0 +1,87 @@
+// 定义菜谱的类型接口
+export interface Ingredient {
+ name: string;
+ quantity: number | null;
+ unit: string | null;
+ text_quantity: string;
+ notes: string;
+}
+
+export interface Step {
+ step: number;
+ description: string;
+}
+
+export interface Recipe {
+ id: string;
+ name: string;
+ description: string;
+ source_path: string;
+ image_path: string | null;
+ category: string;
+ difficulty: number;
+ tags: string[];
+ servings: number;
+ ingredients: Ingredient[];
+ steps: Step[];
+ prep_time_minutes: number | null;
+ cook_time_minutes: number | null;
+ total_time_minutes: number | null;
+ additional_notes: string[];
+}
+
+// 添加简化版的Recipe接口,只包含id、name和description
+export interface SimpleRecipe {
+ id: string;
+ name: string;
+ description: string;
+ ingredients: {
+ name: string;
+ text_quantity: string;
+ }[];
+}
+
+// 更简化的Recipe接口,只包含name和description,用于getAllRecipes
+export interface NameOnlyRecipe {
+ name: string;
+ description: string;
+}
+
+// 定义膳食计划相关接口
+export interface MealPlan {
+ weekdays: Array;
+ weekend: Array;
+ groceryList: GroceryList;
+}
+
+export interface DayPlan {
+ day: string;
+ breakfast: SimpleRecipe[];
+ lunch: SimpleRecipe[];
+ dinner: SimpleRecipe[];
+}
+
+export interface GroceryList {
+ ingredients: Array<{
+ name: string;
+ totalQuantity: number | null;
+ unit: string | null;
+ recipeCount: number;
+ recipes: string[];
+ }>;
+ shoppingPlan: {
+ fresh: string[];
+ pantry: string[];
+ spices: string[];
+ others: string[];
+ };
+}
+
+// 定义推荐菜品的接口
+export interface DishRecommendation {
+ peopleCount: number;
+ meatDishCount: number;
+ vegetableDishCount: number;
+ dishes: SimpleRecipe[];
+ message: string;
+}
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/src/utils/recipeUtils.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/src/utils/recipeUtils.ts
new file mode 100644
index 00000000..bef44e69
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/src/utils/recipeUtils.ts
@@ -0,0 +1,91 @@
+import { Recipe, SimpleRecipe, NameOnlyRecipe, Ingredient } from '../types/index.js';
+
+// 创建简化版的Recipe数据
+export function simplifyRecipe(recipe: Recipe): SimpleRecipe {
+ return {
+ id: recipe.id,
+ name: recipe.name,
+ description: recipe.description,
+ ingredients: recipe.ingredients.map((ingredient: Ingredient) => ({
+ name: ingredient.name,
+ text_quantity: ingredient.text_quantity
+ }))
+ };
+}
+
+// 创建只包含name和description的Recipe数据
+export function simplifyRecipeNameOnly(recipe: Recipe): NameOnlyRecipe {
+ return {
+ name: recipe.name,
+ description: recipe.description
+ };
+}
+
+// 处理食材清单,收集菜谱的所有食材
+export function processRecipeIngredients(recipe: Recipe, ingredientMap: Map) {
+ recipe.ingredients?.forEach((ingredient: Ingredient) => {
+ const key = ingredient.name.toLowerCase();
+
+ if (!ingredientMap.has(key)) {
+ ingredientMap.set(key, {
+ totalQuantity: ingredient.quantity,
+ unit: ingredient.unit,
+ recipeCount: 1,
+ recipes: [recipe.name]
+ });
+ } else {
+ const existing = ingredientMap.get(key)!;
+
+ // 对于有明确数量和单位的食材,进行汇总
+ if (existing.unit && ingredient.unit && existing.unit === ingredient.unit && existing.totalQuantity !== null && ingredient.quantity !== null) {
+ existing.totalQuantity += ingredient.quantity;
+ } else {
+ // 否则保留 null,表示数量不确定
+ existing.totalQuantity = null;
+ existing.unit = null;
+ }
+
+ existing.recipeCount += 1;
+ if (!existing.recipes.includes(recipe.name)) {
+ existing.recipes.push(recipe.name);
+ }
+ }
+ });
+}
+
+// 根据食材类型进行分类
+export function categorizeIngredients(ingredients: Array<{
+ name: string,
+ totalQuantity: number | null,
+ unit: string | null,
+ recipeCount: number,
+ recipes: string[]
+}>, shoppingPlan: {
+ fresh: string[],
+ pantry: string[],
+ spices: string[],
+ others: string[]
+}) {
+ const spiceKeywords = ['盐', '糖', '酱油', '醋', '料酒', '香料', '胡椒', '孜然', '辣椒', '花椒', '姜', '蒜', '葱', '调味'];
+ const freshKeywords = ['肉', '鱼', '虾', '蛋', '奶', '菜', '菠菜', '白菜', '青菜', '豆腐', '生菜', '水产', '豆芽', '西红柿', '番茄', '水果', '香菇', '木耳', '蘑菇'];
+ const pantryKeywords = ['米', '面', '粉', '油', '酒', '醋', '糖', '盐', '酱', '豆', '干', '罐头', '方便面', '面条', '米饭', '意大利面', '燕麦'];
+
+ ingredients.forEach(ingredient => {
+ const name = ingredient.name.toLowerCase();
+
+ if (spiceKeywords.some(keyword => name.includes(keyword))) {
+ shoppingPlan.spices.push(ingredient.name);
+ } else if (freshKeywords.some(keyword => name.includes(keyword))) {
+ shoppingPlan.fresh.push(ingredient.name);
+ } else if (pantryKeywords.some(keyword => name.includes(keyword))) {
+ shoppingPlan.pantry.push(ingredient.name);
+ } else {
+ shoppingPlan.others.push(ingredient.name);
+ }
+ });
+}
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/tsconfig.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/tsconfig.json
new file mode 100644
index 00000000..d03b0e00
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/HowToCook-mcp/tsconfig.json
@@ -0,0 +1,18 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "NodeNext",
+ "moduleResolution": "NodeNext",
+ "outDir": "./build",
+ "rootDir": "./src",
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true,
+
+ "resolveJsonModule": true,
+ "allowSyntheticDefaultImports": true
+ },
+ "include": ["src/**/*"],
+ "exclude": ["node_modules"]
+}
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/.github/workflows/publish.yml b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/.github/workflows/publish.yml
new file mode 100644
index 00000000..f98890ff
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/.github/workflows/publish.yml
@@ -0,0 +1,47 @@
+name: Publish to PyPI
+
+on:
+ release:
+ types: [published]
+ workflow_dispatch:
+ inputs:
+ version:
+ description: 'Version to publish (leave empty to use pyproject.toml version)'
+ required: false
+ type: string
+
+jobs:
+ build-and-publish:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: '3.10'
+
+ - name: Install build dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install build twine
+
+ - name: Build package
+ run: python -m build
+
+ - name: Check package
+ run: twine check dist/*
+
+ - name: Publish to PyPI
+ env:
+ TWINE_USERNAME: __token__
+ TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
+ run: twine upload dist/*
+
+ - name: Upload build artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ name: dist
+ path: dist/
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/.gitignore b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/.gitignore
new file mode 100644
index 00000000..88b7cb8b
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/.gitignore
@@ -0,0 +1,14 @@
+# Python-generated files
+__pycache__/
+*.py[oc]
+build/
+dist/
+wheels/
+*.egg-info
+
+# Virtual environments
+.venv
+*.bak
+test/
+.DS_Store
+CLAUDE.md
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/Dockerfile b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/Dockerfile
new file mode 100644
index 00000000..87fc3705
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/Dockerfile
@@ -0,0 +1,19 @@
+# Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile
+FROM python:3.10-alpine
+
+# Install system dependencies (if any required, e.g., for pillow)
+RUN apk add --no-cache gcc musl-dev libffi-dev
+
+# Set work directory
+WORKDIR /app
+
+# Copy the application code
+COPY . .
+
+# Install Python dependencies
+RUN pip install --no-cache-dir -r requirements.txt
+
+# Expose port if needed (not needed for stdio)
+
+# Set the entrypoint to run the MCP server
+ENTRYPOINT ["python", "ppt_mcp_server.py"]
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/LICENSE b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/LICENSE
new file mode 100644
index 00000000..31323d1d
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2025 GongRzhe
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/README.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/README.md
new file mode 100644
index 00000000..a6e104a1
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/README.md
@@ -0,0 +1,986 @@
+# Office-PowerPoint-MCP-Server
+[](https://smithery.ai/server/@GongRzhe/Office-PowerPoint-MCP-Server)
+
+
+A comprehensive MCP (Model Context Protocol) server for PowerPoint manipulation using python-pptx. **Version 2.0** provides 32 powerful tools organized into 11 specialized modules, offering complete PowerPoint creation, management, and professional design capabilities. The server features a modular architecture with enhanced parameter handling, intelligent operation selection, and comprehensive error handling.
+
+----
+
+# **Not so ugly anymore with new slide_layout_templates**
+
+
+
+----
+
+### Example
+
+#### Prompt
+
+
+
+#### Output
+
+
+
+#### Demo's GIF -> (./public/demo.mp4)
+
+
+
+## Features
+
+### Core PowerPoint Operations
+- **Round-trip support** for any Open XML presentation (.pptx file) including all elements
+- **Template support** with automatic theme and layout preservation
+- **Multi-presentation management** with global state tracking
+- **Core document properties** management (title, subject, author, keywords, comments)
+
+### Content Creation & Management
+- **Slide management** with flexible layout selection
+- **Text manipulation** with placeholder population and bullet point creation
+- **Advanced text formatting** with font, color, alignment, and style controls
+- **Text validation** with automatic fit checking and optimization suggestions
+
+### Visual Elements
+- **Image handling** with file and base64 input support
+- **Image enhancement** using Pillow with brightness, contrast, saturation, and filter controls
+- **Professional image effects** including shadows, reflections, glows, and soft edges
+- **Shape creation** with 20+ auto shape types (rectangles, ovals, flowchart elements, etc.)
+- **Table creation** with advanced cell formatting and styling
+
+### Charts & Data Visualization
+- **Chart support** for column, bar, line, and pie charts
+- **Data series management** with categories and multiple series support
+- **Chart formatting** with legends, data labels, and titles
+
+### Professional Design Features
+- **4 professional color schemes** (Modern Blue, Corporate Gray, Elegant Green, Warm Red)
+- **Professional typography** with Segoe UI font family and size presets
+- **Theme application** with automatic styling across presentations
+- **Gradient backgrounds** with customizable directions and color schemes
+- **Slide enhancement** tools for existing content
+- **25 built-in slide templates** with dynamic sizing and visual effects
+- **Advanced template features** including auto-wrapping, dynamic font sizing, and professional animations
+
+### Advanced Features
+- **Font analysis and optimization** using FontTools
+- **Picture effects** with 9 different visual effects (shadow, reflection, glow, bevel, etc.)
+- **Comprehensive validation** with automatic error fixing
+- **Template search** with configurable directory paths
+- **Professional layout calculations** with margin and spacing management
+
+## Installation
+
+### Installing via Smithery
+
+To install PowerPoint Manipulation Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@GongRzhe/Office-PowerPoint-MCP-Server):
+
+```bash
+npx -y @smithery/cli install @GongRzhe/Office-PowerPoint-MCP-Server --client claude
+```
+
+### Prerequisites
+
+- Python 3.6 or higher (as specified in pyproject.toml)
+- pip package manager
+- Optional: uvx for package execution without local installation
+
+### Installation Options
+
+#### Option 1: Using the Setup Script (Recommended)
+
+The easiest way to set up the PowerPoint MCP Server is using the provided setup script, which automates the installation process:
+
+```bash
+python setup_mcp.py
+```
+
+This script will:
+- Check prerequisites
+- Offer installation options:
+ - Install from PyPI (recommended for most users)
+ - Set up local development environment
+- Install required dependencies
+- Generate the appropriate MCP configuration file
+- Provide instructions for integrating with Claude Desktop
+
+The script offers different paths based on your environment:
+- If you have `uvx` installed, it will configure using UVX (recommended)
+- If the server is already installed, it provides configuration options
+- If the server is not installed, it offers installation methods
+
+#### Option 2: Manual Installation
+
+1. Clone the repository:
+ ```bash
+ git clone https://github.com/GongRzhe/Office-PowerPoint-MCP-Server.git
+ cd Office-PowerPoint-MCP-Server
+ ```
+
+2. Install dependencies:
+ ```bash
+ pip install -r requirements.txt
+ ```
+
+3. Make the server executable:
+ ```bash
+ chmod +x ppt_mcp_server.py
+ ```
+
+## Usage
+
+Display help text:
+```bash
+python ppt_mcp_server.py -h
+```
+
+### Starting the Stdio Server
+
+Run the stdio server:
+
+```bash
+python ppt_mcp_server.py
+```
+
+### Starting the Streamable-Http Server
+
+Run the streamable-http server on port 8000:
+
+```bash
+python ppt_mcp_server.py --transport http --port 8000
+```
+
+Run in Docker
+```bash
+docker build -t ppt_mcp_server .
+docker run -d --rm -p 8000:8000 ppt_mcp_server -t http
+```
+
+
+### MCP Configuration
+
+#### Option 1: Local Python Server
+
+Add the server to your MCP settings configuration file:
+
+```json
+{
+ "mcpServers": {
+ "ppt": {
+ "command": "python",
+ "args": ["/path/to/ppt_mcp_server.py"],
+ "env": {}
+ }
+ }
+}
+```
+
+#### Option 2: Using UVX (No Local Installation Required)
+
+If you have `uvx` installed, you can run the server directly from PyPI without local installation:
+
+```json
+{
+ "mcpServers": {
+ "ppt": {
+ "command": "uvx",
+ "args": [
+ "--from", "office-powerpoint-mcp-server", "ppt_mcp_server"
+ ],
+ "env": {}
+ }
+ }
+}
+```
+
+## 🚀 What's New in v2.0
+
+### **Comprehensive Tool Suite (32 Tools)**
+- **Complete PowerPoint manipulation** with 34 specialized tools
+- **11 organized modules** covering all aspects of presentation creation
+- **Enhanced parameter handling** with comprehensive validation
+- **Intelligent defaults** and operation-based interfaces
+
+### **Built-in Slide Templates**
+- **25+ professional slide templates** with dynamic features built-in
+- **Advanced template system** with auto-generation capabilities
+- **Auto-sizing text** that adapts to content length and container size
+- **Professional visual effects** including shadows, glows, and gradients
+- **Complete presentation generation** from template sequences
+
+### **Modular Architecture**
+- **11 specialized modules**: presentation, content, structural, professional, template, hyperlink, chart, connector, master, and transition tools
+- **Better maintainability** with separated concerns
+- **Easier extensibility** for adding new features
+- **Cleaner code structure** with shared utilities
+
+## Available Tools
+
+The server provides **34 specialized tools** organized into the following categories:
+
+### **Presentation Management (7 tools)**
+1. **create_presentation** - Create new presentations
+2. **create_presentation_from_template** - Create from templates with theme preservation
+3. **open_presentation** - Open existing presentations
+4. **save_presentation** - Save presentations to files
+5. **get_presentation_info** - Get comprehensive presentation information
+6. **get_template_file_info** - Analyze template files and layouts
+7. **set_core_properties** - Set document properties
+
+### **Content Management (8 tools)**
+8. **add_slide** - Add slides with optional background styling
+9. **get_slide_info** - Get detailed slide information
+10. **extract_slide_text** - ✨ **NEW** Extract all text content from a specific slide
+11. **extract_presentation_text** - ✨ **NEW** Extract text content from all slides in presentation
+12. **populate_placeholder** - Populate placeholders with text
+13. **add_bullet_points** - Add formatted bullet points
+14. **manage_text** - ✨ **Unified text tool** (add/format/validate/format_runs)
+15. **manage_image** - ✨ **Unified image tool** (add/enhance)
+
+### **Template Operations (7 tools)**
+16. **list_slide_templates** - Browse available slide layout templates
+17. **apply_slide_template** - Apply structured layout templates to existing slides
+18. **create_slide_from_template** - Create new slides using layout templates
+19. **create_presentation_from_templates** - Create complete presentations from template sequences
+20. **get_template_info** - Get detailed information about specific templates
+21. **auto_generate_presentation** - Automatically generate presentations based on topic
+22. **optimize_slide_text** - Optimize text elements for better readability and fit
+
+### **Structural Elements (4 tools)**
+23. **add_table** - Create tables with enhanced formatting
+24. **format_table_cell** - Format individual table cells
+25. **add_shape** - Add shapes with text and formatting options
+26. **add_chart** - Create charts with comprehensive customization
+
+### **Professional Design (3 tools)**
+27. **apply_professional_design** - ✨ **Unified design tool** (themes/slides/enhancement)
+28. **apply_picture_effects** - ✨ **Unified effects tool** (9+ effects combined)
+29. **manage_fonts** - ✨ **Unified font tool** (analyze/optimize/recommend)
+
+### **Specialized Features (5 tools)**
+30. **manage_hyperlinks** - Complete hyperlink management (add/remove/list/update)
+31. **manage_slide_masters** - Access and manage slide master properties and layouts
+32. **add_connector** - Add connector lines/arrows between points on slides
+33. **update_chart_data** - Replace existing chart data with new categories and series
+34. **manage_slide_transitions** - Basic slide transition management
+
+## 🌟 Key Unified Tools
+
+### **`manage_text`** - All-in-One Text Management
+```python
+# Add text box
+manage_text(slide_index=0, operation="add", text="Hello World", font_size=24)
+
+# Format existing text
+manage_text(slide_index=0, operation="format", shape_index=0, bold=True, color=[255,0,0])
+
+# Validate text fit with auto-fix
+manage_text(slide_index=0, operation="validate", shape_index=0, validation_only=False)
+```
+
+### **`manage_image`** - Complete Image Handling
+```python
+# Add image with enhancement
+manage_image(slide_index=0, operation="add", image_source="logo.png",
+ enhancement_style="presentation")
+
+# Enhance existing image
+manage_image(slide_index=0, operation="enhance", image_source="photo.jpg",
+ brightness=1.2, contrast=1.1, saturation=1.3)
+```
+
+### **`apply_picture_effects`** - Multiple Effects in One Call
+```python
+# Apply combined effects
+apply_picture_effects(slide_index=0, shape_index=0, effects={
+ "shadow": {"blur_radius": 4.0, "color": [128,128,128]},
+ "glow": {"size": 5.0, "color": [0,176,240]},
+ "rotation": {"rotation": 15.0}
+})
+```
+
+### **`apply_professional_design`** - Theme & Design Management
+```python
+# Add professional slide
+apply_professional_design(operation="slide", slide_type="title_content",
+ color_scheme="modern_blue", title="My Presentation")
+
+# Apply theme to entire presentation
+apply_professional_design(operation="theme", color_scheme="corporate_gray")
+
+# Enhance existing slide
+apply_professional_design(operation="enhance", slide_index=0, color_scheme="elegant_green")
+```
+
+## Examples
+
+### Creating a New Presentation
+
+```python
+# Create a new presentation
+result = use_mcp_tool(
+ server_name="ppt",
+ tool_name="create_presentation",
+ arguments={}
+)
+presentation_id = result["presentation_id"]
+
+# Add a title slide
+result = use_mcp_tool(
+ server_name="ppt",
+ tool_name="add_slide",
+ arguments={
+ "layout_index": 0, # Title slide layout
+ "title": "My Presentation",
+ "presentation_id": presentation_id
+ }
+)
+slide_index = result["slide_index"]
+
+# Populate subtitle placeholder
+result = use_mcp_tool(
+ server_name="ppt",
+ tool_name="populate_placeholder",
+ arguments={
+ "slide_index": slide_index,
+ "placeholder_idx": 1, # Subtitle placeholder
+ "text": "Created with PowerPoint MCP Server",
+ "presentation_id": presentation_id
+ }
+)
+
+# Save the presentation
+result = use_mcp_tool(
+ server_name="ppt",
+ tool_name="save_presentation",
+ arguments={
+ "file_path": "my_presentation.pptx",
+ "presentation_id": presentation_id
+ }
+)
+```
+
+### Creating a Professional Presentation with v2.0
+
+```python
+# Create a professional slide with modern styling - CONSOLIDATED TOOL
+result = use_mcp_tool(
+ server_name="ppt",
+ tool_name="apply_professional_design",
+ arguments={
+ "operation": "slide",
+ "slide_type": "title_content",
+ "color_scheme": "modern_blue",
+ "title": "Quarterly Business Review",
+ "content": [
+ "Revenue increased by 15% compared to last quarter",
+ "Customer satisfaction scores reached all-time high of 94%",
+ "Successfully launched 3 new product features",
+ "Expanded team by 12 new talented professionals"
+ ]
+ }
+)
+
+# Apply professional theme to entire presentation - SAME TOOL, DIFFERENT OPERATION
+result = use_mcp_tool(
+ server_name="ppt",
+ tool_name="apply_professional_design",
+ arguments={
+ "operation": "theme",
+ "color_scheme": "modern_blue",
+ "apply_to_existing": True
+ }
+)
+
+# Add slide with gradient background - ENHANCED ADD_SLIDE
+result = use_mcp_tool(
+ server_name="ppt",
+ tool_name="add_slide",
+ arguments={
+ "layout_index": 0,
+ "background_type": "professional_gradient",
+ "color_scheme": "modern_blue",
+ "gradient_direction": "diagonal"
+ }
+)
+```
+
+### Working with Built-in Slide Templates (New in v2.0)
+
+```python
+# List all available slide templates with their features
+result = use_mcp_tool(
+ server_name="ppt",
+ tool_name="list_slide_templates",
+ arguments={}
+)
+
+# Apply a professional template to an existing slide
+result = use_mcp_tool(
+ server_name="ppt",
+ tool_name="apply_slide_template",
+ arguments={
+ "slide_index": 0,
+ "template_id": "title_slide",
+ "color_scheme": "modern_blue",
+ "content_mapping": {
+ "title": "Quarterly Business Review",
+ "subtitle": "Q4 2024 Results",
+ "author": "Leadership Team"
+ }
+ }
+)
+
+# Create a new slide using a template
+result = use_mcp_tool(
+ server_name="ppt",
+ tool_name="create_slide_from_template",
+ arguments={
+ "template_id": "text_with_image",
+ "color_scheme": "elegant_green",
+ "content_mapping": {
+ "title": "Our Revolutionary Solution",
+ "content": "• 250% increase in efficiency\n• 98% customer satisfaction\n• Industry-leading performance"
+ },
+ "image_paths": {
+ "supporting": "path/to/product_image.jpg"
+ }
+ }
+)
+
+# Generate a complete presentation from multiple templates
+result = use_mcp_tool(
+ server_name="ppt",
+ tool_name="create_presentation_from_templates",
+ arguments={
+ "template_sequence": [
+ {
+ "template_id": "title_slide",
+ "content": {
+ "title": "2024 Annual Report",
+ "subtitle": "Growth and Innovation",
+ "author": "Executive Team"
+ }
+ },
+ {
+ "template_id": "key_metrics_dashboard",
+ "content": {
+ "metric_1_value": "94%",
+ "metric_2_value": "$2.4M",
+ "metric_3_value": "247"
+ }
+ },
+ {
+ "template_id": "before_after_comparison",
+ "content": {
+ "content_left": "Manual processes taking hours",
+ "content_right": "Automated workflows in minutes"
+ }
+ }
+ ],
+ "color_scheme": "modern_blue"
+ }
+)
+```
+
+### Enhanced Image Management with v2.0
+
+```python
+# Add image with automatic enhancement - CONSOLIDATED TOOL
+result = use_mcp_tool(
+ server_name="ppt",
+ tool_name="manage_image",
+ arguments={
+ "slide_index": 1,
+ "operation": "add",
+ "image_source": "company_logo.png",
+ "left": 1.0,
+ "top": 1.0,
+ "width": 3.0,
+ "height": 2.0,
+ "enhancement_style": "presentation"
+ }
+)
+
+# Apply multiple picture effects at once - CONSOLIDATED TOOL
+result = use_mcp_tool(
+ server_name="ppt",
+ tool_name="apply_picture_effects",
+ arguments={
+ "slide_index": 1,
+ "shape_index": 0,
+ "effects": {
+ "shadow": {
+ "shadow_type": "outer",
+ "blur_radius": 4.0,
+ "distance": 3.0,
+ "direction": 315.0,
+ "color": [128, 128, 128],
+ "transparency": 0.6
+ },
+ "glow": {
+ "size": 5.0,
+ "color": [0, 176, 240],
+ "transparency": 0.4
+ }
+ }
+ }
+)
+```
+
+### Advanced Text Management with v2.0
+
+```python
+# Add and format text in one operation - CONSOLIDATED TOOL
+result = use_mcp_tool(
+ server_name="ppt",
+ tool_name="manage_text",
+ arguments={
+ "slide_index": 0,
+ "operation": "add",
+ "left": 1.0,
+ "top": 2.0,
+ "width": 8.0,
+ "height": 1.5,
+ "text": "Welcome to Our Quarterly Review",
+ "font_size": 32,
+ "font_name": "Segoe UI",
+ "bold": True,
+ "color": [0, 120, 215],
+ "alignment": "center",
+ "auto_fit": True
+ }
+)
+
+# Validate and fix text fit issues - SAME TOOL, DIFFERENT OPERATION
+result = use_mcp_tool(
+ server_name="ppt",
+ tool_name="manage_text",
+ arguments={
+ "slide_index": 0,
+ "operation": "validate",
+ "shape_index": 0,
+ "validation_only": False, # Auto-fix enabled
+ "min_font_size": 10,
+ "max_font_size": 48
+ }
+)
+```
+
+### Creating a Presentation from Template
+
+```python
+# First, inspect a template to see its layouts and properties
+result = use_mcp_tool(
+ server_name="ppt",
+ tool_name="get_template_info",
+ arguments={
+ "template_path": "company_template.pptx"
+ }
+)
+template_info = result
+
+# Create a new presentation from the template
+result = use_mcp_tool(
+ server_name="ppt",
+ tool_name="create_presentation_from_template",
+ arguments={
+ "template_path": "company_template.pptx"
+ }
+)
+presentation_id = result["presentation_id"]
+
+# Add a slide using one of the template's layouts
+result = use_mcp_tool(
+ server_name="ppt",
+ tool_name="add_slide",
+ arguments={
+ "layout_index": 1, # Use layout from template
+ "title": "Quarterly Report",
+ "presentation_id": presentation_id
+ }
+)
+
+# Save the presentation
+result = use_mcp_tool(
+ server_name="ppt",
+ tool_name="save_presentation",
+ arguments={
+ "file_path": "quarterly_report.pptx",
+ "presentation_id": presentation_id
+ }
+)
+```
+
+### Adding Advanced Charts and Data Visualization
+
+```python
+# Add a chart slide
+result = use_mcp_tool(
+ server_name="ppt",
+ tool_name="add_slide",
+ arguments={
+ "layout_index": 1, # Content slide layout
+ "title": "Sales Data",
+ "presentation_id": presentation_id
+ }
+)
+slide_index = result["slide_index"]
+
+# Add a column chart with comprehensive customization
+result = use_mcp_tool(
+ server_name="ppt",
+ tool_name="add_chart",
+ arguments={
+ "slide_index": slide_index,
+ "chart_type": "column",
+ "left": 1.0,
+ "top": 2.0,
+ "width": 8.0,
+ "height": 4.5,
+ "categories": ["Q1", "Q2", "Q3", "Q4"],
+ "series_names": ["2023", "2024"],
+ "series_values": [
+ [100, 120, 140, 160],
+ [110, 130, 150, 170]
+ ],
+ "has_legend": True,
+ "legend_position": "bottom",
+ "has_data_labels": True,
+ "title": "Quarterly Sales Performance",
+ "presentation_id": presentation_id
+ }
+)
+```
+
+### Text Validation and Optimization with v2.0
+
+```python
+# Validate text fit and get optimization suggestions - USING CONSOLIDATED TOOL
+result = use_mcp_tool(
+ server_name="ppt",
+ tool_name="manage_text",
+ arguments={
+ "slide_index": 0,
+ "operation": "validate",
+ "shape_index": 0,
+ "text": "This is a very long title that might not fit properly in the designated text box area",
+ "font_size": 24,
+ "validation_only": True
+ }
+)
+
+# Comprehensive slide validation with automatic fixes - SAME TOOL, AUTO-FIX ENABLED
+result = use_mcp_tool(
+ server_name="ppt",
+ tool_name="manage_text",
+ arguments={
+ "slide_index": 0,
+ "operation": "validate",
+ "shape_index": 0,
+ "validation_only": False, # Auto-fix enabled
+ "min_font_size": 10,
+ "max_font_size": 48
+ }
+)
+```
+
+### Reading Slide Content with New Text Extraction Tools (v2.1)
+
+```python
+# Extract text content from a specific slide - NEW TOOL
+result = use_mcp_tool(
+ server_name="ppt",
+ tool_name="extract_slide_text",
+ arguments={
+ "slide_index": 0,
+ "presentation_id": presentation_id
+ }
+)
+
+# The result includes:
+{
+ "success": True,
+ "slide_index": 0,
+ "text_content": {
+ "slide_title": "Quarterly Business Review",
+ "placeholders": [
+ {
+ "shape_index": 1,
+ "shape_name": "Subtitle Placeholder 2",
+ "text": "Q4 2024 Results",
+ "placeholder_type": "SUBTITLE",
+ "placeholder_idx": 1
+ }
+ ],
+ "text_shapes": [
+ {
+ "shape_index": 3,
+ "shape_name": "TextBox 4",
+ "text": "Revenue increased by 15%"
+ }
+ ],
+ "table_text": [],
+ "all_text_combined": "Quarterly Business Review\nQ4 2024 Results\nRevenue increased by 15%"
+ },
+ "total_text_shapes": 2,
+ "has_title": True,
+ "has_tables": False
+}
+
+# Extract text from all slides in the presentation - NEW TOOL
+result = use_mcp_tool(
+ server_name="ppt",
+ tool_name="extract_presentation_text",
+ arguments={
+ "presentation_id": presentation_id,
+ "include_slide_info": True
+ }
+)
+
+# The result includes comprehensive text extraction:
+{
+ "success": True,
+ "presentation_id": "pres_123",
+ "total_slides": 5,
+ "slides_with_text": 4,
+ "total_text_shapes": 12,
+ "slides_with_titles": 3,
+ "slides_with_tables": 1,
+ "slides_text": [...], # Detailed per-slide text content
+ "all_presentation_text_combined": "=== SLIDE 1 ===\nTitle Here\nContent here..."
+}
+
+# Extract text without additional slide metadata for cleaner output
+result = use_mcp_tool(
+ server_name="ppt",
+ tool_name="extract_presentation_text",
+ arguments={
+ "presentation_id": presentation_id,
+ "include_slide_info": False
+ }
+)
+```
+
+## Template Support
+
+### Working with Templates
+
+The PowerPoint MCP Server provides comprehensive template support for creating presentations from existing template files. This feature enables:
+
+- **Corporate branding** with predefined themes, layouts, and styles
+- **Consistent presentations** across teams and projects
+- **Custom slide masters** and specialized layouts
+- **Pre-configured properties** and document settings
+- **Flexible template discovery** with configurable search paths
+
+### Template File Requirements
+
+- **Supported formats**: `.pptx` and `.potx` files
+- **Existing content**: Templates can contain existing slides (preserved during creation)
+- **Layout availability**: All custom layouts and slide masters are accessible
+- **Search locations**: Configurable via `PPT_TEMPLATE_PATH` environment variable
+- **Default search paths**: Current directory, `./templates`, `./assets`, `./resources`
+
+### Template Configuration
+
+Set the `PPT_TEMPLATE_PATH` environment variable to specify custom template directories:
+
+```bash
+# Unix/Linux/macOS
+export PPT_TEMPLATE_PATH="/path/to/templates:/another/path"
+
+# Windows
+set PPT_TEMPLATE_PATH="C:\templates;C:\company_templates"
+```
+
+### Template Workflow
+
+1. **Inspect Template**: Use `get_template_info` to analyze available layouts and properties
+2. **Create from Template**: Use `create_presentation_from_template` with automatic theme preservation
+3. **Use Template Layouts**: Reference layout indices from template analysis when adding slides
+4. **Maintain Branding**: Template themes, fonts, and colors are automatically applied to new content
+
+### Professional Color Schemes
+
+The server includes 4 built-in professional color schemes:
+- **Modern Blue**: Microsoft-inspired blue theme with complementary colors
+- **Corporate Gray**: Professional grayscale theme with blue accents
+- **Elegant Green**: Forest green theme with cream and light green accents
+- **Warm Red**: Deep red theme with orange and yellow accents
+
+Each scheme includes primary, secondary, accent, light, and text colors optimized for business presentations.
+
+## 🎨 Built-in Slide Templates (New in v2.0)
+
+The PowerPoint MCP Server now includes **25 professional slide templates** with advanced dynamic features. All templates support:
+
+### **Dynamic Features**
+- **Automatic text sizing** based on content length and container dimensions
+- **Intelligent text wrapping** to fit within specified areas
+- **Visual effects** including shadows, glows, and outlines
+- **Gradient backgrounds** with multi-layer compositions
+- **Professional animations** ready for presentation delivery
+- **Interactive hover effects** for enhanced user experience
+- **Smart content overflow handling** with automatic adjustments
+
+### **Available Template Categories**
+
+#### **Title & Introduction Slides**
+- `title_slide` - Dynamic title slide with gradient background and text effects
+- `chapter_intro` - Section divider with chapter numbering and styling
+- `thank_you_slide` - Closing slide with contact information and effects
+
+#### **Content Layout Slides**
+- `text_with_image` - Text content with stylized image and interactive elements
+- `two_column_text` - Two equal columns of text with dynamic sizing
+- `two_column_text_images` - Two columns with text and corresponding images
+- `three_column_layout` - Three equal columns with text and images
+- `full_image_slide` - Large background image with text overlay
+
+#### **Business & Analytics Slides**
+- `key_metrics_dashboard` - Interactive metrics dashboard with animated counters
+- `before_after_comparison` - Dynamic comparison layout with visual dividers
+- `chart_comparison` - Two charts side by side for performance comparison
+- `data_table_slide` - Slide focused on tabular data with professional styling
+- `timeline_slide` - Horizontal timeline with milestones and effects
+
+#### **Process & Flow Slides**
+- `process_flow` - Step-by-step process visualization with enhanced effects
+- `agenda_slide` - Table of contents or agenda overview with styling
+- `quote_testimonial` - Featured quote or customer testimonial with effects
+
+#### **Team & Organization Slides**
+- `team_introduction` - Team member showcase with photos and roles
+
+### **Template Usage Examples**
+
+```python
+# Browse all available templates
+templates = use_mcp_tool("ppt", "list_slide_templates", {})
+
+# Key templates with their features:
+{
+ "title_slide": {
+ "features": ["Dynamic text sizing", "Gradient backgrounds", "Text effects"],
+ "elements": ["title", "subtitle", "author", "decorative_accent"]
+ },
+ "key_metrics_dashboard": {
+ "features": ["Animated counters", "Gradient containers", "Trend visualization"],
+ "elements": ["3 metric containers", "trend chart", "insights callout"]
+ },
+ "before_after_comparison": {
+ "features": ["Split gradient background", "VS divider", "Improvement arrow"],
+ "elements": ["before/after headers", "comparison content", "improvement metrics"]
+ }
+}
+```
+
+### **Color Scheme Integration**
+All templates work seamlessly with the 4 professional color schemes:
+- **modern_blue**: Microsoft-inspired theme with dynamic gradients
+- **corporate_gray**: Professional grayscale with blue accents
+- **elegant_green**: Forest green with cream and light accents
+- **warm_red**: Deep red with orange and yellow highlights
+
+### **Dynamic Content Adaptation**
+Templates automatically adjust to content:
+- **Font sizes** scale based on text length (8pt - 44pt range)
+- **Line spacing** adjusts for readability (1.0x - 1.4x)
+- **Text wrapping** intelligently breaks lines at optimal points
+- **Container sizing** adapts to content overflow
+- **Visual effects** scale appropriately with element sizes
+
+## 📁 File Structure
+
+```
+Office-PowerPoint-MCP-Server/
+├── ppt_mcp_server.py # Main consolidated server (v2.0)
+├── slide_layout_templates.json # 25+ professional slide templates with dynamic features
+├── tools/ # 11 specialized tool modules (32 tools total)
+│ ├── __init__.py
+│ ├── presentation_tools.py # Presentation management (7 tools)
+│ ├── content_tools.py # Content & slides (6 tools)
+│ ├── template_tools.py # Template operations (7 tools)
+│ ├── structural_tools.py # Tables, shapes, charts (4 tools)
+│ ├── professional_tools.py # Themes, effects, fonts (3 tools)
+│ ├── hyperlink_tools.py # Hyperlink management (1 tool)
+│ ├── chart_tools.py # Advanced chart operations (1 tool)
+│ ├── connector_tools.py # Connector lines/arrows (1 tool)
+│ ├── master_tools.py # Slide master management (1 tool)
+│ └── transition_tools.py # Slide transitions (1 tool)
+├── utils/ # 7 organized utility modules (68+ functions)
+│ ├── __init__.py
+│ ├── core_utils.py # Error handling & safe operations
+│ ├── presentation_utils.py # Presentation management utilities
+│ ├── content_utils.py # Content & slide operations
+│ ├── design_utils.py # Themes, colors, effects & fonts
+│ ├── template_utils.py # Template management & dynamic features
+│ └── validation_utils.py # Text & layout validation
+├── setup_mcp.py # Interactive setup script
+├── pyproject.toml # Updated for v2.0
+└── README.md # This documentation
+```
+
+## 🏗️ Architecture Benefits
+
+### **Modular Design**
+- **7 focused utility modules** with clear responsibilities
+- **11 organized tool modules** for comprehensive coverage
+- **68+ utility functions** organized by functionality
+- **32 MCP tools** covering all PowerPoint manipulation needs
+- **Clear separation of concerns** for easier development
+
+### **Code Organization**
+- **Logical grouping** of related functionality across modules
+- **Better discoverability** with organized tool categories
+- **Improved testability** with isolated modules
+- **Future extensibility** through modular structure
+
+### **Comprehensive Coverage**
+- **Complete PowerPoint lifecycle** from creation to presentation
+- **Advanced template system** with auto-generation capabilities
+- **Professional design tools** with multiple effects and styling options
+- **Specialized features** including hyperlinks, connectors, and slide masters
+
+### **Developer Experience**
+- **Clear responsibility boundaries** between modules
+- **Easier debugging** with smaller, focused files
+- **Simpler testing** with isolated functionality
+- **Enhanced maintainability** through separation of concerns
+
+## 🔄 What's New in Version 2.0
+
+**Enhanced functionality with comprehensive tool coverage!** The updated server provides:
+
+### **New Specialized Tools Added:**
+- **`manage_hyperlinks`** - Complete hyperlink management for text elements
+- **`update_chart_data`** - Advanced chart data replacement and updating
+- **`add_connector`** - Connector lines and arrows between slide elements
+- **`manage_slide_masters`** - Access to slide master properties and layouts
+- **`manage_slide_transitions`** - Basic slide transition management
+- **`auto_generate_presentation`** - AI-powered presentation generation
+- **`optimize_slide_text`** - Text optimization for better readability
+
+### **Enhanced Existing Tools:**
+- **`manage_text`** - Now supports text run formatting with `format_runs` operation
+- **`create_presentation_from_templates`** - Enhanced template sequence processing
+- **`apply_picture_effects`** - Expanded effect combinations and options
+
+## 🔄 What's New in Version 2.1
+
+**Text extraction capabilities added!** Now you can read content from existing presentations:
+
+### **New Text Extraction Tools Added:**
+- **`extract_slide_text`** - Extract all text content from a specific slide including titles, placeholders, text shapes, and tables
+- **`extract_presentation_text`** - Extract text content from all slides in a presentation with comprehensive statistics and combined output
+
+### **Key Features of Text Extraction:**
+- **Complete text coverage** - Extracts from titles, placeholders, text boxes, and table cells
+- **Structured output** - Organized by content type (titles, placeholders, shapes, tables)
+- **Presentation-wide analysis** - Statistics on text distribution across slides
+- **Flexible output options** - Individual slide content or combined presentation text
+- **Error handling** - Graceful handling of slides that cannot be processed
+
+## License
+
+MIT
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/__init__.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/__init__.py
new file mode 100644
index 00000000..085298ed
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/__init__.py
@@ -0,0 +1 @@
+# PowerPoint MCP Server
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/mcp-config.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/mcp-config.json
new file mode 100644
index 00000000..102b316d
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/mcp-config.json
@@ -0,0 +1,14 @@
+{
+ "mcpServers": {
+ "ppt": {
+ "command": "/Users/gongzhe/GitRepos/Office-PowerPoint-MCP-Server/.venv/bin/python",
+ "args": [
+ "/Users/gongzhe/GitRepos/Office-PowerPoint-MCP-Server/ppt_mcp_server.py"
+ ],
+ "env": {
+ "PYTHONPATH": "/Users/gongzhe/GitRepos/Office-PowerPoint-MCP-Server",
+ "PPT_TEMPLATE_PATH": "/Users/gongzhe/GitRepos/Office-PowerPoint-MCP-Server/templates"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/mcp_all_tools_templates_effects_demo.pptx b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/mcp_all_tools_templates_effects_demo.pptx
new file mode 100644
index 00000000..521f92df
Binary files /dev/null and b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/mcp_all_tools_templates_effects_demo.pptx differ
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/mcp_config_sample.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/mcp_config_sample.json
new file mode 100644
index 00000000..59b7652e
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/mcp_config_sample.json
@@ -0,0 +1,13 @@
+{
+ "mcpServers": {
+ "word-document-server": {
+ "command": "D:\\BackDataService\\Office-Word-MCP-Server\\.venv\\Scripts\\python.exe",
+ "args": [
+ "D:\\BackDataService\\Office-Word-MCP-Server\\word_server.py"
+ ],
+ "env": {
+ "PYTHONPATH": "D:\\BackDataService\\Office-Word-MCP-Server"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/ppt_mcp_server.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/ppt_mcp_server.py
new file mode 100644
index 00000000..cad383bd
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/ppt_mcp_server.py
@@ -0,0 +1,450 @@
+#!/usr/bin/env python
+"""
+MCP Server for PowerPoint manipulation using python-pptx.
+Consolidated version with 20 tools organized into multiple modules.
+"""
+import os
+import argparse
+from typing import Dict, Any
+from mcp.server.fastmcp import FastMCP
+
+# import utils # Currently unused
+from tools import (
+ register_presentation_tools,
+ register_content_tools,
+ register_structural_tools,
+ register_professional_tools,
+ register_template_tools,
+ register_hyperlink_tools,
+ register_chart_tools,
+ register_connector_tools,
+ register_master_tools,
+ register_transition_tools
+)
+
+# Initialize the FastMCP server
+app = FastMCP(
+ name="ppt-mcp-server"
+)
+
+# Global state to store presentations in memory
+presentations = {}
+current_presentation_id = None
+
+# Template configuration
+def get_template_search_directories():
+ """
+ Get list of directories to search for templates.
+ Uses environment variable PPT_TEMPLATE_PATH if set, otherwise uses default directories.
+
+ Returns:
+ List of directories to search for templates
+ """
+ template_env_path = os.environ.get('PPT_TEMPLATE_PATH')
+
+ if template_env_path:
+ # If environment variable is set, use it as the primary template directory
+ # Support multiple paths separated by colon (Unix) or semicolon (Windows)
+ import platform
+ separator = ';' if platform.system() == "Windows" else ':'
+ env_dirs = [path.strip() for path in template_env_path.split(separator) if path.strip()]
+
+ # Verify that the directories exist
+ valid_env_dirs = []
+ for dir_path in env_dirs:
+ expanded_path = os.path.expanduser(dir_path)
+ if os.path.exists(expanded_path) and os.path.isdir(expanded_path):
+ valid_env_dirs.append(expanded_path)
+
+ if valid_env_dirs:
+ # Add default fallback directories
+ return valid_env_dirs + ['.', './templates', './assets', './resources']
+ else:
+ print(f"Warning: PPT_TEMPLATE_PATH directories not found: {template_env_path}")
+
+ # Default search directories when no environment variable or invalid paths
+ return ['.', './templates', './assets', './resources']
+
+# ---- Helper Functions ----
+
+def get_current_presentation():
+ """Get the current presentation object or raise an error if none is loaded."""
+ if current_presentation_id is None or current_presentation_id not in presentations:
+ raise ValueError("No presentation is currently loaded. Please create or open a presentation first.")
+ return presentations[current_presentation_id]
+
+def get_current_presentation_id():
+ """Get the current presentation ID."""
+ return current_presentation_id
+
+def set_current_presentation_id(pres_id):
+ """Set the current presentation ID."""
+ global current_presentation_id
+ current_presentation_id = pres_id
+
+def validate_parameters(params):
+ """
+ Validate parameters against constraints.
+
+ Args:
+ params: Dictionary of parameter name: (value, constraints) pairs
+
+ Returns:
+ (True, None) if all valid, or (False, error_message) if invalid
+ """
+ for param_name, (value, constraints) in params.items():
+ for constraint_func, error_msg in constraints:
+ if not constraint_func(value):
+ return False, f"Parameter '{param_name}': {error_msg}"
+ return True, None
+
+def is_positive(value):
+ """Check if a value is positive."""
+ return value > 0
+
+def is_non_negative(value):
+ """Check if a value is non-negative."""
+ return value >= 0
+
+def is_in_range(min_val, max_val):
+ """Create a function that checks if a value is in a range."""
+ return lambda x: min_val <= x <= max_val
+
+def is_in_list(valid_list):
+ """Create a function that checks if a value is in a list."""
+ return lambda x: x in valid_list
+
+def is_valid_rgb(color_list):
+ """Check if a color list is a valid RGB tuple."""
+ if not isinstance(color_list, list) or len(color_list) != 3:
+ return False
+ return all(isinstance(c, int) and 0 <= c <= 255 for c in color_list)
+
+def add_shape_direct(slide, shape_type: str, left: float, top: float, width: float, height: float) -> Any:
+ """
+ Add an auto shape to a slide using direct integer values instead of enum objects.
+
+ This implementation provides a reliable alternative that bypasses potential
+ enum-related issues in the python-pptx library.
+
+ Args:
+ slide: The slide object
+ shape_type: Shape type string (e.g., 'rectangle', 'oval', 'triangle')
+ left: Left position in inches
+ top: Top position in inches
+ width: Width in inches
+ height: Height in inches
+
+ Returns:
+ The created shape
+ """
+ from pptx.util import Inches
+
+ # Direct mapping of shape types to their integer values
+ # Values from MSO_AUTO_SHAPE_TYPE enum: https://github.com/scanny/python-pptx/blob/master/src/pptx/enum/shapes.py
+ shape_type_map = {
+ 'rectangle': 1, # RECTANGLE
+ 'rounded_rectangle': 5, # ROUNDED_RECTANGLE
+ 'oval': 9, # OVAL
+ 'diamond': 4, # DIAMOND
+ 'triangle': 7, # ISOSCELES_TRIANGLE
+ 'right_triangle': 8, # RIGHT_TRIANGLE
+ 'pentagon': 51, # PENTAGON
+ 'hexagon': 10, # HEXAGON
+ 'heptagon': 145, # HEPTAGON
+ 'octagon': 6, # OCTAGON
+ 'star': 92, # STAR_5_POINT
+ 'arrow': 33, # RIGHT_ARROW
+ 'cloud': 179, # CLOUD
+ 'heart': 21, # HEART
+ 'lightning_bolt': 22, # LIGHTNING_BOLT
+ 'sun': 23, # SUN
+ 'moon': 24, # MOON
+ 'smiley_face': 17, # SMILEY_FACE
+ 'no_symbol': 19, # NO_SYMBOL
+ 'flowchart_process': 61, # FLOWCHART_PROCESS
+ 'flowchart_decision': 63, # FLOWCHART_DECISION
+ 'flowchart_data': 64, # FLOWCHART_DATA
+ 'flowchart_document': 67 # FLOWCHART_DOCUMENT
+ }
+
+ # Check if shape type is valid before trying to use it
+ shape_type_lower = str(shape_type).lower()
+ if shape_type_lower not in shape_type_map:
+ available_shapes = ', '.join(sorted(shape_type_map.keys()))
+ raise ValueError(f"Unsupported shape type: '{shape_type}'. Available shape types: {available_shapes}")
+
+ # Get the integer value for the shape type
+ shape_value = shape_type_map[shape_type_lower]
+
+ # Create the shape using the direct integer value
+ try:
+ # The integer value is passed directly to add_shape
+ shape = slide.shapes.add_shape(
+ shape_value, Inches(left), Inches(top), Inches(width), Inches(height)
+ )
+ return shape
+ except Exception as e:
+ raise ValueError(f"Failed to create '{shape_type}' shape using direct value {shape_value}: {str(e)}")
+
+# ---- Custom presentation management wrapper ----
+
+class PresentationManager:
+ """Wrapper to handle presentation state updates."""
+
+ def __init__(self, presentations_dict):
+ self.presentations = presentations_dict
+
+ def store_presentation(self, pres, pres_id):
+ """Store a presentation and set it as current."""
+ self.presentations[pres_id] = pres
+ set_current_presentation_id(pres_id)
+ return pres_id
+
+# ---- Register Tools ----
+
+# Create presentation manager wrapper
+presentation_manager = PresentationManager(presentations)
+
+# Wrapper functions to handle state management
+def create_presentation_wrapper(original_func):
+ """Wrapper to handle presentation creation with state management."""
+ def wrapper(*args, **kwargs):
+ result = original_func(*args, **kwargs)
+ if "presentation_id" in result and result["presentation_id"] in presentations:
+ set_current_presentation_id(result["presentation_id"])
+ return result
+ return wrapper
+
+def open_presentation_wrapper(original_func):
+ """Wrapper to handle presentation opening with state management."""
+ def wrapper(*args, **kwargs):
+ result = original_func(*args, **kwargs)
+ if "presentation_id" in result and result["presentation_id"] in presentations:
+ set_current_presentation_id(result["presentation_id"])
+ return result
+ return wrapper
+
+# Register all tool modules
+register_presentation_tools(
+ app,
+ presentations,
+ get_current_presentation_id,
+ get_template_search_directories
+)
+
+register_content_tools(
+ app,
+ presentations,
+ get_current_presentation_id,
+ validate_parameters,
+ is_positive,
+ is_non_negative,
+ is_in_range,
+ is_valid_rgb
+)
+
+register_structural_tools(
+ app,
+ presentations,
+ get_current_presentation_id,
+ validate_parameters,
+ is_positive,
+ is_non_negative,
+ is_in_range,
+ is_valid_rgb,
+ add_shape_direct
+)
+
+register_professional_tools(
+ app,
+ presentations,
+ get_current_presentation_id
+)
+
+register_template_tools(
+ app,
+ presentations,
+ get_current_presentation_id
+)
+
+register_hyperlink_tools(
+ app,
+ presentations,
+ get_current_presentation_id,
+ validate_parameters,
+ is_positive,
+ is_non_negative,
+ is_in_range,
+ is_valid_rgb
+)
+
+register_chart_tools(
+ app,
+ presentations,
+ get_current_presentation_id,
+ validate_parameters,
+ is_positive,
+ is_non_negative,
+ is_in_range,
+ is_valid_rgb
+)
+
+
+register_connector_tools(
+ app,
+ presentations,
+ get_current_presentation_id,
+ validate_parameters,
+ is_positive,
+ is_non_negative,
+ is_in_range,
+ is_valid_rgb
+)
+
+register_master_tools(
+ app,
+ presentations,
+ get_current_presentation_id,
+ validate_parameters,
+ is_positive,
+ is_non_negative,
+ is_in_range,
+ is_valid_rgb
+)
+
+register_transition_tools(
+ app,
+ presentations,
+ get_current_presentation_id,
+ validate_parameters,
+ is_positive,
+ is_non_negative,
+ is_in_range,
+ is_valid_rgb
+)
+
+
+# ---- Additional Utility Tools ----
+
+@app.tool()
+def list_presentations() -> Dict:
+ """List all loaded presentations."""
+ return {
+ "presentations": [
+ {
+ "id": pres_id,
+ "slide_count": len(pres.slides),
+ "is_current": pres_id == current_presentation_id
+ }
+ for pres_id, pres in presentations.items()
+ ],
+ "current_presentation_id": current_presentation_id,
+ "total_presentations": len(presentations)
+ }
+
+@app.tool()
+def switch_presentation(presentation_id: str) -> Dict:
+ """Switch to a different loaded presentation."""
+ if presentation_id not in presentations:
+ return {
+ "error": f"Presentation '{presentation_id}' not found. Available presentations: {list(presentations.keys())}"
+ }
+
+ global current_presentation_id
+ old_id = current_presentation_id
+ current_presentation_id = presentation_id
+
+ return {
+ "message": f"Switched from presentation '{old_id}' to '{presentation_id}'",
+ "previous_presentation_id": old_id,
+ "current_presentation_id": current_presentation_id
+ }
+
+@app.tool()
+def get_server_info() -> Dict:
+ """Get information about the MCP server."""
+ return {
+ "name": "PowerPoint MCP Server - Enhanced Edition",
+ "version": "2.1.0",
+ "total_tools": 32, # Organized into 11 specialized modules
+ "loaded_presentations": len(presentations),
+ "current_presentation": current_presentation_id,
+ "features": [
+ "Presentation Management (7 tools)",
+ "Content Management (6 tools)",
+ "Template Operations (7 tools)",
+ "Structural Elements (4 tools)",
+ "Professional Design (3 tools)",
+ "Specialized Features (5 tools)"
+ ],
+ "improvements": [
+ "32 specialized tools organized into 11 focused modules",
+ "68+ utility functions across 7 organized utility modules",
+ "Enhanced parameter handling and validation",
+ "Unified operation interfaces with comprehensive coverage",
+ "Advanced template system with auto-generation capabilities",
+ "Professional design tools with multiple effects and styling",
+ "Specialized features including hyperlinks, connectors, slide masters",
+ "Dynamic text sizing and intelligent wrapping",
+ "Advanced visual effects and styling",
+ "Content-aware optimization and validation",
+ "Complete PowerPoint lifecycle management",
+ "Modular architecture for better maintainability"
+ ],
+ "new_enhanced_features": [
+ "Hyperlink Management - Add, update, remove, and list hyperlinks in text",
+ "Advanced Chart Data Updates - Replace chart data with new categories and series",
+ "Advanced Text Run Formatting - Apply formatting to specific text runs",
+ "Shape Connectors - Add connector lines and arrows between points",
+ "Slide Master Management - Access and manage slide masters and layouts",
+ "Slide Transitions - Basic transition management (placeholder for future)"
+ ]
+ }
+
+# ---- Main Function ----
+def main(transport: str = "stdio", port: int = 8000):
+ if transport == "http":
+ import asyncio
+ # Set the port for HTTP transport
+ app.settings.port = port
+ # Start the FastMCP server with HTTP transport
+ try:
+ app.run(transport='streamable-http')
+ except asyncio.exceptions.CancelledError:
+ print("Server stopped by user.")
+ except KeyboardInterrupt:
+ print("Server stopped by user.")
+ except Exception as e:
+ print(f"Error starting server: {e}")
+
+ elif transport == "sse":
+ # Run the FastMCP server in SSE (Server Side Events) mode
+ app.run(transport='sse')
+
+ else:
+ # Run the FastMCP server
+ app.run(transport='stdio')
+
+if __name__ == "__main__":
+ # Parse command line arguments
+ parser = argparse.ArgumentParser(description="MCP Server for PowerPoint manipulation using python-pptx")
+
+ parser.add_argument(
+ "-t",
+ "--transport",
+ type=str,
+ default="stdio",
+ choices=["stdio", "http", "sse"],
+ help="Transport method for the MCP server (default: stdio)"
+ )
+
+ parser.add_argument(
+ "-p",
+ "--port",
+ type=int,
+ default=8000,
+ help="Port to run the MCP server on (default: 8000)"
+ )
+ args = parser.parse_args()
+ main(args.transport, args.port)
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/public/demo.gif b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/public/demo.gif
new file mode 100644
index 00000000..7795f49b
Binary files /dev/null and b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/public/demo.gif differ
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/public/demo.mp4 b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/public/demo.mp4
new file mode 100644
index 00000000..4188f2f8
Binary files /dev/null and b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/public/demo.mp4 differ
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/pyproject.toml b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/pyproject.toml
new file mode 100644
index 00000000..f9a21a07
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/pyproject.toml
@@ -0,0 +1,43 @@
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[project]
+name = "office-powerpoint-mcp-server"
+version = "2.0.7"
+description = "MCP Server for PowerPoint manipulation using python-pptx - Consolidated Edition"
+readme = "README.md"
+license = {file = "LICENSE"}
+authors = [
+ {name = "GongRzhe", email = "gongrzhe@gmail.com"}
+]
+classifiers = [
+ "Programming Language :: Python :: 3",
+ "License :: OSI Approved :: MIT License",
+ "Operating System :: OS Independent",
+]
+requires-python = ">=3.10"
+dependencies = [
+ "python-pptx>=0.6.21",
+ "mcp[cli]>=1.8.0",
+ "Pillow>=8.0.0",
+ "fonttools>=4.0.0",
+]
+
+[project.urls]
+"Homepage" = "https://github.com/GongRzhe/Office-PowerPoint-MCP-Server.git"
+"Bug Tracker" = "https://github.com/GongRzhe/Office-PowerPoint-MCP-Server.git/issues"
+
+[tool.hatch.build.targets.wheel]
+only-include = ["ppt_mcp_server.py", "tools/", "utils/", "enhanced_slide_templates.json", "slide_layout_templates.json"]
+sources = ["."]
+
+[tool.hatch.build]
+exclude = [
+ "public/demo.mp4",
+ "public/demo.gif",
+ "*.pptx"
+]
+
+[project.scripts]
+ppt_mcp_server = "ppt_mcp_server:main"
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/requirements.txt b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/requirements.txt
new file mode 100644
index 00000000..9f97c217
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/requirements.txt
@@ -0,0 +1,4 @@
+mcp[cli]
+python-pptx
+Pillow
+fonttools
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/setup_mcp.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/setup_mcp.py
new file mode 100644
index 00000000..1f3343bc
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/setup_mcp.py
@@ -0,0 +1,561 @@
+# Import necessary Python standard libraries
+import os # For operating with file system, handling files and directory paths
+import json # For processing JSON format data
+import subprocess # For creating and managing subprocesses
+import sys # For accessing Python interpreter related variables and functions
+import platform # For getting current operating system information
+import shutil # For checking if executables exist in PATH
+
+def check_prerequisites():
+ """
+ Check if necessary prerequisites are installed
+
+ Returns:
+ tuple: (python_ok, uv_installed, uvx_installed, ppt_server_installed)
+ """
+ # Check Python version
+ python_version = sys.version_info
+ python_ok = python_version.major >= 3 and python_version.minor >= 6
+
+ # Check if uv/uvx is installed
+ uv_installed = shutil.which("uv") is not None
+ uvx_installed = shutil.which("uvx") is not None
+
+ # Check if office-powerpoint-mcp-server is already installed via pip
+ try:
+ result = subprocess.run(
+ [sys.executable, "-m", "pip", "show", "office-powerpoint-mcp-server"],
+ capture_output=True,
+ text=True,
+ check=False
+ )
+ ppt_server_installed = result.returncode == 0
+ except Exception:
+ ppt_server_installed = False
+
+ return (python_ok, uv_installed, uvx_installed, ppt_server_installed)
+
+def setup_venv():
+ """
+ Function to set up Python virtual environment
+
+ Features:
+ - Checks if Python version meets requirements (3.6+)
+ - Creates Python virtual environment (if it doesn't exist)
+ - Installs required dependencies in the newly created virtual environment
+
+ No parameters required
+
+ Returns: Path to Python interpreter in the virtual environment
+ """
+ # Check Python version
+ python_version = sys.version_info
+ if python_version.major < 3 or (python_version.major == 3 and python_version.minor < 6):
+ print("Error: Python 3.6 or higher is required.")
+ sys.exit(1)
+
+ # Get absolute path of the directory containing the current script
+ base_path = os.path.abspath(os.path.dirname(__file__))
+ # Set virtual environment directory path
+ venv_path = os.path.join(base_path, '.venv')
+
+ # Determine pip and python executable paths based on operating system
+ is_windows = platform.system() == "Windows"
+ if is_windows:
+ pip_path = os.path.join(venv_path, 'Scripts', 'pip.exe')
+ python_path = os.path.join(venv_path, 'Scripts', 'python.exe')
+ else:
+ pip_path = os.path.join(venv_path, 'bin', 'pip')
+ python_path = os.path.join(venv_path, 'bin', 'python')
+
+ # Check if virtual environment already exists and is valid
+ venv_exists = os.path.exists(venv_path)
+ pip_exists = os.path.exists(pip_path)
+
+ if not venv_exists or not pip_exists:
+ print("Creating new virtual environment...")
+ # Remove existing venv if it's invalid
+ if venv_exists and not pip_exists:
+ print("Existing virtual environment is incomplete, recreating it...")
+ try:
+ shutil.rmtree(venv_path)
+ except Exception as e:
+ print(f"Warning: Could not remove existing virtual environment: {e}")
+ print("Please delete the .venv directory manually and try again.")
+ sys.exit(1)
+
+ # Create virtual environment
+ try:
+ subprocess.run([sys.executable, '-m', 'venv', venv_path], check=True)
+ print("Virtual environment created successfully!")
+ except subprocess.CalledProcessError as e:
+ print(f"Error creating virtual environment: {e}")
+ sys.exit(1)
+ else:
+ print("Valid virtual environment already exists.")
+
+ # Double-check that pip exists after creating venv
+ if not os.path.exists(pip_path):
+ print(f"Error: pip executable not found at {pip_path}")
+ print("Try creating the virtual environment manually with: python -m venv .venv")
+ sys.exit(1)
+
+ # Install or update dependencies
+ print("\nInstalling requirements...")
+ try:
+ # Install mcp package
+ subprocess.run([pip_path, 'install', 'mcp[cli]'], check=True)
+ # Install python-pptx package
+ subprocess.run([pip_path, 'install', 'python-pptx'], check=True)
+
+ # Also install dependencies from requirements.txt if it exists
+ requirements_path = os.path.join(base_path, 'requirements.txt')
+ if os.path.exists(requirements_path):
+ subprocess.run([pip_path, 'install', '-r', requirements_path], check=True)
+
+
+ print("Requirements installed successfully!")
+ except subprocess.CalledProcessError as e:
+ print(f"Error installing requirements: {e}")
+ sys.exit(1)
+ except FileNotFoundError:
+ print(f"Error: Could not execute {pip_path}")
+ print("Try activating the virtual environment manually and installing requirements:")
+ if is_windows:
+ print(f".venv\\Scripts\\activate")
+ else:
+ print("source .venv/bin/activate")
+ print("pip install mcp[cli] python-pptx")
+ sys.exit(1)
+
+ return python_path
+
+def generate_mcp_config_local(python_path):
+ """
+ Generate MCP configuration for locally installed office-powerpoint-mcp-server
+
+ Parameters:
+ - python_path: Path to Python interpreter in the virtual environment
+
+ Returns: Path to the generated config file
+ """
+ # Get absolute path of the directory containing the current script
+ base_path = os.path.abspath(os.path.dirname(__file__))
+
+ # Path to PowerPoint Server script
+ server_script_path = os.path.join(base_path, 'ppt_mcp_server.py')
+
+ # Path to templates directory
+ templates_path = os.path.join(base_path, 'templates')
+
+ # Create MCP configuration dictionary
+ config = {
+ "mcpServers": {
+ "ppt": {
+ "command": python_path,
+ "args": [server_script_path],
+ "env": {
+ "PYTHONPATH": base_path,
+ "PPT_TEMPLATE_PATH": templates_path
+ }
+ }
+ }
+ }
+
+ # Save configuration to JSON file
+ config_path = os.path.join(base_path, 'mcp-config.json')
+ with open(config_path, 'w') as f:
+ json.dump(config, f, indent=2) # indent=2 gives the JSON file good formatting
+
+ return config_path
+
+def generate_mcp_config_uvx():
+ """
+ Generate MCP configuration for PyPI-installed office-powerpoint-mcp-server using UVX
+
+ Returns: Path to the generated config file
+ """
+ # Get absolute path of the directory containing the current script
+ base_path = os.path.abspath(os.path.dirname(__file__))
+
+ # Path to templates directory (optional for UVX installs)
+ templates_path = os.path.join(base_path, 'templates')
+
+ # Create MCP configuration dictionary
+ env_config = {}
+ if os.path.exists(templates_path):
+ env_config["PPT_TEMPLATE_PATH"] = templates_path
+
+ config = {
+ "mcpServers": {
+ "ppt": {
+ "command": "uvx",
+ "args": ["--from", "office-powerpoint-mcp-server", "ppt_mcp_server"],
+ "env": env_config
+ }
+ }
+ }
+
+ # Save configuration to JSON file
+ config_path = os.path.join(base_path, 'mcp-config.json')
+ with open(config_path, 'w') as f:
+ json.dump(config, f, indent=2) # indent=2 gives the JSON file good formatting
+
+ return config_path
+
+def generate_mcp_config_module():
+ """
+ Generate MCP configuration for PyPI-installed office-powerpoint-mcp-server using Python module
+
+ Returns: Path to the generated config file
+ """
+ # Get absolute path of the directory containing the current script
+ base_path = os.path.abspath(os.path.dirname(__file__))
+
+ # Path to templates directory (optional for module installs)
+ templates_path = os.path.join(base_path, 'templates')
+
+ # Create MCP configuration dictionary
+ env_config = {}
+ if os.path.exists(templates_path):
+ env_config["PPT_TEMPLATE_PATH"] = templates_path
+
+ config = {
+ "mcpServers": {
+ "ppt": {
+ "command": sys.executable,
+ "args": ["-m", "office_powerpoint_mcp_server"],
+ "env": env_config
+ }
+ }
+ }
+
+ # Save configuration to JSON file
+ config_path = os.path.join(base_path, 'mcp-config.json')
+ with open(config_path, 'w') as f:
+ json.dump(config, f, indent=2) # indent=2 gives the JSON file good formatting
+
+ return config_path
+
+def install_from_pypi():
+ """
+ Install office-powerpoint-mcp-server from PyPI
+
+ Returns: True if successful, False otherwise
+ """
+ print("\nInstalling office-powerpoint-mcp-server from PyPI...")
+ try:
+ subprocess.run([sys.executable, "-m", "pip", "install", "office-powerpoint-mcp-server"], check=True)
+ print("office-powerpoint-mcp-server successfully installed from PyPI!")
+ return True
+ except subprocess.CalledProcessError:
+ print("Failed to install office-powerpoint-mcp-server from PyPI.")
+ return False
+
+def print_config_instructions(config_path):
+ """
+ Print instructions for using the generated config
+
+ Parameters:
+ - config_path: Path to the generated config file
+ """
+ print(f"\nMCP configuration has been written to: {config_path}")
+
+ with open(config_path, 'r') as f:
+ config = json.load(f)
+
+ print("\nMCP configuration for Claude Desktop:")
+ print(json.dumps(config, indent=2))
+
+ # Provide instructions for adding configuration to Claude Desktop configuration file
+ if platform.system() == "Windows":
+ claude_config_path = os.path.expandvars("%APPDATA%\\Claude\\claude_desktop_config.json")
+ else: # macOS
+ claude_config_path = os.path.expanduser("~/Library/Application Support/Claude/claude_desktop_config.json")
+
+ print(f"\nTo use with Claude Desktop, merge this configuration into: {claude_config_path}")
+
+def create_package_structure():
+ """
+ Create necessary package structure and directories
+ """
+ # Get absolute path of the directory containing the current script
+ base_path = os.path.abspath(os.path.dirname(__file__))
+
+ # Create __init__.py file
+ init_path = os.path.join(base_path, '__init__.py')
+ if not os.path.exists(init_path):
+ with open(init_path, 'w') as f:
+ f.write('# PowerPoint MCP Server')
+ print(f"Created __init__.py at: {init_path}")
+
+ # Create requirements.txt file
+ requirements_path = os.path.join(base_path, 'requirements.txt')
+ if not os.path.exists(requirements_path):
+ with open(requirements_path, 'w') as f:
+ f.write('mcp[cli]\npython-pptx\n')
+ print(f"Created requirements.txt at: {requirements_path}")
+
+ # Create templates directory for PowerPoint templates
+ templates_dir = os.path.join(base_path, 'templates')
+ if not os.path.exists(templates_dir):
+ os.makedirs(templates_dir)
+ print(f"Created templates directory at: {templates_dir}")
+
+ # Create a README file in templates directory
+ readme_path = os.path.join(templates_dir, 'README.md')
+ with open(readme_path, 'w') as f:
+ f.write("""# PowerPoint Templates
+
+This directory is for storing PowerPoint template files (.pptx or .potx) that can be used with the MCP server.
+
+## Usage
+
+1. Place your template files in this directory
+2. Use the `create_presentation_from_template` tool with the template filename
+3. The server will automatically search for templates in this directory
+
+## Supported Formats
+
+- `.pptx` - PowerPoint presentation files
+- `.potx` - PowerPoint template files
+
+## Example
+
+```python
+# Create presentation from template
+result = create_presentation_from_template("company_template.pptx")
+```
+
+The server will search for templates in:
+- Current directory
+- ./templates/ (this directory)
+- ./assets/
+- ./resources/
+""")
+ print(f"Created templates README at: {readme_path}")
+
+ # Offer to create a sample template
+ create_sample = input("\nWould you like to create a sample template for testing? (y/n): ").lower().strip()
+ if create_sample in ['y', 'yes']:
+ create_sample_template(templates_dir)
+
+def create_sample_template(templates_dir):
+ """
+ Create a sample PowerPoint template for testing
+
+ Parameters:
+ - templates_dir: Directory where templates are stored
+ """
+ try:
+ # Import required modules for creating a sample template
+ from pptx import Presentation
+ from pptx.util import Inches, Pt
+ from pptx.dml.color import RGBColor
+ from pptx.enum.text import PP_ALIGN
+
+ print("Creating sample template...")
+
+ # Create a new presentation
+ prs = Presentation()
+
+ # Get the title slide layout
+ title_slide_layout = prs.slide_layouts[0]
+ slide = prs.slides.add_slide(title_slide_layout)
+
+ # Set title and subtitle
+ title = slide.shapes.title
+ subtitle = slide.placeholders[1]
+
+ title.text = "Sample Company Template"
+ subtitle.text = "Professional Presentation Template\nCreated by PowerPoint MCP Server"
+
+ # Format title
+ title_paragraph = title.text_frame.paragraphs[0]
+ title_paragraph.font.size = Pt(44)
+ title_paragraph.font.bold = True
+ title_paragraph.font.color.rgb = RGBColor(31, 73, 125) # Dark blue
+
+ # Format subtitle
+ for paragraph in subtitle.text_frame.paragraphs:
+ paragraph.font.size = Pt(18)
+ paragraph.font.color.rgb = RGBColor(68, 84, 106) # Gray blue
+ paragraph.alignment = PP_ALIGN.CENTER
+
+ # Add a content slide
+ content_slide_layout = prs.slide_layouts[1]
+ content_slide = prs.slides.add_slide(content_slide_layout)
+
+ content_title = content_slide.shapes.title
+ content_title.text = "Sample Content Slide"
+
+ # Add bullet points to content
+ content_placeholder = content_slide.placeholders[1]
+ text_frame = content_placeholder.text_frame
+ text_frame.text = "Key Features"
+
+ # Add bullet points
+ bullet_points = [
+ "Professional theme and colors",
+ "Custom layouts and placeholders",
+ "Ready for content creation",
+ "Compatible with MCP server tools"
+ ]
+
+ for point in bullet_points:
+ p = text_frame.add_paragraph()
+ p.text = point
+ p.level = 1
+
+ # Add a section header slide
+ section_slide_layout = prs.slide_layouts[2] if len(prs.slide_layouts) > 2 else prs.slide_layouts[0]
+ section_slide = prs.slides.add_slide(section_slide_layout)
+
+ if section_slide.shapes.title:
+ section_slide.shapes.title.text = "Template Features"
+
+ # Save the sample template
+ template_path = os.path.join(templates_dir, 'sample_template.pptx')
+ prs.save(template_path)
+
+ print(f"✅ Sample template created: {template_path}")
+ print(" You can now test the template feature with:")
+ print(" • get_template_info('sample_template.pptx')")
+ print(" • create_presentation_from_template('sample_template.pptx')")
+
+ except ImportError:
+ print("⚠️ Cannot create sample template: python-pptx not installed yet")
+ print(" Run the setup first, then manually create templates in the templates/ directory")
+ except Exception as e:
+ print(f"❌ Failed to create sample template: {str(e)}")
+ print(" You can manually add template files to the templates/ directory")
+
+# Main execution entry point
+if __name__ == '__main__':
+ # Check prerequisites
+ python_ok, uv_installed, uvx_installed, ppt_server_installed = check_prerequisites()
+
+ if not python_ok:
+ print("Error: Python 3.6 or higher is required.")
+ sys.exit(1)
+
+ print("PowerPoint MCP Server Setup")
+ print("===========================\n")
+
+ # Create necessary files
+ create_package_structure()
+
+ # If office-powerpoint-mcp-server is already installed, offer config options
+ if ppt_server_installed:
+ print("office-powerpoint-mcp-server is already installed via pip.")
+
+ if uvx_installed:
+ print("\nOptions:")
+ print("1. Generate MCP config for UVX (recommended)")
+ print("2. Generate MCP config for Python module")
+ print("3. Set up local development environment")
+
+ choice = input("\nEnter your choice (1-3): ")
+
+ if choice == "1":
+ config_path = generate_mcp_config_uvx()
+ print_config_instructions(config_path)
+ elif choice == "2":
+ config_path = generate_mcp_config_module()
+ print_config_instructions(config_path)
+ elif choice == "3":
+ python_path = setup_venv()
+ config_path = generate_mcp_config_local(python_path)
+ print_config_instructions(config_path)
+ else:
+ print("Invalid choice. Exiting.")
+ sys.exit(1)
+ else:
+ print("\nOptions:")
+ print("1. Generate MCP config for Python module")
+ print("2. Set up local development environment")
+
+ choice = input("\nEnter your choice (1-2): ")
+
+ if choice == "1":
+ config_path = generate_mcp_config_module()
+ print_config_instructions(config_path)
+ elif choice == "2":
+ python_path = setup_venv()
+ config_path = generate_mcp_config_local(python_path)
+ print_config_instructions(config_path)
+ else:
+ print("Invalid choice. Exiting.")
+ sys.exit(1)
+
+ # If office-powerpoint-mcp-server is not installed, offer installation options
+ else:
+ print("office-powerpoint-mcp-server is not installed.")
+
+ print("\nOptions:")
+ print("1. Install from PyPI (recommended)")
+ print("2. Set up local development environment")
+
+ choice = input("\nEnter your choice (1-2): ")
+
+ if choice == "1":
+ if install_from_pypi():
+ if uvx_installed:
+ print("\nNow generating MCP config for UVX...")
+ config_path = generate_mcp_config_uvx()
+ else:
+ print("\nUVX not found. Generating MCP config for Python module...")
+ config_path = generate_mcp_config_module()
+ print_config_instructions(config_path)
+ elif choice == "2":
+ python_path = setup_venv()
+ config_path = generate_mcp_config_local(python_path)
+ print_config_instructions(config_path)
+ else:
+ print("Invalid choice. Exiting.")
+ sys.exit(1)
+
+ print("\nSetup complete! You can now use the PowerPoint MCP server with compatible clients like Claude Desktop.")
+
+ print("\n" + "="*60)
+ print("POWERPOINT MCP SERVER - NEW FEATURES")
+ print("="*60)
+ print("\n📁 Template Support:")
+ print(" • Place PowerPoint templates (.pptx/.potx) in the ./templates/ directory")
+ print(" • Use 'create_presentation_from_template' tool to create presentations from templates")
+ print(" • Use 'get_template_info' tool to inspect template layouts and properties")
+ print(" • Templates preserve branding, themes, and custom layouts")
+ print(" • Template path configured via PPT_TEMPLATE_PATH environment variable")
+
+ print("\n🔧 Available MCP Tools:")
+ print(" Presentations:")
+ print(" • create_presentation - Create new blank presentation")
+ print(" • create_presentation_from_template - Create from template file")
+ print(" • get_template_info - Inspect template file details")
+ print(" • open_presentation - Open existing presentation")
+ print(" • save_presentation - Save presentation to file")
+
+ print("\n Content:")
+ print(" • add_slide - Add slides with various layouts")
+ print(" • add_textbox - Add formatted text boxes")
+ print(" • add_image - Add images from files or base64")
+ print(" • add_table - Add formatted tables")
+ print(" • add_shape - Add various auto shapes")
+ print(" • add_chart - Add column, bar, line, and pie charts")
+
+ print("\n📚 Documentation:")
+ print(" • Full API documentation available in README.md")
+ print(" • Template usage examples included")
+ print(" • Check ./templates/README.md for template guidelines")
+
+ print("\n🚀 Quick Start with Templates:")
+ print(" 1. Copy your .pptx template to ./templates/")
+ print(" 2. Use: create_presentation_from_template('your_template.pptx')")
+ print(" 3. Add slides using template layouts")
+ print(" 4. Save your presentation")
+ print("\n💡 Custom Template Paths:")
+ print(" • Set PPT_TEMPLATE_PATH environment variable for custom locations")
+ print(" • Supports multiple paths (colon-separated on Unix, semicolon on Windows)")
+ print(" • Example: PPT_TEMPLATE_PATH='/path/to/templates:/path/to/more/templates'")
+
+ print("\n" + "="*60)
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/slide_layout_templates.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/slide_layout_templates.json
new file mode 100644
index 00000000..4f49c4e5
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/slide_layout_templates.json
@@ -0,0 +1,3690 @@
+{
+ "metadata": {
+ "version": "4.1",
+ "description": "Unified comprehensive slide layout templates with enhanced dynamic features and modern 2024 design trends",
+ "created_date": "2025-01-20",
+ "updated_date": "2025-06-20",
+ "total_templates": 31,
+ "supported_features": [
+ "Dynamic text sizing and wrapping",
+ "Multiple font combinations per template",
+ "Advanced visual effects for all elements",
+ "Gradient backgrounds and overlays",
+ "Shadow and glow effects",
+ "Automatic content adaptation",
+ "Stylistic text variations",
+ "Professional animation readiness",
+ "Interactive hover effects",
+ "Animated counters and metrics",
+ "Smart content overflow handling",
+ "Multi-layer gradient backgrounds",
+ "Text placeholders",
+ "Image placeholders",
+ "Chart placeholders",
+ "Table placeholders",
+ "Shape elements",
+ "Professional color schemes",
+ "Typography styles",
+ "Layout positioning",
+ "Custom image masks (circle, hexagon)",
+ "Interactive poll elements",
+ "Organic shape design",
+ "Neon cyberpunk styling",
+ "Nature-inspired themes",
+ "Minimalist clean aesthetics",
+ "Pastel color schemes",
+ "Brutalist bold typography",
+ "Split-screen comparisons",
+ "Product showcase layouts",
+ "Real-time visualization",
+ "Modern gradient effects"
+ ]
+ },
+ "color_schemes": {
+ "modern_blue": {
+ "primary": [0, 120, 215],
+ "secondary": [40, 40, 40],
+ "accent1": [0, 176, 240],
+ "accent2": [255, 192, 0],
+ "light": [247, 247, 247],
+ "text": [68, 68, 68],
+ "gradient_start": [0, 176, 240],
+ "gradient_end": [0, 120, 215]
+ },
+ "corporate_gray": {
+ "primary": [68, 68, 68],
+ "secondary": [0, 120, 215],
+ "accent1": [89, 89, 89],
+ "accent2": [217, 217, 217],
+ "light": [242, 242, 242],
+ "text": [51, 51, 51],
+ "gradient_start": [217, 217, 217],
+ "gradient_end": [89, 89, 89]
+ },
+ "elegant_green": {
+ "primary": [70, 136, 71],
+ "secondary": [255, 255, 255],
+ "accent1": [146, 208, 80],
+ "accent2": [112, 173, 71],
+ "light": [238, 236, 225],
+ "text": [89, 89, 89],
+ "gradient_start": [146, 208, 80],
+ "gradient_end": [70, 136, 71]
+ },
+ "warm_red": {
+ "primary": [192, 80, 77],
+ "secondary": [68, 68, 68],
+ "accent1": [230, 126, 34],
+ "accent2": [241, 196, 15],
+ "light": [253, 253, 253],
+ "text": [44, 62, 80],
+ "gradient_start": [241, 196, 15],
+ "gradient_end": [192, 80, 77]
+ },
+ "pastel_dream": {
+ "primary": [156, 136, 255],
+ "secondary": [255, 154, 162],
+ "accent1": [255, 206, 147],
+ "accent2": [163, 228, 215],
+ "light": [248, 248, 255],
+ "text": [88, 88, 88],
+ "gradient_start": [255, 206, 147],
+ "gradient_end": [156, 136, 255]
+ },
+ "nature_earth": {
+ "primary": [101, 123, 131],
+ "secondary": [162, 132, 94],
+ "accent1": [218, 215, 205],
+ "accent2": [143, 151, 121],
+ "light": [245, 243, 238],
+ "text": [68, 68, 68],
+ "gradient_start": [218, 215, 205],
+ "gradient_end": [101, 123, 131]
+ },
+ "neon_vibrant": {
+ "primary": [255, 20, 147],
+ "secondary": [0, 191, 255],
+ "accent1": [57, 255, 20],
+ "accent2": [255, 140, 0],
+ "light": [248, 248, 248],
+ "text": [34, 34, 34],
+ "gradient_start": [255, 20, 147],
+ "gradient_end": [0, 191, 255]
+ },
+ "minimalist_mono": {
+ "primary": [34, 34, 34],
+ "secondary": [128, 128, 128],
+ "accent1": [68, 68, 68],
+ "accent2": [187, 187, 187],
+ "light": [250, 250, 250],
+ "text": [51, 51, 51],
+ "gradient_start": [187, 187, 187],
+ "gradient_end": [68, 68, 68]
+ }
+ },
+ "typography_styles": {
+ "modern_sans": {
+ "title": {"name": "Segoe UI", "weight": "bold", "style": "normal"},
+ "subtitle": {"name": "Segoe UI Light", "weight": "normal", "style": "normal"},
+ "body": {"name": "Segoe UI", "weight": "normal", "style": "normal"},
+ "accent": {"name": "Segoe UI Semibold", "weight": "semibold", "style": "normal"}
+ },
+ "elegant_serif": {
+ "title": {"name": "Times New Roman", "weight": "bold", "style": "normal"},
+ "subtitle": {"name": "Georgia", "weight": "normal", "style": "italic"},
+ "body": {"name": "Times New Roman", "weight": "normal", "style": "normal"},
+ "accent": {"name": "Georgia", "weight": "bold", "style": "normal"}
+ },
+ "tech_modern": {
+ "title": {"name": "Arial", "weight": "bold", "style": "normal"},
+ "subtitle": {"name": "Helvetica", "weight": "light", "style": "normal"},
+ "body": {"name": "Arial", "weight": "normal", "style": "normal"},
+ "accent": {"name": "Helvetica", "weight": "bold", "style": "normal"}
+ },
+ "organic_flow": {
+ "title": {"name": "Montserrat", "weight": "bold", "style": "normal"},
+ "subtitle": {"name": "Open Sans", "weight": "light", "style": "normal"},
+ "body": {"name": "Source Sans Pro", "weight": "normal", "style": "normal"},
+ "accent": {"name": "Montserrat", "weight": "semibold", "style": "normal"}
+ },
+ "brutalist_bold": {
+ "title": {"name": "Impact", "weight": "bold", "style": "normal"},
+ "subtitle": {"name": "Arial Black", "weight": "normal", "style": "normal"},
+ "body": {"name": "Helvetica", "weight": "normal", "style": "normal"},
+ "accent": {"name": "Impact", "weight": "normal", "style": "normal"}
+ }
+ },
+ "typography": {
+ "title": {
+ "font_name": "Segoe UI",
+ "font_size_large": 36,
+ "font_size_medium": 28,
+ "font_size_small": 24,
+ "bold": true
+ },
+ "subtitle": {
+ "font_name": "Segoe UI Light",
+ "font_size_large": 20,
+ "font_size_medium": 18,
+ "font_size_small": 16,
+ "bold": false
+ },
+ "body": {
+ "font_name": "Segoe UI",
+ "font_size_large": 16,
+ "font_size_medium": 14,
+ "font_size_small": 12,
+ "bold": false
+ },
+ "caption": {
+ "font_name": "Segoe UI",
+ "font_size_large": 12,
+ "font_size_medium": 10,
+ "font_size_small": 9,
+ "bold": false
+ }
+ },
+ "text_effects": {
+ "shadow_soft": {
+ "type": "shadow",
+ "blur_radius": 3.0,
+ "distance": 2.0,
+ "direction": 315.0,
+ "color": [0, 0, 0],
+ "transparency": 0.4
+ },
+ "shadow_strong": {
+ "type": "shadow",
+ "blur_radius": 6.0,
+ "distance": 4.0,
+ "direction": 315.0,
+ "color": [0, 0, 0],
+ "transparency": 0.6
+ },
+ "glow_subtle": {
+ "type": "glow",
+ "size": 3.0,
+ "color_role": "accent1",
+ "transparency": 0.3
+ },
+ "glow_vibrant": {
+ "type": "glow",
+ "size": 8.0,
+ "color_role": "accent2",
+ "transparency": 0.5
+ },
+ "outline_thin": {
+ "type": "outline",
+ "width": 1.0,
+ "color_role": "primary"
+ },
+ "outline_thick": {
+ "type": "outline",
+ "width": 2.5,
+ "color_role": "secondary"
+ }
+ },
+ "image_effects": {
+ "professional_shadow": {
+ "shadow": {
+ "blur_radius": 8.0,
+ "distance": 5.0,
+ "direction": 315.0,
+ "color": [0, 0, 0],
+ "transparency": 0.3
+ },
+ "border": {
+ "width": 2.0,
+ "color": [255, 255, 255]
+ }
+ },
+ "modern_glow": {
+ "glow": {
+ "size": 6.0,
+ "color_role": "accent1",
+ "transparency": 0.4
+ },
+ "soft_edges": {
+ "radius": 4.0
+ }
+ },
+ "elegant_frame": {
+ "border": {
+ "width": 3.0,
+ "color_role": "primary"
+ },
+ "reflection": {
+ "size": 0.3,
+ "transparency": 0.4
+ }
+ },
+ "custom_mask_circle": {
+ "mask_type": "circle",
+ "border_radius": "50%",
+ "shadow": {
+ "blur_radius": 12.0,
+ "distance": 6.0,
+ "color": [0, 0, 0],
+ "transparency": 0.25
+ }
+ },
+ "custom_mask_hexagon": {
+ "mask_type": "polygon",
+ "polygon_points": 6,
+ "glow": {
+ "size": 8.0,
+ "color_role": "accent1",
+ "transparency": 0.3
+ }
+ },
+ "neon_outline": {
+ "border": {
+ "width": 4.0,
+ "color_role": "accent1"
+ },
+ "glow": {
+ "size": 10.0,
+ "color_role": "accent1",
+ "transparency": 0.6
+ },
+ "double_exposure": true
+ },
+ "organic_blend": {
+ "blend_mode": "multiply",
+ "organic_mask": true,
+ "soft_edges": {
+ "radius": 8.0
+ },
+ "color_overlay": {
+ "color_role": "accent2",
+ "opacity": 0.2
+ }
+ }
+ },
+ "dynamic_sizing": {
+ "text_length_thresholds": {
+ "short": 50,
+ "medium": 150,
+ "long": 300,
+ "very_long": 500
+ },
+ "font_size_adjustments": {
+ "short": {"multiplier": 1.2, "min_size": 14, "max_size": 36},
+ "medium": {"multiplier": 1.0, "min_size": 12, "max_size": 28},
+ "long": {"multiplier": 0.9, "min_size": 10, "max_size": 24},
+ "very_long": {"multiplier": 0.8, "min_size": 9, "max_size": 18}
+ },
+ "line_spacing_adjustments": {
+ "short": 1.0,
+ "medium": 1.2,
+ "long": 1.3,
+ "very_long": 1.4
+ }
+ },
+ "auto_sizing_rules": {
+ "text_measurement": {
+ "characters_per_line": {
+ "title": 40,
+ "subtitle": 50,
+ "body": 60,
+ "caption": 70
+ },
+ "base_font_sizes": {
+ "title": {"min": 18, "max": 44, "default": 28},
+ "subtitle": {"min": 14, "max": 24, "default": 18},
+ "body": {"min": 10, "max": 18, "default": 14},
+ "caption": {"min": 8, "max": 14, "default": 11}
+ }
+ },
+ "dynamic_adjustments": {
+ "content_overflow": {
+ "action": "reduce_font_size",
+ "min_reduction": 1,
+ "max_reduction": 4
+ },
+ "content_underflow": {
+ "action": "increase_font_size",
+ "max_increase": 2
+ },
+ "line_wrapping": {
+ "enabled": true,
+ "break_long_words": false,
+ "preserve_formatting": true
+ }
+ }
+ },
+ "effect_presets": {
+ "professional": {
+ "text_effects": ["shadow_soft"],
+ "image_effects": ["professional_shadow"],
+ "shape_effects": ["shadow_soft", "glow_subtle"]
+ },
+ "modern": {
+ "text_effects": ["glow_subtle", "shadow_soft"],
+ "image_effects": ["modern_glow"],
+ "shape_effects": ["glow_vibrant", "shadow_strong"]
+ },
+ "elegant": {
+ "text_effects": ["outline_thin", "shadow_soft"],
+ "image_effects": ["elegant_frame"],
+ "shape_effects": ["shadow_soft"]
+ },
+ "neon_cyberpunk": {
+ "text_effects": ["glow_vibrant", "outline_thick"],
+ "image_effects": ["neon_outline"],
+ "shape_effects": ["glow_vibrant", "shadow_strong"]
+ },
+ "organic_nature": {
+ "text_effects": ["shadow_soft"],
+ "image_effects": ["organic_blend"],
+ "shape_effects": ["shadow_soft", "glow_subtle"]
+ },
+ "minimalist_clean": {
+ "text_effects": ["outline_thin"],
+ "image_effects": ["custom_mask_circle"],
+ "shape_effects": ["shadow_soft"]
+ },
+ "brutalist_bold": {
+ "text_effects": ["shadow_strong", "outline_thick"],
+ "image_effects": ["custom_mask_hexagon"],
+ "shape_effects": ["shadow_strong", "glow_vibrant"]
+ }
+ },
+ "templates": {
+ "title_slide": {
+ "name": "Dynamic Title Slide",
+ "description": "Main title slide with gradient background and text effects",
+ "layout_type": "title",
+ "typography_style": "modern_sans",
+ "elements": [
+ {
+ "type": "text",
+ "role": "title",
+ "position": {
+ "left": 1.0,
+ "top": 2.0,
+ "width": 8.0,
+ "height": 2.0
+ },
+ "styling": {
+ "font_type": "title",
+ "font_size": "dynamic",
+ "alignment": "center",
+ "color_role": "primary",
+ "text_effects": ["shadow_strong", "glow_subtle"],
+ "auto_wrap": true,
+ "auto_fit": true,
+ "gradient_text": true,
+ "gradient_colors": ["primary", "accent1"]
+ },
+ "placeholder_text": "Transform Your Business",
+ "dynamic_content": {
+ "short_version": "Success",
+ "medium_version": "Business Success",
+ "long_version": "Transform Your Business for Success"
+ }
+ },
+ {
+ "type": "text",
+ "role": "subtitle",
+ "position": {
+ "left": 1.0,
+ "top": 4.2,
+ "width": 8.0,
+ "height": 1.0
+ },
+ "styling": {
+ "font_type": "subtitle",
+ "font_size": "dynamic",
+ "alignment": "center",
+ "color_role": "text",
+ "text_effects": ["shadow_soft"],
+ "auto_wrap": true,
+ "auto_fit": true,
+ "italic": true
+ },
+ "placeholder_text": "Strategic Innovation for 2024",
+ "dynamic_content": {
+ "enhancement": "animated_fade_in"
+ }
+ },
+ {
+ "type": "text",
+ "role": "author",
+ "position": {
+ "left": 1.0,
+ "top": 5.8,
+ "width": 8.0,
+ "height": 0.8
+ },
+ "styling": {
+ "font_type": "body",
+ "font_size": "medium",
+ "alignment": "center",
+ "color_role": "accent1",
+ "text_effects": ["outline_thin"],
+ "auto_wrap": true,
+ "bold": true
+ },
+ "placeholder_text": "Leadership Team"
+ },
+ {
+ "type": "shape",
+ "role": "decorative_accent",
+ "shape_type": "rectangle",
+ "position": {
+ "left": 3.5,
+ "top": 6.8,
+ "width": 3.0,
+ "height": 0.1
+ },
+ "styling": {
+ "fill_gradient": {
+ "start_color_role": "accent1",
+ "end_color_role": "accent2",
+ "direction": "horizontal"
+ },
+ "shadow": "shadow_soft",
+ "no_border": true
+ }
+ }
+ ],
+ "background": {
+ "type": "advanced_gradient",
+ "style": "radial",
+ "start_color_role": "light",
+ "end_color_role": "gradient_end",
+ "opacity": 0.8,
+ "overlay_pattern": "subtle_dots"
+ }
+ },
+ "text_with_image": {
+ "name": "Dynamic Text + Image",
+ "description": "Text content with stylized image and interactive elements",
+ "layout_type": "content",
+ "typography_style": "modern_sans",
+ "elements": [
+ {
+ "type": "text",
+ "role": "title",
+ "position": {
+ "left": 0.5,
+ "top": 0.4,
+ "width": 9.0,
+ "height": 1.0
+ },
+ "styling": {
+ "font_type": "title",
+ "font_size": "dynamic",
+ "alignment": "left",
+ "color_role": "primary",
+ "text_effects": ["shadow_soft", "glow_subtle"],
+ "auto_wrap": true,
+ "auto_fit": true,
+ "underline": true,
+ "underline_color_role": "accent1"
+ },
+ "placeholder_text": "Revolutionary Solutions"
+ },
+ {
+ "type": "shape",
+ "role": "title_underline",
+ "shape_type": "rectangle",
+ "position": {
+ "left": 0.5,
+ "top": 1.3,
+ "width": 3.0,
+ "height": 0.08
+ },
+ "styling": {
+ "fill_gradient": {
+ "start_color_role": "accent1",
+ "end_color_role": "primary",
+ "direction": "horizontal"
+ },
+ "no_border": true,
+ "glow": "glow_subtle"
+ }
+ },
+ {
+ "type": "text",
+ "role": "content",
+ "position": {
+ "left": 0.5,
+ "top": 1.8,
+ "width": 4.3,
+ "height": 4.8
+ },
+ "styling": {
+ "font_type": "body",
+ "font_size": "dynamic",
+ "alignment": "left",
+ "color_role": "text",
+ "text_effects": ["shadow_soft"],
+ "auto_wrap": true,
+ "auto_fit": true,
+ "line_spacing": "dynamic",
+ "bullet_style": "custom",
+ "bullet_color_role": "accent1",
+ "bullet_shape": "diamond"
+ },
+ "placeholder_text": "◆ Breakthrough innovation in technology\n◆ 250% increase in efficiency metrics\n◆ Sustainable solutions for the future\n◆ Industry-leading performance standards\n◆ Customer satisfaction at 98%",
+ "dynamic_formatting": {
+ "bullet_animation": "slide_in_left",
+ "text_highlight": "accent2",
+ "emphasis_words": ["breakthrough", "250%", "sustainable", "98%"]
+ }
+ },
+ {
+ "type": "image",
+ "role": "supporting",
+ "position": {
+ "left": 5.2,
+ "top": 1.6,
+ "width": 4.3,
+ "height": 4.0
+ },
+ "styling": {
+ "effects": "professional_shadow",
+ "border_radius": 12,
+ "hover_effect": "scale_105",
+ "overlay_gradient": {
+ "color_role": "primary",
+ "opacity": 0.1,
+ "direction": "diagonal"
+ }
+ },
+ "placeholder_text": "High-Impact Visual"
+ },
+ {
+ "type": "shape",
+ "role": "image_frame",
+ "shape_type": "rectangle",
+ "position": {
+ "left": 5.0,
+ "top": 1.4,
+ "width": 4.7,
+ "height": 4.4
+ },
+ "styling": {
+ "fill_color": "transparent",
+ "line_color_role": "accent1",
+ "line_width": 3.0,
+ "border_radius": 15,
+ "glow": "glow_vibrant"
+ }
+ },
+ {
+ "type": "text",
+ "role": "call_to_action",
+ "position": {
+ "left": 2.0,
+ "top": 6.0,
+ "width": 6.0,
+ "height": 0.8
+ },
+ "styling": {
+ "font_type": "accent",
+ "font_size": "large",
+ "alignment": "center",
+ "color_role": "accent2",
+ "text_effects": ["shadow_strong", "glow_vibrant"],
+ "auto_wrap": true,
+ "bold": true,
+ "italic": true
+ },
+ "placeholder_text": "Ready to Transform? Let's Begin!"
+ }
+ ],
+ "background": {
+ "type": "layered_gradient",
+ "base_color_role": "light",
+ "accent_gradient": {
+ "start_color_role": "accent1",
+ "end_color_role": "transparent",
+ "opacity": 0.05,
+ "direction": "diagonal"
+ }
+ }
+ },
+ "two_column_text": {
+ "name": "Two Columns of Text",
+ "description": "Two equal columns of text content with dynamic sizing",
+ "layout_type": "content",
+ "typography_style": "modern_sans",
+ "elements": [
+ {
+ "type": "text",
+ "role": "title",
+ "position": {
+ "left": 0.5,
+ "top": 0.5,
+ "width": 9.0,
+ "height": 0.8
+ },
+ "styling": {
+ "font_type": "title",
+ "font_size": "dynamic",
+ "alignment": "center",
+ "color_role": "primary",
+ "text_effects": ["shadow_soft"],
+ "auto_wrap": true,
+ "auto_fit": true
+ },
+ "placeholder_text": "Slide Title"
+ },
+ {
+ "type": "text",
+ "role": "content_left",
+ "position": {
+ "left": 0.5,
+ "top": 1.5,
+ "width": 4.25,
+ "height": 5.0
+ },
+ "styling": {
+ "font_type": "body",
+ "font_size": "dynamic",
+ "alignment": "left",
+ "color_role": "text",
+ "text_effects": ["shadow_soft"],
+ "auto_wrap": true,
+ "auto_fit": true,
+ "line_spacing": "dynamic"
+ },
+ "placeholder_text": "Column 1:\n• Point A\n• Point B\n• Point C\n• Supporting details"
+ },
+ {
+ "type": "text",
+ "role": "content_right",
+ "position": {
+ "left": 5.25,
+ "top": 1.5,
+ "width": 4.25,
+ "height": 5.0
+ },
+ "styling": {
+ "font_type": "body",
+ "font_size": "dynamic",
+ "alignment": "left",
+ "color_role": "text",
+ "text_effects": ["shadow_soft"],
+ "auto_wrap": true,
+ "auto_fit": true,
+ "line_spacing": "dynamic"
+ },
+ "placeholder_text": "Column 2:\n• Point D\n• Point E\n• Point F\n• Additional details"
+ }
+ ]
+ },
+ "two_column_text_images": {
+ "name": "Two Columns Text + Images",
+ "description": "Two columns with text and corresponding images with effects",
+ "layout_type": "content",
+ "typography_style": "modern_sans",
+ "elements": [
+ {
+ "type": "text",
+ "role": "title",
+ "position": {
+ "left": 0.5,
+ "top": 0.5,
+ "width": 9.0,
+ "height": 0.8
+ },
+ "styling": {
+ "font_type": "title",
+ "font_size": "dynamic",
+ "alignment": "center",
+ "color_role": "primary",
+ "text_effects": ["shadow_soft", "glow_subtle"],
+ "auto_wrap": true,
+ "auto_fit": true
+ },
+ "placeholder_text": "Comparison Title"
+ },
+ {
+ "type": "text",
+ "role": "content_left",
+ "position": {
+ "left": 0.5,
+ "top": 1.5,
+ "width": 4.25,
+ "height": 2.5
+ },
+ "styling": {
+ "font_type": "body",
+ "font_size": "dynamic",
+ "alignment": "left",
+ "color_role": "text",
+ "text_effects": ["shadow_soft"],
+ "auto_wrap": true,
+ "auto_fit": true,
+ "line_spacing": "dynamic"
+ },
+ "placeholder_text": "Left Section:\n• Key point 1\n• Key point 2"
+ },
+ {
+ "type": "image",
+ "role": "supporting_left",
+ "position": {
+ "left": 0.5,
+ "top": 4.2,
+ "width": 4.25,
+ "height": 2.3
+ },
+ "styling": {
+ "effects": "professional_shadow",
+ "border_radius": 8
+ },
+ "placeholder_text": "Left Image"
+ },
+ {
+ "type": "text",
+ "role": "content_right",
+ "position": {
+ "left": 5.25,
+ "top": 1.5,
+ "width": 4.25,
+ "height": 2.5
+ },
+ "styling": {
+ "font_type": "body",
+ "font_size": "dynamic",
+ "alignment": "left",
+ "color_role": "text",
+ "text_effects": ["shadow_soft"],
+ "auto_wrap": true,
+ "auto_fit": true,
+ "line_spacing": "dynamic"
+ },
+ "placeholder_text": "Right Section:\n• Key point 3\n• Key point 4"
+ },
+ {
+ "type": "image",
+ "role": "supporting_right",
+ "position": {
+ "left": 5.25,
+ "top": 4.2,
+ "width": 4.25,
+ "height": 2.3
+ },
+ "styling": {
+ "effects": "professional_shadow",
+ "border_radius": 8
+ },
+ "placeholder_text": "Right Image"
+ }
+ ]
+ },
+ "three_column_layout": {
+ "name": "Three Columns Text + Images",
+ "description": "Three equal columns with text and images with effects",
+ "layout_type": "content",
+ "typography_style": "modern_sans",
+ "elements": [
+ {
+ "type": "text",
+ "role": "title",
+ "position": {
+ "left": 0.5,
+ "top": 0.5,
+ "width": 9.0,
+ "height": 0.8
+ },
+ "styling": {
+ "font_type": "title",
+ "font_size": "dynamic",
+ "alignment": "center",
+ "color_role": "primary",
+ "text_effects": ["shadow_soft", "glow_subtle"],
+ "auto_wrap": true,
+ "auto_fit": true
+ },
+ "placeholder_text": "Three-Part Analysis"
+ },
+ {
+ "type": "text",
+ "role": "content_1",
+ "position": {
+ "left": 0.5,
+ "top": 1.5,
+ "width": 2.8,
+ "height": 2.0
+ },
+ "styling": {
+ "font_type": "body",
+ "font_size": "dynamic",
+ "alignment": "center",
+ "color_role": "text",
+ "text_effects": ["shadow_soft"],
+ "auto_wrap": true,
+ "auto_fit": true,
+ "line_spacing": "dynamic"
+ },
+ "placeholder_text": "Section 1:\n• Point A\n• Point B"
+ },
+ {
+ "type": "image",
+ "role": "supporting_1",
+ "position": {
+ "left": 0.5,
+ "top": 3.7,
+ "width": 2.8,
+ "height": 2.8
+ },
+ "styling": {
+ "effects": "modern_glow",
+ "border_radius": 10
+ },
+ "placeholder_text": "Image 1"
+ },
+ {
+ "type": "text",
+ "role": "content_2",
+ "position": {
+ "left": 3.6,
+ "top": 1.5,
+ "width": 2.8,
+ "height": 2.0
+ },
+ "styling": {
+ "font_type": "body",
+ "font_size": "dynamic",
+ "alignment": "center",
+ "color_role": "text",
+ "text_effects": ["shadow_soft"],
+ "auto_wrap": true,
+ "auto_fit": true,
+ "line_spacing": "dynamic"
+ },
+ "placeholder_text": "Section 2:\n• Point C\n• Point D"
+ },
+ {
+ "type": "image",
+ "role": "supporting_2",
+ "position": {
+ "left": 3.6,
+ "top": 3.7,
+ "width": 2.8,
+ "height": 2.8
+ },
+ "styling": {
+ "effects": "modern_glow",
+ "border_radius": 10
+ },
+ "placeholder_text": "Image 2"
+ },
+ {
+ "type": "text",
+ "role": "content_3",
+ "position": {
+ "left": 6.7,
+ "top": 1.5,
+ "width": 2.8,
+ "height": 2.0
+ },
+ "styling": {
+ "font_type": "body",
+ "font_size": "dynamic",
+ "alignment": "center",
+ "color_role": "text",
+ "text_effects": ["shadow_soft"],
+ "auto_wrap": true,
+ "auto_fit": true,
+ "line_spacing": "dynamic"
+ },
+ "placeholder_text": "Section 3:\n• Point E\n• Point F"
+ },
+ {
+ "type": "image",
+ "role": "supporting_3",
+ "position": {
+ "left": 6.7,
+ "top": 3.7,
+ "width": 2.8,
+ "height": 2.8
+ },
+ "styling": {
+ "effects": "modern_glow",
+ "border_radius": 10
+ },
+ "placeholder_text": "Image 3"
+ }
+ ]
+ },
+ "agenda_slide": {
+ "name": "Agenda/Directory Page",
+ "description": "Table of contents or agenda overview with styling",
+ "layout_type": "content",
+ "typography_style": "modern_sans",
+ "elements": [
+ {
+ "type": "text",
+ "role": "title",
+ "position": {
+ "left": 0.5,
+ "top": 0.5,
+ "width": 9.0,
+ "height": 0.8
+ },
+ "styling": {
+ "font_type": "title",
+ "font_size": "dynamic",
+ "alignment": "center",
+ "color_role": "primary",
+ "text_effects": ["shadow_soft", "glow_subtle"],
+ "auto_wrap": true,
+ "auto_fit": true
+ },
+ "placeholder_text": "Agenda"
+ },
+ {
+ "type": "shape",
+ "role": "decorative",
+ "shape_type": "rectangle",
+ "position": {
+ "left": 1.0,
+ "top": 1.8,
+ "width": 8.0,
+ "height": 0.1
+ },
+ "styling": {
+ "fill_color_role": "accent1",
+ "no_border": true,
+ "glow": "glow_subtle"
+ }
+ },
+ {
+ "type": "text",
+ "role": "agenda_items",
+ "position": {
+ "left": 2.0,
+ "top": 2.5,
+ "width": 6.0,
+ "height": 4.0
+ },
+ "styling": {
+ "font_type": "body",
+ "font_size": "dynamic",
+ "alignment": "left",
+ "color_role": "text",
+ "text_effects": ["shadow_soft"],
+ "auto_wrap": true,
+ "auto_fit": true,
+ "line_spacing": "dynamic"
+ },
+ "placeholder_text": "1. Introduction\n\n2. Problem Statement\n\n3. Proposed Solution\n\n4. Implementation Plan\n\n5. Timeline & Budget\n\n6. Q&A Session"
+ }
+ ],
+ "background": {
+ "type": "solid",
+ "color_role": "light"
+ }
+ },
+ "chapter_intro": {
+ "name": "Chapter Introduction Page",
+ "description": "Section divider with chapter number and title with effects",
+ "layout_type": "content",
+ "typography_style": "modern_sans",
+ "elements": [
+ {
+ "type": "shape",
+ "role": "background_accent",
+ "shape_type": "rectangle",
+ "position": {
+ "left": 0.0,
+ "top": 0.0,
+ "width": 4.0,
+ "height": 7.5
+ },
+ "styling": {
+ "fill_gradient": {
+ "start_color_role": "primary",
+ "end_color_role": "accent1",
+ "direction": "diagonal"
+ },
+ "no_border": true
+ }
+ },
+ {
+ "type": "text",
+ "role": "chapter_number",
+ "position": {
+ "left": 0.5,
+ "top": 2.0,
+ "width": 3.0,
+ "height": 1.5
+ },
+ "styling": {
+ "font_type": "title",
+ "font_size": "dynamic",
+ "alignment": "center",
+ "color": [255, 255, 255],
+ "text_effects": ["shadow_strong"],
+ "auto_wrap": true,
+ "auto_fit": true,
+ "bold": true
+ },
+ "placeholder_text": "03"
+ },
+ {
+ "type": "text",
+ "role": "chapter_title",
+ "position": {
+ "left": 4.5,
+ "top": 2.5,
+ "width": 5.0,
+ "height": 2.5
+ },
+ "styling": {
+ "font_type": "title",
+ "font_size": "dynamic",
+ "alignment": "left",
+ "color_role": "primary",
+ "text_effects": ["shadow_soft", "glow_subtle"],
+ "auto_wrap": true,
+ "auto_fit": true
+ },
+ "placeholder_text": "Chapter Title"
+ },
+ {
+ "type": "text",
+ "role": "chapter_description",
+ "position": {
+ "left": 4.5,
+ "top": 5.0,
+ "width": 5.0,
+ "height": 1.5
+ },
+ "styling": {
+ "font_type": "body",
+ "font_size": "dynamic",
+ "alignment": "left",
+ "color_role": "text",
+ "text_effects": ["shadow_soft"],
+ "auto_wrap": true,
+ "auto_fit": true,
+ "line_spacing": "dynamic"
+ },
+ "placeholder_text": "Brief description of what this chapter covers"
+ }
+ ]
+ },
+ "thank_you_slide": {
+ "name": "Thank You/End Page",
+ "description": "Closing slide with contact information and effects",
+ "layout_type": "content",
+ "typography_style": "modern_sans",
+ "elements": [
+ {
+ "type": "text",
+ "role": "thank_you",
+ "position": {
+ "left": 1.0,
+ "top": 2.0,
+ "width": 8.0,
+ "height": 1.5
+ },
+ "styling": {
+ "font_type": "title",
+ "font_size": "dynamic",
+ "alignment": "center",
+ "color_role": "primary",
+ "text_effects": ["shadow_strong", "glow_vibrant"],
+ "auto_wrap": true,
+ "auto_fit": true
+ },
+ "placeholder_text": "Thank You"
+ },
+ {
+ "type": "text",
+ "role": "questions",
+ "position": {
+ "left": 1.0,
+ "top": 3.8,
+ "width": 8.0,
+ "height": 1.0
+ },
+ "styling": {
+ "font_type": "subtitle",
+ "font_size": "dynamic",
+ "alignment": "center",
+ "color_role": "text",
+ "text_effects": ["shadow_soft"],
+ "auto_wrap": true,
+ "auto_fit": true,
+ "italic": true
+ },
+ "placeholder_text": "Questions & Discussion"
+ },
+ {
+ "type": "text",
+ "role": "contact",
+ "position": {
+ "left": 2.0,
+ "top": 5.5,
+ "width": 6.0,
+ "height": 1.5
+ },
+ "styling": {
+ "font_type": "body",
+ "font_size": "dynamic",
+ "alignment": "center",
+ "color_role": "text",
+ "text_effects": ["shadow_soft"],
+ "auto_wrap": true,
+ "auto_fit": true,
+ "line_spacing": "dynamic"
+ },
+ "placeholder_text": "Contact Information:\nemail@company.com\nphone: (555) 123-4567"
+ }
+ ],
+ "background": {
+ "type": "professional_gradient",
+ "style": "subtle",
+ "direction": "horizontal"
+ }
+ },
+ "timeline_slide": {
+ "name": "Timeline Page",
+ "description": "Horizontal timeline with milestones and effects",
+ "layout_type": "content",
+ "typography_style": "modern_sans",
+ "elements": [
+ {
+ "type": "text",
+ "role": "title",
+ "position": {
+ "left": 0.5,
+ "top": 0.5,
+ "width": 9.0,
+ "height": 0.8
+ },
+ "styling": {
+ "font_type": "title",
+ "font_size": "dynamic",
+ "alignment": "center",
+ "color_role": "primary",
+ "text_effects": ["shadow_soft", "glow_subtle"],
+ "auto_wrap": true,
+ "auto_fit": true
+ },
+ "placeholder_text": "Project Timeline"
+ },
+ {
+ "type": "shape",
+ "role": "timeline_line",
+ "shape_type": "rectangle",
+ "position": {
+ "left": 1.0,
+ "top": 3.5,
+ "width": 8.0,
+ "height": 0.1
+ },
+ "styling": {
+ "fill_gradient": {
+ "start_color_role": "accent1",
+ "end_color_role": "accent2",
+ "direction": "horizontal"
+ },
+ "no_border": true,
+ "glow": "glow_subtle"
+ }
+ },
+ {
+ "type": "shape",
+ "role": "milestone_1",
+ "shape_type": "oval",
+ "position": {
+ "left": 1.5,
+ "top": 3.25,
+ "width": 0.5,
+ "height": 0.5
+ },
+ "styling": {
+ "fill_color_role": "primary",
+ "line_color_role": "primary",
+ "shadow": "shadow_soft"
+ }
+ },
+ {
+ "type": "text",
+ "role": "milestone_1_text",
+ "position": {
+ "left": 1.0,
+ "top": 4.0,
+ "width": 1.5,
+ "height": 1.5
+ },
+ "styling": {
+ "font_type": "body",
+ "font_size": "dynamic",
+ "alignment": "center",
+ "color_role": "text",
+ "text_effects": ["shadow_soft"],
+ "auto_wrap": true,
+ "auto_fit": true
+ },
+ "placeholder_text": "Phase 1\nQ1 2024"
+ },
+ {
+ "type": "shape",
+ "role": "milestone_2",
+ "shape_type": "oval",
+ "position": {
+ "left": 3.5,
+ "top": 3.25,
+ "width": 0.5,
+ "height": 0.5
+ },
+ "styling": {
+ "fill_color_role": "primary",
+ "line_color_role": "primary",
+ "shadow": "shadow_soft"
+ }
+ },
+ {
+ "type": "text",
+ "role": "milestone_2_text",
+ "position": {
+ "left": 3.0,
+ "top": 4.0,
+ "width": 1.5,
+ "height": 1.5
+ },
+ "styling": {
+ "font_type": "body",
+ "font_size": "dynamic",
+ "alignment": "center",
+ "color_role": "text",
+ "text_effects": ["shadow_soft"],
+ "auto_wrap": true,
+ "auto_fit": true
+ },
+ "placeholder_text": "Phase 2\nQ2 2024"
+ },
+ {
+ "type": "shape",
+ "role": "milestone_3",
+ "shape_type": "oval",
+ "position": {
+ "left": 5.5,
+ "top": 3.25,
+ "width": 0.5,
+ "height": 0.5
+ },
+ "styling": {
+ "fill_color_role": "primary",
+ "line_color_role": "primary",
+ "shadow": "shadow_soft"
+ }
+ },
+ {
+ "type": "text",
+ "role": "milestone_3_text",
+ "position": {
+ "left": 5.0,
+ "top": 4.0,
+ "width": 1.5,
+ "height": 1.5
+ },
+ "styling": {
+ "font_type": "body",
+ "font_size": "dynamic",
+ "alignment": "center",
+ "color_role": "text",
+ "text_effects": ["shadow_soft"],
+ "auto_wrap": true,
+ "auto_fit": true
+ },
+ "placeholder_text": "Phase 3\nQ3 2024"
+ },
+ {
+ "type": "shape",
+ "role": "milestone_4",
+ "shape_type": "oval",
+ "position": {
+ "left": 7.5,
+ "top": 3.25,
+ "width": 0.5,
+ "height": 0.5
+ },
+ "styling": {
+ "fill_color_role": "accent2",
+ "line_color_role": "accent2",
+ "shadow": "shadow_soft",
+ "glow": "glow_vibrant"
+ }
+ },
+ {
+ "type": "text",
+ "role": "milestone_4_text",
+ "position": {
+ "left": 7.0,
+ "top": 4.0,
+ "width": 1.5,
+ "height": 1.5
+ },
+ "styling": {
+ "font_type": "body",
+ "font_size": "dynamic",
+ "alignment": "center",
+ "color_role": "text",
+ "text_effects": ["shadow_soft"],
+ "auto_wrap": true,
+ "auto_fit": true
+ },
+ "placeholder_text": "Launch\nQ4 2024"
+ }
+ ]
+ },
+ "data_table_slide": {
+ "name": "Data Table Page",
+ "description": "Slide focused on tabular data presentation with styling",
+ "layout_type": "content",
+ "typography_style": "modern_sans",
+ "elements": [
+ {
+ "type": "text",
+ "role": "title",
+ "position": {
+ "left": 0.5,
+ "top": 0.5,
+ "width": 9.0,
+ "height": 0.8
+ },
+ "styling": {
+ "font_type": "title",
+ "font_size": "dynamic",
+ "alignment": "center",
+ "color_role": "primary",
+ "text_effects": ["shadow_soft", "glow_subtle"],
+ "auto_wrap": true,
+ "auto_fit": true
+ },
+ "placeholder_text": "Data Analysis Results"
+ },
+ {
+ "type": "table",
+ "role": "main_data",
+ "position": {
+ "left": 1.0,
+ "top": 1.8,
+ "width": 8.0,
+ "height": 4.2
+ },
+ "table_config": {
+ "rows": 5,
+ "cols": 4,
+ "header_row": true,
+ "data": [
+ ["Metric", "Q1", "Q2", "Q3"],
+ ["Revenue", "$1.2M", "$1.5M", "$1.8M"],
+ ["Growth", "15%", "25%", "20%"],
+ ["Customers", "1,200", "1,500", "1,800"],
+ ["Satisfaction", "4.2/5", "4.4/5", "4.6/5"]
+ ]
+ },
+ "styling": {
+ "header_bg_color_role": "primary",
+ "header_text_color": [255, 255, 255],
+ "body_bg_color_role": "light",
+ "border_color_role": "secondary",
+ "shadow": "shadow_soft"
+ }
+ },
+ {
+ "type": "text",
+ "role": "insights",
+ "position": {
+ "left": 1.0,
+ "top": 6.2,
+ "width": 8.0,
+ "height": 1.0
+ },
+ "styling": {
+ "font_type": "body",
+ "font_size": "dynamic",
+ "alignment": "center",
+ "color_role": "text",
+ "text_effects": ["shadow_soft"],
+ "auto_wrap": true,
+ "auto_fit": true,
+ "italic": true
+ },
+ "placeholder_text": "Key insight: Consistent growth across all metrics with strong customer satisfaction"
+ }
+ ]
+ },
+ "chart_comparison": {
+ "name": "Chart Comparison Page",
+ "description": "Two charts side by side for comparison with effects",
+ "layout_type": "content",
+ "typography_style": "modern_sans",
+ "elements": [
+ {
+ "type": "text",
+ "role": "title",
+ "position": {
+ "left": 0.5,
+ "top": 0.5,
+ "width": 9.0,
+ "height": 0.8
+ },
+ "styling": {
+ "font_type": "title",
+ "font_size": "dynamic",
+ "alignment": "center",
+ "color_role": "primary",
+ "text_effects": ["shadow_soft", "glow_subtle"],
+ "auto_wrap": true,
+ "auto_fit": true
+ },
+ "placeholder_text": "Performance Comparison"
+ },
+ {
+ "type": "chart",
+ "role": "chart_left",
+ "position": {
+ "left": 0.5,
+ "top": 1.5,
+ "width": 4.25,
+ "height": 4.5
+ },
+ "chart_config": {
+ "type": "column",
+ "title": "Before Implementation",
+ "categories": ["Jan", "Feb", "Mar", "Apr"],
+ "series": [
+ {
+ "name": "Sales",
+ "values": [100, 120, 110, 130]
+ }
+ ]
+ },
+ "styling": {
+ "color_scheme": "corporate_gray",
+ "shadow": "shadow_soft"
+ }
+ },
+ {
+ "type": "chart",
+ "role": "chart_right",
+ "position": {
+ "left": 5.25,
+ "top": 1.5,
+ "width": 4.25,
+ "height": 4.5
+ },
+ "chart_config": {
+ "type": "column",
+ "title": "After Implementation",
+ "categories": ["Jan", "Feb", "Mar", "Apr"],
+ "series": [
+ {
+ "name": "Sales",
+ "values": [140, 170, 160, 190]
+ }
+ ]
+ },
+ "styling": {
+ "color_scheme": "modern_blue",
+ "shadow": "shadow_soft"
+ }
+ },
+ {
+ "type": "text",
+ "role": "comparison_note",
+ "position": {
+ "left": 2.0,
+ "top": 6.2,
+ "width": 6.0,
+ "height": 0.8
+ },
+ "styling": {
+ "font_type": "body",
+ "font_size": "dynamic",
+ "alignment": "center",
+ "color_role": "accent1",
+ "text_effects": ["shadow_soft", "glow_subtle"],
+ "auto_wrap": true,
+ "auto_fit": true,
+ "bold": true
+ },
+ "placeholder_text": "35% average improvement across all metrics"
+ }
+ ]
+ },
+ "full_image_slide": {
+ "name": "Full Image with Overlay Text",
+ "description": "Large background image with text overlay and effects",
+ "layout_type": "content",
+ "typography_style": "modern_sans",
+ "elements": [
+ {
+ "type": "image",
+ "role": "background",
+ "position": {
+ "left": 0.0,
+ "top": 0.0,
+ "width": 10.0,
+ "height": 7.5
+ },
+ "styling": {
+ "transparency": 0.3,
+ "overlay_gradient": {
+ "color_role": "primary",
+ "opacity": 0.2,
+ "direction": "vertical"
+ }
+ },
+ "placeholder_text": "Full Background Image"
+ },
+ {
+ "type": "shape",
+ "role": "text_overlay_bg",
+ "shape_type": "rectangle",
+ "position": {
+ "left": 1.0,
+ "top": 2.0,
+ "width": 8.0,
+ "height": 3.5
+ },
+ "styling": {
+ "fill_color": [0, 0, 0],
+ "transparency": 0.6,
+ "no_border": true,
+ "border_radius": 15
+ }
+ },
+ {
+ "type": "text",
+ "role": "overlay_title",
+ "position": {
+ "left": 1.5,
+ "top": 2.5,
+ "width": 7.0,
+ "height": 1.2
+ },
+ "styling": {
+ "font_type": "title",
+ "font_size": "dynamic",
+ "alignment": "center",
+ "color": [255, 255, 255],
+ "text_effects": ["shadow_strong", "glow_vibrant"],
+ "auto_wrap": true,
+ "auto_fit": true,
+ "bold": true
+ },
+ "placeholder_text": "Impactful Statement"
+ },
+ {
+ "type": "text",
+ "role": "overlay_subtitle",
+ "position": {
+ "left": 1.5,
+ "top": 3.8,
+ "width": 7.0,
+ "height": 1.2
+ },
+ "styling": {
+ "font_type": "subtitle",
+ "font_size": "dynamic",
+ "alignment": "center",
+ "color": [255, 255, 255],
+ "text_effects": ["shadow_soft"],
+ "auto_wrap": true,
+ "auto_fit": true
+ },
+ "placeholder_text": "Supporting message or call to action"
+ }
+ ]
+ },
+ "process_flow": {
+ "name": "Process Flow Diagram",
+ "description": "Step-by-step process visualization with enhanced effects",
+ "layout_type": "content",
+ "typography_style": "tech_modern",
+ "elements": [
+ {
+ "type": "text",
+ "role": "title",
+ "position": {
+ "left": 0.5,
+ "top": 0.3,
+ "width": 9.0,
+ "height": 0.8
+ },
+ "styling": {
+ "font_type": "title",
+ "font_size": "dynamic",
+ "alignment": "center",
+ "color_role": "primary",
+ "text_effects": ["shadow_soft", "glow_subtle"],
+ "auto_wrap": true,
+ "auto_fit": true
+ },
+ "placeholder_text": "Streamlined Process Architecture"
+ },
+ {
+ "type": "shape",
+ "role": "step_1_container",
+ "shape_type": "rounded_rectangle",
+ "position": {
+ "left": 0.5,
+ "top": 1.8,
+ "width": 2.0,
+ "height": 1.5
+ },
+ "styling": {
+ "fill_gradient": {
+ "start_color_role": "primary",
+ "end_color_role": "accent1",
+ "direction": "diagonal"
+ },
+ "shadow": "shadow_strong",
+ "glow": "glow_subtle",
+ "border_radius": 15,
+ "no_border": true
+ }
+ },
+ {
+ "type": "text",
+ "role": "step_1_number",
+ "position": {
+ "left": 0.6,
+ "top": 1.0,
+ "width": 0.8,
+ "height": 0.8
+ },
+ "styling": {
+ "font_type": "title",
+ "font_size": "dynamic",
+ "alignment": "center",
+ "color": [255, 255, 255],
+ "text_effects": ["shadow_strong"],
+ "auto_wrap": true,
+ "auto_fit": true,
+ "bold": true,
+ "background_shape": {
+ "type": "oval",
+ "color_role": "accent2",
+ "shadow": "shadow_strong"
+ }
+ },
+ "placeholder_text": "01"
+ },
+ {
+ "type": "text",
+ "role": "step_1_text",
+ "position": {
+ "left": 0.5,
+ "top": 1.8,
+ "width": 2.0,
+ "height": 1.5
+ },
+ "styling": {
+ "font_type": "body",
+ "font_size": "dynamic",
+ "alignment": "center",
+ "color": [255, 255, 255],
+ "text_effects": ["shadow_soft"],
+ "auto_wrap": true,
+ "auto_fit": true,
+ "bold": true,
+ "vertical_alignment": "middle"
+ },
+ "placeholder_text": "Data\nCollection\n& Analysis"
+ },
+ {
+ "type": "shape",
+ "role": "connector_1",
+ "shape_type": "arrow",
+ "position": {
+ "left": 2.7,
+ "top": 2.4,
+ "width": 1.0,
+ "height": 0.4
+ },
+ "styling": {
+ "fill_gradient": {
+ "start_color_role": "accent1",
+ "end_color_role": "accent2",
+ "direction": "horizontal"
+ },
+ "glow": "glow_vibrant",
+ "shadow": "shadow_soft",
+ "no_border": true
+ }
+ },
+ {
+ "type": "shape",
+ "role": "step_2_container",
+ "shape_type": "rounded_rectangle",
+ "position": {
+ "left": 4.0,
+ "top": 1.8,
+ "width": 2.0,
+ "height": 1.5
+ },
+ "styling": {
+ "fill_gradient": {
+ "start_color_role": "accent1",
+ "end_color_role": "secondary",
+ "direction": "diagonal"
+ },
+ "shadow": "shadow_strong",
+ "glow": "glow_subtle",
+ "border_radius": 15,
+ "no_border": true
+ }
+ },
+ {
+ "type": "text",
+ "role": "step_2_number",
+ "position": {
+ "left": 4.1,
+ "top": 1.0,
+ "width": 0.8,
+ "height": 0.8
+ },
+ "styling": {
+ "font_type": "title",
+ "font_size": "dynamic",
+ "alignment": "center",
+ "color": [255, 255, 255],
+ "text_effects": ["shadow_strong"],
+ "auto_wrap": true,
+ "auto_fit": true,
+ "bold": true,
+ "background_shape": {
+ "type": "oval",
+ "color_role": "accent2",
+ "shadow": "shadow_strong"
+ }
+ },
+ "placeholder_text": "02"
+ },
+ {
+ "type": "text",
+ "role": "step_2_text",
+ "position": {
+ "left": 4.0,
+ "top": 1.8,
+ "width": 2.0,
+ "height": 1.5
+ },
+ "styling": {
+ "font_type": "body",
+ "font_size": "dynamic",
+ "alignment": "center",
+ "color": [255, 255, 255],
+ "text_effects": ["shadow_soft"],
+ "auto_wrap": true,
+ "auto_fit": true,
+ "bold": true,
+ "vertical_alignment": "middle"
+ },
+ "placeholder_text": "AI-Powered\nProcessing\n& Optimization"
+ },
+ {
+ "type": "shape",
+ "role": "connector_2",
+ "shape_type": "arrow",
+ "position": {
+ "left": 6.2,
+ "top": 2.4,
+ "width": 1.0,
+ "height": 0.4
+ },
+ "styling": {
+ "fill_gradient": {
+ "start_color_role": "accent1",
+ "end_color_role": "accent2",
+ "direction": "horizontal"
+ },
+ "glow": "glow_vibrant",
+ "shadow": "shadow_soft",
+ "no_border": true
+ }
+ },
+ {
+ "type": "shape",
+ "role": "step_3_container",
+ "shape_type": "rounded_rectangle",
+ "position": {
+ "left": 7.5,
+ "top": 1.8,
+ "width": 2.0,
+ "height": 1.5
+ },
+ "styling": {
+ "fill_gradient": {
+ "start_color_role": "accent2",
+ "end_color_role": "primary",
+ "direction": "diagonal"
+ },
+ "shadow": "shadow_strong",
+ "glow": "glow_vibrant",
+ "border_radius": 15,
+ "no_border": true
+ }
+ },
+ {
+ "type": "text",
+ "role": "step_3_number",
+ "position": {
+ "left": 7.6,
+ "top": 1.0,
+ "width": 0.8,
+ "height": 0.8
+ },
+ "styling": {
+ "font_type": "title",
+ "font_size": "dynamic",
+ "alignment": "center",
+ "color": [255, 255, 255],
+ "text_effects": ["shadow_strong"],
+ "auto_wrap": true,
+ "auto_fit": true,
+ "bold": true,
+ "background_shape": {
+ "type": "oval",
+ "color_role": "primary",
+ "shadow": "shadow_strong"
+ }
+ },
+ "placeholder_text": "03"
+ },
+ {
+ "type": "text",
+ "role": "step_3_text",
+ "position": {
+ "left": 7.5,
+ "top": 1.8,
+ "width": 2.0,
+ "height": 1.5
+ },
+ "styling": {
+ "font_type": "body",
+ "font_size": "dynamic",
+ "alignment": "center",
+ "color": [255, 255, 255],
+ "text_effects": ["shadow_soft"],
+ "auto_wrap": true,
+ "auto_fit": true,
+ "bold": true,
+ "vertical_alignment": "middle"
+ },
+ "placeholder_text": "Intelligent\nDelivery\n& Results"
+ },
+ {
+ "type": "text",
+ "role": "process_benefits",
+ "position": {
+ "left": 1.0,
+ "top": 4.0,
+ "width": 8.0,
+ "height": 2.5
+ },
+ "styling": {
+ "font_type": "body",
+ "font_size": "dynamic",
+ "alignment": "center",
+ "color_role": "text",
+ "text_effects": ["shadow_soft"],
+ "auto_wrap": true,
+ "auto_fit": true,
+ "line_spacing": "dynamic",
+ "background_shape": {
+ "type": "rounded_rectangle",
+ "color_role": "light",
+ "opacity": 0.8,
+ "shadow": "shadow_soft"
+ }
+ },
+ "placeholder_text": "🎯 Complete automation reduces processing time by 85%\n⚡ Real-time optimization delivers instant insights\n🔄 Continuous learning improves accuracy over time\n📊 Comprehensive analytics provide actionable intelligence\n🚀 Scalable architecture grows with your business needs",
+ "dynamic_formatting": {
+ "icon_glow": "glow_subtle",
+ "emphasis_words": ["85%", "real-time", "continuous", "comprehensive", "scalable"]
+ }
+ }
+ ],
+ "background": {
+ "type": "tech_gradient",
+ "base_gradient": {
+ "start_color_role": "light",
+ "end_color_role": "secondary",
+ "direction": "diagonal",
+ "opacity": 0.1
+ },
+ "tech_pattern": "circuit_subtle"
+ }
+ },
+ "quote_testimonial": {
+ "name": "Quote/Testimonial Slide",
+ "description": "Featured quote or customer testimonial with effects",
+ "layout_type": "content",
+ "typography_style": "elegant_serif",
+ "elements": [
+ {
+ "type": "shape",
+ "role": "quote_mark",
+ "shape_type": "cloud",
+ "position": {
+ "left": 1.0,
+ "top": 1.5,
+ "width": 1.0,
+ "height": 1.0
+ },
+ "styling": {
+ "fill_color_role": "accent1",
+ "no_border": true,
+ "transparency": 0.3,
+ "glow": "glow_subtle"
+ }
+ },
+ {
+ "type": "text",
+ "role": "quote_text",
+ "position": {
+ "left": 1.5,
+ "top": 2.5,
+ "width": 7.0,
+ "height": 2.5
+ },
+ "styling": {
+ "font_type": "subtitle",
+ "font_size": "dynamic",
+ "alignment": "center",
+ "color_role": "primary",
+ "text_effects": ["shadow_soft", "glow_subtle"],
+ "auto_wrap": true,
+ "auto_fit": true,
+ "italic": true
+ },
+ "placeholder_text": "\"This solution transformed our business operations and increased efficiency by 40%. We couldn't be more satisfied with the results.\""
+ },
+ {
+ "type": "text",
+ "role": "attribution",
+ "position": {
+ "left": 2.0,
+ "top": 5.5,
+ "width": 6.0,
+ "height": 1.0
+ },
+ "styling": {
+ "font_type": "body",
+ "font_size": "dynamic",
+ "alignment": "center",
+ "color_role": "text",
+ "text_effects": ["shadow_soft"],
+ "auto_wrap": true,
+ "auto_fit": true,
+ "bold": true
+ },
+ "placeholder_text": "— Jane Smith, CEO, Company Name"
+ }
+ ],
+ "background": {
+ "type": "professional_gradient",
+ "style": "subtle",
+ "direction": "vertical"
+ }
+ },
+ "key_metrics_dashboard": {
+ "name": "KPI Dashboard",
+ "description": "Interactive metrics dashboard with animated counters and effects",
+ "layout_type": "content",
+ "typography_style": "modern_sans",
+ "elements": [
+ {
+ "type": "text",
+ "role": "title",
+ "position": {
+ "left": 0.5,
+ "top": 0.2,
+ "width": 9.0,
+ "height": 0.8
+ },
+ "styling": {
+ "font_type": "title",
+ "font_size": "dynamic",
+ "alignment": "center",
+ "color_role": "primary",
+ "text_effects": ["shadow_soft", "glow_subtle"],
+ "auto_wrap": true,
+ "auto_fit": true,
+ "gradient_text": true,
+ "gradient_colors": ["primary", "accent1"]
+ },
+ "placeholder_text": "Performance Dashboard 2024"
+ },
+ {
+ "type": "shape",
+ "role": "metric_1_container",
+ "shape_type": "rounded_rectangle",
+ "position": {
+ "left": 0.5,
+ "top": 1.3,
+ "width": 2.2,
+ "height": 2.2
+ },
+ "styling": {
+ "fill_gradient": {
+ "start_color_role": "accent1",
+ "end_color_role": "primary",
+ "direction": "diagonal",
+ "angle": 45
+ },
+ "shadow": "shadow_strong",
+ "glow": "glow_subtle",
+ "border_radius": 20,
+ "no_border": true
+ }
+ },
+ {
+ "type": "text",
+ "role": "metric_1_value",
+ "position": {
+ "left": 0.5,
+ "top": 1.6,
+ "width": 2.2,
+ "height": 0.8
+ },
+ "styling": {
+ "font_type": "title",
+ "font_size": "dynamic",
+ "alignment": "center",
+ "color": [255, 255, 255],
+ "text_effects": ["shadow_strong"],
+ "auto_wrap": true,
+ "auto_fit": true,
+ "bold": true
+ },
+ "placeholder_text": "94%",
+ "dynamic_content": {
+ "animation": "count_up",
+ "duration": 2000,
+ "from_value": 0
+ }
+ },
+ {
+ "type": "text",
+ "role": "metric_1_label",
+ "position": {
+ "left": 0.5,
+ "top": 2.5,
+ "width": 2.2,
+ "height": 0.6
+ },
+ "styling": {
+ "font_type": "body",
+ "font_size": "dynamic",
+ "alignment": "center",
+ "color": [255, 255, 255],
+ "text_effects": ["shadow_soft"],
+ "auto_wrap": true,
+ "auto_fit": true
+ },
+ "placeholder_text": "Customer\nSatisfaction"
+ },
+ {
+ "type": "shape",
+ "role": "metric_2_container",
+ "shape_type": "rounded_rectangle",
+ "position": {
+ "left": 3.2,
+ "top": 1.3,
+ "width": 2.2,
+ "height": 2.2
+ },
+ "styling": {
+ "fill_gradient": {
+ "start_color_role": "accent2",
+ "end_color_role": "primary",
+ "direction": "diagonal",
+ "angle": 135
+ },
+ "shadow": "shadow_strong",
+ "glow": "glow_subtle",
+ "border_radius": 20,
+ "no_border": true
+ }
+ },
+ {
+ "type": "text",
+ "role": "metric_2_value",
+ "position": {
+ "left": 3.2,
+ "top": 1.6,
+ "width": 2.2,
+ "height": 0.8
+ },
+ "styling": {
+ "font_type": "title",
+ "font_size": "dynamic",
+ "alignment": "center",
+ "color": [255, 255, 255],
+ "text_effects": ["shadow_strong"],
+ "auto_wrap": true,
+ "auto_fit": true,
+ "bold": true
+ },
+ "placeholder_text": "$2.4M",
+ "dynamic_content": {
+ "animation": "count_up",
+ "duration": 2500,
+ "format": "currency"
+ }
+ },
+ {
+ "type": "text",
+ "role": "metric_2_label",
+ "position": {
+ "left": 3.2,
+ "top": 2.5,
+ "width": 2.2,
+ "height": 0.6
+ },
+ "styling": {
+ "font_type": "body",
+ "font_size": "dynamic",
+ "alignment": "center",
+ "color": [255, 255, 255],
+ "text_effects": ["shadow_soft"],
+ "auto_wrap": true,
+ "auto_fit": true
+ },
+ "placeholder_text": "Annual\nRevenue"
+ },
+ {
+ "type": "shape",
+ "role": "metric_3_container",
+ "shape_type": "rounded_rectangle",
+ "position": {
+ "left": 5.9,
+ "top": 1.3,
+ "width": 2.2,
+ "height": 2.2
+ },
+ "styling": {
+ "fill_gradient": {
+ "start_color_role": "secondary",
+ "end_color_role": "accent1",
+ "direction": "radial"
+ },
+ "shadow": "shadow_strong",
+ "glow": "glow_subtle",
+ "border_radius": 20,
+ "no_border": true
+ }
+ },
+ {
+ "type": "text",
+ "role": "metric_3_value",
+ "position": {
+ "left": 5.9,
+ "top": 1.6,
+ "width": 2.2,
+ "height": 0.8
+ },
+ "styling": {
+ "font_type": "title",
+ "font_size": "dynamic",
+ "alignment": "center",
+ "color": [255, 255, 255],
+ "text_effects": ["shadow_strong"],
+ "auto_wrap": true,
+ "auto_fit": true,
+ "bold": true
+ },
+ "placeholder_text": "247",
+ "dynamic_content": {
+ "animation": "count_up",
+ "duration": 1800
+ }
+ },
+ {
+ "type": "text",
+ "role": "metric_3_label",
+ "position": {
+ "left": 5.9,
+ "top": 2.5,
+ "width": 2.2,
+ "height": 0.6
+ },
+ "styling": {
+ "font_type": "body",
+ "font_size": "dynamic",
+ "alignment": "center",
+ "color": [255, 255, 255],
+ "text_effects": ["shadow_soft"],
+ "auto_wrap": true,
+ "auto_fit": true
+ },
+ "placeholder_text": "New\nCustomers"
+ },
+ {
+ "type": "chart",
+ "role": "trend_visualization",
+ "position": {
+ "left": 1.0,
+ "top": 4.0,
+ "width": 8.0,
+ "height": 3.0
+ },
+ "chart_config": {
+ "type": "line_smooth",
+ "title": "Performance Trend Analysis",
+ "categories": ["Q1", "Q2", "Q3", "Q4"],
+ "series": [
+ {
+ "name": "Revenue Growth",
+ "values": [100, 125, 150, 180],
+ "color_role": "accent1",
+ "line_style": "smooth_gradient"
+ },
+ {
+ "name": "Customer Acquisition",
+ "values": [80, 95, 120, 145],
+ "color_role": "accent2",
+ "line_style": "smooth_gradient"
+ }
+ ]
+ },
+ "styling": {
+ "background_gradient": {
+ "start_color_role": "light",
+ "end_color_role": "accent1",
+ "opacity": 0.1
+ },
+ "shadow": "shadow_soft"
+ }
+ }
+ ],
+ "background": {
+ "type": "premium_gradient",
+ "base_gradient": {
+ "start_color_role": "light",
+ "end_color_role": "gradient_end",
+ "direction": "radial",
+ "opacity": 0.3
+ },
+ "overlay_pattern": "geometric_subtle"
+ }
+ },
+ "before_after_comparison": {
+ "name": "Before/After Comparison",
+ "description": "Dynamic comparison layout with visual dividers and effects",
+ "layout_type": "content",
+ "typography_style": "tech_modern",
+ "elements": [
+ {
+ "type": "text",
+ "role": "title",
+ "position": {
+ "left": 0.5,
+ "top": 0.3,
+ "width": 9.0,
+ "height": 0.9
+ },
+ "styling": {
+ "font_type": "title",
+ "font_size": "dynamic",
+ "alignment": "center",
+ "color_role": "primary",
+ "text_effects": ["shadow_soft", "outline_thin"],
+ "auto_wrap": true,
+ "auto_fit": true,
+ "letter_spacing": 1.2
+ },
+ "placeholder_text": "Before vs After Transformation"
+ },
+ {
+ "type": "shape",
+ "role": "vs_divider",
+ "shape_type": "oval",
+ "position": {
+ "left": 4.6,
+ "top": 3.0,
+ "width": 0.8,
+ "height": 0.8
+ },
+ "styling": {
+ "fill_color_role": "accent2",
+ "line_color_role": "primary",
+ "line_width": 3.0,
+ "glow": "glow_vibrant",
+ "shadow": "shadow_strong"
+ }
+ },
+ {
+ "type": "text",
+ "role": "vs_text",
+ "position": {
+ "left": 4.6,
+ "top": 3.0,
+ "width": 0.8,
+ "height": 0.8
+ },
+ "styling": {
+ "font_type": "accent",
+ "font_size": "dynamic",
+ "alignment": "center",
+ "color": [255, 255, 255],
+ "text_effects": ["shadow_strong"],
+ "auto_wrap": true,
+ "auto_fit": true,
+ "bold": true,
+ "vertical_alignment": "middle"
+ },
+ "placeholder_text": "VS"
+ },
+ {
+ "type": "text",
+ "role": "left_header",
+ "position": {
+ "left": 0.5,
+ "top": 1.4,
+ "width": 4.0,
+ "height": 0.6
+ },
+ "styling": {
+ "font_type": "subtitle",
+ "font_size": "dynamic",
+ "alignment": "center",
+ "color_role": "secondary",
+ "text_effects": ["shadow_soft"],
+ "auto_wrap": true,
+ "auto_fit": true,
+ "bold": true,
+ "background_shape": {
+ "type": "rounded_rectangle",
+ "color_role": "light",
+ "opacity": 0.8
+ }
+ },
+ "placeholder_text": "BEFORE"
+ },
+ {
+ "type": "text",
+ "role": "content_left",
+ "position": {
+ "left": 0.5,
+ "top": 2.1,
+ "width": 4.0,
+ "height": 3.8
+ },
+ "styling": {
+ "font_type": "body",
+ "font_size": "dynamic",
+ "alignment": "left",
+ "color_role": "text",
+ "text_effects": ["shadow_soft"],
+ "auto_wrap": true,
+ "auto_fit": true,
+ "line_spacing": "dynamic",
+ "bullet_style": "custom",
+ "bullet_color_role": "secondary",
+ "negative_emphasis": true
+ },
+ "placeholder_text": "✗ Manual processes taking 8+ hours\n✗ 40% error rate in operations\n✗ Limited scalability options\n✗ Customer complaints increasing\n✗ High operational costs\n✗ Inefficient resource allocation",
+ "dynamic_formatting": {
+ "negative_indicators": ["✗", "manual", "error", "limited", "complaints", "high", "inefficient"],
+ "text_color_negative": "secondary"
+ }
+ },
+ {
+ "type": "text",
+ "role": "right_header",
+ "position": {
+ "left": 5.5,
+ "top": 1.4,
+ "width": 4.0,
+ "height": 0.6
+ },
+ "styling": {
+ "font_type": "subtitle",
+ "font_size": "dynamic",
+ "alignment": "center",
+ "color_role": "primary",
+ "text_effects": ["shadow_soft", "glow_subtle"],
+ "auto_wrap": true,
+ "auto_fit": true,
+ "bold": true,
+ "background_shape": {
+ "type": "rounded_rectangle",
+ "color_role": "accent1",
+ "opacity": 0.2
+ }
+ },
+ "placeholder_text": "AFTER"
+ },
+ {
+ "type": "text",
+ "role": "content_right",
+ "position": {
+ "left": 5.5,
+ "top": 2.1,
+ "width": 4.0,
+ "height": 3.8
+ },
+ "styling": {
+ "font_type": "body",
+ "font_size": "dynamic",
+ "alignment": "left",
+ "color_role": "text",
+ "text_effects": ["shadow_soft", "glow_subtle"],
+ "auto_wrap": true,
+ "auto_fit": true,
+ "line_spacing": "dynamic",
+ "bullet_style": "custom",
+ "bullet_color_role": "primary",
+ "positive_emphasis": true
+ },
+ "placeholder_text": "✓ Automated workflows in 2 hours\n✓ 2% error rate with AI validation\n✓ Infinite scalability in cloud\n✓ 98% customer satisfaction score\n✓ 60% reduction in operational costs\n✓ Optimized resource management",
+ "dynamic_formatting": {
+ "positive_indicators": ["✓", "automated", "AI", "infinite", "98%", "60% reduction", "optimized"],
+ "text_color_positive": "primary",
+ "emphasis_glow": "glow_subtle"
+ }
+ },
+ {
+ "type": "shape",
+ "role": "improvement_arrow",
+ "shape_type": "arrow",
+ "position": {
+ "left": 3.8,
+ "top": 6.2,
+ "width": 2.4,
+ "height": 0.6
+ },
+ "styling": {
+ "fill_gradient": {
+ "start_color_role": "accent1",
+ "end_color_role": "primary",
+ "direction": "horizontal"
+ },
+ "glow": "glow_vibrant",
+ "shadow": "shadow_strong",
+ "no_border": true
+ }
+ },
+ {
+ "type": "text",
+ "role": "improvement_text",
+ "position": {
+ "left": 3.0,
+ "top": 6.9,
+ "width": 4.0,
+ "height": 0.5
+ },
+ "styling": {
+ "font_type": "accent",
+ "font_size": "dynamic",
+ "alignment": "center",
+ "color_role": "accent2",
+ "text_effects": ["shadow_soft", "glow_subtle"],
+ "auto_wrap": true,
+ "auto_fit": true,
+ "bold": true,
+ "italic": true
+ },
+ "placeholder_text": "75% Overall Improvement"
+ }
+ ],
+ "background": {
+ "type": "split_gradient",
+ "left_gradient": {
+ "start_color_role": "secondary",
+ "end_color_role": "light",
+ "opacity": 0.1
+ },
+ "right_gradient": {
+ "start_color_role": "primary",
+ "end_color_role": "light",
+ "opacity": 0.1
+ }
+ }
+ },
+ "team_introduction": {
+ "name": "Team Introduction",
+ "description": "Team member showcase with photos, roles and effects",
+ "layout_type": "content",
+ "typography_style": "modern_sans",
+ "elements": [
+ {
+ "type": "text",
+ "role": "title",
+ "position": {
+ "left": 0.5,
+ "top": 0.5,
+ "width": 9.0,
+ "height": 0.8
+ },
+ "styling": {
+ "font_type": "title",
+ "font_size": "dynamic",
+ "alignment": "center",
+ "color_role": "primary",
+ "text_effects": ["shadow_soft", "glow_subtle"],
+ "auto_wrap": true,
+ "auto_fit": true
+ },
+ "placeholder_text": "Meet the Team"
+ },
+ {
+ "type": "image",
+ "role": "team_member_1",
+ "position": {
+ "left": 1.0,
+ "top": 2.0,
+ "width": 2.0,
+ "height": 2.0
+ },
+ "styling": {
+ "effects": "elegant_frame",
+ "border_radius": "50%",
+ "hover_effect": "scale_105"
+ },
+ "placeholder_text": "Team Member 1"
+ },
+ {
+ "type": "text",
+ "role": "member_1_name",
+ "position": {
+ "left": 1.0,
+ "top": 4.2,
+ "width": 2.0,
+ "height": 0.4
+ },
+ "styling": {
+ "font_type": "body",
+ "font_size": "dynamic",
+ "alignment": "center",
+ "color_role": "primary",
+ "text_effects": ["shadow_soft"],
+ "auto_wrap": true,
+ "auto_fit": true,
+ "bold": true
+ },
+ "placeholder_text": "Alice Johnson"
+ },
+ {
+ "type": "text",
+ "role": "member_1_role",
+ "position": {
+ "left": 1.0,
+ "top": 4.7,
+ "width": 2.0,
+ "height": 0.4
+ },
+ "styling": {
+ "font_type": "body",
+ "font_size": "dynamic",
+ "alignment": "center",
+ "color_role": "text",
+ "text_effects": ["shadow_soft"],
+ "auto_wrap": true,
+ "auto_fit": true
+ },
+ "placeholder_text": "Project Manager"
+ },
+ {
+ "type": "image",
+ "role": "team_member_2",
+ "position": {
+ "left": 4.0,
+ "top": 2.0,
+ "width": 2.0,
+ "height": 2.0
+ },
+ "styling": {
+ "effects": "elegant_frame",
+ "border_radius": "50%",
+ "hover_effect": "scale_105"
+ },
+ "placeholder_text": "Team Member 2"
+ },
+ {
+ "type": "text",
+ "role": "member_2_name",
+ "position": {
+ "left": 4.0,
+ "top": 4.2,
+ "width": 2.0,
+ "height": 0.4
+ },
+ "styling": {
+ "font_type": "body",
+ "font_size": "dynamic",
+ "alignment": "center",
+ "color_role": "primary",
+ "text_effects": ["shadow_soft"],
+ "auto_wrap": true,
+ "auto_fit": true,
+ "bold": true
+ },
+ "placeholder_text": "Bob Smith"
+ },
+ {
+ "type": "text",
+ "role": "member_2_role",
+ "position": {
+ "left": 4.0,
+ "top": 4.7,
+ "width": 2.0,
+ "height": 0.4
+ },
+ "styling": {
+ "font_type": "body",
+ "font_size": "dynamic",
+ "alignment": "center",
+ "color_role": "text",
+ "text_effects": ["shadow_soft"],
+ "auto_wrap": true,
+ "auto_fit": true
+ },
+ "placeholder_text": "Lead Developer"
+ },
+ {
+ "type": "image",
+ "role": "team_member_3",
+ "position": {
+ "left": 7.0,
+ "top": 2.0,
+ "width": 2.0,
+ "height": 2.0
+ },
+ "styling": {
+ "effects": "elegant_frame",
+ "border_radius": "50%",
+ "hover_effect": "scale_105"
+ },
+ "placeholder_text": "Team Member 3"
+ },
+ {
+ "type": "text",
+ "role": "member_3_name",
+ "position": {
+ "left": 7.0,
+ "top": 4.2,
+ "width": 2.0,
+ "height": 0.4
+ },
+ "styling": {
+ "font_type": "body",
+ "font_size": "dynamic",
+ "alignment": "center",
+ "color_role": "primary",
+ "text_effects": ["shadow_soft"],
+ "auto_wrap": true,
+ "auto_fit": true,
+ "bold": true
+ },
+ "placeholder_text": "Carol Davis"
+ },
+ {
+ "type": "text",
+ "role": "member_3_role",
+ "position": {
+ "left": 7.0,
+ "top": 4.7,
+ "width": 2.0,
+ "height": 0.4
+ },
+ "styling": {
+ "font_type": "body",
+ "font_size": "dynamic",
+ "alignment": "center",
+ "color_role": "text",
+ "text_effects": ["shadow_soft"],
+ "auto_wrap": true,
+ "auto_fit": true
+ },
+ "placeholder_text": "UX Designer"
+ },
+ {
+ "type": "text",
+ "role": "team_description",
+ "position": {
+ "left": 1.5,
+ "top": 5.5,
+ "width": 7.0,
+ "height": 1.5
+ },
+ "styling": {
+ "font_type": "body",
+ "font_size": "dynamic",
+ "alignment": "center",
+ "color_role": "text",
+ "text_effects": ["shadow_soft"],
+ "auto_wrap": true,
+ "auto_fit": true,
+ "line_spacing": "dynamic"
+ },
+ "placeholder_text": "Our experienced team combines technical expertise with creative vision to deliver exceptional results for every project."
+ }
+ ]
+ },
+ "minimalist_hero": {
+ "name": "Minimalist Hero Slide",
+ "description": "Clean, spacious design with bold typography and minimal elements",
+ "layout_type": "content",
+ "typography_style": "modern_sans",
+ "elements": [
+ {
+ "type": "text",
+ "role": "hero_title",
+ "position": {
+ "left": 1.0,
+ "top": 2.5,
+ "width": 8.0,
+ "height": 2.5
+ },
+ "styling": {
+ "font_type": "title",
+ "font_size": "dynamic",
+ "alignment": "center",
+ "color_role": "primary",
+ "text_effects": ["outline_thin"],
+ "auto_wrap": true,
+ "auto_fit": true,
+ "letter_spacing": 1.5,
+ "line_height": 1.2
+ },
+ "placeholder_text": "Less is More",
+ "dynamic_content": {
+ "responsive_sizing": true,
+ "min_font_size": 32,
+ "max_font_size": 72
+ }
+ },
+ {
+ "type": "shape",
+ "role": "accent_line",
+ "shape_type": "rectangle",
+ "position": {
+ "left": 4.0,
+ "top": 5.2,
+ "width": 2.0,
+ "height": 0.05
+ },
+ "styling": {
+ "fill_color_role": "accent1",
+ "no_border": true
+ }
+ }
+ ],
+ "background": {
+ "type": "solid",
+ "color_role": "light",
+ "texture": "paper_subtle"
+ }
+ },
+ "neon_cyberpunk": {
+ "name": "Neon Cyberpunk Slide",
+ "description": "Futuristic design with neon colors and cyber elements",
+ "layout_type": "content",
+ "typography_style": "tech_modern",
+ "elements": [
+ {
+ "type": "text",
+ "role": "cyber_title",
+ "position": {
+ "left": 0.5,
+ "top": 1.0,
+ "width": 9.0,
+ "height": 1.5
+ },
+ "styling": {
+ "font_type": "title",
+ "font_size": "dynamic",
+ "alignment": "center",
+ "color_role": "accent1",
+ "text_effects": ["glow_vibrant", "outline_thick"],
+ "auto_wrap": true,
+ "auto_fit": true,
+ "text_transform": "uppercase",
+ "letter_spacing": 2.0
+ },
+ "placeholder_text": "FUTURE TECH",
+ "dynamic_content": {
+ "neon_flicker": true,
+ "animation": "glow_pulse"
+ }
+ },
+ {
+ "type": "shape",
+ "role": "cyber_grid",
+ "shape_type": "rectangle",
+ "position": {
+ "left": 0.0,
+ "top": 0.0,
+ "width": 10.0,
+ "height": 7.5
+ },
+ "styling": {
+ "fill_color": "transparent",
+ "line_color_role": "accent1",
+ "line_width": 1.0,
+ "opacity": 0.3,
+ "pattern": "grid_cyber"
+ }
+ },
+ {
+ "type": "text",
+ "role": "cyber_content",
+ "position": {
+ "left": 1.0,
+ "top": 3.0,
+ "width": 8.0,
+ "height": 3.0
+ },
+ "styling": {
+ "font_type": "body",
+ "font_size": "dynamic",
+ "alignment": "left",
+ "color_role": "light",
+ "text_effects": ["glow_subtle"],
+ "auto_wrap": true,
+ "auto_fit": true,
+ "line_spacing": "dynamic"
+ },
+ "placeholder_text": "◇ Neural network integration\n◇ Quantum processing capabilities\n◇ Holographic interface design\n◇ AI-driven optimization",
+ "dynamic_formatting": {
+ "bullet_glow": true,
+ "typewriter_effect": true
+ }
+ }
+ ],
+ "background": {
+ "type": "cyber_gradient",
+ "base_color": [10, 10, 15],
+ "accent_gradient": {
+ "start_color_role": "accent1",
+ "end_color_role": "accent2",
+ "opacity": 0.2,
+ "direction": "diagonal"
+ },
+ "tech_pattern": "circuit_matrix"
+ }
+ },
+ "nature_organic": {
+ "name": "Organic Nature Slide",
+ "description": "Earth-toned design with organic shapes and natural textures",
+ "layout_type": "content",
+ "typography_style": "organic_flow",
+ "elements": [
+ {
+ "type": "text",
+ "role": "nature_title",
+ "position": {
+ "left": 1.0,
+ "top": 1.0,
+ "width": 8.0,
+ "height": 1.2
+ },
+ "styling": {
+ "font_type": "title",
+ "font_size": "dynamic",
+ "alignment": "left",
+ "color_role": "primary",
+ "text_effects": ["shadow_soft"],
+ "auto_wrap": true,
+ "auto_fit": true,
+ "organic_curve": true
+ },
+ "placeholder_text": "Sustainable Growth"
+ },
+ {
+ "type": "shape",
+ "role": "organic_blob_1",
+ "shape_type": "organic_blob",
+ "position": {
+ "left": 7.0,
+ "top": 0.5,
+ "width": 2.5,
+ "height": 2.0
+ },
+ "styling": {
+ "fill_gradient": {
+ "start_color_role": "accent1",
+ "end_color_role": "accent2",
+ "direction": "radial"
+ },
+ "opacity": 0.7,
+ "no_border": true
+ }
+ },
+ {
+ "type": "text",
+ "role": "nature_content",
+ "position": {
+ "left": 1.0,
+ "top": 2.5,
+ "width": 5.5,
+ "height": 4.0
+ },
+ "styling": {
+ "font_type": "body",
+ "font_size": "dynamic",
+ "alignment": "left",
+ "color_role": "text",
+ "text_effects": ["shadow_soft"],
+ "auto_wrap": true,
+ "auto_fit": true,
+ "line_spacing": "dynamic"
+ },
+ "placeholder_text": "🌱 Renewable energy solutions\n🌍 Carbon-neutral operations\n🌿 Biodegradable materials\n🌊 Water conservation systems"
+ },
+ {
+ "type": "shape",
+ "role": "organic_blob_2",
+ "shape_type": "organic_blob",
+ "position": {
+ "left": 0.2,
+ "top": 5.0,
+ "width": 3.0,
+ "height": 2.0
+ },
+ "styling": {
+ "fill_gradient": {
+ "start_color_role": "accent2",
+ "end_color_role": "primary",
+ "direction": "radial"
+ },
+ "opacity": 0.5,
+ "no_border": true
+ }
+ }
+ ],
+ "background": {
+ "type": "organic_gradient",
+ "base_color_role": "light",
+ "texture": "organic_paper",
+ "overlay_pattern": "leaves_subtle"
+ }
+ },
+ "interactive_poll": {
+ "name": "Interactive Poll Slide",
+ "description": "Engaging poll slide with interactive elements and real-time visualization",
+ "layout_type": "content",
+ "typography_style": "modern_sans",
+ "elements": [
+ {
+ "type": "text",
+ "role": "poll_question",
+ "position": {
+ "left": 0.5,
+ "top": 0.5,
+ "width": 9.0,
+ "height": 1.2
+ },
+ "styling": {
+ "font_type": "title",
+ "font_size": "dynamic",
+ "alignment": "center",
+ "color_role": "primary",
+ "text_effects": ["shadow_soft", "glow_subtle"],
+ "auto_wrap": true,
+ "auto_fit": true
+ },
+ "placeholder_text": "What's your biggest challenge?",
+ "dynamic_content": {
+ "interactive": true,
+ "poll_integration": true
+ }
+ },
+ {
+ "type": "shape",
+ "role": "poll_option_1",
+ "shape_type": "rounded_rectangle",
+ "position": {
+ "left": 1.0,
+ "top": 2.0,
+ "width": 8.0,
+ "height": 0.8
+ },
+ "styling": {
+ "fill_gradient": {
+ "start_color_role": "accent1",
+ "end_color_role": "primary",
+ "direction": "horizontal"
+ },
+ "border_radius": 25,
+ "shadow": "shadow_soft",
+ "interactive": true,
+ "hover_effect": "scale_102"
+ }
+ },
+ {
+ "type": "text",
+ "role": "poll_option_1_text",
+ "position": {
+ "left": 1.2,
+ "top": 2.1,
+ "width": 6.0,
+ "height": 0.6
+ },
+ "styling": {
+ "font_type": "body",
+ "font_size": "dynamic",
+ "alignment": "left",
+ "color": [255, 255, 255],
+ "text_effects": ["shadow_soft"],
+ "auto_wrap": true,
+ "auto_fit": true,
+ "bold": true
+ },
+ "placeholder_text": "A) Time Management"
+ },
+ {
+ "type": "text",
+ "role": "poll_option_1_percentage",
+ "position": {
+ "left": 7.5,
+ "top": 2.1,
+ "width": 1.3,
+ "height": 0.6
+ },
+ "styling": {
+ "font_type": "accent",
+ "font_size": "dynamic",
+ "alignment": "right",
+ "color": [255, 255, 255],
+ "text_effects": ["shadow_soft"],
+ "auto_wrap": true,
+ "auto_fit": true,
+ "bold": true
+ },
+ "placeholder_text": "45%",
+ "dynamic_content": {
+ "animation": "count_up",
+ "live_update": true
+ }
+ },
+ {
+ "type": "shape",
+ "role": "poll_option_2",
+ "shape_type": "rounded_rectangle",
+ "position": {
+ "left": 1.0,
+ "top": 3.0,
+ "width": 6.0,
+ "height": 0.8
+ },
+ "styling": {
+ "fill_gradient": {
+ "start_color_role": "accent2",
+ "end_color_role": "secondary",
+ "direction": "horizontal"
+ },
+ "border_radius": 25,
+ "shadow": "shadow_soft",
+ "interactive": true,
+ "hover_effect": "scale_102"
+ }
+ },
+ {
+ "type": "text",
+ "role": "poll_option_2_text",
+ "position": {
+ "left": 1.2,
+ "top": 3.1,
+ "width": 4.5,
+ "height": 0.6
+ },
+ "styling": {
+ "font_type": "body",
+ "font_size": "dynamic",
+ "alignment": "left",
+ "color": [255, 255, 255],
+ "text_effects": ["shadow_soft"],
+ "auto_wrap": true,
+ "auto_fit": true,
+ "bold": true
+ },
+ "placeholder_text": "B) Budget Constraints"
+ },
+ {
+ "type": "text",
+ "role": "poll_option_2_percentage",
+ "position": {
+ "left": 6.0,
+ "top": 3.1,
+ "width": 1.0,
+ "height": 0.6
+ },
+ "styling": {
+ "font_type": "accent",
+ "font_size": "dynamic",
+ "alignment": "right",
+ "color": [255, 255, 255],
+ "text_effects": ["shadow_soft"],
+ "auto_wrap": true,
+ "auto_fit": true,
+ "bold": true
+ },
+ "placeholder_text": "30%",
+ "dynamic_content": {
+ "animation": "count_up",
+ "live_update": true
+ }
+ },
+ {
+ "type": "shape",
+ "role": "poll_option_3",
+ "shape_type": "rounded_rectangle",
+ "position": {
+ "left": 1.0,
+ "top": 4.0,
+ "width": 5.0,
+ "height": 0.8
+ },
+ "styling": {
+ "fill_gradient": {
+ "start_color_role": "primary",
+ "end_color_role": "accent1",
+ "direction": "horizontal"
+ },
+ "border_radius": 25,
+ "shadow": "shadow_soft",
+ "interactive": true,
+ "hover_effect": "scale_102"
+ }
+ },
+ {
+ "type": "text",
+ "role": "poll_option_3_text",
+ "position": {
+ "left": 1.2,
+ "top": 4.1,
+ "width": 3.5,
+ "height": 0.6
+ },
+ "styling": {
+ "font_type": "body",
+ "font_size": "dynamic",
+ "alignment": "left",
+ "color": [255, 255, 255],
+ "text_effects": ["shadow_soft"],
+ "auto_wrap": true,
+ "auto_fit": true,
+ "bold": true
+ },
+ "placeholder_text": "C) Team Communication"
+ },
+ {
+ "type": "text",
+ "role": "poll_option_3_percentage",
+ "position": {
+ "left": 5.0,
+ "top": 4.1,
+ "width": 1.0,
+ "height": 0.6
+ },
+ "styling": {
+ "font_type": "accent",
+ "font_size": "dynamic",
+ "alignment": "right",
+ "color": [255, 255, 255],
+ "text_effects": ["shadow_soft"],
+ "auto_wrap": true,
+ "auto_fit": true,
+ "bold": true
+ },
+ "placeholder_text": "25%",
+ "dynamic_content": {
+ "animation": "count_up",
+ "live_update": true
+ }
+ },
+ {
+ "type": "text",
+ "role": "poll_instruction",
+ "position": {
+ "left": 1.0,
+ "top": 5.5,
+ "width": 8.0,
+ "height": 1.0
+ },
+ "styling": {
+ "font_type": "body",
+ "font_size": "dynamic",
+ "alignment": "center",
+ "color_role": "text",
+ "text_effects": ["shadow_soft"],
+ "auto_wrap": true,
+ "auto_fit": true,
+ "italic": true
+ },
+ "placeholder_text": "🗳️ Vote now using your mobile device or click on an option"
+ }
+ ],
+ "background": {
+ "type": "interactive_gradient",
+ "base_color_role": "light",
+ "pulse_effect": true
+ }
+ },
+ "split_screen_comparison": {
+ "name": "Split Screen Comparison",
+ "description": "Modern split-screen layout for comparing two concepts or solutions",
+ "layout_type": "content",
+ "typography_style": "tech_modern",
+ "elements": [
+ {
+ "type": "text",
+ "role": "main_title",
+ "position": {
+ "left": 0.5,
+ "top": 0.3,
+ "width": 9.0,
+ "height": 0.9
+ },
+ "styling": {
+ "font_type": "title",
+ "font_size": "dynamic",
+ "alignment": "center",
+ "color_role": "primary",
+ "text_effects": ["shadow_soft", "outline_thin"],
+ "auto_wrap": true,
+ "auto_fit": true
+ },
+ "placeholder_text": "Choose Your Path"
+ },
+ {
+ "type": "shape",
+ "role": "left_section_bg",
+ "shape_type": "rectangle",
+ "position": {
+ "left": 0.0,
+ "top": 1.5,
+ "width": 4.9,
+ "height": 6.0
+ },
+ "styling": {
+ "fill_gradient": {
+ "start_color_role": "accent1",
+ "end_color_role": "primary",
+ "direction": "diagonal"
+ },
+ "opacity": 0.9,
+ "no_border": true
+ }
+ },
+ {
+ "type": "shape",
+ "role": "right_section_bg",
+ "shape_type": "rectangle",
+ "position": {
+ "left": 5.1,
+ "top": 1.5,
+ "width": 4.9,
+ "height": 6.0
+ },
+ "styling": {
+ "fill_gradient": {
+ "start_color_role": "accent2",
+ "end_color_role": "secondary",
+ "direction": "diagonal"
+ },
+ "opacity": 0.9,
+ "no_border": true
+ }
+ },
+ {
+ "type": "text",
+ "role": "left_title",
+ "position": {
+ "left": 0.2,
+ "top": 2.0,
+ "width": 4.5,
+ "height": 0.8
+ },
+ "styling": {
+ "font_type": "subtitle",
+ "font_size": "dynamic",
+ "alignment": "center",
+ "color": [255, 255, 255],
+ "text_effects": ["shadow_strong"],
+ "auto_wrap": true,
+ "auto_fit": true,
+ "bold": true
+ },
+ "placeholder_text": "TRADITIONAL"
+ },
+ {
+ "type": "text",
+ "role": "right_title",
+ "position": {
+ "left": 5.3,
+ "top": 2.0,
+ "width": 4.5,
+ "height": 0.8
+ },
+ "styling": {
+ "font_type": "subtitle",
+ "font_size": "dynamic",
+ "alignment": "center",
+ "color": [255, 255, 255],
+ "text_effects": ["shadow_strong"],
+ "auto_wrap": true,
+ "auto_fit": true,
+ "bold": true
+ },
+ "placeholder_text": "INNOVATIVE"
+ },
+ {
+ "type": "text",
+ "role": "left_content",
+ "position": {
+ "left": 0.3,
+ "top": 3.0,
+ "width": 4.3,
+ "height": 3.5
+ },
+ "styling": {
+ "font_type": "body",
+ "font_size": "dynamic",
+ "alignment": "left",
+ "color": [255, 255, 255],
+ "text_effects": ["shadow_soft"],
+ "auto_wrap": true,
+ "auto_fit": true,
+ "line_spacing": "dynamic"
+ },
+ "placeholder_text": "• Manual processes\n• Limited scalability\n• Higher costs\n• Slower execution\n• Traditional methods"
+ },
+ {
+ "type": "text",
+ "role": "right_content",
+ "position": {
+ "left": 5.4,
+ "top": 3.0,
+ "width": 4.3,
+ "height": 3.5
+ },
+ "styling": {
+ "font_type": "body",
+ "font_size": "dynamic",
+ "alignment": "left",
+ "color": [255, 255, 255],
+ "text_effects": ["shadow_soft"],
+ "auto_wrap": true,
+ "auto_fit": true,
+ "line_spacing": "dynamic"
+ },
+ "placeholder_text": "• Automated workflows\n• Infinite scalability\n• Cost optimization\n• Rapid deployment\n• AI-powered solutions"
+ },
+ {
+ "type": "shape",
+ "role": "divider_line",
+ "shape_type": "rectangle",
+ "position": {
+ "left": 4.95,
+ "top": 1.5,
+ "width": 0.1,
+ "height": 6.0
+ },
+ "styling": {
+ "fill_color": [255, 255, 255],
+ "opacity": 0.8,
+ "no_border": true
+ }
+ }
+ ],
+ "background": {
+ "type": "split_gradient",
+ "left_gradient": {
+ "start_color_role": "accent1",
+ "end_color_role": "light",
+ "opacity": 0.1
+ },
+ "right_gradient": {
+ "start_color_role": "accent2",
+ "end_color_role": "light",
+ "opacity": 0.1
+ }
+ }
+ },
+ "product_showcase": {
+ "name": "Product Showcase",
+ "description": "Modern product presentation with custom image masks and interactive elements",
+ "layout_type": "content",
+ "typography_style": "modern_sans",
+ "elements": [
+ {
+ "type": "text",
+ "role": "product_title",
+ "position": {
+ "left": 0.5,
+ "top": 0.3,
+ "width": 9.0,
+ "height": 1.0
+ },
+ "styling": {
+ "font_type": "title",
+ "font_size": "dynamic",
+ "alignment": "center",
+ "color_role": "primary",
+ "text_effects": ["shadow_soft", "glow_subtle"],
+ "auto_wrap": true,
+ "auto_fit": true,
+ "gradient_text": true,
+ "gradient_colors": ["primary", "accent1"]
+ },
+ "placeholder_text": "Revolutionary Product"
+ },
+ {
+ "type": "image",
+ "role": "hero_product",
+ "position": {
+ "left": 1.0,
+ "top": 1.8,
+ "width": 4.0,
+ "height": 4.0
+ },
+ "styling": {
+ "effects": "custom_mask_hexagon",
+ "hover_effect": "rotate_360",
+ "interactive": true,
+ "zoom_on_click": true
+ },
+ "placeholder_text": "Product Hero Image"
+ },
+ {
+ "type": "text",
+ "role": "product_features",
+ "position": {
+ "left": 5.5,
+ "top": 1.8,
+ "width": 4.0,
+ "height": 2.5
+ },
+ "styling": {
+ "font_type": "body",
+ "font_size": "dynamic",
+ "alignment": "left",
+ "color_role": "text",
+ "text_effects": ["shadow_soft"],
+ "auto_wrap": true,
+ "auto_fit": true,
+ "line_spacing": "dynamic"
+ },
+ "placeholder_text": "✨ Cutting-edge technology\n🚀 10x performance boost\n🔒 Enterprise-grade security\n🌐 Global compatibility\n📱 Mobile-first design"
+ },
+ {
+ "type": "shape",
+ "role": "feature_highlight_1",
+ "shape_type": "rounded_rectangle",
+ "position": {
+ "left": 5.5,
+ "top": 4.5,
+ "width": 1.8,
+ "height": 1.2
+ },
+ "styling": {
+ "fill_gradient": {
+ "start_color_role": "accent1",
+ "end_color_role": "primary",
+ "direction": "diagonal"
+ },
+ "border_radius": 15,
+ "shadow": "shadow_soft",
+ "glow": "glow_subtle"
+ }
+ },
+ {
+ "type": "text",
+ "role": "highlight_1_text",
+ "position": {
+ "left": 5.6,
+ "top": 4.8,
+ "width": 1.6,
+ "height": 0.6
+ },
+ "styling": {
+ "font_type": "accent",
+ "font_size": "dynamic",
+ "alignment": "center",
+ "color": [255, 255, 255],
+ "text_effects": ["shadow_soft"],
+ "auto_wrap": true,
+ "auto_fit": true,
+ "bold": true
+ },
+ "placeholder_text": "99.9%\nUptime"
+ },
+ {
+ "type": "shape",
+ "role": "feature_highlight_2",
+ "shape_type": "rounded_rectangle",
+ "position": {
+ "left": 7.5,
+ "top": 4.5,
+ "width": 1.8,
+ "height": 1.2
+ },
+ "styling": {
+ "fill_gradient": {
+ "start_color_role": "accent2",
+ "end_color_role": "secondary",
+ "direction": "diagonal"
+ },
+ "border_radius": 15,
+ "shadow": "shadow_soft",
+ "glow": "glow_subtle"
+ }
+ },
+ {
+ "type": "text",
+ "role": "highlight_2_text",
+ "position": {
+ "left": 7.6,
+ "top": 4.8,
+ "width": 1.6,
+ "height": 0.6
+ },
+ "styling": {
+ "font_type": "accent",
+ "font_size": "dynamic",
+ "alignment": "center",
+ "color": [255, 255, 255],
+ "text_effects": ["shadow_soft"],
+ "auto_wrap": true,
+ "auto_fit": true,
+ "bold": true
+ },
+ "placeholder_text": "< 1ms\nLatency"
+ },
+ {
+ "type": "text",
+ "role": "cta_button",
+ "position": {
+ "left": 3.0,
+ "top": 6.2,
+ "width": 4.0,
+ "height": 0.8
+ },
+ "styling": {
+ "font_type": "accent",
+ "font_size": "dynamic",
+ "alignment": "center",
+ "color": [255, 255, 255],
+ "text_effects": ["shadow_strong", "glow_vibrant"],
+ "auto_wrap": true,
+ "auto_fit": true,
+ "bold": true,
+ "background_shape": {
+ "type": "rounded_rectangle",
+ "color_role": "accent2",
+ "border_radius": 25,
+ "glow": "glow_vibrant"
+ },
+ "interactive": true,
+ "hover_effect": "scale_105"
+ },
+ "placeholder_text": "Experience the Future →"
+ }
+ ],
+ "background": {
+ "type": "product_gradient",
+ "base_gradient": {
+ "start_color_role": "light",
+ "end_color_role": "accent1",
+ "direction": "radial",
+ "opacity": 0.1
+ },
+ "floating_particles": true
+ }
+ }
+ },
+ "usage_guide": {
+ "basic_usage": "Templates can be applied by specifying the template name and color scheme",
+ "customization": "All elements can be customized including positions, colors, fonts, and content",
+ "positioning": "All positions are in inches from top-left corner of slide (10\" x 7.5\" standard)",
+ "color_roles": "Use color_role to automatically apply colors from selected scheme",
+ "dynamic_features": {
+ "auto_text_sizing": "Text automatically adjusts based on content length and container size",
+ "responsive_layouts": "Elements reposition based on content overflow",
+ "effect_combinations": "Multiple effects can be applied simultaneously",
+ "gradient_backgrounds": "Advanced gradient backgrounds with patterns and animations"
+ },
+ "element_types": [
+ "text - Text content with various formatting options and dynamic sizing",
+ "image - Pictures with positioning, styling and visual effects",
+ "shape - Geometric shapes with gradient fills and advanced effects",
+ "table - Data tables with header and cell formatting",
+ "chart - Various chart types with data series and styling"
+ ],
+ "styling_tips": [
+ "Use 'dynamic' font size for automatic text fitting",
+ "Combine text effects for maximum visual impact",
+ "Leverage gradient fills for modern appearance",
+ "Apply hover effects for interactive elements",
+ "Use animation properties for engaging presentations"
+ ],
+ "best_practices": [
+ "Use consistent color schemes throughout presentation",
+ "Maintain proper text hierarchy with title/subtitle/body fonts",
+ "Leave adequate white space for readability",
+ "Ensure images are high resolution for professional appearance",
+ "Test layouts with actual content before finalizing",
+ "Enable auto-wrapping for content that may vary in length",
+ "Use dynamic sizing for presentations with variable content"
+ ]
+ },
+ "implementation_notes": {
+ "mcp_integration": "Templates designed to work with existing MCP server tools",
+ "backward_compatibility": "All templates use existing presentation capabilities",
+ "extensibility": "New templates can be added by following the same JSON structure",
+ "automation": "Templates can be automatically applied based on content type detection",
+ "dynamic_features": "Enhanced templates include automatic sizing, wrapping, and effects",
+ "performance": "Optimized algorithms for real-time text sizing and layout adaptation"
+ }
+}
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/smithery.yaml b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/smithery.yaml
new file mode 100644
index 00000000..1025ca49
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/smithery.yaml
@@ -0,0 +1,16 @@
+# Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml
+
+startCommand:
+ type: stdio
+ configSchema:
+ # JSON Schema defining the configuration options for the MCP.
+ {}
+ commandFunction:
+ # A JS function that produces the CLI command based on the given config to start the MCP on stdio.
+ |-
+ (config) => ({
+ command: 'python',
+ args: ['ppt_mcp_server.py'],
+ env: {}
+ })
+ exampleConfig: {}
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/tools/__init__.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/tools/__init__.py
new file mode 100644
index 00000000..c1fb003a
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/tools/__init__.py
@@ -0,0 +1,28 @@
+"""
+Tools package for PowerPoint MCP Server.
+Organizes tools into logical modules for better maintainability.
+"""
+
+from .presentation_tools import register_presentation_tools
+from .content_tools import register_content_tools
+from .structural_tools import register_structural_tools
+from .professional_tools import register_professional_tools
+from .template_tools import register_template_tools
+from .hyperlink_tools import register_hyperlink_tools
+from .chart_tools import register_chart_tools
+from .connector_tools import register_connector_tools
+from .master_tools import register_master_tools
+from .transition_tools import register_transition_tools
+
+__all__ = [
+ "register_presentation_tools",
+ "register_content_tools",
+ "register_structural_tools",
+ "register_professional_tools",
+ "register_template_tools",
+ "register_hyperlink_tools",
+ "register_chart_tools",
+ "register_connector_tools",
+ "register_master_tools",
+ "register_transition_tools"
+]
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/tools/chart_tools.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/tools/chart_tools.py
new file mode 100644
index 00000000..0bb98174
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/tools/chart_tools.py
@@ -0,0 +1,87 @@
+"""
+Chart data management tools for PowerPoint MCP Server.
+Implements advanced chart data manipulation capabilities.
+"""
+
+from typing import Dict, List, Optional, Any
+from mcp.types import ToolAnnotations
+from pptx.chart.data import ChartData
+
+def register_chart_tools(app, presentations, get_current_presentation_id, validate_parameters,
+ is_positive, is_non_negative, is_in_range, is_valid_rgb):
+ """Register chart data management tools with the FastMCP app."""
+
+ @app.tool(
+ annotations=ToolAnnotations(
+ title="Update Chart Data",
+ ),
+ )
+ def update_chart_data(
+ slide_index: int,
+ shape_index: int,
+ categories: List[str],
+ series_data: List[Dict],
+ presentation_id: str = None
+ ) -> Dict:
+ """
+ Replace existing chart data with new categories and series.
+
+ Args:
+ slide_index: Index of the slide (0-based)
+ shape_index: Index of the chart shape (0-based)
+ categories: List of category names
+ series_data: List of dictionaries with 'name' and 'values' keys
+ presentation_id: Optional presentation ID (uses current if not provided)
+
+ Returns:
+ Dictionary with operation results
+ """
+ try:
+ # Get presentation
+ pres_id = presentation_id or get_current_presentation_id()
+ if pres_id not in presentations:
+ return {"error": "Presentation not found"}
+
+ pres = presentations[pres_id]
+
+ # Validate slide index
+ if not (0 <= slide_index < len(pres.slides)):
+ return {"error": f"Slide index {slide_index} out of range"}
+
+ slide = pres.slides[slide_index]
+
+ # Validate shape index
+ if not (0 <= shape_index < len(slide.shapes)):
+ return {"error": f"Shape index {shape_index} out of range"}
+
+ shape = slide.shapes[shape_index]
+
+ # Check if shape is a chart
+ if not hasattr(shape, 'has_chart') or not shape.has_chart:
+ return {"error": "Shape is not a chart"}
+
+ chart = shape.chart
+
+ # Create new ChartData
+ chart_data = ChartData()
+ chart_data.categories = categories
+
+ # Add series data
+ for series in series_data:
+ if 'name' not in series or 'values' not in series:
+ return {"error": "Each series must have 'name' and 'values' keys"}
+
+ chart_data.add_series(series['name'], series['values'])
+
+ # Replace chart data
+ chart.replace_data(chart_data)
+
+ return {
+ "message": f"Updated chart data on slide {slide_index}, shape {shape_index}",
+ "categories": categories,
+ "series_count": len(series_data),
+ "series_names": [s['name'] for s in series_data]
+ }
+
+ except Exception as e:
+ return {"error": f"Failed to update chart data: {str(e)}"}
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/tools/connector_tools.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/tools/connector_tools.py
new file mode 100644
index 00000000..4851bc92
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/tools/connector_tools.py
@@ -0,0 +1,96 @@
+"""
+Connector and line tools for PowerPoint MCP Server.
+Implements connector line/arrow drawing capabilities.
+"""
+
+from typing import Dict, List, Optional, Any
+from mcp.types import ToolAnnotations
+from pptx.util import Inches, Pt
+from pptx.enum.shapes import MSO_CONNECTOR
+from pptx.dml.color import RGBColor
+
+def register_connector_tools(app, presentations, get_current_presentation_id, validate_parameters,
+ is_positive, is_non_negative, is_in_range, is_valid_rgb):
+ """Register connector tools with the FastMCP app."""
+
+ @app.tool(
+ annotations=ToolAnnotations(
+ title="Add Connector",
+ ),
+ )
+ def add_connector(
+ slide_index: int,
+ connector_type: str,
+ start_x: float,
+ start_y: float,
+ end_x: float,
+ end_y: float,
+ line_width: float = 1.0,
+ color: List[int] = None,
+ presentation_id: str = None
+ ) -> Dict:
+ """
+ Add connector lines/arrows between points on a slide.
+
+ Args:
+ slide_index: Index of the slide (0-based)
+ connector_type: Type of connector ("straight", "elbow", "curved")
+ start_x: Starting X coordinate in inches
+ start_y: Starting Y coordinate in inches
+ end_x: Ending X coordinate in inches
+ end_y: Ending Y coordinate in inches
+ line_width: Width of the connector line in points
+ color: RGB color as [r, g, b] list
+ presentation_id: Optional presentation ID (uses current if not provided)
+
+ Returns:
+ Dictionary with operation results
+ """
+ try:
+ # Get presentation
+ pres_id = presentation_id or get_current_presentation_id()
+ if pres_id not in presentations:
+ return {"error": "Presentation not found"}
+
+ pres = presentations[pres_id]
+
+ # Validate slide index
+ if not (0 <= slide_index < len(pres.slides)):
+ return {"error": f"Slide index {slide_index} out of range"}
+
+ slide = pres.slides[slide_index]
+
+ # Map connector types
+ connector_map = {
+ 'straight': MSO_CONNECTOR.STRAIGHT,
+ 'elbow': MSO_CONNECTOR.ELBOW,
+ 'curved': MSO_CONNECTOR.CURVED
+ }
+
+ if connector_type.lower() not in connector_map:
+ return {"error": f"Invalid connector type. Use: {list(connector_map.keys())}"}
+
+ # Add connector
+ connector = slide.shapes.add_connector(
+ connector_map[connector_type.lower()],
+ Inches(start_x), Inches(start_y),
+ Inches(end_x), Inches(end_y)
+ )
+
+ # Apply formatting
+ if line_width:
+ connector.line.width = Pt(line_width)
+
+ if color and is_valid_rgb(color):
+ connector.line.color.rgb = RGBColor(*color)
+
+ return {
+ "message": f"Added {connector_type} connector to slide {slide_index}",
+ "connector_type": connector_type,
+ "start_point": [start_x, start_y],
+ "end_point": [end_x, end_y],
+ "shape_index": len(slide.shapes) - 1
+ }
+
+ except Exception as e:
+ return {"error": f"Failed to add connector: {str(e)}"}
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/tools/content_tools.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/tools/content_tools.py
new file mode 100644
index 00000000..3c25e59b
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/tools/content_tools.py
@@ -0,0 +1,629 @@
+"""
+Content management tools for PowerPoint MCP Server.
+Handles slides, text, images, and content manipulation.
+"""
+from typing import Dict, List, Optional, Any, Union
+from mcp.server.fastmcp import FastMCP
+from mcp.types import ToolAnnotations
+import utils as ppt_utils
+import tempfile
+import base64
+import os
+
+
+def register_content_tools(app: FastMCP, presentations: Dict, get_current_presentation_id, validate_parameters, is_positive, is_non_negative, is_in_range, is_valid_rgb):
+ """Register content management tools with the FastMCP app"""
+
+ @app.tool(
+ annotations=ToolAnnotations(
+ title="Add Slide",
+ ),
+ )
+ def add_slide(
+ layout_index: int = 1,
+ title: Optional[str] = None,
+ background_type: Optional[str] = None, # "solid", "gradient", "professional_gradient"
+ background_colors: Optional[List[List[int]]] = None, # For gradient: [[start_rgb], [end_rgb]]
+ gradient_direction: str = "horizontal",
+ color_scheme: str = "modern_blue",
+ presentation_id: Optional[str] = None
+ ) -> Dict:
+ """Add a new slide to the presentation with optional background styling."""
+ pres_id = presentation_id if presentation_id is not None else get_current_presentation_id()
+
+ if pres_id is None or pres_id not in presentations:
+ return {
+ "error": "No presentation is currently loaded or the specified ID is invalid"
+ }
+
+ pres = presentations[pres_id]
+
+ # Validate layout index
+ if layout_index < 0 or layout_index >= len(pres.slide_layouts):
+ return {
+ "error": f"Invalid layout index: {layout_index}. Available layouts: 0-{len(pres.slide_layouts) - 1}"
+ }
+
+ try:
+ # Add the slide
+ slide, layout = ppt_utils.add_slide(pres, layout_index)
+ slide_index = len(pres.slides) - 1
+
+ # Set title if provided
+ if title:
+ ppt_utils.set_title(slide, title)
+
+ # Apply background if specified
+ if background_type == "gradient" and background_colors and len(background_colors) >= 2:
+ ppt_utils.set_slide_gradient_background(
+ slide, background_colors[0], background_colors[1], gradient_direction
+ )
+ elif background_type == "professional_gradient":
+ ppt_utils.create_professional_gradient_background(
+ slide, color_scheme, "subtle", gradient_direction
+ )
+
+ return {
+ "message": f"Added slide {slide_index} with layout {layout_index}",
+ "slide_index": slide_index,
+ "layout_name": layout.name if hasattr(layout, 'name') else f"Layout {layout_index}"
+ }
+ except Exception as e:
+ return {
+ "error": f"Failed to add slide: {str(e)}"
+ }
+
+ @app.tool(
+ annotations=ToolAnnotations(
+ title="Get Slide Info",
+ readOnlyHint=True,
+ ),
+ )
+ def get_slide_info(slide_index: int, presentation_id: Optional[str] = None) -> Dict:
+ """Get information about a specific slide."""
+ pres_id = presentation_id if presentation_id is not None else get_current_presentation_id()
+
+ if pres_id is None or pres_id not in presentations:
+ return {
+ "error": "No presentation is currently loaded or the specified ID is invalid"
+ }
+
+ pres = presentations[pres_id]
+
+ if slide_index < 0 or slide_index >= len(pres.slides):
+ return {
+ "error": f"Invalid slide index: {slide_index}. Available slides: 0-{len(pres.slides) - 1}"
+ }
+
+ slide = pres.slides[slide_index]
+
+ try:
+ return ppt_utils.get_slide_info(slide, slide_index)
+ except Exception as e:
+ return {
+ "error": f"Failed to get slide info: {str(e)}"
+ }
+
+ @app.tool(
+ annotations=ToolAnnotations(
+ title="Extract Slide Text",
+ readOnlyHint=True,
+ ),
+ )
+ def extract_slide_text(slide_index: int, presentation_id: Optional[str] = None) -> Dict:
+ """Extract all text content from a specific slide."""
+ pres_id = presentation_id if presentation_id is not None else get_current_presentation_id()
+
+ if pres_id is None or pres_id not in presentations:
+ return {
+ "error": "No presentation is currently loaded or the specified ID is invalid"
+ }
+
+ pres = presentations[pres_id]
+
+ if slide_index < 0 or slide_index >= len(pres.slides):
+ return {
+ "error": f"Invalid slide index: {slide_index}. Available slides: 0-{len(pres.slides) - 1}"
+ }
+
+ slide = pres.slides[slide_index]
+
+ try:
+ result = ppt_utils.extract_slide_text_content(slide)
+ result["slide_index"] = slide_index
+ return result
+ except Exception as e:
+ return {
+ "error": f"Failed to extract slide text: {str(e)}"
+ }
+
+ @app.tool(
+ annotations=ToolAnnotations(
+ title="Extract Presentation Text",
+ readOnlyHint=True,
+ ),
+ )
+ def extract_presentation_text(presentation_id: Optional[str] = None, include_slide_info: bool = True) -> Dict:
+ """Extract all text content from all slides in the presentation."""
+ pres_id = presentation_id if presentation_id is not None else get_current_presentation_id()
+
+ if pres_id is None or pres_id not in presentations:
+ return {
+ "error": "No presentation is currently loaded or the specified ID is invalid"
+ }
+
+ pres = presentations[pres_id]
+
+ try:
+ slides_text = []
+ total_text_shapes = 0
+ slides_with_tables = 0
+ slides_with_titles = 0
+ all_presentation_text = []
+
+ for slide_index, slide in enumerate(pres.slides):
+ slide_text_result = ppt_utils.extract_slide_text_content(slide)
+
+ if slide_text_result["success"]:
+ slide_data = {
+ "slide_index": slide_index,
+ "text_content": slide_text_result["text_content"]
+ }
+
+ if include_slide_info:
+ # Add basic slide info
+ slide_data["layout_name"] = slide.slide_layout.name
+ slide_data["total_text_shapes"] = slide_text_result["total_text_shapes"]
+ slide_data["has_title"] = slide_text_result["has_title"]
+ slide_data["has_tables"] = slide_text_result["has_tables"]
+
+ slides_text.append(slide_data)
+
+ # Accumulate statistics
+ total_text_shapes += slide_text_result["total_text_shapes"]
+ if slide_text_result["has_tables"]:
+ slides_with_tables += 1
+ if slide_text_result["has_title"]:
+ slides_with_titles += 1
+
+ # Collect all text for combined output
+ if slide_text_result["text_content"]["all_text_combined"]:
+ all_presentation_text.append(f"=== SLIDE {slide_index + 1} ===")
+ all_presentation_text.append(slide_text_result["text_content"]["all_text_combined"])
+ all_presentation_text.append("") # Empty line separator
+ else:
+ slides_text.append({
+ "slide_index": slide_index,
+ "error": slide_text_result.get("error", "Unknown error"),
+ "text_content": None
+ })
+
+ return {
+ "success": True,
+ "presentation_id": pres_id,
+ "total_slides": len(pres.slides),
+ "slides_with_text": len([s for s in slides_text if s.get("text_content") is not None]),
+ "total_text_shapes": total_text_shapes,
+ "slides_with_titles": slides_with_titles,
+ "slides_with_tables": slides_with_tables,
+ "slides_text": slides_text,
+ "all_presentation_text_combined": "\n".join(all_presentation_text)
+ }
+
+ except Exception as e:
+ return {
+ "error": f"Failed to extract presentation text: {str(e)}"
+ }
+
+ @app.tool(
+ annotations=ToolAnnotations(
+ title="Populate Placeholder",
+ ),
+ )
+ def populate_placeholder(
+ slide_index: int,
+ placeholder_idx: int,
+ text: str,
+ presentation_id: Optional[str] = None
+ ) -> Dict:
+ """Populate a placeholder with text."""
+ pres_id = presentation_id if presentation_id is not None else get_current_presentation_id()
+
+ if pres_id is None or pres_id not in presentations:
+ return {
+ "error": "No presentation is currently loaded or the specified ID is invalid"
+ }
+
+ pres = presentations[pres_id]
+
+ if slide_index < 0 or slide_index >= len(pres.slides):
+ return {
+ "error": f"Invalid slide index: {slide_index}. Available slides: 0-{len(pres.slides) - 1}"
+ }
+
+ slide = pres.slides[slide_index]
+
+ try:
+ ppt_utils.populate_placeholder(slide, placeholder_idx, text)
+ return {
+ "message": f"Populated placeholder {placeholder_idx} on slide {slide_index}"
+ }
+ except Exception as e:
+ return {
+ "error": f"Failed to populate placeholder: {str(e)}"
+ }
+
+ @app.tool(
+ annotations=ToolAnnotations(
+ title="Add Bullet Points",
+ ),
+ )
+ def add_bullet_points(
+ slide_index: int,
+ placeholder_idx: int,
+ bullet_points: List[str],
+ presentation_id: Optional[str] = None
+ ) -> Dict:
+ """Add bullet points to a placeholder."""
+ pres_id = presentation_id if presentation_id is not None else get_current_presentation_id()
+
+ if pres_id is None or pres_id not in presentations:
+ return {
+ "error": "No presentation is currently loaded or the specified ID is invalid"
+ }
+
+ pres = presentations[pres_id]
+
+ if slide_index < 0 or slide_index >= len(pres.slides):
+ return {
+ "error": f"Invalid slide index: {slide_index}. Available slides: 0-{len(pres.slides) - 1}"
+ }
+
+ slide = pres.slides[slide_index]
+
+ try:
+ placeholder = slide.placeholders[placeholder_idx]
+ ppt_utils.add_bullet_points(placeholder, bullet_points)
+ return {
+ "message": f"Added {len(bullet_points)} bullet points to placeholder {placeholder_idx} on slide {slide_index}"
+ }
+ except Exception as e:
+ return {
+ "error": f"Failed to add bullet points: {str(e)}"
+ }
+
+ @app.tool(
+ annotations=ToolAnnotations(
+ title="Manage Text",
+ ),
+ )
+ def manage_text(
+ slide_index: int,
+ operation: str, # "add", "format", "validate", "format_runs"
+ left: float = 1.0,
+ top: float = 1.0,
+ width: float = 4.0,
+ height: float = 2.0,
+ text: str = "",
+ shape_index: Optional[int] = None, # For format/validate operations
+ text_runs: Optional[List[Dict]] = None, # For format_runs operation
+ # Formatting options
+ font_size: Optional[int] = None,
+ font_name: Optional[str] = None,
+ bold: Optional[bool] = None,
+ italic: Optional[bool] = None,
+ underline: Optional[bool] = None,
+ color: Optional[List[int]] = None,
+ bg_color: Optional[List[int]] = None,
+ alignment: Optional[str] = None,
+ vertical_alignment: Optional[str] = None,
+ # Advanced options
+ auto_fit: bool = True,
+ validation_only: bool = False,
+ min_font_size: int = 8,
+ max_font_size: int = 72,
+ presentation_id: Optional[str] = None
+ ) -> Dict:
+ """Unified text management tool for adding, formatting, validating text, and formatting multiple text runs."""
+ pres_id = presentation_id if presentation_id is not None else get_current_presentation_id()
+
+ if pres_id is None or pres_id not in presentations:
+ return {
+ "error": "No presentation is currently loaded or the specified ID is invalid"
+ }
+
+ pres = presentations[pres_id]
+
+ if slide_index < 0 or slide_index >= len(pres.slides):
+ return {
+ "error": f"Invalid slide index: {slide_index}. Available slides: 0-{len(pres.slides) - 1}"
+ }
+
+ slide = pres.slides[slide_index]
+
+ # Validate parameters
+ validations = {}
+ if font_size is not None:
+ validations["font_size"] = (font_size, [(is_positive, "must be a positive integer")])
+ if color is not None:
+ validations["color"] = (color, [(is_valid_rgb, "must be a valid RGB list [R, G, B] with values 0-255")])
+ if bg_color is not None:
+ validations["bg_color"] = (bg_color, [(is_valid_rgb, "must be a valid RGB list [R, G, B] with values 0-255")])
+
+ if validations:
+ valid, error = validate_parameters(validations)
+ if not valid:
+ return {"error": error}
+
+ try:
+ if operation == "add":
+ # Add new textbox
+ shape = ppt_utils.add_textbox(
+ slide, left, top, width, height, text,
+ font_size=font_size,
+ font_name=font_name,
+ bold=bold,
+ italic=italic,
+ underline=underline,
+ color=tuple(color) if color else None,
+ bg_color=tuple(bg_color) if bg_color else None,
+ alignment=alignment,
+ vertical_alignment=vertical_alignment,
+ auto_fit=auto_fit
+ )
+ return {
+ "message": f"Added text box to slide {slide_index}",
+ "shape_index": len(slide.shapes) - 1,
+ "text": text
+ }
+
+ elif operation == "format":
+ # Format existing text shape
+ if shape_index is None or shape_index < 0 or shape_index >= len(slide.shapes):
+ return {
+ "error": f"Invalid shape index for formatting: {shape_index}. Available shapes: 0-{len(slide.shapes) - 1}"
+ }
+
+ shape = slide.shapes[shape_index]
+ ppt_utils.format_text_advanced(
+ shape,
+ font_size=font_size,
+ font_name=font_name,
+ bold=bold,
+ italic=italic,
+ underline=underline,
+ color=tuple(color) if color else None,
+ bg_color=tuple(bg_color) if bg_color else None,
+ alignment=alignment,
+ vertical_alignment=vertical_alignment
+ )
+ return {
+ "message": f"Formatted text shape {shape_index} on slide {slide_index}"
+ }
+
+ elif operation == "validate":
+ # Validate text fit
+ if shape_index is None or shape_index < 0 or shape_index >= len(slide.shapes):
+ return {
+ "error": f"Invalid shape index for validation: {shape_index}. Available shapes: 0-{len(slide.shapes) - 1}"
+ }
+
+ validation_result = ppt_utils.validate_text_fit(
+ slide.shapes[shape_index],
+ text_content=text or None,
+ font_size=font_size or 12
+ )
+
+ if not validation_only and validation_result.get("needs_optimization"):
+ # Apply automatic fixes
+ fix_result = ppt_utils.validate_and_fix_slide(
+ slide,
+ auto_fix=True,
+ min_font_size=min_font_size,
+ max_font_size=max_font_size
+ )
+ validation_result.update(fix_result)
+
+ return validation_result
+
+ elif operation == "format_runs":
+ # Format multiple text runs with different formatting
+ if shape_index is None or shape_index < 0 or shape_index >= len(slide.shapes):
+ return {
+ "error": f"Invalid shape index for format_runs: {shape_index}. Available shapes: 0-{len(slide.shapes) - 1}"
+ }
+
+ if not text_runs:
+ return {"error": "text_runs parameter is required for format_runs operation"}
+
+ shape = slide.shapes[shape_index]
+
+ # Check if shape has text
+ if not hasattr(shape, 'text_frame') or not shape.text_frame:
+ return {"error": "Shape does not contain text"}
+
+ # Clear existing text and rebuild with formatted runs
+ text_frame = shape.text_frame
+ text_frame.clear()
+
+ formatted_runs = []
+
+ for run_data in text_runs:
+ if 'text' not in run_data:
+ continue
+
+ # Add paragraph if needed
+ if not text_frame.paragraphs:
+ paragraph = text_frame.paragraphs[0]
+ else:
+ paragraph = text_frame.add_paragraph()
+
+ # Add run with text
+ run = paragraph.add_run()
+ run.text = run_data['text']
+
+ # Apply formatting using pptx imports
+ from pptx.util import Pt
+ from pptx.dml.color import RGBColor
+
+ if 'bold' in run_data:
+ run.font.bold = run_data['bold']
+ if 'italic' in run_data:
+ run.font.italic = run_data['italic']
+ if 'underline' in run_data:
+ run.font.underline = run_data['underline']
+ if 'font_size' in run_data:
+ run.font.size = Pt(run_data['font_size'])
+ if 'font_name' in run_data:
+ run.font.name = run_data['font_name']
+ if 'color' in run_data and is_valid_rgb(run_data['color']):
+ run.font.color.rgb = RGBColor(*run_data['color'])
+ if 'hyperlink' in run_data:
+ run.hyperlink.address = run_data['hyperlink']
+
+ formatted_runs.append({
+ "text": run_data['text'],
+ "formatting_applied": {k: v for k, v in run_data.items() if k != 'text'}
+ })
+
+ return {
+ "message": f"Applied formatting to {len(formatted_runs)} text runs on shape {shape_index}",
+ "slide_index": slide_index,
+ "shape_index": shape_index,
+ "formatted_runs": formatted_runs
+ }
+
+ else:
+ return {
+ "error": f"Invalid operation: {operation}. Must be 'add', 'format', 'validate', or 'format_runs'"
+ }
+
+ except Exception as e:
+ return {
+ "error": f"Failed to {operation} text: {str(e)}"
+ }
+
+ @app.tool(
+ annotations=ToolAnnotations(
+ title="Manage Image",
+ ),
+ )
+ def manage_image(
+ slide_index: int,
+ operation: str, # "add", "enhance"
+ image_source: str, # file path or base64 string
+ source_type: str = "file", # "file" or "base64"
+ left: float = 1.0,
+ top: float = 1.0,
+ width: Optional[float] = None,
+ height: Optional[float] = None,
+ # Enhancement options
+ enhancement_style: Optional[str] = None, # "presentation", "custom"
+ brightness: float = 1.0,
+ contrast: float = 1.0,
+ saturation: float = 1.0,
+ sharpness: float = 1.0,
+ blur_radius: float = 0,
+ filter_type: Optional[str] = None,
+ output_path: Optional[str] = None,
+ presentation_id: Optional[str] = None
+ ) -> Dict:
+ """Unified image management tool for adding and enhancing images."""
+ pres_id = presentation_id if presentation_id is not None else get_current_presentation_id()
+
+ if pres_id is None or pres_id not in presentations:
+ return {
+ "error": "No presentation is currently loaded or the specified ID is invalid"
+ }
+
+ pres = presentations[pres_id]
+
+ if slide_index < 0 or slide_index >= len(pres.slides):
+ return {
+ "error": f"Invalid slide index: {slide_index}. Available slides: 0-{len(pres.slides) - 1}"
+ }
+
+ slide = pres.slides[slide_index]
+
+ try:
+ if operation == "add":
+ if source_type == "base64":
+ # Handle base64 image
+ try:
+ image_data = base64.b64decode(image_source)
+ with tempfile.NamedTemporaryFile(delete=False, suffix='.png') as temp_file:
+ temp_file.write(image_data)
+ temp_path = temp_file.name
+
+ # Add image from temporary file
+ shape = ppt_utils.add_image(slide, temp_path, left, top, width, height)
+
+ # Clean up temporary file
+ os.unlink(temp_path)
+
+ return {
+ "message": f"Added image from base64 to slide {slide_index}",
+ "shape_index": len(slide.shapes) - 1
+ }
+ except Exception as e:
+ return {
+ "error": f"Failed to process base64 image: {str(e)}"
+ }
+ else:
+ # Handle file path
+ if not os.path.exists(image_source):
+ return {
+ "error": f"Image file not found: {image_source}"
+ }
+
+ shape = ppt_utils.add_image(slide, image_source, left, top, width, height)
+ return {
+ "message": f"Added image to slide {slide_index}",
+ "shape_index": len(slide.shapes) - 1,
+ "image_path": image_source
+ }
+
+ elif operation == "enhance":
+ # Enhance existing image file
+ if source_type == "base64":
+ return {
+ "error": "Enhancement operation requires file path, not base64 data"
+ }
+
+ if not os.path.exists(image_source):
+ return {
+ "error": f"Image file not found: {image_source}"
+ }
+
+ if enhancement_style == "presentation":
+ # Apply professional enhancement
+ enhanced_path = ppt_utils.apply_professional_image_enhancement(
+ image_source, style="presentation", output_path=output_path
+ )
+ else:
+ # Apply custom enhancement
+ enhanced_path = ppt_utils.enhance_image_with_pillow(
+ image_source,
+ brightness=brightness,
+ contrast=contrast,
+ saturation=saturation,
+ sharpness=sharpness,
+ blur_radius=blur_radius,
+ filter_type=filter_type,
+ output_path=output_path
+ )
+
+ return {
+ "message": f"Enhanced image: {image_source}",
+ "enhanced_path": enhanced_path
+ }
+
+ else:
+ return {
+ "error": f"Invalid operation: {operation}. Must be 'add' or 'enhance'"
+ }
+
+ except Exception as e:
+ return {
+ "error": f"Failed to {operation} image: {str(e)}"
+ }
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/tools/hyperlink_tools.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/tools/hyperlink_tools.py
new file mode 100644
index 00000000..fd045377
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/tools/hyperlink_tools.py
@@ -0,0 +1,143 @@
+"""
+Hyperlink management tools for PowerPoint MCP Server.
+Implements hyperlink operations for text shapes and runs.
+"""
+
+from typing import Dict, List, Optional, Any
+from mcp.types import ToolAnnotations
+
+def register_hyperlink_tools(app, presentations, get_current_presentation_id, validate_parameters,
+ is_positive, is_non_negative, is_in_range, is_valid_rgb):
+ """Register hyperlink management tools with the FastMCP app."""
+
+ @app.tool(
+ annotations=ToolAnnotations(
+ title="Manage Hyperlinks",
+ ),
+ )
+ def manage_hyperlinks(
+ operation: str,
+ slide_index: int,
+ shape_index: int = None,
+ text: str = None,
+ url: str = None,
+ run_index: int = 0,
+ presentation_id: str = None
+ ) -> Dict:
+ """
+ Manage hyperlinks in text shapes and runs.
+
+ Args:
+ operation: Operation type ("add", "remove", "list", "update")
+ slide_index: Index of the slide (0-based)
+ shape_index: Index of the shape on the slide (0-based)
+ text: Text to make into hyperlink (for "add" operation)
+ url: URL for the hyperlink
+ run_index: Index of text run within the shape (0-based)
+ presentation_id: Optional presentation ID (uses current if not provided)
+
+ Returns:
+ Dictionary with operation results
+ """
+ try:
+ # Get presentation
+ pres_id = presentation_id or get_current_presentation_id()
+ if pres_id not in presentations:
+ return {"error": "Presentation not found"}
+
+ pres = presentations[pres_id]
+
+ # Validate slide index
+ if not (0 <= slide_index < len(pres.slides)):
+ return {"error": f"Slide index {slide_index} out of range"}
+
+ slide = pres.slides[slide_index]
+
+ if operation == "list":
+ # List all hyperlinks in the slide
+ hyperlinks = []
+ for shape_idx, shape in enumerate(slide.shapes):
+ if hasattr(shape, 'text_frame') and shape.text_frame:
+ for para_idx, paragraph in enumerate(shape.text_frame.paragraphs):
+ for run_idx, run in enumerate(paragraph.runs):
+ if run.hyperlink.address:
+ hyperlinks.append({
+ "shape_index": shape_idx,
+ "paragraph_index": para_idx,
+ "run_index": run_idx,
+ "text": run.text,
+ "url": run.hyperlink.address
+ })
+
+ return {
+ "message": f"Found {len(hyperlinks)} hyperlinks on slide {slide_index}",
+ "hyperlinks": hyperlinks
+ }
+
+ # For other operations, validate shape index
+ if shape_index is None or not (0 <= shape_index < len(slide.shapes)):
+ return {"error": f"Shape index {shape_index} out of range"}
+
+ shape = slide.shapes[shape_index]
+
+ # Check if shape has text
+ if not hasattr(shape, 'text_frame') or not shape.text_frame:
+ return {"error": "Shape does not contain text"}
+
+ if operation == "add":
+ if not text or not url:
+ return {"error": "Both 'text' and 'url' are required for adding hyperlinks"}
+
+ # Add new text run with hyperlink
+ paragraph = shape.text_frame.paragraphs[0]
+ run = paragraph.add_run()
+ run.text = text
+ run.hyperlink.address = url
+
+ return {
+ "message": f"Added hyperlink '{text}' -> '{url}' to shape {shape_index}",
+ "text": text,
+ "url": url
+ }
+
+ elif operation == "update":
+ if not url:
+ return {"error": "URL is required for updating hyperlinks"}
+
+ # Update existing hyperlink
+ paragraphs = shape.text_frame.paragraphs
+ if run_index < len(paragraphs[0].runs):
+ run = paragraphs[0].runs[run_index]
+ old_url = run.hyperlink.address
+ run.hyperlink.address = url
+
+ return {
+ "message": f"Updated hyperlink from '{old_url}' to '{url}'",
+ "old_url": old_url,
+ "new_url": url,
+ "text": run.text
+ }
+ else:
+ return {"error": f"Run index {run_index} out of range"}
+
+ elif operation == "remove":
+ # Remove hyperlink from specific run
+ paragraphs = shape.text_frame.paragraphs
+ if run_index < len(paragraphs[0].runs):
+ run = paragraphs[0].runs[run_index]
+ old_url = run.hyperlink.address
+ run.hyperlink.address = None
+
+ return {
+ "message": f"Removed hyperlink '{old_url}' from text '{run.text}'",
+ "removed_url": old_url,
+ "text": run.text
+ }
+ else:
+ return {"error": f"Run index {run_index} out of range"}
+
+ else:
+ return {"error": f"Unsupported operation: {operation}. Use 'add', 'remove', 'list', or 'update'"}
+
+ except Exception as e:
+ return {"error": f"Failed to manage hyperlinks: {str(e)}"}
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/tools/master_tools.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/tools/master_tools.py
new file mode 100644
index 00000000..b2446177
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/tools/master_tools.py
@@ -0,0 +1,119 @@
+"""
+Slide master management tools for PowerPoint MCP Server.
+Implements slide master and layout access capabilities.
+"""
+
+from typing import Dict, List, Optional, Any
+from mcp.types import ToolAnnotations
+
+def register_master_tools(app, presentations, get_current_presentation_id, validate_parameters,
+ is_positive, is_non_negative, is_in_range, is_valid_rgb):
+ """Register slide master management tools with the FastMCP app."""
+
+ @app.tool(
+ annotations=ToolAnnotations(
+ title="Manage Slide Masters",
+ ),
+ )
+ def manage_slide_masters(
+ operation: str,
+ master_index: int = 0,
+ layout_index: int = None,
+ presentation_id: str = None
+ ) -> Dict:
+ """
+ Access and manage slide master properties and layouts.
+
+ Args:
+ operation: Operation type ("list", "get_layouts", "get_info")
+ master_index: Index of the slide master (0-based)
+ layout_index: Index of specific layout within master (0-based)
+ presentation_id: Optional presentation ID (uses current if not provided)
+
+ Returns:
+ Dictionary with slide master information
+ """
+ try:
+ # Get presentation
+ pres_id = presentation_id or get_current_presentation_id()
+ if pres_id not in presentations:
+ return {"error": "Presentation not found"}
+
+ pres = presentations[pres_id]
+
+ if operation == "list":
+ # List all slide masters
+ masters_info = []
+ for idx, master in enumerate(pres.slide_masters):
+ masters_info.append({
+ "index": idx,
+ "layout_count": len(master.slide_layouts),
+ "name": getattr(master, 'name', f"Master {idx}")
+ })
+
+ return {
+ "message": f"Found {len(masters_info)} slide masters",
+ "masters": masters_info,
+ "total_masters": len(pres.slide_masters)
+ }
+
+ # Validate master index
+ if not (0 <= master_index < len(pres.slide_masters)):
+ return {"error": f"Master index {master_index} out of range"}
+
+ master = pres.slide_masters[master_index]
+
+ if operation == "get_layouts":
+ # Get all layouts for a specific master
+ layouts_info = []
+ for idx, layout in enumerate(master.slide_layouts):
+ layouts_info.append({
+ "index": idx,
+ "name": layout.name,
+ "placeholder_count": len(layout.placeholders) if hasattr(layout, 'placeholders') else 0
+ })
+
+ return {
+ "message": f"Master {master_index} has {len(layouts_info)} layouts",
+ "master_index": master_index,
+ "layouts": layouts_info
+ }
+
+ elif operation == "get_info":
+ # Get detailed info about master or specific layout
+ if layout_index is not None:
+ if not (0 <= layout_index < len(master.slide_layouts)):
+ return {"error": f"Layout index {layout_index} out of range"}
+
+ layout = master.slide_layouts[layout_index]
+ placeholders_info = []
+
+ if hasattr(layout, 'placeholders'):
+ for placeholder in layout.placeholders:
+ placeholders_info.append({
+ "idx": placeholder.placeholder_format.idx,
+ "type": str(placeholder.placeholder_format.type),
+ "name": getattr(placeholder, 'name', 'Unnamed')
+ })
+
+ return {
+ "message": f"Layout info for master {master_index}, layout {layout_index}",
+ "master_index": master_index,
+ "layout_index": layout_index,
+ "layout_name": layout.name,
+ "placeholders": placeholders_info
+ }
+ else:
+ # Master info
+ return {
+ "message": f"Master {master_index} information",
+ "master_index": master_index,
+ "layout_count": len(master.slide_layouts),
+ "name": getattr(master, 'name', f"Master {master_index}")
+ }
+
+ else:
+ return {"error": f"Unsupported operation: {operation}. Use 'list', 'get_layouts', or 'get_info'"}
+
+ except Exception as e:
+ return {"error": f"Failed to manage slide masters: {str(e)}"}
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/tools/presentation_tools.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/tools/presentation_tools.py
new file mode 100644
index 00000000..2f1ee5c7
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/tools/presentation_tools.py
@@ -0,0 +1,245 @@
+"""
+Presentation management tools for PowerPoint MCP Server.
+Handles presentation creation, opening, saving, and core properties.
+"""
+from typing import Dict, List, Optional, Any
+import os
+from mcp.server.fastmcp import FastMCP
+from mcp.types import ToolAnnotations
+import utils as ppt_utils
+
+
+def register_presentation_tools(app: FastMCP, presentations: Dict, get_current_presentation_id, get_template_search_directories):
+ """Register presentation management tools with the FastMCP app"""
+
+ @app.tool(
+ annotations=ToolAnnotations(
+ title="Create Presentation",
+ ),
+ )
+ def create_presentation(id: Optional[str] = None) -> Dict:
+ """Create a new PowerPoint presentation."""
+ # Create a new presentation
+ pres = ppt_utils.create_presentation()
+
+ # Generate an ID if not provided
+ if id is None:
+ id = f"presentation_{len(presentations) + 1}"
+
+ # Store the presentation
+ presentations[id] = pres
+ # Set as current presentation (this would need to be handled by caller)
+
+ return {
+ "presentation_id": id,
+ "message": f"Created new presentation with ID: {id}",
+ "slide_count": len(pres.slides)
+ }
+
+ @app.tool(
+ annotations=ToolAnnotations(
+ title="Create Presentation from Template",
+ ),
+ )
+ def create_presentation_from_template(template_path: str, id: Optional[str] = None) -> Dict:
+ """Create a new PowerPoint presentation from a template file."""
+ # Check if template file exists
+ if not os.path.exists(template_path):
+ # Try to find the template by searching in configured directories
+ search_dirs = get_template_search_directories()
+ template_name = os.path.basename(template_path)
+
+ for directory in search_dirs:
+ potential_path = os.path.join(directory, template_name)
+ if os.path.exists(potential_path):
+ template_path = potential_path
+ break
+ else:
+ env_path_info = f" (PPT_TEMPLATE_PATH: {os.environ.get('PPT_TEMPLATE_PATH', 'not set')})" if os.environ.get('PPT_TEMPLATE_PATH') else ""
+ return {
+ "error": f"Template file not found: {template_path}. Searched in {', '.join(search_dirs)}{env_path_info}"
+ }
+
+ # Create presentation from template
+ try:
+ pres = ppt_utils.create_presentation_from_template(template_path)
+ except Exception as e:
+ return {
+ "error": f"Failed to create presentation from template: {str(e)}"
+ }
+
+ # Generate an ID if not provided
+ if id is None:
+ id = f"presentation_{len(presentations) + 1}"
+
+ # Store the presentation
+ presentations[id] = pres
+
+ return {
+ "presentation_id": id,
+ "message": f"Created new presentation from template '{template_path}' with ID: {id}",
+ "template_path": template_path,
+ "slide_count": len(pres.slides),
+ "layout_count": len(pres.slide_layouts)
+ }
+
+ @app.tool(
+ annotations=ToolAnnotations(
+ title="Open Presentation",
+ readOnlyHint=True,
+ ),
+ )
+ def open_presentation(file_path: str, id: Optional[str] = None) -> Dict:
+ """Open an existing PowerPoint presentation from a file."""
+ # Check if file exists
+ if not os.path.exists(file_path):
+ return {
+ "error": f"File not found: {file_path}"
+ }
+
+ # Open the presentation
+ try:
+ pres = ppt_utils.open_presentation(file_path)
+ except Exception as e:
+ return {
+ "error": f"Failed to open presentation: {str(e)}"
+ }
+
+ # Generate an ID if not provided
+ if id is None:
+ id = f"presentation_{len(presentations) + 1}"
+
+ # Store the presentation
+ presentations[id] = pres
+
+ return {
+ "presentation_id": id,
+ "message": f"Opened presentation from {file_path} with ID: {id}",
+ "slide_count": len(pres.slides)
+ }
+
+ @app.tool(
+ annotations=ToolAnnotations(
+ title="Save Presentation",
+ destructiveHint=True,
+ ),
+ )
+ def save_presentation(file_path: str, presentation_id: Optional[str] = None) -> Dict:
+ """Save a presentation to a file."""
+ # Use the specified presentation or the current one
+ pres_id = presentation_id if presentation_id is not None else get_current_presentation_id()
+
+ if pres_id is None or pres_id not in presentations:
+ return {
+ "error": "No presentation is currently loaded or the specified ID is invalid"
+ }
+
+ # Save the presentation
+ try:
+ saved_path = ppt_utils.save_presentation(presentations[pres_id], file_path)
+ return {
+ "message": f"Presentation saved to {saved_path}",
+ "file_path": saved_path
+ }
+ except Exception as e:
+ return {
+ "error": f"Failed to save presentation: {str(e)}"
+ }
+
+ @app.tool(
+ annotations=ToolAnnotations(
+ title="Get Presentation Info",
+ readOnlyHint=True,
+ ),
+ )
+ def get_presentation_info(presentation_id: Optional[str] = None) -> Dict:
+ """Get information about a presentation."""
+ pres_id = presentation_id if presentation_id is not None else get_current_presentation_id()
+
+ if pres_id is None or pres_id not in presentations:
+ return {
+ "error": "No presentation is currently loaded or the specified ID is invalid"
+ }
+
+ pres = presentations[pres_id]
+
+ try:
+ info = ppt_utils.get_presentation_info(pres)
+ info["presentation_id"] = pres_id
+ return info
+ except Exception as e:
+ return {
+ "error": f"Failed to get presentation info: {str(e)}"
+ }
+
+ @app.tool(
+ annotations=ToolAnnotations(
+ title="Get Template File Info",
+ readOnlyHint=True,
+ ),
+ )
+ def get_template_file_info(template_path: str) -> Dict:
+ """Get information about a template file including layouts and properties."""
+ # Check if template file exists
+ if not os.path.exists(template_path):
+ # Try to find the template by searching in configured directories
+ search_dirs = get_template_search_directories()
+ template_name = os.path.basename(template_path)
+
+ for directory in search_dirs:
+ potential_path = os.path.join(directory, template_name)
+ if os.path.exists(potential_path):
+ template_path = potential_path
+ break
+ else:
+ return {
+ "error": f"Template file not found: {template_path}. Searched in {', '.join(search_dirs)}"
+ }
+
+ try:
+ return ppt_utils.get_template_info(template_path)
+ except Exception as e:
+ return {
+ "error": f"Failed to get template info: {str(e)}"
+ }
+
+ @app.tool(
+ annotations=ToolAnnotations(
+ title="Set Core Properties",
+ ),
+ )
+ def set_core_properties(
+ title: Optional[str] = None,
+ subject: Optional[str] = None,
+ author: Optional[str] = None,
+ keywords: Optional[str] = None,
+ comments: Optional[str] = None,
+ presentation_id: Optional[str] = None
+ ) -> Dict:
+ """Set core document properties."""
+ pres_id = presentation_id if presentation_id is not None else get_current_presentation_id()
+
+ if pres_id is None or pres_id not in presentations:
+ return {
+ "error": "No presentation is currently loaded or the specified ID is invalid"
+ }
+
+ pres = presentations[pres_id]
+
+ try:
+ ppt_utils.set_core_properties(
+ pres,
+ title=title,
+ subject=subject,
+ author=author,
+ keywords=keywords,
+ comments=comments
+ )
+
+ return {
+ "message": "Core properties updated successfully"
+ }
+ except Exception as e:
+ return {
+ "error": f"Failed to set core properties: {str(e)}"
+ }
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/tools/professional_tools.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/tools/professional_tools.py
new file mode 100644
index 00000000..d9ef70b0
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/tools/professional_tools.py
@@ -0,0 +1,303 @@
+"""
+Professional design tools for PowerPoint MCP Server.
+Handles themes, effects, fonts, and advanced formatting.
+"""
+from typing import Dict, List, Optional, Any
+from mcp.server.fastmcp import FastMCP
+from mcp.types import ToolAnnotations
+import utils as ppt_utils
+
+
+def register_professional_tools(app: FastMCP, presentations: Dict, get_current_presentation_id):
+ """Register professional design tools with the FastMCP app"""
+
+ @app.tool(
+ annotations=ToolAnnotations(
+ title="Apply Professional Design",
+ ),
+ )
+ def apply_professional_design(
+ operation: str, # "professional_slide", "theme", "enhance", "get_schemes"
+ slide_index: Optional[int] = None,
+ slide_type: str = "title_content",
+ color_scheme: str = "modern_blue",
+ title: Optional[str] = None,
+ content: Optional[List[str]] = None,
+ apply_to_existing: bool = True,
+ enhance_title: bool = True,
+ enhance_content: bool = True,
+ enhance_shapes: bool = True,
+ enhance_charts: bool = True,
+ presentation_id: Optional[str] = None
+ ) -> Dict:
+ """Unified professional design tool for themes, slides, and visual enhancements.
+ This applies professional styling and themes rather than structural layout changes."""
+ pres_id = presentation_id if presentation_id is not None else get_current_presentation_id()
+
+ if operation == "get_schemes":
+ # Return available color schemes
+ return ppt_utils.get_color_schemes()
+
+ if pres_id is None or pres_id not in presentations:
+ return {
+ "error": "No presentation is currently loaded or the specified ID is invalid"
+ }
+
+ pres = presentations[pres_id]
+
+ try:
+ if operation == "professional_slide":
+ # Add professional slide with advanced styling
+ if slide_index is not None and (slide_index < 0 or slide_index >= len(pres.slides)):
+ return {
+ "error": f"Invalid slide index: {slide_index}. Available slides: 0-{len(pres.slides) - 1}"
+ }
+
+ result = ppt_utils.add_professional_slide(
+ pres,
+ slide_type=slide_type,
+ color_scheme=color_scheme,
+ title=title,
+ content=content
+ )
+
+ return {
+ "message": f"Added professional {slide_type} slide",
+ "slide_index": len(pres.slides) - 1,
+ "color_scheme": color_scheme,
+ "slide_type": slide_type
+ }
+
+ elif operation == "theme":
+ # Apply professional theme
+ ppt_utils.apply_professional_theme(
+ pres,
+ color_scheme=color_scheme,
+ apply_to_existing=apply_to_existing
+ )
+
+ return {
+ "message": f"Applied {color_scheme} theme to presentation",
+ "color_scheme": color_scheme,
+ "applied_to_existing": apply_to_existing
+ }
+
+ elif operation == "enhance":
+ # Enhance existing slide
+ if slide_index is None:
+ return {
+ "error": "slide_index is required for enhance operation"
+ }
+
+ if slide_index < 0 or slide_index >= len(pres.slides):
+ return {
+ "error": f"Invalid slide index: {slide_index}. Available slides: 0-{len(pres.slides) - 1}"
+ }
+
+ slide = pres.slides[slide_index]
+ result = ppt_utils.enhance_existing_slide(
+ slide,
+ color_scheme=color_scheme,
+ enhance_title=enhance_title,
+ enhance_content=enhance_content,
+ enhance_shapes=enhance_shapes,
+ enhance_charts=enhance_charts
+ )
+
+ return {
+ "message": f"Enhanced slide {slide_index} with {color_scheme} scheme",
+ "slide_index": slide_index,
+ "color_scheme": color_scheme,
+ "enhancements_applied": result.get("enhancements_applied", [])
+ }
+
+ else:
+ return {
+ "error": f"Invalid operation: {operation}. Must be 'slide', 'theme', 'enhance', or 'get_schemes'"
+ }
+
+ except Exception as e:
+ return {
+ "error": f"Failed to apply professional design: {str(e)}"
+ }
+
+ @app.tool(
+ annotations=ToolAnnotations(
+ title="Apply Picture Effects",
+ ),
+ )
+ def apply_picture_effects(
+ slide_index: int,
+ shape_index: int,
+ effects: Dict[str, Dict], # {"shadow": {"blur_radius": 4.0, ...}, "glow": {...}}
+ presentation_id: Optional[str] = None
+ ) -> Dict:
+ """Apply multiple picture effects in combination."""
+ pres_id = presentation_id if presentation_id is not None else get_current_presentation_id()
+
+ if pres_id is None or pres_id not in presentations:
+ return {
+ "error": "No presentation is currently loaded or the specified ID is invalid"
+ }
+
+ pres = presentations[pres_id]
+
+ if slide_index < 0 or slide_index >= len(pres.slides):
+ return {
+ "error": f"Invalid slide index: {slide_index}. Available slides: 0-{len(pres.slides) - 1}"
+ }
+
+ slide = pres.slides[slide_index]
+
+ if shape_index < 0 or shape_index >= len(slide.shapes):
+ return {
+ "error": f"Invalid shape index: {shape_index}. Available shapes: 0-{len(slide.shapes) - 1}"
+ }
+
+ shape = slide.shapes[shape_index]
+
+ try:
+ applied_effects = []
+ warnings = []
+
+ # Apply each effect
+ for effect_type, effect_params in effects.items():
+ try:
+ if effect_type == "shadow":
+ ppt_utils.apply_picture_shadow(
+ shape,
+ shadow_type=effect_params.get("shadow_type", "outer"),
+ blur_radius=effect_params.get("blur_radius", 4.0),
+ distance=effect_params.get("distance", 3.0),
+ direction=effect_params.get("direction", 315.0),
+ color=effect_params.get("color", [0, 0, 0]),
+ transparency=effect_params.get("transparency", 0.6)
+ )
+ applied_effects.append("shadow")
+
+ elif effect_type == "reflection":
+ ppt_utils.apply_picture_reflection(
+ shape,
+ size=effect_params.get("size", 0.5),
+ transparency=effect_params.get("transparency", 0.5),
+ distance=effect_params.get("distance", 0.0),
+ blur=effect_params.get("blur", 4.0)
+ )
+ applied_effects.append("reflection")
+
+ elif effect_type == "glow":
+ ppt_utils.apply_picture_glow(
+ shape,
+ size=effect_params.get("size", 5.0),
+ color=effect_params.get("color", [0, 176, 240]),
+ transparency=effect_params.get("transparency", 0.4)
+ )
+ applied_effects.append("glow")
+
+ elif effect_type == "soft_edges":
+ ppt_utils.apply_picture_soft_edges(
+ shape,
+ radius=effect_params.get("radius", 2.5)
+ )
+ applied_effects.append("soft_edges")
+
+ elif effect_type == "rotation":
+ ppt_utils.apply_picture_rotation(
+ shape,
+ rotation=effect_params.get("rotation", 0.0)
+ )
+ applied_effects.append("rotation")
+
+ elif effect_type == "transparency":
+ ppt_utils.apply_picture_transparency(
+ shape,
+ transparency=effect_params.get("transparency", 0.0)
+ )
+ applied_effects.append("transparency")
+
+ elif effect_type == "bevel":
+ ppt_utils.apply_picture_bevel(
+ shape,
+ bevel_type=effect_params.get("bevel_type", "circle"),
+ width=effect_params.get("width", 6.0),
+ height=effect_params.get("height", 6.0)
+ )
+ applied_effects.append("bevel")
+
+ elif effect_type == "filter":
+ ppt_utils.apply_picture_filter(
+ shape,
+ filter_type=effect_params.get("filter_type", "none"),
+ intensity=effect_params.get("intensity", 0.5)
+ )
+ applied_effects.append("filter")
+
+ else:
+ warnings.append(f"Unknown effect type: {effect_type}")
+
+ except Exception as e:
+ warnings.append(f"Failed to apply {effect_type} effect: {str(e)}")
+
+ result = {
+ "message": f"Applied {len(applied_effects)} effects to shape {shape_index} on slide {slide_index}",
+ "applied_effects": applied_effects
+ }
+
+ if warnings:
+ result["warnings"] = warnings
+
+ return result
+
+ except Exception as e:
+ return {
+ "error": f"Failed to apply picture effects: {str(e)}"
+ }
+
+ @app.tool(
+ annotations=ToolAnnotations(
+ title="Manage Fonts",
+ ),
+ )
+ def manage_fonts(
+ operation: str, # "analyze", "optimize", "recommend"
+ font_path: str,
+ output_path: Optional[str] = None,
+ presentation_type: str = "business",
+ text_content: Optional[str] = None
+ ) -> Dict:
+ """Unified font management tool for analysis, optimization, and recommendations."""
+ try:
+ if operation == "analyze":
+ # Analyze font file
+ return ppt_utils.analyze_font_file(font_path)
+
+ elif operation == "optimize":
+ # Optimize font file
+ optimized_path = ppt_utils.optimize_font_for_presentation(
+ font_path,
+ output_path=output_path,
+ text_content=text_content
+ )
+
+ return {
+ "message": f"Optimized font: {font_path}",
+ "original_path": font_path,
+ "optimized_path": optimized_path
+ }
+
+ elif operation == "recommend":
+ # Get font recommendations
+ return ppt_utils.get_font_recommendations(
+ font_path,
+ presentation_type=presentation_type
+ )
+
+ else:
+ return {
+ "error": f"Invalid operation: {operation}. Must be 'analyze', 'optimize', or 'recommend'"
+ }
+
+ except Exception as e:
+ return {
+ "error": f"Failed to {operation} font: {str(e)}"
+ }
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/tools/structural_tools.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/tools/structural_tools.py
new file mode 100644
index 00000000..c1489efe
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/tools/structural_tools.py
@@ -0,0 +1,390 @@
+"""
+Structural element tools for PowerPoint MCP Server.
+Handles tables, shapes, and charts.
+"""
+from typing import Dict, List, Optional, Any
+from mcp.server.fastmcp import FastMCP
+from mcp.types import ToolAnnotations
+import utils as ppt_utils
+
+
+def register_structural_tools(app: FastMCP, presentations: Dict, get_current_presentation_id, validate_parameters, is_positive, is_non_negative, is_in_range, is_valid_rgb, add_shape_direct):
+ """Register structural element tools with the FastMCP app"""
+
+ @app.tool(
+ annotations=ToolAnnotations(
+ title="Add Table",
+ ),
+ )
+ def add_table(
+ slide_index: int,
+ rows: int,
+ cols: int,
+ left: float,
+ top: float,
+ width: float,
+ height: float,
+ data: Optional[List[List[str]]] = None,
+ header_row: bool = True,
+ header_font_size: int = 12,
+ body_font_size: int = 10,
+ header_bg_color: Optional[List[int]] = None,
+ body_bg_color: Optional[List[int]] = None,
+ border_color: Optional[List[int]] = None,
+ presentation_id: Optional[str] = None
+ ) -> Dict:
+ """Add a table to a slide with enhanced formatting options."""
+ pres_id = presentation_id if presentation_id is not None else get_current_presentation_id()
+
+ if pres_id is None or pres_id not in presentations:
+ return {
+ "error": "No presentation is currently loaded or the specified ID is invalid"
+ }
+
+ pres = presentations[pres_id]
+
+ if slide_index < 0 or slide_index >= len(pres.slides):
+ return {
+ "error": f"Invalid slide index: {slide_index}. Available slides: 0-{len(pres.slides) - 1}"
+ }
+
+ slide = pres.slides[slide_index]
+
+ # Validate parameters
+ validations = {
+ "rows": (rows, [(is_positive, "must be a positive integer")]),
+ "cols": (cols, [(is_positive, "must be a positive integer")]),
+ "left": (left, [(is_non_negative, "must be non-negative")]),
+ "top": (top, [(is_non_negative, "must be non-negative")]),
+ "width": (width, [(is_positive, "must be positive")]),
+ "height": (height, [(is_positive, "must be positive")])
+ }
+
+ if header_bg_color is not None:
+ validations["header_bg_color"] = (header_bg_color, [(is_valid_rgb, "must be a valid RGB list [R, G, B] with values 0-255")])
+ if body_bg_color is not None:
+ validations["body_bg_color"] = (body_bg_color, [(is_valid_rgb, "must be a valid RGB list [R, G, B] with values 0-255")])
+ if border_color is not None:
+ validations["border_color"] = (border_color, [(is_valid_rgb, "must be a valid RGB list [R, G, B] with values 0-255")])
+
+ valid, error = validate_parameters(validations)
+ if not valid:
+ return {"error": error}
+
+ # Validate data if provided
+ if data:
+ if len(data) != rows:
+ return {
+ "error": f"Data has {len(data)} rows but table should have {rows} rows"
+ }
+ for i, row in enumerate(data):
+ if len(row) != cols:
+ return {
+ "error": f"Row {i} has {len(row)} columns but table should have {cols} columns"
+ }
+
+ try:
+ # Add the table
+ table_shape = ppt_utils.add_table(slide, rows, cols, left, top, width, height)
+ table = table_shape.table
+
+ # Populate with data if provided
+ if data:
+ for r in range(rows):
+ for c in range(cols):
+ if r < len(data) and c < len(data[r]):
+ table.cell(r, c).text = str(data[r][c])
+
+ # Apply formatting
+ for r in range(rows):
+ for c in range(cols):
+ cell = table.cell(r, c)
+
+ # Header row formatting
+ if r == 0 and header_row:
+ if header_bg_color:
+ ppt_utils.format_table_cell(
+ cell, bg_color=tuple(header_bg_color), font_size=header_font_size, bold=True
+ )
+ else:
+ ppt_utils.format_table_cell(cell, font_size=header_font_size, bold=True)
+ else:
+ # Body cell formatting
+ if body_bg_color:
+ ppt_utils.format_table_cell(
+ cell, bg_color=tuple(body_bg_color), font_size=body_font_size
+ )
+ else:
+ ppt_utils.format_table_cell(cell, font_size=body_font_size)
+
+ return {
+ "message": f"Added {rows}x{cols} table to slide {slide_index}",
+ "shape_index": len(slide.shapes) - 1,
+ "rows": rows,
+ "cols": cols
+ }
+ except Exception as e:
+ return {
+ "error": f"Failed to add table: {str(e)}"
+ }
+
+ @app.tool(
+ annotations=ToolAnnotations(
+ title="Format Table Cell",
+ ),
+ )
+ def format_table_cell(
+ slide_index: int,
+ shape_index: int,
+ row: int,
+ col: int,
+ font_size: Optional[int] = None,
+ font_name: Optional[str] = None,
+ bold: Optional[bool] = None,
+ italic: Optional[bool] = None,
+ color: Optional[List[int]] = None,
+ bg_color: Optional[List[int]] = None,
+ alignment: Optional[str] = None,
+ vertical_alignment: Optional[str] = None,
+ presentation_id: Optional[str] = None
+ ) -> Dict:
+ """Format a specific table cell."""
+ pres_id = presentation_id if presentation_id is not None else get_current_presentation_id()
+
+ if pres_id is None or pres_id not in presentations:
+ return {
+ "error": "No presentation is currently loaded or the specified ID is invalid"
+ }
+
+ pres = presentations[pres_id]
+
+ if slide_index < 0 or slide_index >= len(pres.slides):
+ return {
+ "error": f"Invalid slide index: {slide_index}. Available slides: 0-{len(pres.slides) - 1}"
+ }
+
+ slide = pres.slides[slide_index]
+
+ if shape_index < 0 or shape_index >= len(slide.shapes):
+ return {
+ "error": f"Invalid shape index: {shape_index}. Available shapes: 0-{len(slide.shapes) - 1}"
+ }
+
+ shape = slide.shapes[shape_index]
+
+ try:
+ if not hasattr(shape, 'table'):
+ return {
+ "error": f"Shape at index {shape_index} is not a table"
+ }
+
+ table = shape.table
+
+ if row < 0 or row >= len(table.rows):
+ return {
+ "error": f"Invalid row index: {row}. Available rows: 0-{len(table.rows) - 1}"
+ }
+
+ if col < 0 or col >= len(table.columns):
+ return {
+ "error": f"Invalid column index: {col}. Available columns: 0-{len(table.columns) - 1}"
+ }
+
+ cell = table.cell(row, col)
+
+ ppt_utils.format_table_cell(
+ cell,
+ font_size=font_size,
+ font_name=font_name,
+ bold=bold,
+ italic=italic,
+ color=tuple(color) if color else None,
+ bg_color=tuple(bg_color) if bg_color else None,
+ alignment=alignment,
+ vertical_alignment=vertical_alignment
+ )
+
+ return {
+ "message": f"Formatted cell at row {row}, column {col} in table at shape index {shape_index} on slide {slide_index}"
+ }
+ except Exception as e:
+ return {
+ "error": f"Failed to format table cell: {str(e)}"
+ }
+
+ @app.tool(
+ annotations=ToolAnnotations(
+ title="Add Shape",
+ ),
+ )
+ def add_shape(
+ slide_index: int,
+ shape_type: str,
+ left: float,
+ top: float,
+ width: float,
+ height: float,
+ fill_color: Optional[List[int]] = None,
+ line_color: Optional[List[int]] = None,
+ line_width: Optional[float] = None,
+ text: Optional[str] = None, # Add text to shape
+ font_size: Optional[int] = None,
+ font_color: Optional[List[int]] = None,
+ presentation_id: Optional[str] = None
+ ) -> Dict:
+ """Add an auto shape to a slide with enhanced options."""
+ pres_id = presentation_id if presentation_id is not None else get_current_presentation_id()
+
+ if pres_id is None or pres_id not in presentations:
+ return {
+ "error": "No presentation is currently loaded or the specified ID is invalid"
+ }
+
+ pres = presentations[pres_id]
+
+ if slide_index < 0 or slide_index >= len(pres.slides):
+ return {
+ "error": f"Invalid slide index: {slide_index}. Available slides: 0-{len(pres.slides) - 1}"
+ }
+
+ slide = pres.slides[slide_index]
+
+ try:
+ # Use the direct implementation that bypasses the enum issues
+ shape = add_shape_direct(slide, shape_type, left, top, width, height)
+
+ # Format the shape if formatting options are provided
+ if any([fill_color, line_color, line_width]):
+ ppt_utils.format_shape(
+ shape,
+ fill_color=tuple(fill_color) if fill_color else None,
+ line_color=tuple(line_color) if line_color else None,
+ line_width=line_width
+ )
+
+ # Add text to shape if provided
+ if text and hasattr(shape, 'text_frame'):
+ shape.text_frame.text = text
+ if font_size or font_color:
+ ppt_utils.format_text(
+ shape.text_frame,
+ font_size=font_size,
+ color=tuple(font_color) if font_color else None
+ )
+
+ return {
+ "message": f"Added {shape_type} shape to slide {slide_index}",
+ "shape_index": len(slide.shapes) - 1
+ }
+ except ValueError as e:
+ return {
+ "error": str(e)
+ }
+ except Exception as e:
+ return {
+ "error": f"Failed to add shape '{shape_type}': {str(e)}"
+ }
+
+ @app.tool(
+ annotations=ToolAnnotations(
+ title="Add Chart",
+ ),
+ )
+ def add_chart(
+ slide_index: int,
+ chart_type: str,
+ left: float,
+ top: float,
+ width: float,
+ height: float,
+ categories: List[str],
+ series_names: List[str],
+ series_values: List[List[float]],
+ has_legend: bool = True,
+ legend_position: str = "right",
+ has_data_labels: bool = False,
+ title: Optional[str] = None,
+ x_axis_title: Optional[str] = None,
+ y_axis_title: Optional[str] = None,
+ color_scheme: Optional[str] = None,
+ presentation_id: Optional[str] = None
+ ) -> Dict:
+ """Add a chart to a slide with comprehensive formatting options."""
+ pres_id = presentation_id if presentation_id is not None else get_current_presentation_id()
+
+ if pres_id is None or pres_id not in presentations:
+ return {
+ "error": "No presentation is currently loaded or the specified ID is invalid"
+ }
+
+ pres = presentations[pres_id]
+
+ if slide_index < 0 or slide_index >= len(pres.slides):
+ return {
+ "error": f"Invalid slide index: {slide_index}. Available slides: 0-{len(pres.slides) - 1}"
+ }
+
+ slide = pres.slides[slide_index]
+
+ # Validate chart type
+ valid_chart_types = [
+ 'column', 'stacked_column', 'bar', 'stacked_bar', 'line',
+ 'line_markers', 'pie', 'doughnut', 'area', 'stacked_area',
+ 'scatter', 'radar', 'radar_markers'
+ ]
+ if chart_type.lower() not in valid_chart_types:
+ return {
+ "error": f"Invalid chart type: '{chart_type}'. Valid types are: {', '.join(valid_chart_types)}"
+ }
+
+ # Validate series data
+ if len(series_names) != len(series_values):
+ return {
+ "error": f"Number of series names ({len(series_names)}) must match number of series values ({len(series_values)})"
+ }
+
+ if not categories:
+ return {
+ "error": "Categories list cannot be empty"
+ }
+
+ # Validate that all series have the same number of values as categories
+ for i, values in enumerate(series_values):
+ if len(values) != len(categories):
+ return {
+ "error": f"Series '{series_names[i]}' has {len(values)} values but there are {len(categories)} categories"
+ }
+
+ try:
+ # Add the chart
+ chart = ppt_utils.add_chart(
+ slide, chart_type, left, top, width, height,
+ categories, series_names, series_values
+ )
+
+ if chart is None:
+ return {"error": "Failed to create chart"}
+
+ # Format the chart
+ ppt_utils.format_chart(
+ chart,
+ has_legend=has_legend,
+ legend_position=legend_position,
+ has_data_labels=has_data_labels,
+ title=title,
+ x_axis_title=x_axis_title,
+ y_axis_title=y_axis_title,
+ color_scheme=color_scheme
+ )
+
+ return {
+ "message": f"Added {chart_type} chart to slide {slide_index}",
+ "shape_index": len(slide.shapes) - 1,
+ "chart_type": chart_type,
+ "series_count": len(series_names),
+ "categories_count": len(categories)
+ }
+ except Exception as e:
+ return {
+ "error": f"Failed to add chart: {str(e)}"
+ }
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/tools/template_tools.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/tools/template_tools.py
new file mode 100644
index 00000000..e6f81190
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/tools/template_tools.py
@@ -0,0 +1,552 @@
+"""
+Enhanced template-based slide creation tools for PowerPoint MCP Server.
+Handles template application, template management, automated slide generation,
+and advanced features like dynamic sizing, auto-wrapping, and visual effects.
+"""
+from typing import Dict, List, Optional, Any
+from mcp.server.fastmcp import FastMCP
+from mcp.types import ToolAnnotations
+import utils.template_utils as template_utils
+
+
+def register_template_tools(app: FastMCP, presentations: Dict, get_current_presentation_id):
+ """Register template-based tools with the FastMCP app"""
+
+ @app.tool(
+ annotations=ToolAnnotations(
+ title="List Slide Templates",
+ readOnlyHint=True,
+ ),
+ )
+ def list_slide_templates() -> Dict:
+ """List all available slide layout templates."""
+ try:
+ available_templates = template_utils.get_available_templates()
+ usage_examples = template_utils.get_template_usage_examples()
+
+ return {
+ "available_templates": available_templates,
+ "total_templates": len(available_templates),
+ "usage_examples": usage_examples,
+ "message": "Use apply_slide_template to apply templates to slides"
+ }
+ except Exception as e:
+ return {
+ "error": f"Failed to list templates: {str(e)}"
+ }
+
+ @app.tool(
+ annotations=ToolAnnotations(
+ title="Apply Slide Template",
+ ),
+ )
+ def apply_slide_template(
+ slide_index: int,
+ template_id: str,
+ color_scheme: str = "modern_blue",
+ content_mapping: Optional[Dict[str, str]] = None,
+ image_paths: Optional[Dict[str, str]] = None,
+ presentation_id: Optional[str] = None
+ ) -> Dict:
+ """
+ Apply a structured layout template to an existing slide.
+ This modifies slide layout and content structure using predefined templates.
+
+ Args:
+ slide_index: Index of the slide to apply template to
+ template_id: ID of the template to apply (e.g., 'title_slide', 'text_with_image')
+ color_scheme: Color scheme to use ('modern_blue', 'corporate_gray', 'elegant_green', 'warm_red')
+ content_mapping: Dictionary mapping element roles to custom content
+ image_paths: Dictionary mapping image element roles to file paths
+ presentation_id: Presentation ID (uses current if None)
+ """
+ pres_id = presentation_id if presentation_id is not None else get_current_presentation_id()
+
+ if pres_id is None or pres_id not in presentations:
+ return {
+ "error": "No presentation is currently loaded or the specified ID is invalid"
+ }
+
+ pres = presentations[pres_id]
+
+ if slide_index < 0 or slide_index >= len(pres.slides):
+ return {
+ "error": f"Invalid slide index: {slide_index}. Available slides: 0-{len(pres.slides) - 1}"
+ }
+
+ slide = pres.slides[slide_index]
+
+ try:
+ result = template_utils.apply_slide_template(
+ slide, template_id, color_scheme,
+ content_mapping or {}, image_paths or {}
+ )
+
+ if result['success']:
+ return {
+ "message": f"Applied template '{template_id}' to slide {slide_index}",
+ "slide_index": slide_index,
+ "template_applied": result
+ }
+ else:
+ return {
+ "error": f"Failed to apply template: {result.get('error', 'Unknown error')}"
+ }
+
+ except Exception as e:
+ return {
+ "error": f"Failed to apply template: {str(e)}"
+ }
+
+ @app.tool(
+ annotations=ToolAnnotations(
+ title="Create Slide from Template",
+ ),
+ )
+ def create_slide_from_template(
+ template_id: str,
+ color_scheme: str = "modern_blue",
+ content_mapping: Optional[Dict[str, str]] = None,
+ image_paths: Optional[Dict[str, str]] = None,
+ layout_index: int = 1,
+ presentation_id: Optional[str] = None
+ ) -> Dict:
+ """
+ Create a new slide using a layout template.
+
+ Args:
+ template_id: ID of the template to use (e.g., 'title_slide', 'text_with_image')
+ color_scheme: Color scheme to use ('modern_blue', 'corporate_gray', 'elegant_green', 'warm_red')
+ content_mapping: Dictionary mapping element roles to custom content
+ image_paths: Dictionary mapping image element roles to file paths
+ layout_index: PowerPoint layout index to use as base (default: 1)
+ presentation_id: Presentation ID (uses current if None)
+ """
+ pres_id = presentation_id if presentation_id is not None else get_current_presentation_id()
+
+ if pres_id is None or pres_id not in presentations:
+ return {
+ "error": "No presentation is currently loaded or the specified ID is invalid"
+ }
+
+ pres = presentations[pres_id]
+
+ # Validate layout index
+ if layout_index < 0 or layout_index >= len(pres.slide_layouts):
+ return {
+ "error": f"Invalid layout index: {layout_index}. Available layouts: 0-{len(pres.slide_layouts) - 1}"
+ }
+
+ try:
+ # Add new slide
+ layout = pres.slide_layouts[layout_index]
+ slide = pres.slides.add_slide(layout)
+ slide_index = len(pres.slides) - 1
+
+ # Apply template
+ result = template_utils.apply_slide_template(
+ slide, template_id, color_scheme,
+ content_mapping or {}, image_paths or {}
+ )
+
+ if result['success']:
+ return {
+ "message": f"Created slide {slide_index} using template '{template_id}'",
+ "slide_index": slide_index,
+ "template_applied": result
+ }
+ else:
+ return {
+ "error": f"Failed to apply template to new slide: {result.get('error', 'Unknown error')}"
+ }
+
+ except Exception as e:
+ return {
+ "error": f"Failed to create slide from template: {str(e)}"
+ }
+
+ @app.tool(
+ annotations=ToolAnnotations(
+ title="Create Presentation from Templates",
+ ),
+ )
+ def create_presentation_from_templates(
+ template_sequence: List[Dict[str, Any]],
+ color_scheme: str = "modern_blue",
+ presentation_title: Optional[str] = None,
+ presentation_id: Optional[str] = None
+ ) -> Dict:
+ """
+ Create a complete presentation from a sequence of templates.
+
+ Args:
+ template_sequence: List of template configurations, each containing:
+ - template_id: Template to use
+ - content: Content mapping for the template
+ - images: Image path mapping for the template
+ color_scheme: Color scheme to apply to all slides
+ presentation_title: Optional title for the presentation
+ presentation_id: Presentation ID (uses current if None)
+
+ Example template_sequence:
+ [
+ {
+ "template_id": "title_slide",
+ "content": {
+ "title": "My Presentation",
+ "subtitle": "Annual Report 2024",
+ "author": "John Doe"
+ }
+ },
+ {
+ "template_id": "text_with_image",
+ "content": {
+ "title": "Key Results",
+ "content": "• Achievement 1\\n• Achievement 2"
+ },
+ "images": {
+ "supporting": "/path/to/image.jpg"
+ }
+ }
+ ]
+ """
+ pres_id = presentation_id if presentation_id is not None else get_current_presentation_id()
+
+ if pres_id is None or pres_id not in presentations:
+ return {
+ "error": "No presentation is currently loaded or the specified ID is invalid"
+ }
+
+ pres = presentations[pres_id]
+
+ if not template_sequence:
+ return {
+ "error": "Template sequence cannot be empty"
+ }
+
+ try:
+ # Set presentation title if provided
+ if presentation_title:
+ pres.core_properties.title = presentation_title
+
+ # Create slides from template sequence
+ result = template_utils.create_presentation_from_template_sequence(
+ pres, template_sequence, color_scheme
+ )
+
+ if result['success']:
+ return {
+ "message": f"Created presentation with {result['total_slides']} slides",
+ "presentation_id": pres_id,
+ "creation_result": result,
+ "total_slides": len(pres.slides)
+ }
+ else:
+ return {
+ "warning": "Presentation created with some errors",
+ "presentation_id": pres_id,
+ "creation_result": result,
+ "total_slides": len(pres.slides)
+ }
+
+ except Exception as e:
+ return {
+ "error": f"Failed to create presentation from templates: {str(e)}"
+ }
+
+ @app.tool(
+ annotations=ToolAnnotations(
+ title="Get Template Info",
+ readOnlyHint=True,
+ ),
+ )
+ def get_template_info(template_id: str) -> Dict:
+ """
+ Get detailed information about a specific template.
+
+ Args:
+ template_id: ID of the template to get information about
+ """
+ try:
+ templates_data = template_utils.load_slide_templates()
+
+ if template_id not in templates_data.get('templates', {}):
+ available_templates = list(templates_data.get('templates', {}).keys())
+ return {
+ "error": f"Template '{template_id}' not found",
+ "available_templates": available_templates
+ }
+
+ template = templates_data['templates'][template_id]
+
+ # Extract element information
+ elements_info = []
+ for element in template.get('elements', []):
+ element_info = {
+ "type": element.get('type'),
+ "role": element.get('role'),
+ "position": element.get('position'),
+ "placeholder_text": element.get('placeholder_text', ''),
+ "styling_options": list(element.get('styling', {}).keys())
+ }
+ elements_info.append(element_info)
+
+ return {
+ "template_id": template_id,
+ "name": template.get('name'),
+ "description": template.get('description'),
+ "layout_type": template.get('layout_type'),
+ "elements": elements_info,
+ "element_count": len(elements_info),
+ "has_background": 'background' in template,
+ "background_type": template.get('background', {}).get('type'),
+ "color_schemes": list(templates_data.get('color_schemes', {}).keys()),
+ "usage_tip": f"Use create_slide_from_template with template_id='{template_id}' to create a slide with this layout"
+ }
+
+ except Exception as e:
+ return {
+ "error": f"Failed to get template info: {str(e)}"
+ }
+
+ @app.tool(
+ annotations=ToolAnnotations(
+ title="Auto Generate Presentation",
+ ),
+ )
+ def auto_generate_presentation(
+ topic: str,
+ slide_count: int = 5,
+ presentation_type: str = "business",
+ color_scheme: str = "modern_blue",
+ include_charts: bool = True,
+ include_images: bool = False,
+ presentation_id: Optional[str] = None
+ ) -> Dict:
+ """
+ Automatically generate a presentation based on topic and preferences.
+
+ Args:
+ topic: Main topic/theme for the presentation
+ slide_count: Number of slides to generate (3-20)
+ presentation_type: Type of presentation ('business', 'academic', 'creative')
+ color_scheme: Color scheme to use
+ include_charts: Whether to include chart slides
+ include_images: Whether to include image placeholders
+ presentation_id: Presentation ID (uses current if None)
+ """
+ pres_id = presentation_id if presentation_id is not None else get_current_presentation_id()
+
+ if pres_id is None or pres_id not in presentations:
+ return {
+ "error": "No presentation is currently loaded or the specified ID is invalid"
+ }
+
+ if slide_count < 3 or slide_count > 20:
+ return {
+ "error": "Slide count must be between 3 and 20"
+ }
+
+ try:
+ # Define presentation structures based on type
+ if presentation_type == "business":
+ base_templates = [
+ ("title_slide", {"title": f"{topic}", "subtitle": "Executive Presentation", "author": "Business Team"}),
+ ("agenda_slide", {"agenda_items": "1. Executive Summary\n\n2. Current Situation\n\n3. Analysis & Insights\n\n4. Recommendations\n\n5. Next Steps"}),
+ ("key_metrics_dashboard", {"title": "Key Performance Indicators"}),
+ ("text_with_image", {"title": "Current Situation", "content": f"Overview of {topic}:\n• Current status\n• Key challenges\n• Market position"}),
+ ("two_column_text", {"title": "Analysis", "content_left": "Strengths:\n• Advantage 1\n• Advantage 2\n• Advantage 3", "content_right": "Opportunities:\n• Opportunity 1\n• Opportunity 2\n• Opportunity 3"}),
+ ]
+ if include_charts:
+ base_templates.append(("chart_comparison", {"title": "Performance Comparison"}))
+ base_templates.append(("thank_you_slide", {"contact": "Thank you for your attention\nQuestions & Discussion"}))
+
+ elif presentation_type == "academic":
+ base_templates = [
+ ("title_slide", {"title": f"Research on {topic}", "subtitle": "Academic Study", "author": "Research Team"}),
+ ("agenda_slide", {"agenda_items": "1. Introduction\n\n2. Literature Review\n\n3. Methodology\n\n4. Results\n\n5. Conclusions"}),
+ ("text_with_image", {"title": "Introduction", "content": f"Research focus on {topic}:\n• Background\n• Problem statement\n• Research questions"}),
+ ("two_column_text", {"title": "Methodology", "content_left": "Approach:\n• Method 1\n• Method 2\n• Method 3", "content_right": "Data Sources:\n• Source 1\n• Source 2\n• Source 3"}),
+ ("data_table_slide", {"title": "Results Summary"}),
+ ]
+ if include_charts:
+ base_templates.append(("chart_comparison", {"title": "Data Analysis"}))
+ base_templates.append(("thank_you_slide", {"contact": "Questions & Discussion\nContact: research@university.edu"}))
+
+ else: # creative
+ base_templates = [
+ ("title_slide", {"title": f"Creative Vision: {topic}", "subtitle": "Innovative Concepts", "author": "Creative Team"}),
+ ("full_image_slide", {"overlay_title": f"Exploring {topic}", "overlay_subtitle": "Creative possibilities"}),
+ ("three_column_layout", {"title": "Creative Concepts"}),
+ ("quote_testimonial", {"quote_text": f"Innovation in {topic} requires thinking beyond conventional boundaries", "attribution": "— Creative Director"}),
+ ("process_flow", {"title": "Creative Process"}),
+ ]
+ if include_charts:
+ base_templates.append(("key_metrics_dashboard", {"title": "Impact Metrics"}))
+ base_templates.append(("thank_you_slide", {"contact": "Let's create something amazing together\ncreative@studio.com"}))
+
+ # Adjust templates to match requested slide count
+ template_sequence = []
+ templates_to_use = base_templates[:slide_count]
+
+ # If we need more slides, add content slides
+ while len(templates_to_use) < slide_count:
+ if include_images:
+ templates_to_use.insert(-1, ("text_with_image", {"title": f"{topic} - Additional Topic", "content": "• Key point\n• Supporting detail\n• Additional insight"}))
+ else:
+ templates_to_use.insert(-1, ("two_column_text", {"title": f"{topic} - Analysis", "content_left": "Key Points:\n• Point 1\n• Point 2", "content_right": "Details:\n• Detail 1\n• Detail 2"}))
+
+ # Convert to proper template sequence format
+ for i, (template_id, content) in enumerate(templates_to_use):
+ template_config = {
+ "template_id": template_id,
+ "content": content
+ }
+ template_sequence.append(template_config)
+
+ # Create the presentation
+ result = template_utils.create_presentation_from_template_sequence(
+ presentations[pres_id], template_sequence, color_scheme
+ )
+
+ return {
+ "message": f"Auto-generated {slide_count}-slide presentation on '{topic}'",
+ "topic": topic,
+ "presentation_type": presentation_type,
+ "color_scheme": color_scheme,
+ "slide_count": slide_count,
+ "generation_result": result,
+ "templates_used": [t[0] for t in templates_to_use]
+ }
+
+ except Exception as e:
+ return {
+ "error": f"Failed to auto-generate presentation: {str(e)}"
+ }
+
+ # Text optimization tools
+
+
+ @app.tool(
+ annotations=ToolAnnotations(
+ title="Optimize Slide Text",
+ ),
+ )
+ def optimize_slide_text(
+ slide_index: int,
+ auto_resize: bool = True,
+ auto_wrap: bool = True,
+ optimize_spacing: bool = True,
+ min_font_size: int = 8,
+ max_font_size: int = 36,
+ presentation_id: Optional[str] = None
+ ) -> Dict:
+ """
+ Optimize text elements on a slide for better readability and fit.
+
+ Args:
+ slide_index: Index of the slide to optimize
+ auto_resize: Whether to automatically resize fonts to fit containers
+ auto_wrap: Whether to apply intelligent text wrapping
+ optimize_spacing: Whether to optimize line spacing
+ min_font_size: Minimum allowed font size
+ max_font_size: Maximum allowed font size
+ presentation_id: Presentation ID (uses current if None)
+ """
+ pres_id = presentation_id if presentation_id is not None else get_current_presentation_id()
+
+ if pres_id is None or pres_id not in presentations:
+ return {
+ "error": "No presentation is currently loaded or the specified ID is invalid"
+ }
+
+ pres = presentations[pres_id]
+
+ if slide_index < 0 or slide_index >= len(pres.slides):
+ return {
+ "error": f"Invalid slide index: {slide_index}. Available slides: 0-{len(pres.slides) - 1}"
+ }
+
+ slide = pres.slides[slide_index]
+
+ try:
+ optimizations_applied = []
+ manager = template_utils.get_enhanced_template_manager()
+
+ # Analyze each text shape on the slide
+ for i, shape in enumerate(slide.shapes):
+ if hasattr(shape, 'text_frame') and shape.text_frame.text:
+ text = shape.text_frame.text
+
+ # Calculate container dimensions
+ container_width = shape.width.inches
+ container_height = shape.height.inches
+
+ shape_optimizations = []
+
+ # Apply auto-resize if enabled
+ if auto_resize:
+ optimal_size = template_utils.calculate_dynamic_font_size(
+ text, container_width, container_height
+ )
+ optimal_size = max(min_font_size, min(max_font_size, optimal_size))
+
+ # Apply the calculated font size
+ for paragraph in shape.text_frame.paragraphs:
+ for run in paragraph.runs:
+ run.font.size = template_utils.Pt(optimal_size)
+
+ shape_optimizations.append(f"Font resized to {optimal_size}pt")
+
+ # Apply auto-wrap if enabled
+ if auto_wrap:
+ current_font_size = 14 # Default assumption
+ if shape.text_frame.paragraphs and shape.text_frame.paragraphs[0].runs:
+ if shape.text_frame.paragraphs[0].runs[0].font.size:
+ current_font_size = shape.text_frame.paragraphs[0].runs[0].font.size.pt
+
+ wrapped_text = template_utils.wrap_text_automatically(
+ text, container_width, current_font_size
+ )
+
+ if wrapped_text != text:
+ shape.text_frame.text = wrapped_text
+ shape_optimizations.append("Text wrapped automatically")
+
+ # Optimize spacing if enabled
+ if optimize_spacing:
+ text_length = len(text)
+ if text_length > 300:
+ line_spacing = 1.4
+ elif text_length > 150:
+ line_spacing = 1.3
+ else:
+ line_spacing = 1.2
+
+ for paragraph in shape.text_frame.paragraphs:
+ paragraph.line_spacing = line_spacing
+
+ shape_optimizations.append(f"Line spacing set to {line_spacing}")
+
+ if shape_optimizations:
+ optimizations_applied.append({
+ "shape_index": i,
+ "optimizations": shape_optimizations
+ })
+
+ return {
+ "message": f"Optimized {len(optimizations_applied)} text elements on slide {slide_index}",
+ "slide_index": slide_index,
+ "optimizations_applied": optimizations_applied,
+ "settings": {
+ "auto_resize": auto_resize,
+ "auto_wrap": auto_wrap,
+ "optimize_spacing": optimize_spacing,
+ "font_size_range": f"{min_font_size}-{max_font_size}pt"
+ }
+ }
+
+ except Exception as e:
+ return {
+ "error": f"Failed to optimize slide text: {str(e)}"
+ }
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/tools/transition_tools.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/tools/transition_tools.py
new file mode 100644
index 00000000..16dcc8f0
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/tools/transition_tools.py
@@ -0,0 +1,80 @@
+"""
+Slide transition management tools for PowerPoint MCP Server.
+Implements slide transition and timing capabilities.
+"""
+
+from typing import Dict, List, Optional, Any
+from mcp.types import ToolAnnotations
+
+def register_transition_tools(app, presentations, get_current_presentation_id, validate_parameters,
+ is_positive, is_non_negative, is_in_range, is_valid_rgb):
+ """Register slide transition management tools with the FastMCP app."""
+
+ @app.tool(
+ annotations=ToolAnnotations(
+ title="Manage Slide Transitions",
+ ),
+ )
+ def manage_slide_transitions(
+ slide_index: int,
+ operation: str,
+ transition_type: str = None,
+ duration: float = 1.0,
+ presentation_id: str = None
+ ) -> Dict:
+ """
+ Manage slide transitions and timing.
+
+ Args:
+ slide_index: Index of the slide (0-based)
+ operation: Operation type ("set", "remove", "get")
+ transition_type: Type of transition (basic support)
+ duration: Duration of transition in seconds
+ presentation_id: Optional presentation ID (uses current if not provided)
+
+ Returns:
+ Dictionary with transition information
+ """
+ try:
+ # Get presentation
+ pres_id = presentation_id or get_current_presentation_id()
+ if pres_id not in presentations:
+ return {"error": "Presentation not found"}
+
+ pres = presentations[pres_id]
+
+ # Validate slide index
+ if not (0 <= slide_index < len(pres.slides)):
+ return {"error": f"Slide index {slide_index} out of range"}
+
+ slide = pres.slides[slide_index]
+
+ if operation == "get":
+ # Get current transition info (limited python-pptx support)
+ return {
+ "message": f"Transition info for slide {slide_index}",
+ "slide_index": slide_index,
+ "note": "Transition reading has limited support in python-pptx"
+ }
+
+ elif operation == "set":
+ return {
+ "message": f"Transition setting requested for slide {slide_index}",
+ "slide_index": slide_index,
+ "transition_type": transition_type,
+ "duration": duration,
+ "note": "Transition setting has limited support in python-pptx - this is a placeholder for future enhancement"
+ }
+
+ elif operation == "remove":
+ return {
+ "message": f"Transition removal requested for slide {slide_index}",
+ "slide_index": slide_index,
+ "note": "Transition removal has limited support in python-pptx - this is a placeholder for future enhancement"
+ }
+
+ else:
+ return {"error": f"Unsupported operation: {operation}. Use 'set', 'remove', or 'get'"}
+
+ except Exception as e:
+ return {"error": f"Failed to manage slide transitions: {str(e)}"}
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/utils/__init__.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/utils/__init__.py
new file mode 100644
index 00000000..43d45035
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/utils/__init__.py
@@ -0,0 +1,69 @@
+"""
+PowerPoint utilities package.
+Organized utility functions for PowerPoint manipulation.
+"""
+
+from .core_utils import *
+from .presentation_utils import *
+from .content_utils import *
+from .design_utils import *
+from .validation_utils import *
+
+__all__ = [
+ # Core utilities
+ "safe_operation",
+ "try_multiple_approaches",
+
+ # Presentation utilities
+ "create_presentation",
+ "open_presentation",
+ "save_presentation",
+ "create_presentation_from_template",
+ "get_presentation_info",
+ "get_template_info",
+ "set_core_properties",
+ "get_core_properties",
+
+ # Content utilities
+ "add_slide",
+ "get_slide_info",
+ "set_title",
+ "populate_placeholder",
+ "add_bullet_points",
+ "add_textbox",
+ "format_text",
+ "format_text_advanced",
+ "add_image",
+ "add_table",
+ "format_table_cell",
+ "add_chart",
+ "format_chart",
+
+ # Design utilities
+ "get_professional_color",
+ "get_professional_font",
+ "get_color_schemes",
+ "add_professional_slide",
+ "apply_professional_theme",
+ "enhance_existing_slide",
+ "apply_professional_image_enhancement",
+ "enhance_image_with_pillow",
+ "set_slide_gradient_background",
+ "create_professional_gradient_background",
+ "format_shape",
+ "apply_picture_shadow",
+ "apply_picture_reflection",
+ "apply_picture_glow",
+ "apply_picture_soft_edges",
+ "apply_picture_rotation",
+ "apply_picture_transparency",
+ "apply_picture_bevel",
+ "apply_picture_filter",
+ "analyze_font_file",
+ "optimize_font_for_presentation",
+ "get_font_recommendations",
+
+ # Validation utilities
+ "validate_text_fit",
+ "validate_and_fix_slide"
+]
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/utils/content_utils.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/utils/content_utils.py
new file mode 100644
index 00000000..27266d10
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/utils/content_utils.py
@@ -0,0 +1,579 @@
+"""
+Content management utilities for PowerPoint MCP Server.
+Functions for slides, text, images, tables, charts, and shapes.
+"""
+from pptx import Presentation
+from pptx.chart.data import CategoryChartData
+from pptx.enum.chart import XL_CHART_TYPE
+from pptx.enum.text import PP_ALIGN
+from pptx.util import Inches, Pt
+from pptx.dml.color import RGBColor
+from typing import Dict, List, Tuple, Optional, Any
+import tempfile
+import os
+import base64
+
+
+def add_slide(presentation: Presentation, layout_index: int = 1) -> Tuple:
+ """
+ Add a slide to the presentation.
+
+ Args:
+ presentation: The Presentation object
+ layout_index: Index of the slide layout to use
+
+ Returns:
+ A tuple containing the slide and its layout
+ """
+ layout = presentation.slide_layouts[layout_index]
+ slide = presentation.slides.add_slide(layout)
+ return slide, layout
+
+
+def get_slide_info(slide, slide_index: int) -> Dict:
+ """
+ Get information about a specific slide.
+
+ Args:
+ slide: The slide object
+ slide_index: Index of the slide
+
+ Returns:
+ Dictionary containing slide information
+ """
+ try:
+ placeholders = []
+ for placeholder in slide.placeholders:
+ placeholder_info = {
+ "idx": placeholder.placeholder_format.idx,
+ "type": str(placeholder.placeholder_format.type),
+ "name": placeholder.name
+ }
+ placeholders.append(placeholder_info)
+
+ shapes = []
+ for i, shape in enumerate(slide.shapes):
+ shape_info = {
+ "index": i,
+ "name": shape.name,
+ "shape_type": str(shape.shape_type),
+ "left": shape.left,
+ "top": shape.top,
+ "width": shape.width,
+ "height": shape.height
+ }
+ shapes.append(shape_info)
+
+ return {
+ "slide_index": slide_index,
+ "layout_name": slide.slide_layout.name,
+ "placeholder_count": len(placeholders),
+ "placeholders": placeholders,
+ "shape_count": len(shapes),
+ "shapes": shapes
+ }
+ except Exception as e:
+ raise Exception(f"Failed to get slide info: {str(e)}")
+
+
+def set_title(slide, title: str) -> None:
+ """
+ Set the title of a slide.
+
+ Args:
+ slide: The slide object
+ title: The title text
+ """
+ if slide.shapes.title:
+ slide.shapes.title.text = title
+
+
+def populate_placeholder(slide, placeholder_idx: int, text: str) -> None:
+ """
+ Populate a placeholder with text.
+
+ Args:
+ slide: The slide object
+ placeholder_idx: The index of the placeholder
+ text: The text to add
+ """
+ placeholder = slide.placeholders[placeholder_idx]
+ placeholder.text = text
+
+
+def add_bullet_points(placeholder, bullet_points: List[str]) -> None:
+ """
+ Add bullet points to a placeholder.
+
+ Args:
+ placeholder: The placeholder object
+ bullet_points: List of bullet point texts
+ """
+ text_frame = placeholder.text_frame
+ text_frame.clear()
+
+ for i, point in enumerate(bullet_points):
+ p = text_frame.add_paragraph()
+ p.text = point
+ p.level = 0
+
+
+def add_textbox(slide, left: float, top: float, width: float, height: float, text: str,
+ font_size: int = None, font_name: str = None, bold: bool = None,
+ italic: bool = None, underline: bool = None,
+ color: Tuple[int, int, int] = None, bg_color: Tuple[int, int, int] = None,
+ alignment: str = None, vertical_alignment: str = None,
+ auto_fit: bool = True) -> Any:
+ """
+ Add a textbox to a slide with formatting options.
+
+ Args:
+ slide: The slide object
+ left: Left position in inches
+ top: Top position in inches
+ width: Width in inches
+ height: Height in inches
+ text: Text content
+ font_size: Font size in points
+ font_name: Font name
+ bold: Whether text should be bold
+ italic: Whether text should be italic
+ underline: Whether text should be underlined
+ color: RGB color tuple (r, g, b)
+ bg_color: Background RGB color tuple (r, g, b)
+ alignment: Text alignment ('left', 'center', 'right', 'justify')
+ vertical_alignment: Vertical alignment ('top', 'middle', 'bottom')
+ auto_fit: Whether to auto-fit text
+
+ Returns:
+ The created textbox shape
+ """
+ textbox = slide.shapes.add_textbox(
+ Inches(left), Inches(top), Inches(width), Inches(height)
+ )
+
+ textbox.text_frame.text = text
+
+ # Apply formatting if provided
+ if any([font_size, font_name, bold, italic, underline, color, bg_color, alignment, vertical_alignment]):
+ format_text_advanced(
+ textbox.text_frame,
+ font_size=font_size,
+ font_name=font_name,
+ bold=bold,
+ italic=italic,
+ underline=underline,
+ color=color,
+ bg_color=bg_color,
+ alignment=alignment,
+ vertical_alignment=vertical_alignment
+ )
+
+ return textbox
+
+
+def format_text(text_frame, font_size: int = None, font_name: str = None,
+ bold: bool = None, italic: bool = None, color: Tuple[int, int, int] = None,
+ alignment: str = None) -> None:
+ """
+ Format text in a text frame.
+
+ Args:
+ text_frame: The text frame to format
+ font_size: Font size in points
+ font_name: Font name
+ bold: Whether text should be bold
+ italic: Whether text should be italic
+ color: RGB color tuple (r, g, b)
+ alignment: Text alignment ('left', 'center', 'right', 'justify')
+ """
+ alignment_map = {
+ 'left': PP_ALIGN.LEFT,
+ 'center': PP_ALIGN.CENTER,
+ 'right': PP_ALIGN.RIGHT,
+ 'justify': PP_ALIGN.JUSTIFY
+ }
+
+ for paragraph in text_frame.paragraphs:
+ if alignment and alignment in alignment_map:
+ paragraph.alignment = alignment_map[alignment]
+
+ for run in paragraph.runs:
+ font = run.font
+
+ if font_size is not None:
+ font.size = Pt(font_size)
+ if font_name is not None:
+ font.name = font_name
+ if bold is not None:
+ font.bold = bold
+ if italic is not None:
+ font.italic = italic
+ if color is not None:
+ r, g, b = color
+ font.color.rgb = RGBColor(r, g, b)
+
+
+def format_text_advanced(text_frame, font_size: int = None, font_name: str = None,
+ bold: bool = None, italic: bool = None, underline: bool = None,
+ color: Tuple[int, int, int] = None, bg_color: Tuple[int, int, int] = None,
+ alignment: str = None, vertical_alignment: str = None) -> Dict:
+ """
+ Advanced text formatting with comprehensive options.
+
+ Args:
+ text_frame: The text frame to format
+ font_size: Font size in points
+ font_name: Font name
+ bold: Whether text should be bold
+ italic: Whether text should be italic
+ underline: Whether text should be underlined
+ color: RGB color tuple (r, g, b)
+ bg_color: Background RGB color tuple (r, g, b)
+ alignment: Text alignment ('left', 'center', 'right', 'justify')
+ vertical_alignment: Vertical alignment ('top', 'middle', 'bottom')
+
+ Returns:
+ Dictionary with formatting results
+ """
+ result = {
+ 'success': True,
+ 'warnings': []
+ }
+
+ try:
+ alignment_map = {
+ 'left': PP_ALIGN.LEFT,
+ 'center': PP_ALIGN.CENTER,
+ 'right': PP_ALIGN.RIGHT,
+ 'justify': PP_ALIGN.JUSTIFY
+ }
+
+ # Enable text wrapping
+ text_frame.word_wrap = True
+
+ # Apply formatting to all paragraphs and runs
+ for paragraph in text_frame.paragraphs:
+ if alignment and alignment in alignment_map:
+ paragraph.alignment = alignment_map[alignment]
+
+ for run in paragraph.runs:
+ font = run.font
+
+ if font_size is not None:
+ font.size = Pt(font_size)
+ if font_name is not None:
+ font.name = font_name
+ if bold is not None:
+ font.bold = bold
+ if italic is not None:
+ font.italic = italic
+ if underline is not None:
+ font.underline = underline
+ if color is not None:
+ r, g, b = color
+ font.color.rgb = RGBColor(r, g, b)
+
+ return result
+
+ except Exception as e:
+ result['success'] = False
+ result['error'] = str(e)
+ return result
+
+
+def add_image(slide, image_path: str, left: float, top: float, width: float = None, height: float = None) -> Any:
+ """
+ Add an image to a slide.
+
+ Args:
+ slide: The slide object
+ image_path: Path to the image file
+ left: Left position in inches
+ top: Top position in inches
+ width: Width in inches (optional)
+ height: Height in inches (optional)
+
+ Returns:
+ The created image shape
+ """
+ if width is not None and height is not None:
+ return slide.shapes.add_picture(
+ image_path, Inches(left), Inches(top), Inches(width), Inches(height)
+ )
+ elif width is not None:
+ return slide.shapes.add_picture(
+ image_path, Inches(left), Inches(top), Inches(width)
+ )
+ elif height is not None:
+ return slide.shapes.add_picture(
+ image_path, Inches(left), Inches(top), height=Inches(height)
+ )
+ else:
+ return slide.shapes.add_picture(
+ image_path, Inches(left), Inches(top)
+ )
+
+
+def add_table(slide, rows: int, cols: int, left: float, top: float, width: float, height: float) -> Any:
+ """
+ Add a table to a slide.
+
+ Args:
+ slide: The slide object
+ rows: Number of rows
+ cols: Number of columns
+ left: Left position in inches
+ top: Top position in inches
+ width: Width in inches
+ height: Height in inches
+
+ Returns:
+ The created table shape
+ """
+ return slide.shapes.add_table(
+ rows, cols, Inches(left), Inches(top), Inches(width), Inches(height)
+ )
+
+
+def format_table_cell(cell, font_size: int = None, font_name: str = None,
+ bold: bool = None, italic: bool = None,
+ color: Tuple[int, int, int] = None, bg_color: Tuple[int, int, int] = None,
+ alignment: str = None, vertical_alignment: str = None) -> None:
+ """
+ Format a table cell.
+
+ Args:
+ cell: The table cell object
+ font_size: Font size in points
+ font_name: Font name
+ bold: Whether text should be bold
+ italic: Whether text should be italic
+ color: RGB color tuple (r, g, b)
+ bg_color: Background RGB color tuple (r, g, b)
+ alignment: Text alignment
+ vertical_alignment: Vertical alignment
+ """
+ # Format text
+ if any([font_size, font_name, bold, italic, color, alignment]):
+ format_text_advanced(
+ cell.text_frame,
+ font_size=font_size,
+ font_name=font_name,
+ bold=bold,
+ italic=italic,
+ color=color,
+ alignment=alignment
+ )
+
+ # Set background color
+ if bg_color:
+ cell.fill.solid()
+ cell.fill.fore_color.rgb = RGBColor(*bg_color)
+
+
+def add_chart(slide, chart_type: str, left: float, top: float, width: float, height: float,
+ categories: List[str], series_names: List[str], series_values: List[List[float]]) -> Any:
+ """
+ Add a chart to a slide.
+
+ Args:
+ slide: The slide object
+ chart_type: Type of chart ('column', 'bar', 'line', 'pie', etc.)
+ left: Left position in inches
+ top: Top position in inches
+ width: Width in inches
+ height: Height in inches
+ categories: List of category names
+ series_names: List of series names
+ series_values: List of value lists for each series
+
+ Returns:
+ The created chart object
+ """
+ # Map chart type names to enum values
+ chart_type_map = {
+ 'column': XL_CHART_TYPE.COLUMN_CLUSTERED,
+ 'stacked_column': XL_CHART_TYPE.COLUMN_STACKED,
+ 'bar': XL_CHART_TYPE.BAR_CLUSTERED,
+ 'stacked_bar': XL_CHART_TYPE.BAR_STACKED,
+ 'line': XL_CHART_TYPE.LINE,
+ 'line_markers': XL_CHART_TYPE.LINE_MARKERS,
+ 'pie': XL_CHART_TYPE.PIE,
+ 'doughnut': XL_CHART_TYPE.DOUGHNUT,
+ 'area': XL_CHART_TYPE.AREA,
+ 'stacked_area': XL_CHART_TYPE.AREA_STACKED,
+ 'scatter': XL_CHART_TYPE.XY_SCATTER,
+ 'radar': XL_CHART_TYPE.RADAR,
+ 'radar_markers': XL_CHART_TYPE.RADAR_MARKERS
+ }
+
+ xl_chart_type = chart_type_map.get(chart_type.lower(), XL_CHART_TYPE.COLUMN_CLUSTERED)
+
+ # Create chart data
+ chart_data = CategoryChartData()
+ chart_data.categories = categories
+
+ for i, series_name in enumerate(series_names):
+ if i < len(series_values):
+ chart_data.add_series(series_name, series_values[i])
+
+ # Add chart to slide
+ chart_shape = slide.shapes.add_chart(
+ xl_chart_type, Inches(left), Inches(top), Inches(width), Inches(height), chart_data
+ )
+
+ return chart_shape.chart
+
+
+def format_chart(chart, has_legend: bool = True, legend_position: str = 'right',
+ has_data_labels: bool = False, title: str = None,
+ x_axis_title: str = None, y_axis_title: str = None,
+ color_scheme: str = None) -> None:
+ """
+ Format a chart with various options.
+
+ Args:
+ chart: The chart object
+ has_legend: Whether to show legend
+ legend_position: Position of legend ('right', 'top', 'bottom', 'left')
+ has_data_labels: Whether to show data labels
+ title: Chart title
+ x_axis_title: X-axis title
+ y_axis_title: Y-axis title
+ color_scheme: Color scheme to apply
+ """
+ try:
+ # Set chart title
+ if title:
+ chart.chart_title.text_frame.text = title
+
+ # Configure legend
+ if has_legend:
+ chart.has_legend = True
+ # Note: Legend position setting may vary by chart type
+ else:
+ chart.has_legend = False
+
+ # Configure data labels
+ if has_data_labels:
+ for series in chart.series:
+ series.has_data_labels = True
+
+ # Set axis titles if available
+ try:
+ if x_axis_title and hasattr(chart, 'category_axis'):
+ chart.category_axis.axis_title.text_frame.text = x_axis_title
+ if y_axis_title and hasattr(chart, 'value_axis'):
+ chart.value_axis.axis_title.text_frame.text = y_axis_title
+ except:
+ pass # Axis titles may not be available for all chart types
+
+ except Exception:
+ pass # Graceful degradation for chart formatting
+
+
+def extract_slide_text_content(slide) -> Dict:
+ """
+ Extract all text content from a slide including placeholders and text shapes.
+
+ Args:
+ slide: The slide object to extract text from
+
+ Returns:
+ Dictionary containing all text content organized by source type
+ """
+ try:
+ text_content = {
+ "slide_title": "",
+ "placeholders": [],
+ "text_shapes": [],
+ "table_text": [],
+ "all_text_combined": ""
+ }
+
+ all_texts = []
+
+ # Extract title from slide if available
+ if hasattr(slide, 'shapes') and hasattr(slide.shapes, 'title') and slide.shapes.title:
+ try:
+ title_text = slide.shapes.title.text_frame.text.strip()
+ if title_text:
+ text_content["slide_title"] = title_text
+ all_texts.append(title_text)
+ except:
+ pass
+
+ # Extract text from all shapes
+ for i, shape in enumerate(slide.shapes):
+ shape_text_info = {
+ "shape_index": i,
+ "shape_name": shape.name,
+ "shape_type": str(shape.shape_type),
+ "text": ""
+ }
+
+ try:
+ # Check if shape has text frame
+ if hasattr(shape, 'text_frame') and shape.text_frame:
+ text = shape.text_frame.text.strip()
+ if text:
+ shape_text_info["text"] = text
+ all_texts.append(text)
+
+ # Categorize by shape type
+ if hasattr(shape, 'placeholder_format'):
+ # This is a placeholder
+ placeholder_info = shape_text_info.copy()
+ placeholder_info["placeholder_type"] = str(shape.placeholder_format.type)
+ placeholder_info["placeholder_idx"] = shape.placeholder_format.idx
+ text_content["placeholders"].append(placeholder_info)
+ else:
+ # This is a regular text shape
+ text_content["text_shapes"].append(shape_text_info)
+
+ # Extract text from tables
+ elif hasattr(shape, 'table'):
+ table_texts = []
+ table = shape.table
+ for row_idx, row in enumerate(table.rows):
+ row_texts = []
+ for col_idx, cell in enumerate(row.cells):
+ cell_text = cell.text_frame.text.strip()
+ if cell_text:
+ row_texts.append(cell_text)
+ all_texts.append(cell_text)
+ if row_texts:
+ table_texts.append({
+ "row": row_idx,
+ "cells": row_texts
+ })
+
+ if table_texts:
+ text_content["table_text"].append({
+ "shape_index": i,
+ "shape_name": shape.name,
+ "table_content": table_texts
+ })
+
+ except Exception as e:
+ # Skip shapes that can't be processed
+ continue
+
+ # Combine all text
+ text_content["all_text_combined"] = "\n".join(all_texts)
+
+ return {
+ "success": True,
+ "text_content": text_content,
+ "total_text_shapes": len(text_content["placeholders"]) + len(text_content["text_shapes"]),
+ "has_title": bool(text_content["slide_title"]),
+ "has_tables": len(text_content["table_text"]) > 0
+ }
+
+ except Exception as e:
+ return {
+ "success": False,
+ "error": f"Failed to extract text content: {str(e)}",
+ "text_content": None
+ }
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/utils/core_utils.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/utils/core_utils.py
new file mode 100644
index 00000000..c19c66cd
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/utils/core_utils.py
@@ -0,0 +1,55 @@
+"""
+Core utility functions for PowerPoint MCP Server.
+Basic operations and error handling.
+"""
+from typing import Any, Callable, List, Tuple, Optional
+
+
+def try_multiple_approaches(operation_name: str, approaches: List[Tuple[Callable, str]]) -> Tuple[Any, Optional[str]]:
+ """
+ Try multiple approaches to perform an operation, returning the first successful result.
+
+ Args:
+ operation_name: Name of the operation for error reporting
+ approaches: List of (approach_func, description) tuples to try
+
+ Returns:
+ Tuple of (result, None) if any approach succeeded, or (None, error_messages) if all failed
+ """
+ error_messages = []
+
+ for approach_func, description in approaches:
+ try:
+ result = approach_func()
+ return result, None
+ except Exception as e:
+ error_messages.append(f"{description}: {str(e)}")
+
+ return None, f"Failed to {operation_name} after trying multiple approaches: {'; '.join(error_messages)}"
+
+
+def safe_operation(operation_name: str, operation_func: Callable, error_message: Optional[str] = None, *args, **kwargs) -> Tuple[Any, Optional[str]]:
+ """
+ Execute an operation safely with standard error handling.
+
+ Args:
+ operation_name: Name of the operation for error reporting
+ operation_func: Function to execute
+ error_message: Custom error message (optional)
+ *args, **kwargs: Arguments to pass to the operation function
+
+ Returns:
+ A tuple (result, error) where error is None if operation was successful
+ """
+ try:
+ result = operation_func(*args, **kwargs)
+ return result, None
+ except ValueError as e:
+ error_msg = error_message or f"Invalid input for {operation_name}: {str(e)}"
+ return None, error_msg
+ except TypeError as e:
+ error_msg = error_message or f"Type error in {operation_name}: {str(e)}"
+ return None, error_msg
+ except Exception as e:
+ error_msg = error_message or f"Failed to execute {operation_name}: {str(e)}"
+ return None, error_msg
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/utils/design_utils.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/utils/design_utils.py
new file mode 100644
index 00000000..b2b22fc7
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/utils/design_utils.py
@@ -0,0 +1,689 @@
+"""
+Design and professional styling utilities for PowerPoint MCP Server.
+Functions for themes, colors, fonts, backgrounds, and visual effects.
+"""
+from pptx import Presentation
+from pptx.util import Inches, Pt
+from pptx.dml.color import RGBColor
+from typing import Dict, List, Tuple, Optional, Any
+from PIL import Image, ImageEnhance, ImageFilter, ImageDraw
+import tempfile
+import os
+from fontTools.ttLib import TTFont
+from fontTools.subset import Subsetter
+
+# Professional color schemes
+PROFESSIONAL_COLOR_SCHEMES = {
+ 'modern_blue': {
+ 'primary': (0, 120, 215), # Microsoft Blue
+ 'secondary': (40, 40, 40), # Dark Gray
+ 'accent1': (0, 176, 240), # Light Blue
+ 'accent2': (255, 192, 0), # Orange
+ 'light': (247, 247, 247), # Light Gray
+ 'text': (68, 68, 68), # Text Gray
+ },
+ 'corporate_gray': {
+ 'primary': (68, 68, 68), # Charcoal
+ 'secondary': (0, 120, 215), # Blue
+ 'accent1': (89, 89, 89), # Medium Gray
+ 'accent2': (217, 217, 217), # Light Gray
+ 'light': (242, 242, 242), # Very Light Gray
+ 'text': (51, 51, 51), # Dark Text
+ },
+ 'elegant_green': {
+ 'primary': (70, 136, 71), # Forest Green
+ 'secondary': (255, 255, 255), # White
+ 'accent1': (146, 208, 80), # Light Green
+ 'accent2': (112, 173, 71), # Medium Green
+ 'light': (238, 236, 225), # Cream
+ 'text': (89, 89, 89), # Gray Text
+ },
+ 'warm_red': {
+ 'primary': (192, 80, 77), # Deep Red
+ 'secondary': (68, 68, 68), # Dark Gray
+ 'accent1': (230, 126, 34), # Orange
+ 'accent2': (241, 196, 15), # Yellow
+ 'light': (253, 253, 253), # White
+ 'text': (44, 62, 80), # Blue Gray
+ }
+}
+
+# Professional typography settings
+PROFESSIONAL_FONTS = {
+ 'title': {
+ 'name': 'Segoe UI',
+ 'size_large': 36,
+ 'size_medium': 28,
+ 'size_small': 24,
+ 'bold': True
+ },
+ 'subtitle': {
+ 'name': 'Segoe UI Light',
+ 'size_large': 20,
+ 'size_medium': 18,
+ 'size_small': 16,
+ 'bold': False
+ },
+ 'body': {
+ 'name': 'Segoe UI',
+ 'size_large': 16,
+ 'size_medium': 14,
+ 'size_small': 12,
+ 'bold': False
+ },
+ 'caption': {
+ 'name': 'Segoe UI',
+ 'size_large': 12,
+ 'size_medium': 10,
+ 'size_small': 9,
+ 'bold': False
+ }
+}
+
+
+def get_professional_color(scheme_name: str, color_type: str) -> Tuple[int, int, int]:
+ """
+ Get a professional color from predefined color schemes.
+
+ Args:
+ scheme_name: Name of the color scheme
+ color_type: Type of color ('primary', 'secondary', 'accent1', 'accent2', 'light', 'text')
+
+ Returns:
+ RGB color tuple (r, g, b)
+ """
+ if scheme_name not in PROFESSIONAL_COLOR_SCHEMES:
+ scheme_name = 'modern_blue' # Default fallback
+
+ scheme = PROFESSIONAL_COLOR_SCHEMES[scheme_name]
+ return scheme.get(color_type, scheme['primary'])
+
+
+def get_professional_font(font_type: str, size_category: str = 'medium') -> Dict:
+ """
+ Get professional font settings.
+
+ Args:
+ font_type: Type of font ('title', 'subtitle', 'body', 'caption')
+ size_category: Size category ('large', 'medium', 'small')
+
+ Returns:
+ Dictionary with font settings
+ """
+ if font_type not in PROFESSIONAL_FONTS:
+ font_type = 'body' # Default fallback
+
+ font_config = PROFESSIONAL_FONTS[font_type]
+ size_key = f'size_{size_category}'
+
+ return {
+ 'name': font_config['name'],
+ 'size': font_config.get(size_key, font_config['size_medium']),
+ 'bold': font_config['bold']
+ }
+
+
+def get_color_schemes() -> Dict:
+ """
+ Get all available professional color schemes.
+
+ Returns:
+ Dictionary of all color schemes with their color values
+ """
+ return {
+ "available_schemes": list(PROFESSIONAL_COLOR_SCHEMES.keys()),
+ "schemes": PROFESSIONAL_COLOR_SCHEMES,
+ "color_types": ["primary", "secondary", "accent1", "accent2", "light", "text"],
+ "description": "Professional color schemes optimized for business presentations"
+ }
+
+
+def add_professional_slide(presentation: Presentation, slide_type: str = 'title_content',
+ color_scheme: str = 'modern_blue', title: str = None,
+ content: List[str] = None) -> Dict:
+ """
+ Add a professionally designed slide.
+
+ Args:
+ presentation: The Presentation object
+ slide_type: Type of slide ('title', 'title_content', 'content', 'blank')
+ color_scheme: Color scheme to apply
+ title: Slide title
+ content: List of content items
+
+ Returns:
+ Dictionary with slide creation results
+ """
+ # Map slide types to layout indices
+ layout_map = {
+ 'title': 0, # Title slide
+ 'title_content': 1, # Title and content
+ 'content': 6, # Content only
+ 'blank': 6 # Blank layout
+ }
+
+ layout_index = layout_map.get(slide_type, 1)
+
+ try:
+ layout = presentation.slide_layouts[layout_index]
+ slide = presentation.slides.add_slide(layout)
+
+ # Set title if provided
+ if title and slide.shapes.title:
+ slide.shapes.title.text = title
+
+ # Add content if provided
+ if content and len(slide.placeholders) > 1:
+ content_placeholder = slide.placeholders[1]
+ content_text = '\n'.join([f"• {item}" for item in content])
+ content_placeholder.text = content_text
+
+ return {
+ "success": True,
+ "slide_index": len(presentation.slides) - 1,
+ "slide_type": slide_type,
+ "color_scheme": color_scheme
+ }
+ except Exception as e:
+ return {
+ "success": False,
+ "error": str(e)
+ }
+
+
+def apply_professional_theme(presentation: Presentation, color_scheme: str = 'modern_blue',
+ apply_to_existing: bool = True) -> Dict:
+ """
+ Apply a professional theme to the presentation.
+
+ Args:
+ presentation: The Presentation object
+ color_scheme: Color scheme to apply
+ apply_to_existing: Whether to apply to existing slides
+
+ Returns:
+ Dictionary with theme application results
+ """
+ try:
+ # This is a placeholder implementation as theme application
+ # requires deep manipulation of presentation XML
+ return {
+ "success": True,
+ "color_scheme": color_scheme,
+ "slides_affected": len(presentation.slides) if apply_to_existing else 0,
+ "message": f"Applied {color_scheme} theme to presentation"
+ }
+ except Exception as e:
+ return {
+ "success": False,
+ "error": str(e)
+ }
+
+
+def enhance_existing_slide(slide, color_scheme: str = 'modern_blue',
+ enhance_title: bool = True, enhance_content: bool = True,
+ enhance_shapes: bool = True, enhance_charts: bool = True) -> Dict:
+ """
+ Enhance an existing slide with professional styling.
+
+ Args:
+ slide: The slide object
+ color_scheme: Color scheme to apply
+ enhance_title: Whether to enhance title formatting
+ enhance_content: Whether to enhance content formatting
+ enhance_shapes: Whether to enhance shape formatting
+ enhance_charts: Whether to enhance chart formatting
+
+ Returns:
+ Dictionary with enhancement results
+ """
+ enhancements_applied = []
+
+ try:
+ # Enhance title
+ if enhance_title and slide.shapes.title:
+ primary_color = get_professional_color(color_scheme, 'primary')
+ title_font = get_professional_font('title', 'large')
+ # Apply title formatting (simplified)
+ enhancements_applied.append("title")
+
+ # Enhance other shapes
+ if enhance_shapes:
+ for shape in slide.shapes:
+ if hasattr(shape, 'text_frame') and shape != slide.shapes.title:
+ # Apply content formatting (simplified)
+ pass
+ enhancements_applied.append("shapes")
+
+ return {
+ "success": True,
+ "enhancements_applied": enhancements_applied,
+ "color_scheme": color_scheme
+ }
+ except Exception as e:
+ return {
+ "success": False,
+ "error": str(e)
+ }
+
+
+def set_slide_gradient_background(slide, start_color: Tuple[int, int, int],
+ end_color: Tuple[int, int, int], direction: str = "horizontal") -> None:
+ """
+ Set a gradient background for a slide using a generated image.
+
+ Args:
+ slide: The slide object
+ start_color: Starting RGB color tuple
+ end_color: Ending RGB color tuple
+ direction: Gradient direction ('horizontal', 'vertical', 'diagonal')
+ """
+ try:
+ # Create gradient image
+ width, height = 1920, 1080 # Standard slide dimensions
+ gradient_img = create_gradient_image(width, height, start_color, end_color, direction)
+
+ # Save to temporary file
+ with tempfile.NamedTemporaryFile(delete=False, suffix='.png') as temp_file:
+ gradient_img.save(temp_file.name, 'PNG')
+ temp_path = temp_file.name
+
+ # Add as background image (simplified - actual implementation would need XML manipulation)
+ try:
+ slide.shapes.add_picture(temp_path, 0, 0, Inches(10), Inches(7.5))
+ finally:
+ # Clean up temporary file
+ if os.path.exists(temp_path):
+ os.unlink(temp_path)
+
+ except Exception:
+ pass # Graceful fallback
+
+
+def create_professional_gradient_background(slide, color_scheme: str = 'modern_blue',
+ style: str = 'subtle', direction: str = 'diagonal') -> None:
+ """
+ Create a professional gradient background using predefined color schemes.
+
+ Args:
+ slide: The slide object
+ color_scheme: Professional color scheme to use
+ style: Gradient style ('subtle', 'bold', 'accent')
+ direction: Gradient direction ('horizontal', 'vertical', 'diagonal')
+ """
+ # Get colors based on style
+ if style == 'subtle':
+ start_color = get_professional_color(color_scheme, 'light')
+ end_color = get_professional_color(color_scheme, 'secondary')
+ elif style == 'bold':
+ start_color = get_professional_color(color_scheme, 'primary')
+ end_color = get_professional_color(color_scheme, 'accent1')
+ else: # accent
+ start_color = get_professional_color(color_scheme, 'accent1')
+ end_color = get_professional_color(color_scheme, 'accent2')
+
+ set_slide_gradient_background(slide, start_color, end_color, direction)
+
+
+def create_gradient_image(width: int, height: int, start_color: Tuple[int, int, int],
+ end_color: Tuple[int, int, int], direction: str = 'horizontal') -> Image.Image:
+ """
+ Create a gradient image using PIL.
+
+ Args:
+ width: Image width in pixels
+ height: Image height in pixels
+ start_color: Starting RGB color tuple
+ end_color: Ending RGB color tuple
+ direction: Gradient direction
+
+ Returns:
+ PIL Image object with gradient
+ """
+ img = Image.new('RGB', (width, height))
+ draw = ImageDraw.Draw(img)
+
+ if direction == 'horizontal':
+ for x in range(width):
+ ratio = x / width
+ r = int(start_color[0] * (1 - ratio) + end_color[0] * ratio)
+ g = int(start_color[1] * (1 - ratio) + end_color[1] * ratio)
+ b = int(start_color[2] * (1 - ratio) + end_color[2] * ratio)
+ draw.line([(x, 0), (x, height)], fill=(r, g, b))
+ elif direction == 'vertical':
+ for y in range(height):
+ ratio = y / height
+ r = int(start_color[0] * (1 - ratio) + end_color[0] * ratio)
+ g = int(start_color[1] * (1 - ratio) + end_color[1] * ratio)
+ b = int(start_color[2] * (1 - ratio) + end_color[2] * ratio)
+ draw.line([(0, y), (width, y)], fill=(r, g, b))
+ else: # diagonal
+ for x in range(width):
+ for y in range(height):
+ ratio = (x + y) / (width + height)
+ r = int(start_color[0] * (1 - ratio) + end_color[0] * ratio)
+ g = int(start_color[1] * (1 - ratio) + end_color[1] * ratio)
+ b = int(start_color[2] * (1 - ratio) + end_color[2] * ratio)
+ img.putpixel((x, y), (r, g, b))
+
+ return img
+
+
+def format_shape(shape, fill_color: Tuple[int, int, int] = None,
+ line_color: Tuple[int, int, int] = None, line_width: float = None) -> None:
+ """
+ Format a shape with color and line properties.
+
+ Args:
+ shape: The shape object
+ fill_color: RGB fill color tuple
+ line_color: RGB line color tuple
+ line_width: Line width in points
+ """
+ try:
+ if fill_color:
+ shape.fill.solid()
+ shape.fill.fore_color.rgb = RGBColor(*fill_color)
+
+ if line_color:
+ shape.line.color.rgb = RGBColor(*line_color)
+
+ if line_width is not None:
+ shape.line.width = Pt(line_width)
+ except Exception:
+ pass # Graceful fallback
+
+
+# Image enhancement functions
+def enhance_image_with_pillow(image_path: str, brightness: float = 1.0, contrast: float = 1.0,
+ saturation: float = 1.0, sharpness: float = 1.0,
+ blur_radius: float = 0, filter_type: str = None,
+ output_path: str = None) -> str:
+ """
+ Enhance an image using PIL with various adjustments.
+
+ Args:
+ image_path: Path to input image
+ brightness: Brightness factor (1.0 = no change)
+ contrast: Contrast factor (1.0 = no change)
+ saturation: Saturation factor (1.0 = no change)
+ sharpness: Sharpness factor (1.0 = no change)
+ blur_radius: Blur radius (0 = no blur)
+ filter_type: Filter type ('BLUR', 'SHARPEN', 'SMOOTH', etc.)
+ output_path: Output path (if None, generates temporary file)
+
+ Returns:
+ Path to enhanced image
+ """
+ if not os.path.exists(image_path):
+ raise FileNotFoundError(f"Image file not found: {image_path}")
+
+ # Open image
+ img = Image.open(image_path)
+
+ # Apply enhancements
+ if brightness != 1.0:
+ enhancer = ImageEnhance.Brightness(img)
+ img = enhancer.enhance(brightness)
+
+ if contrast != 1.0:
+ enhancer = ImageEnhance.Contrast(img)
+ img = enhancer.enhance(contrast)
+
+ if saturation != 1.0:
+ enhancer = ImageEnhance.Color(img)
+ img = enhancer.enhance(saturation)
+
+ if sharpness != 1.0:
+ enhancer = ImageEnhance.Sharpness(img)
+ img = enhancer.enhance(sharpness)
+
+ if blur_radius > 0:
+ img = img.filter(ImageFilter.GaussianBlur(radius=blur_radius))
+
+ if filter_type:
+ filter_map = {
+ 'BLUR': ImageFilter.BLUR,
+ 'SHARPEN': ImageFilter.SHARPEN,
+ 'SMOOTH': ImageFilter.SMOOTH,
+ 'EDGE_ENHANCE': ImageFilter.EDGE_ENHANCE
+ }
+ if filter_type.upper() in filter_map:
+ img = img.filter(filter_map[filter_type.upper()])
+
+ # Save enhanced image
+ if output_path is None:
+ output_path = tempfile.mktemp(suffix='.png')
+
+ img.save(output_path)
+ return output_path
+
+
+def apply_professional_image_enhancement(image_path: str, style: str = 'presentation',
+ output_path: str = None) -> str:
+ """
+ Apply professional image enhancement presets.
+
+ Args:
+ image_path: Path to input image
+ style: Enhancement style ('presentation', 'bright', 'soft')
+ output_path: Output path (if None, generates temporary file)
+
+ Returns:
+ Path to enhanced image
+ """
+ enhancement_presets = {
+ 'presentation': {
+ 'brightness': 1.1,
+ 'contrast': 1.15,
+ 'saturation': 1.1,
+ 'sharpness': 1.2
+ },
+ 'bright': {
+ 'brightness': 1.2,
+ 'contrast': 1.1,
+ 'saturation': 1.2,
+ 'sharpness': 1.1
+ },
+ 'soft': {
+ 'brightness': 1.05,
+ 'contrast': 0.95,
+ 'saturation': 0.95,
+ 'sharpness': 0.9,
+ 'blur_radius': 0.5
+ }
+ }
+
+ preset = enhancement_presets.get(style, enhancement_presets['presentation'])
+ return enhance_image_with_pillow(image_path, output_path=output_path, **preset)
+
+
+# Picture effects functions (simplified implementations)
+def apply_picture_shadow(picture_shape, shadow_type: str = 'outer', blur_radius: float = 4.0,
+ distance: float = 3.0, direction: float = 315.0,
+ color: Tuple[int, int, int] = (0, 0, 0), transparency: float = 0.6) -> Dict:
+ """Apply shadow effect to a picture shape."""
+ try:
+ # Simplified implementation - actual shadow effects require XML manipulation
+ return {"success": True, "effect": "shadow", "message": "Shadow effect applied"}
+ except Exception as e:
+ return {"success": False, "error": str(e)}
+
+
+def apply_picture_reflection(picture_shape, size: float = 0.5, transparency: float = 0.5,
+ distance: float = 0.0, blur: float = 4.0) -> Dict:
+ """Apply reflection effect to a picture shape."""
+ try:
+ return {"success": True, "effect": "reflection", "message": "Reflection effect applied"}
+ except Exception as e:
+ return {"success": False, "error": str(e)}
+
+
+def apply_picture_glow(picture_shape, size: float = 5.0, color: Tuple[int, int, int] = (0, 176, 240),
+ transparency: float = 0.4) -> Dict:
+ """Apply glow effect to a picture shape."""
+ try:
+ return {"success": True, "effect": "glow", "message": "Glow effect applied"}
+ except Exception as e:
+ return {"success": False, "error": str(e)}
+
+
+def apply_picture_soft_edges(picture_shape, radius: float = 2.5) -> Dict:
+ """Apply soft edges effect to a picture shape."""
+ try:
+ return {"success": True, "effect": "soft_edges", "message": "Soft edges effect applied"}
+ except Exception as e:
+ return {"success": False, "error": str(e)}
+
+
+def apply_picture_rotation(picture_shape, rotation: float) -> Dict:
+ """Apply rotation to a picture shape."""
+ try:
+ picture_shape.rotation = rotation
+ return {"success": True, "effect": "rotation", "message": f"Rotated by {rotation} degrees"}
+ except Exception as e:
+ return {"success": False, "error": str(e)}
+
+
+def apply_picture_transparency(picture_shape, transparency: float) -> Dict:
+ """Apply transparency to a picture shape."""
+ try:
+ return {"success": True, "effect": "transparency", "message": "Transparency applied"}
+ except Exception as e:
+ return {"success": False, "error": str(e)}
+
+
+def apply_picture_bevel(picture_shape, bevel_type: str = 'circle', width: float = 6.0,
+ height: float = 6.0) -> Dict:
+ """Apply bevel effect to a picture shape."""
+ try:
+ return {"success": True, "effect": "bevel", "message": "Bevel effect applied"}
+ except Exception as e:
+ return {"success": False, "error": str(e)}
+
+
+def apply_picture_filter(picture_shape, filter_type: str = 'none', intensity: float = 0.5) -> Dict:
+ """Apply color filter to a picture shape."""
+ try:
+ return {"success": True, "effect": "filter", "message": f"Applied {filter_type} filter"}
+ except Exception as e:
+ return {"success": False, "error": str(e)}
+
+
+# Font management functions
+def analyze_font_file(font_path: str) -> Dict:
+ """
+ Analyze a font file using FontTools.
+
+ Args:
+ font_path: Path to the font file
+
+ Returns:
+ Dictionary with font analysis results
+ """
+ try:
+ font = TTFont(font_path)
+
+ # Get basic font information
+ name_table = font['name']
+ font_family = ""
+ font_style = ""
+
+ for record in name_table.names:
+ if record.nameID == 1: # Font Family name
+ font_family = str(record)
+ elif record.nameID == 2: # Font Subfamily name
+ font_style = str(record)
+
+ return {
+ "file_path": font_path,
+ "font_family": font_family,
+ "font_style": font_style,
+ "num_glyphs": font.getGlyphSet().keys().__len__(),
+ "file_size": os.path.getsize(font_path),
+ "analysis_success": True
+ }
+ except Exception as e:
+ return {
+ "file_path": font_path,
+ "analysis_success": False,
+ "error": str(e)
+ }
+
+
+def optimize_font_for_presentation(font_path: str, output_path: str = None,
+ text_content: str = None) -> str:
+ """
+ Optimize a font file for presentation use.
+
+ Args:
+ font_path: Path to input font file
+ output_path: Path for optimized font (if None, generates temporary file)
+ text_content: Text content to subset for (if None, keeps all characters)
+
+ Returns:
+ Path to optimized font file
+ """
+ try:
+ font = TTFont(font_path)
+
+ if text_content:
+ # Subset font to only include used characters
+ subsetter = Subsetter()
+ subsetter.populate(text=text_content)
+ subsetter.subset(font)
+
+ # Generate output path if not provided
+ if output_path is None:
+ output_path = tempfile.mktemp(suffix='.ttf')
+
+ font.save(output_path)
+ return output_path
+ except Exception as e:
+ raise Exception(f"Font optimization failed: {str(e)}")
+
+
+def get_font_recommendations(font_path: str, presentation_type: str = 'business') -> Dict:
+ """
+ Get font usage recommendations.
+
+ Args:
+ font_path: Path to font file
+ presentation_type: Type of presentation ('business', 'creative', 'academic')
+
+ Returns:
+ Dictionary with font recommendations
+ """
+ try:
+ analysis = analyze_font_file(font_path)
+
+ recommendations = {
+ "suitable_for": [],
+ "recommended_sizes": {},
+ "usage_tips": [],
+ "compatibility": "good"
+ }
+
+ if presentation_type == 'business':
+ recommendations["suitable_for"] = ["titles", "body_text", "captions"]
+ recommendations["recommended_sizes"] = {
+ "title": "24-36pt",
+ "subtitle": "16-20pt",
+ "body": "12-16pt"
+ }
+ recommendations["usage_tips"] = [
+ "Use for professional presentations",
+ "Good for readability at distance",
+ "Works well with business themes"
+ ]
+
+ return {
+ "font_analysis": analysis,
+ "presentation_type": presentation_type,
+ "recommendations": recommendations
+ }
+ except Exception as e:
+ return {
+ "error": str(e),
+ "recommendations": None
+ }
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/utils/presentation_utils.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/utils/presentation_utils.py
new file mode 100644
index 00000000..849d8bb0
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/utils/presentation_utils.py
@@ -0,0 +1,217 @@
+"""
+Presentation management utilities for PowerPoint MCP Server.
+Functions for creating, opening, saving, and managing presentations.
+"""
+from pptx import Presentation
+from typing import Dict, List, Optional
+import os
+
+
+def create_presentation() -> Presentation:
+ """
+ Create a new PowerPoint presentation.
+
+ Returns:
+ A new Presentation object
+ """
+ return Presentation()
+
+
+def open_presentation(file_path: str) -> Presentation:
+ """
+ Open an existing PowerPoint presentation.
+
+ Args:
+ file_path: Path to the PowerPoint file
+
+ Returns:
+ A Presentation object
+ """
+ return Presentation(file_path)
+
+
+def create_presentation_from_template(template_path: str) -> Presentation:
+ """
+ Create a new PowerPoint presentation from a template file.
+
+ Args:
+ template_path: Path to the template .pptx file
+
+ Returns:
+ A new Presentation object based on the template
+
+ Raises:
+ FileNotFoundError: If the template file doesn't exist
+ Exception: If the template file is corrupted or invalid
+ """
+ if not os.path.exists(template_path):
+ raise FileNotFoundError(f"Template file not found: {template_path}")
+
+ if not template_path.lower().endswith(('.pptx', '.potx')):
+ raise ValueError("Template file must be a .pptx or .potx file")
+
+ try:
+ # Load the template file as a presentation
+ presentation = Presentation(template_path)
+ return presentation
+ except Exception as e:
+ raise Exception(f"Failed to load template file '{template_path}': {str(e)}")
+
+
+def save_presentation(presentation: Presentation, file_path: str) -> str:
+ """
+ Save a PowerPoint presentation to a file.
+
+ Args:
+ presentation: The Presentation object
+ file_path: Path where the file should be saved
+
+ Returns:
+ The file path where the presentation was saved
+ """
+ presentation.save(file_path)
+ return file_path
+
+
+def get_template_info(template_path: str) -> Dict:
+ """
+ Get information about a template file.
+
+ Args:
+ template_path: Path to the template .pptx file
+
+ Returns:
+ Dictionary containing template information
+ """
+ if not os.path.exists(template_path):
+ raise FileNotFoundError(f"Template file not found: {template_path}")
+
+ try:
+ presentation = Presentation(template_path)
+
+ # Get slide layouts
+ layouts = get_slide_layouts(presentation)
+
+ # Get core properties
+ core_props = get_core_properties(presentation)
+
+ # Get slide count
+ slide_count = len(presentation.slides)
+
+ # Get file size
+ file_size = os.path.getsize(template_path)
+
+ return {
+ "template_path": template_path,
+ "file_size_bytes": file_size,
+ "slide_count": slide_count,
+ "layout_count": len(layouts),
+ "slide_layouts": layouts,
+ "core_properties": core_props
+ }
+ except Exception as e:
+ raise Exception(f"Failed to read template info from '{template_path}': {str(e)}")
+
+
+def get_presentation_info(presentation: Presentation) -> Dict:
+ """
+ Get information about a presentation.
+
+ Args:
+ presentation: The Presentation object
+
+ Returns:
+ Dictionary containing presentation information
+ """
+ try:
+ # Get slide layouts
+ layouts = get_slide_layouts(presentation)
+
+ # Get core properties
+ core_props = get_core_properties(presentation)
+
+ # Get slide count
+ slide_count = len(presentation.slides)
+
+ return {
+ "slide_count": slide_count,
+ "layout_count": len(layouts),
+ "slide_layouts": layouts,
+ "core_properties": core_props,
+ "slide_width": presentation.slide_width,
+ "slide_height": presentation.slide_height
+ }
+ except Exception as e:
+ raise Exception(f"Failed to get presentation info: {str(e)}")
+
+
+def get_slide_layouts(presentation: Presentation) -> List[Dict]:
+ """
+ Get all available slide layouts in the presentation.
+
+ Args:
+ presentation: The Presentation object
+
+ Returns:
+ A list of dictionaries with layout information
+ """
+ layouts = []
+ for i, layout in enumerate(presentation.slide_layouts):
+ layout_info = {
+ "index": i,
+ "name": layout.name,
+ "placeholder_count": len(layout.placeholders)
+ }
+ layouts.append(layout_info)
+ return layouts
+
+
+def set_core_properties(presentation: Presentation, title: str = None, subject: str = None,
+ author: str = None, keywords: str = None, comments: str = None) -> None:
+ """
+ Set core document properties.
+
+ Args:
+ presentation: The Presentation object
+ title: Document title
+ subject: Document subject
+ author: Document author
+ keywords: Document keywords
+ comments: Document comments
+ """
+ core_props = presentation.core_properties
+
+ if title is not None:
+ core_props.title = title
+ if subject is not None:
+ core_props.subject = subject
+ if author is not None:
+ core_props.author = author
+ if keywords is not None:
+ core_props.keywords = keywords
+ if comments is not None:
+ core_props.comments = comments
+
+
+def get_core_properties(presentation: Presentation) -> Dict:
+ """
+ Get core document properties.
+
+ Args:
+ presentation: The Presentation object
+
+ Returns:
+ Dictionary containing core properties
+ """
+ core_props = presentation.core_properties
+
+ return {
+ "title": core_props.title,
+ "subject": core_props.subject,
+ "author": core_props.author,
+ "keywords": core_props.keywords,
+ "comments": core_props.comments,
+ "created": core_props.created.isoformat() if core_props.created else None,
+ "last_modified_by": core_props.last_modified_by,
+ "modified": core_props.modified.isoformat() if core_props.modified else None
+ }
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/utils/template_utils.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/utils/template_utils.py
new file mode 100644
index 00000000..ebd1f532
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/utils/template_utils.py
@@ -0,0 +1,1143 @@
+"""
+Enhanced template management utilities for PowerPoint MCP Server.
+Advanced slide creation with dynamic sizing, auto-wrapping, and visual effects.
+Combines features from both basic and enhanced template systems.
+"""
+import json
+import os
+import re
+from typing import Dict, List, Optional, Any, Tuple
+from pptx import Presentation
+from pptx.util import Inches, Pt
+from pptx.dml.color import RGBColor
+from pptx.enum.text import PP_ALIGN, MSO_VERTICAL_ANCHOR
+from pptx.enum.shapes import MSO_SHAPE
+import utils.content_utils as content_utils
+import utils.design_utils as design_utils
+
+
+class TextSizeCalculator:
+ """Calculate optimal text sizes based on content and container dimensions."""
+
+ def __init__(self):
+ self.character_widths = {
+ 'narrow': 0.6, # i, l, t
+ 'normal': 1.0, # most characters
+ 'wide': 1.3, # m, w
+ 'space': 0.5 # space character
+ }
+
+ def estimate_text_width(self, text: str, font_size: int) -> float:
+ """Estimate text width in points based on character analysis."""
+ if not text:
+ return 0
+
+ width = 0
+ for char in text:
+ if char in 'iltj':
+ width += self.character_widths['narrow']
+ elif char in 'mwMW':
+ width += self.character_widths['wide']
+ elif char == ' ':
+ width += self.character_widths['space']
+ else:
+ width += self.character_widths['normal']
+
+ return width * font_size * 0.6 # Approximation factor
+
+ def estimate_text_height(self, text: str, font_size: int, line_spacing: float = 1.2) -> float:
+ """Estimate text height based on line count and spacing."""
+ lines = len(text.split('\n'))
+ return lines * font_size * line_spacing * 1.3 # Convert to points
+
+ def calculate_optimal_font_size(self, text: str, container_width: float,
+ container_height: float, font_type: str = 'body',
+ min_size: int = 8, max_size: int = 36) -> int:
+ """Calculate optimal font size to fit text in container."""
+ container_width_pts = container_width * 72 # Convert inches to points
+ container_height_pts = container_height * 72
+
+ # Start with a reasonable size and adjust
+ for font_size in range(max_size, min_size - 1, -1):
+ estimated_width = self.estimate_text_width(text, font_size)
+ estimated_height = self.estimate_text_height(text, font_size)
+
+ if estimated_width <= container_width_pts * 0.9 and estimated_height <= container_height_pts * 0.9:
+ return font_size
+
+ return min_size
+
+ def wrap_text_intelligently(self, text: str, max_width: float, font_size: int) -> str:
+ """Intelligently wrap text to fit within specified width."""
+ if not text:
+ return text
+
+ max_width_pts = max_width * 72
+ words = text.split()
+ wrapped_lines = []
+ current_line = []
+
+ for word in words:
+ test_line = current_line + [word]
+ test_text = ' '.join(test_line)
+
+ if self.estimate_text_width(test_text, font_size) <= max_width_pts:
+ current_line.append(word)
+ else:
+ if current_line:
+ wrapped_lines.append(' '.join(current_line))
+ current_line = [word]
+ else:
+ # Single word is too long, force wrap
+ wrapped_lines.append(word)
+
+ if current_line:
+ wrapped_lines.append(' '.join(current_line))
+
+ return '\n'.join(wrapped_lines)
+
+
+class VisualEffectsManager:
+ """Manage and apply visual effects to PowerPoint elements."""
+
+ def __init__(self, templates_data: Dict):
+ self.templates_data = templates_data
+ self.text_effects = templates_data.get('text_effects', {})
+ self.image_effects = templates_data.get('image_effects', {})
+
+ def apply_text_effects(self, text_frame, effects: List[str], color_scheme: str) -> None:
+ """Apply text effects like shadows, glows, and outlines."""
+ for effect_name in effects:
+ if effect_name not in self.text_effects:
+ continue
+
+ effect_config = self.text_effects[effect_name]
+ effect_type = effect_config.get('type')
+
+ # Note: These are simplified implementations
+ # Full implementation would require XML manipulation
+ try:
+ if effect_type == 'shadow':
+ self._apply_text_shadow(text_frame, effect_config, color_scheme)
+ elif effect_type == 'glow':
+ self._apply_text_glow(text_frame, effect_config, color_scheme)
+ elif effect_type == 'outline':
+ self._apply_text_outline(text_frame, effect_config, color_scheme)
+ except Exception:
+ # Graceful fallback if effect application fails
+ pass
+
+ def _apply_text_shadow(self, text_frame, config: Dict, color_scheme: str) -> None:
+ """Apply shadow effect to text (simplified implementation)."""
+ # In a full implementation, this would manipulate the XML directly
+ # For now, we'll apply basic formatting that creates a shadow-like effect
+ for paragraph in text_frame.paragraphs:
+ for run in paragraph.runs:
+ # Make text slightly bolder to simulate shadow depth
+ run.font.bold = True
+
+ def _apply_text_glow(self, text_frame, config: Dict, color_scheme: str) -> None:
+ """Apply glow effect to text (simplified implementation)."""
+ pass # Would require XML manipulation for true glow effect
+
+ def _apply_text_outline(self, text_frame, config: Dict, color_scheme: str) -> None:
+ """Apply outline effect to text (simplified implementation)."""
+ pass # Would require XML manipulation for true outline effect
+
+ def apply_image_effects(self, image_shape, effect_name: str, color_scheme: str) -> None:
+ """Apply visual effects to image shapes."""
+ if effect_name not in self.image_effects:
+ return
+
+ effect_config = self.image_effects[effect_name]
+
+ try:
+ # Apply shadow if specified
+ if 'shadow' in effect_config:
+ shadow_config = effect_config['shadow']
+ # Simplified shadow application
+ pass
+
+ # Apply border if specified
+ if 'border' in effect_config:
+ border_config = effect_config['border']
+ if 'width' in border_config:
+ image_shape.line.width = Pt(border_config['width'])
+ if 'color_role' in border_config:
+ color = self._get_color_from_scheme(color_scheme, border_config['color_role'])
+ image_shape.line.color.rgb = RGBColor(*color)
+ elif 'color' in border_config:
+ image_shape.line.color.rgb = RGBColor(*border_config['color'])
+
+ except Exception:
+ # Graceful fallback
+ pass
+
+ def _get_color_from_scheme(self, color_scheme: str, color_role: str) -> Tuple[int, int, int]:
+ """Get color from scheme (helper method)."""
+ schemes = self.templates_data.get('color_schemes', {})
+ if color_scheme in schemes and color_role in schemes[color_scheme]:
+ return tuple(schemes[color_scheme][color_role])
+ return (0, 0, 0) # Default black
+
+
+class EnhancedTemplateManager:
+ """Enhanced template manager with dynamic features."""
+
+ def __init__(self, template_file_path: str = None):
+ self.text_calculator = TextSizeCalculator()
+ self.load_templates(template_file_path)
+ self.effects_manager = VisualEffectsManager(self.templates_data)
+
+ def load_templates(self, template_file_path: str = None) -> None:
+ """Load unified templates with all dynamic features."""
+ if template_file_path is None:
+ current_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+ # Use the unified template file
+ template_file_path = os.path.join(current_dir, 'slide_layout_templates.json')
+
+ try:
+ with open(template_file_path, 'r', encoding='utf-8') as f:
+ self.templates_data = json.load(f)
+ except FileNotFoundError:
+ raise FileNotFoundError(f"Template file not found: {template_file_path}")
+ except json.JSONDecodeError as e:
+ raise ValueError(f"Invalid JSON in template file: {str(e)}")
+
+
+ def get_dynamic_font_size(self, element: Dict, content: str = None) -> int:
+ """Calculate dynamic font size based on content and container."""
+ content = content or element.get('placeholder_text', '')
+ if not content:
+ return 14 # Default size
+
+ # Get container dimensions
+ pos = element.get('position', {})
+ container_width = pos.get('width', 4.0)
+ container_height = pos.get('height', 1.0)
+
+ # Get font constraints
+ font_type = element.get('styling', {}).get('font_type', 'body')
+ sizing_rules = self.templates_data.get('auto_sizing_rules', {})
+ base_sizes = sizing_rules.get('text_measurement', {}).get('base_font_sizes', {})
+
+ if font_type in base_sizes:
+ min_size = base_sizes[font_type]['min']
+ max_size = base_sizes[font_type]['max']
+ default_size = base_sizes[font_type]['default']
+ else:
+ min_size, max_size, default_size = 10, 18, 14
+
+ # Check if dynamic sizing is requested
+ font_size_setting = element.get('styling', {}).get('font_size')
+ if font_size_setting == 'dynamic':
+ return self.text_calculator.calculate_optimal_font_size(
+ content, container_width, container_height, font_type, min_size, max_size
+ )
+
+ return default_size
+
+ def apply_enhanced_slide_template(self, slide, template_id: str, color_scheme: str = 'modern_blue',
+ content_mapping: Dict = None, image_paths: Dict = None) -> Dict:
+ """Apply enhanced slide template with all dynamic features."""
+ try:
+ if template_id not in self.templates_data.get('templates', {}):
+ # Fall back to regular template application
+ return apply_slide_template_basic(slide, template_id, color_scheme, content_mapping, image_paths)
+
+ template = self.templates_data['templates'][template_id]
+ elements_created = []
+
+ # Apply enhanced background if specified
+ background_config = template.get('background')
+ if background_config:
+ apply_slide_background(slide, background_config, self.templates_data, color_scheme)
+
+ # Create enhanced elements
+ for element in template.get('elements', []):
+ element_type = element.get('type')
+ element_role = element.get('role', '')
+
+ try:
+ # Override content if provided
+ custom_content = None
+ if content_mapping and element_role in content_mapping:
+ custom_content = content_mapping[element_role]
+
+ created_element = None
+
+ if element_type == 'text':
+ created_element = self.create_enhanced_text_element(
+ slide, element, self.templates_data, color_scheme, custom_content
+ )
+ elif element_type == 'shape':
+ created_element = create_shape_element(slide, element, self.templates_data, color_scheme)
+ elif element_type == 'image':
+ image_path = image_paths.get(element_role) if image_paths else None
+ created_element = create_image_element(slide, element, image_path)
+ elif element_type == 'table':
+ created_element = create_table_element(slide, element, self.templates_data, color_scheme)
+ elif element_type == 'chart':
+ created_element = create_chart_element(slide, element, self.templates_data, color_scheme)
+
+ if created_element:
+ elements_created.append({
+ 'type': element_type,
+ 'role': element_role,
+ 'index': len(slide.shapes) - 1,
+ 'enhanced_features': self.get_element_features(element)
+ })
+
+ except Exception as e:
+ elements_created.append({
+ 'type': element_type,
+ 'role': element_role,
+ 'error': str(e)
+ })
+
+ return {
+ 'success': True,
+ 'template_id': template_id,
+ 'template_name': template.get('name', template_id),
+ 'color_scheme': color_scheme,
+ 'elements_created': elements_created,
+ 'enhanced_features_applied': [
+ 'Dynamic text sizing',
+ 'Automatic text wrapping',
+ 'Visual effects',
+ 'Intelligent content adaptation'
+ ]
+ }
+
+ except Exception as e:
+ return {
+ 'success': False,
+ 'error': f"Failed to apply enhanced template: {str(e)}"
+ }
+
+ def create_enhanced_text_element(self, slide, element: Dict, templates_data: Dict,
+ color_scheme: str, custom_content: str = None) -> Any:
+ """Create text element with enhanced features."""
+ pos = element['position']
+
+ # Determine content
+ content = custom_content or element.get('placeholder_text', '')
+
+ # Apply auto-wrapping if enabled
+ styling = element.get('styling', {})
+ if styling.get('auto_wrap', False):
+ container_width = pos.get('width', 4.0)
+ font_size = self.get_dynamic_font_size(element, content)
+ content = self.text_calculator.wrap_text_intelligently(content, container_width, font_size)
+
+ # Create text box
+ textbox = slide.shapes.add_textbox(
+ Inches(pos['left']),
+ Inches(pos['top']),
+ Inches(pos['width']),
+ Inches(pos['height'])
+ )
+
+ textbox.text_frame.text = content
+ textbox.text_frame.word_wrap = True
+
+ # Apply dynamic font sizing
+ font_size = self.get_dynamic_font_size(element, content)
+
+ # Apply enhanced styling
+ self.apply_enhanced_text_styling(textbox.text_frame, element, templates_data, color_scheme, font_size)
+
+ # Apply auto-fit if enabled
+ if styling.get('auto_fit', False):
+ textbox.text_frame.auto_size = True
+
+ return textbox
+
+ def apply_enhanced_text_styling(self, text_frame, element: Dict, templates_data: Dict,
+ color_scheme: str, font_size: int) -> None:
+ """Apply enhanced text styling with effects and dynamic features."""
+ styling = element.get('styling', {})
+
+ # Get typography style
+ typography_style = templates_data.get('typography_styles', {}).get('modern_sans', {})
+ font_type = styling.get('font_type', 'body')
+ font_config = typography_style.get(font_type, {'name': 'Segoe UI', 'weight': 'normal'})
+
+ # Color handling
+ color = None
+ if 'color_role' in styling:
+ color = get_color_from_scheme(templates_data, color_scheme, styling['color_role'])
+ elif 'color' in styling:
+ color = tuple(styling['color'])
+
+ # Alignment mapping
+ alignment_map = {
+ 'left': PP_ALIGN.LEFT,
+ 'center': PP_ALIGN.CENTER,
+ 'right': PP_ALIGN.RIGHT,
+ 'justify': PP_ALIGN.JUSTIFY
+ }
+
+ # Vertical alignment mapping
+ vertical_alignment_map = {
+ 'top': MSO_VERTICAL_ANCHOR.TOP,
+ 'middle': MSO_VERTICAL_ANCHOR.MIDDLE,
+ 'bottom': MSO_VERTICAL_ANCHOR.BOTTOM
+ }
+
+ # Apply vertical alignment to text frame
+ if 'vertical_alignment' in styling:
+ v_align = styling['vertical_alignment']
+ if v_align in vertical_alignment_map:
+ text_frame.vertical_anchor = vertical_alignment_map[v_align]
+
+ # Dynamic line spacing
+ line_spacing = styling.get('line_spacing', 1.2)
+ if line_spacing == 'dynamic':
+ content_length = len(text_frame.text)
+ if content_length > 300:
+ line_spacing = 1.4
+ elif content_length > 150:
+ line_spacing = 1.3
+ else:
+ line_spacing = 1.2
+
+ # Apply formatting to paragraphs and runs
+ for paragraph in text_frame.paragraphs:
+ # Set alignment
+ if 'alignment' in styling and styling['alignment'] in alignment_map:
+ paragraph.alignment = alignment_map[styling['alignment']]
+
+ # Set line spacing
+ paragraph.line_spacing = line_spacing
+
+ # Apply formatting to runs
+ for run in paragraph.runs:
+ font = run.font
+
+ # Font family and size
+ font.name = font_config['name']
+ font.size = Pt(font_size)
+
+ # Font weight and style
+ weight = font_config.get('weight', 'normal')
+ font.bold = styling.get('bold', weight in ['bold', 'semibold'])
+ font.italic = styling.get('italic', font_config.get('style') == 'italic')
+ font.underline = styling.get('underline', False)
+
+ # Color
+ if color:
+ font.color.rgb = RGBColor(*color)
+
+ # Apply text effects
+ text_effects = styling.get('text_effects', [])
+ if text_effects:
+ self.effects_manager.apply_text_effects(text_frame, text_effects, color_scheme)
+
+ def get_element_features(self, element: Dict) -> List[str]:
+ """Get list of enhanced features applied to an element."""
+ features = []
+ styling = element.get('styling', {})
+
+ if styling.get('font_size') == 'dynamic':
+ features.append('Dynamic text sizing')
+ if styling.get('auto_wrap'):
+ features.append('Automatic text wrapping')
+ if styling.get('text_effects'):
+ features.append('Text visual effects')
+ if styling.get('auto_fit'):
+ features.append('Auto-fit content')
+ if 'fill_gradient' in styling:
+ features.append('Gradient fills')
+ if styling.get('shadow') or styling.get('glow'):
+ features.append('Advanced visual effects')
+
+ return features
+
+
+# Global instance for enhanced features
+enhanced_template_manager = EnhancedTemplateManager()
+
+
+def get_enhanced_template_manager() -> EnhancedTemplateManager:
+ """Get the global enhanced template manager instance."""
+ return enhanced_template_manager
+
+
+def calculate_dynamic_font_size(text: str, container_width: float, container_height: float,
+ font_type: str = 'body') -> int:
+ """Calculate optimal font size for given text and container."""
+ return enhanced_template_manager.text_calculator.calculate_optimal_font_size(
+ text, container_width, container_height, font_type
+ )
+
+
+def wrap_text_automatically(text: str, container_width: float, font_size: int) -> str:
+ """Automatically wrap text to fit container width."""
+ return enhanced_template_manager.text_calculator.wrap_text_intelligently(
+ text, container_width, font_size
+ )
+
+
+def load_slide_templates(template_file_path: str = None) -> Dict:
+ """
+ Load slide layout templates from JSON file.
+
+ Args:
+ template_file_path: Path to template JSON file (defaults to slide_layout_templates.json)
+
+ Returns:
+ Dictionary containing all template definitions
+ """
+ if template_file_path is None:
+ # Default to the template file in the same directory as the script
+ current_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+ template_file_path = os.path.join(current_dir, 'slide_layout_templates.json')
+
+ try:
+ with open(template_file_path, 'r', encoding='utf-8') as f:
+ templates = json.load(f)
+ return templates
+ except FileNotFoundError:
+ raise FileNotFoundError(f"Template file not found: {template_file_path}")
+ except json.JSONDecodeError as e:
+ raise ValueError(f"Invalid JSON in template file: {str(e)}")
+
+
+def get_available_templates() -> List[Dict]:
+ """
+ Get a list of all available slide templates.
+
+ Returns:
+ List of template information dictionaries
+ """
+ try:
+ templates_data = load_slide_templates()
+ template_list = []
+
+ for template_id, template_info in templates_data.get('templates', {}).items():
+ template_list.append({
+ 'id': template_id,
+ 'name': template_info.get('name', template_id),
+ 'description': template_info.get('description', ''),
+ 'layout_type': template_info.get('layout_type', 'content'),
+ 'element_count': len(template_info.get('elements', []))
+ })
+
+ return template_list
+ except Exception as e:
+ return [{'error': f"Failed to load templates: {str(e)}"}]
+
+
+def get_color_from_scheme(templates_data: Dict, color_scheme: str, color_role: str) -> Tuple[int, int, int]:
+ """
+ Get RGB color values from a color scheme.
+
+ Args:
+ templates_data: Template data dictionary
+ color_scheme: Name of the color scheme
+ color_role: Role of the color (primary, secondary, accent1, etc.)
+
+ Returns:
+ RGB color tuple (r, g, b)
+ """
+ color_schemes = templates_data.get('color_schemes', {})
+
+ if color_scheme not in color_schemes:
+ color_scheme = 'modern_blue' # Default fallback
+
+ scheme = color_schemes[color_scheme]
+ return tuple(scheme.get(color_role, scheme.get('primary', [0, 120, 215])))
+
+
+def get_font_settings(templates_data: Dict, font_type: str, font_size: str) -> Dict:
+ """
+ Get font settings from typography configuration.
+
+ Args:
+ templates_data: Template data dictionary
+ font_type: Type of font (title, subtitle, body, caption)
+ font_size: Size category (large, medium, small)
+
+ Returns:
+ Dictionary with font settings
+ """
+ typography = templates_data.get('typography', {})
+
+ if font_type not in typography:
+ font_type = 'body' # Default fallback
+
+ font_config = typography[font_type]
+ size_key = f'font_size_{font_size}'
+
+ return {
+ 'name': font_config.get('font_name', 'Segoe UI'),
+ 'size': font_config.get(size_key, font_config.get('font_size_medium', 14)),
+ 'bold': font_config.get('bold', False)
+ }
+
+
+def apply_text_styling(text_frame, styling: Dict, templates_data: Dict, color_scheme: str) -> None:
+ """
+ Apply text styling based on template configuration.
+
+ Args:
+ text_frame: PowerPoint text frame object
+ styling: Styling configuration from template
+ templates_data: Template data dictionary
+ color_scheme: Selected color scheme
+ """
+ # Get font settings
+ font_type = styling.get('font_type', 'body')
+ font_size_category = styling.get('font_size', 'medium')
+ font_settings = get_font_settings(templates_data, font_type, font_size_category)
+
+ # Get color
+ color = None
+ if 'color_role' in styling:
+ color = get_color_from_scheme(templates_data, color_scheme, styling['color_role'])
+ elif 'color' in styling:
+ color = tuple(styling['color'])
+
+ # Apply alignment
+ alignment_map = {
+ 'left': PP_ALIGN.LEFT,
+ 'center': PP_ALIGN.CENTER,
+ 'right': PP_ALIGN.RIGHT,
+ 'justify': PP_ALIGN.JUSTIFY
+ }
+
+ # Apply formatting to all paragraphs and runs
+ for paragraph in text_frame.paragraphs:
+ if 'alignment' in styling and styling['alignment'] in alignment_map:
+ paragraph.alignment = alignment_map[styling['alignment']]
+
+ for run in paragraph.runs:
+ font = run.font
+ font.name = font_settings['name']
+ font.size = Pt(font_settings['size'])
+ font.bold = styling.get('bold', font_settings['bold'])
+ font.italic = styling.get('italic', False)
+ font.underline = styling.get('underline', False)
+
+ if color:
+ font.color.rgb = RGBColor(*color)
+
+
+def create_text_element(slide, element: Dict, templates_data: Dict, color_scheme: str) -> Any:
+ """
+ Create a text element on a slide based on template configuration.
+
+ Args:
+ slide: PowerPoint slide object
+ element: Element configuration from template
+ templates_data: Template data dictionary
+ color_scheme: Selected color scheme
+
+ Returns:
+ Created text box shape
+ """
+ pos = element['position']
+ textbox = slide.shapes.add_textbox(
+ Inches(pos['left']),
+ Inches(pos['top']),
+ Inches(pos['width']),
+ Inches(pos['height'])
+ )
+
+ # Set text content
+ textbox.text_frame.text = element.get('placeholder_text', '')
+
+ # Apply styling
+ styling = element.get('styling', {})
+ apply_text_styling(textbox.text_frame, styling, templates_data, color_scheme)
+
+ return textbox
+
+
+def create_image_element(slide, element: Dict, image_path: str = None) -> Any:
+ """
+ Create an image element on a slide based on template configuration.
+
+ Args:
+ slide: PowerPoint slide object
+ element: Element configuration from template
+ image_path: Optional path to image file
+
+ Returns:
+ Created image shape or None if no image provided
+ """
+ if not image_path:
+ # Create placeholder rectangle if no image provided
+ pos = element['position']
+ placeholder = slide.shapes.add_shape(
+ 1, # Rectangle shape
+ Inches(pos['left']),
+ Inches(pos['top']),
+ Inches(pos['width']),
+ Inches(pos['height'])
+ )
+
+ # Add placeholder text
+ if hasattr(placeholder, 'text_frame'):
+ placeholder.text_frame.text = element.get('placeholder_text', 'Image Placeholder')
+
+ return placeholder
+
+ try:
+ pos = element['position']
+ image_shape = content_utils.add_image(
+ slide,
+ image_path,
+ pos['left'],
+ pos['top'],
+ pos['width'],
+ pos['height']
+ )
+
+ # Apply styling if specified
+ styling = element.get('styling', {})
+ if styling.get('shadow'):
+ # Apply shadow effect (simplified)
+ pass
+
+ return image_shape
+ except Exception:
+ # Fallback to placeholder if image fails to load
+ return create_image_element(slide, element, None)
+
+
+def create_shape_element(slide, element: Dict, templates_data: Dict, color_scheme: str) -> Any:
+ """
+ Create a shape element on a slide based on template configuration.
+
+ Args:
+ slide: PowerPoint slide object
+ element: Element configuration from template
+ templates_data: Template data dictionary
+ color_scheme: Selected color scheme
+
+ Returns:
+ Created shape
+ """
+ pos = element['position']
+ shape_type = element.get('shape_type', 'rectangle')
+
+ try:
+ # Import the shape creation function from the main server
+ from ppt_mcp_server import add_shape_direct
+ shape = add_shape_direct(slide, shape_type, pos['left'], pos['top'], pos['width'], pos['height'])
+
+ # Apply styling
+ styling = element.get('styling', {})
+
+ # Fill color
+ if 'fill_color_role' in styling:
+ fill_color = get_color_from_scheme(templates_data, color_scheme, styling['fill_color_role'])
+ shape.fill.solid()
+ shape.fill.fore_color.rgb = RGBColor(*fill_color)
+ elif 'fill_color' in styling:
+ shape.fill.solid()
+ shape.fill.fore_color.rgb = RGBColor(*styling['fill_color'])
+
+ # Line color
+ if 'line_color_role' in styling:
+ line_color = get_color_from_scheme(templates_data, color_scheme, styling['line_color_role'])
+ shape.line.color.rgb = RGBColor(*line_color)
+ elif styling.get('no_border'):
+ shape.line.fill.background()
+
+ # Transparency
+ if 'transparency' in styling:
+ # Note: Transparency implementation would need additional XML manipulation
+ pass
+
+ return shape
+ except Exception as e:
+ # Create a simple rectangle as fallback
+ textbox = slide.shapes.add_textbox(
+ Inches(pos['left']),
+ Inches(pos['top']),
+ Inches(pos['width']),
+ Inches(pos['height'])
+ )
+ textbox.text_frame.text = f"Shape: {shape_type}"
+ return textbox
+
+
+def create_table_element(slide, element: Dict, templates_data: Dict, color_scheme: str) -> Any:
+ """
+ Create a table element on a slide based on template configuration.
+
+ Args:
+ slide: PowerPoint slide object
+ element: Element configuration from template
+ templates_data: Template data dictionary
+ color_scheme: Selected color scheme
+
+ Returns:
+ Created table shape
+ """
+ pos = element['position']
+ table_config = element.get('table_config', {})
+
+ rows = table_config.get('rows', 3)
+ cols = table_config.get('cols', 3)
+
+ # Create table
+ table_shape = content_utils.add_table(
+ slide, rows, cols, pos['left'], pos['top'], pos['width'], pos['height']
+ )
+ table = table_shape.table
+
+ # Populate with data if provided
+ data = table_config.get('data', [])
+ for r in range(min(rows, len(data))):
+ for c in range(min(cols, len(data[r]))):
+ table.cell(r, c).text = str(data[r][c])
+
+ # Apply styling
+ styling = element.get('styling', {})
+ header_row = table_config.get('header_row', True)
+
+ for r in range(rows):
+ for c in range(cols):
+ cell = table.cell(r, c)
+
+ if r == 0 and header_row:
+ # Header styling
+ if 'header_bg_color_role' in styling:
+ bg_color = get_color_from_scheme(templates_data, color_scheme, styling['header_bg_color_role'])
+ cell.fill.solid()
+ cell.fill.fore_color.rgb = RGBColor(*bg_color)
+
+ # Header text color
+ if 'header_text_color' in styling:
+ for paragraph in cell.text_frame.paragraphs:
+ for run in paragraph.runs:
+ run.font.color.rgb = RGBColor(*styling['header_text_color'])
+ run.font.bold = True
+ else:
+ # Body styling
+ if 'body_bg_color_role' in styling:
+ bg_color = get_color_from_scheme(templates_data, color_scheme, styling['body_bg_color_role'])
+ cell.fill.solid()
+ cell.fill.fore_color.rgb = RGBColor(*bg_color)
+
+ return table_shape
+
+
+def create_chart_element(slide, element: Dict, templates_data: Dict, color_scheme: str) -> Any:
+ """
+ Create a chart element on a slide based on template configuration.
+
+ Args:
+ slide: PowerPoint slide object
+ element: Element configuration from template
+ templates_data: Template data dictionary
+ color_scheme: Selected color scheme
+
+ Returns:
+ Created chart object
+ """
+ pos = element['position']
+ chart_config = element.get('chart_config', {})
+
+ chart_type = chart_config.get('type', 'column')
+ categories = chart_config.get('categories', ['A', 'B', 'C'])
+ series_data = chart_config.get('series', [{'name': 'Series 1', 'values': [1, 2, 3]}])
+
+ # Extract series names and values
+ series_names = [s['name'] for s in series_data]
+ series_values = [s['values'] for s in series_data]
+
+ try:
+ # Create chart
+ chart = content_utils.add_chart(
+ slide, chart_type, pos['left'], pos['top'], pos['width'], pos['height'],
+ categories, series_names, series_values
+ )
+
+ # Apply formatting
+ chart_title = chart_config.get('title')
+ if chart_title:
+ content_utils.format_chart(chart, title=chart_title)
+
+ return chart
+ except Exception as e:
+ # Create placeholder if chart creation fails
+ textbox = slide.shapes.add_textbox(
+ Inches(pos['left']),
+ Inches(pos['top']),
+ Inches(pos['width']),
+ Inches(pos['height'])
+ )
+ textbox.text_frame.text = f"Chart: {chart_type}\n{chart_title or 'Chart Placeholder'}"
+ return textbox
+
+
+def apply_slide_background(slide, background_config: Dict, templates_data: Dict, color_scheme: str) -> None:
+ """
+ Apply background styling to a slide based on template configuration.
+
+ Args:
+ slide: PowerPoint slide object
+ background_config: Background configuration from template
+ templates_data: Template data dictionary
+ color_scheme: Selected color scheme
+ """
+ if not background_config:
+ return
+
+ bg_type = background_config.get('type', 'solid')
+
+ if bg_type == 'professional_gradient':
+ style = background_config.get('style', 'subtle')
+ direction = background_config.get('direction', 'diagonal')
+ design_utils.create_professional_gradient_background(slide, color_scheme, style, direction)
+ elif bg_type == 'solid':
+ color_role = background_config.get('color_role', 'light')
+ # Note: Solid background would require XML manipulation for proper implementation
+ pass
+
+
+
+
+def apply_slide_template_basic(slide, template_id: str, color_scheme: str = 'modern_blue',
+ content_mapping: Dict = None, image_paths: Dict = None) -> Dict:
+ """
+ Apply a basic slide template to create a formatted slide.
+
+ Args:
+ slide: PowerPoint slide object
+ template_id: ID of the template to apply
+ color_scheme: Color scheme to use
+ content_mapping: Dictionary mapping element roles to content
+ image_paths: Dictionary mapping image element roles to file paths
+
+ Returns:
+ Dictionary with application results
+ """
+ try:
+ # Load templates
+ templates_data = load_slide_templates()
+
+ if template_id not in templates_data.get('templates', {}):
+ return {
+ 'success': False,
+ 'error': f"Template '{template_id}' not found"
+ }
+
+ template = templates_data['templates'][template_id]
+ elements_created = []
+
+ # Apply background if specified
+ background_config = template.get('background')
+ if background_config:
+ apply_slide_background(slide, background_config, templates_data, color_scheme)
+
+ # Create elements
+ for element in template.get('elements', []):
+ element_type = element.get('type')
+ element_role = element.get('role', '')
+
+ try:
+ # Override placeholder text with custom content if provided
+ if content_mapping and element_role in content_mapping:
+ element = element.copy() # Don't modify original template
+ element['placeholder_text'] = content_mapping[element_role]
+
+ created_element = None
+
+ if element_type == 'text':
+ created_element = create_text_element(slide, element, templates_data, color_scheme)
+ elif element_type == 'image':
+ image_path = image_paths.get(element_role) if image_paths else None
+ created_element = create_image_element(slide, element, image_path)
+ elif element_type == 'shape':
+ created_element = create_shape_element(slide, element, templates_data, color_scheme)
+ elif element_type == 'table':
+ created_element = create_table_element(slide, element, templates_data, color_scheme)
+ elif element_type == 'chart':
+ created_element = create_chart_element(slide, element, templates_data, color_scheme)
+
+ if created_element:
+ elements_created.append({
+ 'type': element_type,
+ 'role': element_role,
+ 'index': len(slide.shapes) - 1
+ })
+
+ except Exception as e:
+ # Continue with other elements if one fails
+ elements_created.append({
+ 'type': element_type,
+ 'role': element_role,
+ 'error': str(e)
+ })
+
+ return {
+ 'success': True,
+ 'template_id': template_id,
+ 'template_name': template.get('name', template_id),
+ 'color_scheme': color_scheme,
+ 'elements_created': elements_created,
+ 'total_elements': len(template.get('elements', []))
+ }
+
+ except Exception as e:
+ return {
+ 'success': False,
+ 'error': f"Failed to apply template: {str(e)}"
+ }
+
+
+def apply_slide_template(slide, template_id: str, color_scheme: str = 'modern_blue',
+ content_mapping: Dict = None, image_paths: Dict = None) -> Dict:
+ """
+ Apply a slide template with all enhanced features.
+
+ Args:
+ slide: PowerPoint slide object
+ template_id: ID of the template to apply
+ color_scheme: Color scheme to use
+ content_mapping: Dictionary mapping element roles to content
+ image_paths: Dictionary mapping image element roles to file paths
+
+ Returns:
+ Dictionary with application results
+ """
+ # All templates now have enhanced features built-in
+ return enhanced_template_manager.apply_enhanced_slide_template(
+ slide, template_id, color_scheme, content_mapping, image_paths
+ )
+
+
+def create_presentation_from_template_sequence(presentation: Presentation, template_sequence: List[Dict],
+ color_scheme: str = 'modern_blue') -> Dict:
+ """
+ Create a complete presentation from a sequence of templates.
+
+ Args:
+ presentation: PowerPoint presentation object
+ template_sequence: List of template configurations
+ color_scheme: Color scheme to apply to all slides
+
+ Returns:
+ Dictionary with creation results
+ """
+ results = {
+ 'success': True,
+ 'slides_created': [],
+ 'total_slides': len(template_sequence),
+ 'color_scheme': color_scheme
+ }
+
+ for i, slide_config in enumerate(template_sequence):
+ try:
+ # Get template configuration
+ template_id = slide_config.get('template_id')
+ content_mapping = slide_config.get('content', {})
+ image_paths = slide_config.get('images', {})
+
+ if not template_id:
+ results['slides_created'].append({
+ 'slide_index': i,
+ 'success': False,
+ 'error': 'No template_id specified'
+ })
+ continue
+
+ # Add new slide (using layout 1 as default content layout)
+ layout = presentation.slide_layouts[1]
+ slide = presentation.slides.add_slide(layout)
+
+ # Apply template
+ template_result = apply_slide_template(
+ slide, template_id, color_scheme, content_mapping, image_paths
+ )
+
+ template_result['slide_index'] = i
+ results['slides_created'].append(template_result)
+
+ if not template_result['success']:
+ results['success'] = False
+
+ except Exception as e:
+ results['slides_created'].append({
+ 'slide_index': i,
+ 'success': False,
+ 'error': f"Failed to create slide {i}: {str(e)}"
+ })
+ results['success'] = False
+
+ return results
+
+
+def get_template_usage_examples() -> Dict:
+ """
+ Get examples of how to use different templates.
+
+ Returns:
+ Dictionary with usage examples
+ """
+ return {
+ "single_slide_example": {
+ "description": "Apply a single template to a slide",
+ "code": {
+ "template_id": "text_with_image",
+ "color_scheme": "modern_blue",
+ "content_mapping": {
+ "title": "Our Solution",
+ "content": "• Increased efficiency by 40%\n• Reduced costs significantly\n• Improved user satisfaction",
+ },
+ "image_paths": {
+ "supporting": "/path/to/solution_image.jpg"
+ }
+ }
+ },
+ "presentation_sequence_example": {
+ "description": "Create a complete presentation from templates",
+ "code": [
+ {
+ "template_id": "title_slide",
+ "content": {
+ "title": "2024 Business Review",
+ "subtitle": "Annual Performance Report",
+ "author": "John Smith, CEO"
+ }
+ },
+ {
+ "template_id": "agenda_slide",
+ "content": {
+ "agenda_items": "1. Executive Summary\n\n2. Financial Performance\n\n3. Market Analysis\n\n4. Future Strategy"
+ }
+ },
+ {
+ "template_id": "key_metrics_dashboard",
+ "content": {
+ "metric_1_value": "92%",
+ "metric_2_value": "$3.2M",
+ "metric_3_value": "340",
+ "metric_4_value": "18%"
+ }
+ },
+ {
+ "template_id": "thank_you_slide",
+ "content": {
+ "contact": "Questions?\njohn.smith@company.com\n(555) 123-4567"
+ }
+ }
+ ]
+ },
+ "available_templates": [
+ "title_slide", "text_with_image", "two_column_text", "two_column_text_images",
+ "three_column_layout", "agenda_slide", "chapter_intro", "thank_you_slide",
+ "timeline_slide", "data_table_slide", "chart_comparison", "full_image_slide",
+ "process_flow", "quote_testimonial", "key_metrics_dashboard",
+ "before_after_comparison", "team_introduction"
+ ],
+ "color_schemes": [
+ "modern_blue", "corporate_gray", "elegant_green", "warm_red"
+ ]
+ }
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/utils/validation_utils.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/utils/validation_utils.py
new file mode 100644
index 00000000..2ccaa175
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/utils/validation_utils.py
@@ -0,0 +1,323 @@
+"""
+Validation utilities for PowerPoint MCP Server.
+Functions for validating and fixing slide content, text fit, and layouts.
+"""
+from typing import Dict, List, Optional, Any
+
+
+def validate_text_fit(shape, text_content: str = None, font_size: int = 12) -> Dict:
+ """
+ Validate if text content will fit in a shape container.
+
+ Args:
+ shape: The shape containing the text
+ text_content: The text to validate (if None, uses existing text)
+ font_size: The font size to check
+
+ Returns:
+ Dictionary with validation results and suggestions
+ """
+ result = {
+ 'fits': True,
+ 'estimated_overflow': False,
+ 'suggested_font_size': font_size,
+ 'suggested_dimensions': None,
+ 'warnings': [],
+ 'needs_optimization': False
+ }
+
+ try:
+ # Use existing text if not provided
+ if text_content is None and hasattr(shape, 'text_frame'):
+ text_content = shape.text_frame.text
+
+ if not text_content:
+ return result
+
+ # Basic heuristic: estimate if text will overflow
+ if hasattr(shape, 'width') and hasattr(shape, 'height'):
+ # Rough estimation: average character width is about 0.6 * font_size
+ avg_char_width = font_size * 0.6
+ estimated_width = len(text_content) * avg_char_width
+
+ # Convert shape dimensions to points (assuming they're in EMU)
+ shape_width_pt = shape.width / 12700 # EMU to points conversion
+ shape_height_pt = shape.height / 12700
+
+ if estimated_width > shape_width_pt:
+ result['fits'] = False
+ result['estimated_overflow'] = True
+ result['needs_optimization'] = True
+
+ # Suggest smaller font size
+ suggested_size = int((shape_width_pt / len(text_content)) * 0.8)
+ result['suggested_font_size'] = max(suggested_size, 8)
+
+ # Suggest larger dimensions
+ result['suggested_dimensions'] = {
+ 'width': estimated_width * 1.2,
+ 'height': shape_height_pt
+ }
+
+ result['warnings'].append(
+ f"Text may overflow. Consider font size {result['suggested_font_size']} "
+ f"or increase width to {result['suggested_dimensions']['width']:.1f} points"
+ )
+
+ # Check for very long lines that might cause formatting issues
+ lines = text_content.split('\n')
+ max_line_length = max(len(line) for line in lines) if lines else 0
+
+ if max_line_length > 100: # Arbitrary threshold
+ result['warnings'].append("Very long lines detected. Consider adding line breaks.")
+ result['needs_optimization'] = True
+
+ return result
+
+ except Exception as e:
+ result['fits'] = False
+ result['error'] = str(e)
+ return result
+
+
+def validate_and_fix_slide(slide, auto_fix: bool = True, min_font_size: int = 8,
+ max_font_size: int = 72) -> Dict:
+ """
+ Comprehensively validate and automatically fix slide content issues.
+
+ Args:
+ slide: The slide object to validate
+ auto_fix: Whether to automatically apply fixes
+ min_font_size: Minimum allowed font size
+ max_font_size: Maximum allowed font size
+
+ Returns:
+ Dictionary with validation results and applied fixes
+ """
+ result = {
+ 'validation_passed': True,
+ 'issues_found': [],
+ 'fixes_applied': [],
+ 'warnings': [],
+ 'shapes_processed': 0,
+ 'text_shapes_optimized': 0
+ }
+
+ try:
+ shapes_with_text = []
+
+ # Find all shapes with text content
+ for i, shape in enumerate(slide.shapes):
+ result['shapes_processed'] += 1
+
+ if hasattr(shape, 'text_frame') and shape.text_frame.text.strip():
+ shapes_with_text.append((i, shape))
+
+ # Validate each text shape
+ for shape_index, shape in shapes_with_text:
+ shape_name = f"Shape {shape_index}"
+
+ # Validate text fit
+ text_validation = validate_text_fit(shape, font_size=12)
+
+ if not text_validation['fits'] or text_validation['needs_optimization']:
+ issue = f"{shape_name}: Text may not fit properly"
+ result['issues_found'].append(issue)
+ result['validation_passed'] = False
+
+ if auto_fix and text_validation['suggested_font_size']:
+ try:
+ # Apply suggested font size
+ suggested_size = max(min_font_size,
+ min(text_validation['suggested_font_size'], max_font_size))
+
+ # Apply font size to all runs in the text frame
+ for paragraph in shape.text_frame.paragraphs:
+ for run in paragraph.runs:
+ if hasattr(run, 'font'):
+ run.font.size = suggested_size * 12700 # Convert to EMU
+
+ fix = f"{shape_name}: Adjusted font size to {suggested_size}pt"
+ result['fixes_applied'].append(fix)
+ result['text_shapes_optimized'] += 1
+
+ except Exception as e:
+ warning = f"{shape_name}: Could not auto-fix font size: {str(e)}"
+ result['warnings'].append(warning)
+
+ # Check for other potential issues
+ if len(shape.text_frame.text) > 500: # Very long text
+ result['warnings'].append(f"{shape_name}: Contains very long text (>500 chars)")
+
+ # Check for empty paragraphs
+ empty_paragraphs = sum(1 for p in shape.text_frame.paragraphs if not p.text.strip())
+ if empty_paragraphs > 2:
+ result['warnings'].append(f"{shape_name}: Contains {empty_paragraphs} empty paragraphs")
+
+ # Check slide-level issues
+ if len(slide.shapes) > 20:
+ result['warnings'].append("Slide contains many shapes (>20), may affect performance")
+
+ # Summary
+ if result['validation_passed']:
+ result['summary'] = "Slide validation passed successfully"
+ else:
+ result['summary'] = f"Found {len(result['issues_found'])} issues"
+ if auto_fix:
+ result['summary'] += f", applied {len(result['fixes_applied'])} fixes"
+
+ return result
+
+ except Exception as e:
+ result['validation_passed'] = False
+ result['error'] = str(e)
+ return result
+
+
+def validate_slide_layout(slide) -> Dict:
+ """
+ Validate slide layout for common issues.
+
+ Args:
+ slide: The slide object
+
+ Returns:
+ Dictionary with layout validation results
+ """
+ result = {
+ 'layout_valid': True,
+ 'issues': [],
+ 'suggestions': [],
+ 'shape_count': len(slide.shapes),
+ 'overlapping_shapes': []
+ }
+
+ try:
+ shapes = list(slide.shapes)
+
+ # Check for overlapping shapes
+ for i, shape1 in enumerate(shapes):
+ for j, shape2 in enumerate(shapes[i+1:], i+1):
+ if shapes_overlap(shape1, shape2):
+ result['overlapping_shapes'].append({
+ 'shape1_index': i,
+ 'shape2_index': j,
+ 'shape1_name': getattr(shape1, 'name', f'Shape {i}'),
+ 'shape2_name': getattr(shape2, 'name', f'Shape {j}')
+ })
+
+ if result['overlapping_shapes']:
+ result['layout_valid'] = False
+ result['issues'].append(f"Found {len(result['overlapping_shapes'])} overlapping shapes")
+ result['suggestions'].append("Consider repositioning overlapping shapes")
+
+ # Check for shapes outside slide boundaries
+ slide_width = 10 * 914400 # Standard slide width in EMU
+ slide_height = 7.5 * 914400 # Standard slide height in EMU
+
+ shapes_outside = []
+ for i, shape in enumerate(shapes):
+ if (shape.left < 0 or shape.top < 0 or
+ shape.left + shape.width > slide_width or
+ shape.top + shape.height > slide_height):
+ shapes_outside.append(i)
+
+ if shapes_outside:
+ result['layout_valid'] = False
+ result['issues'].append(f"Found {len(shapes_outside)} shapes outside slide boundaries")
+ result['suggestions'].append("Reposition shapes to fit within slide boundaries")
+
+ # Check shape spacing
+ if len(shapes) > 1:
+ min_spacing = check_minimum_spacing(shapes)
+ if min_spacing < 0.1 * 914400: # Less than 0.1 inch spacing
+ result['suggestions'].append("Consider increasing spacing between shapes")
+
+ return result
+
+ except Exception as e:
+ result['layout_valid'] = False
+ result['error'] = str(e)
+ return result
+
+
+def shapes_overlap(shape1, shape2) -> bool:
+ """
+ Check if two shapes overlap.
+
+ Args:
+ shape1: First shape
+ shape2: Second shape
+
+ Returns:
+ True if shapes overlap, False otherwise
+ """
+ try:
+ # Get boundaries
+ left1, top1 = shape1.left, shape1.top
+ right1, bottom1 = left1 + shape1.width, top1 + shape1.height
+
+ left2, top2 = shape2.left, shape2.top
+ right2, bottom2 = left2 + shape2.width, top2 + shape2.height
+
+ # Check for overlap
+ return not (right1 <= left2 or right2 <= left1 or bottom1 <= top2 or bottom2 <= top1)
+ except:
+ return False
+
+
+def check_minimum_spacing(shapes: List) -> float:
+ """
+ Check minimum spacing between shapes.
+
+ Args:
+ shapes: List of shapes
+
+ Returns:
+ Minimum spacing found between shapes (in EMU)
+ """
+ min_spacing = float('inf')
+
+ try:
+ for i, shape1 in enumerate(shapes):
+ for shape2 in shapes[i+1:]:
+ # Calculate distance between shape edges
+ distance = calculate_shape_distance(shape1, shape2)
+ min_spacing = min(min_spacing, distance)
+
+ return min_spacing if min_spacing != float('inf') else 0
+ except:
+ return 0
+
+
+def calculate_shape_distance(shape1, shape2) -> float:
+ """
+ Calculate distance between two shapes.
+
+ Args:
+ shape1: First shape
+ shape2: Second shape
+
+ Returns:
+ Distance between shape edges (in EMU)
+ """
+ try:
+ # Get centers
+ center1_x = shape1.left + shape1.width / 2
+ center1_y = shape1.top + shape1.height / 2
+
+ center2_x = shape2.left + shape2.width / 2
+ center2_y = shape2.top + shape2.height / 2
+
+ # Calculate center-to-center distance
+ dx = abs(center2_x - center1_x)
+ dy = abs(center2_y - center1_y)
+
+ # Subtract half-widths and half-heights to get edge distance
+ edge_distance_x = max(0, dx - (shape1.width + shape2.width) / 2)
+ edge_distance_y = max(0, dy - (shape1.height + shape2.height) / 2)
+
+ # Return minimum edge distance
+ return min(edge_distance_x, edge_distance_y)
+ except:
+ return 0
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/uv.lock b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/uv.lock
new file mode 100644
index 00000000..23e6c2f6
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-PowerPoint-MCP-Server/uv.lock
@@ -0,0 +1,1137 @@
+version = 1
+revision = 2
+requires-python = ">=3.10"
+
+[[package]]
+name = "annotated-doc"
+version = "0.0.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" },
+]
+
+[[package]]
+name = "annotated-types"
+version = "0.7.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
+]
+
+[[package]]
+name = "anyio"
+version = "4.12.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
+ { name = "idna" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" },
+]
+
+[[package]]
+name = "attrs"
+version = "25.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" },
+]
+
+[[package]]
+name = "certifi"
+version = "2026.2.25"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" },
+]
+
+[[package]]
+name = "cffi"
+version = "2.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pycparser", marker = "implementation_name != 'PyPy'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" },
+ { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" },
+ { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" },
+ { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" },
+ { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" },
+ { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" },
+ { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" },
+ { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" },
+ { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" },
+ { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" },
+ { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" },
+ { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" },
+ { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" },
+ { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" },
+ { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" },
+ { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" },
+ { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" },
+ { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" },
+ { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" },
+ { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" },
+ { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" },
+ { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" },
+ { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" },
+ { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" },
+ { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
+]
+
+[[package]]
+name = "click"
+version = "8.3.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" },
+]
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
+]
+
+[[package]]
+name = "cryptography"
+version = "46.0.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/60/04/ee2a9e8542e4fa2773b81771ff8349ff19cdd56b7258a0cc442639052edb/cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d", size = 750064, upload-time = "2026-02-10T19:18:38.255Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f7/81/b0bb27f2ba931a65409c6b8a8b358a7f03c0e46eceacddff55f7c84b1f3b/cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad", size = 7176289, upload-time = "2026-02-10T19:17:08.274Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/9e/6b4397a3e3d15123de3b1806ef342522393d50736c13b20ec4c9ea6693a6/cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b", size = 4275637, upload-time = "2026-02-10T19:17:10.53Z" },
+ { url = "https://files.pythonhosted.org/packages/63/e7/471ab61099a3920b0c77852ea3f0ea611c9702f651600397ac567848b897/cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b", size = 4424742, upload-time = "2026-02-10T19:17:12.388Z" },
+ { url = "https://files.pythonhosted.org/packages/37/53/a18500f270342d66bf7e4d9f091114e31e5ee9e7375a5aba2e85a91e0044/cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263", size = 4277528, upload-time = "2026-02-10T19:17:13.853Z" },
+ { url = "https://files.pythonhosted.org/packages/22/29/c2e812ebc38c57b40e7c583895e73c8c5adb4d1e4a0cc4c5a4fdab2b1acc/cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d", size = 4947993, upload-time = "2026-02-10T19:17:15.618Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/e7/237155ae19a9023de7e30ec64e5d99a9431a567407ac21170a046d22a5a3/cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed", size = 4456855, upload-time = "2026-02-10T19:17:17.221Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/87/fc628a7ad85b81206738abbd213b07702bcbdada1dd43f72236ef3cffbb5/cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2", size = 3984635, upload-time = "2026-02-10T19:17:18.792Z" },
+ { url = "https://files.pythonhosted.org/packages/84/29/65b55622bde135aedf4565dc509d99b560ee4095e56989e815f8fd2aa910/cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2", size = 4277038, upload-time = "2026-02-10T19:17:20.256Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/36/45e76c68d7311432741faf1fbf7fac8a196a0a735ca21f504c75d37e2558/cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0", size = 4912181, upload-time = "2026-02-10T19:17:21.825Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/1a/c1ba8fead184d6e3d5afcf03d569acac5ad063f3ac9fb7258af158f7e378/cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731", size = 4456482, upload-time = "2026-02-10T19:17:25.133Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/e5/3fb22e37f66827ced3b902cf895e6a6bc1d095b5b26be26bd13c441fdf19/cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82", size = 4405497, upload-time = "2026-02-10T19:17:26.66Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/df/9d58bb32b1121a8a2f27383fabae4d63080c7ca60b9b5c88be742be04ee7/cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1", size = 4667819, upload-time = "2026-02-10T19:17:28.569Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/ed/325d2a490c5e94038cdb0117da9397ece1f11201f425c4e9c57fe5b9f08b/cryptography-46.0.5-cp311-abi3-win32.whl", hash = "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48", size = 3028230, upload-time = "2026-02-10T19:17:30.518Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/5a/ac0f49e48063ab4255d9e3b79f5def51697fce1a95ea1370f03dc9db76f6/cryptography-46.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4", size = 3480909, upload-time = "2026-02-10T19:17:32.083Z" },
+ { url = "https://files.pythonhosted.org/packages/00/13/3d278bfa7a15a96b9dc22db5a12ad1e48a9eb3d40e1827ef66a5df75d0d0/cryptography-46.0.5-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2", size = 7119287, upload-time = "2026-02-10T19:17:33.801Z" },
+ { url = "https://files.pythonhosted.org/packages/67/c8/581a6702e14f0898a0848105cbefd20c058099e2c2d22ef4e476dfec75d7/cryptography-46.0.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678", size = 4265728, upload-time = "2026-02-10T19:17:35.569Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/4a/ba1a65ce8fc65435e5a849558379896c957870dd64fecea97b1ad5f46a37/cryptography-46.0.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87", size = 4408287, upload-time = "2026-02-10T19:17:36.938Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/67/8ffdbf7b65ed1ac224d1c2df3943553766914a8ca718747ee3871da6107e/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee", size = 4270291, upload-time = "2026-02-10T19:17:38.748Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/e5/f52377ee93bc2f2bba55a41a886fd208c15276ffbd2569f2ddc89d50e2c5/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981", size = 4927539, upload-time = "2026-02-10T19:17:40.241Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/02/cfe39181b02419bbbbcf3abdd16c1c5c8541f03ca8bda240debc467d5a12/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9", size = 4442199, upload-time = "2026-02-10T19:17:41.789Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/96/2fcaeb4873e536cf71421a388a6c11b5bc846e986b2b069c79363dc1648e/cryptography-46.0.5-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648", size = 3960131, upload-time = "2026-02-10T19:17:43.379Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/d2/b27631f401ddd644e94c5cf33c9a4069f72011821cf3dc7309546b0642a0/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4", size = 4270072, upload-time = "2026-02-10T19:17:45.481Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/a7/60d32b0370dae0b4ebe55ffa10e8599a2a59935b5ece1b9f06edb73abdeb/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0", size = 4892170, upload-time = "2026-02-10T19:17:46.997Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/b9/cf73ddf8ef1164330eb0b199a589103c363afa0cf794218c24d524a58eab/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663", size = 4441741, upload-time = "2026-02-10T19:17:48.661Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/eb/eee00b28c84c726fe8fa0158c65afe312d9c3b78d9d01daf700f1f6e37ff/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826", size = 4396728, upload-time = "2026-02-10T19:17:50.058Z" },
+ { url = "https://files.pythonhosted.org/packages/65/f4/6bc1a9ed5aef7145045114b75b77c2a8261b4d38717bd8dea111a63c3442/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d", size = 4652001, upload-time = "2026-02-10T19:17:51.54Z" },
+ { url = "https://files.pythonhosted.org/packages/86/ef/5d00ef966ddd71ac2e6951d278884a84a40ffbd88948ef0e294b214ae9e4/cryptography-46.0.5-cp314-cp314t-win32.whl", hash = "sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a", size = 3003637, upload-time = "2026-02-10T19:17:52.997Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/57/f3f4160123da6d098db78350fdfd9705057aad21de7388eacb2401dceab9/cryptography-46.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4", size = 3469487, upload-time = "2026-02-10T19:17:54.549Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/fa/a66aa722105ad6a458bebd64086ca2b72cdd361fed31763d20390f6f1389/cryptography-46.0.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31", size = 7170514, upload-time = "2026-02-10T19:17:56.267Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/04/c85bdeab78c8bc77b701bf0d9bdcf514c044e18a46dcff330df5448631b0/cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18", size = 4275349, upload-time = "2026-02-10T19:17:58.419Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/32/9b87132a2f91ee7f5223b091dc963055503e9b442c98fc0b8a5ca765fab0/cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235", size = 4420667, upload-time = "2026-02-10T19:18:00.619Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/a6/a7cb7010bec4b7c5692ca6f024150371b295ee1c108bdc1c400e4c44562b/cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a", size = 4276980, upload-time = "2026-02-10T19:18:02.379Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/7c/c4f45e0eeff9b91e3f12dbd0e165fcf2a38847288fcfd889deea99fb7b6d/cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76", size = 4939143, upload-time = "2026-02-10T19:18:03.964Z" },
+ { url = "https://files.pythonhosted.org/packages/37/19/e1b8f964a834eddb44fa1b9a9976f4e414cbb7aa62809b6760c8803d22d1/cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614", size = 4453674, upload-time = "2026-02-10T19:18:05.588Z" },
+ { url = "https://files.pythonhosted.org/packages/db/ed/db15d3956f65264ca204625597c410d420e26530c4e2943e05a0d2f24d51/cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229", size = 3978801, upload-time = "2026-02-10T19:18:07.167Z" },
+ { url = "https://files.pythonhosted.org/packages/41/e2/df40a31d82df0a70a0daf69791f91dbb70e47644c58581d654879b382d11/cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1", size = 4276755, upload-time = "2026-02-10T19:18:09.813Z" },
+ { url = "https://files.pythonhosted.org/packages/33/45/726809d1176959f4a896b86907b98ff4391a8aa29c0aaaf9450a8a10630e/cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d", size = 4901539, upload-time = "2026-02-10T19:18:11.263Z" },
+ { url = "https://files.pythonhosted.org/packages/99/0f/a3076874e9c88ecb2ecc31382f6e7c21b428ede6f55aafa1aa272613e3cd/cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c", size = 4452794, upload-time = "2026-02-10T19:18:12.914Z" },
+ { url = "https://files.pythonhosted.org/packages/02/ef/ffeb542d3683d24194a38f66ca17c0a4b8bf10631feef44a7ef64e631b1a/cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4", size = 4404160, upload-time = "2026-02-10T19:18:14.375Z" },
+ { url = "https://files.pythonhosted.org/packages/96/93/682d2b43c1d5f1406ed048f377c0fc9fc8f7b0447a478d5c65ab3d3a66eb/cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9", size = 4667123, upload-time = "2026-02-10T19:18:15.886Z" },
+ { url = "https://files.pythonhosted.org/packages/45/2d/9c5f2926cb5300a8eefc3f4f0b3f3df39db7f7ce40c8365444c49363cbda/cryptography-46.0.5-cp38-abi3-win32.whl", hash = "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72", size = 3010220, upload-time = "2026-02-10T19:18:17.361Z" },
+ { url = "https://files.pythonhosted.org/packages/48/ef/0c2f4a8e31018a986949d34a01115dd057bf536905dca38897bacd21fac3/cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595", size = 3467050, upload-time = "2026-02-10T19:18:18.899Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/dd/2d9fdb07cebdf3d51179730afb7d5e576153c6744c3ff8fded23030c204e/cryptography-46.0.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:3b4995dc971c9fb83c25aa44cf45f02ba86f71ee600d81091c2f0cbae116b06c", size = 3476964, upload-time = "2026-02-10T19:18:20.687Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/6f/6cc6cc9955caa6eaf83660b0da2b077c7fe8ff9950a3c5e45d605038d439/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bc84e875994c3b445871ea7181d424588171efec3e185dced958dad9e001950a", size = 4218321, upload-time = "2026-02-10T19:18:22.349Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/5d/c4da701939eeee699566a6c1367427ab91a8b7088cc2328c09dbee940415/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:2ae6971afd6246710480e3f15824ed3029a60fc16991db250034efd0b9fb4356", size = 4381786, upload-time = "2026-02-10T19:18:24.529Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/97/a538654732974a94ff96c1db621fa464f455c02d4bb7d2652f4edc21d600/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d861ee9e76ace6cf36a6a89b959ec08e7bc2493ee39d07ffe5acb23ef46d27da", size = 4217990, upload-time = "2026-02-10T19:18:25.957Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/11/7e500d2dd3ba891197b9efd2da5454b74336d64a7cc419aa7327ab74e5f6/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:2b7a67c9cd56372f3249b39699f2ad479f6991e62ea15800973b956f4b73e257", size = 4381252, upload-time = "2026-02-10T19:18:27.496Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/58/6b3d24e6b9bc474a2dcdee65dfd1f008867015408a271562e4b690561a4d/cryptography-46.0.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8456928655f856c6e1533ff59d5be76578a7157224dbd9ce6872f25055ab9ab7", size = 3407605, upload-time = "2026-02-10T19:18:29.233Z" },
+]
+
+[[package]]
+name = "exceptiongroup"
+version = "1.3.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
+]
+
+[[package]]
+name = "fonttools"
+version = "4.61.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ec/ca/cf17b88a8df95691275a3d77dc0a5ad9907f328ae53acbe6795da1b2f5ed/fonttools-4.61.1.tar.gz", hash = "sha256:6675329885c44657f826ef01d9e4fb33b9158e9d93c537d84ad8399539bc6f69", size = 3565756, upload-time = "2025-12-12T17:31:24.246Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5b/94/8a28707adb00bed1bf22dac16ccafe60faf2ade353dcb32c3617ee917307/fonttools-4.61.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c7db70d57e5e1089a274cbb2b1fd635c9a24de809a231b154965d415d6c6d24", size = 2854799, upload-time = "2025-12-12T17:29:27.5Z" },
+ { url = "https://files.pythonhosted.org/packages/94/93/c2e682faaa5ee92034818d8f8a8145ae73eb83619600495dcf8503fa7771/fonttools-4.61.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5fe9fd43882620017add5eabb781ebfbc6998ee49b35bd7f8f79af1f9f99a958", size = 2403032, upload-time = "2025-12-12T17:29:30.115Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/62/1748f7e7e1ee41aa52279fd2e3a6d0733dc42a673b16932bad8e5d0c8b28/fonttools-4.61.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8db08051fc9e7d8bc622f2112511b8107d8f27cd89e2f64ec45e9825e8288da", size = 4897863, upload-time = "2025-12-12T17:29:32.535Z" },
+ { url = "https://files.pythonhosted.org/packages/69/69/4ca02ee367d2c98edcaeb83fc278d20972502ee071214ad9d8ca85e06080/fonttools-4.61.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a76d4cb80f41ba94a6691264be76435e5f72f2cb3cab0b092a6212855f71c2f6", size = 4859076, upload-time = "2025-12-12T17:29:34.907Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/f5/660f9e3cefa078861a7f099107c6d203b568a6227eef163dd173bfc56bdc/fonttools-4.61.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a13fc8aeb24bad755eea8f7f9d409438eb94e82cf86b08fe77a03fbc8f6a96b1", size = 4875623, upload-time = "2025-12-12T17:29:37.33Z" },
+ { url = "https://files.pythonhosted.org/packages/63/d1/9d7c5091d2276ed47795c131c1bf9316c3c1ab2789c22e2f59e0572ccd38/fonttools-4.61.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b846a1fcf8beadeb9ea4f44ec5bdde393e2f1569e17d700bfc49cd69bde75881", size = 4993327, upload-time = "2025-12-12T17:29:39.781Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/2d/28def73837885ae32260d07660a052b99f0aa00454867d33745dfe49dbf0/fonttools-4.61.1-cp310-cp310-win32.whl", hash = "sha256:78a7d3ab09dc47ac1a363a493e6112d8cabed7ba7caad5f54dbe2f08676d1b47", size = 1502180, upload-time = "2025-12-12T17:29:42.217Z" },
+ { url = "https://files.pythonhosted.org/packages/63/fa/bfdc98abb4dd2bd491033e85e3ba69a2313c850e759a6daa014bc9433b0f/fonttools-4.61.1-cp310-cp310-win_amd64.whl", hash = "sha256:eff1ac3cc66c2ac7cda1e64b4e2f3ffef474b7335f92fc3833fc632d595fcee6", size = 1550654, upload-time = "2025-12-12T17:29:44.564Z" },
+ { url = "https://files.pythonhosted.org/packages/69/12/bf9f4eaa2fad039356cc627587e30ed008c03f1cebd3034376b5ee8d1d44/fonttools-4.61.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c6604b735bb12fef8e0efd5578c9fb5d3d8532d5001ea13a19cddf295673ee09", size = 2852213, upload-time = "2025-12-12T17:29:46.675Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/49/4138d1acb6261499bedde1c07f8c2605d1d8f9d77a151e5507fd3ef084b6/fonttools-4.61.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5ce02f38a754f207f2f06557523cd39a06438ba3aafc0639c477ac409fc64e37", size = 2401689, upload-time = "2025-12-12T17:29:48.769Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/fe/e6ce0fe20a40e03aef906af60aa87668696f9e4802fa283627d0b5ed777f/fonttools-4.61.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77efb033d8d7ff233385f30c62c7c79271c8885d5c9657d967ede124671bbdfb", size = 5058809, upload-time = "2025-12-12T17:29:51.701Z" },
+ { url = "https://files.pythonhosted.org/packages/79/61/1ca198af22f7dd22c17ab86e9024ed3c06299cfdb08170640e9996d501a0/fonttools-4.61.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:75c1a6dfac6abd407634420c93864a1e274ebc1c7531346d9254c0d8f6ca00f9", size = 5036039, upload-time = "2025-12-12T17:29:53.659Z" },
+ { url = "https://files.pythonhosted.org/packages/99/cc/fa1801e408586b5fce4da9f5455af8d770f4fc57391cd5da7256bb364d38/fonttools-4.61.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0de30bfe7745c0d1ffa2b0b7048fb7123ad0d71107e10ee090fa0b16b9452e87", size = 5034714, upload-time = "2025-12-12T17:29:55.592Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/aa/b7aeafe65adb1b0a925f8f25725e09f078c635bc22754f3fecb7456955b0/fonttools-4.61.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:58b0ee0ab5b1fc9921eccfe11d1435added19d6494dde14e323f25ad2bc30c56", size = 5158648, upload-time = "2025-12-12T17:29:57.861Z" },
+ { url = "https://files.pythonhosted.org/packages/99/f9/08ea7a38663328881384c6e7777bbefc46fd7d282adfd87a7d2b84ec9d50/fonttools-4.61.1-cp311-cp311-win32.whl", hash = "sha256:f79b168428351d11e10c5aeb61a74e1851ec221081299f4cf56036a95431c43a", size = 2280681, upload-time = "2025-12-12T17:29:59.943Z" },
+ { url = "https://files.pythonhosted.org/packages/07/ad/37dd1ae5fa6e01612a1fbb954f0927681f282925a86e86198ccd7b15d515/fonttools-4.61.1-cp311-cp311-win_amd64.whl", hash = "sha256:fe2efccb324948a11dd09d22136fe2ac8a97d6c1347cf0b58a911dcd529f66b7", size = 2331951, upload-time = "2025-12-12T17:30:02.254Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/16/7decaa24a1bd3a70c607b2e29f0adc6159f36a7e40eaba59846414765fd4/fonttools-4.61.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f3cb4a569029b9f291f88aafc927dd53683757e640081ca8c412781ea144565e", size = 2851593, upload-time = "2025-12-12T17:30:04.225Z" },
+ { url = "https://files.pythonhosted.org/packages/94/98/3c4cb97c64713a8cf499b3245c3bf9a2b8fd16a3e375feff2aed78f96259/fonttools-4.61.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41a7170d042e8c0024703ed13b71893519a1a6d6e18e933e3ec7507a2c26a4b2", size = 2400231, upload-time = "2025-12-12T17:30:06.47Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/37/82dbef0f6342eb01f54bca073ac1498433d6ce71e50c3c3282b655733b31/fonttools-4.61.1-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10d88e55330e092940584774ee5e8a6971b01fc2f4d3466a1d6c158230880796", size = 4954103, upload-time = "2025-12-12T17:30:08.432Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/44/f3aeac0fa98e7ad527f479e161aca6c3a1e47bb6996b053d45226fe37bf2/fonttools-4.61.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:15acc09befd16a0fb8a8f62bc147e1a82817542d72184acca9ce6e0aeda9fa6d", size = 5004295, upload-time = "2025-12-12T17:30:10.56Z" },
+ { url = "https://files.pythonhosted.org/packages/14/e8/7424ced75473983b964d09f6747fa09f054a6d656f60e9ac9324cf40c743/fonttools-4.61.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e6bcdf33aec38d16508ce61fd81838f24c83c90a1d1b8c68982857038673d6b8", size = 4944109, upload-time = "2025-12-12T17:30:12.874Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/8b/6391b257fa3d0b553d73e778f953a2f0154292a7a7a085e2374b111e5410/fonttools-4.61.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5fade934607a523614726119164ff621e8c30e8fa1ffffbbd358662056ba69f0", size = 5093598, upload-time = "2025-12-12T17:30:15.79Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/71/fd2ea96cdc512d92da5678a1c98c267ddd4d8c5130b76d0f7a80f9a9fde8/fonttools-4.61.1-cp312-cp312-win32.whl", hash = "sha256:75da8f28eff26defba42c52986de97b22106cb8f26515b7c22443ebc9c2d3261", size = 2269060, upload-time = "2025-12-12T17:30:18.058Z" },
+ { url = "https://files.pythonhosted.org/packages/80/3b/a3e81b71aed5a688e89dfe0e2694b26b78c7d7f39a5ffd8a7d75f54a12a8/fonttools-4.61.1-cp312-cp312-win_amd64.whl", hash = "sha256:497c31ce314219888c0e2fce5ad9178ca83fe5230b01a5006726cdf3ac9f24d9", size = 2319078, upload-time = "2025-12-12T17:30:22.862Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/cf/00ba28b0990982530addb8dc3e9e6f2fa9cb5c20df2abdda7baa755e8fe1/fonttools-4.61.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c56c488ab471628ff3bfa80964372fc13504ece601e0d97a78ee74126b2045c", size = 2846454, upload-time = "2025-12-12T17:30:24.938Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/ca/468c9a8446a2103ae645d14fee3f610567b7042aba85031c1c65e3ef7471/fonttools-4.61.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dc492779501fa723b04d0ab1f5be046797fee17d27700476edc7ee9ae535a61e", size = 2398191, upload-time = "2025-12-12T17:30:27.343Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/4b/d67eedaed19def5967fade3297fed8161b25ba94699efc124b14fb68cdbc/fonttools-4.61.1-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:64102ca87e84261419c3747a0d20f396eb024bdbeb04c2bfb37e2891f5fadcb5", size = 4928410, upload-time = "2025-12-12T17:30:29.771Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/8d/6fb3494dfe61a46258cd93d979cf4725ded4eb46c2a4ca35e4490d84daea/fonttools-4.61.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c1b526c8d3f615a7b1867f38a9410849c8f4aef078535742198e942fba0e9bd", size = 4984460, upload-time = "2025-12-12T17:30:32.073Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/f1/a47f1d30b3dc00d75e7af762652d4cbc3dff5c2697a0dbd5203c81afd9c3/fonttools-4.61.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:41ed4b5ec103bd306bb68f81dc166e77409e5209443e5773cb4ed837bcc9b0d3", size = 4925800, upload-time = "2025-12-12T17:30:34.339Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/01/e6ae64a0981076e8a66906fab01539799546181e32a37a0257b77e4aa88b/fonttools-4.61.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b501c862d4901792adaec7c25b1ecc749e2662543f68bb194c42ba18d6eec98d", size = 5067859, upload-time = "2025-12-12T17:30:36.593Z" },
+ { url = "https://files.pythonhosted.org/packages/73/aa/28e40b8d6809a9b5075350a86779163f074d2b617c15d22343fce81918db/fonttools-4.61.1-cp313-cp313-win32.whl", hash = "sha256:4d7092bb38c53bbc78e9255a59158b150bcdc115a1e3b3ce0b5f267dc35dd63c", size = 2267821, upload-time = "2025-12-12T17:30:38.478Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/59/453c06d1d83dc0951b69ef692d6b9f1846680342927df54e9a1ca91c6f90/fonttools-4.61.1-cp313-cp313-win_amd64.whl", hash = "sha256:21e7c8d76f62ab13c9472ccf74515ca5b9a761d1bde3265152a6dc58700d895b", size = 2318169, upload-time = "2025-12-12T17:30:40.951Z" },
+ { url = "https://files.pythonhosted.org/packages/32/8f/4e7bf82c0cbb738d3c2206c920ca34ca74ef9dabde779030145d28665104/fonttools-4.61.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fff4f534200a04b4a36e7ae3cb74493afe807b517a09e99cb4faa89a34ed6ecd", size = 2846094, upload-time = "2025-12-12T17:30:43.511Z" },
+ { url = "https://files.pythonhosted.org/packages/71/09/d44e45d0a4f3a651f23a1e9d42de43bc643cce2971b19e784cc67d823676/fonttools-4.61.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d9203500f7c63545b4ce3799319fe4d9feb1a1b89b28d3cb5abd11b9dd64147e", size = 2396589, upload-time = "2025-12-12T17:30:45.681Z" },
+ { url = "https://files.pythonhosted.org/packages/89/18/58c64cafcf8eb677a99ef593121f719e6dcbdb7d1c594ae5a10d4997ca8a/fonttools-4.61.1-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fa646ecec9528bef693415c79a86e733c70a4965dd938e9a226b0fc64c9d2e6c", size = 4877892, upload-time = "2025-12-12T17:30:47.709Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/ec/9e6b38c7ba1e09eb51db849d5450f4c05b7e78481f662c3b79dbde6f3d04/fonttools-4.61.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11f35ad7805edba3aac1a3710d104592df59f4b957e30108ae0ba6c10b11dd75", size = 4972884, upload-time = "2025-12-12T17:30:49.656Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/87/b5339da8e0256734ba0dbbf5b6cdebb1dd79b01dc8c270989b7bcd465541/fonttools-4.61.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b931ae8f62db78861b0ff1ac017851764602288575d65b8e8ff1963fed419063", size = 4924405, upload-time = "2025-12-12T17:30:51.735Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/47/e3409f1e1e69c073a3a6fd8cb886eb18c0bae0ee13db2c8d5e7f8495e8b7/fonttools-4.61.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b148b56f5de675ee16d45e769e69f87623a4944f7443850bf9a9376e628a89d2", size = 5035553, upload-time = "2025-12-12T17:30:54.823Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/b6/1f6600161b1073a984294c6c031e1a56ebf95b6164249eecf30012bb2e38/fonttools-4.61.1-cp314-cp314-win32.whl", hash = "sha256:9b666a475a65f4e839d3d10473fad6d47e0a9db14a2f4a224029c5bfde58ad2c", size = 2271915, upload-time = "2025-12-12T17:30:57.913Z" },
+ { url = "https://files.pythonhosted.org/packages/52/7b/91e7b01e37cc8eb0e1f770d08305b3655e4f002fc160fb82b3390eabacf5/fonttools-4.61.1-cp314-cp314-win_amd64.whl", hash = "sha256:4f5686e1fe5fce75d82d93c47a438a25bf0d1319d2843a926f741140b2b16e0c", size = 2323487, upload-time = "2025-12-12T17:30:59.804Z" },
+ { url = "https://files.pythonhosted.org/packages/39/5c/908ad78e46c61c3e3ed70c3b58ff82ab48437faf84ec84f109592cabbd9f/fonttools-4.61.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:e76ce097e3c57c4bcb67c5aa24a0ecdbd9f74ea9219997a707a4061fbe2707aa", size = 2929571, upload-time = "2025-12-12T17:31:02.574Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/41/975804132c6dea64cdbfbaa59f3518a21c137a10cccf962805b301ac6ab2/fonttools-4.61.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9cfef3ab326780c04d6646f68d4b4742aae222e8b8ea1d627c74e38afcbc9d91", size = 2435317, upload-time = "2025-12-12T17:31:04.974Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/5a/aef2a0a8daf1ebaae4cfd83f84186d4a72ee08fd6a8451289fcd03ffa8a4/fonttools-4.61.1-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a75c301f96db737e1c5ed5fd7d77d9c34466de16095a266509e13da09751bd19", size = 4882124, upload-time = "2025-12-12T17:31:07.456Z" },
+ { url = "https://files.pythonhosted.org/packages/80/33/d6db3485b645b81cea538c9d1c9219d5805f0877fda18777add4671c5240/fonttools-4.61.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:91669ccac46bbc1d09e9273546181919064e8df73488ea087dcac3e2968df9ba", size = 5100391, upload-time = "2025-12-12T17:31:09.732Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/d6/675ba631454043c75fcf76f0ca5463eac8eb0666ea1d7badae5fea001155/fonttools-4.61.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c33ab3ca9d3ccd581d58e989d67554e42d8d4ded94ab3ade3508455fe70e65f7", size = 4978800, upload-time = "2025-12-12T17:31:11.681Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/33/d3ec753d547a8d2bdaedd390d4a814e8d5b45a093d558f025c6b990b554c/fonttools-4.61.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:664c5a68ec406f6b1547946683008576ef8b38275608e1cee6c061828171c118", size = 5006426, upload-time = "2025-12-12T17:31:13.764Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/40/cc11f378b561a67bea850ab50063366a0d1dd3f6d0a30ce0f874b0ad5664/fonttools-4.61.1-cp314-cp314t-win32.whl", hash = "sha256:aed04cabe26f30c1647ef0e8fbb207516fd40fe9472e9439695f5c6998e60ac5", size = 2335377, upload-time = "2025-12-12T17:31:16.49Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/ff/c9a2b66b39f8628531ea58b320d66d951267c98c6a38684daa8f50fb02f8/fonttools-4.61.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2180f14c141d2f0f3da43f3a81bc8aa4684860f6b0e6f9e165a4831f24e6a23b", size = 2400613, upload-time = "2025-12-12T17:31:18.769Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/4e/ce75a57ff3aebf6fc1f4e9d508b8e5810618a33d900ad6c19eb30b290b97/fonttools-4.61.1-py3-none-any.whl", hash = "sha256:17d2bf5d541add43822bcf0c43d7d847b160c9bb01d15d5007d84e2217aaa371", size = 1148996, upload-time = "2025-12-12T17:31:21.03Z" },
+]
+
+[[package]]
+name = "h11"
+version = "0.16.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
+]
+
+[[package]]
+name = "httpcore"
+version = "1.0.9"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "h11" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
+]
+
+[[package]]
+name = "httpx"
+version = "0.28.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "certifi" },
+ { name = "httpcore" },
+ { name = "idna" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
+]
+
+[[package]]
+name = "httpx-sse"
+version = "0.4.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" },
+]
+
+[[package]]
+name = "idna"
+version = "3.11"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
+]
+
+[[package]]
+name = "jsonschema"
+version = "4.26.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "attrs" },
+ { name = "jsonschema-specifications" },
+ { name = "referencing" },
+ { name = "rpds-py" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" },
+]
+
+[[package]]
+name = "jsonschema-specifications"
+version = "2025.9.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "referencing" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" },
+]
+
+[[package]]
+name = "lxml"
+version = "6.0.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/aa/88/262177de60548e5a2bfc46ad28232c9e9cbde697bd94132aeb80364675cb/lxml-6.0.2.tar.gz", hash = "sha256:cd79f3367bd74b317dda655dc8fcfa304d9eb6e4fb06b7168c5cf27f96e0cd62", size = 4073426, upload-time = "2025-09-22T04:04:59.287Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/db/8a/f8192a08237ef2fb1b19733f709db88a4c43bc8ab8357f01cb41a27e7f6a/lxml-6.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e77dd455b9a16bbd2a5036a63ddbd479c19572af81b624e79ef422f929eef388", size = 8590589, upload-time = "2025-09-22T04:00:10.51Z" },
+ { url = "https://files.pythonhosted.org/packages/12/64/27bcd07ae17ff5e5536e8d88f4c7d581b48963817a13de11f3ac3329bfa2/lxml-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d444858b9f07cefff6455b983aea9a67f7462ba1f6cbe4a21e8bf6791bf2153", size = 4629671, upload-time = "2025-09-22T04:00:15.411Z" },
+ { url = "https://files.pythonhosted.org/packages/02/5a/a7d53b3291c324e0b6e48f3c797be63836cc52156ddf8f33cd72aac78866/lxml-6.0.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f952dacaa552f3bb8834908dddd500ba7d508e6ea6eb8c52eb2d28f48ca06a31", size = 4999961, upload-time = "2025-09-22T04:00:17.619Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/55/d465e9b89df1761674d8672bb3e4ae2c47033b01ec243964b6e334c6743f/lxml-6.0.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:71695772df6acea9f3c0e59e44ba8ac50c4f125217e84aab21074a1a55e7e5c9", size = 5157087, upload-time = "2025-09-22T04:00:19.868Z" },
+ { url = "https://files.pythonhosted.org/packages/62/38/3073cd7e3e8dfc3ba3c3a139e33bee3a82de2bfb0925714351ad3d255c13/lxml-6.0.2-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:17f68764f35fd78d7c4cc4ef209a184c38b65440378013d24b8aecd327c3e0c8", size = 5067620, upload-time = "2025-09-22T04:00:21.877Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/d3/1e001588c5e2205637b08985597827d3827dbaaece16348c8822bfe61c29/lxml-6.0.2-cp310-cp310-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:058027e261afed589eddcfe530fcc6f3402d7fd7e89bfd0532df82ebc1563dba", size = 5406664, upload-time = "2025-09-22T04:00:23.714Z" },
+ { url = "https://files.pythonhosted.org/packages/20/cf/cab09478699b003857ed6ebfe95e9fb9fa3d3c25f1353b905c9b73cfb624/lxml-6.0.2-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8ffaeec5dfea5881d4c9d8913a32d10cfe3923495386106e4a24d45300ef79c", size = 5289397, upload-time = "2025-09-22T04:00:25.544Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/84/02a2d0c38ac9a8b9f9e5e1bbd3f24b3f426044ad618b552e9549ee91bd63/lxml-6.0.2-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:f2e3b1a6bb38de0bc713edd4d612969dd250ca8b724be8d460001a387507021c", size = 4772178, upload-time = "2025-09-22T04:00:27.602Z" },
+ { url = "https://files.pythonhosted.org/packages/56/87/e1ceadcc031ec4aa605fe95476892d0b0ba3b7f8c7dcdf88fdeff59a9c86/lxml-6.0.2-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d6690ec5ec1cce0385cb20896b16be35247ac8c2046e493d03232f1c2414d321", size = 5358148, upload-time = "2025-09-22T04:00:29.323Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/13/5bb6cf42bb228353fd4ac5f162c6a84fd68a4d6f67c1031c8cf97e131fc6/lxml-6.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f2a50c3c1d11cad0ebebbac357a97b26aa79d2bcaf46f256551152aa85d3a4d1", size = 5112035, upload-time = "2025-09-22T04:00:31.061Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/e2/ea0498552102e59834e297c5c6dff8d8ded3db72ed5e8aad77871476f073/lxml-6.0.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:3efe1b21c7801ffa29a1112fab3b0f643628c30472d507f39544fd48e9549e34", size = 4799111, upload-time = "2025-09-22T04:00:33.11Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/9e/8de42b52a73abb8af86c66c969b3b4c2a96567b6ac74637c037d2e3baa60/lxml-6.0.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:59c45e125140b2c4b33920d21d83681940ca29f0b83f8629ea1a2196dc8cfe6a", size = 5351662, upload-time = "2025-09-22T04:00:35.237Z" },
+ { url = "https://files.pythonhosted.org/packages/28/a2/de776a573dfb15114509a37351937c367530865edb10a90189d0b4b9b70a/lxml-6.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:452b899faa64f1805943ec1c0c9ebeaece01a1af83e130b69cdefeda180bb42c", size = 5314973, upload-time = "2025-09-22T04:00:37.086Z" },
+ { url = "https://files.pythonhosted.org/packages/50/a0/3ae1b1f8964c271b5eec91db2043cf8c6c0bce101ebb2a633b51b044db6c/lxml-6.0.2-cp310-cp310-win32.whl", hash = "sha256:1e786a464c191ca43b133906c6903a7e4d56bef376b75d97ccbb8ec5cf1f0a4b", size = 3611953, upload-time = "2025-09-22T04:00:39.224Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/70/bd42491f0634aad41bdfc1e46f5cff98825fb6185688dc82baa35d509f1a/lxml-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:dacf3c64ef3f7440e3167aa4b49aa9e0fb99e0aa4f9ff03795640bf94531bcb0", size = 4032695, upload-time = "2025-09-22T04:00:41.402Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/d0/05c6a72299f54c2c561a6c6cbb2f512e047fca20ea97a05e57931f194ac4/lxml-6.0.2-cp310-cp310-win_arm64.whl", hash = "sha256:45f93e6f75123f88d7f0cfd90f2d05f441b808562bf0bc01070a00f53f5028b5", size = 3680051, upload-time = "2025-09-22T04:00:43.525Z" },
+ { url = "https://files.pythonhosted.org/packages/77/d5/becbe1e2569b474a23f0c672ead8a29ac50b2dc1d5b9de184831bda8d14c/lxml-6.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:13e35cbc684aadf05d8711a5d1b5857c92e5e580efa9a0d2be197199c8def607", size = 8634365, upload-time = "2025-09-22T04:00:45.672Z" },
+ { url = "https://files.pythonhosted.org/packages/28/66/1ced58f12e804644426b85d0bb8a4478ca77bc1761455da310505f1a3526/lxml-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b1675e096e17c6fe9c0e8c81434f5736c0739ff9ac6123c87c2d452f48fc938", size = 4650793, upload-time = "2025-09-22T04:00:47.783Z" },
+ { url = "https://files.pythonhosted.org/packages/11/84/549098ffea39dfd167e3f174b4ce983d0eed61f9d8d25b7bf2a57c3247fc/lxml-6.0.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8ac6e5811ae2870953390452e3476694196f98d447573234592d30488147404d", size = 4944362, upload-time = "2025-09-22T04:00:49.845Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/bd/f207f16abf9749d2037453d56b643a7471d8fde855a231a12d1e095c4f01/lxml-6.0.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5aa0fc67ae19d7a64c3fe725dc9a1bb11f80e01f78289d05c6f62545affec438", size = 5083152, upload-time = "2025-09-22T04:00:51.709Z" },
+ { url = "https://files.pythonhosted.org/packages/15/ae/bd813e87d8941d52ad5b65071b1affb48da01c4ed3c9c99e40abb266fbff/lxml-6.0.2-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de496365750cc472b4e7902a485d3f152ecf57bd3ba03ddd5578ed8ceb4c5964", size = 5023539, upload-time = "2025-09-22T04:00:53.593Z" },
+ { url = "https://files.pythonhosted.org/packages/02/cd/9bfef16bd1d874fbe0cb51afb00329540f30a3283beb9f0780adbb7eec03/lxml-6.0.2-cp311-cp311-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:200069a593c5e40b8f6fc0d84d86d970ba43138c3e68619ffa234bc9bb806a4d", size = 5344853, upload-time = "2025-09-22T04:00:55.524Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/89/ea8f91594bc5dbb879734d35a6f2b0ad50605d7fb419de2b63d4211765cc/lxml-6.0.2-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d2de809c2ee3b888b59f995625385f74629707c9355e0ff856445cdcae682b7", size = 5225133, upload-time = "2025-09-22T04:00:57.269Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/37/9c735274f5dbec726b2db99b98a43950395ba3d4a1043083dba2ad814170/lxml-6.0.2-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:b2c3da8d93cf5db60e8858c17684c47d01fee6405e554fb55018dd85fc23b178", size = 4677944, upload-time = "2025-09-22T04:00:59.052Z" },
+ { url = "https://files.pythonhosted.org/packages/20/28/7dfe1ba3475d8bfca3878365075abe002e05d40dfaaeb7ec01b4c587d533/lxml-6.0.2-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:442de7530296ef5e188373a1ea5789a46ce90c4847e597856570439621d9c553", size = 5284535, upload-time = "2025-09-22T04:01:01.335Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/cf/5f14bc0de763498fc29510e3532bf2b4b3a1c1d5d0dff2e900c16ba021ef/lxml-6.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2593c77efde7bfea7f6389f1ab249b15ed4aa5bc5cb5131faa3b843c429fbedb", size = 5067343, upload-time = "2025-09-22T04:01:03.13Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/b0/bb8275ab5472f32b28cfbbcc6db7c9d092482d3439ca279d8d6fa02f7025/lxml-6.0.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3e3cb08855967a20f553ff32d147e14329b3ae70ced6edc2f282b94afbc74b2a", size = 4725419, upload-time = "2025-09-22T04:01:05.013Z" },
+ { url = "https://files.pythonhosted.org/packages/25/4c/7c222753bc72edca3b99dbadba1b064209bc8ed4ad448af990e60dcce462/lxml-6.0.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2ed6c667fcbb8c19c6791bbf40b7268ef8ddf5a96940ba9404b9f9a304832f6c", size = 5275008, upload-time = "2025-09-22T04:01:07.327Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/8c/478a0dc6b6ed661451379447cdbec77c05741a75736d97e5b2b729687828/lxml-6.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b8f18914faec94132e5b91e69d76a5c1d7b0c73e2489ea8929c4aaa10b76bbf7", size = 5248906, upload-time = "2025-09-22T04:01:09.452Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/d9/5be3a6ab2784cdf9accb0703b65e1b64fcdd9311c9f007630c7db0cfcce1/lxml-6.0.2-cp311-cp311-win32.whl", hash = "sha256:6605c604e6daa9e0d7f0a2137bdc47a2e93b59c60a65466353e37f8272f47c46", size = 3610357, upload-time = "2025-09-22T04:01:11.102Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/7d/ca6fb13349b473d5732fb0ee3eec8f6c80fc0688e76b7d79c1008481bf1f/lxml-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e5867f2651016a3afd8dd2c8238baa66f1e2802f44bc17e236f547ace6647078", size = 4036583, upload-time = "2025-09-22T04:01:12.766Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/a2/51363b5ecd3eab46563645f3a2c3836a2fc67d01a1b87c5017040f39f567/lxml-6.0.2-cp311-cp311-win_arm64.whl", hash = "sha256:4197fb2534ee05fd3e7afaab5d8bfd6c2e186f65ea7f9cd6a82809c887bd1285", size = 3680591, upload-time = "2025-09-22T04:01:14.874Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/c8/8ff2bc6b920c84355146cd1ab7d181bc543b89241cfb1ebee824a7c81457/lxml-6.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a59f5448ba2ceccd06995c95ea59a7674a10de0810f2ce90c9006f3cbc044456", size = 8661887, upload-time = "2025-09-22T04:01:17.265Z" },
+ { url = "https://files.pythonhosted.org/packages/37/6f/9aae1008083bb501ef63284220ce81638332f9ccbfa53765b2b7502203cf/lxml-6.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e8113639f3296706fbac34a30813929e29247718e88173ad849f57ca59754924", size = 4667818, upload-time = "2025-09-22T04:01:19.688Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/ca/31fb37f99f37f1536c133476674c10b577e409c0a624384147653e38baf2/lxml-6.0.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a8bef9b9825fa8bc816a6e641bb67219489229ebc648be422af695f6e7a4fa7f", size = 4950807, upload-time = "2025-09-22T04:01:21.487Z" },
+ { url = "https://files.pythonhosted.org/packages/da/87/f6cb9442e4bada8aab5ae7e1046264f62fdbeaa6e3f6211b93f4c0dd97f1/lxml-6.0.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:65ea18d710fd14e0186c2f973dc60bb52039a275f82d3c44a0e42b43440ea534", size = 5109179, upload-time = "2025-09-22T04:01:23.32Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/20/a7760713e65888db79bbae4f6146a6ae5c04e4a204a3c48896c408cd6ed2/lxml-6.0.2-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c371aa98126a0d4c739ca93ceffa0fd7a5d732e3ac66a46e74339acd4d334564", size = 5023044, upload-time = "2025-09-22T04:01:25.118Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/b0/7e64e0460fcb36471899f75831509098f3fd7cd02a3833ac517433cb4f8f/lxml-6.0.2-cp312-cp312-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:700efd30c0fa1a3581d80a748157397559396090a51d306ea59a70020223d16f", size = 5359685, upload-time = "2025-09-22T04:01:27.398Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/e1/e5df362e9ca4e2f48ed6411bd4b3a0ae737cc842e96877f5bf9428055ab4/lxml-6.0.2-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c33e66d44fe60e72397b487ee92e01da0d09ba2d66df8eae42d77b6d06e5eba0", size = 5654127, upload-time = "2025-09-22T04:01:29.629Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/d1/232b3309a02d60f11e71857778bfcd4acbdb86c07db8260caf7d008b08f8/lxml-6.0.2-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90a345bbeaf9d0587a3aaffb7006aa39ccb6ff0e96a57286c0cb2fd1520ea192", size = 5253958, upload-time = "2025-09-22T04:01:31.535Z" },
+ { url = "https://files.pythonhosted.org/packages/35/35/d955a070994725c4f7d80583a96cab9c107c57a125b20bb5f708fe941011/lxml-6.0.2-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:064fdadaf7a21af3ed1dcaa106b854077fbeada827c18f72aec9346847cd65d0", size = 4711541, upload-time = "2025-09-22T04:01:33.801Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/be/667d17363b38a78c4bd63cfd4b4632029fd68d2c2dc81f25ce9eb5224dd5/lxml-6.0.2-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fbc74f42c3525ac4ffa4b89cbdd00057b6196bcefe8bce794abd42d33a018092", size = 5267426, upload-time = "2025-09-22T04:01:35.639Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/47/62c70aa4a1c26569bc958c9ca86af2bb4e1f614e8c04fb2989833874f7ae/lxml-6.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6ddff43f702905a4e32bc24f3f2e2edfe0f8fde3277d481bffb709a4cced7a1f", size = 5064917, upload-time = "2025-09-22T04:01:37.448Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/55/6ceddaca353ebd0f1908ef712c597f8570cc9c58130dbb89903198e441fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6da5185951d72e6f5352166e3da7b0dc27aa70bd1090b0eb3f7f7212b53f1bb8", size = 4788795, upload-time = "2025-09-22T04:01:39.165Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/e8/fd63e15da5e3fd4c2146f8bbb3c14e94ab850589beab88e547b2dbce22e1/lxml-6.0.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:57a86e1ebb4020a38d295c04fc79603c7899e0df71588043eb218722dabc087f", size = 5676759, upload-time = "2025-09-22T04:01:41.506Z" },
+ { url = "https://files.pythonhosted.org/packages/76/47/b3ec58dc5c374697f5ba37412cd2728f427d056315d124dd4b61da381877/lxml-6.0.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2047d8234fe735ab77802ce5f2297e410ff40f5238aec569ad7c8e163d7b19a6", size = 5255666, upload-time = "2025-09-22T04:01:43.363Z" },
+ { url = "https://files.pythonhosted.org/packages/19/93/03ba725df4c3d72afd9596eef4a37a837ce8e4806010569bedfcd2cb68fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f91fd2b2ea15a6800c8e24418c0775a1694eefc011392da73bc6cef2623b322", size = 5277989, upload-time = "2025-09-22T04:01:45.215Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/80/c06de80bfce881d0ad738576f243911fccf992687ae09fd80b734712b39c/lxml-6.0.2-cp312-cp312-win32.whl", hash = "sha256:3ae2ce7d6fedfb3414a2b6c5e20b249c4c607f72cb8d2bb7cc9c6ec7c6f4e849", size = 3611456, upload-time = "2025-09-22T04:01:48.243Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/d7/0cdfb6c3e30893463fb3d1e52bc5f5f99684a03c29a0b6b605cfae879cd5/lxml-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:72c87e5ee4e58a8354fb9c7c84cbf95a1c8236c127a5d1b7683f04bed8361e1f", size = 4011793, upload-time = "2025-09-22T04:01:50.042Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/7b/93c73c67db235931527301ed3785f849c78991e2e34f3fd9a6663ffda4c5/lxml-6.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:61cb10eeb95570153e0c0e554f58df92ecf5109f75eacad4a95baa709e26c3d6", size = 3672836, upload-time = "2025-09-22T04:01:52.145Z" },
+ { url = "https://files.pythonhosted.org/packages/53/fd/4e8f0540608977aea078bf6d79f128e0e2c2bba8af1acf775c30baa70460/lxml-6.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9b33d21594afab46f37ae58dfadd06636f154923c4e8a4d754b0127554eb2e77", size = 8648494, upload-time = "2025-09-22T04:01:54.242Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/f4/2a94a3d3dfd6c6b433501b8d470a1960a20ecce93245cf2db1706adf6c19/lxml-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c8963287d7a4c5c9a432ff487c52e9c5618667179c18a204bdedb27310f022f", size = 4661146, upload-time = "2025-09-22T04:01:56.282Z" },
+ { url = "https://files.pythonhosted.org/packages/25/2e/4efa677fa6b322013035d38016f6ae859d06cac67437ca7dc708a6af7028/lxml-6.0.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1941354d92699fb5ffe6ed7b32f9649e43c2feb4b97205f75866f7d21aa91452", size = 4946932, upload-time = "2025-09-22T04:01:58.989Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/0f/526e78a6d38d109fdbaa5049c62e1d32fdd70c75fb61c4eadf3045d3d124/lxml-6.0.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb2f6ca0ae2d983ded09357b84af659c954722bbf04dea98030064996d156048", size = 5100060, upload-time = "2025-09-22T04:02:00.812Z" },
+ { url = "https://files.pythonhosted.org/packages/81/76/99de58d81fa702cc0ea7edae4f4640416c2062813a00ff24bd70ac1d9c9b/lxml-6.0.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb2a12d704f180a902d7fa778c6d71f36ceb7b0d317f34cdc76a5d05aa1dd1df", size = 5019000, upload-time = "2025-09-22T04:02:02.671Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/35/9e57d25482bc9a9882cb0037fdb9cc18f4b79d85df94fa9d2a89562f1d25/lxml-6.0.2-cp313-cp313-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:6ec0e3f745021bfed19c456647f0298d60a24c9ff86d9d051f52b509663feeb1", size = 5348496, upload-time = "2025-09-22T04:02:04.904Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/8e/cb99bd0b83ccc3e8f0f528e9aa1f7a9965dfec08c617070c5db8d63a87ce/lxml-6.0.2-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:846ae9a12d54e368933b9759052d6206a9e8b250291109c48e350c1f1f49d916", size = 5643779, upload-time = "2025-09-22T04:02:06.689Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/34/9e591954939276bb679b73773836c6684c22e56d05980e31d52a9a8deb18/lxml-6.0.2-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef9266d2aa545d7374938fb5c484531ef5a2ec7f2d573e62f8ce722c735685fd", size = 5244072, upload-time = "2025-09-22T04:02:08.587Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/27/b29ff065f9aaca443ee377aff699714fcbffb371b4fce5ac4ca759e436d5/lxml-6.0.2-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:4077b7c79f31755df33b795dc12119cb557a0106bfdab0d2c2d97bd3cf3dffa6", size = 4718675, upload-time = "2025-09-22T04:02:10.783Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/9f/f756f9c2cd27caa1a6ef8c32ae47aadea697f5c2c6d07b0dae133c244fbe/lxml-6.0.2-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a7c5d5e5f1081955358533be077166ee97ed2571d6a66bdba6ec2f609a715d1a", size = 5255171, upload-time = "2025-09-22T04:02:12.631Z" },
+ { url = "https://files.pythonhosted.org/packages/61/46/bb85ea42d2cb1bd8395484fd72f38e3389611aa496ac7772da9205bbda0e/lxml-6.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8f8d0cbd0674ee89863a523e6994ac25fd5be9c8486acfc3e5ccea679bad2679", size = 5057175, upload-time = "2025-09-22T04:02:14.718Z" },
+ { url = "https://files.pythonhosted.org/packages/95/0c/443fc476dcc8e41577f0af70458c50fe299a97bb6b7505bb1ae09aa7f9ac/lxml-6.0.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2cbcbf6d6e924c28f04a43f3b6f6e272312a090f269eff68a2982e13e5d57659", size = 4785688, upload-time = "2025-09-22T04:02:16.957Z" },
+ { url = "https://files.pythonhosted.org/packages/48/78/6ef0b359d45bb9697bc5a626e1992fa5d27aa3f8004b137b2314793b50a0/lxml-6.0.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dfb874cfa53340009af6bdd7e54ebc0d21012a60a4e65d927c2e477112e63484", size = 5660655, upload-time = "2025-09-22T04:02:18.815Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/ea/e1d33808f386bc1339d08c0dcada6e4712d4ed8e93fcad5f057070b7988a/lxml-6.0.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fb8dae0b6b8b7f9e96c26fdd8121522ce5de9bb5538010870bd538683d30e9a2", size = 5247695, upload-time = "2025-09-22T04:02:20.593Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/47/eba75dfd8183673725255247a603b4ad606f4ae657b60c6c145b381697da/lxml-6.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:358d9adae670b63e95bc59747c72f4dc97c9ec58881d4627fe0120da0f90d314", size = 5269841, upload-time = "2025-09-22T04:02:22.489Z" },
+ { url = "https://files.pythonhosted.org/packages/76/04/5c5e2b8577bc936e219becb2e98cdb1aca14a4921a12995b9d0c523502ae/lxml-6.0.2-cp313-cp313-win32.whl", hash = "sha256:e8cd2415f372e7e5a789d743d133ae474290a90b9023197fd78f32e2dc6873e2", size = 3610700, upload-time = "2025-09-22T04:02:24.465Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/0a/4643ccc6bb8b143e9f9640aa54e38255f9d3b45feb2cbe7ae2ca47e8782e/lxml-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:b30d46379644fbfc3ab81f8f82ae4de55179414651f110a1514f0b1f8f6cb2d7", size = 4010347, upload-time = "2025-09-22T04:02:26.286Z" },
+ { url = "https://files.pythonhosted.org/packages/31/ef/dcf1d29c3f530577f61e5fe2f1bd72929acf779953668a8a47a479ae6f26/lxml-6.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:13dcecc9946dca97b11b7c40d29fba63b55ab4170d3c0cf8c0c164343b9bfdcf", size = 3671248, upload-time = "2025-09-22T04:02:27.918Z" },
+ { url = "https://files.pythonhosted.org/packages/03/15/d4a377b385ab693ce97b472fe0c77c2b16ec79590e688b3ccc71fba19884/lxml-6.0.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:b0c732aa23de8f8aec23f4b580d1e52905ef468afb4abeafd3fec77042abb6fe", size = 8659801, upload-time = "2025-09-22T04:02:30.113Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/e8/c128e37589463668794d503afaeb003987373c5f94d667124ffd8078bbd9/lxml-6.0.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4468e3b83e10e0317a89a33d28f7aeba1caa4d1a6fd457d115dd4ffe90c5931d", size = 4659403, upload-time = "2025-09-22T04:02:32.119Z" },
+ { url = "https://files.pythonhosted.org/packages/00/ce/74903904339decdf7da7847bb5741fc98a5451b42fc419a86c0c13d26fe2/lxml-6.0.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:abd44571493973bad4598a3be7e1d807ed45aa2adaf7ab92ab7c62609569b17d", size = 4966974, upload-time = "2025-09-22T04:02:34.155Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/d3/131dec79ce61c5567fecf82515bd9bc36395df42501b50f7f7f3bd065df0/lxml-6.0.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:370cd78d5855cfbffd57c422851f7d3864e6ae72d0da615fca4dad8c45d375a5", size = 5102953, upload-time = "2025-09-22T04:02:36.054Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/ea/a43ba9bb750d4ffdd885f2cd333572f5bb900cd2408b67fdda07e85978a0/lxml-6.0.2-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:901e3b4219fa04ef766885fb40fa516a71662a4c61b80c94d25336b4934b71c0", size = 5055054, upload-time = "2025-09-22T04:02:38.154Z" },
+ { url = "https://files.pythonhosted.org/packages/60/23/6885b451636ae286c34628f70a7ed1fcc759f8d9ad382d132e1c8d3d9bfd/lxml-6.0.2-cp314-cp314-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:a4bf42d2e4cf52c28cc1812d62426b9503cdb0c87a6de81442626aa7d69707ba", size = 5352421, upload-time = "2025-09-22T04:02:40.413Z" },
+ { url = "https://files.pythonhosted.org/packages/48/5b/fc2ddfc94ddbe3eebb8e9af6e3fd65e2feba4967f6a4e9683875c394c2d8/lxml-6.0.2-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2c7fdaa4d7c3d886a42534adec7cfac73860b89b4e5298752f60aa5984641a0", size = 5673684, upload-time = "2025-09-22T04:02:42.288Z" },
+ { url = "https://files.pythonhosted.org/packages/29/9c/47293c58cc91769130fbf85531280e8cc7868f7fbb6d92f4670071b9cb3e/lxml-6.0.2-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98a5e1660dc7de2200b00d53fa00bcd3c35a3608c305d45a7bbcaf29fa16e83d", size = 5252463, upload-time = "2025-09-22T04:02:44.165Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/da/ba6eceb830c762b48e711ded880d7e3e89fc6c7323e587c36540b6b23c6b/lxml-6.0.2-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:dc051506c30b609238d79eda75ee9cab3e520570ec8219844a72a46020901e37", size = 4698437, upload-time = "2025-09-22T04:02:46.524Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/24/7be3f82cb7990b89118d944b619e53c656c97dc89c28cfb143fdb7cd6f4d/lxml-6.0.2-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8799481bbdd212470d17513a54d568f44416db01250f49449647b5ab5b5dccb9", size = 5269890, upload-time = "2025-09-22T04:02:48.812Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/bd/dcfb9ea1e16c665efd7538fc5d5c34071276ce9220e234217682e7d2c4a5/lxml-6.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9261bb77c2dab42f3ecd9103951aeca2c40277701eb7e912c545c1b16e0e4917", size = 5097185, upload-time = "2025-09-22T04:02:50.746Z" },
+ { url = "https://files.pythonhosted.org/packages/21/04/a60b0ff9314736316f28316b694bccbbabe100f8483ad83852d77fc7468e/lxml-6.0.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:65ac4a01aba353cfa6d5725b95d7aed6356ddc0a3cd734de00124d285b04b64f", size = 4745895, upload-time = "2025-09-22T04:02:52.968Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/bd/7d54bd1846e5a310d9c715921c5faa71cf5c0853372adf78aee70c8d7aa2/lxml-6.0.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b22a07cbb82fea98f8a2fd814f3d1811ff9ed76d0fc6abc84eb21527596e7cc8", size = 5695246, upload-time = "2025-09-22T04:02:54.798Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/32/5643d6ab947bc371da21323acb2a6e603cedbe71cb4c99c8254289ab6f4e/lxml-6.0.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d759cdd7f3e055d6bc8d9bec3ad905227b2e4c785dc16c372eb5b5e83123f48a", size = 5260797, upload-time = "2025-09-22T04:02:57.058Z" },
+ { url = "https://files.pythonhosted.org/packages/33/da/34c1ec4cff1eea7d0b4cd44af8411806ed943141804ac9c5d565302afb78/lxml-6.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:945da35a48d193d27c188037a05fec5492937f66fb1958c24fc761fb9d40d43c", size = 5277404, upload-time = "2025-09-22T04:02:58.966Z" },
+ { url = "https://files.pythonhosted.org/packages/82/57/4eca3e31e54dc89e2c3507e1cd411074a17565fa5ffc437c4ae0a00d439e/lxml-6.0.2-cp314-cp314-win32.whl", hash = "sha256:be3aaa60da67e6153eb15715cc2e19091af5dc75faef8b8a585aea372507384b", size = 3670072, upload-time = "2025-09-22T04:03:38.05Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/e0/c96cf13eccd20c9421ba910304dae0f619724dcf1702864fd59dd386404d/lxml-6.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:fa25afbadead523f7001caf0c2382afd272c315a033a7b06336da2637d92d6ed", size = 4080617, upload-time = "2025-09-22T04:03:39.835Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/5d/b3f03e22b3d38d6f188ef044900a9b29b2fe0aebb94625ce9fe244011d34/lxml-6.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:063eccf89df5b24e361b123e257e437f9e9878f425ee9aae3144c77faf6da6d8", size = 3754930, upload-time = "2025-09-22T04:03:41.565Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/5c/42c2c4c03554580708fc738d13414801f340c04c3eff90d8d2d227145275/lxml-6.0.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:6162a86d86893d63084faaf4ff937b3daea233e3682fb4474db07395794fa80d", size = 8910380, upload-time = "2025-09-22T04:03:01.645Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/4f/12df843e3e10d18d468a7557058f8d3733e8b6e12401f30b1ef29360740f/lxml-6.0.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:414aaa94e974e23a3e92e7ca5b97d10c0cf37b6481f50911032c69eeb3991bba", size = 4775632, upload-time = "2025-09-22T04:03:03.814Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/0c/9dc31e6c2d0d418483cbcb469d1f5a582a1cd00a1f4081953d44051f3c50/lxml-6.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48461bd21625458dd01e14e2c38dd0aea69addc3c4f960c30d9f59d7f93be601", size = 4975171, upload-time = "2025-09-22T04:03:05.651Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/2b/9b870c6ca24c841bdd887504808f0417aa9d8d564114689266f19ddf29c8/lxml-6.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:25fcc59afc57d527cfc78a58f40ab4c9b8fd096a9a3f964d2781ffb6eb33f4ed", size = 5110109, upload-time = "2025-09-22T04:03:07.452Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/0c/4f5f2a4dd319a178912751564471355d9019e220c20d7db3fb8307ed8582/lxml-6.0.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5179c60288204e6ddde3f774a93350177e08876eaf3ab78aa3a3649d43eb7d37", size = 5041061, upload-time = "2025-09-22T04:03:09.297Z" },
+ { url = "https://files.pythonhosted.org/packages/12/64/554eed290365267671fe001a20d72d14f468ae4e6acef1e179b039436967/lxml-6.0.2-cp314-cp314t-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:967aab75434de148ec80597b75062d8123cadf2943fb4281f385141e18b21338", size = 5306233, upload-time = "2025-09-22T04:03:11.651Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/31/1d748aa275e71802ad9722df32a7a35034246b42c0ecdd8235412c3396ef/lxml-6.0.2-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d100fcc8930d697c6561156c6810ab4a508fb264c8b6779e6e61e2ed5e7558f9", size = 5604739, upload-time = "2025-09-22T04:03:13.592Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/41/2c11916bcac09ed561adccacceaedd2bf0e0b25b297ea92aab99fd03d0fa/lxml-6.0.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ca59e7e13e5981175b8b3e4ab84d7da57993eeff53c07764dcebda0d0e64ecd", size = 5225119, upload-time = "2025-09-22T04:03:15.408Z" },
+ { url = "https://files.pythonhosted.org/packages/99/05/4e5c2873d8f17aa018e6afde417c80cc5d0c33be4854cce3ef5670c49367/lxml-6.0.2-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:957448ac63a42e2e49531b9d6c0fa449a1970dbc32467aaad46f11545be9af1d", size = 4633665, upload-time = "2025-09-22T04:03:17.262Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/c9/dcc2da1bebd6275cdc723b515f93edf548b82f36a5458cca3578bc899332/lxml-6.0.2-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b7fc49c37f1786284b12af63152fe1d0990722497e2d5817acfe7a877522f9a9", size = 5234997, upload-time = "2025-09-22T04:03:19.14Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/e2/5172e4e7468afca64a37b81dba152fc5d90e30f9c83c7c3213d6a02a5ce4/lxml-6.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e19e0643cc936a22e837f79d01a550678da8377d7d801a14487c10c34ee49c7e", size = 5090957, upload-time = "2025-09-22T04:03:21.436Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/b3/15461fd3e5cd4ddcb7938b87fc20b14ab113b92312fc97afe65cd7c85de1/lxml-6.0.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:1db01e5cf14345628e0cbe71067204db658e2fb8e51e7f33631f5f4735fefd8d", size = 4764372, upload-time = "2025-09-22T04:03:23.27Z" },
+ { url = "https://files.pythonhosted.org/packages/05/33/f310b987c8bf9e61c4dd8e8035c416bd3230098f5e3cfa69fc4232de7059/lxml-6.0.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:875c6b5ab39ad5291588aed6925fac99d0097af0dd62f33c7b43736043d4a2ec", size = 5634653, upload-time = "2025-09-22T04:03:25.767Z" },
+ { url = "https://files.pythonhosted.org/packages/70/ff/51c80e75e0bc9382158133bdcf4e339b5886c6ee2418b5199b3f1a61ed6d/lxml-6.0.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:cdcbed9ad19da81c480dfd6dd161886db6096083c9938ead313d94b30aadf272", size = 5233795, upload-time = "2025-09-22T04:03:27.62Z" },
+ { url = "https://files.pythonhosted.org/packages/56/4d/4856e897df0d588789dd844dbed9d91782c4ef0b327f96ce53c807e13128/lxml-6.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:80dadc234ebc532e09be1975ff538d154a7fa61ea5031c03d25178855544728f", size = 5257023, upload-time = "2025-09-22T04:03:30.056Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/85/86766dfebfa87bea0ab78e9ff7a4b4b45225df4b4d3b8cc3c03c5cd68464/lxml-6.0.2-cp314-cp314t-win32.whl", hash = "sha256:da08e7bb297b04e893d91087df19638dc7a6bb858a954b0cc2b9f5053c922312", size = 3911420, upload-time = "2025-09-22T04:03:32.198Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/1a/b248b355834c8e32614650b8008c69ffeb0ceb149c793961dd8c0b991bb3/lxml-6.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:252a22982dca42f6155125ac76d3432e548a7625d56f5a273ee78a5057216eca", size = 4406837, upload-time = "2025-09-22T04:03:34.027Z" },
+ { url = "https://files.pythonhosted.org/packages/92/aa/df863bcc39c5e0946263454aba394de8a9084dbaff8ad143846b0d844739/lxml-6.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:bb4c1847b303835d89d785a18801a883436cdfd5dc3d62947f9c49e24f0f5a2c", size = 3822205, upload-time = "2025-09-22T04:03:36.249Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/9c/780c9a8fce3f04690b374f72f41306866b0400b9d0fdf3e17aaa37887eed/lxml-6.0.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e748d4cf8fef2526bb2a589a417eba0c8674e29ffcb570ce2ceca44f1e567bf6", size = 3939264, upload-time = "2025-09-22T04:04:32.892Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/5a/1ab260c00adf645d8bf7dec7f920f744b032f69130c681302821d5debea6/lxml-6.0.2-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4ddb1049fa0579d0cbd00503ad8c58b9ab34d1254c77bc6a5576d96ec7853dba", size = 4216435, upload-time = "2025-09-22T04:04:34.907Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/37/565f3b3d7ffede22874b6d86be1a1763d00f4ea9fc5b9b6ccb11e4ec8612/lxml-6.0.2-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cb233f9c95f83707dae461b12b720c1af9c28c2d19208e1be03387222151daf5", size = 4325913, upload-time = "2025-09-22T04:04:37.205Z" },
+ { url = "https://files.pythonhosted.org/packages/22/ec/f3a1b169b2fb9d03467e2e3c0c752ea30e993be440a068b125fc7dd248b0/lxml-6.0.2-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc456d04db0515ce3320d714a1eac7a97774ff0849e7718b492d957da4631dd4", size = 4269357, upload-time = "2025-09-22T04:04:39.322Z" },
+ { url = "https://files.pythonhosted.org/packages/77/a2/585a28fe3e67daa1cf2f06f34490d556d121c25d500b10082a7db96e3bcd/lxml-6.0.2-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2613e67de13d619fd283d58bda40bff0ee07739f624ffee8b13b631abf33083d", size = 4412295, upload-time = "2025-09-22T04:04:41.647Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/d9/a57dd8bcebd7c69386c20263830d4fa72d27e6b72a229ef7a48e88952d9a/lxml-6.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:24a8e756c982c001ca8d59e87c80c4d9dcd4d9b44a4cbeb8d9be4482c514d41d", size = 3516913, upload-time = "2025-09-22T04:04:43.602Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/11/29d08bc103a62c0eba8016e7ed5aeebbf1e4312e83b0b1648dd203b0e87d/lxml-6.0.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1c06035eafa8404b5cf475bb37a9f6088b0aca288d4ccc9d69389750d5543700", size = 3949829, upload-time = "2025-09-22T04:04:45.608Z" },
+ { url = "https://files.pythonhosted.org/packages/12/b3/52ab9a3b31e5ab8238da241baa19eec44d2ab426532441ee607165aebb52/lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c7d13103045de1bdd6fe5d61802565f1a3537d70cd3abf596aa0af62761921ee", size = 4226277, upload-time = "2025-09-22T04:04:47.754Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/33/1eaf780c1baad88224611df13b1c2a9dfa460b526cacfe769103ff50d845/lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a3c150a95fbe5ac91de323aa756219ef9cf7fde5a3f00e2281e30f33fa5fa4f", size = 4330433, upload-time = "2025-09-22T04:04:49.907Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/c1/27428a2ff348e994ab4f8777d3a0ad510b6b92d37718e5887d2da99952a2/lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60fa43be34f78bebb27812ed90f1925ec99560b0fa1decdb7d12b84d857d31e9", size = 4272119, upload-time = "2025-09-22T04:04:51.801Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/d0/3020fa12bcec4ab62f97aab026d57c2f0cfd480a558758d9ca233bb6a79d/lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21c73b476d3cfe836be731225ec3421fa2f048d84f6df6a8e70433dff1376d5a", size = 4417314, upload-time = "2025-09-22T04:04:55.024Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/77/d7f491cbc05303ac6801651aabeb262d43f319288c1ea96c66b1d2692ff3/lxml-6.0.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:27220da5be049e936c3aca06f174e8827ca6445a4353a1995584311487fc4e3e", size = 3518768, upload-time = "2025-09-22T04:04:57.097Z" },
+]
+
+[[package]]
+name = "markdown-it-py"
+version = "4.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "mdurl" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
+]
+
+[[package]]
+name = "mcp"
+version = "1.26.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "httpx" },
+ { name = "httpx-sse" },
+ { name = "jsonschema" },
+ { name = "pydantic" },
+ { name = "pydantic-settings" },
+ { name = "pyjwt", extra = ["crypto"] },
+ { name = "python-multipart" },
+ { name = "pywin32", marker = "sys_platform == 'win32'" },
+ { name = "sse-starlette" },
+ { name = "starlette" },
+ { name = "typing-extensions" },
+ { name = "typing-inspection" },
+ { name = "uvicorn", marker = "sys_platform != 'emscripten'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/fc/6d/62e76bbb8144d6ed86e202b5edd8a4cb631e7c8130f3f4893c3f90262b10/mcp-1.26.0.tar.gz", hash = "sha256:db6e2ef491eecc1a0d93711a76f28dec2e05999f93afd48795da1c1137142c66", size = 608005, upload-time = "2026-01-24T19:40:32.468Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fd/d9/eaa1f80170d2b7c5ba23f3b59f766f3a0bb41155fbc32a69adfa1adaaef9/mcp-1.26.0-py3-none-any.whl", hash = "sha256:904a21c33c25aa98ddbeb47273033c435e595bbacfdb177f4bd87f6dceebe1ca", size = 233615, upload-time = "2026-01-24T19:40:30.652Z" },
+]
+
+[package.optional-dependencies]
+cli = [
+ { name = "python-dotenv" },
+ { name = "typer" },
+]
+
+[[package]]
+name = "mdurl"
+version = "0.1.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
+]
+
+[[package]]
+name = "office-powerpoint-mcp-server"
+version = "2.0.7"
+source = { editable = "." }
+dependencies = [
+ { name = "fonttools" },
+ { name = "mcp", extra = ["cli"] },
+ { name = "pillow" },
+ { name = "python-pptx" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "fonttools", specifier = ">=4.0.0" },
+ { name = "mcp", extras = ["cli"], specifier = ">=1.8.0" },
+ { name = "pillow", specifier = ">=8.0.0" },
+ { name = "python-pptx", specifier = ">=0.6.21" },
+]
+
+[[package]]
+name = "pillow"
+version = "12.1.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1f/42/5c74462b4fd957fcd7b13b04fb3205ff8349236ea74c7c375766d6c82288/pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4", size = 46980264, upload-time = "2026-02-11T04:23:07.146Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1d/30/5bd3d794762481f8c8ae9c80e7b76ecea73b916959eb587521358ef0b2f9/pillow-12.1.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1f1625b72740fdda5d77b4def688eb8fd6490975d06b909fd19f13f391e077e0", size = 5304099, upload-time = "2026-02-11T04:20:06.13Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/c1/aab9e8f3eeb4490180e357955e15c2ef74b31f64790ff356c06fb6cf6d84/pillow-12.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:178aa072084bd88ec759052feca8e56cbb14a60b39322b99a049e58090479713", size = 4657880, upload-time = "2026-02-11T04:20:09.291Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/0a/9879e30d56815ad529d3985aeff5af4964202425c27261a6ada10f7cbf53/pillow-12.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b66e95d05ba806247aaa1561f080abc7975daf715c30780ff92a20e4ec546e1b", size = 6222587, upload-time = "2026-02-11T04:20:10.82Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/5f/a1b72ff7139e4f89014e8d451442c74a774d5c43cd938fb0a9f878576b37/pillow-12.1.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:89c7e895002bbe49cdc5426150377cbbc04767d7547ed145473f496dfa40408b", size = 8027678, upload-time = "2026-02-11T04:20:12.455Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/c2/c7cb187dac79a3d22c3ebeae727abee01e077c8c7d930791dc592f335153/pillow-12.1.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a5cbdcddad0af3da87cb16b60d23648bc3b51967eb07223e9fed77a82b457c4", size = 6335777, upload-time = "2026-02-11T04:20:14.441Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/7b/f9b09a7804ec7336effb96c26d37c29d27225783dc1501b7d62dcef6ae25/pillow-12.1.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9f51079765661884a486727f0729d29054242f74b46186026582b4e4769918e4", size = 7027140, upload-time = "2026-02-11T04:20:16.387Z" },
+ { url = "https://files.pythonhosted.org/packages/98/b2/2fa3c391550bd421b10849d1a2144c44abcd966daadd2f7c12e19ea988c4/pillow-12.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:99c1506ea77c11531d75e3a412832a13a71c7ebc8192ab9e4b2e355555920e3e", size = 6449855, upload-time = "2026-02-11T04:20:18.554Z" },
+ { url = "https://files.pythonhosted.org/packages/96/ff/9caf4b5b950c669263c39e96c78c0d74a342c71c4f43fd031bb5cb7ceac9/pillow-12.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:36341d06738a9f66c8287cf8b876d24b18db9bd8740fa0672c74e259ad408cff", size = 7151329, upload-time = "2026-02-11T04:20:20.646Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/f8/4b24841f582704da675ca535935bccb32b00a6da1226820845fac4a71136/pillow-12.1.1-cp310-cp310-win32.whl", hash = "sha256:6c52f062424c523d6c4db85518774cc3d50f5539dd6eed32b8f6229b26f24d40", size = 6325574, upload-time = "2026-02-11T04:20:22.43Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/f9/9f6b01c0881d7036063aa6612ef04c0e2cad96be21325a1e92d0203f8e91/pillow-12.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:c6008de247150668a705a6338156efb92334113421ceecf7438a12c9a12dab23", size = 7032347, upload-time = "2026-02-11T04:20:23.932Z" },
+ { url = "https://files.pythonhosted.org/packages/79/13/c7922edded3dcdaf10c59297540b72785620abc0538872c819915746757d/pillow-12.1.1-cp310-cp310-win_arm64.whl", hash = "sha256:1a9b0ee305220b392e1124a764ee4265bd063e54a751a6b62eff69992f457fa9", size = 2453457, upload-time = "2026-02-11T04:20:25.392Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/46/5da1ec4a5171ee7bf1a0efa064aba70ba3d6e0788ce3f5acd1375d23c8c0/pillow-12.1.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e879bb6cd5c73848ef3b2b48b8af9ff08c5b71ecda8048b7dd22d8a33f60be32", size = 5304084, upload-time = "2026-02-11T04:20:27.501Z" },
+ { url = "https://files.pythonhosted.org/packages/78/93/a29e9bc02d1cf557a834da780ceccd54e02421627200696fcf805ebdc3fb/pillow-12.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:365b10bb9417dd4498c0e3b128018c4a624dc11c7b97d8cc54effe3b096f4c38", size = 4657866, upload-time = "2026-02-11T04:20:29.827Z" },
+ { url = "https://files.pythonhosted.org/packages/13/84/583a4558d492a179d31e4aae32eadce94b9acf49c0337c4ce0b70e0a01f2/pillow-12.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d4ce8e329c93845720cd2014659ca67eac35f6433fd3050393d85f3ecef0dad5", size = 6232148, upload-time = "2026-02-11T04:20:31.329Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/e2/53c43334bbbb2d3b938978532fbda8e62bb6e0b23a26ce8592f36bcc4987/pillow-12.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc354a04072b765eccf2204f588a7a532c9511e8b9c7f900e1b64e3e33487090", size = 8038007, upload-time = "2026-02-11T04:20:34.225Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/a6/3d0e79c8a9d58150dd98e199d7c1c56861027f3829a3a60b3c2784190180/pillow-12.1.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e7976bf1910a8116b523b9f9f58bf410f3e8aa330cd9a2bb2953f9266ab49af", size = 6345418, upload-time = "2026-02-11T04:20:35.858Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/c8/46dfeac5825e600579157eea177be43e2f7ff4a99da9d0d0a49533509ac5/pillow-12.1.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:597bd9c8419bc7c6af5604e55847789b69123bbe25d65cc6ad3012b4f3c98d8b", size = 7034590, upload-time = "2026-02-11T04:20:37.91Z" },
+ { url = "https://files.pythonhosted.org/packages/af/bf/e6f65d3db8a8bbfeaf9e13cc0417813f6319863a73de934f14b2229ada18/pillow-12.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c1fc0f2ca5f96a3c8407e41cca26a16e46b21060fe6d5b099d2cb01412222f5", size = 6458655, upload-time = "2026-02-11T04:20:39.496Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/c2/66091f3f34a25894ca129362e510b956ef26f8fb67a0e6417bc5744e56f1/pillow-12.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:578510d88c6229d735855e1f278aa305270438d36a05031dfaae5067cc8eb04d", size = 7159286, upload-time = "2026-02-11T04:20:41.139Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/5a/24bc8eb526a22f957d0cec6243146744966d40857e3d8deb68f7902ca6c1/pillow-12.1.1-cp311-cp311-win32.whl", hash = "sha256:7311c0a0dcadb89b36b7025dfd8326ecfa36964e29913074d47382706e516a7c", size = 6328663, upload-time = "2026-02-11T04:20:43.184Z" },
+ { url = "https://files.pythonhosted.org/packages/31/03/bef822e4f2d8f9d7448c133d0a18185d3cce3e70472774fffefe8b0ed562/pillow-12.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:fbfa2a7c10cc2623f412753cddf391c7f971c52ca40a3f65dc5039b2939e8563", size = 7031448, upload-time = "2026-02-11T04:20:44.696Z" },
+ { url = "https://files.pythonhosted.org/packages/49/70/f76296f53610bd17b2e7d31728b8b7825e3ac3b5b3688b51f52eab7c0818/pillow-12.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:b81b5e3511211631b3f672a595e3221252c90af017e399056d0faabb9538aa80", size = 2453651, upload-time = "2026-02-11T04:20:46.243Z" },
+ { url = "https://files.pythonhosted.org/packages/07/d3/8df65da0d4df36b094351dce696f2989bec731d4f10e743b1c5f4da4d3bf/pillow-12.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052", size = 5262803, upload-time = "2026-02-11T04:20:47.653Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/71/5026395b290ff404b836e636f51d7297e6c83beceaa87c592718747e670f/pillow-12.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984", size = 4657601, upload-time = "2026-02-11T04:20:49.328Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/2e/1001613d941c67442f745aff0f7cc66dd8df9a9c084eb497e6a543ee6f7e/pillow-12.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79", size = 6234995, upload-time = "2026-02-11T04:20:51.032Z" },
+ { url = "https://files.pythonhosted.org/packages/07/26/246ab11455b2549b9233dbd44d358d033a2f780fa9007b61a913c5b2d24e/pillow-12.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293", size = 8045012, upload-time = "2026-02-11T04:20:52.882Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/8b/07587069c27be7535ac1fe33874e32de118fbd34e2a73b7f83436a88368c/pillow-12.1.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397", size = 6349638, upload-time = "2026-02-11T04:20:54.444Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/79/6df7b2ee763d619cda2fb4fea498e5f79d984dae304d45a8999b80d6cf5c/pillow-12.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0", size = 7041540, upload-time = "2026-02-11T04:20:55.97Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/5e/2ba19e7e7236d7529f4d873bdaf317a318896bac289abebd4bb00ef247f0/pillow-12.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3", size = 6462613, upload-time = "2026-02-11T04:20:57.542Z" },
+ { url = "https://files.pythonhosted.org/packages/03/03/31216ec124bb5c3dacd74ce8efff4cc7f52643653bad4825f8f08c697743/pillow-12.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35", size = 7166745, upload-time = "2026-02-11T04:20:59.196Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/e7/7c4552d80052337eb28653b617eafdef39adfb137c49dd7e831b8dc13bc5/pillow-12.1.1-cp312-cp312-win32.whl", hash = "sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a", size = 6328823, upload-time = "2026-02-11T04:21:01.385Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/17/688626d192d7261bbbf98846fc98995726bddc2c945344b65bec3a29d731/pillow-12.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6", size = 7033367, upload-time = "2026-02-11T04:21:03.536Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/fe/a0ef1f73f939b0eca03ee2c108d0043a87468664770612602c63266a43c4/pillow-12.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523", size = 2453811, upload-time = "2026-02-11T04:21:05.116Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/11/6db24d4bd7685583caeae54b7009584e38da3c3d4488ed4cd25b439de486/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e", size = 4062689, upload-time = "2026-02-11T04:21:06.804Z" },
+ { url = "https://files.pythonhosted.org/packages/33/c0/ce6d3b1fe190f0021203e0d9b5b99e57843e345f15f9ef22fcd43842fd21/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9", size = 4138535, upload-time = "2026-02-11T04:21:08.452Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/c6/d5eb6a4fb32a3f9c21a8c7613ec706534ea1cf9f4b3663e99f0d83f6fca8/pillow-12.1.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6", size = 3601364, upload-time = "2026-02-11T04:21:10.194Z" },
+ { url = "https://files.pythonhosted.org/packages/14/a1/16c4b823838ba4c9c52c0e6bbda903a3fe5a1bdbf1b8eb4fff7156f3e318/pillow-12.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c6db3b84c87d48d0088943bf33440e0c42370b99b1c2a7989216f7b42eede60", size = 5262561, upload-time = "2026-02-11T04:21:11.742Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/ad/ad9dc98ff24f485008aa5cdedaf1a219876f6f6c42a4626c08bc4e80b120/pillow-12.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b7e5304e34942bf62e15184219a7b5ad4ff7f3bb5cca4d984f37df1a0e1aee2", size = 4657460, upload-time = "2026-02-11T04:21:13.786Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/1b/f1a4ea9a895b5732152789326202a82464d5254759fbacae4deea3069334/pillow-12.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5bddd742a44b7e6b1e773ab5db102bd7a94c32555ba656e76d319d19c3850", size = 6232698, upload-time = "2026-02-11T04:21:15.949Z" },
+ { url = "https://files.pythonhosted.org/packages/95/f4/86f51b8745070daf21fd2e5b1fe0eb35d4db9ca26e6d58366562fb56a743/pillow-12.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc44ef1f3de4f45b50ccf9136999d71abb99dca7706bc75d222ed350b9fd2289", size = 8041706, upload-time = "2026-02-11T04:21:17.723Z" },
+ { url = "https://files.pythonhosted.org/packages/29/9b/d6ecd956bb1266dd1045e995cce9b8d77759e740953a1c9aad9502a0461e/pillow-12.1.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a8eb7ed8d4198bccbd07058416eeec51686b498e784eda166395a23eb99138e", size = 6346621, upload-time = "2026-02-11T04:21:19.547Z" },
+ { url = "https://files.pythonhosted.org/packages/71/24/538bff45bde96535d7d998c6fed1a751c75ac7c53c37c90dc2601b243893/pillow-12.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47b94983da0c642de92ced1702c5b6c292a84bd3a8e1d1702ff923f183594717", size = 7038069, upload-time = "2026-02-11T04:21:21.378Z" },
+ { url = "https://files.pythonhosted.org/packages/94/0e/58cb1a6bc48f746bc4cb3adb8cabff73e2742c92b3bf7a220b7cf69b9177/pillow-12.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:518a48c2aab7ce596d3bf79d0e275661b846e86e4d0e7dec34712c30fe07f02a", size = 6460040, upload-time = "2026-02-11T04:21:23.148Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/57/9045cb3ff11eeb6c1adce3b2d60d7d299d7b273a2e6c8381a524abfdc474/pillow-12.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a550ae29b95c6dc13cf69e2c9dc5747f814c54eeb2e32d683e5e93af56caa029", size = 7164523, upload-time = "2026-02-11T04:21:25.01Z" },
+ { url = "https://files.pythonhosted.org/packages/73/f2/9be9cb99f2175f0d4dbadd6616ce1bf068ee54a28277ea1bf1fbf729c250/pillow-12.1.1-cp313-cp313-win32.whl", hash = "sha256:a003d7422449f6d1e3a34e3dd4110c22148336918ddbfc6a32581cd54b2e0b2b", size = 6332552, upload-time = "2026-02-11T04:21:27.238Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/eb/b0834ad8b583d7d9d42b80becff092082a1c3c156bb582590fcc973f1c7c/pillow-12.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:344cf1e3dab3be4b1fa08e449323d98a2a3f819ad20f4b22e77a0ede31f0faa1", size = 7040108, upload-time = "2026-02-11T04:21:29.462Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/7d/fc09634e2aabdd0feabaff4a32f4a7d97789223e7c2042fd805ea4b4d2c2/pillow-12.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c0dd1636633e7e6a0afe7bf6a51a14992b7f8e60de5789018ebbdfae55b040a", size = 2453712, upload-time = "2026-02-11T04:21:31.072Z" },
+ { url = "https://files.pythonhosted.org/packages/19/2a/b9d62794fc8a0dd14c1943df68347badbd5511103e0d04c035ffe5cf2255/pillow-12.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0330d233c1a0ead844fc097a7d16c0abff4c12e856c0b325f231820fee1f39da", size = 5264880, upload-time = "2026-02-11T04:21:32.865Z" },
+ { url = "https://files.pythonhosted.org/packages/26/9d/e03d857d1347fa5ed9247e123fcd2a97b6220e15e9cb73ca0a8d91702c6e/pillow-12.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dae5f21afb91322f2ff791895ddd8889e5e947ff59f71b46041c8ce6db790bc", size = 4660616, upload-time = "2026-02-11T04:21:34.97Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/ec/8a6d22afd02570d30954e043f09c32772bfe143ba9285e2fdb11284952cd/pillow-12.1.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e0c664be47252947d870ac0d327fea7e63985a08794758aa8af5b6cb6ec0c9c", size = 6269008, upload-time = "2026-02-11T04:21:36.623Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/1d/6d875422c9f28a4a361f495a5f68d9de4a66941dc2c619103ca335fa6446/pillow-12.1.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:691ab2ac363b8217f7d31b3497108fb1f50faab2f75dfb03284ec2f217e87bf8", size = 8073226, upload-time = "2026-02-11T04:21:38.585Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/cd/134b0b6ee5eda6dc09e25e24b40fdafe11a520bc725c1d0bbaa5e00bf95b/pillow-12.1.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9e8064fb1cc019296958595f6db671fba95209e3ceb0c4734c9baf97de04b20", size = 6380136, upload-time = "2026-02-11T04:21:40.562Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/a9/7628f013f18f001c1b98d8fffe3452f306a70dc6aba7d931019e0492f45e/pillow-12.1.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:472a8d7ded663e6162dafdf20015c486a7009483ca671cece7a9279b512fcb13", size = 7067129, upload-time = "2026-02-11T04:21:42.521Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/f8/66ab30a2193b277785601e82ee2d49f68ea575d9637e5e234faaa98efa4c/pillow-12.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:89b54027a766529136a06cfebeecb3a04900397a3590fd252160b888479517bf", size = 6491807, upload-time = "2026-02-11T04:21:44.22Z" },
+ { url = "https://files.pythonhosted.org/packages/da/0b/a877a6627dc8318fdb84e357c5e1a758c0941ab1ddffdafd231983788579/pillow-12.1.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:86172b0831b82ce4f7877f280055892b31179e1576aa00d0df3bb1bbf8c3e524", size = 7190954, upload-time = "2026-02-11T04:21:46.114Z" },
+ { url = "https://files.pythonhosted.org/packages/83/43/6f732ff85743cf746b1361b91665d9f5155e1483817f693f8d57ea93147f/pillow-12.1.1-cp313-cp313t-win32.whl", hash = "sha256:44ce27545b6efcf0fdbdceb31c9a5bdea9333e664cda58a7e674bb74608b3986", size = 6336441, upload-time = "2026-02-11T04:21:48.22Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/44/e865ef3986611bb75bfabdf94a590016ea327833f434558801122979cd0e/pillow-12.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a285e3eb7a5a45a2ff504e31f4a8d1b12ef62e84e5411c6804a42197c1cf586c", size = 7045383, upload-time = "2026-02-11T04:21:50.015Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/c6/f4fb24268d0c6908b9f04143697ea18b0379490cb74ba9e8d41b898bd005/pillow-12.1.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cc7d296b5ea4d29e6570dabeaed58d31c3fea35a633a69679fb03d7664f43fb3", size = 2456104, upload-time = "2026-02-11T04:21:51.633Z" },
+ { url = "https://files.pythonhosted.org/packages/03/d0/bebb3ffbf31c5a8e97241476c4cf8b9828954693ce6744b4a2326af3e16b/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:417423db963cb4be8bac3fc1204fe61610f6abeed1580a7a2cbb2fbda20f12af", size = 4062652, upload-time = "2026-02-11T04:21:53.19Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/c0/0e16fb0addda4851445c28f8350d8c512f09de27bbb0d6d0bbf8b6709605/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b957b71c6b2387610f556a7eb0828afbe40b4a98036fc0d2acfa5a44a0c2036f", size = 4138823, upload-time = "2026-02-11T04:22:03.088Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/fb/6170ec655d6f6bb6630a013dd7cf7bc218423d7b5fa9071bf63dc32175ae/pillow-12.1.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:097690ba1f2efdeb165a20469d59d8bb03c55fb6621eb2041a060ae8ea3e9642", size = 3601143, upload-time = "2026-02-11T04:22:04.909Z" },
+ { url = "https://files.pythonhosted.org/packages/59/04/dc5c3f297510ba9a6837cbb318b87dd2b8f73eb41a43cc63767f65cb599c/pillow-12.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2815a87ab27848db0321fb78c7f0b2c8649dee134b7f2b80c6a45c6831d75ccd", size = 5266254, upload-time = "2026-02-11T04:22:07.656Z" },
+ { url = "https://files.pythonhosted.org/packages/05/30/5db1236b0d6313f03ebf97f5e17cda9ca060f524b2fcc875149a8360b21c/pillow-12.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f7ed2c6543bad5a7d5530eb9e78c53132f93dfa44a28492db88b41cdab885202", size = 4657499, upload-time = "2026-02-11T04:22:09.613Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/18/008d2ca0eb612e81968e8be0bbae5051efba24d52debf930126d7eaacbba/pillow-12.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:652a2c9ccfb556235b2b501a3a7cf3742148cd22e04b5625c5fe057ea3e3191f", size = 6232137, upload-time = "2026-02-11T04:22:11.434Z" },
+ { url = "https://files.pythonhosted.org/packages/70/f1/f14d5b8eeb4b2cd62b9f9f847eb6605f103df89ef619ac68f92f748614ea/pillow-12.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6e4571eedf43af33d0fc233a382a76e849badbccdf1ac438841308652a08e1f", size = 8042721, upload-time = "2026-02-11T04:22:13.321Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/d6/17824509146e4babbdabf04d8171491fa9d776f7061ff6e727522df9bd03/pillow-12.1.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b574c51cf7d5d62e9be37ba446224b59a2da26dc4c1bb2ecbe936a4fb1a7cb7f", size = 6347798, upload-time = "2026-02-11T04:22:15.449Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/ee/c85a38a9ab92037a75615aba572c85ea51e605265036e00c5b67dfafbfe2/pillow-12.1.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a37691702ed687799de29a518d63d4682d9016932db66d4e90c345831b02fb4e", size = 7039315, upload-time = "2026-02-11T04:22:17.24Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/f3/bc8ccc6e08a148290d7523bde4d9a0d6c981db34631390dc6e6ec34cacf6/pillow-12.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f95c00d5d6700b2b890479664a06e754974848afaae5e21beb4d83c106923fd0", size = 6462360, upload-time = "2026-02-11T04:22:19.111Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/ab/69a42656adb1d0665ab051eec58a41f169ad295cf81ad45406963105408f/pillow-12.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:559b38da23606e68681337ad74622c4dbba02254fc9cb4488a305dd5975c7eeb", size = 7165438, upload-time = "2026-02-11T04:22:21.041Z" },
+ { url = "https://files.pythonhosted.org/packages/02/46/81f7aa8941873f0f01d4b55cc543b0a3d03ec2ee30d617a0448bf6bd6dec/pillow-12.1.1-cp314-cp314-win32.whl", hash = "sha256:03edcc34d688572014ff223c125a3f77fb08091e4607e7745002fc214070b35f", size = 6431503, upload-time = "2026-02-11T04:22:22.833Z" },
+ { url = "https://files.pythonhosted.org/packages/40/72/4c245f7d1044b67affc7f134a09ea619d4895333d35322b775b928180044/pillow-12.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:50480dcd74fa63b8e78235957d302d98d98d82ccbfac4c7e12108ba9ecbdba15", size = 7176748, upload-time = "2026-02-11T04:22:24.64Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/ad/8a87bdbe038c5c698736e3348af5c2194ffb872ea52f11894c95f9305435/pillow-12.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:5cb1785d97b0c3d1d1a16bc1d710c4a0049daefc4935f3a8f31f827f4d3d2e7f", size = 2544314, upload-time = "2026-02-11T04:22:26.685Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/9d/efd18493f9de13b87ede7c47e69184b9e859e4427225ea962e32e56a49bc/pillow-12.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1f90cff8aa76835cba5769f0b3121a22bd4eb9e6884cfe338216e557a9a548b8", size = 5268612, upload-time = "2026-02-11T04:22:29.884Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/f1/4f42eb2b388eb2ffc660dcb7f7b556c1015c53ebd5f7f754965ef997585b/pillow-12.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f1be78ce9466a7ee64bfda57bdba0f7cc499d9794d518b854816c41bf0aa4e9", size = 4660567, upload-time = "2026-02-11T04:22:31.799Z" },
+ { url = "https://files.pythonhosted.org/packages/01/54/df6ef130fa43e4b82e32624a7b821a2be1c5653a5fdad8469687a7db4e00/pillow-12.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:42fc1f4677106188ad9a55562bbade416f8b55456f522430fadab3cef7cd4e60", size = 6269951, upload-time = "2026-02-11T04:22:33.921Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/48/618752d06cc44bb4aae8ce0cd4e6426871929ed7b46215638088270d9b34/pillow-12.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98edb152429ab62a1818039744d8fbb3ccab98a7c29fc3d5fcef158f3f1f68b7", size = 8074769, upload-time = "2026-02-11T04:22:35.877Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/bd/f1d71eb39a72fa088d938655afba3e00b38018d052752f435838961127d8/pillow-12.1.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d470ab1178551dd17fdba0fef463359c41aaa613cdcd7ff8373f54be629f9f8f", size = 6381358, upload-time = "2026-02-11T04:22:37.698Z" },
+ { url = "https://files.pythonhosted.org/packages/64/ef/c784e20b96674ed36a5af839305f55616f8b4f8aa8eeccf8531a6e312243/pillow-12.1.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6408a7b064595afcab0a49393a413732a35788f2a5092fdc6266952ed67de586", size = 7068558, upload-time = "2026-02-11T04:22:39.597Z" },
+ { url = "https://files.pythonhosted.org/packages/73/cb/8059688b74422ae61278202c4e1ad992e8a2e7375227be0a21c6b87ca8d5/pillow-12.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5d8c41325b382c07799a3682c1c258469ea2ff97103c53717b7893862d0c98ce", size = 6493028, upload-time = "2026-02-11T04:22:42.73Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/da/e3c008ed7d2dd1f905b15949325934510b9d1931e5df999bb15972756818/pillow-12.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7697918b5be27424e9ce568193efd13d925c4481dd364e43f5dff72d33e10f8", size = 7191940, upload-time = "2026-02-11T04:22:44.543Z" },
+ { url = "https://files.pythonhosted.org/packages/01/4a/9202e8d11714c1fc5951f2e1ef362f2d7fbc595e1f6717971d5dd750e969/pillow-12.1.1-cp314-cp314t-win32.whl", hash = "sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36", size = 6438736, upload-time = "2026-02-11T04:22:46.347Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/ca/cbce2327eb9885476b3957b2e82eb12c866a8b16ad77392864ad601022ce/pillow-12.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b", size = 7182894, upload-time = "2026-02-11T04:22:48.114Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/d2/de599c95ba0a973b94410477f8bf0b6f0b5e67360eb89bcb1ad365258beb/pillow-12.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334", size = 2546446, upload-time = "2026-02-11T04:22:50.342Z" },
+ { url = "https://files.pythonhosted.org/packages/56/11/5d43209aa4cb58e0cc80127956ff1796a68b928e6324bbf06ef4db34367b/pillow-12.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:600fd103672b925fe62ed08e0d874ea34d692474df6f4bf7ebe148b30f89f39f", size = 5228606, upload-time = "2026-02-11T04:22:52.106Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/d5/3b005b4e4fda6698b371fa6c21b097d4707585d7db99e98d9b0b87ac612a/pillow-12.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:665e1b916b043cef294bc54d47bf02d87e13f769bc4bc5fa225a24b3a6c5aca9", size = 4622321, upload-time = "2026-02-11T04:22:53.827Z" },
+ { url = "https://files.pythonhosted.org/packages/df/36/ed3ea2d594356fd8037e5a01f6156c74bc8d92dbb0fa60746cc96cabb6e8/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:495c302af3aad1ca67420ddd5c7bd480c8867ad173528767d906428057a11f0e", size = 5247579, upload-time = "2026-02-11T04:22:56.094Z" },
+ { url = "https://files.pythonhosted.org/packages/54/9a/9cc3e029683cf6d20ae5085da0dafc63148e3252c2f13328e553aaa13cfb/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8fd420ef0c52c88b5a035a0886f367748c72147b2b8f384c9d12656678dfdfa9", size = 6989094, upload-time = "2026-02-11T04:22:58.288Z" },
+ { url = "https://files.pythonhosted.org/packages/00/98/fc53ab36da80b88df0967896b6c4b4cd948a0dc5aa40a754266aa3ae48b3/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f975aa7ef9684ce7e2c18a3aa8f8e2106ce1e46b94ab713d156b2898811651d3", size = 5313850, upload-time = "2026-02-11T04:23:00.554Z" },
+ { url = "https://files.pythonhosted.org/packages/30/02/00fa585abfd9fe9d73e5f6e554dc36cc2b842898cbfc46d70353dae227f8/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8089c852a56c2966cf18835db62d9b34fef7ba74c726ad943928d494fa7f4735", size = 5963343, upload-time = "2026-02-11T04:23:02.934Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/26/c56ce33ca856e358d27fda9676c055395abddb82c35ac0f593877ed4562e/pillow-12.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:cb9bb857b2d057c6dfc72ac5f3b44836924ba15721882ef103cecb40d002d80e", size = 7029880, upload-time = "2026-02-11T04:23:04.783Z" },
+]
+
+[[package]]
+name = "pycparser"
+version = "3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" },
+]
+
+[[package]]
+name = "pydantic"
+version = "2.12.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "annotated-types" },
+ { name = "pydantic-core" },
+ { name = "typing-extensions" },
+ { name = "typing-inspection" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" },
+]
+
+[[package]]
+name = "pydantic-core"
+version = "2.41.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" },
+ { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" },
+ { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" },
+ { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" },
+ { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" },
+ { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" },
+ { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" },
+ { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" },
+ { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" },
+ { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" },
+ { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" },
+ { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" },
+ { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" },
+ { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" },
+ { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" },
+ { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" },
+ { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" },
+ { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" },
+ { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" },
+ { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" },
+ { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" },
+ { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" },
+ { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" },
+ { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" },
+ { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" },
+ { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" },
+ { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" },
+ { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" },
+ { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" },
+ { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" },
+ { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" },
+ { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" },
+ { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" },
+ { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" },
+ { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" },
+ { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" },
+ { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" },
+ { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" },
+ { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" },
+ { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" },
+ { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" },
+ { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" },
+ { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" },
+]
+
+[[package]]
+name = "pydantic-settings"
+version = "2.13.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pydantic" },
+ { name = "python-dotenv" },
+ { name = "typing-inspection" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/52/6d/fffca34caecc4a3f97bda81b2098da5e8ab7efc9a66e819074a11955d87e/pydantic_settings-2.13.1.tar.gz", hash = "sha256:b4c11847b15237fb0171e1462bf540e294affb9b86db4d9aa5c01730bdbe4025", size = 223826, upload-time = "2026-02-19T13:45:08.055Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/00/4b/ccc026168948fec4f7555b9164c724cf4125eac006e176541483d2c959be/pydantic_settings-2.13.1-py3-none-any.whl", hash = "sha256:d56fd801823dbeae7f0975e1f8c8e25c258eb75d278ea7abb5d9cebb01b56237", size = 58929, upload-time = "2026-02-19T13:45:06.034Z" },
+]
+
+[[package]]
+name = "pygments"
+version = "2.19.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
+]
+
+[[package]]
+name = "pyjwt"
+version = "2.11.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/5c/5a/b46fa56bf322901eee5b0454a34343cdbdae202cd421775a8ee4e42fd519/pyjwt-2.11.0.tar.gz", hash = "sha256:35f95c1f0fbe5d5ba6e43f00271c275f7a1a4db1dab27bf708073b75318ea623", size = 98019, upload-time = "2026-01-30T19:59:55.694Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6f/01/c26ce75ba460d5cd503da9e13b21a33804d38c2165dec7b716d06b13010c/pyjwt-2.11.0-py3-none-any.whl", hash = "sha256:94a6bde30eb5c8e04fee991062b534071fd1439ef58d2adc9ccb823e7bcd0469", size = 28224, upload-time = "2026-01-30T19:59:54.539Z" },
+]
+
+[package.optional-dependencies]
+crypto = [
+ { name = "cryptography" },
+]
+
+[[package]]
+name = "python-dotenv"
+version = "1.2.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" },
+]
+
+[[package]]
+name = "python-multipart"
+version = "0.0.22"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" },
+]
+
+[[package]]
+name = "python-pptx"
+version = "1.0.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "lxml" },
+ { name = "pillow" },
+ { name = "typing-extensions" },
+ { name = "xlsxwriter" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/52/a9/0c0db8d37b2b8a645666f7fd8accea4c6224e013c42b1d5c17c93590cd06/python_pptx-1.0.2.tar.gz", hash = "sha256:479a8af0eaf0f0d76b6f00b0887732874ad2e3188230315290cd1f9dd9cc7095", size = 10109297, upload-time = "2024-08-07T17:33:37.772Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d9/4f/00be2196329ebbff56ce564aa94efb0fbc828d00de250b1980de1a34ab49/python_pptx-1.0.2-py3-none-any.whl", hash = "sha256:160838e0b8565a8b1f67947675886e9fea18aa5e795db7ae531606d68e785cba", size = 472788, upload-time = "2024-08-07T17:33:28.192Z" },
+]
+
+[[package]]
+name = "pywin32"
+version = "311"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7b/40/44efbb0dfbd33aca6a6483191dae0716070ed99e2ecb0c53683f400a0b4f/pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3", size = 8760432, upload-time = "2025-07-14T20:13:05.9Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/bf/360243b1e953bd254a82f12653974be395ba880e7ec23e3731d9f73921cc/pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b", size = 9590103, upload-time = "2025-07-14T20:13:07.698Z" },
+ { url = "https://files.pythonhosted.org/packages/57/38/d290720e6f138086fb3d5ffe0b6caa019a791dd57866940c82e4eeaf2012/pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b", size = 8778557, upload-time = "2025-07-14T20:13:11.11Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" },
+ { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" },
+ { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" },
+ { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" },
+ { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" },
+]
+
+[[package]]
+name = "referencing"
+version = "0.37.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "attrs" },
+ { name = "rpds-py" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" },
+]
+
+[[package]]
+name = "rich"
+version = "14.3.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markdown-it-py" },
+ { name = "pygments" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" },
+]
+
+[[package]]
+name = "rpds-py"
+version = "0.30.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/06/0c/0c411a0ec64ccb6d104dcabe0e713e05e153a9a2c3c2bd2b32ce412166fe/rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288", size = 370490, upload-time = "2025-11-30T20:21:33.256Z" },
+ { url = "https://files.pythonhosted.org/packages/19/6a/4ba3d0fb7297ebae71171822554abe48d7cab29c28b8f9f2c04b79988c05/rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00", size = 359751, upload-time = "2025-11-30T20:21:34.591Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/7c/e4933565ef7f7a0818985d87c15d9d273f1a649afa6a52ea35ad011195ea/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6", size = 389696, upload-time = "2025-11-30T20:21:36.122Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/01/6271a2511ad0815f00f7ed4390cf2567bec1d4b1da39e2c27a41e6e3b4de/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7", size = 403136, upload-time = "2025-11-30T20:21:37.728Z" },
+ { url = "https://files.pythonhosted.org/packages/55/64/c857eb7cd7541e9b4eee9d49c196e833128a55b89a9850a9c9ac33ccf897/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324", size = 524699, upload-time = "2025-11-30T20:21:38.92Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/ed/94816543404078af9ab26159c44f9e98e20fe47e2126d5d32c9d9948d10a/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df", size = 412022, upload-time = "2025-11-30T20:21:40.407Z" },
+ { url = "https://files.pythonhosted.org/packages/61/b5/707f6cf0066a6412aacc11d17920ea2e19e5b2f04081c64526eb35b5c6e7/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3", size = 390522, upload-time = "2025-11-30T20:21:42.17Z" },
+ { url = "https://files.pythonhosted.org/packages/13/4e/57a85fda37a229ff4226f8cbcf09f2a455d1ed20e802ce5b2b4a7f5ed053/rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221", size = 404579, upload-time = "2025-11-30T20:21:43.769Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/da/c9339293513ec680a721e0e16bf2bac3db6e5d7e922488de471308349bba/rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7", size = 421305, upload-time = "2025-11-30T20:21:44.994Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/be/522cb84751114f4ad9d822ff5a1aa3c98006341895d5f084779b99596e5c/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff", size = 572503, upload-time = "2025-11-30T20:21:46.91Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/9b/de879f7e7ceddc973ea6e4629e9b380213a6938a249e94b0cdbcc325bb66/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7", size = 598322, upload-time = "2025-11-30T20:21:48.709Z" },
+ { url = "https://files.pythonhosted.org/packages/48/ac/f01fc22efec3f37d8a914fc1b2fb9bcafd56a299edbe96406f3053edea5a/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139", size = 560792, upload-time = "2025-11-30T20:21:50.024Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/da/4e2b19d0f131f35b6146425f846563d0ce036763e38913d917187307a671/rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464", size = 221901, upload-time = "2025-11-30T20:21:51.32Z" },
+ { url = "https://files.pythonhosted.org/packages/96/cb/156d7a5cf4f78a7cc571465d8aec7a3c447c94f6749c5123f08438bcf7bc/rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169", size = 235823, upload-time = "2025-11-30T20:21:52.505Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" },
+ { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" },
+ { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" },
+ { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" },
+ { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" },
+ { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" },
+ { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" },
+ { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" },
+ { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" },
+ { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" },
+ { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" },
+ { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" },
+ { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" },
+ { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" },
+ { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" },
+ { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" },
+ { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" },
+ { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" },
+ { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" },
+ { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" },
+ { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" },
+ { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" },
+ { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" },
+ { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" },
+ { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" },
+ { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" },
+ { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" },
+ { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" },
+ { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" },
+ { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" },
+ { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" },
+ { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" },
+ { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" },
+ { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" },
+ { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" },
+ { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" },
+ { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" },
+ { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" },
+ { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" },
+ { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" },
+ { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" },
+ { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" },
+ { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" },
+ { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" },
+ { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" },
+ { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" },
+]
+
+[[package]]
+name = "shellingham"
+version = "1.5.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
+]
+
+[[package]]
+name = "sse-starlette"
+version = "3.3.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "starlette" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/5a/9f/c3695c2d2d4ef70072c3a06992850498b01c6bc9be531950813716b426fa/sse_starlette-3.3.2.tar.gz", hash = "sha256:678fca55a1945c734d8472a6cad186a55ab02840b4f6786f5ee8770970579dcd", size = 32326, upload-time = "2026-02-28T11:24:34.36Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/61/28/8cb142d3fe80c4a2d8af54ca0b003f47ce0ba920974e7990fa6e016402d1/sse_starlette-3.3.2-py3-none-any.whl", hash = "sha256:5c3ea3dad425c601236726af2f27689b74494643f57017cafcb6f8c9acfbb862", size = 14270, upload-time = "2026-02-28T11:24:32.984Z" },
+]
+
+[[package]]
+name = "starlette"
+version = "0.52.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c4/68/79977123bb7be889ad680d79a40f339082c1978b5cfcf62c2d8d196873ac/starlette-0.52.1.tar.gz", hash = "sha256:834edd1b0a23167694292e94f597773bc3f89f362be6effee198165a35d62933", size = 2653702, upload-time = "2026-01-18T13:34:11.062Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl", hash = "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74", size = 74272, upload-time = "2026-01-18T13:34:09.188Z" },
+]
+
+[[package]]
+name = "typer"
+version = "0.24.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "annotated-doc" },
+ { name = "click" },
+ { name = "rich" },
+ { name = "shellingham" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/f5/24/cb09efec5cc954f7f9b930bf8279447d24618bb6758d4f6adf2574c41780/typer-0.24.1.tar.gz", hash = "sha256:e39b4732d65fbdcde189ae76cf7cd48aeae72919dea1fdfc16593be016256b45", size = 118613, upload-time = "2026-02-21T16:54:40.609Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl", hash = "sha256:112c1f0ce578bfb4cab9ffdabc68f031416ebcc216536611ba21f04e9aa84c9e", size = 56085, upload-time = "2026-02-21T16:54:41.616Z" },
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.15.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
+]
+
+[[package]]
+name = "typing-inspection"
+version = "0.4.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
+]
+
+[[package]]
+name = "uvicorn"
+version = "0.41.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+ { name = "h11" },
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/32/ce/eeb58ae4ac36fe09e3842eb02e0eb676bf2c53ae062b98f1b2531673efdd/uvicorn-0.41.0.tar.gz", hash = "sha256:09d11cf7008da33113824ee5a1c6422d89fbc2ff476540d69a34c87fab8b571a", size = 82633, upload-time = "2026-02-16T23:07:24.1Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/83/e4/d04a086285c20886c0daad0e026f250869201013d18f81d9ff5eada73a88/uvicorn-0.41.0-py3-none-any.whl", hash = "sha256:29e35b1d2c36a04b9e180d4007ede3bcb32a85fbdfd6c6aeb3f26839de088187", size = 68783, upload-time = "2026-02-16T23:07:22.357Z" },
+]
+
+[[package]]
+name = "xlsxwriter"
+version = "3.2.9"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/46/2c/c06ef49dc36e7954e55b802a8b231770d286a9758b3d936bd1e04ce5ba88/xlsxwriter-3.2.9.tar.gz", hash = "sha256:254b1c37a368c444eac6e2f867405cc9e461b0ed97a3233b2ac1e574efb4140c", size = 215940, upload-time = "2025-09-16T00:16:21.63Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3a/0c/3662f4a66880196a590b202f0db82d919dd2f89e99a27fadef91c4a33d41/xlsxwriter-3.2.9-py3-none-any.whl", hash = "sha256:9a5db42bc5dff014806c58a20b9eae7322a134abb6fce3c92c181bfb275ec5b3", size = 175315, upload-time = "2025-09-16T00:16:20.108Z" },
+]
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/.github/workflows/publish.yml b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/.github/workflows/publish.yml
new file mode 100644
index 00000000..3f4a74ef
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/.github/workflows/publish.yml
@@ -0,0 +1,30 @@
+name: Publish to PyPI
+
+on:
+ release:
+ types: [published]
+
+jobs:
+ publish:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.11"
+
+ - name: Install build dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install build twine
+
+ - name: Build package
+ run: python -m build
+
+ - name: Publish to PyPI
+ env:
+ TWINE_USERNAME: __token__
+ TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
+ run: twine upload dist/*
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/.gitignore b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/.gitignore
new file mode 100644
index 00000000..f4fde46e
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/.gitignore
@@ -0,0 +1,16 @@
+# Project files
+.idea
+.DS_Store
+
+# Python-generated files
+__pycache__/
+*.py[oc]
+build/
+dist/
+wheels/
+*.egg-info
+
+# Virtual environments
+.venv
+.env.example
+.idea
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/Dockerfile b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/Dockerfile
new file mode 100644
index 00000000..666236c1
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/Dockerfile
@@ -0,0 +1,22 @@
+# Generated by https://smithery.ai. See: https://smithery.ai/docs/build/project-config
+# syntax=docker/dockerfile:1
+
+# Use official Python runtime
+FROM python:3.11-slim
+
+# Set working directory
+WORKDIR /app
+
+# Install build dependencies
+RUN apt-get update \
+ && apt-get install -y --no-install-recommends build-essential \
+ && rm -rf /var/lib/apt/lists/*
+
+# Copy project files
+COPY . /app
+
+# Install Python dependencies
+RUN pip install --no-cache-dir .
+
+# Default command
+ENTRYPOINT ["word_mcp_server"]
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/LICENSE b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/LICENSE
new file mode 100644
index 00000000..31323d1d
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2025 GongRzhe
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/README.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/README.md
new file mode 100644
index 00000000..20b5d25f
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/README.md
@@ -0,0 +1,394 @@
+# Office-Word-MCP-Server
+
+[](https://smithery.ai/server/@GongRzhe/Office-Word-MCP-Server)
+
+A Model Context Protocol (MCP) server for creating, reading, and manipulating Microsoft Word documents. This server enables AI assistants to work with Word documents through a standardized interface, providing rich document editing capabilities.
+
+
+
+
+
+
+
+## Overview
+
+Office-Word-MCP-Server implements the [Model Context Protocol](https://modelcontextprotocol.io/) to expose Word document operations as tools and resources. It serves as a bridge between AI assistants and Microsoft Word documents, allowing for document creation, content addition, formatting, and analysis.
+
+The server features a modular architecture that separates concerns into core functionality, tools, and utilities, making it highly maintainable and extensible for future enhancements.
+
+### Example
+
+#### Pormpt
+
+
+
+#### Output
+
+
+
+## Features
+
+### Document Management
+
+- Create new Word documents with metadata
+- Extract text and analyze document structure
+- View document properties and statistics
+- List available documents in a directory
+- Create copies of existing documents
+- Merge multiple documents into a single document
+- Convert Word documents to PDF format
+
+### Content Creation
+
+- Add headings with different levels and direct formatting (font, size, bold, italic, borders)
+- Insert paragraphs with optional styling and direct formatting (font, size, bold, italic, color)
+- Create tables with custom data
+- Add images with proportional scaling
+- Insert page breaks
+- Insert bulleted and numbered lists with proper XML formatting
+- Add footnotes and endnotes to documents
+- Convert footnotes to endnotes
+- Customize footnote and endnote styling
+- Create professional table layouts for technical documentation
+- Design callout boxes and formatted content for instructional materials
+- Build structured data tables for business reports with consistent styling
+- Insert content relative to existing text or paragraph indices
+
+### Rich Text Formatting
+
+- Format specific text sections (bold, italic, underline)
+- Change text color and font properties
+- Apply custom styles to text elements
+- Search and replace text throughout documents
+- Individual cell text formatting within tables
+- Multiple formatting combinations for enhanced visual appeal
+- Font customization with family and size control
+- Direct formatting during content creation (paragraphs and headings)
+- Reduce function calls by combining content creation with formatting
+- Add section header borders for visual separation
+
+### Table Formatting
+
+- Format tables with borders and styles
+- Create header rows with distinct formatting
+- Apply cell shading and custom borders
+- Structure tables for better readability
+- Individual cell background shading with color support
+- Alternating row colors for improved readability
+- Enhanced header row highlighting with custom colors
+- Cell text formatting with bold, italic, underline, color, font size, and font family
+- Comprehensive color support with named colors and hex color codes
+- Cell padding management with independent control of all sides
+- Cell alignment (horizontal and vertical positioning)
+- Cell merging (horizontal, vertical, and rectangular areas)
+- Column width management with multiple units (points, percentage, auto-fit)
+- Auto-fit capabilities for dynamic column sizing
+- Professional callout table support with icon cells and styled content
+
+### Advanced Document Manipulation
+
+- Delete paragraphs
+- Insert content relative to specific text or paragraph indices
+- Insert bulleted and numbered lists with proper XML numbering structure
+- Insert headers and paragraphs before or after target locations
+- Create custom document styles
+- Apply consistent formatting throughout documents
+- Format specific ranges of text with detailed control
+- Flexible padding units with support for points and percentage-based measurements
+- Clear, readable table presentation with proper alignment and spacing
+
+### Document Protection
+
+- Add password protection to documents
+- Implement restricted editing with editable sections
+- Add digital signatures to documents
+- Verify document authenticity and integrity
+
+### Comment Extraction
+
+- Extract all comments from a document
+- Filter comments by author
+- Get comments for specific paragraphs
+- Access comment metadata (author, date, text)
+
+## Installation
+
+### Installing via Smithery
+
+To install Office Word Document Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@GongRzhe/Office-Word-MCP-Server):
+
+```bash
+npx -y @smithery/cli install @GongRzhe/Office-Word-MCP-Server --client claude
+```
+
+### Prerequisites
+
+- Python 3.8 or higher
+- pip package manager
+
+### Basic Installation
+
+```bash
+# Clone the repository
+git clone https://github.com/GongRzhe/Office-Word-MCP-Server.git
+cd Office-Word-MCP-Server
+
+# Install dependencies
+pip install -r requirements.txt
+```
+
+### Using the Setup Script
+
+Alternatively, you can use the provided setup script which handles:
+
+- Checking prerequisites
+- Setting up a virtual environment
+- Installing dependencies
+- Generating MCP configuration
+
+```bash
+python setup_mcp.py
+```
+
+## Usage with Claude for Desktop
+
+### Configuration
+
+#### Method 1: After Local Installation
+
+1. After installation, add the server to your Claude for Desktop configuration file:
+
+```json
+{
+ "mcpServers": {
+ "word-document-server": {
+ "command": "python",
+ "args": ["/path/to/word_mcp_server.py"]
+ }
+ }
+}
+```
+
+#### Method 2: Without Installation (Using uvx)
+
+1. You can also configure Claude for Desktop to use the server without local installation by using the uvx package manager:
+
+```json
+{
+ "mcpServers": {
+ "word-document-server": {
+ "command": "uvx",
+ "args": ["--from", "office-word-mcp-server", "word_mcp_server"]
+ }
+ }
+}
+```
+
+2. Configuration file locations:
+
+ - macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
+ - Windows: `%APPDATA%\Claude\claude_desktop_config.json`
+
+3. Restart Claude for Desktop to load the configuration.
+
+### Example Operations
+
+Once configured, you can ask Claude to perform operations like:
+
+- "Create a new document called 'report.docx' with a title page"
+- "Add a heading and three paragraphs to my document"
+- "Add my name in Helvetica 36pt bold at the top of the document"
+- "Add a section heading 'Summary' in Helvetica 14pt bold with a bottom border"
+- "Add a paragraph in Times New Roman 14pt with italic blue text"
+- "Insert a bulleted list after the paragraph containing 'Introduction'"
+- "Insert a numbered list with items: 'First step', 'Second step', 'Third step'"
+- "Add bullet points after the 'Summary' heading"
+- "Insert a 4x4 table with sales data"
+- "Format the word 'important' in paragraph 2 to be bold and red"
+- "Search and replace all instances of 'old term' with 'new term'"
+- "Create a custom style for section headings"
+- "Apply formatting to the table in my document"
+- "Extract all comments from my document"
+- "Show me all comments by John Doe"
+- "Get comments for paragraph 3"
+- "Make the text in table cell (1,2) bold and blue with 14pt font"
+- "Add 10 points of padding to all sides of the header cells"
+- "Create a callout table with a blue checkmark icon and white text"
+- "Set the first column width to 50 points and auto-fit the remaining columns"
+- "Apply alternating row colors to make the table more readable"
+
+
+## API Reference
+
+### Document Creation and Properties
+
+```python
+create_document(filename, title=None, author=None)
+get_document_info(filename)
+get_document_text(filename)
+get_document_outline(filename)
+list_available_documents(directory=".")
+copy_document(source_filename, destination_filename=None)
+convert_to_pdf(filename, output_filename=None)
+```
+
+### Content Addition
+
+```python
+add_heading(filename, text, level=1, font_name=None, font_size=None,
+ bold=None, italic=None, border_bottom=False)
+add_paragraph(filename, text, style=None, font_name=None, font_size=None,
+ bold=None, italic=None, color=None)
+add_table(filename, rows, cols, data=None)
+add_picture(filename, image_path, width=None)
+add_page_break(filename)
+```
+
+### Advanced Content Manipulation
+
+```python
+# Insert content relative to existing text or paragraph index
+insert_header_near_text(filename, target_text=None, header_title=None,
+ position='after', header_style='Heading 1',
+ target_paragraph_index=None)
+
+insert_line_or_paragraph_near_text(filename, target_text=None, line_text=None,
+ position='after', line_style=None,
+ target_paragraph_index=None)
+
+# Insert bulleted or numbered lists with proper XML formatting
+insert_numbered_list_near_text(filename, target_text=None, list_items=None,
+ position='after', target_paragraph_index=None,
+ bullet_type='bullet')
+# bullet_type options:
+# 'bullet' - Creates bulleted list with bullets (•)
+# 'number' - Creates numbered list (1, 2, 3, ...)
+```
+
+### Content Extraction
+
+```python
+get_document_text(filename)
+get_paragraph_text_from_document(filename, paragraph_index)
+find_text_in_document(filename, text_to_find, match_case=True, whole_word=False)
+```
+
+### Text Formatting
+
+```python
+format_text(filename, paragraph_index, start_pos, end_pos, bold=None,
+ italic=None, underline=None, color=None, font_size=None, font_name=None)
+search_and_replace(filename, find_text, replace_text)
+delete_paragraph(filename, paragraph_index)
+create_custom_style(filename, style_name, bold=None, italic=None,
+ font_size=None, font_name=None, color=None, base_style=None)
+```
+
+### Table Formatting
+
+```python
+format_table(filename, table_index, has_header_row=None,
+ border_style=None, shading=None)
+set_table_cell_shading(filename, table_index, row_index, col_index,
+ fill_color, pattern="clear")
+apply_table_alternating_rows(filename, table_index,
+ color1="FFFFFF", color2="F2F2F2")
+highlight_table_header(filename, table_index,
+ header_color="4472C4", text_color="FFFFFF")
+
+# Cell merging tools
+merge_table_cells(filename, table_index, start_row, start_col, end_row, end_col)
+merge_table_cells_horizontal(filename, table_index, row_index, start_col, end_col)
+merge_table_cells_vertical(filename, table_index, col_index, start_row, end_row)
+
+# Cell alignment tools
+set_table_cell_alignment(filename, table_index, row_index, col_index,
+ horizontal="left", vertical="top")
+set_table_alignment_all(filename, table_index,
+ horizontal="left", vertical="top")
+
+# Cell text formatting tools
+format_table_cell_text(filename, table_index, row_index, col_index,
+ text_content=None, bold=None, italic=None, underline=None,
+ color=None, font_size=None, font_name=None)
+
+# Cell padding tools
+set_table_cell_padding(filename, table_index, row_index, col_index,
+ top=None, bottom=None, left=None, right=None, unit="points")
+
+# Column width management
+set_table_column_width(filename, table_index, col_index, width, width_type="points")
+set_table_column_widths(filename, table_index, widths, width_type="points")
+set_table_width(filename, table_index, width, width_type="points")
+auto_fit_table_columns(filename, table_index)
+```
+
+### Comment Extraction
+
+```python
+get_all_comments(filename)
+get_comments_by_author(filename, author)
+get_comments_for_paragraph(filename, paragraph_index)
+```
+
+## Troubleshooting
+
+### Common Issues
+
+1. **Missing Styles**
+
+ - Some documents may lack required styles for heading and table operations
+ - The server will attempt to create missing styles or use direct formatting
+ - For best results, use templates with standard Word styles
+
+2. **Permission Issues**
+
+ - Ensure the server has permission to read/write to the document paths
+ - Use the `copy_document` function to create editable copies of locked documents
+ - Check file ownership and permissions if operations fail
+
+3. **Image Insertion Problems**
+ - Use absolute paths for image files
+ - Verify image format compatibility (JPEG, PNG recommended)
+ - Check image file size and permissions
+
+4. **Table Formatting Issues**
+
+ - **Cell index errors**: Ensure row and column indices are within table bounds (0-based indexing)
+ - **Color format problems**: Use hex colors without '#' prefix (e.g., "FF0000" for red) or standard color names
+ - **Padding unit confusion**: Specify "points" or "percent" explicitly when setting cell padding
+ - **Column width conflicts**: Auto-fit may override manual column width settings
+ - **Text formatting persistence**: Apply cell text formatting after setting cell content for best results
+
+### Debugging
+
+Enable detailed logging by setting the environment variable:
+
+```bash
+export MCP_DEBUG=1 # Linux/macOS
+set MCP_DEBUG=1 # Windows
+```
+
+## Contributing
+
+Contributions are welcome! Please feel free to submit a Pull Request.
+
+1. Fork the repository
+2. Create your feature branch (`git checkout -b feature/amazing-feature`)
+3. Commit your changes (`git commit -m 'Add some amazing feature'`)
+4. Push to the branch (`git push origin feature/amazing-feature`)
+5. Open a Pull Request
+
+## License
+
+This project is licensed under the MIT License - see the LICENSE file for details.
+
+## Acknowledgments
+
+- [Model Context Protocol](https://modelcontextprotocol.io/) for the protocol specification
+- [python-docx](https://python-docx.readthedocs.io/) for Word document manipulation
+- [FastMCP](https://github.com/modelcontextprotocol/python-sdk) for the Python MCP implementation
+
+---
+
+_Note: This server interacts with document files on your system. Always verify that requested operations are appropriate before confirming them in Claude for Desktop or other MCP clients._
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/RENDER_DEPLOYMENT.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/RENDER_DEPLOYMENT.md
new file mode 100644
index 00000000..70b12704
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/RENDER_DEPLOYMENT.md
@@ -0,0 +1,59 @@
+# Render Deployment Guide
+
+This document explains how to deploy the Office Word MCP Server on Render.
+
+## Required Environment Variables
+
+Set the following environment variables in your Render service:
+
+### `MCP_TRANSPORT`
+- **Value**: `sse`
+- **Description**: Sets the transport type to Server-Sent Events (SSE) for HTTP communication
+- **Required**: Yes (for Render deployment)
+
+### `MCP_HOST`
+- **Value**: `0.0.0.0`
+- **Description**: Binds the server to all network interfaces
+- **Required**: No (defaults to 0.0.0.0)
+
+### `FASTMCP_LOG_LEVEL`
+- **Value**: `INFO`
+- **Description**: Sets the logging level for FastMCP
+- **Required**: No (defaults to INFO)
+
+## How to Set Environment Variables
+
+1. Go to your Render dashboard: https://dashboard.render.com
+2. Navigate to your service: `Office-Word-MCP-Server`
+3. Click on "Environment" in the left sidebar
+4. Add the environment variable:
+ - Key: `MCP_TRANSPORT`
+ - Value: `sse`
+5. Click "Save Changes"
+
+## Deployment
+
+After setting the environment variables:
+1. Render will automatically redeploy your service
+2. The server will start with SSE transport on the port provided by Render
+3. Access your server at: `https://office-word-mcp-server-bzlp.onrender.com/sse`
+
+## Health Check Endpoint
+
+The FastMCP server with SSE transport automatically provides a health check endpoint at:
+- `https://your-service.onrender.com/health`
+
+## Troubleshooting
+
+### Server exits with status 1
+- **Cause**: Server is running in STDIO mode instead of SSE
+- **Fix**: Ensure `MCP_TRANSPORT=sse` is set in environment variables
+
+### Port binding errors
+- **Cause**: Server not using Render's PORT environment variable
+- **Fix**: This has been fixed in the latest version of main.py
+
+### Cannot connect to server
+- **Cause**: Health checks failing
+- **Fix**: Ensure SSE transport is enabled and server is listening on 0.0.0.0
+
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/__init__.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/__init__.py
new file mode 100644
index 00000000..d9b86b65
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/__init__.py
@@ -0,0 +1,4 @@
+"""Office Word MCP Server package entry point."""
+from word_document_server.main import run_server
+
+__all__ = ["run_server"]
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/mcp-config.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/mcp-config.json
new file mode 100644
index 00000000..246a3ef2
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/mcp-config.json
@@ -0,0 +1,14 @@
+{
+ "mcpServers": {
+ "word-document-server": {
+ "command": "/Users/gongzhe/GitRepos/Office-Word-MCP-Server/.venv/bin/python",
+ "args": [
+ "/Users/gongzhe/GitRepos/Office-Word-MCP-Server/word_mcp_server.py"
+ ],
+ "env": {
+ "PYTHONPATH": "/Users/gongzhe/GitRepos/Office-Word-MCP-Server",
+ "MCP_TRANSPORT": "stdio"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/office_word_mcp_server/__init__.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/office_word_mcp_server/__init__.py
new file mode 100644
index 00000000..d71a049f
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/office_word_mcp_server/__init__.py
@@ -0,0 +1,3 @@
+from word_document_server.main import run_server
+
+__all__ = ["run_server"]
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/pyproject.toml b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/pyproject.toml
new file mode 100644
index 00000000..ca9ea6a0
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/pyproject.toml
@@ -0,0 +1,40 @@
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[project]
+name = "office-word-mcp-server"
+version = "1.1.11"
+description = "MCP server for manipulating Microsoft Word documents"
+readme = "README.md"
+license = {file = "LICENSE"}
+authors = [
+ {name = "GongRzhe", email = "gongrzhe@gmail.com"}
+]
+classifiers = [
+ "Programming Language :: Python :: 3",
+ "License :: OSI Approved :: MIT License",
+ "Operating System :: OS Independent",
+]
+requires-python = ">=3.11"
+dependencies = [
+ "python-docx>=1.1.2",
+ "fastmcp>=2.8.1",
+ "msoffcrypto-tool>=5.4.2",
+ "docx2pdf>=0.1.8",
+ "pytest>=8.4.2",
+]
+
+[project.urls]
+"Homepage" = "https://github.com/GongRzhe/Office-Word-MCP-Server.git"
+"Bug Tracker" = "https://github.com/GongRzhe/Office-Word-MCP-Server.git/issues"
+
+[tool.hatch.build.targets.wheel]
+only-include = [
+ "word_document_server",
+ "office_word_mcp_server",
+]
+sources = ["."]
+
+[project.scripts]
+word_mcp_server = "word_document_server.main:run_server"
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/requirements.txt b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/requirements.txt
new file mode 100644
index 00000000..7077dbb0
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/requirements.txt
@@ -0,0 +1,5 @@
+fastmcp
+python-docx
+msoffcrypto-tool
+docx2pdf
+python-dotenv
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/setup_mcp.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/setup_mcp.py
new file mode 100644
index 00000000..f0caa5c4
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/setup_mcp.py
@@ -0,0 +1,524 @@
+# Import necessary Python standard libraries
+import os
+import json
+import subprocess
+import sys
+import shutil
+import platform
+
+def check_prerequisites():
+ """
+ Check if necessary prerequisites are installed
+
+ Returns:
+ tuple: (python_ok, uv_installed, uvx_installed, word_server_installed)
+ """
+ # Check Python version
+ python_version = sys.version_info
+ python_ok = python_version.major >= 3 and python_version.minor >= 8
+
+ # Check if uv/uvx is installed
+ uv_installed = shutil.which("uv") is not None
+ uvx_installed = shutil.which("uvx") is not None
+
+ # Check if word-document-server is already installed via pip
+ try:
+ result = subprocess.run(
+ [sys.executable, "-m", "pip", "show", "word-document-server"],
+ capture_output=True,
+ text=True,
+ check=False
+ )
+ word_server_installed = result.returncode == 0
+ except Exception:
+ word_server_installed = False
+
+ return (python_ok, uv_installed, uvx_installed, word_server_installed)
+
+def get_transport_choice():
+ """
+ Ask user to choose transport type
+
+ Returns:
+ dict: Transport configuration
+ """
+ print("\nTransport Configuration:")
+ print("1. STDIO (default, local execution)")
+ print("2. Streamable HTTP (modern, recommended for web deployment)")
+ print("3. SSE (Server-Sent Events, for compatibility)")
+
+ choice = input("\nSelect transport type (1-3, default: 1): ").strip()
+
+ if choice == "2":
+ host = input("Host (default: 127.0.0.1): ").strip() or "127.0.0.1"
+ port = input("Port (default: 8000): ").strip() or "8000"
+ path = input("Path (default: /mcp): ").strip() or "/mcp"
+
+ return {
+ "transport": "streamable-http",
+ "host": host,
+ "port": port,
+ "path": path
+ }
+ elif choice == "3":
+ host = input("Host (default: 127.0.0.1): ").strip() or "127.0.0.1"
+ port = input("Port (default: 8000): ").strip() or "8000"
+ sse_path = input("SSE Path (default: /sse): ").strip() or "/sse"
+
+ return {
+ "transport": "sse",
+ "host": host,
+ "port": port,
+ "sse_path": sse_path
+ }
+ else:
+ # Default to stdio
+ return {
+ "transport": "stdio"
+ }
+
+def setup_venv():
+ """
+ Function to set up Python virtual environment
+
+ Features:
+ - Checks if Python version meets requirements (3.8+)
+ - Creates Python virtual environment (if it doesn't exist)
+ - Installs required dependencies in the newly created virtual environment
+
+ No parameters required
+
+ Returns: Path to Python interpreter in the virtual environment
+ """
+ # Check Python version
+ python_version = sys.version_info
+ if python_version.major < 3 or (python_version.major == 3 and python_version.minor < 8):
+ print("Error: Python 3.8 or higher is required.")
+ sys.exit(1)
+
+ # Get absolute path of the directory containing the current script
+ base_path = os.path.abspath(os.path.dirname(__file__))
+ # Set virtual environment directory path
+ venv_path = os.path.join(base_path, '.venv')
+
+ # Determine pip and python executable paths based on operating system
+ is_windows = platform.system() == "Windows"
+ if is_windows:
+ pip_path = os.path.join(venv_path, 'Scripts', 'pip.exe')
+ python_path = os.path.join(venv_path, 'Scripts', 'python.exe')
+ else:
+ pip_path = os.path.join(venv_path, 'bin', 'pip')
+ python_path = os.path.join(venv_path, 'bin', 'python')
+
+ # Check if virtual environment already exists and is valid
+ venv_exists = os.path.exists(venv_path)
+ pip_exists = os.path.exists(pip_path)
+
+ if not venv_exists or not pip_exists:
+ print("Creating new virtual environment...")
+ # Remove existing venv if it's invalid
+ if venv_exists and not pip_exists:
+ print("Existing virtual environment is incomplete, recreating it...")
+ try:
+ shutil.rmtree(venv_path)
+ except Exception as e:
+ print(f"Warning: Could not remove existing virtual environment: {e}")
+ print("Please delete the .venv directory manually and try again.")
+ sys.exit(1)
+
+ # Create virtual environment
+ try:
+ subprocess.run([sys.executable, '-m', 'venv', venv_path], check=True)
+ print("Virtual environment created successfully!")
+ except subprocess.CalledProcessError as e:
+ print(f"Error creating virtual environment: {e}")
+ sys.exit(1)
+ else:
+ print("Valid virtual environment already exists.")
+
+ # Double-check that pip exists after creating venv
+ if not os.path.exists(pip_path):
+ print(f"Error: pip executable not found at {pip_path}")
+ print("Try creating the virtual environment manually with: python -m venv .venv")
+ sys.exit(1)
+
+ # Install or update dependencies
+ print("\nInstalling requirements...")
+ try:
+ # Install FastMCP package (standalone library)
+ subprocess.run([pip_path, 'install', 'fastmcp'], check=True)
+ # Install python-docx package
+ subprocess.run([pip_path, 'install', 'python-docx'], check=True)
+
+ # Also install dependencies from requirements.txt if it exists
+ requirements_path = os.path.join(base_path, 'requirements.txt')
+ if os.path.exists(requirements_path):
+ subprocess.run([pip_path, 'install', '-r', requirements_path], check=True)
+
+ print("Requirements installed successfully!")
+ except subprocess.CalledProcessError as e:
+ print(f"Error installing requirements: {e}")
+ sys.exit(1)
+ except FileNotFoundError:
+ print(f"Error: Could not execute {pip_path}")
+ print("Try activating the virtual environment manually and installing requirements:")
+ if is_windows:
+ print(f".venv\\Scripts\\activate")
+ else:
+ print("source .venv/bin/activate")
+ print("pip install mcp[cli] python-docx")
+ sys.exit(1)
+
+ return python_path
+
+def generate_mcp_config_local(python_path, transport_config):
+ """
+ Generate MCP configuration for locally installed word-document-server
+
+ Parameters:
+ - python_path: Path to Python interpreter in the virtual environment
+ - transport_config: Transport configuration dictionary
+
+ Returns: Path to the generated config file
+ """
+ # Get absolute path of the directory containing the current script
+ base_path = os.path.abspath(os.path.dirname(__file__))
+
+ # Path to Word Document Server script
+ server_script_path = os.path.join(base_path, 'word_mcp_server.py')
+
+ # Build environment variables
+ env = {
+ "PYTHONPATH": base_path,
+ "MCP_TRANSPORT": transport_config["transport"]
+ }
+
+ # Add transport-specific environment variables
+ if transport_config["transport"] == "streamable-http":
+ env.update({
+ "MCP_HOST": transport_config["host"],
+ "MCP_PORT": transport_config["port"],
+ "MCP_PATH": transport_config["path"]
+ })
+ elif transport_config["transport"] == "sse":
+ env.update({
+ "MCP_HOST": transport_config["host"],
+ "MCP_PORT": transport_config["port"],
+ "MCP_SSE_PATH": transport_config["sse_path"]
+ })
+ # For stdio transport, no additional environment variables needed
+
+ # Create MCP configuration dictionary
+ config = {
+ "mcpServers": {
+ "word-document-server": {
+ "command": python_path,
+ "args": [server_script_path],
+ "env": env
+ }
+ }
+ }
+
+ # Save configuration to JSON file
+ config_path = os.path.join(base_path, 'mcp-config.json')
+ with open(config_path, 'w') as f:
+ json.dump(config, f, indent=2)
+
+ return config_path
+
+def generate_mcp_config_uvx(transport_config):
+ """
+ Generate MCP configuration for PyPI-installed word-document-server using UVX
+
+ Parameters:
+ - transport_config: Transport configuration dictionary
+
+ Returns: Path to the generated config file
+ """
+ # Get absolute path of the directory containing the current script
+ base_path = os.path.abspath(os.path.dirname(__file__))
+
+ # Build environment variables
+ env = {
+ "MCP_TRANSPORT": transport_config["transport"]
+ }
+
+ # Add transport-specific environment variables
+ if transport_config["transport"] == "streamable-http":
+ env.update({
+ "MCP_HOST": transport_config["host"],
+ "MCP_PORT": transport_config["port"],
+ "MCP_PATH": transport_config["path"]
+ })
+ elif transport_config["transport"] == "sse":
+ env.update({
+ "MCP_HOST": transport_config["host"],
+ "MCP_PORT": transport_config["port"],
+ "MCP_SSE_PATH": transport_config["sse_path"]
+ })
+ # For stdio transport, no additional environment variables needed
+
+ # Create MCP configuration dictionary
+ config = {
+ "mcpServers": {
+ "word-document-server": {
+ "command": "uvx",
+ "args": ["--from", "word-mcp-server", "word_mcp_server"],
+ "env": env
+ }
+ }
+ }
+
+ # Save configuration to JSON file
+ config_path = os.path.join(base_path, 'mcp-config.json')
+ with open(config_path, 'w') as f:
+ json.dump(config, f, indent=2)
+
+ return config_path
+
+def generate_mcp_config_module(transport_config):
+ """
+ Generate MCP configuration for PyPI-installed word-document-server using Python module
+
+ Parameters:
+ - transport_config: Transport configuration dictionary
+
+ Returns: Path to the generated config file
+ """
+ # Get absolute path of the directory containing the current script
+ base_path = os.path.abspath(os.path.dirname(__file__))
+
+ # Build environment variables
+ env = {
+ "MCP_TRANSPORT": transport_config["transport"]
+ }
+
+ # Add transport-specific environment variables
+ if transport_config["transport"] == "streamable-http":
+ env.update({
+ "MCP_HOST": transport_config["host"],
+ "MCP_PORT": transport_config["port"],
+ "MCP_PATH": transport_config["path"]
+ })
+ elif transport_config["transport"] == "sse":
+ env.update({
+ "MCP_HOST": transport_config["host"],
+ "MCP_PORT": transport_config["port"],
+ "MCP_SSE_PATH": transport_config["sse_path"]
+ })
+
+
+ # Create MCP configuration dictionary
+ config = {
+ "mcpServers": {
+ "word-document-server": {
+ "command": sys.executable,
+ "args": ["-m", "word_document_server"],
+ "env": env
+ }
+ }
+ }
+
+ # Save configuration to JSON file
+ config_path = os.path.join(base_path, 'mcp-config.json')
+ with open(config_path, 'w') as f:
+ json.dump(config, f, indent=2)
+
+ return config_path
+
+def install_from_pypi():
+ """
+ Install word-document-server from PyPI
+
+ Returns: True if successful, False otherwise
+ """
+ print("\nInstalling word-document-server from PyPI...")
+ try:
+ subprocess.run([sys.executable, "-m", "pip", "install", "word-mcp-server"], check=True)
+ print("word-mcp-server successfully installed from PyPI!")
+ return True
+ except subprocess.CalledProcessError:
+ print("Failed to install word-mcp-server from PyPI.")
+ return False
+
+def print_config_instructions(config_path, transport_config):
+ """
+ Print instructions for using the generated config
+
+ Parameters:
+ - config_path: Path to the generated config file
+ - transport_config: Transport configuration dictionary
+ """
+ print(f"\nMCP configuration has been written to: {config_path}")
+
+ with open(config_path, 'r') as f:
+ config = json.load(f)
+
+ print("\nMCP configuration for Claude Desktop:")
+ print(json.dumps(config, indent=2))
+
+ # Print transport-specific instructions
+ if transport_config["transport"] == "streamable-http":
+ print(f"\n📡 Streamable HTTP Transport Configuration:")
+ print(f" Server will be accessible at: http://{transport_config['host']}:{transport_config['port']}{transport_config['path']}")
+ print(f" \n To test the server manually:")
+ print(f" curl -X POST http://{transport_config['host']}:{transport_config['port']}{transport_config['path']}")
+
+ elif transport_config["transport"] == "sse":
+ print(f"\n📡 SSE Transport Configuration:")
+ print(f" Server will be accessible at: http://{transport_config['host']}:{transport_config['port']}{transport_config['sse_path']}")
+ print(f" \n To test the server manually:")
+ print(f" curl http://{transport_config['host']}:{transport_config['port']}{transport_config['sse_path']}")
+
+ else: # stdio
+ print(f"\n💻 STDIO Transport Configuration:")
+ print(f" Server runs locally with standard input/output")
+
+ # Provide instructions for adding configuration to Claude Desktop configuration file
+ if platform.system() == "Windows":
+ claude_config_path = os.path.expandvars("%APPDATA%\\Claude\\claude_desktop_config.json")
+ else: # macOS
+ claude_config_path = os.path.expanduser("~/Library/Application Support/Claude/claude_desktop_config.json")
+
+ print(f"\nTo use with Claude Desktop, merge this configuration into: {claude_config_path}")
+
+def create_package_structure():
+ """
+ Create necessary package structure and environment files
+ """
+ # Get absolute path of the directory containing the current script
+ base_path = os.path.abspath(os.path.dirname(__file__))
+
+ # Create __init__.py file
+ init_path = os.path.join(base_path, '__init__.py')
+ if not os.path.exists(init_path):
+ with open(init_path, 'w') as f:
+ f.write('# Word Document MCP Server')
+ print(f"Created __init__.py at: {init_path}")
+
+ # Create requirements.txt file
+ requirements_path = os.path.join(base_path, 'requirements.txt')
+ if not os.path.exists(requirements_path):
+ with open(requirements_path, 'w') as f:
+ f.write('fastmcp\npython-docx\nmsoffcrypto-tool\ndocx2pdf\nhttpx\ncryptography\n')
+ print(f"Created requirements.txt at: {requirements_path}")
+
+ # Create .env.example file
+ env_example_path = os.path.join(base_path, '.env.example')
+ if not os.path.exists(env_example_path):
+ with open(env_example_path, 'w') as f:
+ f.write("""# Transport Configuration
+# Valid options: stdio, streamable-http, sse
+MCP_TRANSPORT=stdio
+
+# HTTP/SSE Configuration (when not using stdio)
+MCP_HOST=127.0.0.1
+MCP_PORT=8000
+
+# Streamable HTTP specific
+MCP_PATH=/mcp
+
+# SSE specific
+MCP_SSE_PATH=/sse
+
+""")
+ print(f"Created .env.example at: {env_example_path}")
+
+# Main execution entry point
+if __name__ == '__main__':
+ # Check prerequisites
+ python_ok, uv_installed, uvx_installed, word_server_installed = check_prerequisites()
+
+ if not python_ok:
+ print("Error: Python 3.8 or higher is required.")
+ sys.exit(1)
+
+ print("Word Document MCP Server Setup (Multi-Transport)")
+ print("===============================================\n")
+
+ # Create necessary files
+ create_package_structure()
+
+ # Get transport configuration
+ transport_config = get_transport_choice()
+
+ # If word-document-server is already installed, offer config options
+ if word_server_installed:
+ print("word-document-server is already installed via pip.")
+
+ if uvx_installed:
+ print("\nOptions:")
+ print("1. Generate MCP config for UVX (recommended)")
+ print("2. Generate MCP config for Python module")
+ print("3. Set up local development environment")
+
+ choice = input("\nEnter your choice (1-3): ")
+
+ if choice == "1":
+ config_path = generate_mcp_config_uvx(transport_config)
+ print_config_instructions(config_path, transport_config)
+ elif choice == "2":
+ config_path = generate_mcp_config_module(transport_config)
+ print_config_instructions(config_path, transport_config)
+ elif choice == "3":
+ python_path = setup_venv()
+ config_path = generate_mcp_config_local(python_path, transport_config)
+ print_config_instructions(config_path, transport_config)
+ else:
+ print("Invalid choice. Exiting.")
+ sys.exit(1)
+ else:
+ print("\nOptions:")
+ print("1. Generate MCP config for Python module")
+ print("2. Set up local development environment")
+
+ choice = input("\nEnter your choice (1-2): ")
+
+ if choice == "1":
+ config_path = generate_mcp_config_module(transport_config)
+ print_config_instructions(config_path, transport_config)
+ elif choice == "2":
+ python_path = setup_venv()
+ config_path = generate_mcp_config_local(python_path, transport_config)
+ print_config_instructions(config_path, transport_config)
+ else:
+ print("Invalid choice. Exiting.")
+ sys.exit(1)
+
+ # If word-document-server is not installed, offer installation options
+ else:
+ print("word-document-server is not installed.")
+
+ print("\nOptions:")
+ print("1. Install from PyPI (recommended)")
+ print("2. Set up local development environment")
+
+ choice = input("\nEnter your choice (1-2): ")
+
+ if choice == "1":
+ if install_from_pypi():
+ if uvx_installed:
+ print("\nNow generating MCP config for UVX...")
+ config_path = generate_mcp_config_uvx(transport_config)
+ else:
+ print("\nUVX not found. Generating MCP config for Python module...")
+ config_path = generate_mcp_config_module(transport_config)
+ print_config_instructions(config_path, transport_config)
+ elif choice == "2":
+ python_path = setup_venv()
+ config_path = generate_mcp_config_local(python_path, transport_config)
+ print_config_instructions(config_path, transport_config)
+ else:
+ print("Invalid choice. Exiting.")
+ sys.exit(1)
+
+ print("\nSetup complete! You can now use the Word Document MCP server with compatible clients like Claude Desktop.")
+ print("\nTransport Summary:")
+ print(f" - Transport: {transport_config['transport']}")
+ if transport_config['transport'] != 'stdio':
+ print(f" - Host: {transport_config.get('host', 'N/A')}")
+ print(f" - Port: {transport_config.get('port', 'N/A')}")
+ if transport_config['transport'] == 'streamable-http':
+ print(f" - Path: {transport_config.get('path', 'N/A')}")
+ elif transport_config['transport'] == 'sse':
+ print(f" - SSE Path: {transport_config.get('sse_path', 'N/A')}")
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/smithery.yaml b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/smithery.yaml
new file mode 100644
index 00000000..49719515
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/smithery.yaml
@@ -0,0 +1,13 @@
+# Smithery configuration file: https://smithery.ai/docs/build/project-config
+
+startCommand:
+ type: stdio
+ configSchema:
+ # JSON Schema defining the configuration options for the MCP.
+ type: object
+ description: No configuration options required
+ commandFunction:
+ # A JS function that produces the CLI command based on the given config to start the MCP on stdio.
+ |-
+ (config) => ({command:'word_mcp_server', args:[]})
+ exampleConfig: {}
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/test_doc.docx b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/test_doc.docx
new file mode 100644
index 00000000..4bea9d7b
Binary files /dev/null and b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/test_doc.docx differ
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/test_formatting.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/test_formatting.py
new file mode 100644
index 00000000..f29f932a
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/test_formatting.py
@@ -0,0 +1,108 @@
+"""
+Test script for add_paragraph and add_heading formatting parameters.
+"""
+import asyncio
+from docx import Document
+from word_document_server.tools.content_tools import add_paragraph, add_heading
+from word_document_server.tools.document_tools import create_document
+
+
+async def test_formatting():
+ """Test the new formatting parameters."""
+ test_doc = 'test_formatting.docx'
+
+ # Create test document
+ print("Creating test document...")
+ await create_document(test_doc, title="Formatting Test", author="Test Script")
+
+ # Test 1: Name with large font
+ print("Test 1: Adding name with large Helvetica 36pt bold...")
+ result = await add_paragraph(
+ test_doc,
+ "JAMES MEHORTER",
+ font_name="Helvetica",
+ font_size=36,
+ bold=True
+ )
+ print(f" Result: {result}")
+
+ # Test 2: Title line
+ print("Test 2: Adding title with Helvetica 14pt...")
+ result = await add_paragraph(
+ test_doc,
+ "Principal Software Engineer | Technical Team Lead",
+ font_name="Helvetica",
+ font_size=14
+ )
+ print(f" Result: {result}")
+
+ # Test 3: Section header with border
+ print("Test 3: Adding section header with border...")
+ result = await add_heading(
+ test_doc,
+ "PROFESSIONAL SUMMARY",
+ level=2,
+ font_name="Helvetica",
+ font_size=14,
+ bold=True,
+ border_bottom=True
+ )
+ print(f" Result: {result}")
+
+ # Test 4: Body text in Times New Roman
+ print("Test 4: Adding body text in Times New Roman 14pt...")
+ result = await add_paragraph(
+ test_doc,
+ "This is body text that should be in Times New Roman at 14pt. "
+ "It demonstrates the ability to apply different fonts to different paragraphs.",
+ font_name="Times New Roman",
+ font_size=14
+ )
+ print(f" Result: {result}")
+
+ # Test 5: Another section header
+ print("Test 5: Adding another section header with border...")
+ result = await add_heading(
+ test_doc,
+ "SKILLS",
+ level=2,
+ font_name="Helvetica",
+ font_size=14,
+ bold=True,
+ border_bottom=True
+ )
+ print(f" Result: {result}")
+
+ # Test 6: Italic text with color
+ print("Test 6: Adding italic text with color...")
+ result = await add_paragraph(
+ test_doc,
+ "This text is italic and colored blue.",
+ font_name="Arial",
+ font_size=12,
+ italic=True,
+ color="0000FF"
+ )
+ print(f" Result: {result}")
+
+ print(f"\n✅ Test document created: {test_doc}")
+
+ # Verify formatting
+ print("\nVerifying formatting...")
+ verify_doc = Document(test_doc)
+ for i, para in enumerate(verify_doc.paragraphs):
+ if para.runs:
+ run = para.runs[0]
+ text_preview = para.text[:50] + "..." if len(para.text) > 50 else para.text
+ print(f"\nParagraph {i}: {text_preview}")
+ print(f" Font: {run.font.name}")
+ print(f" Size: {run.font.size}")
+ print(f" Bold: {run.font.bold}")
+ print(f" Italic: {run.font.italic}")
+
+ print("\n✅ All tests completed successfully!")
+ print(f"Open {test_doc} in Word to verify the formatting visually.")
+
+
+if __name__ == "__main__":
+ asyncio.run(test_formatting())
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/tests/test_convert_to_pdf.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/tests/test_convert_to_pdf.py
new file mode 100644
index 00000000..c692fc82
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/tests/test_convert_to_pdf.py
@@ -0,0 +1,84 @@
+import asyncio
+from pathlib import Path
+
+import pytest
+from docx import Document
+
+# Target for testing: convert_to_pdf (async function)
+from word_document_server.tools.extended_document_tools import convert_to_pdf
+
+
+def _make_sample_docx(path: Path) -> None:
+ """Generates a simple .docx file in a temporary directory."""
+ doc = Document()
+ doc.add_heading("Conversion Test Document", level=1)
+ doc.add_paragraph("This is a test paragraph for PDF conversion. Contains ASCII too.")
+ doc.add_paragraph("Second paragraph: Contains special characters and spaces to cover path/content edge cases.")
+ doc.save(path)
+
+
+def test_convert_to_pdf_with_temp_docx(tmp_path: Path):
+ """
+ End-to-end test: Create a temporary .docx -> call convert_to_pdf -> validate the PDF output.
+
+ Notes:
+ - On Linux/macOS, it first tries LibreOffice (soffice/libreoffice),
+ and falls back to docx2pdf on failure (requires Microsoft Word).
+ - If these tools are missing or the command is unavailable, the test is skipped with a reason.
+ """
+ # 1) Generate a docx file with spaces in its name in the temp directory
+ src_doc = tmp_path / "sample document with spaces.docx"
+ _make_sample_docx(src_doc)
+
+ # 2) Define the output PDF path (also in the temp directory)
+ out_pdf = tmp_path / "converted output.pdf"
+
+ # 3) Run the asynchronous function under test
+ result_msg = asyncio.run(convert_to_pdf(str(src_doc), output_filename=str(out_pdf)))
+
+ # 4) Success condition: the return message contains success keywords, or the target PDF exists
+ success_keywords = ["successfully converted", "converted to PDF"]
+ success = any(k.lower() in result_msg.lower() for k in success_keywords) or out_pdf.exists()
+
+ if not success:
+ # When LibreOffice or Microsoft Word is not installed, the tool returns a hint.
+ # In this case, skip the test instead of failing.
+ pytest.skip(f"PDF conversion tool unavailable or conversion failed: {result_msg}")
+
+ # 5) Assert: The PDF file was generated and is not empty
+ # Some environments (especially docx2pdf) might ignore the exact output filename
+ # and just generate a PDF with the same name as the source in the output or source directory,
+ # so we check multiple possible locations.
+ candidates = [
+ out_pdf,
+ # Common: A PDF with the same name as the source file in the output directory
+ out_pdf.parent / f"{src_doc.stem}.pdf",
+ # Fallback: A PDF in the same directory as the source file
+ src_doc.with_suffix(".pdf"),
+ ]
+
+ # If none of the above paths exist, search for any newly generated PDF in the temp directory
+ found = None
+ for p in candidates:
+ if p.exists():
+ found = p
+ break
+ if not found:
+ pdfs = sorted(tmp_path.glob("*.pdf"), key=lambda p: p.stat().st_mtime, reverse=True)
+ if pdfs:
+ found = pdfs[0]
+
+ if not found:
+ # If the tool returns success but the output can't be found,
+ # treat it as an environment/tooling difference and skip instead of failing.
+ pytest.skip(f"Could not find the generated PDF. Function output: {result_msg}")
+
+ assert found.exists(), f"Generated PDF not found: {found}, function output: {result_msg}"
+ assert found.stat().st_size > 0, f"The generated PDF file is empty: {found}"
+
+
+if __name__ == "__main__":
+ # Allow running this file directly for quick verification:
+ # python tests/test_convert_to_pdf.py
+ import sys
+ sys.exit(pytest.main([__file__, "-q"]))
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/uv.lock b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/uv.lock
new file mode 100644
index 00000000..d87a4b65
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/uv.lock
@@ -0,0 +1,1491 @@
+version = 1
+revision = 2
+requires-python = ">=3.11"
+
+[[package]]
+name = "aiofile"
+version = "3.9.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "caio" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/67/e2/d7cb819de8df6b5c1968a2756c3cb4122d4fa2b8fc768b53b7c9e5edb646/aiofile-3.9.0.tar.gz", hash = "sha256:e5ad718bb148b265b6df1b3752c4d1d83024b93da9bd599df74b9d9ffcf7919b", size = 17943, upload-time = "2024-10-08T10:39:35.846Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/50/25/da1f0b4dd970e52bf5a36c204c107e11a0c6d3ed195eba0bfbc664c312b2/aiofile-3.9.0-py3-none-any.whl", hash = "sha256:ce2f6c1571538cbdfa0143b04e16b208ecb0e9cb4148e528af8a640ed51cc8aa", size = 19539, upload-time = "2024-10-08T10:39:32.955Z" },
+]
+
+[[package]]
+name = "annotated-types"
+version = "0.7.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
+]
+
+[[package]]
+name = "anyio"
+version = "4.9.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "idna" },
+ { name = "sniffio" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload-time = "2025-03-17T00:02:54.77Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" },
+]
+
+[[package]]
+name = "appscript"
+version = "1.3.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "lxml" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a3/84/5c0aec149c6a002d46af17e3d2c5efbe5e8258ef7574cfc17cd1b26c726e/appscript-1.3.0.tar.gz", hash = "sha256:80943118bc97f9f78a8aa55f85565752ed4d82c7893427d7d9ebfdf401c12b2c", size = 295205, upload-time = "2024-10-13T12:34:00.57Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/99/64/db8dddd3c561fe5085e5b3a60419bfb560f07e1ca0dc1c7027cbaa5fb582/appscript-1.3.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:76a3507b27c78bf79af83a5f6fac49664b53d530d75632c023e53df1bd350caf", size = 99353, upload-time = "2024-10-13T12:33:51.589Z" },
+ { url = "https://files.pythonhosted.org/packages/40/ee/4e0dee488d3dd35aab03c2f6ecb6dc0161fad200077cca68afe041079d2b/appscript-1.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:94ca097d672de5b8cfc82b4179b00cabd21588dbfd939347cf14a9e81955b2d5", size = 85401, upload-time = "2024-10-13T12:33:52.46Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/e2/05fd221bea1d309211569130a1a8f0966eb56394e46df068a69df0f29d61/appscript-1.3.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c0b5c160908de728072d4a0ae57f286608c5d7692bfccbc6eadde868aac2742b", size = 99575, upload-time = "2024-10-13T12:33:53.629Z" },
+ { url = "https://files.pythonhosted.org/packages/df/2f/3ee4190ce97b0b39df58184210d3baaa5fe59ae0972e63c2c85f122ca887/appscript-1.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d2a287b81030c81017127d4fb1c24729623576c50d2ff41694476b9af3ce0a97", size = 85496, upload-time = "2024-10-13T12:33:55.108Z" },
+ { url = "https://files.pythonhosted.org/packages/92/5a/3b642e3e904fb37d45e40bb07b4362979160bdecb0d37aa74f2506b1a47e/appscript-1.3.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:13094640e2694b888827d4e133f33dad1e08c9d7102b447c3cc8a73246fdab40", size = 99574, upload-time = "2024-10-13T12:33:56.317Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/bc/d8558bec737e02a9c404fb3b985b8636c313bb65a176375d551cb839e876/appscript-1.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e7b4760105810e9b1ecd5b40aba7617e0a047346fb94ee4370e9d37e4383b78d", size = 85503, upload-time = "2024-10-13T12:33:57.54Z" },
+]
+
+[[package]]
+name = "attrs"
+version = "25.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" },
+]
+
+[[package]]
+name = "authlib"
+version = "1.6.9"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cryptography" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/af/98/00d3dd826d46959ad8e32af2dbb2398868fd9fd0683c26e56d0789bd0e68/authlib-1.6.9.tar.gz", hash = "sha256:d8f2421e7e5980cc1ddb4e32d3f5fa659cfaf60d8eaf3281ebed192e4ab74f04", size = 165134, upload-time = "2026-03-02T07:44:01.998Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/53/23/b65f568ed0c22f1efacb744d2db1a33c8068f384b8c9b482b52ebdbc3ef6/authlib-1.6.9-py2.py3-none-any.whl", hash = "sha256:f08b4c14e08f0861dc18a32357b33fbcfd2ea86cfe3fe149484b4d764c4a0ac3", size = 244197, upload-time = "2026-03-02T07:44:00.307Z" },
+]
+
+[[package]]
+name = "backports-tarfile"
+version = "1.2.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/86/72/cd9b395f25e290e633655a100af28cb253e4393396264a98bd5f5951d50f/backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991", size = 86406, upload-time = "2024-05-28T17:01:54.731Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", size = 30181, upload-time = "2024-05-28T17:01:53.112Z" },
+]
+
+[[package]]
+name = "beartype"
+version = "0.22.9"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/c7/94/1009e248bbfbab11397abca7193bea6626806be9a327d399810d523a07cb/beartype-0.22.9.tar.gz", hash = "sha256:8f82b54aa723a2848a56008d18875f91c1db02c32ef6a62319a002e3e25a975f", size = 1608866, upload-time = "2025-12-13T06:50:30.72Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/71/cc/18245721fa7747065ab478316c7fea7c74777d07f37ae60db2e84f8172e8/beartype-0.22.9-py3-none-any.whl", hash = "sha256:d16c9bbc61ea14637596c5f6fbff2ee99cbe3573e46a716401734ef50c3060c2", size = 1333658, upload-time = "2025-12-13T06:50:28.266Z" },
+]
+
+[[package]]
+name = "cachetools"
+version = "7.0.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/48/5c/3b882b82e9af737906539a2eafb62f96a229f1fa80255bede0c7b554cbc4/cachetools-7.0.3.tar.gz", hash = "sha256:8c246313b95849964e54a909c03b327a87ab0428b068fac10da7b105ca275ef6", size = 37187, upload-time = "2026-03-05T21:00:57.918Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/05/4a/573185481c50a8841331f54ddae44e4a3469c46aa0b397731c53a004369a/cachetools-7.0.3-py3-none-any.whl", hash = "sha256:c128ffca156eef344c25fcd08a96a5952803786fa33097f5f2d49edf76f79d53", size = 13907, upload-time = "2026-03-05T21:00:56.486Z" },
+]
+
+[[package]]
+name = "caio"
+version = "0.9.25"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/92/88/b8527e1b00c1811db339a1df8bd1ae49d146fcea9d6a5c40e3a80aaeb38d/caio-0.9.25.tar.gz", hash = "sha256:16498e7f81d1d0f5a4c0ad3f2540e65fe25691376e0a5bd367f558067113ed10", size = 26781, upload-time = "2025-12-26T15:21:36.501Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ec/90/543f556fcfcfa270713eef906b6352ab048e1e557afec12925c991dc93c2/caio-0.9.25-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d6956d9e4a27021c8bd6c9677f3a59eb1d820cc32d0343cea7961a03b1371965", size = 36839, upload-time = "2025-12-26T15:21:40.267Z" },
+ { url = "https://files.pythonhosted.org/packages/51/3b/36f3e8ec38dafe8de4831decd2e44c69303d2a3892d16ceda42afed44e1b/caio-0.9.25-cp311-cp311-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bf84bfa039f25ad91f4f52944452a5f6f405e8afab4d445450978cd6241d1478", size = 80255, upload-time = "2025-12-26T15:22:20.271Z" },
+ { url = "https://files.pythonhosted.org/packages/df/ce/65e64867d928e6aff1b4f0e12dba0ef6d5bf412c240dc1df9d421ac10573/caio-0.9.25-cp311-cp311-manylinux_2_34_aarch64.whl", hash = "sha256:ae3d62587332bce600f861a8de6256b1014d6485cfd25d68c15caf1611dd1f7c", size = 80052, upload-time = "2026-03-04T22:08:20.402Z" },
+ { url = "https://files.pythonhosted.org/packages/46/90/e278863c47e14ec58309aa2e38a45882fbe67b4cc29ec9bc8f65852d3e45/caio-0.9.25-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:fc220b8533dcf0f238a6b1a4a937f92024c71e7b10b5a2dfc1c73604a25709bc", size = 78273, upload-time = "2026-03-04T22:08:21.368Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/25/79c98ebe12df31548ba4eaf44db11b7cad6b3e7b4203718335620939083c/caio-0.9.25-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fb7ff95af4c31ad3f03179149aab61097a71fd85e05f89b4786de0359dffd044", size = 36983, upload-time = "2025-12-26T15:21:36.075Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/2b/21288691f16d479945968a0a4f2856818c1c5be56881d51d4dac9b255d26/caio-0.9.25-cp312-cp312-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:97084e4e30dfa598449d874c4d8e0c8d5ea17d2f752ef5e48e150ff9d240cd64", size = 82012, upload-time = "2025-12-26T15:22:20.983Z" },
+ { url = "https://files.pythonhosted.org/packages/03/c4/8a1b580875303500a9c12b9e0af58cb82e47f5bcf888c2457742a138273c/caio-0.9.25-cp312-cp312-manylinux_2_34_aarch64.whl", hash = "sha256:4fa69eba47e0f041b9d4f336e2ad40740681c43e686b18b191b6c5f4c5544bfb", size = 81502, upload-time = "2026-03-04T22:08:22.381Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/1c/0fe770b8ffc8362c48134d1592d653a81a3d8748d764bec33864db36319d/caio-0.9.25-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:6bebf6f079f1341d19f7386db9b8b1f07e8cc15ae13bfdaff573371ba0575d69", size = 80200, upload-time = "2026-03-04T22:08:23.382Z" },
+ { url = "https://files.pythonhosted.org/packages/31/57/5e6ff127e6f62c9f15d989560435c642144aa4210882f9494204bc892305/caio-0.9.25-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d6c2a3411af97762a2b03840c3cec2f7f728921ff8adda53d7ea2315a8563451", size = 36979, upload-time = "2025-12-26T15:21:35.484Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/9f/f21af50e72117eb528c422d4276cbac11fb941b1b812b182e0a9c70d19c5/caio-0.9.25-cp313-cp313-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0998210a4d5cd5cb565b32ccfe4e53d67303f868a76f212e002a8554692870e6", size = 81900, upload-time = "2025-12-26T15:22:21.919Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/12/c39ae2a4037cb10ad5eb3578eb4d5f8c1a2575c62bba675f3406b7ef0824/caio-0.9.25-cp313-cp313-manylinux_2_34_aarch64.whl", hash = "sha256:1a177d4777141b96f175fe2c37a3d96dec7911ed9ad5f02bac38aaa1c936611f", size = 81523, upload-time = "2026-03-04T22:08:25.187Z" },
+ { url = "https://files.pythonhosted.org/packages/22/59/f8f2e950eb4f1a5a3883e198dca514b9d475415cb6cd7b78b9213a0dd45a/caio-0.9.25-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:9ed3cfb28c0e99fec5e208c934e5c157d0866aa9c32aa4dc5e9b6034af6286b7", size = 80243, upload-time = "2026-03-04T22:08:26.449Z" },
+ { url = "https://files.pythonhosted.org/packages/69/ca/a08fdc7efdcc24e6a6131a93c85be1f204d41c58f474c42b0670af8c016b/caio-0.9.25-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fab6078b9348e883c80a5e14b382e6ad6aabbc4429ca034e76e730cf464269db", size = 36978, upload-time = "2025-12-26T15:21:41.055Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/6c/d4d24f65e690213c097174d26eda6831f45f4734d9d036d81790a27e7b78/caio-0.9.25-cp314-cp314-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:44a6b58e52d488c75cfaa5ecaa404b2b41cc965e6c417e03251e868ecd5b6d77", size = 81832, upload-time = "2025-12-26T15:22:22.757Z" },
+ { url = "https://files.pythonhosted.org/packages/87/a4/e534cf7d2d0e8d880e25dd61e8d921ffcfe15bd696734589826f5a2df727/caio-0.9.25-cp314-cp314-manylinux_2_34_aarch64.whl", hash = "sha256:628a630eb7fb22381dd8e3c8ab7f59e854b9c806639811fc3f4310c6bd711d79", size = 81565, upload-time = "2026-03-04T22:08:27.483Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/ed/bf81aeac1d290017e5e5ac3e880fd56ee15e50a6d0353986799d1bc5cfd5/caio-0.9.25-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:0ba16aa605ccb174665357fc729cf500679c2d94d5f1458a6f0d5ca48f2060a7", size = 80071, upload-time = "2026-03-04T22:08:28.751Z" },
+ { url = "https://files.pythonhosted.org/packages/86/93/1f76c8d1bafe3b0614e06b2195784a3765bbf7b0a067661af9e2dd47fc33/caio-0.9.25-py3-none-any.whl", hash = "sha256:06c0bb02d6b929119b1cfbe1ca403c768b2013a369e2db46bfa2a5761cf82e40", size = 19087, upload-time = "2025-12-26T15:22:00.221Z" },
+]
+
+[[package]]
+name = "certifi"
+version = "2025.1.31"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577, upload-time = "2025-01-31T02:16:47.166Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393, upload-time = "2025-01-31T02:16:45.015Z" },
+]
+
+[[package]]
+name = "cffi"
+version = "1.17.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pycparser" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264, upload-time = "2024-09-04T20:43:51.124Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651, upload-time = "2024-09-04T20:43:52.872Z" },
+ { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259, upload-time = "2024-09-04T20:43:56.123Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200, upload-time = "2024-09-04T20:43:57.891Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235, upload-time = "2024-09-04T20:44:00.18Z" },
+ { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721, upload-time = "2024-09-04T20:44:01.585Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242, upload-time = "2024-09-04T20:44:03.467Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999, upload-time = "2024-09-04T20:44:05.023Z" },
+ { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242, upload-time = "2024-09-04T20:44:06.444Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604, upload-time = "2024-09-04T20:44:08.206Z" },
+ { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727, upload-time = "2024-09-04T20:44:09.481Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400, upload-time = "2024-09-04T20:44:10.873Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178, upload-time = "2024-09-04T20:44:12.232Z" },
+ { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840, upload-time = "2024-09-04T20:44:13.739Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload-time = "2024-09-04T20:44:15.231Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850, upload-time = "2024-09-04T20:44:17.188Z" },
+ { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729, upload-time = "2024-09-04T20:44:18.688Z" },
+ { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256, upload-time = "2024-09-04T20:44:20.248Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424, upload-time = "2024-09-04T20:44:21.673Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568, upload-time = "2024-09-04T20:44:23.245Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736, upload-time = "2024-09-04T20:44:24.757Z" },
+ { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448, upload-time = "2024-09-04T20:44:26.208Z" },
+ { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976, upload-time = "2024-09-04T20:44:27.578Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989, upload-time = "2024-09-04T20:44:28.956Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802, upload-time = "2024-09-04T20:44:30.289Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload-time = "2024-09-04T20:44:32.01Z" },
+ { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload-time = "2024-09-04T20:44:33.606Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload-time = "2024-09-04T20:44:35.191Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200, upload-time = "2024-09-04T20:44:36.743Z" },
+ { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447, upload-time = "2024-09-04T20:44:38.492Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358, upload-time = "2024-09-04T20:44:40.046Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469, upload-time = "2024-09-04T20:44:41.616Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475, upload-time = "2024-09-04T20:44:43.733Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009, upload-time = "2024-09-04T20:44:45.309Z" },
+]
+
+[[package]]
+name = "click"
+version = "8.1.8"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" },
+]
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
+]
+
+[[package]]
+name = "cryptography"
+version = "44.0.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/cd/25/4ce80c78963834b8a9fd1cc1266be5ed8d1840785c0f2e1b73b8d128d505/cryptography-44.0.2.tar.gz", hash = "sha256:c63454aa261a0cf0c5b4718349629793e9e634993538db841165b3df74f37ec0", size = 710807, upload-time = "2025-03-02T00:01:37.692Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/92/ef/83e632cfa801b221570c5f58c0369db6fa6cef7d9ff859feab1aae1a8a0f/cryptography-44.0.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:efcfe97d1b3c79e486554efddeb8f6f53a4cdd4cf6086642784fa31fc384e1d7", size = 6676361, upload-time = "2025-03-02T00:00:06.528Z" },
+ { url = "https://files.pythonhosted.org/packages/30/ec/7ea7c1e4c8fc8329506b46c6c4a52e2f20318425d48e0fe597977c71dbce/cryptography-44.0.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29ecec49f3ba3f3849362854b7253a9f59799e3763b0c9d0826259a88efa02f1", size = 3952350, upload-time = "2025-03-02T00:00:09.537Z" },
+ { url = "https://files.pythonhosted.org/packages/27/61/72e3afdb3c5ac510330feba4fc1faa0fe62e070592d6ad00c40bb69165e5/cryptography-44.0.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc821e161ae88bfe8088d11bb39caf2916562e0a2dc7b6d56714a48b784ef0bb", size = 4166572, upload-time = "2025-03-02T00:00:12.03Z" },
+ { url = "https://files.pythonhosted.org/packages/26/e4/ba680f0b35ed4a07d87f9e98f3ebccb05091f3bf6b5a478b943253b3bbd5/cryptography-44.0.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3c00b6b757b32ce0f62c574b78b939afab9eecaf597c4d624caca4f9e71e7843", size = 3958124, upload-time = "2025-03-02T00:00:14.518Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/e8/44ae3e68c8b6d1cbc59040288056df2ad7f7f03bbcaca6b503c737ab8e73/cryptography-44.0.2-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7bdcd82189759aba3816d1f729ce42ffded1ac304c151d0a8e89b9996ab863d5", size = 3678122, upload-time = "2025-03-02T00:00:17.212Z" },
+ { url = "https://files.pythonhosted.org/packages/27/7b/664ea5e0d1eab511a10e480baf1c5d3e681c7d91718f60e149cec09edf01/cryptography-44.0.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:4973da6ca3db4405c54cd0b26d328be54c7747e89e284fcff166132eb7bccc9c", size = 4191831, upload-time = "2025-03-02T00:00:19.696Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/07/79554a9c40eb11345e1861f46f845fa71c9e25bf66d132e123d9feb8e7f9/cryptography-44.0.2-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4e389622b6927d8133f314949a9812972711a111d577a5d1f4bee5e58736b80a", size = 3960583, upload-time = "2025-03-02T00:00:22.488Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/6d/858e356a49a4f0b591bd6789d821427de18432212e137290b6d8a817e9bf/cryptography-44.0.2-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f514ef4cd14bb6fb484b4a60203e912cfcb64f2ab139e88c2274511514bf7308", size = 4191753, upload-time = "2025-03-02T00:00:25.038Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/80/62df41ba4916067fa6b125aa8c14d7e9181773f0d5d0bd4dcef580d8b7c6/cryptography-44.0.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1bc312dfb7a6e5d66082c87c34c8a62176e684b6fe3d90fcfe1568de675e6688", size = 4079550, upload-time = "2025-03-02T00:00:26.929Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/cd/2558cc08f7b1bb40683f99ff4327f8dcfc7de3affc669e9065e14824511b/cryptography-44.0.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b721b8b4d948b218c88cb8c45a01793483821e709afe5f622861fc6182b20a7", size = 4298367, upload-time = "2025-03-02T00:00:28.735Z" },
+ { url = "https://files.pythonhosted.org/packages/71/59/94ccc74788945bc3bd4cf355d19867e8057ff5fdbcac781b1ff95b700fb1/cryptography-44.0.2-cp37-abi3-win32.whl", hash = "sha256:51e4de3af4ec3899d6d178a8c005226491c27c4ba84101bfb59c901e10ca9f79", size = 2772843, upload-time = "2025-03-02T00:00:30.592Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/2c/0d0bbaf61ba05acb32f0841853cfa33ebb7a9ab3d9ed8bb004bd39f2da6a/cryptography-44.0.2-cp37-abi3-win_amd64.whl", hash = "sha256:c505d61b6176aaf982c5717ce04e87da5abc9a36a5b39ac03905c4aafe8de7aa", size = 3209057, upload-time = "2025-03-02T00:00:33.393Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/be/7a26142e6d0f7683d8a382dd963745e65db895a79a280a30525ec92be890/cryptography-44.0.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8e0ddd63e6bf1161800592c71ac794d3fb8001f2caebe0966e77c5234fa9efc3", size = 6677789, upload-time = "2025-03-02T00:00:36.009Z" },
+ { url = "https://files.pythonhosted.org/packages/06/88/638865be7198a84a7713950b1db7343391c6066a20e614f8fa286eb178ed/cryptography-44.0.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81276f0ea79a208d961c433a947029e1a15948966658cf6710bbabb60fcc2639", size = 3951919, upload-time = "2025-03-02T00:00:38.581Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/fc/99fe639bcdf58561dfad1faa8a7369d1dc13f20acd78371bb97a01613585/cryptography-44.0.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a1e657c0f4ea2a23304ee3f964db058c9e9e635cc7019c4aa21c330755ef6fd", size = 4167812, upload-time = "2025-03-02T00:00:42.934Z" },
+ { url = "https://files.pythonhosted.org/packages/53/7b/aafe60210ec93d5d7f552592a28192e51d3c6b6be449e7fd0a91399b5d07/cryptography-44.0.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6210c05941994290f3f7f175a4a57dbbb2afd9273657614c506d5976db061181", size = 3958571, upload-time = "2025-03-02T00:00:46.026Z" },
+ { url = "https://files.pythonhosted.org/packages/16/32/051f7ce79ad5a6ef5e26a92b37f172ee2d6e1cce09931646eef8de1e9827/cryptography-44.0.2-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1c3572526997b36f245a96a2b1713bf79ce99b271bbcf084beb6b9b075f29ea", size = 3679832, upload-time = "2025-03-02T00:00:48.647Z" },
+ { url = "https://files.pythonhosted.org/packages/78/2b/999b2a1e1ba2206f2d3bca267d68f350beb2b048a41ea827e08ce7260098/cryptography-44.0.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b042d2a275c8cee83a4b7ae30c45a15e6a4baa65a179a0ec2d78ebb90e4f6699", size = 4193719, upload-time = "2025-03-02T00:00:51.397Z" },
+ { url = "https://files.pythonhosted.org/packages/72/97/430e56e39a1356e8e8f10f723211a0e256e11895ef1a135f30d7d40f2540/cryptography-44.0.2-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:d03806036b4f89e3b13b6218fefea8d5312e450935b1a2d55f0524e2ed7c59d9", size = 3960852, upload-time = "2025-03-02T00:00:53.317Z" },
+ { url = "https://files.pythonhosted.org/packages/89/33/c1cf182c152e1d262cac56850939530c05ca6c8d149aa0dcee490b417e99/cryptography-44.0.2-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c7362add18b416b69d58c910caa217f980c5ef39b23a38a0880dfd87bdf8cd23", size = 4193906, upload-time = "2025-03-02T00:00:56.49Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/99/87cf26d4f125380dc674233971069bc28d19b07f7755b29861570e513650/cryptography-44.0.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:8cadc6e3b5a1f144a039ea08a0bdb03a2a92e19c46be3285123d32029f40a922", size = 4081572, upload-time = "2025-03-02T00:00:59.995Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/9f/6a3e0391957cc0c5f84aef9fbdd763035f2b52e998a53f99345e3ac69312/cryptography-44.0.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6f101b1f780f7fc613d040ca4bdf835c6ef3b00e9bd7125a4255ec574c7916e4", size = 4298631, upload-time = "2025-03-02T00:01:01.623Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/a5/5bc097adb4b6d22a24dea53c51f37e480aaec3465285c253098642696423/cryptography-44.0.2-cp39-abi3-win32.whl", hash = "sha256:3dc62975e31617badc19a906481deacdeb80b4bb454394b4098e3f2525a488c5", size = 2773792, upload-time = "2025-03-02T00:01:04.133Z" },
+ { url = "https://files.pythonhosted.org/packages/33/cf/1f7649b8b9a3543e042d3f348e398a061923ac05b507f3f4d95f11938aa9/cryptography-44.0.2-cp39-abi3-win_amd64.whl", hash = "sha256:5f6f90b72d8ccadb9c6e311c775c8305381db88374c65fa1a68250aa8a9cb3a6", size = 3210957, upload-time = "2025-03-02T00:01:06.987Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/d7/f30e75a6aa7d0f65031886fa4a1485c2fbfe25a1896953920f6a9cfe2d3b/cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:909c97ab43a9c0c0b0ada7a1281430e4e5ec0458e6d9244c0e821bbf152f061d", size = 3887513, upload-time = "2025-03-02T00:01:22.911Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/b4/7a494ce1032323ca9db9a3661894c66e0d7142ad2079a4249303402d8c71/cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:96e7a5e9d6e71f9f4fca8eebfd603f8e86c5225bb18eb621b2c1e50b290a9471", size = 4107432, upload-time = "2025-03-02T00:01:24.701Z" },
+ { url = "https://files.pythonhosted.org/packages/45/f8/6b3ec0bc56123b344a8d2b3264a325646d2dcdbdd9848b5e6f3d37db90b3/cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d1b3031093a366ac767b3feb8bcddb596671b3aaff82d4050f984da0c248b615", size = 3891421, upload-time = "2025-03-02T00:01:26.335Z" },
+ { url = "https://files.pythonhosted.org/packages/57/ff/f3b4b2d007c2a646b0f69440ab06224f9cf37a977a72cdb7b50632174e8a/cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:04abd71114848aa25edb28e225ab5f268096f44cf0127f3d36975bdf1bdf3390", size = 4107081, upload-time = "2025-03-02T00:01:28.938Z" },
+]
+
+[[package]]
+name = "cyclopts"
+version = "4.7.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "attrs" },
+ { name = "docstring-parser" },
+ { name = "rich" },
+ { name = "rich-rst" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b3/a7/61825c9c46dd9d3d2a231c9792753fc3fe2822a90734a619b1a23ed0f05f/cyclopts-4.7.0.tar.gz", hash = "sha256:1d0fd440b8d21a55d14f830033eb1ac156933424df3e90afeea34cfb3ed73822", size = 163447, upload-time = "2026-03-05T02:57:49.501Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5b/08/a631a99df0e9f49c73ec682a9d1e05e5887cf79f04076792aacb4caac6b2/cyclopts-4.7.0-py3-none-any.whl", hash = "sha256:c659d930797a8470f2914a8f8f8be263b339cb6ffb6593b4a59fa9d84b8e0e38", size = 201270, upload-time = "2026-03-05T02:57:50.988Z" },
+]
+
+[[package]]
+name = "dnspython"
+version = "2.8.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" },
+]
+
+[[package]]
+name = "docstring-parser"
+version = "0.17.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442, upload-time = "2025-07-21T07:35:01.868Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" },
+]
+
+[[package]]
+name = "docutils"
+version = "0.22.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" },
+]
+
+[[package]]
+name = "docx2pdf"
+version = "0.1.8"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "appscript", marker = "sys_platform == 'darwin'" },
+ { name = "pywin32", marker = "sys_platform == 'win32'" },
+ { name = "tqdm" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ab/5d/112531fff53cf60513e14fa1707755c874d47880ec4de7b2235302ad19a0/docx2pdf-0.1.8.tar.gz", hash = "sha256:6d2c20f9ad36eec75f4da017dc7a97622946954a6124ca0b11772875fa86fbed", size = 6483, upload-time = "2021-12-11T16:56:36.75Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/53/4f/1155781308281e67f80b829738a29e5354e03664c62311f753056afc873b/docx2pdf-0.1.8-py3-none-any.whl", hash = "sha256:00be1401fd486640314e993423a0a1cbdbc21142186f68549d962d505b2e8a12", size = 6741, upload-time = "2021-12-11T16:56:35.163Z" },
+]
+
+[[package]]
+name = "email-validator"
+version = "2.3.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "dnspython" },
+ { name = "idna" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238, upload-time = "2025-08-26T13:09:06.831Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" },
+]
+
+[[package]]
+name = "exceptiongroup"
+version = "1.3.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
+]
+
+[[package]]
+name = "fastmcp"
+version = "3.1.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "authlib" },
+ { name = "cyclopts" },
+ { name = "exceptiongroup" },
+ { name = "httpx" },
+ { name = "jsonref" },
+ { name = "jsonschema-path" },
+ { name = "mcp" },
+ { name = "openapi-pydantic" },
+ { name = "opentelemetry-api" },
+ { name = "packaging" },
+ { name = "platformdirs" },
+ { name = "py-key-value-aio", extra = ["filetree", "keyring", "memory"] },
+ { name = "pydantic", extra = ["email"] },
+ { name = "pyperclip" },
+ { name = "python-dotenv" },
+ { name = "pyyaml" },
+ { name = "rich" },
+ { name = "uncalled-for" },
+ { name = "uvicorn" },
+ { name = "watchfiles" },
+ { name = "websockets" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/0a/70/862026c4589441f86ad3108f05bfb2f781c6b322ad60a982f40b303b47d7/fastmcp-3.1.0.tar.gz", hash = "sha256:e25264794c734b9977502a51466961eeecff92a0c2f3b49c40c070993628d6d0", size = 17347083, upload-time = "2026-03-03T02:43:11.283Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/17/07/516f5b20d88932e5a466c2216b628e5358a71b3a9f522215607c3281de05/fastmcp-3.1.0-py3-none-any.whl", hash = "sha256:b1f73b56fd3b0cb2bd9e2a144fc650d5cc31587ed129d996db7710e464ae8010", size = 633749, upload-time = "2026-03-03T02:43:09.06Z" },
+]
+
+[[package]]
+name = "h11"
+version = "0.14.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418, upload-time = "2022-09-25T15:40:01.519Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259, upload-time = "2022-09-25T15:39:59.68Z" },
+]
+
+[[package]]
+name = "httpcore"
+version = "1.0.8"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "h11" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/9f/45/ad3e1b4d448f22c0cff4f5692f5ed0666658578e358b8d58a19846048059/httpcore-1.0.8.tar.gz", hash = "sha256:86e94505ed24ea06514883fd44d2bc02d90e77e7979c8eb71b90f41d364a1bad", size = 85385, upload-time = "2025-04-11T14:42:46.661Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/18/8d/f052b1e336bb2c1fc7ed1aaed898aa570c0b61a09707b108979d9fc6e308/httpcore-1.0.8-py3-none-any.whl", hash = "sha256:5254cf149bcb5f75e9d1b2b9f729ea4a4b883d1ad7379fc632b727cec23674be", size = 78732, upload-time = "2025-04-11T14:42:44.896Z" },
+]
+
+[[package]]
+name = "httpx"
+version = "0.28.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "certifi" },
+ { name = "httpcore" },
+ { name = "idna" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
+]
+
+[[package]]
+name = "httpx-sse"
+version = "0.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624, upload-time = "2023-12-22T08:01:21.083Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819, upload-time = "2023-12-22T08:01:19.89Z" },
+]
+
+[[package]]
+name = "idna"
+version = "3.10"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
+]
+
+[[package]]
+name = "importlib-metadata"
+version = "8.7.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "zipp" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" },
+]
+
+[[package]]
+name = "iniconfig"
+version = "2.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
+]
+
+[[package]]
+name = "jaraco-classes"
+version = "3.4.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "more-itertools" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780, upload-time = "2024-03-31T07:27:36.643Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777, upload-time = "2024-03-31T07:27:34.792Z" },
+]
+
+[[package]]
+name = "jaraco-context"
+version = "6.1.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "backports-tarfile", marker = "python_full_version < '3.12'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/cb/9c/a788f5bb29c61e456b8ee52ce76dbdd32fd72cd73dd67bc95f42c7a8d13c/jaraco_context-6.1.0.tar.gz", hash = "sha256:129a341b0a85a7db7879e22acd66902fda67882db771754574338898b2d5d86f", size = 15850, upload-time = "2026-01-13T02:53:53.847Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8d/48/aa685dbf1024c7bd82bede569e3a85f82c32fd3d79ba5fea578f0159571a/jaraco_context-6.1.0-py3-none-any.whl", hash = "sha256:a43b5ed85815223d0d3cfdb6d7ca0d2bc8946f28f30b6f3216bda070f68badda", size = 7065, upload-time = "2026-01-13T02:53:53.031Z" },
+]
+
+[[package]]
+name = "jaraco-functools"
+version = "4.4.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "more-itertools" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/0f/27/056e0638a86749374d6f57d0b0db39f29509cce9313cf91bdc0ac4d91084/jaraco_functools-4.4.0.tar.gz", hash = "sha256:da21933b0417b89515562656547a77b4931f98176eb173644c0d35032a33d6bb", size = 19943, upload-time = "2025-12-21T09:29:43.6Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl", hash = "sha256:9eec1e36f45c818d9bf307c8948eb03b2b56cd44087b3cdc989abca1f20b9176", size = 10481, upload-time = "2025-12-21T09:29:42.27Z" },
+]
+
+[[package]]
+name = "jeepney"
+version = "0.9.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758, upload-time = "2025-02-27T18:51:01.684Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" },
+]
+
+[[package]]
+name = "jsonref"
+version = "1.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/aa/0d/c1f3277e90ccdb50d33ed5ba1ec5b3f0a242ed8c1b1a85d3afeb68464dca/jsonref-1.1.0.tar.gz", hash = "sha256:32fe8e1d85af0fdefbebce950af85590b22b60f9e95443176adbde4e1ecea552", size = 8814, upload-time = "2023-01-16T16:10:04.455Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0c/ec/e1db9922bceb168197a558a2b8c03a7963f1afe93517ddd3cf99f202f996/jsonref-1.1.0-py3-none-any.whl", hash = "sha256:590dc7773df6c21cbf948b5dac07a72a251db28b0238ceecce0a2abfa8ec30a9", size = 9425, upload-time = "2023-01-16T16:10:02.255Z" },
+]
+
+[[package]]
+name = "jsonschema"
+version = "4.26.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "attrs" },
+ { name = "jsonschema-specifications" },
+ { name = "referencing" },
+ { name = "rpds-py" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" },
+]
+
+[[package]]
+name = "jsonschema-path"
+version = "0.4.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pathable" },
+ { name = "pyyaml" },
+ { name = "referencing" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/5b/8a/7e6102f2b8bdc6705a9eb5294f8f6f9ccd3a8420e8e8e19671d1dd773251/jsonschema_path-0.4.5.tar.gz", hash = "sha256:c6cd7d577ae290c7defd4f4029e86fdb248ca1bd41a07557795b3c95e5144918", size = 15113, upload-time = "2026-03-03T09:56:46.87Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/04/d5/4e96c44f6c1ea3d812cf5391d81a4f5abaa540abf8d04ecd7f66e0ed11df/jsonschema_path-0.4.5-py3-none-any.whl", hash = "sha256:7d77a2c3f3ec569a40efe5c5f942c44c1af2a6f96fe0866794c9ef5b8f87fd65", size = 19368, upload-time = "2026-03-03T09:56:45.39Z" },
+]
+
+[[package]]
+name = "jsonschema-specifications"
+version = "2025.9.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "referencing" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" },
+]
+
+[[package]]
+name = "keyring"
+version = "25.7.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "importlib-metadata", marker = "python_full_version < '3.12'" },
+ { name = "jaraco-classes" },
+ { name = "jaraco-context" },
+ { name = "jaraco-functools" },
+ { name = "jeepney", marker = "sys_platform == 'linux'" },
+ { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" },
+ { name = "secretstorage", marker = "sys_platform == 'linux'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/43/4b/674af6ef2f97d56f0ab5153bf0bfa28ccb6c3ed4d1babf4305449668807b/keyring-25.7.0.tar.gz", hash = "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b", size = 63516, upload-time = "2025-11-16T16:26:09.482Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f", size = 39160, upload-time = "2025-11-16T16:26:08.402Z" },
+]
+
+[[package]]
+name = "lxml"
+version = "5.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/76/3d/14e82fc7c8fb1b7761f7e748fd47e2ec8276d137b6acfe5a4bb73853e08f/lxml-5.4.0.tar.gz", hash = "sha256:d12832e1dbea4be280b22fd0ea7c9b87f0d8fc51ba06e92dc62d52f804f78ebd", size = 3679479, upload-time = "2025-04-23T01:50:29.322Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/81/2d/67693cc8a605a12e5975380d7ff83020dcc759351b5a066e1cced04f797b/lxml-5.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:98a3912194c079ef37e716ed228ae0dcb960992100461b704aea4e93af6b0bb9", size = 8083240, upload-time = "2025-04-23T01:45:18.566Z" },
+ { url = "https://files.pythonhosted.org/packages/73/53/b5a05ab300a808b72e848efd152fe9c022c0181b0a70b8bca1199f1bed26/lxml-5.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0ea0252b51d296a75f6118ed0d8696888e7403408ad42345d7dfd0d1e93309a7", size = 4387685, upload-time = "2025-04-23T01:45:21.387Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/cb/1a3879c5f512bdcd32995c301886fe082b2edd83c87d41b6d42d89b4ea4d/lxml-5.4.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b92b69441d1bd39f4940f9eadfa417a25862242ca2c396b406f9272ef09cdcaa", size = 4991164, upload-time = "2025-04-23T01:45:23.849Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/94/bbc66e42559f9d04857071e3b3d0c9abd88579367fd2588a4042f641f57e/lxml-5.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20e16c08254b9b6466526bc1828d9370ee6c0d60a4b64836bc3ac2917d1e16df", size = 4746206, upload-time = "2025-04-23T01:45:26.361Z" },
+ { url = "https://files.pythonhosted.org/packages/66/95/34b0679bee435da2d7cae895731700e519a8dfcab499c21662ebe671603e/lxml-5.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7605c1c32c3d6e8c990dd28a0970a3cbbf1429d5b92279e37fda05fb0c92190e", size = 5342144, upload-time = "2025-04-23T01:45:28.939Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/5d/abfcc6ab2fa0be72b2ba938abdae1f7cad4c632f8d552683ea295d55adfb/lxml-5.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ecf4c4b83f1ab3d5a7ace10bafcb6f11df6156857a3c418244cef41ca9fa3e44", size = 4825124, upload-time = "2025-04-23T01:45:31.361Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/78/6bd33186c8863b36e084f294fc0a5e5eefe77af95f0663ef33809cc1c8aa/lxml-5.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0cef4feae82709eed352cd7e97ae062ef6ae9c7b5dbe3663f104cd2c0e8d94ba", size = 4876520, upload-time = "2025-04-23T01:45:34.191Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/74/4d7ad4839bd0fc64e3d12da74fc9a193febb0fae0ba6ebd5149d4c23176a/lxml-5.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:df53330a3bff250f10472ce96a9af28628ff1f4efc51ccba351a8820bca2a8ba", size = 4765016, upload-time = "2025-04-23T01:45:36.7Z" },
+ { url = "https://files.pythonhosted.org/packages/24/0d/0a98ed1f2471911dadfc541003ac6dd6879fc87b15e1143743ca20f3e973/lxml-5.4.0-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:aefe1a7cb852fa61150fcb21a8c8fcea7b58c4cb11fbe59c97a0a4b31cae3c8c", size = 5362884, upload-time = "2025-04-23T01:45:39.291Z" },
+ { url = "https://files.pythonhosted.org/packages/48/de/d4f7e4c39740a6610f0f6959052b547478107967362e8424e1163ec37ae8/lxml-5.4.0-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:ef5a7178fcc73b7d8c07229e89f8eb45b2908a9238eb90dcfc46571ccf0383b8", size = 4902690, upload-time = "2025-04-23T01:45:42.386Z" },
+ { url = "https://files.pythonhosted.org/packages/07/8c/61763abd242af84f355ca4ef1ee096d3c1b7514819564cce70fd18c22e9a/lxml-5.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d2ed1b3cb9ff1c10e6e8b00941bb2e5bb568b307bfc6b17dffbbe8be5eecba86", size = 4944418, upload-time = "2025-04-23T01:45:46.051Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/c5/6d7e3b63e7e282619193961a570c0a4c8a57fe820f07ca3fe2f6bd86608a/lxml-5.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:72ac9762a9f8ce74c9eed4a4e74306f2f18613a6b71fa065495a67ac227b3056", size = 4827092, upload-time = "2025-04-23T01:45:48.943Z" },
+ { url = "https://files.pythonhosted.org/packages/71/4a/e60a306df54680b103348545706a98a7514a42c8b4fbfdcaa608567bb065/lxml-5.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f5cb182f6396706dc6cc1896dd02b1c889d644c081b0cdec38747573db88a7d7", size = 5418231, upload-time = "2025-04-23T01:45:51.481Z" },
+ { url = "https://files.pythonhosted.org/packages/27/f2/9754aacd6016c930875854f08ac4b192a47fe19565f776a64004aa167521/lxml-5.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:3a3178b4873df8ef9457a4875703488eb1622632a9cee6d76464b60e90adbfcd", size = 5261798, upload-time = "2025-04-23T01:45:54.146Z" },
+ { url = "https://files.pythonhosted.org/packages/38/a2/0c49ec6941428b1bd4f280650d7b11a0f91ace9db7de32eb7aa23bcb39ff/lxml-5.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e094ec83694b59d263802ed03a8384594fcce477ce484b0cbcd0008a211ca751", size = 4988195, upload-time = "2025-04-23T01:45:56.685Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/75/87a3963a08eafc46a86c1131c6e28a4de103ba30b5ae903114177352a3d7/lxml-5.4.0-cp311-cp311-win32.whl", hash = "sha256:4329422de653cdb2b72afa39b0aa04252fca9071550044904b2e7036d9d97fe4", size = 3474243, upload-time = "2025-04-23T01:45:58.863Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/f9/1f0964c4f6c2be861c50db380c554fb8befbea98c6404744ce243a3c87ef/lxml-5.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:fd3be6481ef54b8cfd0e1e953323b7aa9d9789b94842d0e5b142ef4bb7999539", size = 3815197, upload-time = "2025-04-23T01:46:01.096Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/4c/d101ace719ca6a4ec043eb516fcfcb1b396a9fccc4fcd9ef593df34ba0d5/lxml-5.4.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b5aff6f3e818e6bdbbb38e5967520f174b18f539c2b9de867b1e7fde6f8d95a4", size = 8127392, upload-time = "2025-04-23T01:46:04.09Z" },
+ { url = "https://files.pythonhosted.org/packages/11/84/beddae0cec4dd9ddf46abf156f0af451c13019a0fa25d7445b655ba5ccb7/lxml-5.4.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:942a5d73f739ad7c452bf739a62a0f83e2578afd6b8e5406308731f4ce78b16d", size = 4415103, upload-time = "2025-04-23T01:46:07.227Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/25/d0d93a4e763f0462cccd2b8a665bf1e4343dd788c76dcfefa289d46a38a9/lxml-5.4.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:460508a4b07364d6abf53acaa0a90b6d370fafde5693ef37602566613a9b0779", size = 5024224, upload-time = "2025-04-23T01:46:10.237Z" },
+ { url = "https://files.pythonhosted.org/packages/31/ce/1df18fb8f7946e7f3388af378b1f34fcf253b94b9feedb2cec5969da8012/lxml-5.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:529024ab3a505fed78fe3cc5ddc079464e709f6c892733e3f5842007cec8ac6e", size = 4769913, upload-time = "2025-04-23T01:46:12.757Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/62/f4a6c60ae7c40d43657f552f3045df05118636be1165b906d3423790447f/lxml-5.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ca56ebc2c474e8f3d5761debfd9283b8b18c76c4fc0967b74aeafba1f5647f9", size = 5290441, upload-time = "2025-04-23T01:46:16.037Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/aa/04f00009e1e3a77838c7fc948f161b5d2d5de1136b2b81c712a263829ea4/lxml-5.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a81e1196f0a5b4167a8dafe3a66aa67c4addac1b22dc47947abd5d5c7a3f24b5", size = 4820165, upload-time = "2025-04-23T01:46:19.137Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/1f/e0b2f61fa2404bf0f1fdf1898377e5bd1b74cc9b2cf2c6ba8509b8f27990/lxml-5.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00b8686694423ddae324cf614e1b9659c2edb754de617703c3d29ff568448df5", size = 4932580, upload-time = "2025-04-23T01:46:21.963Z" },
+ { url = "https://files.pythonhosted.org/packages/24/a2/8263f351b4ffe0ed3e32ea7b7830f845c795349034f912f490180d88a877/lxml-5.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:c5681160758d3f6ac5b4fea370495c48aac0989d6a0f01bb9a72ad8ef5ab75c4", size = 4759493, upload-time = "2025-04-23T01:46:24.316Z" },
+ { url = "https://files.pythonhosted.org/packages/05/00/41db052f279995c0e35c79d0f0fc9f8122d5b5e9630139c592a0b58c71b4/lxml-5.4.0-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:2dc191e60425ad70e75a68c9fd90ab284df64d9cd410ba8d2b641c0c45bc006e", size = 5324679, upload-time = "2025-04-23T01:46:27.097Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/be/ee99e6314cdef4587617d3b3b745f9356d9b7dd12a9663c5f3b5734b64ba/lxml-5.4.0-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:67f779374c6b9753ae0a0195a892a1c234ce8416e4448fe1e9f34746482070a7", size = 4890691, upload-time = "2025-04-23T01:46:30.009Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/36/239820114bf1d71f38f12208b9c58dec033cbcf80101cde006b9bde5cffd/lxml-5.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:79d5bfa9c1b455336f52343130b2067164040604e41f6dc4d8313867ed540079", size = 4955075, upload-time = "2025-04-23T01:46:32.33Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/e1/1b795cc0b174efc9e13dbd078a9ff79a58728a033142bc6d70a1ee8fc34d/lxml-5.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3d3c30ba1c9b48c68489dc1829a6eede9873f52edca1dda900066542528d6b20", size = 4838680, upload-time = "2025-04-23T01:46:34.852Z" },
+ { url = "https://files.pythonhosted.org/packages/72/48/3c198455ca108cec5ae3662ae8acd7fd99476812fd712bb17f1b39a0b589/lxml-5.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1af80c6316ae68aded77e91cd9d80648f7dd40406cef73df841aa3c36f6907c8", size = 5391253, upload-time = "2025-04-23T01:46:37.608Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/10/5bf51858971c51ec96cfc13e800a9951f3fd501686f4c18d7d84fe2d6352/lxml-5.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4d885698f5019abe0de3d352caf9466d5de2baded00a06ef3f1216c1a58ae78f", size = 5261651, upload-time = "2025-04-23T01:46:40.183Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/11/06710dd809205377da380546f91d2ac94bad9ff735a72b64ec029f706c85/lxml-5.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aea53d51859b6c64e7c51d522c03cc2c48b9b5d6172126854cc7f01aa11f52bc", size = 5024315, upload-time = "2025-04-23T01:46:43.333Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/b0/15b6217834b5e3a59ebf7f53125e08e318030e8cc0d7310355e6edac98ef/lxml-5.4.0-cp312-cp312-win32.whl", hash = "sha256:d90b729fd2732df28130c064aac9bb8aff14ba20baa4aee7bd0795ff1187545f", size = 3486149, upload-time = "2025-04-23T01:46:45.684Z" },
+ { url = "https://files.pythonhosted.org/packages/91/1e/05ddcb57ad2f3069101611bd5f5084157d90861a2ef460bf42f45cced944/lxml-5.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1dc4ca99e89c335a7ed47d38964abcb36c5910790f9bd106f2a8fa2ee0b909d2", size = 3817095, upload-time = "2025-04-23T01:46:48.521Z" },
+ { url = "https://files.pythonhosted.org/packages/87/cb/2ba1e9dd953415f58548506fa5549a7f373ae55e80c61c9041b7fd09a38a/lxml-5.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:773e27b62920199c6197130632c18fb7ead3257fce1ffb7d286912e56ddb79e0", size = 8110086, upload-time = "2025-04-23T01:46:52.218Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/3e/6602a4dca3ae344e8609914d6ab22e52ce42e3e1638c10967568c5c1450d/lxml-5.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ce9c671845de9699904b1e9df95acfe8dfc183f2310f163cdaa91a3535af95de", size = 4404613, upload-time = "2025-04-23T01:46:55.281Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/72/bf00988477d3bb452bef9436e45aeea82bb40cdfb4684b83c967c53909c7/lxml-5.4.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9454b8d8200ec99a224df8854786262b1bd6461f4280064c807303c642c05e76", size = 5012008, upload-time = "2025-04-23T01:46:57.817Z" },
+ { url = "https://files.pythonhosted.org/packages/92/1f/93e42d93e9e7a44b2d3354c462cd784dbaaf350f7976b5d7c3f85d68d1b1/lxml-5.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cccd007d5c95279e529c146d095f1d39ac05139de26c098166c4beb9374b0f4d", size = 4760915, upload-time = "2025-04-23T01:47:00.745Z" },
+ { url = "https://files.pythonhosted.org/packages/45/0b/363009390d0b461cf9976a499e83b68f792e4c32ecef092f3f9ef9c4ba54/lxml-5.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0fce1294a0497edb034cb416ad3e77ecc89b313cff7adbee5334e4dc0d11f422", size = 5283890, upload-time = "2025-04-23T01:47:04.702Z" },
+ { url = "https://files.pythonhosted.org/packages/19/dc/6056c332f9378ab476c88e301e6549a0454dbee8f0ae16847414f0eccb74/lxml-5.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:24974f774f3a78ac12b95e3a20ef0931795ff04dbb16db81a90c37f589819551", size = 4812644, upload-time = "2025-04-23T01:47:07.833Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/8a/f8c66bbb23ecb9048a46a5ef9b495fd23f7543df642dabeebcb2eeb66592/lxml-5.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:497cab4d8254c2a90bf988f162ace2ddbfdd806fce3bda3f581b9d24c852e03c", size = 4921817, upload-time = "2025-04-23T01:47:10.317Z" },
+ { url = "https://files.pythonhosted.org/packages/04/57/2e537083c3f381f83d05d9b176f0d838a9e8961f7ed8ddce3f0217179ce3/lxml-5.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:e794f698ae4c5084414efea0f5cc9f4ac562ec02d66e1484ff822ef97c2cadff", size = 4753916, upload-time = "2025-04-23T01:47:12.823Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/80/ea8c4072109a350848f1157ce83ccd9439601274035cd045ac31f47f3417/lxml-5.4.0-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:2c62891b1ea3094bb12097822b3d44b93fc6c325f2043c4d2736a8ff09e65f60", size = 5289274, upload-time = "2025-04-23T01:47:15.916Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/47/c4be287c48cdc304483457878a3f22999098b9a95f455e3c4bda7ec7fc72/lxml-5.4.0-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:142accb3e4d1edae4b392bd165a9abdee8a3c432a2cca193df995bc3886249c8", size = 4874757, upload-time = "2025-04-23T01:47:19.793Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/04/6ef935dc74e729932e39478e44d8cfe6a83550552eaa072b7c05f6f22488/lxml-5.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:1a42b3a19346e5601d1b8296ff6ef3d76038058f311902edd574461e9c036982", size = 4947028, upload-time = "2025-04-23T01:47:22.401Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/f9/c33fc8daa373ef8a7daddb53175289024512b6619bc9de36d77dca3df44b/lxml-5.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4291d3c409a17febf817259cb37bc62cb7eb398bcc95c1356947e2871911ae61", size = 4834487, upload-time = "2025-04-23T01:47:25.513Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/30/fc92bb595bcb878311e01b418b57d13900f84c2b94f6eca9e5073ea756e6/lxml-5.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4f5322cf38fe0e21c2d73901abf68e6329dc02a4994e483adbcf92b568a09a54", size = 5381688, upload-time = "2025-04-23T01:47:28.454Z" },
+ { url = "https://files.pythonhosted.org/packages/43/d1/3ba7bd978ce28bba8e3da2c2e9d5ae3f8f521ad3f0ca6ea4788d086ba00d/lxml-5.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:0be91891bdb06ebe65122aa6bf3fc94489960cf7e03033c6f83a90863b23c58b", size = 5242043, upload-time = "2025-04-23T01:47:31.208Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/cd/95fa2201041a610c4d08ddaf31d43b98ecc4b1d74b1e7245b1abdab443cb/lxml-5.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:15a665ad90054a3d4f397bc40f73948d48e36e4c09f9bcffc7d90c87410e478a", size = 5021569, upload-time = "2025-04-23T01:47:33.805Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/a6/31da006fead660b9512d08d23d31e93ad3477dd47cc42e3285f143443176/lxml-5.4.0-cp313-cp313-win32.whl", hash = "sha256:d5663bc1b471c79f5c833cffbc9b87d7bf13f87e055a5c86c363ccd2348d7e82", size = 3485270, upload-time = "2025-04-23T01:47:36.133Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/14/c115516c62a7d2499781d2d3d7215218c0731b2c940753bf9f9b7b73924d/lxml-5.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:bcb7a1096b4b6b24ce1ac24d4942ad98f983cd3810f9711bcd0293f43a9d8b9f", size = 3814606, upload-time = "2025-04-23T01:47:39.028Z" },
+]
+
+[[package]]
+name = "markdown-it-py"
+version = "3.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "mdurl" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" },
+]
+
+[[package]]
+name = "mcp"
+version = "1.26.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "httpx" },
+ { name = "httpx-sse" },
+ { name = "jsonschema" },
+ { name = "pydantic" },
+ { name = "pydantic-settings" },
+ { name = "pyjwt", extra = ["crypto"] },
+ { name = "python-multipart" },
+ { name = "pywin32", marker = "sys_platform == 'win32'" },
+ { name = "sse-starlette" },
+ { name = "starlette" },
+ { name = "typing-extensions" },
+ { name = "typing-inspection" },
+ { name = "uvicorn", marker = "sys_platform != 'emscripten'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/fc/6d/62e76bbb8144d6ed86e202b5edd8a4cb631e7c8130f3f4893c3f90262b10/mcp-1.26.0.tar.gz", hash = "sha256:db6e2ef491eecc1a0d93711a76f28dec2e05999f93afd48795da1c1137142c66", size = 608005, upload-time = "2026-01-24T19:40:32.468Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fd/d9/eaa1f80170d2b7c5ba23f3b59f766f3a0bb41155fbc32a69adfa1adaaef9/mcp-1.26.0-py3-none-any.whl", hash = "sha256:904a21c33c25aa98ddbeb47273033c435e595bbacfdb177f4bd87f6dceebe1ca", size = 233615, upload-time = "2026-01-24T19:40:30.652Z" },
+]
+
+[[package]]
+name = "mdurl"
+version = "0.1.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
+]
+
+[[package]]
+name = "more-itertools"
+version = "10.8.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ea/5d/38b681d3fce7a266dd9ab73c66959406d565b3e85f21d5e66e1181d93721/more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd", size = 137431, upload-time = "2025-09-02T15:23:11.018Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667, upload-time = "2025-09-02T15:23:09.635Z" },
+]
+
+[[package]]
+name = "msoffcrypto-tool"
+version = "5.4.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cryptography" },
+ { name = "olefile" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/d2/b7/0fd6573157e0ec60c0c470e732ab3322fba4d2834fd24e1088d670522a01/msoffcrypto_tool-5.4.2.tar.gz", hash = "sha256:44b545adba0407564a0cc3d6dde6ca36b7c0fdf352b85bca51618fa1d4817370", size = 41183, upload-time = "2024-08-08T15:50:28.462Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/03/54/7f6d3d9acad083dae8c22d9ab483b657359a1bf56fee1d7af88794677707/msoffcrypto_tool-5.4.2-py3-none-any.whl", hash = "sha256:274fe2181702d1e5a107ec1b68a4c9fea997a44972ae1cc9ae0cb4f6a50fef0e", size = 48713, upload-time = "2024-08-08T15:50:27.093Z" },
+]
+
+[[package]]
+name = "office-word-mcp-server"
+version = "1.1.11"
+source = { editable = "." }
+dependencies = [
+ { name = "docx2pdf" },
+ { name = "fastmcp" },
+ { name = "msoffcrypto-tool" },
+ { name = "pytest" },
+ { name = "python-docx" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "docx2pdf", specifier = ">=0.1.8" },
+ { name = "fastmcp", specifier = ">=2.8.1" },
+ { name = "msoffcrypto-tool", specifier = ">=5.4.2" },
+ { name = "pytest", specifier = ">=8.4.2" },
+ { name = "python-docx", specifier = ">=1.1.2" },
+]
+
+[[package]]
+name = "olefile"
+version = "0.47"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/69/1b/077b508e3e500e1629d366249c3ccb32f95e50258b231705c09e3c7a4366/olefile-0.47.zip", hash = "sha256:599383381a0bf3dfbd932ca0ca6515acd174ed48870cbf7fee123d698c192c1c", size = 112240, upload-time = "2023-12-01T16:22:53.025Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/17/d3/b64c356a907242d719fc668b71befd73324e47ab46c8ebbbede252c154b2/olefile-0.47-py2.py3-none-any.whl", hash = "sha256:543c7da2a7adadf21214938bb79c83ea12b473a4b6ee4ad4bf854e7715e13d1f", size = 114565, upload-time = "2023-12-01T16:22:51.518Z" },
+]
+
+[[package]]
+name = "openapi-pydantic"
+version = "0.5.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pydantic" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/02/2e/58d83848dd1a79cb92ed8e63f6ba901ca282c5f09d04af9423ec26c56fd7/openapi_pydantic-0.5.1.tar.gz", hash = "sha256:ff6835af6bde7a459fb93eb93bb92b8749b754fc6e51b2f1590a19dc3005ee0d", size = 60892, upload-time = "2025-01-08T19:29:27.083Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/12/cf/03675d8bd8ecbf4445504d8071adab19f5f993676795708e36402ab38263/openapi_pydantic-0.5.1-py3-none-any.whl", hash = "sha256:a3a09ef4586f5bd760a8df7f43028b60cafb6d9f61de2acba9574766255ab146", size = 96381, upload-time = "2025-01-08T19:29:25.275Z" },
+]
+
+[[package]]
+name = "opentelemetry-api"
+version = "1.40.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "importlib-metadata" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/2c/1d/4049a9e8698361cc1a1aa03a6c59e4fa4c71e0c0f94a30f988a6876a2ae6/opentelemetry_api-1.40.0.tar.gz", hash = "sha256:159be641c0b04d11e9ecd576906462773eb97ae1b657730f0ecf64d32071569f", size = 70851, upload-time = "2026-03-04T14:17:21.555Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5f/bf/93795954016c522008da367da292adceed71cca6ee1717e1d64c83089099/opentelemetry_api-1.40.0-py3-none-any.whl", hash = "sha256:82dd69331ae74b06f6a874704be0cfaa49a1650e1537d4a813b86ecef7d0ecf9", size = 68676, upload-time = "2026-03-04T14:17:01.24Z" },
+]
+
+[[package]]
+name = "packaging"
+version = "26.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
+]
+
+[[package]]
+name = "pathable"
+version = "0.5.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/72/55/b748445cb4ea6b125626f15379be7c96d1035d4fa3e8fee362fa92298abf/pathable-0.5.0.tar.gz", hash = "sha256:d81938348a1cacb525e7c75166270644782c0fb9c8cecc16be033e71427e0ef1", size = 16655, upload-time = "2026-02-20T08:47:00.748Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/52/96/5a770e5c461462575474468e5af931cff9de036e7c2b4fea23c1c58d2cbe/pathable-0.5.0-py3-none-any.whl", hash = "sha256:646e3d09491a6351a0c82632a09c02cdf70a252e73196b36d8a15ba0a114f0a6", size = 16867, upload-time = "2026-02-20T08:46:59.536Z" },
+]
+
+[[package]]
+name = "platformdirs"
+version = "4.9.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/19/56/8d4c30c8a1d07013911a8fdbd8f89440ef9f08d07a1b50ab8ca8be5a20f9/platformdirs-4.9.4.tar.gz", hash = "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934", size = 28737, upload-time = "2026-03-05T18:34:13.271Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868", size = 21216, upload-time = "2026-03-05T18:34:12.172Z" },
+]
+
+[[package]]
+name = "pluggy"
+version = "1.6.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
+]
+
+[[package]]
+name = "py-key-value-aio"
+version = "0.4.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "beartype" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/04/3c/0397c072a38d4bc580994b42e0c90c5f44f679303489e4376289534735e5/py_key_value_aio-0.4.4.tar.gz", hash = "sha256:e3012e6243ed7cc09bb05457bd4d03b1ba5c2b1ca8700096b3927db79ffbbe55", size = 92300, upload-time = "2026-02-16T21:21:43.245Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/32/69/f1b537ee70b7def42d63124a539ed3026a11a3ffc3086947a1ca6e861868/py_key_value_aio-0.4.4-py3-none-any.whl", hash = "sha256:18e17564ecae61b987f909fc2cd41ee2012c84b4b1dcb8c055cf8b4bc1bf3f5d", size = 152291, upload-time = "2026-02-16T21:21:44.241Z" },
+]
+
+[package.optional-dependencies]
+filetree = [
+ { name = "aiofile" },
+ { name = "anyio" },
+]
+keyring = [
+ { name = "keyring" },
+]
+memory = [
+ { name = "cachetools" },
+]
+
+[[package]]
+name = "pycparser"
+version = "2.22"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload-time = "2024-03-30T13:22:22.564Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" },
+]
+
+[[package]]
+name = "pydantic"
+version = "2.12.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "annotated-types" },
+ { name = "pydantic-core" },
+ { name = "typing-extensions" },
+ { name = "typing-inspection" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" },
+]
+
+[package.optional-dependencies]
+email = [
+ { name = "email-validator" },
+]
+
+[[package]]
+name = "pydantic-core"
+version = "2.41.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" },
+ { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" },
+ { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" },
+ { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" },
+ { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" },
+ { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" },
+ { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" },
+ { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" },
+ { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" },
+ { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" },
+ { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" },
+ { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" },
+ { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" },
+ { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" },
+ { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" },
+ { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" },
+ { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" },
+ { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" },
+ { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" },
+ { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" },
+ { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" },
+ { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" },
+ { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" },
+ { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" },
+ { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" },
+ { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" },
+ { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" },
+ { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" },
+ { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" },
+ { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" },
+ { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" },
+ { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" },
+ { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" },
+ { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" },
+ { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" },
+ { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" },
+ { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" },
+]
+
+[[package]]
+name = "pydantic-settings"
+version = "2.9.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pydantic" },
+ { name = "python-dotenv" },
+ { name = "typing-inspection" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/67/1d/42628a2c33e93f8e9acbde0d5d735fa0850f3e6a2f8cb1eb6c40b9a732ac/pydantic_settings-2.9.1.tar.gz", hash = "sha256:c509bf79d27563add44e8446233359004ed85066cd096d8b510f715e6ef5d268", size = 163234, upload-time = "2025-04-18T16:44:48.265Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b6/5f/d6d641b490fd3ec2c4c13b4244d68deea3a1b970a97be64f34fb5504ff72/pydantic_settings-2.9.1-py3-none-any.whl", hash = "sha256:59b4f431b1defb26fe620c71a7d3968a710d719f5f4cdbbdb7926edeb770f6ef", size = 44356, upload-time = "2025-04-18T16:44:46.617Z" },
+]
+
+[[package]]
+name = "pygments"
+version = "2.19.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581, upload-time = "2025-01-06T17:26:30.443Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload-time = "2025-01-06T17:26:25.553Z" },
+]
+
+[[package]]
+name = "pyjwt"
+version = "2.11.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/5c/5a/b46fa56bf322901eee5b0454a34343cdbdae202cd421775a8ee4e42fd519/pyjwt-2.11.0.tar.gz", hash = "sha256:35f95c1f0fbe5d5ba6e43f00271c275f7a1a4db1dab27bf708073b75318ea623", size = 98019, upload-time = "2026-01-30T19:59:55.694Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6f/01/c26ce75ba460d5cd503da9e13b21a33804d38c2165dec7b716d06b13010c/pyjwt-2.11.0-py3-none-any.whl", hash = "sha256:94a6bde30eb5c8e04fee991062b534071fd1439ef58d2adc9ccb823e7bcd0469", size = 28224, upload-time = "2026-01-30T19:59:54.539Z" },
+]
+
+[package.optional-dependencies]
+crypto = [
+ { name = "cryptography" },
+]
+
+[[package]]
+name = "pyperclip"
+version = "1.11.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e8/52/d87eba7cb129b81563019d1679026e7a112ef76855d6159d24754dbd2a51/pyperclip-1.11.0.tar.gz", hash = "sha256:244035963e4428530d9e3a6101a1ef97209c6825edab1567beac148ccc1db1b6", size = 12185, upload-time = "2025-09-26T14:40:37.245Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/df/80/fc9d01d5ed37ba4c42ca2b55b4339ae6e200b456be3a1aaddf4a9fa99b8c/pyperclip-1.11.0-py3-none-any.whl", hash = "sha256:299403e9ff44581cb9ba2ffeed69c7aa96a008622ad0c46cb575ca75b5b84273", size = 11063, upload-time = "2025-09-26T14:40:36.069Z" },
+]
+
+[[package]]
+name = "pytest"
+version = "9.0.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+ { name = "iniconfig" },
+ { name = "packaging" },
+ { name = "pluggy" },
+ { name = "pygments" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
+]
+
+[[package]]
+name = "python-docx"
+version = "1.1.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "lxml" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/35/e4/386c514c53684772885009c12b67a7edd526c15157778ac1b138bc75063e/python_docx-1.1.2.tar.gz", hash = "sha256:0cf1f22e95b9002addca7948e16f2cd7acdfd498047f1941ca5d293db7762efd", size = 5656581, upload-time = "2024-05-01T19:41:57.772Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3e/3d/330d9efbdb816d3f60bf2ad92f05e1708e4a1b9abe80461ac3444c83f749/python_docx-1.1.2-py3-none-any.whl", hash = "sha256:08c20d6058916fb19853fcf080f7f42b6270d89eac9fa5f8c15f691c0017fabe", size = 244315, upload-time = "2024-05-01T19:41:47.006Z" },
+]
+
+[[package]]
+name = "python-dotenv"
+version = "1.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920, upload-time = "2025-03-25T10:14:56.835Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256, upload-time = "2025-03-25T10:14:55.034Z" },
+]
+
+[[package]]
+name = "python-multipart"
+version = "0.0.22"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" },
+]
+
+[[package]]
+name = "pywin32"
+version = "310"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f7/b1/68aa2986129fb1011dabbe95f0136f44509afaf072b12b8f815905a39f33/pywin32-310-cp311-cp311-win32.whl", hash = "sha256:1e765f9564e83011a63321bb9d27ec456a0ed90d3732c4b2e312b855365ed8bd", size = 8784284, upload-time = "2025-03-17T00:55:53.124Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/bd/d1592635992dd8db5bb8ace0551bc3a769de1ac8850200cfa517e72739fb/pywin32-310-cp311-cp311-win_amd64.whl", hash = "sha256:126298077a9d7c95c53823934f000599f66ec9296b09167810eb24875f32689c", size = 9520748, upload-time = "2025-03-17T00:55:55.203Z" },
+ { url = "https://files.pythonhosted.org/packages/90/b1/ac8b1ffce6603849eb45a91cf126c0fa5431f186c2e768bf56889c46f51c/pywin32-310-cp311-cp311-win_arm64.whl", hash = "sha256:19ec5fc9b1d51c4350be7bb00760ffce46e6c95eaf2f0b2f1150657b1a43c582", size = 8455941, upload-time = "2025-03-17T00:55:57.048Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/ec/4fdbe47932f671d6e348474ea35ed94227fb5df56a7c30cbbb42cd396ed0/pywin32-310-cp312-cp312-win32.whl", hash = "sha256:8a75a5cc3893e83a108c05d82198880704c44bbaee4d06e442e471d3c9ea4f3d", size = 8796239, upload-time = "2025-03-17T00:55:58.807Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/e5/b0627f8bb84e06991bea89ad8153a9e50ace40b2e1195d68e9dff6b03d0f/pywin32-310-cp312-cp312-win_amd64.whl", hash = "sha256:bf5c397c9a9a19a6f62f3fb821fbf36cac08f03770056711f765ec1503972060", size = 9503839, upload-time = "2025-03-17T00:56:00.8Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/32/9ccf53748df72301a89713936645a664ec001abd35ecc8578beda593d37d/pywin32-310-cp312-cp312-win_arm64.whl", hash = "sha256:2349cc906eae872d0663d4d6290d13b90621eaf78964bb1578632ff20e152966", size = 8459470, upload-time = "2025-03-17T00:56:02.601Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/09/9c1b978ffc4ae53999e89c19c77ba882d9fce476729f23ef55211ea1c034/pywin32-310-cp313-cp313-win32.whl", hash = "sha256:5d241a659c496ada3253cd01cfaa779b048e90ce4b2b38cd44168ad555ce74ab", size = 8794384, upload-time = "2025-03-17T00:56:04.383Z" },
+ { url = "https://files.pythonhosted.org/packages/45/3c/b4640f740ffebadd5d34df35fecba0e1cfef8fde9f3e594df91c28ad9b50/pywin32-310-cp313-cp313-win_amd64.whl", hash = "sha256:667827eb3a90208ddbdcc9e860c81bde63a135710e21e4cb3348968e4bd5249e", size = 9503039, upload-time = "2025-03-17T00:56:06.207Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/f4/f785020090fb050e7fb6d34b780f2231f302609dc964672f72bfaeb59a28/pywin32-310-cp313-cp313-win_arm64.whl", hash = "sha256:e308f831de771482b7cf692a1f308f8fca701b2d8f9dde6cc440c7da17e47b33", size = 8458152, upload-time = "2025-03-17T00:56:07.819Z" },
+]
+
+[[package]]
+name = "pywin32-ctypes"
+version = "0.2.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" },
+]
+
+[[package]]
+name = "pyyaml"
+version = "6.0.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" },
+ { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" },
+ { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" },
+ { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" },
+ { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" },
+ { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
+ { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
+ { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
+ { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
+ { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
+ { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
+ { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
+ { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
+ { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
+ { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
+ { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
+ { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
+ { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
+ { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
+ { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
+ { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
+ { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
+ { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
+]
+
+[[package]]
+name = "referencing"
+version = "0.37.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "attrs" },
+ { name = "rpds-py" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" },
+]
+
+[[package]]
+name = "rich"
+version = "14.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markdown-it-py" },
+ { name = "pygments" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078, upload-time = "2025-03-30T14:15:14.23Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229, upload-time = "2025-03-30T14:15:12.283Z" },
+]
+
+[[package]]
+name = "rich-rst"
+version = "1.3.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "docutils" },
+ { name = "rich" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/bc/6d/a506aaa4a9eaa945ed8ab2b7347859f53593864289853c5d6d62b77246e0/rich_rst-1.3.2.tar.gz", hash = "sha256:a1196fdddf1e364b02ec68a05e8ff8f6914fee10fbca2e6b6735f166bb0da8d4", size = 14936, upload-time = "2025-10-14T16:49:45.332Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/13/2f/b4530fbf948867702d0a3f27de4a6aab1d156f406d72852ab902c4d04de9/rich_rst-1.3.2-py3-none-any.whl", hash = "sha256:a99b4907cbe118cf9d18b0b44de272efa61f15117c61e39ebdc431baf5df722a", size = 12567, upload-time = "2025-10-14T16:49:42.953Z" },
+]
+
+[[package]]
+name = "rpds-py"
+version = "0.30.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" },
+ { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" },
+ { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" },
+ { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" },
+ { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" },
+ { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" },
+ { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" },
+ { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" },
+ { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" },
+ { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" },
+ { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" },
+ { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" },
+ { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" },
+ { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" },
+ { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" },
+ { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" },
+ { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" },
+ { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" },
+ { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" },
+ { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" },
+ { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" },
+ { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" },
+ { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" },
+ { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" },
+ { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" },
+ { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" },
+ { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" },
+ { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" },
+ { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" },
+ { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" },
+ { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" },
+ { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" },
+ { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" },
+ { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" },
+ { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" },
+ { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" },
+ { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" },
+ { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" },
+ { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" },
+ { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" },
+ { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" },
+ { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" },
+ { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" },
+ { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" },
+ { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" },
+ { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" },
+]
+
+[[package]]
+name = "secretstorage"
+version = "3.5.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cryptography" },
+ { name = "jeepney" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" },
+]
+
+[[package]]
+name = "sniffio"
+version = "1.3.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
+]
+
+[[package]]
+name = "sse-starlette"
+version = "2.3.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "starlette" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/86/35/7d8d94eb0474352d55f60f80ebc30f7e59441a29e18886a6425f0bccd0d3/sse_starlette-2.3.3.tar.gz", hash = "sha256:fdd47c254aad42907cfd5c5b83e2282be15be6c51197bf1a9b70b8e990522072", size = 17499, upload-time = "2025-04-23T19:28:25.558Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5d/20/52fdb5ebb158294b0adb5662235dd396fc7e47aa31c293978d8d8942095a/sse_starlette-2.3.3-py3-none-any.whl", hash = "sha256:8b0a0ced04a329ff7341b01007580dd8cf71331cc21c0ccea677d500618da1e0", size = 10235, upload-time = "2025-04-23T19:28:24.115Z" },
+]
+
+[[package]]
+name = "starlette"
+version = "0.46.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ce/20/08dfcd9c983f6a6f4a1000d934b9e6d626cff8d2eeb77a89a68eef20a2b7/starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5", size = 2580846, upload-time = "2025-04-13T13:56:17.942Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35", size = 72037, upload-time = "2025-04-13T13:56:16.21Z" },
+]
+
+[[package]]
+name = "tqdm"
+version = "4.67.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" },
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.15.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
+]
+
+[[package]]
+name = "typing-inspection"
+version = "0.4.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
+]
+
+[[package]]
+name = "uncalled-for"
+version = "0.2.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/02/7c/b5b7d8136f872e3f13b0584e576886de0489d7213a12de6bebf29ff6ebfc/uncalled_for-0.2.0.tar.gz", hash = "sha256:b4f8fdbcec328c5a113807d653e041c5094473dd4afa7c34599ace69ccb7e69f", size = 49488, upload-time = "2026-02-27T17:40:58.137Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ff/7f/4320d9ce3be404e6310b915c3629fe27bf1e2f438a1a7a3cb0396e32e9a9/uncalled_for-0.2.0-py3-none-any.whl", hash = "sha256:2c0bd338faff5f930918f79e7eb9ff48290df2cb05fcc0b40a7f334e55d4d85f", size = 11351, upload-time = "2026-02-27T17:40:56.804Z" },
+]
+
+[[package]]
+name = "uvicorn"
+version = "0.41.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+ { name = "h11" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/32/ce/eeb58ae4ac36fe09e3842eb02e0eb676bf2c53ae062b98f1b2531673efdd/uvicorn-0.41.0.tar.gz", hash = "sha256:09d11cf7008da33113824ee5a1c6422d89fbc2ff476540d69a34c87fab8b571a", size = 82633, upload-time = "2026-02-16T23:07:24.1Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/83/e4/d04a086285c20886c0daad0e026f250869201013d18f81d9ff5eada73a88/uvicorn-0.41.0-py3-none-any.whl", hash = "sha256:29e35b1d2c36a04b9e180d4007ede3bcb32a85fbdfd6c6aeb3f26839de088187", size = 68783, upload-time = "2026-02-16T23:07:22.357Z" },
+]
+
+[[package]]
+name = "watchfiles"
+version = "1.1.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1f/f8/2c5f479fb531ce2f0564eda479faecf253d886b1ab3630a39b7bf7362d46/watchfiles-1.1.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5", size = 406529, upload-time = "2025-10-14T15:04:32.899Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/cd/f515660b1f32f65df671ddf6f85bfaca621aee177712874dc30a97397977/watchfiles-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741", size = 394384, upload-time = "2025-10-14T15:04:33.761Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/c3/28b7dc99733eab43fca2d10f55c86e03bd6ab11ca31b802abac26b23d161/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6", size = 448789, upload-time = "2025-10-14T15:04:34.679Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/24/33e71113b320030011c8e4316ccca04194bf0cbbaeee207f00cbc7d6b9f5/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f537afb3276d12814082a2e9b242bdcf416c2e8fd9f799a737990a1dbe906e5b", size = 460521, upload-time = "2025-10-14T15:04:35.963Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/c3/3c9a55f255aa57b91579ae9e98c88704955fa9dac3e5614fb378291155df/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2cd9e04277e756a2e2d2543d65d1e2166d6fd4c9b183f8808634fda23f17b14", size = 488722, upload-time = "2025-10-14T15:04:37.091Z" },
+ { url = "https://files.pythonhosted.org/packages/49/36/506447b73eb46c120169dc1717fe2eff07c234bb3232a7200b5f5bd816e9/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3f58818dc0b07f7d9aa7fe9eb1037aecb9700e63e1f6acfed13e9fef648f5d", size = 596088, upload-time = "2025-10-14T15:04:38.39Z" },
+ { url = "https://files.pythonhosted.org/packages/82/ab/5f39e752a9838ec4d52e9b87c1e80f1ee3ccdbe92e183c15b6577ab9de16/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb9f66367023ae783551042d31b1d7fd422e8289eedd91f26754a66f44d5cff", size = 472923, upload-time = "2025-10-14T15:04:39.666Z" },
+ { url = "https://files.pythonhosted.org/packages/af/b9/a419292f05e302dea372fa7e6fda5178a92998411f8581b9830d28fb9edb/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aebfd0861a83e6c3d1110b78ad54704486555246e542be3e2bb94195eabb2606", size = 456080, upload-time = "2025-10-14T15:04:40.643Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/c3/d5932fd62bde1a30c36e10c409dc5d54506726f08cb3e1d8d0ba5e2bc8db/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5fac835b4ab3c6487b5dbad78c4b3724e26bcc468e886f8ba8cc4306f68f6701", size = 629432, upload-time = "2025-10-14T15:04:41.789Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/77/16bddd9779fafb795f1a94319dc965209c5641db5bf1edbbccace6d1b3c0/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:399600947b170270e80134ac854e21b3ccdefa11a9529a3decc1327088180f10", size = 623046, upload-time = "2025-10-14T15:04:42.718Z" },
+ { url = "https://files.pythonhosted.org/packages/46/ef/f2ecb9a0f342b4bfad13a2787155c6ee7ce792140eac63a34676a2feeef2/watchfiles-1.1.1-cp311-cp311-win32.whl", hash = "sha256:de6da501c883f58ad50db3a32ad397b09ad29865b5f26f64c24d3e3281685849", size = 271473, upload-time = "2025-10-14T15:04:43.624Z" },
+ { url = "https://files.pythonhosted.org/packages/94/bc/f42d71125f19731ea435c3948cad148d31a64fccde3867e5ba4edee901f9/watchfiles-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:35c53bd62a0b885bf653ebf6b700d1bf05debb78ad9292cf2a942b23513dc4c4", size = 287598, upload-time = "2025-10-14T15:04:44.516Z" },
+ { url = "https://files.pythonhosted.org/packages/57/c9/a30f897351f95bbbfb6abcadafbaca711ce1162f4db95fc908c98a9165f3/watchfiles-1.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:57ca5281a8b5e27593cb7d82c2ac927ad88a96ed406aa446f6344e4328208e9e", size = 277210, upload-time = "2025-10-14T15:04:45.883Z" },
+ { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" },
+ { url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" },
+ { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" },
+ { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042, upload-time = "2025-10-14T15:04:59.046Z" },
+ { url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410, upload-time = "2025-10-14T15:05:00.081Z" },
+ { url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209, upload-time = "2025-10-14T15:05:01.168Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" },
+ { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" },
+ { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" },
+ { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" },
+ { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" },
+ { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" },
+ { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" },
+ { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" },
+ { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" },
+ { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" },
+ { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845, upload-time = "2025-10-14T15:05:30.064Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027, upload-time = "2025-10-14T15:05:31.064Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615, upload-time = "2025-10-14T15:05:32.074Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836, upload-time = "2025-10-14T15:05:33.209Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099, upload-time = "2025-10-14T15:05:34.189Z" },
+ { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626, upload-time = "2025-10-14T15:05:35.216Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519, upload-time = "2025-10-14T15:05:36.259Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078, upload-time = "2025-10-14T15:05:37.63Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664, upload-time = "2025-10-14T15:05:38.95Z" },
+ { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154, upload-time = "2025-10-14T15:05:39.954Z" },
+ { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820, upload-time = "2025-10-14T15:05:40.932Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510, upload-time = "2025-10-14T15:05:41.945Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408, upload-time = "2025-10-14T15:05:43.385Z" },
+ { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968, upload-time = "2025-10-14T15:05:44.404Z" },
+ { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096, upload-time = "2025-10-14T15:05:45.398Z" },
+ { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040, upload-time = "2025-10-14T15:05:46.502Z" },
+ { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847, upload-time = "2025-10-14T15:05:47.484Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" },
+ { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/8e/e500f8b0b77be4ff753ac94dc06b33d8f0d839377fee1b78e8c8d8f031bf/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88", size = 408250, upload-time = "2025-10-14T15:06:10.264Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/95/615e72cd27b85b61eec764a5ca51bd94d40b5adea5ff47567d9ebc4d275a/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336", size = 396117, upload-time = "2025-10-14T15:06:11.28Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/81/e7fe958ce8a7fb5c73cc9fb07f5aeaf755e6aa72498c57d760af760c91f8/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24", size = 450493, upload-time = "2025-10-14T15:06:12.321Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/d4/ed38dd3b1767193de971e694aa544356e63353c33a85d948166b5ff58b9e/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", size = 457546, upload-time = "2025-10-14T15:06:13.372Z" },
+]
+
+[[package]]
+name = "websockets"
+version = "16.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f2/db/de907251b4ff46ae804ad0409809504153b3f30984daf82a1d84a9875830/websockets-16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8", size = 177340, upload-time = "2026-01-10T09:22:34.539Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/fa/abe89019d8d8815c8781e90d697dec52523fb8ebe308bf11664e8de1877e/websockets-16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad", size = 175022, upload-time = "2026-01-10T09:22:36.332Z" },
+ { url = "https://files.pythonhosted.org/packages/58/5d/88ea17ed1ded2079358b40d31d48abe90a73c9e5819dbcde1606e991e2ad/websockets-16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d", size = 175319, upload-time = "2026-01-10T09:22:37.602Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/ae/0ee92b33087a33632f37a635e11e1d99d429d3d323329675a6022312aac2/websockets-16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe", size = 184631, upload-time = "2026-01-10T09:22:38.789Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/c5/27178df583b6c5b31b29f526ba2da5e2f864ecc79c99dae630a85d68c304/websockets-16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b", size = 185870, upload-time = "2026-01-10T09:22:39.893Z" },
+ { url = "https://files.pythonhosted.org/packages/87/05/536652aa84ddc1c018dbb7e2c4cbcd0db884580bf8e95aece7593fde526f/websockets-16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5", size = 185361, upload-time = "2026-01-10T09:22:41.016Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/e2/d5332c90da12b1e01f06fb1b85c50cfc489783076547415bf9f0a659ec19/websockets-16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64", size = 184615, upload-time = "2026-01-10T09:22:42.442Z" },
+ { url = "https://files.pythonhosted.org/packages/77/fb/d3f9576691cae9253b51555f841bc6600bf0a983a461c79500ace5a5b364/websockets-16.0-cp311-cp311-win32.whl", hash = "sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6", size = 178246, upload-time = "2026-01-10T09:22:43.654Z" },
+ { url = "https://files.pythonhosted.org/packages/54/67/eaff76b3dbaf18dcddabc3b8c1dba50b483761cccff67793897945b37408/websockets-16.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac", size = 178684, upload-time = "2026-01-10T09:22:44.941Z" },
+ { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" },
+ { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" },
+ { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" },
+ { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" },
+ { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" },
+ { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" },
+ { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" },
+ { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" },
+ { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" },
+ { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" },
+ { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" },
+ { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" },
+ { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" },
+ { url = "https://files.pythonhosted.org/packages/72/07/c98a68571dcf256e74f1f816b8cc5eae6eb2d3d5cfa44d37f801619d9166/websockets-16.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d", size = 174947, upload-time = "2026-01-10T09:23:36.166Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/52/93e166a81e0305b33fe416338be92ae863563fe7bce446b0f687b9df5aea/websockets-16.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03", size = 175260, upload-time = "2026-01-10T09:23:37.409Z" },
+ { url = "https://files.pythonhosted.org/packages/56/0c/2dbf513bafd24889d33de2ff0368190a0e69f37bcfa19009ef819fe4d507/websockets-16.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da", size = 176071, upload-time = "2026-01-10T09:23:39.158Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/8f/aea9c71cc92bf9b6cc0f7f70df8f0b420636b6c96ef4feee1e16f80f75dd/websockets-16.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c", size = 176968, upload-time = "2026-01-10T09:23:41.031Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/3f/f70e03f40ffc9a30d817eef7da1be72ee4956ba8d7255c399a01b135902a/websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767", size = 178735, upload-time = "2026-01-10T09:23:42.259Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" },
+]
+
+[[package]]
+name = "zipp"
+version = "3.23.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" },
+]
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/__init__.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/__init__.py
new file mode 100644
index 00000000..85d71f39
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/__init__.py
@@ -0,0 +1,15 @@
+"""
+Word Document Server - MCP server for Microsoft Word document manipulation.
+
+This package provides tools for creating, reading, and manipulating Microsoft Word
+documents through the Model Context Protocol (MCP).
+
+Features:
+- Document creation and management
+- Content addition (headings, paragraphs, tables, images)
+- Text and table formatting
+- Document protection (password, restricted editing, signatures)
+- Footnote and endnote management
+"""
+
+__version__ = "1.0.0"
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/core/__init__.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/core/__init__.py
new file mode 100644
index 00000000..a0aa41db
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/core/__init__.py
@@ -0,0 +1,10 @@
+"""
+Core functionality for the Word Document Server.
+
+This package contains the core functionality modules used by the Word Document Server.
+"""
+
+from word_document_server.core.styles import ensure_heading_style, ensure_table_style, create_style
+from word_document_server.core.protection import add_protection_info, verify_document_protection, is_section_editable, create_signature_info, verify_signature
+from word_document_server.core.footnotes import add_footnote, add_endnote, convert_footnotes_to_endnotes, find_footnote_references, get_format_symbols, customize_footnote_formatting
+from word_document_server.core.tables import set_cell_border, apply_table_style, copy_table
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/core/comments.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/core/comments.py
new file mode 100644
index 00000000..9695c8b6
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/core/comments.py
@@ -0,0 +1,210 @@
+"""
+Core comment extraction functionality for Word documents.
+
+This module provides low-level functions to extract and process comments
+from Word documents using the python-docx library.
+"""
+import datetime
+from typing import Dict, List, Optional, Any
+from docx import Document
+from docx.document import Document as DocumentType
+from docx.text.paragraph import Paragraph
+
+
+def extract_all_comments(doc: DocumentType) -> List[Dict[str, Any]]:
+ """
+ Extract all comments from a Word document.
+
+ Args:
+ doc: The Document object to extract comments from
+
+ Returns:
+ List of dictionaries containing comment information
+ """
+ comments = []
+
+ # Access the document's comment part if it exists
+ try:
+ # Get the document part
+ document_part = doc.part
+
+ # Find comments part through relationships
+ comments_part = None
+ for rel_id, rel in document_part.rels.items():
+ if 'comments' in rel.reltype and 'comments' == rel.reltype.split('/')[-1]:
+ comments_part = rel.target_part
+ break
+
+ if comments_part:
+ # Extract comments from the comments part using proper xpath syntax
+ comment_elements = comments_part.element.xpath('.//w:comment')
+
+ for idx, comment_element in enumerate(comment_elements):
+ comment_data = extract_comment_data(comment_element, idx)
+ if comment_data:
+ comments.append(comment_data)
+
+ # If no comments found, try alternative approach
+ if not comments:
+ # Fallback: scan paragraphs for comment references
+ comments = extract_comments_from_paragraphs(doc)
+
+ except Exception as e:
+ # If direct access fails, try alternative approach
+ comments = extract_comments_from_paragraphs(doc)
+
+ return comments
+
+
+def extract_comments_from_paragraphs(doc: DocumentType) -> List[Dict[str, Any]]:
+ """
+ Extract comments by scanning paragraphs for comment references.
+
+ Args:
+ doc: The Document object
+
+ Returns:
+ List of comment dictionaries
+ """
+ comments = []
+ comment_id = 1
+
+ # Check all paragraphs in the document
+ for para_idx, paragraph in enumerate(doc.paragraphs):
+ para_comments = find_paragraph_comments(paragraph, para_idx, comment_id)
+ comments.extend(para_comments)
+ comment_id += len(para_comments)
+
+ # Check paragraphs in tables
+ for table in doc.tables:
+ for row in table.rows:
+ for cell in row.cells:
+ for para_idx, paragraph in enumerate(cell.paragraphs):
+ para_comments = find_paragraph_comments(paragraph, para_idx, comment_id, in_table=True)
+ comments.extend(para_comments)
+ comment_id += len(para_comments)
+
+ return comments
+
+
+def extract_comment_data(comment_element, index: int) -> Optional[Dict[str, Any]]:
+ """
+ Extract data from a comment XML element.
+
+ Args:
+ comment_element: The XML comment element
+ index: Index for generating a unique ID
+
+ Returns:
+ Dictionary with comment data or None
+ """
+ try:
+ # Extract comment attributes
+ comment_id = comment_element.get('{http://schemas.openxmlformats.org/wordprocessingml/2006/main}id', str(index))
+ author = comment_element.get('{http://schemas.openxmlformats.org/wordprocessingml/2006/main}author', 'Unknown')
+ initials = comment_element.get('{http://schemas.openxmlformats.org/wordprocessingml/2006/main}initials', '')
+ date_str = comment_element.get('{http://schemas.openxmlformats.org/wordprocessingml/2006/main}date', '')
+
+ # Parse date if available
+ date = None
+ if date_str:
+ try:
+ date = datetime.datetime.fromisoformat(date_str.replace('Z', '+00:00'))
+ date = date.isoformat()
+ except:
+ date = date_str
+
+ # Extract comment text
+ text_elements = comment_element.xpath('.//w:t')
+ text = ''.join(elem.text or '' for elem in text_elements)
+
+ return {
+ 'id': f'comment_{index + 1}',
+ 'comment_id': comment_id,
+ 'author': author,
+ 'initials': initials,
+ 'date': date,
+ 'text': text.strip(),
+ 'paragraph_index': None, # Will be filled if we can determine it
+ 'in_table': False,
+ 'reference_text': ''
+ }
+
+ except Exception as e:
+ return None
+
+
+def find_paragraph_comments(paragraph: Paragraph, para_index: int,
+ start_id: int, in_table: bool = False) -> List[Dict[str, Any]]:
+ """
+ Find comments associated with a specific paragraph.
+
+ Args:
+ paragraph: The paragraph to check
+ para_index: The index of the paragraph
+ start_id: Starting ID for comments
+ in_table: Whether the paragraph is in a table
+
+ Returns:
+ List of comment dictionaries
+ """
+ comments = []
+
+ try:
+ # Access the paragraph's XML element
+ para_xml = paragraph._element
+
+ # Look for comment range markers (simplified approach)
+ # This is a basic implementation - the full version would need more sophisticated XML parsing
+ xml_text = str(para_xml)
+
+ # Simple check for comment references in the XML
+ if 'commentRangeStart' in xml_text or 'commentReference' in xml_text:
+ # Create a placeholder comment entry
+ comment_info = {
+ 'id': f'comment_{start_id}',
+ 'comment_id': f'{start_id}',
+ 'author': 'Unknown',
+ 'initials': '',
+ 'date': None,
+ 'text': 'Comment detected but content not accessible',
+ 'paragraph_index': para_index,
+ 'in_table': in_table,
+ 'reference_text': paragraph.text[:50] + '...' if len(paragraph.text) > 50 else paragraph.text
+ }
+ comments.append(comment_info)
+
+ except Exception:
+ # If we can't access the XML, skip this paragraph
+ pass
+
+ return comments
+
+
+def filter_comments_by_author(comments: List[Dict[str, Any]], author: str) -> List[Dict[str, Any]]:
+ """
+ Filter comments by author name.
+
+ Args:
+ comments: List of comment dictionaries
+ author: Author name to filter by (case-insensitive)
+
+ Returns:
+ Filtered list of comments
+ """
+ author_lower = author.lower()
+ return [c for c in comments if c.get('author', '').lower() == author_lower]
+
+
+def get_comments_for_paragraph(comments: List[Dict[str, Any]], paragraph_index: int) -> List[Dict[str, Any]]:
+ """
+ Get all comments for a specific paragraph.
+
+ Args:
+ comments: List of all comments
+ paragraph_index: Index of the paragraph
+
+ Returns:
+ Comments for the specified paragraph
+ """
+ return [c for c in comments if c.get('paragraph_index') == paragraph_index]
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/core/footnotes.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/core/footnotes.py
new file mode 100644
index 00000000..f5147e0c
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/core/footnotes.py
@@ -0,0 +1,842 @@
+"""
+Consolidated footnote functionality for Word documents.
+This module combines all footnote implementations with proper namespace handling and Word compliance.
+"""
+
+import os
+import zipfile
+import tempfile
+from typing import Optional, Tuple, Dict, Any, List
+from lxml import etree
+from docx import Document
+from docx.oxml.ns import qn
+
+# Namespace definitions
+W_NS = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'
+R_NS = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships'
+CT_NS = 'http://schemas.openxmlformats.org/package/2006/content-types'
+REL_NS = 'http://schemas.openxmlformats.org/package/2006/relationships'
+
+# Constants
+RESERVED_FOOTNOTE_IDS = {-1, 0, 1} # Reserved for separators and Word internals
+MIN_FOOTNOTE_ID = -2147483648
+MAX_FOOTNOTE_ID = 32767
+MAX_RELATIONSHIP_ID_LENGTH = 255
+FOOTNOTE_REF_STYLE_INDEX = 38
+FOOTNOTE_TEXT_STYLE_INDEX = 29
+
+
+# ============================================================================
+# BASIC UTILITIES (from footnotes.py)
+# ============================================================================
+
+def find_footnote_references(doc):
+ """Find all footnote references in the document."""
+ footnote_refs = []
+ for para_idx, para in enumerate(doc.paragraphs):
+ for run_idx, run in enumerate(para.runs):
+ # Check if this run has superscript formatting
+ if run.font.superscript:
+ # Check if it's likely a footnote reference
+ if run.text.isdigit() or run.text in "¹²³⁴⁵⁶⁷⁸⁹⁰†‡§¶":
+ footnote_refs.append({
+ 'paragraph_index': para_idx,
+ 'run_index': run_idx,
+ 'text': run.text,
+ 'paragraph': para,
+ 'run': run
+ })
+ return footnote_refs
+
+
+def get_format_symbols(format_type: str, count: int) -> list:
+ """Generate format symbols for footnote numbering."""
+ symbols = []
+
+ if format_type == "1, 2, 3":
+ symbols = [str(i) for i in range(1, count + 1)]
+ elif format_type == "i, ii, iii":
+ # Roman numerals
+ roman_map = [(10, 'x'), (9, 'ix'), (5, 'v'), (4, 'iv'), (1, 'i')]
+ for i in range(1, count + 1):
+ result = ''
+ num = i
+ for value, numeral in roman_map:
+ count_sym, num = divmod(num, value)
+ result += numeral * count_sym
+ symbols.append(result)
+ elif format_type == "a, b, c":
+ # Alphabetic
+ for i in range(1, count + 1):
+ if i <= 26:
+ symbols.append(chr(96 + i))
+ else:
+ # For numbers > 26, use aa, ab, etc.
+ first = (i - 1) // 26
+ second = (i - 1) % 26 + 1
+ symbols.append(chr(96 + first) + chr(96 + second))
+ elif format_type == "*, †, ‡":
+ # Special symbols
+ special = ['*', '†', '‡', '§', '¶', '#']
+ for i in range(1, count + 1):
+ if i <= len(special):
+ symbols.append(special[i - 1])
+ else:
+ # Repeat symbols with numbers
+ symbols.append(special[(i - 1) % len(special)] + str((i - 1) // len(special) + 1))
+ else:
+ # Default to numeric
+ symbols = [str(i) for i in range(1, count + 1)]
+
+ return symbols
+
+
+def customize_footnote_formatting(doc, footnote_refs, format_symbols, start_number, footnote_style):
+ """Apply custom formatting to footnotes."""
+ count = 0
+ for i, ref in enumerate(footnote_refs):
+ if i < len(format_symbols):
+ # Update the footnote reference text
+ ref['run'].text = format_symbols[i]
+ ref['run'].font.superscript = True
+
+ # Apply style if available
+ if footnote_style:
+ try:
+ ref['paragraph'].style = footnote_style
+ except:
+ pass
+ count += 1
+ return count
+
+
+# ============================================================================
+# ROBUST IMPLEMENTATION (consolidated from footnotes_robust.py)
+# ============================================================================
+
+def _get_safe_footnote_id(footnotes_root) -> int:
+ """Get a safe footnote ID avoiding conflicts and reserved values."""
+ nsmap = {'w': W_NS}
+ existing_footnotes = footnotes_root.xpath('//w:footnote', namespaces=nsmap)
+
+ used_ids = set()
+ for fn in existing_footnotes:
+ fn_id = fn.get(f'{{{W_NS}}}id')
+ if fn_id:
+ try:
+ used_ids.add(int(fn_id))
+ except ValueError:
+ pass
+
+ # Start from 2 to avoid reserved IDs
+ candidate_id = 2
+ while candidate_id in used_ids or candidate_id in RESERVED_FOOTNOTE_IDS:
+ candidate_id += 1
+ if candidate_id > MAX_FOOTNOTE_ID:
+ raise ValueError("No available footnote IDs")
+
+ return candidate_id
+
+
+def _ensure_content_types(content_types_xml: bytes) -> bytes:
+ """Ensure content types with proper namespace handling."""
+ ct_tree = etree.fromstring(content_types_xml)
+
+ # Content Types uses default namespace - must use namespace-aware XPath
+ nsmap = {'ct': CT_NS}
+
+ # Check for existing override with proper namespace
+ existing_overrides = ct_tree.xpath(
+ "//ct:Override[@PartName='/word/footnotes.xml']",
+ namespaces=nsmap
+ )
+
+ if existing_overrides:
+ return content_types_xml # Already exists
+
+ # Add override with proper namespace
+ override = etree.Element(f'{{{CT_NS}}}Override',
+ PartName='/word/footnotes.xml',
+ ContentType='application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml'
+ )
+ ct_tree.append(override)
+
+ return etree.tostring(ct_tree, encoding='UTF-8', xml_declaration=True, standalone="yes")
+
+
+def _ensure_document_rels(document_rels_xml: bytes) -> bytes:
+ """Ensure document relationships with proper namespace handling."""
+ rels_tree = etree.fromstring(document_rels_xml)
+
+ # Relationships uses default namespace - must use namespace-aware XPath
+ nsmap = {'r': REL_NS}
+
+ # Check for existing footnotes relationship with proper namespace
+ existing_footnote_rels = rels_tree.xpath(
+ "//r:Relationship[contains(@Type, 'footnotes')]",
+ namespaces=nsmap
+ )
+
+ if existing_footnote_rels:
+ return document_rels_xml # Already exists
+
+ # Generate unique rId using namespace-aware XPath
+ all_rels = rels_tree.xpath('//r:Relationship', namespaces=nsmap)
+ existing_ids = {rel.get('Id') for rel in all_rels if rel.get('Id')}
+ rid_num = 1
+ while f'rId{rid_num}' in existing_ids:
+ rid_num += 1
+
+ # Validate ID length
+ new_rid = f'rId{rid_num}'
+ if len(new_rid) > MAX_RELATIONSHIP_ID_LENGTH:
+ raise ValueError(f"Relationship ID too long: {new_rid}")
+
+ # Create relationship with proper namespace
+ rel = etree.Element(f'{{{REL_NS}}}Relationship',
+ Id=new_rid,
+ Type='http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes',
+ Target='footnotes.xml'
+ )
+ rels_tree.append(rel)
+
+ return etree.tostring(rels_tree, encoding='UTF-8', xml_declaration=True, standalone="yes")
+
+
+def _create_minimal_footnotes_xml() -> bytes:
+ """Create minimal footnotes.xml with separators."""
+ xml = f'''
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ '''
+ return xml.encode('utf-8')
+
+
+def _ensure_footnote_styles(styles_root):
+ """Ensure both FootnoteReference and FootnoteText styles exist."""
+ nsmap = {'w': W_NS}
+
+ # Check for FootnoteReference style
+ ref_style = styles_root.xpath('//w:style[@w:styleId="FootnoteReference"]', namespaces=nsmap)
+ if not ref_style:
+ # Create FootnoteReference character style
+ style = etree.Element(f'{{{W_NS}}}style',
+ attrib={
+ f'{{{W_NS}}}type': 'character',
+ f'{{{W_NS}}}styleId': 'FootnoteReference'
+ }
+ )
+ name = etree.SubElement(style, f'{{{W_NS}}}name')
+ name.set(f'{{{W_NS}}}val', 'footnote reference')
+
+ base = etree.SubElement(style, f'{{{W_NS}}}basedOn')
+ base.set(f'{{{W_NS}}}val', 'DefaultParagraphFont')
+
+ rPr = etree.SubElement(style, f'{{{W_NS}}}rPr')
+ vert_align = etree.SubElement(rPr, f'{{{W_NS}}}vertAlign')
+ vert_align.set(f'{{{W_NS}}}val', 'superscript')
+
+ styles_root.append(style)
+
+ # Check for FootnoteText style
+ text_style = styles_root.xpath('//w:style[@w:styleId="FootnoteText"]', namespaces=nsmap)
+ if not text_style:
+ # Create FootnoteText paragraph style
+ style = etree.Element(f'{{{W_NS}}}style',
+ attrib={
+ f'{{{W_NS}}}type': 'paragraph',
+ f'{{{W_NS}}}styleId': 'FootnoteText'
+ }
+ )
+ name = etree.SubElement(style, f'{{{W_NS}}}name')
+ name.set(f'{{{W_NS}}}val', 'footnote text')
+
+ base = etree.SubElement(style, f'{{{W_NS}}}basedOn')
+ base.set(f'{{{W_NS}}}val', 'Normal')
+
+ pPr = etree.SubElement(style, f'{{{W_NS}}}pPr')
+ sz = etree.SubElement(pPr, f'{{{W_NS}}}sz')
+ sz.set(f'{{{W_NS}}}val', '20') # 10pt
+
+ styles_root.append(style)
+
+
+def add_footnote_robust(
+ filename: str,
+ search_text: Optional[str] = None,
+ paragraph_index: Optional[int] = None,
+ footnote_text: str = "",
+ output_filename: Optional[str] = None,
+ position: str = "after",
+ validate_location: bool = True,
+ auto_repair: bool = False
+) -> Tuple[bool, str, Optional[Dict[str, Any]]]:
+ """
+ Add a footnote with robust validation and error handling.
+
+ This is the main production-ready function with all fixes applied.
+ """
+
+ # Validate inputs
+ if not search_text and paragraph_index is None:
+ return False, "Must provide either search_text or paragraph_index", None
+
+ if search_text and paragraph_index is not None:
+ return False, "Cannot provide both search_text and paragraph_index", None
+
+ if not os.path.exists(filename):
+ return False, f"File not found: {filename}", None
+
+ # Set working file
+ working_file = output_filename if output_filename else filename
+ if output_filename and filename != output_filename:
+ import shutil
+ shutil.copy2(filename, output_filename)
+
+ try:
+ # Read document parts
+ doc_parts = {}
+ with zipfile.ZipFile(filename, 'r') as zin:
+ doc_parts['document'] = zin.read('word/document.xml')
+ doc_parts['content_types'] = zin.read('[Content_Types].xml')
+ doc_parts['document_rels'] = zin.read('word/_rels/document.xml.rels')
+
+ # Read or create footnotes.xml
+ if 'word/footnotes.xml' in zin.namelist():
+ doc_parts['footnotes'] = zin.read('word/footnotes.xml')
+ else:
+ doc_parts['footnotes'] = _create_minimal_footnotes_xml()
+
+ # Read styles
+ if 'word/styles.xml' in zin.namelist():
+ doc_parts['styles'] = zin.read('word/styles.xml')
+ else:
+ # Create minimal styles
+ doc_parts['styles'] = b' '
+
+ # Parse XML documents
+ doc_root = etree.fromstring(doc_parts['document'])
+ footnotes_root = etree.fromstring(doc_parts['footnotes'])
+ styles_root = etree.fromstring(doc_parts['styles'])
+
+ # Find target location
+ nsmap = {'w': W_NS}
+
+ if search_text:
+ # Search for text in paragraphs
+ found = False
+ for para in doc_root.xpath('//w:p', namespaces=nsmap):
+ para_text = ''.join(para.xpath('.//w:t/text()', namespaces=nsmap))
+ if search_text in para_text:
+ target_para = para
+ found = True
+ break
+
+ if not found:
+ return False, f"Text '{search_text}' not found in document", None
+ else:
+ # Use paragraph index
+ paragraphs = doc_root.xpath('//w:p', namespaces=nsmap)
+ if paragraph_index >= len(paragraphs):
+ return False, f"Paragraph index {paragraph_index} out of range", None
+ target_para = paragraphs[paragraph_index]
+
+ # Validate location if requested
+ if validate_location:
+ # Check if paragraph is in header/footer
+ parent = target_para.getparent()
+ while parent is not None:
+ if parent.tag in [f'{{{W_NS}}}hdr', f'{{{W_NS}}}ftr']:
+ return False, "Cannot add footnote in header/footer", None
+ parent = parent.getparent()
+
+ # Get safe footnote ID
+ footnote_id = _get_safe_footnote_id(footnotes_root)
+
+ # Add footnote reference to document
+ if position == "after":
+ # Find last run in paragraph or create one
+ runs = target_para.xpath('.//w:r', namespaces=nsmap)
+ if runs:
+ last_run = runs[-1]
+ # Insert after last run
+ insert_pos = target_para.index(last_run) + 1
+ else:
+ insert_pos = len(target_para)
+ else: # before
+ # Find first run with text
+ runs = target_para.xpath('.//w:r[w:t]', namespaces=nsmap)
+ if runs:
+ first_run = runs[0]
+ insert_pos = target_para.index(first_run)
+ else:
+ insert_pos = 0
+
+ # Create footnote reference run
+ ref_run = etree.Element(f'{{{W_NS}}}r')
+
+ # Add run properties with superscript
+ rPr = etree.SubElement(ref_run, f'{{{W_NS}}}rPr')
+ rStyle = etree.SubElement(rPr, f'{{{W_NS}}}rStyle')
+ rStyle.set(f'{{{W_NS}}}val', 'FootnoteReference')
+
+ # Add footnote reference
+ fn_ref = etree.SubElement(ref_run, f'{{{W_NS}}}footnoteReference')
+ fn_ref.set(f'{{{W_NS}}}id', str(footnote_id))
+
+ # Insert the reference run
+ target_para.insert(insert_pos, ref_run)
+
+ # Add footnote content
+ new_footnote = etree.Element(f'{{{W_NS}}}footnote',
+ attrib={f'{{{W_NS}}}id': str(footnote_id)}
+ )
+
+ # Add paragraph to footnote
+ fn_para = etree.SubElement(new_footnote, f'{{{W_NS}}}p')
+
+ # Add paragraph properties
+ pPr = etree.SubElement(fn_para, f'{{{W_NS}}}pPr')
+ pStyle = etree.SubElement(pPr, f'{{{W_NS}}}pStyle')
+ pStyle.set(f'{{{W_NS}}}val', 'FootnoteText')
+
+ # Add the footnote reference marker
+ marker_run = etree.SubElement(fn_para, f'{{{W_NS}}}r')
+ marker_rPr = etree.SubElement(marker_run, f'{{{W_NS}}}rPr')
+ marker_rStyle = etree.SubElement(marker_rPr, f'{{{W_NS}}}rStyle')
+ marker_rStyle.set(f'{{{W_NS}}}val', 'FootnoteReference')
+ marker_ref = etree.SubElement(marker_run, f'{{{W_NS}}}footnoteRef')
+
+ # Add space after marker
+ space_run = etree.SubElement(fn_para, f'{{{W_NS}}}r')
+ space_text = etree.SubElement(space_run, f'{{{W_NS}}}t')
+ space_text.set(f'{{{XML_NS}}}space', 'preserve')
+ space_text.text = ' '
+
+ # Add footnote text
+ text_run = etree.SubElement(fn_para, f'{{{W_NS}}}r')
+ text_elem = etree.SubElement(text_run, f'{{{W_NS}}}t')
+ text_elem.text = footnote_text
+
+ # Append footnote to footnotes.xml
+ footnotes_root.append(new_footnote)
+
+ # Ensure styles exist
+ _ensure_footnote_styles(styles_root)
+
+ # Ensure coherence
+ content_types_xml = _ensure_content_types(doc_parts['content_types'])
+ document_rels_xml = _ensure_document_rels(doc_parts['document_rels'])
+
+ # Write modified document
+ temp_file = working_file + '.tmp'
+ with zipfile.ZipFile(temp_file, 'w', zipfile.ZIP_DEFLATED) as zout:
+ with zipfile.ZipFile(filename, 'r') as zin:
+ # Copy unchanged files
+ for item in zin.infolist():
+ if item.filename not in [
+ 'word/document.xml', 'word/footnotes.xml', 'word/styles.xml',
+ '[Content_Types].xml', 'word/_rels/document.xml.rels'
+ ]:
+ zout.writestr(item, zin.read(item.filename))
+
+ # Write modified files
+ zout.writestr('word/document.xml',
+ etree.tostring(doc_root, encoding='UTF-8', xml_declaration=True, standalone="yes"))
+ zout.writestr('word/footnotes.xml',
+ etree.tostring(footnotes_root, encoding='UTF-8', xml_declaration=True, standalone="yes"))
+ zout.writestr('word/styles.xml',
+ etree.tostring(styles_root, encoding='UTF-8', xml_declaration=True, standalone="yes"))
+ zout.writestr('[Content_Types].xml', content_types_xml)
+ zout.writestr('word/_rels/document.xml.rels', document_rels_xml)
+
+ # Replace original with temp file
+ os.replace(temp_file, working_file)
+
+ details = {
+ 'footnote_id': footnote_id,
+ 'location': 'search_text' if search_text else 'paragraph_index',
+ 'styles_created': ['FootnoteReference', 'FootnoteText'],
+ 'coherence_verified': True
+ }
+
+ return True, f"Successfully added footnote (ID: {footnote_id}) to {working_file}", details
+
+ except Exception as e:
+ # Clean up temp file if exists
+ temp_file = working_file + '.tmp'
+ if os.path.exists(temp_file):
+ os.remove(temp_file)
+ return False, f"Error adding footnote: {str(e)}", None
+
+
+def delete_footnote_robust(
+ filename: str,
+ footnote_id: Optional[int] = None,
+ search_text: Optional[str] = None,
+ output_filename: Optional[str] = None,
+ clean_orphans: bool = True
+) -> Tuple[bool, str, Optional[Dict[str, Any]]]:
+ """Delete a footnote with comprehensive cleanup."""
+
+ if not footnote_id and not search_text:
+ return False, "Must provide either footnote_id or search_text", None
+
+ if not os.path.exists(filename):
+ return False, f"File not found: {filename}", None
+
+ # Set working file
+ working_file = output_filename if output_filename else filename
+ if output_filename and filename != output_filename:
+ import shutil
+ shutil.copy2(filename, output_filename)
+
+ try:
+ # Read document parts
+ with zipfile.ZipFile(filename, 'r') as zin:
+ doc_xml = zin.read('word/document.xml')
+
+ if 'word/footnotes.xml' not in zin.namelist():
+ return False, "No footnotes in document", None
+
+ footnotes_xml = zin.read('word/footnotes.xml')
+
+ # Parse documents
+ doc_root = etree.fromstring(doc_xml)
+ footnotes_root = etree.fromstring(footnotes_xml)
+ nsmap = {'w': W_NS}
+
+ # Find footnote to delete
+ if search_text:
+ # Find footnote reference near text
+ for para in doc_root.xpath('//w:p', namespaces=nsmap):
+ para_text = ''.join(para.xpath('.//w:t/text()', namespaces=nsmap))
+ if search_text in para_text:
+ # Look for footnote reference in this paragraph
+ fn_refs = para.xpath('.//w:footnoteReference', namespaces=nsmap)
+ if fn_refs:
+ footnote_id = int(fn_refs[0].get(f'{{{W_NS}}}id'))
+ break
+
+ if not footnote_id:
+ return False, f"No footnote found near text '{search_text}'", None
+
+ # Remove footnote reference from document
+ refs_removed = 0
+ for fn_ref in doc_root.xpath(f'//w:footnoteReference[@w:id="{footnote_id}"]', namespaces=nsmap):
+ # Remove the entire run containing the reference
+ run = fn_ref.getparent()
+ if run is not None and run.tag == f'{{{W_NS}}}r':
+ para = run.getparent()
+ if para is not None:
+ para.remove(run)
+ refs_removed += 1
+
+ if refs_removed == 0:
+ return False, f"Footnote {footnote_id} not found", None
+
+ # Remove footnote content
+ content_removed = 0
+ for fn in footnotes_root.xpath(f'//w:footnote[@w:id="{footnote_id}"]', namespaces=nsmap):
+ footnotes_root.remove(fn)
+ content_removed += 1
+
+ # Clean orphans if requested
+ orphans_removed = []
+ if clean_orphans:
+ # Find all referenced IDs
+ referenced_ids = set()
+ for ref in doc_root.xpath('//w:footnoteReference', namespaces=nsmap):
+ ref_id = ref.get(f'{{{W_NS}}}id')
+ if ref_id:
+ referenced_ids.add(ref_id)
+
+ # Remove unreferenced footnotes (except separators)
+ for fn in footnotes_root.xpath('//w:footnote', namespaces=nsmap):
+ fn_id = fn.get(f'{{{W_NS}}}id')
+ if fn_id and fn_id not in referenced_ids and fn_id not in ['-1', '0']:
+ footnotes_root.remove(fn)
+ orphans_removed.append(fn_id)
+
+ # Write modified document
+ temp_file = working_file + '.tmp'
+ with zipfile.ZipFile(temp_file, 'w', zipfile.ZIP_DEFLATED) as zout:
+ with zipfile.ZipFile(filename, 'r') as zin:
+ for item in zin.infolist():
+ if item.filename == 'word/document.xml':
+ zout.writestr(item,
+ etree.tostring(doc_root, encoding='UTF-8', xml_declaration=True, standalone="yes"))
+ elif item.filename == 'word/footnotes.xml':
+ zout.writestr(item,
+ etree.tostring(footnotes_root, encoding='UTF-8', xml_declaration=True, standalone="yes"))
+ else:
+ zout.writestr(item, zin.read(item.filename))
+
+ os.replace(temp_file, working_file)
+
+ details = {
+ 'footnote_id': footnote_id,
+ 'references_removed': refs_removed,
+ 'content_removed': content_removed,
+ 'orphans_removed': orphans_removed
+ }
+
+ message = f"Successfully deleted footnote {footnote_id}"
+ if orphans_removed:
+ message += f" and {len(orphans_removed)} orphaned footnotes"
+
+ return True, message, details
+
+ except Exception as e:
+ return False, f"Error deleting footnote: {str(e)}", None
+
+
+def validate_document_footnotes(filename: str) -> Tuple[bool, str, Dict[str, Any]]:
+ """Validate all footnotes in a document for coherence and compliance."""
+
+ if not os.path.exists(filename):
+ return False, f"File not found: {filename}", {}
+
+ report = {
+ 'total_references': 0,
+ 'total_content': 0,
+ 'id_conflicts': [],
+ 'orphaned_content': [],
+ 'missing_references': [],
+ 'invalid_locations': [],
+ 'missing_styles': [],
+ 'coherence_issues': []
+ }
+
+ try:
+ with zipfile.ZipFile(filename, 'r') as zf:
+ # Check document.xml
+ doc_xml = zf.read('word/document.xml')
+ doc_root = etree.fromstring(doc_xml)
+ nsmap = {'w': W_NS}
+
+ # Get all footnote references
+ ref_ids = set()
+ for ref in doc_root.xpath('//w:footnoteReference', namespaces=nsmap):
+ ref_id = ref.get(f'{{{W_NS}}}id')
+ if ref_id:
+ ref_ids.add(ref_id)
+ report['total_references'] += 1
+
+ # Check location
+ parent = ref.getparent()
+ while parent is not None:
+ if parent.tag in [f'{{{W_NS}}}hdr', f'{{{W_NS}}}ftr']:
+ report['invalid_locations'].append(ref_id)
+ break
+ parent = parent.getparent()
+
+ # Check footnotes.xml
+ if 'word/footnotes.xml' in zf.namelist():
+ footnotes_xml = zf.read('word/footnotes.xml')
+ footnotes_root = etree.fromstring(footnotes_xml)
+
+ content_ids = set()
+ for fn in footnotes_root.xpath('//w:footnote', namespaces=nsmap):
+ fn_id = fn.get(f'{{{W_NS}}}id')
+ if fn_id:
+ content_ids.add(fn_id)
+ if fn_id not in ['-1', '0']: # Exclude separators
+ report['total_content'] += 1
+
+ # Find orphans and missing
+ report['orphaned_content'] = list(content_ids - ref_ids - {'-1', '0'})
+ report['missing_references'] = list(ref_ids - content_ids)
+ else:
+ if report['total_references'] > 0:
+ report['coherence_issues'].append('References exist but no footnotes.xml')
+
+ # Check relationships
+ if 'word/_rels/document.xml.rels' in zf.namelist():
+ rels_xml = zf.read('word/_rels/document.xml.rels')
+ rels_root = etree.fromstring(rels_xml)
+ rel_nsmap = {'r': REL_NS}
+
+ fn_rels = rels_root.xpath(
+ "//r:Relationship[contains(@Type, 'footnotes')]",
+ namespaces=rel_nsmap
+ )
+
+ if report['total_content'] > 0 and len(fn_rels) == 0:
+ report['coherence_issues'].append('Missing footnotes relationship')
+ elif len(fn_rels) > 1:
+ report['coherence_issues'].append(f'Multiple footnote relationships: {len(fn_rels)}')
+
+ # Check content types
+ if '[Content_Types].xml' in zf.namelist():
+ ct_xml = zf.read('[Content_Types].xml')
+ ct_root = etree.fromstring(ct_xml)
+ ct_nsmap = {'ct': CT_NS}
+
+ fn_overrides = ct_root.xpath(
+ "//ct:Override[@PartName='/word/footnotes.xml']",
+ namespaces=ct_nsmap
+ )
+
+ if report['total_content'] > 0 and len(fn_overrides) == 0:
+ report['coherence_issues'].append('Missing footnotes content type')
+ elif len(fn_overrides) > 1:
+ report['coherence_issues'].append(f'Multiple footnote content types: {len(fn_overrides)}')
+
+ # Check styles
+ if 'word/styles.xml' in zf.namelist():
+ styles_xml = zf.read('word/styles.xml')
+ styles_root = etree.fromstring(styles_xml)
+
+ ref_style = styles_root.xpath('//w:style[@w:styleId="FootnoteReference"]', namespaces=nsmap)
+ text_style = styles_root.xpath('//w:style[@w:styleId="FootnoteText"]', namespaces=nsmap)
+
+ if not ref_style:
+ report['missing_styles'].append('FootnoteReference')
+ if not text_style:
+ report['missing_styles'].append('FootnoteText')
+
+ # Determine if valid
+ is_valid = (
+ len(report['id_conflicts']) == 0 and
+ len(report['orphaned_content']) == 0 and
+ len(report['missing_references']) == 0 and
+ len(report['invalid_locations']) == 0 and
+ len(report['coherence_issues']) == 0
+ )
+
+ if is_valid:
+ message = "Document footnotes are valid"
+ else:
+ message = "Document has footnote issues"
+
+ return is_valid, message, report
+
+ except Exception as e:
+ return False, f"Error validating document: {str(e)}", report
+
+
+# ============================================================================
+# COMPATIBILITY FUNCTIONS (for backward compatibility)
+# ============================================================================
+
+def add_footnote_at_paragraph_end(
+ filename: str,
+ paragraph_index: int,
+ footnote_text: str,
+ output_filename: Optional[str] = None
+) -> Tuple[bool, str]:
+ """Add footnote at the end of a specific paragraph (backward compatibility)."""
+ success, message, _ = add_footnote_robust(
+ filename=filename,
+ paragraph_index=paragraph_index,
+ footnote_text=footnote_text,
+ output_filename=output_filename,
+ position="after"
+ )
+ return success, message
+
+
+def add_footnote_with_proper_formatting(
+ filename: str,
+ search_text: str,
+ footnote_text: str,
+ output_filename: Optional[str] = None,
+ position: str = "after"
+) -> Tuple[bool, str]:
+ """Add footnote with proper formatting (backward compatibility)."""
+ success, message, _ = add_footnote_robust(
+ filename=filename,
+ search_text=search_text,
+ footnote_text=footnote_text,
+ output_filename=output_filename,
+ position=position
+ )
+ return success, message
+
+
+def delete_footnote(
+ filename: str,
+ footnote_id: Optional[int] = None,
+ search_text: Optional[str] = None,
+ output_filename: Optional[str] = None
+) -> Tuple[bool, str]:
+ """Delete a footnote (backward compatibility)."""
+ success, message, _ = delete_footnote_robust(
+ filename=filename,
+ footnote_id=footnote_id,
+ search_text=search_text,
+ output_filename=output_filename
+ )
+ return success, message
+
+
+# ============================================================================
+# LEGACY FUNCTIONS (for core/__init__.py compatibility)
+# ============================================================================
+
+def add_footnote(doc, paragraph_index: int, footnote_text: str):
+ """Legacy function for adding footnotes to python-docx Document objects.
+ Note: This is a simplified version that doesn't create proper Word footnotes."""
+ if paragraph_index >= len(doc.paragraphs):
+ raise IndexError(f"Paragraph index {paragraph_index} out of range")
+
+ para = doc.paragraphs[paragraph_index]
+ # Add superscript number
+ run = para.add_run()
+ run.text = "¹"
+ run.font.superscript = True
+
+ # Add footnote text at document end
+ doc.add_paragraph("_" * 50)
+ footnote_para = doc.add_paragraph(f"¹ {footnote_text}")
+ footnote_para.style = "Caption"
+
+ return doc
+
+
+def add_endnote(doc, paragraph_index: int, endnote_text: str):
+ """Legacy function for adding endnotes."""
+ if paragraph_index >= len(doc.paragraphs):
+ raise IndexError(f"Paragraph index {paragraph_index} out of range")
+
+ para = doc.paragraphs[paragraph_index]
+ run = para.add_run()
+ run.text = "†"
+ run.font.superscript = True
+
+ # Endnotes go at the very end
+ doc.add_page_break()
+ doc.add_heading("Endnotes", level=1)
+ endnote_para = doc.add_paragraph(f"† {endnote_text}")
+
+ return doc
+
+
+def convert_footnotes_to_endnotes(doc):
+ """Legacy function to convert footnotes to endnotes in a Document object."""
+ # This is a placeholder - real conversion requires XML manipulation
+ return doc
+
+
+# Define XML_NS if needed
+XML_NS = 'http://www.w3.org/XML/1998/namespace'
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/core/protection.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/core/protection.py
new file mode 100644
index 00000000..e706fd79
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/core/protection.py
@@ -0,0 +1,242 @@
+"""
+Document protection functionality for Word Document Server.
+"""
+import os
+import json
+import hashlib
+import datetime
+from typing import Dict, List, Tuple, Optional, Any
+
+
+def add_protection_info(doc_path: str, protection_type: str, password_hash: str,
+ sections: Optional[List[str]] = None,
+ signature_info: Optional[Dict[str, Any]] = None,
+ raw_password: Optional[str] = None) -> bool:
+ """
+ Add document protection information to a separate metadata file and encrypt the document.
+
+ Args:
+ doc_path: Path to the document
+ protection_type: Type of protection ('password', 'restricted', 'signature')
+ password_hash: Hashed password for security
+ sections: List of section names that can be edited (for restricted editing)
+ signature_info: Information about digital signature
+ raw_password: The actual password for document encryption
+
+ Returns:
+ True if protection info was successfully added, False otherwise
+ """
+ # Create metadata filename based on document path
+ base_path, _ = os.path.splitext(doc_path)
+ metadata_path = f"{base_path}.protection"
+
+ # Prepare protection data
+ protection_data = {
+ "type": protection_type,
+ "password_hash": password_hash,
+ "applied_date": datetime.datetime.now().isoformat(),
+ }
+
+ if sections:
+ protection_data["editable_sections"] = sections
+
+ if signature_info:
+ protection_data["signature"] = signature_info
+
+ # Write protection info to metadata file
+ try:
+ with open(metadata_path, 'w') as f:
+ json.dump(protection_data, f, indent=2)
+
+ # Apply actual document encryption if raw_password is provided
+ if protection_type == "password" and raw_password:
+ import msoffcrypto
+ import tempfile
+ import shutil
+
+ # Create a temporary file for the encrypted output
+ temp_fd, temp_path = tempfile.mkstemp(suffix='.docx')
+ os.close(temp_fd)
+
+ try:
+ # Open the document
+ with open(doc_path, 'rb') as f:
+ office_file = msoffcrypto.OfficeFile(f)
+
+ # Encrypt with password
+ office_file.load_key(password=raw_password)
+
+ # Write the encrypted file to the temp path
+ with open(temp_path, 'wb') as out_file:
+ office_file.encrypt(out_file)
+
+ # Replace original with encrypted version
+ shutil.move(temp_path, doc_path)
+
+ # Update metadata to note that true encryption was applied
+ protection_data["true_encryption"] = True
+ with open(metadata_path, 'w') as f:
+ json.dump(protection_data, f, indent=2)
+
+ except Exception as e:
+ print(f"Encryption error: {str(e)}")
+ if os.path.exists(temp_path):
+ os.unlink(temp_path)
+ return False
+
+ return True
+ except Exception as e:
+ print(f"Protection error: {str(e)}")
+ return False
+
+
+def verify_document_protection(doc_path: str, password: Optional[str] = None) -> Tuple[bool, str]:
+ """
+ Verify if a document is protected and if the password is correct.
+
+ Args:
+ doc_path: Path to the document
+ password: Password to verify
+
+ Returns:
+ Tuple of (is_protected_and_verified, message)
+ """
+ base_path, _ = os.path.splitext(doc_path)
+ metadata_path = f"{base_path}.protection"
+
+ # Check if protection metadata exists
+ if not os.path.exists(metadata_path):
+ return False, "Document is not protected"
+
+ try:
+ # Read protection data
+ with open(metadata_path, 'r') as f:
+ protection_data = json.load(f)
+
+ # If password is provided, verify it
+ if password:
+ password_hash = hashlib.sha256(password.encode()).hexdigest()
+ if password_hash != protection_data.get("password_hash"):
+ return False, "Incorrect password"
+
+ # Return protection type
+ protection_type = protection_data.get("type", "unknown")
+ return True, f"Document is protected with {protection_type} protection"
+
+ except Exception as e:
+ return False, f"Error verifying protection: {str(e)}"
+
+
+def is_section_editable(doc_path: str, section_name: str) -> bool:
+ """
+ Check if a specific section of a document is editable.
+
+ Args:
+ doc_path: Path to the document
+ section_name: Name of the section to check
+
+ Returns:
+ True if section is editable, False otherwise
+ """
+ base_path, _ = os.path.splitext(doc_path)
+ metadata_path = f"{base_path}.protection"
+
+ # Check if protection metadata exists
+ if not os.path.exists(metadata_path):
+ # If no protection exists, all sections are editable
+ return True
+
+ try:
+ # Read protection data
+ with open(metadata_path, 'r') as f:
+ protection_data = json.load(f)
+
+ # Check protection type
+ if protection_data.get("type") != "restricted":
+ # If not restricted editing, return based on protection type
+ return protection_data.get("type") != "password"
+
+ # Check if the section is in the list of editable sections
+ editable_sections = protection_data.get("editable_sections", [])
+ return section_name in editable_sections
+
+ except Exception:
+ # In case of error, default to not editable for security
+ return False
+
+
+def create_signature_info(doc, signer_name: str, reason: Optional[str] = None) -> Dict[str, Any]:
+ """
+ Create signature information for a document.
+
+ Args:
+ doc: Document object
+ signer_name: Name of the person signing the document
+ reason: Optional reason for signing
+
+ Returns:
+ Dictionary containing signature information
+ """
+ # Create signature info
+ signature_info = {
+ "signer": signer_name,
+ "timestamp": datetime.datetime.now().isoformat(),
+ }
+
+ if reason:
+ signature_info["reason"] = reason
+
+ # Generate a simple signature hash based on document content and metadata
+ text_content = "\n".join([p.text for p in doc.paragraphs])
+ content_hash = hashlib.sha256(text_content.encode()).hexdigest()
+ signature_info["content_hash"] = content_hash
+
+ return signature_info
+
+
+def verify_signature(doc_path: str) -> Tuple[bool, str]:
+ """
+ Verify a document's digital signature.
+
+ Args:
+ doc_path: Path to the document
+
+ Returns:
+ Tuple of (is_valid, message)
+ """
+ from docx import Document
+
+ base_path, _ = os.path.splitext(doc_path)
+ metadata_path = f"{base_path}.protection"
+
+ if not os.path.exists(metadata_path):
+ return False, "Document is not signed"
+
+ try:
+ # Read protection data
+ with open(metadata_path, 'r') as f:
+ protection_data = json.load(f)
+
+ if protection_data.get("type") != "signature":
+ return False, f"Document is protected with {protection_data.get('type')} protection, not a signature"
+
+ # Get the original content hash
+ signature_info = protection_data.get("signature", {})
+ original_hash = signature_info.get("content_hash")
+
+ if not original_hash:
+ return False, "Invalid signature: missing content hash"
+
+ # Calculate current content hash
+ doc = Document(doc_path)
+ text_content = "\n".join([p.text for p in doc.paragraphs])
+ current_hash = hashlib.sha256(text_content.encode()).hexdigest()
+
+ # Compare hashes
+ if current_hash != original_hash:
+ return False, f"Document has been modified since it was signed by {signature_info.get('signer')}"
+
+ return True, f"Document signature is valid. Signed by {signature_info.get('signer')} on {signature_info.get('timestamp')}"
+
+ except Exception as e:
+ return False, f"Error verifying signature: {str(e)}"
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/core/styles.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/core/styles.py
new file mode 100644
index 00000000..49c5d51f
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/core/styles.py
@@ -0,0 +1,134 @@
+"""
+Style-related functions for Word Document Server.
+"""
+from docx.shared import Pt
+from docx.enum.style import WD_STYLE_TYPE
+
+
+def ensure_heading_style(doc):
+ """
+ Ensure Heading styles exist in the document.
+
+ Args:
+ doc: Document object
+ """
+ for i in range(1, 10): # Create Heading 1 through Heading 9
+ style_name = f'Heading {i}'
+ try:
+ # Try to access the style to see if it exists
+ style = doc.styles[style_name]
+ except KeyError:
+ # Create the style if it doesn't exist
+ try:
+ style = doc.styles.add_style(style_name, WD_STYLE_TYPE.PARAGRAPH)
+ if i == 1:
+ style.font.size = Pt(16)
+ style.font.bold = True
+ elif i == 2:
+ style.font.size = Pt(14)
+ style.font.bold = True
+ else:
+ style.font.size = Pt(12)
+ style.font.bold = True
+ except Exception:
+ # If style creation fails, we'll just use default formatting
+ pass
+
+
+def ensure_table_style(doc):
+ """
+ Ensure Table Grid style exists in the document.
+
+ Args:
+ doc: Document object
+ """
+ try:
+ # Try to access the style to see if it exists
+ style = doc.styles['Table Grid']
+ except KeyError:
+ # If style doesn't exist, we'll handle it at usage time
+ pass
+
+
+def create_style(doc, style_name, style_type, base_style=None, font_properties=None, paragraph_properties=None):
+ """
+ Create a new style in the document.
+
+ Args:
+ doc: Document object
+ style_name: Name for the new style
+ style_type: Type of style (WD_STYLE_TYPE)
+ base_style: Optional base style to inherit from
+ font_properties: Dictionary of font properties (bold, italic, size, name, color)
+ paragraph_properties: Dictionary of paragraph properties (alignment, spacing)
+
+ Returns:
+ The created style
+ """
+ from docx.shared import Pt
+
+ try:
+ # Check if style already exists
+ style = doc.styles.get_by_id(style_name, WD_STYLE_TYPE.PARAGRAPH)
+ return style
+ except:
+ # Create new style
+ new_style = doc.styles.add_style(style_name, style_type)
+
+ # Set base style if specified
+ if base_style:
+ new_style.base_style = doc.styles[base_style]
+
+ # Set font properties
+ if font_properties:
+ font = new_style.font
+ if 'bold' in font_properties:
+ font.bold = font_properties['bold']
+ if 'italic' in font_properties:
+ font.italic = font_properties['italic']
+ if 'size' in font_properties:
+ font.size = Pt(font_properties['size'])
+ if 'name' in font_properties:
+ font.name = font_properties['name']
+ if 'color' in font_properties:
+ from docx.shared import RGBColor
+
+ # Define common RGB colors
+ color_map = {
+ 'red': RGBColor(255, 0, 0),
+ 'blue': RGBColor(0, 0, 255),
+ 'green': RGBColor(0, 128, 0),
+ 'yellow': RGBColor(255, 255, 0),
+ 'black': RGBColor(0, 0, 0),
+ 'gray': RGBColor(128, 128, 128),
+ 'white': RGBColor(255, 255, 255),
+ 'purple': RGBColor(128, 0, 128),
+ 'orange': RGBColor(255, 165, 0)
+ }
+
+ color_value = font_properties['color']
+ try:
+ # Handle string color names
+ if isinstance(color_value, str) and color_value.lower() in color_map:
+ font.color.rgb = color_map[color_value.lower()]
+ # Handle RGBColor objects
+ elif hasattr(color_value, 'rgb'):
+ font.color.rgb = color_value
+ # Try to parse as RGB string
+ elif isinstance(color_value, str):
+ font.color.rgb = RGBColor.from_string(color_value)
+ # Use directly if it's already an RGB value
+ else:
+ font.color.rgb = color_value
+ except Exception as e:
+ # Fallback to black if all else fails
+ font.color.rgb = RGBColor(0, 0, 0)
+
+ # Set paragraph properties
+ if paragraph_properties:
+ if 'alignment' in paragraph_properties:
+ new_style.paragraph_format.alignment = paragraph_properties['alignment']
+ if 'spacing' in paragraph_properties:
+ new_style.paragraph_format.line_spacing = paragraph_properties['spacing']
+
+ return new_style
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/core/tables.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/core/tables.py
new file mode 100644
index 00000000..9cc8dcf6
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/core/tables.py
@@ -0,0 +1,866 @@
+"""
+Table-related operations for Word Document Server.
+"""
+from docx.oxml.shared import OxmlElement, qn
+from docx.oxml.ns import nsdecls
+from docx.oxml import parse_xml
+from docx.shared import RGBColor, Inches, Cm, Pt
+from docx.enum.text import WD_ALIGN_PARAGRAPH
+from docx.enum.table import WD_CELL_VERTICAL_ALIGNMENT
+
+
+def set_cell_border(cell, **kwargs):
+ """
+ Set cell border properties.
+
+ Args:
+ cell: The cell to modify
+ **kwargs: Border properties (top, bottom, left, right, val, color)
+ """
+ tc = cell._tc
+ tcPr = tc.get_or_add_tcPr()
+
+ # Create border elements
+ for key, value in kwargs.items():
+ if key in ['top', 'left', 'bottom', 'right']:
+ tag = 'w:{}'.format(key)
+
+ element = OxmlElement(tag)
+ element.set(qn('w:val'), kwargs.get('val', 'single'))
+ element.set(qn('w:sz'), kwargs.get('sz', '4'))
+ element.set(qn('w:space'), kwargs.get('space', '0'))
+ element.set(qn('w:color'), kwargs.get('color', 'auto'))
+
+ tcBorders = tcPr.first_child_found_in("w:tcBorders")
+ if tcBorders is None:
+ tcBorders = OxmlElement('w:tcBorders')
+ tcPr.append(tcBorders)
+
+ tcBorders.append(element)
+
+
+def apply_table_style(table, has_header_row=False, border_style=None, shading=None):
+ """
+ Apply formatting to a table.
+
+ Args:
+ table: The table to format
+ has_header_row: If True, formats the first row as a header
+ border_style: Style for borders ('none', 'single', 'double', 'thick')
+ shading: 2D list of cell background colors (by row and column)
+
+ Returns:
+ True if successful, False otherwise
+ """
+ try:
+ # Format header row if requested
+ if has_header_row and table.rows:
+ header_row = table.rows[0]
+ for cell in header_row.cells:
+ for paragraph in cell.paragraphs:
+ if paragraph.runs:
+ for run in paragraph.runs:
+ run.bold = True
+
+ # Apply border style if specified
+ if border_style:
+ val_map = {
+ 'none': 'nil',
+ 'single': 'single',
+ 'double': 'double',
+ 'thick': 'thick'
+ }
+ val = val_map.get(border_style.lower(), 'single')
+
+ # Apply to all cells
+ for row in table.rows:
+ for cell in row.cells:
+ set_cell_border(
+ cell,
+ top=True,
+ bottom=True,
+ left=True,
+ right=True,
+ val=val,
+ color="000000"
+ )
+
+ # Apply cell shading if specified
+ if shading:
+ for i, row_colors in enumerate(shading):
+ if i >= len(table.rows):
+ break
+ for j, color in enumerate(row_colors):
+ if j >= len(table.rows[i].cells):
+ break
+ try:
+ # Apply shading to cell
+ cell = table.rows[i].cells[j]
+ shading_elm = parse_xml(f' ')
+ cell._tc.get_or_add_tcPr().append(shading_elm)
+ except:
+ # Skip if color format is invalid
+ pass
+
+ return True
+ except Exception:
+ return False
+
+
+def copy_table(source_table, target_doc):
+ """
+ Copy a table from one document to another.
+
+ Args:
+ source_table: The table to copy
+ target_doc: The document to copy the table to
+
+ Returns:
+ The new table in the target document
+ """
+ # Create a new table with the same dimensions
+ new_table = target_doc.add_table(rows=len(source_table.rows), cols=len(source_table.columns))
+
+ # Try to apply the same style
+ try:
+ if source_table.style:
+ new_table.style = source_table.style
+ except:
+ # Fall back to default grid style
+ try:
+ new_table.style = 'Table Grid'
+ except:
+ pass
+
+ # Copy cell contents
+ for i, row in enumerate(source_table.rows):
+ for j, cell in enumerate(row.cells):
+ for paragraph in cell.paragraphs:
+ if paragraph.text:
+ new_table.cell(i, j).text = paragraph.text
+
+ return new_table
+
+
+def set_cell_shading(cell, fill_color=None, pattern="clear", pattern_color="auto"):
+ """
+ Apply shading/filling to a table cell.
+
+ Args:
+ cell: The table cell to format
+ fill_color: Background color (hex string like "FF0000" or RGBColor)
+ pattern: Shading pattern ("clear", "solid", "pct10", "pct20", etc.)
+ pattern_color: Pattern color for patterned fills
+
+ Returns:
+ True if successful, False otherwise
+ """
+ try:
+ # Get or create table cell properties
+ tc_pr = cell._tc.get_or_add_tcPr()
+
+ # Remove existing shading
+ existing_shd = tc_pr.find(qn('w:shd'))
+ if existing_shd is not None:
+ tc_pr.remove(existing_shd)
+
+ # Create shading element
+ shd_attrs = {
+ 'w:val': pattern,
+ 'w:color': pattern_color if pattern_color != "auto" else "auto"
+ }
+
+ # Set fill color
+ if fill_color:
+ if isinstance(fill_color, str):
+ # Hex color string - remove # if present
+ fill_color = fill_color.lstrip('#').upper()
+ if len(fill_color) == 6: # Valid hex color
+ shd_attrs['w:fill'] = fill_color
+ elif isinstance(fill_color, RGBColor):
+ # RGBColor object
+ hex_color = f"{fill_color.r:02X}{fill_color.g:02X}{fill_color.b:02X}"
+ shd_attrs['w:fill'] = hex_color
+
+ # Build XML string
+ attr_str = ' '.join([f'{k}="{v}"' for k, v in shd_attrs.items()])
+ shd_xml = f' '
+
+ # Parse and append shading element
+ shading_elm = parse_xml(shd_xml)
+ tc_pr.append(shading_elm)
+
+ return True
+
+ except Exception as e:
+ print(f"Error setting cell shading: {e}")
+ return False
+
+
+def apply_alternating_row_shading(table, color1="FFFFFF", color2="F2F2F2"):
+ """
+ Apply alternating row colors for better readability.
+
+ Args:
+ table: The table to format
+ color1: Color for odd rows (hex string)
+ color2: Color for even rows (hex string)
+
+ Returns:
+ True if successful, False otherwise
+ """
+ try:
+ for i, row in enumerate(table.rows):
+ fill_color = color1 if i % 2 == 0 else color2
+ for cell in row.cells:
+ set_cell_shading(cell, fill_color=fill_color)
+ return True
+ except Exception as e:
+ print(f"Error applying alternating row shading: {e}")
+ return False
+
+
+def highlight_header_row(table, header_color="4472C4", text_color="FFFFFF"):
+ """
+ Apply special shading to header row.
+
+ Args:
+ table: The table to format
+ header_color: Background color for header (hex string)
+ text_color: Text color for header (hex string)
+
+ Returns:
+ True if successful, False otherwise
+ """
+ try:
+ if table.rows:
+ for cell in table.rows[0].cells:
+ # Apply background shading
+ set_cell_shading(cell, fill_color=header_color)
+
+ # Apply text formatting
+ for paragraph in cell.paragraphs:
+ for run in paragraph.runs:
+ run.bold = True
+ if text_color and text_color != "auto":
+ # Convert hex to RGB
+ try:
+ text_color = text_color.lstrip('#')
+ r = int(text_color[0:2], 16)
+ g = int(text_color[2:4], 16)
+ b = int(text_color[4:6], 16)
+ run.font.color.rgb = RGBColor(r, g, b)
+ except:
+ pass # Skip if color format is invalid
+ return True
+ except Exception as e:
+ print(f"Error highlighting header row: {e}")
+ return False
+
+
+def set_cell_shading_by_position(table, row_index, col_index, fill_color, pattern="clear"):
+ """
+ Apply shading to a specific cell by row/column position.
+
+ Args:
+ table: The table containing the cell
+ row_index: Row index (0-based)
+ col_index: Column index (0-based)
+ fill_color: Background color (hex string)
+ pattern: Shading pattern
+
+ Returns:
+ True if successful, False otherwise
+ """
+ try:
+ if (0 <= row_index < len(table.rows) and
+ 0 <= col_index < len(table.rows[row_index].cells)):
+ cell = table.rows[row_index].cells[col_index]
+ return set_cell_shading(cell, fill_color=fill_color, pattern=pattern)
+ else:
+ return False
+ except Exception as e:
+ print(f"Error setting cell shading by position: {e}")
+ return False
+
+
+def merge_cells(table, start_row, start_col, end_row, end_col):
+ """
+ Merge cells in a rectangular area.
+
+ Args:
+ table: The table containing cells to merge
+ start_row: Starting row index (0-based)
+ start_col: Starting column index (0-based)
+ end_row: Ending row index (0-based, inclusive)
+ end_col: Ending column index (0-based, inclusive)
+
+ Returns:
+ True if successful, False otherwise
+ """
+ try:
+ # Validate indices
+ if (start_row < 0 or start_col < 0 or end_row < 0 or end_col < 0 or
+ start_row >= len(table.rows) or end_row >= len(table.rows) or
+ start_row > end_row or start_col > end_col):
+ return False
+
+ # Check if all rows have enough columns
+ for row_idx in range(start_row, end_row + 1):
+ if (start_col >= len(table.rows[row_idx].cells) or
+ end_col >= len(table.rows[row_idx].cells)):
+ return False
+
+ # Get the start and end cells
+ start_cell = table.cell(start_row, start_col)
+ end_cell = table.cell(end_row, end_col)
+
+ # Merge the cells
+ start_cell.merge(end_cell)
+
+ return True
+
+ except Exception as e:
+ print(f"Error merging cells: {e}")
+ return False
+
+
+def merge_cells_horizontal(table, row_index, start_col, end_col):
+ """
+ Merge cells horizontally in a single row.
+
+ Args:
+ table: The table containing cells to merge
+ row_index: Row index (0-based)
+ start_col: Starting column index (0-based)
+ end_col: Ending column index (0-based, inclusive)
+
+ Returns:
+ True if successful, False otherwise
+ """
+ return merge_cells(table, row_index, start_col, row_index, end_col)
+
+
+def merge_cells_vertical(table, col_index, start_row, end_row):
+ """
+ Merge cells vertically in a single column.
+
+ Args:
+ table: The table containing cells to merge
+ col_index: Column index (0-based)
+ start_row: Starting row index (0-based)
+ end_row: Ending row index (0-based, inclusive)
+
+ Returns:
+ True if successful, False otherwise
+ """
+ return merge_cells(table, start_row, col_index, end_row, col_index)
+
+
+def set_cell_alignment(cell, horizontal="left", vertical="top"):
+ """
+ Set text alignment within a cell.
+
+ Args:
+ cell: The table cell to format
+ horizontal: Horizontal alignment ("left", "center", "right", "justify")
+ vertical: Vertical alignment ("top", "center", "bottom")
+
+ Returns:
+ True if successful, False otherwise
+ """
+ try:
+ # Set horizontal alignment for all paragraphs in the cell
+ for paragraph in cell.paragraphs:
+ if horizontal.lower() == "center":
+ paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
+ elif horizontal.lower() == "right":
+ paragraph.alignment = WD_ALIGN_PARAGRAPH.RIGHT
+ elif horizontal.lower() == "justify":
+ paragraph.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
+ else: # default to left
+ paragraph.alignment = WD_ALIGN_PARAGRAPH.LEFT
+
+ # Set vertical alignment for the cell using XML manipulation
+ tc_pr = cell._tc.get_or_add_tcPr()
+
+ # Remove existing vertical alignment
+ existing_valign = tc_pr.find(qn('w:vAlign'))
+ if existing_valign is not None:
+ tc_pr.remove(existing_valign)
+
+ # Create vertical alignment element
+ valign_element = OxmlElement('w:vAlign')
+ if vertical.lower() == "center":
+ valign_element.set(qn('w:val'), 'center')
+ elif vertical.lower() == "bottom":
+ valign_element.set(qn('w:val'), 'bottom')
+ else: # default to top
+ valign_element.set(qn('w:val'), 'top')
+
+ tc_pr.append(valign_element)
+
+ return True
+
+ except Exception as e:
+ print(f"Error setting cell alignment: {e}")
+ return False
+
+
+def set_cell_alignment_by_position(table, row_index, col_index, horizontal="left", vertical="top"):
+ """
+ Set text alignment for a specific cell by position.
+
+ Args:
+ table: The table containing the cell
+ row_index: Row index (0-based)
+ col_index: Column index (0-based)
+ horizontal: Horizontal alignment ("left", "center", "right", "justify")
+ vertical: Vertical alignment ("top", "center", "bottom")
+
+ Returns:
+ True if successful, False otherwise
+ """
+ try:
+ if (0 <= row_index < len(table.rows) and
+ 0 <= col_index < len(table.rows[row_index].cells)):
+ cell = table.rows[row_index].cells[col_index]
+ return set_cell_alignment(cell, horizontal, vertical)
+ else:
+ return False
+ except Exception as e:
+ print(f"Error setting cell alignment by position: {e}")
+ return False
+
+
+def set_table_alignment(table, horizontal="left", vertical="top"):
+ """
+ Set text alignment for all cells in a table.
+
+ Args:
+ table: The table to format
+ horizontal: Horizontal alignment ("left", "center", "right", "justify")
+ vertical: Vertical alignment ("top", "center", "bottom")
+
+ Returns:
+ True if successful, False otherwise
+ """
+ try:
+ for row in table.rows:
+ for cell in row.cells:
+ set_cell_alignment(cell, horizontal, vertical)
+ return True
+ except Exception as e:
+ print(f"Error setting table alignment: {e}")
+ return False
+
+
+def set_column_width(table, col_index, width, width_type="dxa"):
+ """
+ Set the width of a specific column in a table.
+
+ Args:
+ table: The table to modify
+ col_index: Column index (0-based)
+ width: Column width value
+ width_type: Width type ("dxa" for points*20, "pct" for percentage*50, "auto")
+
+ Returns:
+ True if successful, False otherwise
+ """
+ try:
+ # Validate column index
+ if col_index < 0 or col_index >= len(table.columns):
+ return False
+
+ # Convert width based on type
+ if width_type == "dxa":
+ # DXA units (twentieths of a point)
+ if isinstance(width, (int, float)):
+ width_value = str(int(width * 20))
+ else:
+ width_value = str(width)
+ elif width_type == "pct":
+ # Percentage (multiply by 50 for Word format)
+ if isinstance(width, (int, float)):
+ width_value = str(int(width * 50))
+ else:
+ width_value = str(width)
+ else:
+ width_value = str(width)
+
+ # Iterate through all rows and set width for cells in the specified column
+ for row in table.rows:
+ if col_index < len(row.cells):
+ cell = row.cells[col_index]
+ tc_pr = cell._tc.get_or_add_tcPr()
+
+ # Remove existing width
+ existing_width = tc_pr.find(qn('w:tcW'))
+ if existing_width is not None:
+ tc_pr.remove(existing_width)
+
+ # Create new width element
+ width_element = OxmlElement('w:tcW')
+ width_element.set(qn('w:w'), width_value)
+ width_element.set(qn('w:type'), width_type)
+
+ tc_pr.append(width_element)
+
+ return True
+
+ except Exception as e:
+ print(f"Error setting column width: {e}")
+ return False
+
+
+def set_column_width_by_position(table, col_index, width, width_type="dxa"):
+ """
+ Set the width of a specific column by position.
+
+ Args:
+ table: The table containing the column
+ col_index: Column index (0-based)
+ width: Column width value
+ width_type: Width type ("dxa" for points*20, "pct" for percentage*50, "auto")
+
+ Returns:
+ True if successful, False otherwise
+ """
+ return set_column_width(table, col_index, width, width_type)
+
+
+def set_column_widths(table, widths, width_type="dxa"):
+ """
+ Set widths for multiple columns in a table.
+
+ Args:
+ table: The table to modify
+ widths: List of width values for each column
+ width_type: Width type ("dxa" for points*20, "pct" for percentage*50, "auto")
+
+ Returns:
+ True if successful, False otherwise
+ """
+ try:
+ for col_index, width in enumerate(widths):
+ if col_index >= len(table.columns):
+ break
+ if not set_column_width(table, col_index, width, width_type):
+ return False
+ return True
+ except Exception as e:
+ print(f"Error setting column widths: {e}")
+ return False
+
+
+def set_table_width(table, width, width_type="dxa"):
+ """
+ Set the overall width of a table.
+
+ Args:
+ table: The table to modify
+ width: Table width value
+ width_type: Width type ("dxa" for points*20, "pct" for percentage*50, "auto")
+
+ Returns:
+ True if successful, False otherwise
+ """
+ try:
+ # Convert width based on type
+ if width_type == "dxa":
+ # DXA units (twentieths of a point)
+ if isinstance(width, (int, float)):
+ width_value = str(int(width * 20))
+ else:
+ width_value = str(width)
+ elif width_type == "pct":
+ # Percentage (multiply by 50 for Word format)
+ if isinstance(width, (int, float)):
+ width_value = str(int(width * 50))
+ else:
+ width_value = str(width)
+ else:
+ width_value = str(width)
+
+ # Get table element and properties
+ tbl = table._tbl
+
+ # Get or create table properties
+ tbl_pr = tbl.find(qn('w:tblPr'))
+ if tbl_pr is None:
+ tbl_pr = OxmlElement('w:tblPr')
+ tbl.insert(0, tbl_pr)
+
+ # Remove existing table width
+ existing_width = tbl_pr.find(qn('w:tblW'))
+ if existing_width is not None:
+ tbl_pr.remove(existing_width)
+
+ # Create new table width element
+ width_element = OxmlElement('w:tblW')
+ width_element.set(qn('w:w'), width_value)
+ width_element.set(qn('w:type'), width_type)
+
+ tbl_pr.append(width_element)
+
+ return True
+
+ except Exception as e:
+ print(f"Error setting table width: {e}")
+ return False
+
+
+def auto_fit_table(table):
+ """
+ Set table to auto-fit columns based on content.
+
+ Args:
+ table: The table to modify
+
+ Returns:
+ True if successful, False otherwise
+ """
+ try:
+ # Get table element and properties
+ tbl = table._tbl
+
+ # Get or create table properties
+ tbl_pr = tbl.find(qn('w:tblPr'))
+ if tbl_pr is None:
+ tbl_pr = OxmlElement('w:tblPr')
+ tbl.insert(0, tbl_pr)
+
+ # Remove existing layout
+ existing_layout = tbl_pr.find(qn('w:tblLayout'))
+ if existing_layout is not None:
+ tbl_pr.remove(existing_layout)
+
+ # Create auto layout element
+ layout_element = OxmlElement('w:tblLayout')
+ layout_element.set(qn('w:type'), 'autofit')
+
+ tbl_pr.append(layout_element)
+
+ # Set all column widths to auto
+ for col_index in range(len(table.columns)):
+ set_column_width(table, col_index, 0, "auto")
+
+ return True
+
+ except Exception as e:
+ print(f"Error setting auto-fit table: {e}")
+ return False
+
+
+def format_cell_text(cell, text_content=None, bold=None, italic=None, underline=None,
+ color=None, font_size=None, font_name=None):
+ """
+ Format text within a table cell.
+
+ Args:
+ cell: The table cell to format
+ text_content: Optional new text content for the cell
+ bold: Set text bold (True/False)
+ italic: Set text italic (True/False)
+ underline: Set text underlined (True/False)
+ color: Text color (hex string like "FF0000" or color name)
+ font_size: Font size in points
+ font_name: Font name/family
+
+ Returns:
+ True if successful, False otherwise
+ """
+ try:
+ # Set text content if provided
+ if text_content is not None:
+ cell.text = str(text_content)
+
+ # Apply formatting to all paragraphs and runs in the cell
+ for paragraph in cell.paragraphs:
+ for run in paragraph.runs:
+ if bold is not None:
+ run.bold = bold
+ if italic is not None:
+ run.italic = italic
+ if underline is not None:
+ run.underline = underline
+
+ if font_size is not None:
+ from docx.shared import Pt
+ run.font.size = Pt(font_size)
+
+ if font_name is not None:
+ run.font.name = font_name
+
+ if color is not None:
+ from docx.shared import RGBColor
+ # Define common RGB colors
+ color_map = {
+ 'red': RGBColor(255, 0, 0),
+ 'blue': RGBColor(0, 0, 255),
+ 'green': RGBColor(0, 128, 0),
+ 'yellow': RGBColor(255, 255, 0),
+ 'black': RGBColor(0, 0, 0),
+ 'gray': RGBColor(128, 128, 128),
+ 'grey': RGBColor(128, 128, 128),
+ 'white': RGBColor(255, 255, 255),
+ 'purple': RGBColor(128, 0, 128),
+ 'orange': RGBColor(255, 165, 0)
+ }
+
+ try:
+ if color.lower() in color_map:
+ # Use predefined RGB color
+ run.font.color.rgb = color_map[color.lower()]
+ elif color.startswith('#'):
+ # Hex color string
+ hex_color = color.lstrip('#')
+ if len(hex_color) == 6:
+ r = int(hex_color[0:2], 16)
+ g = int(hex_color[2:4], 16)
+ b = int(hex_color[4:6], 16)
+ run.font.color.rgb = RGBColor(r, g, b)
+ else:
+ # Try hex without #
+ if len(color) == 6:
+ r = int(color[0:2], 16)
+ g = int(color[2:4], 16)
+ b = int(color[4:6], 16)
+ run.font.color.rgb = RGBColor(r, g, b)
+ except Exception:
+ # If color parsing fails, default to black
+ run.font.color.rgb = RGBColor(0, 0, 0)
+
+ return True
+
+ except Exception as e:
+ print(f"Error formatting cell text: {e}")
+ return False
+
+
+def format_cell_text_by_position(table, row_index, col_index, text_content=None,
+ bold=None, italic=None, underline=None, color=None,
+ font_size=None, font_name=None):
+ """
+ Format text in a specific table cell by position.
+
+ Args:
+ table: The table containing the cell
+ row_index: Row index (0-based)
+ col_index: Column index (0-based)
+ text_content: Optional new text content for the cell
+ bold: Set text bold (True/False)
+ italic: Set text italic (True/False)
+ underline: Set text underlined (True/False)
+ color: Text color (hex string or color name)
+ font_size: Font size in points
+ font_name: Font name/family
+
+ Returns:
+ True if successful, False otherwise
+ """
+ try:
+ if (0 <= row_index < len(table.rows) and
+ 0 <= col_index < len(table.rows[row_index].cells)):
+ cell = table.rows[row_index].cells[col_index]
+ return format_cell_text(cell, text_content, bold, italic, underline,
+ color, font_size, font_name)
+ else:
+ return False
+ except Exception as e:
+ print(f"Error formatting cell text by position: {e}")
+ return False
+
+
+def set_cell_padding(cell, top=None, bottom=None, left=None, right=None, unit="dxa"):
+ """
+ Set padding/margins for a table cell.
+
+ Args:
+ cell: The table cell to format
+ top: Top padding value
+ bottom: Bottom padding value
+ left: Left padding value
+ right: Right padding value
+ unit: Unit type ("dxa" for twentieths of a point, "pct" for percentage)
+
+ Returns:
+ True if successful, False otherwise
+ """
+ try:
+ # Get or create table cell properties
+ tc_pr = cell._tc.get_or_add_tcPr()
+
+ # Remove existing margins
+ existing_margins = tc_pr.find(qn('w:tcMar'))
+ if existing_margins is not None:
+ tc_pr.remove(existing_margins)
+
+ # Create margins element if any padding is specified
+ if any(p is not None for p in [top, bottom, left, right]):
+ margins_element = OxmlElement('w:tcMar')
+
+ # Add individual margin elements
+ margin_sides = {
+ 'w:top': top,
+ 'w:bottom': bottom,
+ 'w:left': left,
+ 'w:right': right
+ }
+
+ for side, value in margin_sides.items():
+ if value is not None:
+ margin_el = OxmlElement(side)
+ if unit == "dxa":
+ # DXA units (twentieths of a point)
+ margin_el.set(qn('w:w'), str(int(value * 20)))
+ margin_el.set(qn('w:type'), 'dxa')
+ elif unit == "pct":
+ # Percentage
+ margin_el.set(qn('w:w'), str(int(value * 50)))
+ margin_el.set(qn('w:type'), 'pct')
+ else:
+ # Default to DXA
+ margin_el.set(qn('w:w'), str(int(value * 20)))
+ margin_el.set(qn('w:type'), 'dxa')
+
+ margins_element.append(margin_el)
+
+ tc_pr.append(margins_element)
+
+ return True
+
+ except Exception as e:
+ print(f"Error setting cell padding: {e}")
+ return False
+
+
+def set_cell_padding_by_position(table, row_index, col_index, top=None, bottom=None,
+ left=None, right=None, unit="dxa"):
+ """
+ Set padding for a specific table cell by position.
+
+ Args:
+ table: The table containing the cell
+ row_index: Row index (0-based)
+ col_index: Column index (0-based)
+ top: Top padding value
+ bottom: Bottom padding value
+ left: Left padding value
+ right: Right padding value
+ unit: Unit type ("dxa" for twentieths of a point, "pct" for percentage)
+
+ Returns:
+ True if successful, False otherwise
+ """
+ try:
+ if (0 <= row_index < len(table.rows) and
+ 0 <= col_index < len(table.rows[row_index].cells)):
+ cell = table.rows[row_index].cells[col_index]
+ return set_cell_padding(cell, top, bottom, left, right, unit)
+ else:
+ return False
+ except Exception as e:
+ print(f"Error setting cell padding by position: {e}")
+ return False
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/core/unprotect.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/core/unprotect.py
new file mode 100644
index 00000000..8daddef2
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/core/unprotect.py
@@ -0,0 +1,78 @@
+"""
+Unprotect document functionality for the Word Document Server.
+
+This module handles removing document protection.
+"""
+import os
+import json
+import hashlib
+import tempfile
+import shutil
+from typing import Tuple, Optional
+
+def remove_protection_info(filename: str, password: Optional[str] = None) -> Tuple[bool, str]:
+ """
+ Remove protection information from a document and decrypt it if necessary.
+
+ Args:
+ filename: Path to the Word document
+ password: Password to verify before removing protection
+
+ Returns:
+ Tuple of (success, message)
+ """
+ base_path, _ = os.path.splitext(filename)
+ metadata_path = f"{base_path}.protection"
+
+ # Check if protection metadata exists
+ if not os.path.exists(metadata_path):
+ return False, "Document is not protected"
+
+ try:
+ # Load protection data
+ with open(metadata_path, 'r') as f:
+ protection_data = json.load(f)
+
+ # Verify password if provided and required
+ if password and protection_data.get("password_hash"):
+ password_hash = hashlib.sha256(password.encode()).hexdigest()
+ if password_hash != protection_data.get("password_hash"):
+ return False, "Incorrect password"
+
+ # Handle true encryption if it was applied
+ if protection_data.get("true_encryption") and password:
+ try:
+ import msoffcrypto
+
+ # Create a temporary file for the decrypted output
+ temp_fd, temp_path = tempfile.mkstemp(suffix='.docx')
+ os.close(temp_fd)
+
+ # Open the encrypted document
+ with open(filename, 'rb') as f:
+ office_file = msoffcrypto.OfficeFile(f)
+
+ # Decrypt with provided password
+ try:
+ office_file.load_key(password=password)
+
+ # Write the decrypted file to the temp path
+ with open(temp_path, 'wb') as out_file:
+ office_file.decrypt(out_file)
+
+ # Replace encrypted file with decrypted version
+ shutil.move(temp_path, filename)
+ except Exception as decrypt_error:
+ if os.path.exists(temp_path):
+ os.unlink(temp_path)
+ return False, f"Failed to decrypt document: {str(decrypt_error)}"
+ except ImportError:
+ return False, "Missing msoffcrypto package required for encryption/decryption"
+ except Exception as e:
+ return False, f"Error decrypting document: {str(e)}"
+
+ # Remove the protection metadata file
+ os.remove(metadata_path)
+ return True, "Protection removed successfully"
+ except Exception as e:
+ return False, f"Error removing protection: {str(e)}"
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/main.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/main.py
new file mode 100644
index 00000000..a4274d73
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/main.py
@@ -0,0 +1,766 @@
+"""
+Main entry point for the Word Document MCP Server.
+Acts as the central controller for the MCP server that handles Word document operations.
+Supports multiple transports: stdio, sse, and streamable-http using standalone FastMCP.
+"""
+
+import os
+import sys
+import builtins
+from dotenv import load_dotenv
+
+# Redirect print to stderr to avoid polluting MCP stdio protocol
+_original_print = builtins.print
+def _stderr_print(*args, **kwargs):
+ kwargs.setdefault('file', sys.stderr)
+ _original_print(*args, **kwargs)
+builtins.print = _stderr_print
+
+# Load environment variables from .env file
+print("Loading configuration from .env file...")
+load_dotenv()
+# Set required environment variable for FastMCP 2.8.1+
+os.environ.setdefault('FASTMCP_LOG_LEVEL', 'INFO')
+from fastmcp import FastMCP
+from mcp.types import ToolAnnotations
+from word_document_server.tools import (
+ document_tools,
+ content_tools,
+ format_tools,
+ protection_tools,
+ footnote_tools,
+ extended_document_tools,
+ comment_tools
+)
+from word_document_server.tools.content_tools import replace_paragraph_block_below_header_tool
+from word_document_server.tools.content_tools import replace_block_between_manual_anchors_tool
+
+def get_transport_config():
+ """
+ Get transport configuration from environment variables.
+
+ Returns:
+ dict: Transport configuration with type, host, port, and other settings
+ """
+ # Default configuration
+ config = {
+ 'transport': 'stdio', # Default to stdio for backward compatibility
+ 'host': '0.0.0.0',
+ 'port': 8000,
+ 'path': '/mcp',
+ 'sse_path': '/sse'
+ }
+
+ # Override with environment variables if provided
+ transport = os.getenv('MCP_TRANSPORT', 'stdio').lower()
+ print(f"Transport: {transport}")
+ # Validate transport type
+ valid_transports = ['stdio', 'streamable-http', 'sse']
+ if transport not in valid_transports:
+ print(f"Warning: Invalid transport '{transport}'. Falling back to 'stdio'.")
+ transport = 'stdio'
+
+ config['transport'] = transport
+ config['host'] = os.getenv('MCP_HOST', config['host'])
+ # Use PORT from Render if available, otherwise fall back to MCP_PORT or default
+ config['port'] = int(os.getenv('PORT', os.getenv('MCP_PORT', config['port'])))
+ config['path'] = os.getenv('MCP_PATH', config['path'])
+ config['sse_path'] = os.getenv('MCP_SSE_PATH', config['sse_path'])
+
+ return config
+
+
+def setup_logging(debug_mode):
+ """
+ Setup logging based on debug mode.
+
+ Args:
+ debug_mode (bool): Whether to enable debug logging
+ """
+ import logging
+
+ if debug_mode:
+ logging.basicConfig(
+ level=logging.DEBUG,
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
+ )
+ print("Debug logging enabled")
+ else:
+ logging.basicConfig(
+ level=logging.INFO,
+ format='%(asctime)s - %(levelname)s - %(message)s'
+ )
+
+
+# Initialize FastMCP server
+mcp = FastMCP("Word Document Server")
+
+
+def register_tools():
+ """Register all tools with the MCP server using FastMCP decorators."""
+
+ # Document tools (create, copy, info, etc.)
+ @mcp.tool(
+ annotations=ToolAnnotations(
+ title="Create Word Document",
+ destructiveHint=True,
+ ),
+ )
+ def create_document(filename: str, title: str = None, author: str = None):
+ """Create a new Word document with optional metadata."""
+ return document_tools.create_document(filename, title, author)
+
+ @mcp.tool(
+ annotations=ToolAnnotations(
+ title="Copy Word Document",
+ destructiveHint=True,
+ ),
+ )
+ def copy_document(source_filename: str, destination_filename: str = None):
+ """Create a copy of a Word document."""
+ return document_tools.copy_document(source_filename, destination_filename)
+
+ @mcp.tool(
+ annotations=ToolAnnotations(
+ title="Get Document Info",
+ readOnlyHint=True,
+ ),
+ )
+ def get_document_info(filename: str):
+ """Get information about a Word document."""
+ return document_tools.get_document_info(filename)
+
+ @mcp.tool(
+ annotations=ToolAnnotations(
+ title="Get Document Text",
+ readOnlyHint=True,
+ ),
+ )
+ def get_document_text(filename: str):
+ """Extract all text from a Word document."""
+ return document_tools.get_document_text(filename)
+
+ @mcp.tool(
+ annotations=ToolAnnotations(
+ title="Get Document Outline",
+ readOnlyHint=True,
+ ),
+ )
+ def get_document_outline(filename: str):
+ """Get the structure of a Word document."""
+ return document_tools.get_document_outline(filename)
+
+ @mcp.tool(
+ annotations=ToolAnnotations(
+ title="List Available Documents",
+ readOnlyHint=True,
+ ),
+ )
+ def list_available_documents(directory: str = "."):
+ """List all .docx files in the specified directory."""
+ return document_tools.list_available_documents(directory)
+
+ @mcp.tool(
+ annotations=ToolAnnotations(
+ title="Get Document XML",
+ readOnlyHint=True,
+ ),
+ )
+ def get_document_xml(filename: str):
+ """Get the raw XML structure of a Word document."""
+ return document_tools.get_document_xml_tool(filename)
+
+ @mcp.tool(
+ annotations=ToolAnnotations(
+ title="Insert Header Near Text",
+ ),
+ )
+ def insert_header_near_text(filename: str, target_text: str = None, header_title: str = None, position: str = 'after', header_style: str = 'Heading 1', target_paragraph_index: int = None):
+ """Insert a header (with specified style) before or after the target paragraph. Specify by text or paragraph index. Args: filename (str), target_text (str, optional), header_title (str), position ('before' or 'after'), header_style (str, default 'Heading 1'), target_paragraph_index (int, optional)."""
+ return content_tools.insert_header_near_text_tool(filename, target_text, header_title, position, header_style, target_paragraph_index)
+
+ @mcp.tool(
+ annotations=ToolAnnotations(
+ title="Insert Line Near Text",
+ ),
+ )
+ def insert_line_or_paragraph_near_text(filename: str, target_text: str = None, line_text: str = None, position: str = 'after', line_style: str = None, target_paragraph_index: int = None):
+ """
+ Insert a new line or paragraph (with specified or matched style) before or after the target paragraph. Specify by text or paragraph index. Args: filename (str), target_text (str, optional), line_text (str), position ('before' or 'after'), line_style (str, optional), target_paragraph_index (int, optional).
+ """
+ return content_tools.insert_line_or_paragraph_near_text_tool(filename, target_text, line_text, position, line_style, target_paragraph_index)
+
+ @mcp.tool(
+ annotations=ToolAnnotations(
+ title="Insert List Near Text",
+ ),
+ )
+ def insert_numbered_list_near_text(filename: str, target_text: str = None, list_items: list[str] = None, position: str = 'after', target_paragraph_index: int = None, bullet_type: str = 'bullet'):
+ """Insert a bulleted or numbered list before or after the target paragraph. Specify by text or paragraph index. Args: filename (str), target_text (str, optional), list_items (list of str), position ('before' or 'after'), target_paragraph_index (int, optional), bullet_type ('bullet' for bullets or 'number' for numbered lists, default: 'bullet')."""
+ return content_tools.insert_numbered_list_near_text_tool(filename, target_text, list_items, position, target_paragraph_index, bullet_type)
+ # Content tools (paragraphs, headings, tables, etc.)
+ @mcp.tool(
+ annotations=ToolAnnotations(
+ title="Add Paragraph",
+ ),
+ )
+ def add_paragraph(filename: str, text: str, style: str = None,
+ font_name: str = None, font_size: int = None,
+ bold: bool = None, italic: bool = None, color: str = None):
+ """Add a paragraph to a Word document with optional formatting.
+
+ Args:
+ filename: Path to Word document
+ text: Paragraph text content
+ style: Optional paragraph style name
+ font_name: Font family (e.g., 'Helvetica', 'Times New Roman')
+ font_size: Font size in points (e.g., 14, 36)
+ bold: Make text bold
+ italic: Make text italic
+ color: Text color as hex RGB (e.g., '000000')
+ """
+ return content_tools.add_paragraph(filename, text, style, font_name, font_size, bold, italic, color)
+
+ @mcp.tool(
+ annotations=ToolAnnotations(
+ title="Add Heading",
+ ),
+ )
+ def add_heading(filename: str, text: str, level: int = 1,
+ font_name: str = None, font_size: int = None,
+ bold: bool = None, italic: bool = None, border_bottom: bool = False):
+ """Add a heading to a Word document with optional formatting.
+
+ Args:
+ filename: Path to Word document
+ text: Heading text
+ level: Heading level (1-9)
+ font_name: Font family (e.g., 'Helvetica')
+ font_size: Font size in points (e.g., 14)
+ bold: Make heading bold
+ italic: Make heading italic
+ border_bottom: Add bottom border (for section headers)
+ """
+ return content_tools.add_heading(filename, text, level, font_name, font_size, bold, italic, border_bottom)
+
+ @mcp.tool(
+ annotations=ToolAnnotations(
+ title="Add Picture",
+ ),
+ )
+ def add_picture(filename: str, image_path: str, width: float = None):
+ """Add an image to a Word document."""
+ return content_tools.add_picture(filename, image_path, width)
+
+ @mcp.tool(
+ annotations=ToolAnnotations(
+ title="Add Table",
+ ),
+ )
+ def add_table(filename: str, rows: int, cols: int, data: list[list[str]] = None):
+ """Add a table to a Word document."""
+ return content_tools.add_table(filename, rows, cols, data)
+
+ @mcp.tool(
+ annotations=ToolAnnotations(
+ title="Add Page Break",
+ ),
+ )
+ def add_page_break(filename: str):
+ """Add a page break to the document."""
+ return content_tools.add_page_break(filename)
+
+ @mcp.tool(
+ annotations=ToolAnnotations(
+ title="Delete Paragraph",
+ destructiveHint=True,
+ ),
+ )
+ def delete_paragraph(filename: str, paragraph_index: int):
+ """Delete a paragraph from a document."""
+ return content_tools.delete_paragraph(filename, paragraph_index)
+
+ @mcp.tool(
+ annotations=ToolAnnotations(
+ title="Search and Replace",
+ destructiveHint=True,
+ ),
+ )
+ def search_and_replace(filename: str, find_text: str, replace_text: str):
+ """Search for text and replace all occurrences."""
+ return content_tools.search_and_replace(filename, find_text, replace_text)
+
+ # Format tools (styling, text formatting, etc.)
+ @mcp.tool(
+ annotations=ToolAnnotations(
+ title="Create Custom Style",
+ ),
+ )
+ def create_custom_style(filename: str, style_name: str, bold: bool = None,
+ italic: bool = None, font_size: int = None,
+ font_name: str = None, color: str = None,
+ base_style: str = None):
+ """Create a custom style in the document."""
+ return format_tools.create_custom_style(
+ filename, style_name, bold, italic, font_size, font_name, color, base_style
+ )
+
+ @mcp.tool(
+ annotations=ToolAnnotations(
+ title="Format Text",
+ ),
+ )
+ def format_text(filename: str, paragraph_index: int, start_pos: int, end_pos: int,
+ bold: bool = None, italic: bool = None, underline: bool = None,
+ color: str = None, font_size: int = None, font_name: str = None):
+ """Format a specific range of text within a paragraph."""
+ return format_tools.format_text(
+ filename, paragraph_index, start_pos, end_pos, bold, italic,
+ underline, color, font_size, font_name
+ )
+
+ @mcp.tool(
+ annotations=ToolAnnotations(
+ title="Format Table",
+ ),
+ )
+ def format_table(filename: str, table_index: int, has_header_row: bool = None,
+ border_style: str = None, shading: list[str] = None):
+ """Format a table with borders, shading, and structure."""
+ return format_tools.format_table(filename, table_index, has_header_row, border_style, shading)
+
+ # New table cell shading tools
+ @mcp.tool(
+ annotations=ToolAnnotations(
+ title="Set Table Cell Shading",
+ ),
+ )
+ def set_table_cell_shading(filename: str, table_index: int, row_index: int,
+ col_index: int, fill_color: str, pattern: str = "clear"):
+ """Apply shading/filling to a specific table cell."""
+ return format_tools.set_table_cell_shading(filename, table_index, row_index, col_index, fill_color, pattern)
+
+ @mcp.tool(
+ annotations=ToolAnnotations(
+ title="Apply Alternating Row Colors",
+ ),
+ )
+ def apply_table_alternating_rows(filename: str, table_index: int,
+ color1: str = "FFFFFF", color2: str = "F2F2F2"):
+ """Apply alternating row colors to a table for better readability."""
+ return format_tools.apply_table_alternating_rows(filename, table_index, color1, color2)
+
+ @mcp.tool(
+ annotations=ToolAnnotations(
+ title="Highlight Table Header",
+ ),
+ )
+ def highlight_table_header(filename: str, table_index: int,
+ header_color: str = "4472C4", text_color: str = "FFFFFF"):
+ """Apply special highlighting to table header row."""
+ return format_tools.highlight_table_header(filename, table_index, header_color, text_color)
+
+ # Cell merging tools
+ @mcp.tool(
+ annotations=ToolAnnotations(
+ title="Merge Table Cells",
+ ),
+ )
+ def merge_table_cells(filename: str, table_index: int, start_row: int, start_col: int,
+ end_row: int, end_col: int):
+ """Merge cells in a rectangular area of a table."""
+ return format_tools.merge_table_cells(filename, table_index, start_row, start_col, end_row, end_col)
+
+ @mcp.tool(
+ annotations=ToolAnnotations(
+ title="Merge Cells Horizontally",
+ ),
+ )
+ def merge_table_cells_horizontal(filename: str, table_index: int, row_index: int,
+ start_col: int, end_col: int):
+ """Merge cells horizontally in a single row."""
+ return format_tools.merge_table_cells_horizontal(filename, table_index, row_index, start_col, end_col)
+
+ @mcp.tool(
+ annotations=ToolAnnotations(
+ title="Merge Cells Vertically",
+ ),
+ )
+ def merge_table_cells_vertical(filename: str, table_index: int, col_index: int,
+ start_row: int, end_row: int):
+ """Merge cells vertically in a single column."""
+ return format_tools.merge_table_cells_vertical(filename, table_index, col_index, start_row, end_row)
+
+ # Cell alignment tools
+ @mcp.tool(
+ annotations=ToolAnnotations(
+ title="Set Cell Alignment",
+ ),
+ )
+ def set_table_cell_alignment(filename: str, table_index: int, row_index: int, col_index: int,
+ horizontal: str = "left", vertical: str = "top"):
+ """Set text alignment for a specific table cell."""
+ return format_tools.set_table_cell_alignment(filename, table_index, row_index, col_index, horizontal, vertical)
+
+ @mcp.tool(
+ annotations=ToolAnnotations(
+ title="Set Table Alignment",
+ ),
+ )
+ def set_table_alignment_all(filename: str, table_index: int,
+ horizontal: str = "left", vertical: str = "top"):
+ """Set text alignment for all cells in a table."""
+ return format_tools.set_table_alignment_all(filename, table_index, horizontal, vertical)
+
+ # Protection tools
+ @mcp.tool(
+ annotations=ToolAnnotations(
+ title="Protect Document",
+ ),
+ )
+ def protect_document(filename: str, password: str):
+ """Add password protection to a Word document."""
+ return protection_tools.protect_document(filename, password)
+
+ @mcp.tool(
+ annotations=ToolAnnotations(
+ title="Unprotect Document",
+ ),
+ )
+ def unprotect_document(filename: str, password: str):
+ """Remove password protection from a Word document."""
+ return protection_tools.unprotect_document(filename, password)
+
+ # Footnote tools
+ @mcp.tool(
+ annotations=ToolAnnotations(
+ title="Add Footnote",
+ ),
+ )
+ def add_footnote_to_document(filename: str, paragraph_index: int, footnote_text: str):
+ """Add a footnote to a specific paragraph in a Word document."""
+ return footnote_tools.add_footnote_to_document(filename, paragraph_index, footnote_text)
+
+ @mcp.tool(
+ annotations=ToolAnnotations(
+ title="Add Footnote After Text",
+ ),
+ )
+ def add_footnote_after_text(filename: str, search_text: str, footnote_text: str,
+ output_filename: str = None):
+ """Add a footnote after specific text with proper superscript formatting.
+ This enhanced function ensures footnotes display correctly as superscript."""
+ return footnote_tools.add_footnote_after_text(filename, search_text, footnote_text, output_filename)
+
+ @mcp.tool(
+ annotations=ToolAnnotations(
+ title="Add Footnote Before Text",
+ ),
+ )
+ def add_footnote_before_text(filename: str, search_text: str, footnote_text: str,
+ output_filename: str = None):
+ """Add a footnote before specific text with proper superscript formatting.
+ This enhanced function ensures footnotes display correctly as superscript."""
+ return footnote_tools.add_footnote_before_text(filename, search_text, footnote_text, output_filename)
+
+ @mcp.tool(
+ annotations=ToolAnnotations(
+ title="Add Footnote Enhanced",
+ ),
+ )
+ def add_footnote_enhanced(filename: str, paragraph_index: int, footnote_text: str,
+ output_filename: str = None):
+ """Enhanced footnote addition with guaranteed superscript formatting.
+ Adds footnote at the end of a specific paragraph with proper style handling."""
+ return footnote_tools.add_footnote_enhanced(filename, paragraph_index, footnote_text, output_filename)
+
+ @mcp.tool(
+ annotations=ToolAnnotations(
+ title="Add Endnote",
+ ),
+ )
+ def add_endnote_to_document(filename: str, paragraph_index: int, endnote_text: str):
+ """Add an endnote to a specific paragraph in a Word document."""
+ return footnote_tools.add_endnote_to_document(filename, paragraph_index, endnote_text)
+
+ @mcp.tool(
+ annotations=ToolAnnotations(
+ title="Customize Footnote Style",
+ ),
+ )
+ def customize_footnote_style(filename: str, numbering_format: str = "1, 2, 3",
+ start_number: int = 1, font_name: str = None,
+ font_size: int = None):
+ """Customize footnote numbering and formatting in a Word document."""
+ return footnote_tools.customize_footnote_style(
+ filename, numbering_format, start_number, font_name, font_size
+ )
+
+ @mcp.tool(
+ annotations=ToolAnnotations(
+ title="Delete Footnote",
+ destructiveHint=True,
+ ),
+ )
+ def delete_footnote_from_document(filename: str, footnote_id: int = None,
+ search_text: str = None, output_filename: str = None):
+ """Delete a footnote from a Word document.
+ Identify the footnote either by ID (1, 2, 3, etc.) or by searching for text near it."""
+ return footnote_tools.delete_footnote_from_document(
+ filename, footnote_id, search_text, output_filename
+ )
+
+ # Robust footnote tools - Production-ready with comprehensive validation
+ @mcp.tool(
+ annotations=ToolAnnotations(
+ title="Add Footnote Robust",
+ ),
+ )
+ def add_footnote_robust(filename: str, search_text: str = None,
+ paragraph_index: int = None, footnote_text: str = "",
+ validate_location: bool = True, auto_repair: bool = False):
+ """Add footnote with robust validation and Word compliance.
+ This is the production-ready version with comprehensive error handling."""
+ return footnote_tools.add_footnote_robust_tool(
+ filename, search_text, paragraph_index, footnote_text,
+ validate_location, auto_repair
+ )
+
+ @mcp.tool(
+ annotations=ToolAnnotations(
+ title="Validate Footnotes",
+ readOnlyHint=True,
+ ),
+ )
+ def validate_document_footnotes(filename: str):
+ """Validate all footnotes in document for coherence and compliance.
+ Returns detailed report on ID conflicts, orphaned content, missing styles, etc."""
+ return footnote_tools.validate_footnotes_tool(filename)
+
+ @mcp.tool(
+ annotations=ToolAnnotations(
+ title="Delete Footnote Robust",
+ destructiveHint=True,
+ ),
+ )
+ def delete_footnote_robust(filename: str, footnote_id: int = None,
+ search_text: str = None, clean_orphans: bool = True):
+ """Delete footnote with comprehensive cleanup and orphan removal.
+ Ensures complete removal from document.xml, footnotes.xml, and relationships."""
+ return footnote_tools.delete_footnote_robust_tool(
+ filename, footnote_id, search_text, clean_orphans
+ )
+
+ # Extended document tools
+ @mcp.tool(
+ annotations=ToolAnnotations(
+ title="Get Paragraph Text",
+ readOnlyHint=True,
+ ),
+ )
+ def get_paragraph_text_from_document(filename: str, paragraph_index: int):
+ """Get text from a specific paragraph in a Word document."""
+ return extended_document_tools.get_paragraph_text_from_document(filename, paragraph_index)
+
+ @mcp.tool(
+ annotations=ToolAnnotations(
+ title="Find Text",
+ readOnlyHint=True,
+ ),
+ )
+ def find_text_in_document(filename: str, text_to_find: str, match_case: bool = True,
+ whole_word: bool = False):
+ """Find occurrences of specific text in a Word document."""
+ return extended_document_tools.find_text_in_document(
+ filename, text_to_find, match_case, whole_word
+ )
+
+ @mcp.tool(
+ annotations=ToolAnnotations(
+ title="Convert to PDF",
+ destructiveHint=True,
+ ),
+ )
+ def convert_to_pdf(filename: str, output_filename: str = None):
+ """Convert a Word document to PDF format."""
+ return extended_document_tools.convert_to_pdf(filename, output_filename)
+
+ @mcp.tool(
+ annotations=ToolAnnotations(
+ title="Replace Block Below Header",
+ ),
+ )
+ def replace_paragraph_block_below_header(filename: str, header_text: str, new_paragraphs: list[str], detect_block_end_fn: str = None):
+ """Reemplaza el bloque de párrafos debajo de un encabezado, evitando modificar TOC."""
+ return replace_paragraph_block_below_header_tool(filename, header_text, new_paragraphs, detect_block_end_fn)
+
+ @mcp.tool(
+ annotations=ToolAnnotations(
+ title="Replace Block Between Anchors",
+ ),
+ )
+ def replace_block_between_manual_anchors(filename: str, start_anchor_text: str, new_paragraphs: list[str], end_anchor_text: str = None, match_fn: str = None, new_paragraph_style: str = None):
+ """Replace all content between start_anchor_text and end_anchor_text (or next logical header if not provided)."""
+ return replace_block_between_manual_anchors_tool(filename, start_anchor_text, new_paragraphs, end_anchor_text, match_fn, new_paragraph_style)
+
+ # Comment tools
+ @mcp.tool(
+ annotations=ToolAnnotations(
+ title="Get All Comments",
+ readOnlyHint=True,
+ ),
+ )
+ def get_all_comments(filename: str):
+ """Extract all comments from a Word document."""
+ return comment_tools.get_all_comments(filename)
+
+ @mcp.tool(
+ annotations=ToolAnnotations(
+ title="Get Comments by Author",
+ readOnlyHint=True,
+ ),
+ )
+ def get_comments_by_author(filename: str, author: str):
+ """Extract comments from a specific author in a Word document."""
+ return comment_tools.get_comments_by_author(filename, author)
+
+ @mcp.tool(
+ annotations=ToolAnnotations(
+ title="Get Comments for Paragraph",
+ readOnlyHint=True,
+ ),
+ )
+ def get_comments_for_paragraph(filename: str, paragraph_index: int):
+ """Extract comments for a specific paragraph in a Word document."""
+ return comment_tools.get_comments_for_paragraph(filename, paragraph_index)
+ # New table column width tools
+ @mcp.tool(
+ annotations=ToolAnnotations(
+ title="Set Column Width",
+ ),
+ )
+ def set_table_column_width(filename: str, table_index: int, col_index: int,
+ width: float, width_type: str = "points"):
+ """Set the width of a specific table column."""
+ return format_tools.set_table_column_width(filename, table_index, col_index, width, width_type)
+
+ @mcp.tool(
+ annotations=ToolAnnotations(
+ title="Set Column Widths",
+ ),
+ )
+ def set_table_column_widths(filename: str, table_index: int, widths: list[float],
+ width_type: str = "points"):
+ """Set the widths of multiple table columns."""
+ return format_tools.set_table_column_widths(filename, table_index, widths, width_type)
+
+ @mcp.tool(
+ annotations=ToolAnnotations(
+ title="Set Table Width",
+ ),
+ )
+ def set_table_width(filename: str, table_index: int, width: float,
+ width_type: str = "points"):
+ """Set the overall width of a table."""
+ return format_tools.set_table_width(filename, table_index, width, width_type)
+
+ @mcp.tool(
+ annotations=ToolAnnotations(
+ title="Auto-Fit Table Columns",
+ ),
+ )
+ def auto_fit_table_columns(filename: str, table_index: int):
+ """Set table columns to auto-fit based on content."""
+ return format_tools.auto_fit_table_columns(filename, table_index)
+
+ # New table cell text formatting and padding tools
+ @mcp.tool(
+ annotations=ToolAnnotations(
+ title="Format Cell Text",
+ ),
+ )
+ def format_table_cell_text(filename: str, table_index: int, row_index: int, col_index: int,
+ text_content: str = None, bold: bool = None, italic: bool = None,
+ underline: bool = None, color: str = None, font_size: int = None,
+ font_name: str = None):
+ """Format text within a specific table cell."""
+ return format_tools.format_table_cell_text(filename, table_index, row_index, col_index,
+ text_content, bold, italic, underline, color, font_size, font_name)
+
+ @mcp.tool(
+ annotations=ToolAnnotations(
+ title="Set Cell Padding",
+ ),
+ )
+ def set_table_cell_padding(filename: str, table_index: int, row_index: int, col_index: int,
+ top: float = None, bottom: float = None, left: float = None,
+ right: float = None, unit: str = "points"):
+ """Set padding/margins for a specific table cell."""
+ return format_tools.set_table_cell_padding(filename, table_index, row_index, col_index,
+ top, bottom, left, right, unit)
+
+
+
+def run_server():
+ """Run the Word Document MCP Server with configurable transport."""
+ # Get transport configuration
+ config = get_transport_config()
+
+ # Setup logging
+ # setup_logging(config['debug'])
+
+ # Register all tools
+ register_tools()
+
+ # Print startup information
+ transport_type = config['transport']
+ print(f"Starting Word Document MCP Server with {transport_type} transport...")
+
+ # if config['debug']:
+ # print(f"Configuration: {config}")
+
+ try:
+ if transport_type == 'stdio':
+ # Run with stdio transport (default, backward compatible)
+ print("Server running on stdio transport")
+ mcp.run(transport='stdio')
+
+ elif transport_type == 'streamable-http':
+ # Run with streamable HTTP transport
+ print(f"Server running on streamable-http transport at http://{config['host']}:{config['port']}{config['path']}")
+ mcp.run(
+ transport='streamable-http',
+ host=config['host'],
+ port=config['port'],
+ path=config['path']
+ )
+
+ elif transport_type == 'sse':
+ # Run with SSE transport
+ print(f"Server running on SSE transport at http://{config['host']}:{config['port']}{config['sse_path']}")
+ mcp.run(
+ transport='sse',
+ host=config['host'],
+ port=config['port'],
+ path=config['sse_path']
+ )
+
+ except KeyboardInterrupt:
+ print("\nShutting down server...")
+ except Exception as e:
+ print(f"Error starting server: {e}")
+ if config['debug']:
+ import traceback
+ traceback.print_exc()
+ sys.exit(1)
+
+ return mcp
+
+
+def main():
+ """Main entry point for the server."""
+ run_server()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/tools/__init__.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/tools/__init__.py
new file mode 100644
index 00000000..e1832536
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/tools/__init__.py
@@ -0,0 +1,42 @@
+"""
+MCP tool implementations for the Word Document Server.
+
+This package contains the MCP tool implementations that expose functionality
+to clients through the Model Context Protocol.
+"""
+
+# Document tools
+from word_document_server.tools.document_tools import (
+ create_document, get_document_info, get_document_text,
+ get_document_outline, list_available_documents,
+ copy_document, merge_documents
+)
+
+# Content tools
+from word_document_server.tools.content_tools import (
+ add_heading, add_paragraph, add_table, add_picture,
+ add_page_break, add_table_of_contents, delete_paragraph,
+ search_and_replace
+)
+
+# Format tools
+from word_document_server.tools.format_tools import (
+ format_text, create_custom_style, format_table
+)
+
+# Protection tools
+from word_document_server.tools.protection_tools import (
+ protect_document, add_restricted_editing,
+ add_digital_signature, verify_document
+)
+
+# Footnote tools
+from word_document_server.tools.footnote_tools import (
+ add_footnote_to_document, add_endnote_to_document,
+ convert_footnotes_to_endnotes_in_document, customize_footnote_style
+)
+
+# Comment tools
+from word_document_server.tools.comment_tools import (
+ get_all_comments, get_comments_by_author, get_comments_for_paragraph
+)
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/tools/comment_tools.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/tools/comment_tools.py
new file mode 100644
index 00000000..ffe88122
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/tools/comment_tools.py
@@ -0,0 +1,168 @@
+"""
+Comment extraction tools for Word Document Server.
+
+These tools provide high-level interfaces for extracting and analyzing
+comments from Word documents through the MCP protocol.
+"""
+import os
+import json
+from typing import Dict, List, Optional, Any
+from docx import Document
+
+from word_document_server.utils.file_utils import ensure_docx_extension
+from word_document_server.core.comments import (
+ extract_all_comments,
+ filter_comments_by_author,
+ get_comments_for_paragraph
+)
+
+
+async def get_all_comments(filename: str) -> str:
+ """
+ Extract all comments from a Word document.
+
+ Args:
+ filename: Path to the Word document
+
+ Returns:
+ JSON string containing all comments with metadata
+ """
+ filename = ensure_docx_extension(filename)
+
+ if not os.path.exists(filename):
+ return json.dumps({
+ 'success': False,
+ 'error': f'Document {filename} does not exist'
+ }, indent=2)
+
+ try:
+ # Load the document
+ doc = Document(filename)
+
+ # Extract all comments
+ comments = extract_all_comments(doc)
+
+ # Return results
+ return json.dumps({
+ 'success': True,
+ 'comments': comments,
+ 'total_comments': len(comments)
+ }, indent=2)
+
+ except Exception as e:
+ return json.dumps({
+ 'success': False,
+ 'error': f'Failed to extract comments: {str(e)}'
+ }, indent=2)
+
+
+async def get_comments_by_author(filename: str, author: str) -> str:
+ """
+ Extract comments from a specific author in a Word document.
+
+ Args:
+ filename: Path to the Word document
+ author: Name of the comment author to filter by
+
+ Returns:
+ JSON string containing filtered comments
+ """
+ filename = ensure_docx_extension(filename)
+
+ if not os.path.exists(filename):
+ return json.dumps({
+ 'success': False,
+ 'error': f'Document {filename} does not exist'
+ }, indent=2)
+
+ if not author or not author.strip():
+ return json.dumps({
+ 'success': False,
+ 'error': 'Author name cannot be empty'
+ }, indent=2)
+
+ try:
+ # Load the document
+ doc = Document(filename)
+
+ # Extract all comments
+ all_comments = extract_all_comments(doc)
+
+ # Filter by author
+ author_comments = filter_comments_by_author(all_comments, author)
+
+ # Return results
+ return json.dumps({
+ 'success': True,
+ 'author': author,
+ 'comments': author_comments,
+ 'total_comments': len(author_comments)
+ }, indent=2)
+
+ except Exception as e:
+ return json.dumps({
+ 'success': False,
+ 'error': f'Failed to extract comments: {str(e)}'
+ }, indent=2)
+
+
+async def get_comments_for_paragraph(filename: str, paragraph_index: int) -> str:
+ """
+ Extract comments for a specific paragraph in a Word document.
+
+ Args:
+ filename: Path to the Word document
+ paragraph_index: Index of the paragraph (0-based)
+
+ Returns:
+ JSON string containing comments for the specified paragraph
+ """
+ filename = ensure_docx_extension(filename)
+
+ if not os.path.exists(filename):
+ return json.dumps({
+ 'success': False,
+ 'error': f'Document {filename} does not exist'
+ }, indent=2)
+
+ if paragraph_index < 0:
+ return json.dumps({
+ 'success': False,
+ 'error': 'Paragraph index must be non-negative'
+ }, indent=2)
+
+ try:
+ # Load the document
+ doc = Document(filename)
+
+ # Check if paragraph index is valid
+ if paragraph_index >= len(doc.paragraphs):
+ return json.dumps({
+ 'success': False,
+ 'error': f'Paragraph index {paragraph_index} is out of range. Document has {len(doc.paragraphs)} paragraphs.'
+ }, indent=2)
+
+ # Extract all comments
+ all_comments = extract_all_comments(doc)
+
+ # Filter for the specific paragraph
+ from word_document_server.core.comments import get_comments_for_paragraph as core_get_comments_for_paragraph
+ para_comments = core_get_comments_for_paragraph(all_comments, paragraph_index)
+
+ # Get the paragraph text for context
+ paragraph_text = doc.paragraphs[paragraph_index].text
+
+ # Return results
+ return json.dumps({
+ 'success': True,
+ 'paragraph_index': paragraph_index,
+ 'paragraph_text': paragraph_text,
+ 'comments': para_comments,
+ 'total_comments': len(para_comments)
+ }, indent=2)
+
+ except Exception as e:
+ return json.dumps({
+ 'success': False,
+ 'error': f'Failed to extract comments: {str(e)}'
+ }, indent=2)
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/tools/content_tools.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/tools/content_tools.py
new file mode 100644
index 00000000..d8250007
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/tools/content_tools.py
@@ -0,0 +1,481 @@
+"""
+Content tools for Word Document Server.
+
+These tools add various types of content to Word documents,
+including headings, paragraphs, tables, images, and page breaks.
+"""
+import os
+from typing import List, Optional, Dict, Any
+from docx import Document
+from docx.shared import Inches, Pt, RGBColor
+
+from word_document_server.utils.file_utils import check_file_writeable, ensure_docx_extension
+from word_document_server.utils.document_utils import find_and_replace_text, insert_header_near_text, insert_numbered_list_near_text, insert_line_or_paragraph_near_text, replace_paragraph_block_below_header, replace_block_between_manual_anchors
+from word_document_server.core.styles import ensure_heading_style, ensure_table_style
+
+
+async def add_heading(filename: str, text: str, level: int = 1,
+ font_name: Optional[str] = None, font_size: Optional[int] = None,
+ bold: Optional[bool] = None, italic: Optional[bool] = None,
+ border_bottom: bool = False) -> str:
+ """Add a heading to a Word document with optional formatting.
+
+ Args:
+ filename: Path to the Word document
+ text: Heading text
+ level: Heading level (1-9, where 1 is the highest level)
+ font_name: Font family (e.g., 'Helvetica')
+ font_size: Font size in points (e.g., 14)
+ bold: True/False for bold text
+ italic: True/False for italic text
+ border_bottom: True to add bottom border (for section headers)
+ """
+ filename = ensure_docx_extension(filename)
+
+ # Ensure level is converted to integer
+ try:
+ level = int(level)
+ except (ValueError, TypeError):
+ return "Invalid parameter: level must be an integer between 1 and 9"
+
+ # Validate level range
+ if level < 1 or level > 9:
+ return f"Invalid heading level: {level}. Level must be between 1 and 9."
+
+ if not os.path.exists(filename):
+ return f"Document {filename} does not exist"
+
+ # Check if file is writeable
+ is_writeable, error_message = check_file_writeable(filename)
+ if not is_writeable:
+ # Suggest creating a copy
+ return f"Cannot modify document: {error_message}. Consider creating a copy first or creating a new document."
+
+ try:
+ doc = Document(filename)
+
+ # Ensure heading styles exist
+ ensure_heading_style(doc)
+
+ # Try to add heading with style
+ try:
+ heading = doc.add_heading(text, level=level)
+ except Exception as style_error:
+ # If style-based approach fails, use direct formatting
+ heading = doc.add_paragraph(text)
+ heading.style = doc.styles['Normal']
+ if heading.runs:
+ run = heading.runs[0]
+ run.bold = True
+ # Adjust size based on heading level
+ if level == 1:
+ run.font.size = Pt(16)
+ elif level == 2:
+ run.font.size = Pt(14)
+ else:
+ run.font.size = Pt(12)
+
+ # Apply formatting to all runs in the heading
+ if any([font_name, font_size, bold is not None, italic is not None]):
+ for run in heading.runs:
+ if font_name:
+ run.font.name = font_name
+ if font_size:
+ run.font.size = Pt(font_size)
+ if bold is not None:
+ run.font.bold = bold
+ if italic is not None:
+ run.font.italic = italic
+
+ # Add bottom border if requested
+ if border_bottom:
+ from docx.oxml import OxmlElement
+ from docx.oxml.ns import qn
+
+ pPr = heading._element.get_or_add_pPr()
+ pBdr = OxmlElement('w:pBdr')
+
+ bottom = OxmlElement('w:bottom')
+ bottom.set(qn('w:val'), 'single')
+ bottom.set(qn('w:sz'), '4') # 0.5pt border
+ bottom.set(qn('w:space'), '0')
+ bottom.set(qn('w:color'), '000000')
+
+ pBdr.append(bottom)
+ pPr.append(pBdr)
+
+ doc.save(filename)
+ return f"Heading '{text}' (level {level}) added to {filename}"
+ except Exception as e:
+ return f"Failed to add heading: {str(e)}"
+
+
+async def add_paragraph(filename: str, text: str, style: Optional[str] = None,
+ font_name: Optional[str] = None, font_size: Optional[int] = None,
+ bold: Optional[bool] = None, italic: Optional[bool] = None,
+ color: Optional[str] = None) -> str:
+ """Add a paragraph to a Word document with optional formatting.
+
+ Args:
+ filename: Path to the Word document
+ text: Paragraph text
+ style: Optional paragraph style name
+ font_name: Font family (e.g., 'Helvetica', 'Times New Roman')
+ font_size: Font size in points (e.g., 14, 36)
+ bold: True/False for bold text
+ italic: True/False for italic text
+ color: RGB color as hex string (e.g., '000000' for black)
+ """
+ filename = ensure_docx_extension(filename)
+
+ if not os.path.exists(filename):
+ return f"Document {filename} does not exist"
+
+ # Check if file is writeable
+ is_writeable, error_message = check_file_writeable(filename)
+ if not is_writeable:
+ # Suggest creating a copy
+ return f"Cannot modify document: {error_message}. Consider creating a copy first or creating a new document."
+
+ try:
+ doc = Document(filename)
+ paragraph = doc.add_paragraph(text)
+
+ if style:
+ try:
+ paragraph.style = style
+ except KeyError:
+ # Style doesn't exist, use normal and report it
+ paragraph.style = doc.styles['Normal']
+ doc.save(filename)
+ return f"Style '{style}' not found, paragraph added with default style to {filename}"
+
+ # Apply formatting to all runs in the paragraph
+ if any([font_name, font_size, bold is not None, italic is not None, color]):
+ for run in paragraph.runs:
+ if font_name:
+ run.font.name = font_name
+ if font_size:
+ run.font.size = Pt(font_size)
+ if bold is not None:
+ run.font.bold = bold
+ if italic is not None:
+ run.font.italic = italic
+ if color:
+ # Remove any '#' prefix if present
+ color_hex = color.lstrip('#')
+ run.font.color.rgb = RGBColor.from_string(color_hex)
+
+ doc.save(filename)
+ return f"Paragraph added to {filename}"
+ except Exception as e:
+ return f"Failed to add paragraph: {str(e)}"
+
+
+async def add_table(filename: str, rows: int, cols: int, data: Optional[List[List[str]]] = None) -> str:
+ """Add a table to a Word document.
+
+ Args:
+ filename: Path to the Word document
+ rows: Number of rows in the table
+ cols: Number of columns in the table
+ data: Optional 2D array of data to fill the table
+ """
+ filename = ensure_docx_extension(filename)
+
+ if not os.path.exists(filename):
+ return f"Document {filename} does not exist"
+
+ # Check if file is writeable
+ is_writeable, error_message = check_file_writeable(filename)
+ if not is_writeable:
+ # Suggest creating a copy
+ return f"Cannot modify document: {error_message}. Consider creating a copy first or creating a new document."
+
+ try:
+ doc = Document(filename)
+ table = doc.add_table(rows=rows, cols=cols)
+
+ # Try to set the table style
+ try:
+ table.style = 'Table Grid'
+ except KeyError:
+ # If style doesn't exist, add basic borders
+ pass
+
+ # Fill table with data if provided
+ if data:
+ for i, row_data in enumerate(data):
+ if i >= rows:
+ break
+ for j, cell_text in enumerate(row_data):
+ if j >= cols:
+ break
+ table.cell(i, j).text = str(cell_text)
+
+ doc.save(filename)
+ return f"Table ({rows}x{cols}) added to {filename}"
+ except Exception as e:
+ return f"Failed to add table: {str(e)}"
+
+
+async def add_picture(filename: str, image_path: str, width: Optional[float] = None) -> str:
+ """Add an image to a Word document.
+
+ Args:
+ filename: Path to the Word document
+ image_path: Path to the image file
+ width: Optional width in inches (proportional scaling)
+ """
+ filename = ensure_docx_extension(filename)
+
+ # Validate document existence
+ if not os.path.exists(filename):
+ return f"Document {filename} does not exist"
+
+ # Get absolute paths for better diagnostics
+ abs_filename = os.path.abspath(filename)
+ abs_image_path = os.path.abspath(image_path)
+
+ # Validate image existence with improved error message
+ if not os.path.exists(abs_image_path):
+ return f"Image file not found: {abs_image_path}"
+
+ # Check image file size
+ try:
+ image_size = os.path.getsize(abs_image_path) / 1024 # Size in KB
+ if image_size <= 0:
+ return f"Image file appears to be empty: {abs_image_path} (0 KB)"
+ except Exception as size_error:
+ return f"Error checking image file: {str(size_error)}"
+
+ # Check if file is writeable
+ is_writeable, error_message = check_file_writeable(abs_filename)
+ if not is_writeable:
+ return f"Cannot modify document: {error_message}. Consider creating a copy first or creating a new document."
+
+ try:
+ doc = Document(abs_filename)
+ # Additional diagnostic info
+ diagnostic = f"Attempting to add image ({abs_image_path}, {image_size:.2f} KB) to document ({abs_filename})"
+
+ try:
+ if width:
+ doc.add_picture(abs_image_path, width=Inches(width))
+ else:
+ doc.add_picture(abs_image_path)
+ doc.save(abs_filename)
+ return f"Picture {image_path} added to {filename}"
+ except Exception as inner_error:
+ # More detailed error for the specific operation
+ error_type = type(inner_error).__name__
+ error_msg = str(inner_error)
+ return f"Failed to add picture: {error_type} - {error_msg or 'No error details available'}\nDiagnostic info: {diagnostic}"
+ except Exception as outer_error:
+ # Fallback error handling
+ error_type = type(outer_error).__name__
+ error_msg = str(outer_error)
+ return f"Document processing error: {error_type} - {error_msg or 'No error details available'}"
+
+
+async def add_page_break(filename: str) -> str:
+ """Add a page break to the document.
+
+ Args:
+ filename: Path to the Word document
+ """
+ filename = ensure_docx_extension(filename)
+
+ if not os.path.exists(filename):
+ return f"Document {filename} does not exist"
+
+ # Check if file is writeable
+ is_writeable, error_message = check_file_writeable(filename)
+ if not is_writeable:
+ return f"Cannot modify document: {error_message}. Consider creating a copy first."
+
+ try:
+ doc = Document(filename)
+ doc.add_page_break()
+ doc.save(filename)
+ return f"Page break added to {filename}."
+ except Exception as e:
+ return f"Failed to add page break: {str(e)}"
+
+
+async def add_table_of_contents(filename: str, title: str = "Table of Contents", max_level: int = 3) -> str:
+ """Add a table of contents to a Word document based on heading styles.
+
+ Args:
+ filename: Path to the Word document
+ title: Optional title for the table of contents
+ max_level: Maximum heading level to include (1-9)
+ """
+ filename = ensure_docx_extension(filename)
+
+ if not os.path.exists(filename):
+ return f"Document {filename} does not exist"
+
+ # Check if file is writeable
+ is_writeable, error_message = check_file_writeable(filename)
+ if not is_writeable:
+ return f"Cannot modify document: {error_message}. Consider creating a copy first."
+
+ try:
+ # Ensure max_level is within valid range
+ max_level = max(1, min(max_level, 9))
+
+ doc = Document(filename)
+
+ # Collect headings and their positions
+ headings = []
+ for i, paragraph in enumerate(doc.paragraphs):
+ # Check if paragraph style is a heading
+ if paragraph.style and paragraph.style.name.startswith('Heading '):
+ try:
+ # Extract heading level from style name
+ level = int(paragraph.style.name.split(' ')[1])
+ if level <= max_level:
+ headings.append({
+ 'level': level,
+ 'text': paragraph.text,
+ 'position': i
+ })
+ except (ValueError, IndexError):
+ # Skip if heading level can't be determined
+ pass
+
+ if not headings:
+ return f"No headings found in document {filename}. Table of contents not created."
+
+ # Create a new document with the TOC
+ toc_doc = Document()
+
+ # Add title
+ if title:
+ toc_doc.add_heading(title, level=1)
+
+ # Add TOC entries
+ for heading in headings:
+ # Indent based on level (using tab characters)
+ indent = ' ' * (heading['level'] - 1)
+ toc_doc.add_paragraph(f"{indent}{heading['text']}")
+
+ # Add page break
+ toc_doc.add_page_break()
+
+ # Get content from original document
+ for paragraph in doc.paragraphs:
+ p = toc_doc.add_paragraph(paragraph.text)
+ # Copy style if possible
+ try:
+ if paragraph.style:
+ p.style = paragraph.style.name
+ except:
+ pass
+
+ # Copy tables
+ for table in doc.tables:
+ # Create a new table with the same dimensions
+ new_table = toc_doc.add_table(rows=len(table.rows), cols=len(table.columns))
+ # Copy cell contents
+ for i, row in enumerate(table.rows):
+ for j, cell in enumerate(row.cells):
+ for paragraph in cell.paragraphs:
+ new_table.cell(i, j).text = paragraph.text
+
+ # Save the new document with TOC
+ toc_doc.save(filename)
+
+ return f"Table of contents with {len(headings)} entries added to {filename}"
+ except Exception as e:
+ return f"Failed to add table of contents: {str(e)}"
+
+
+async def delete_paragraph(filename: str, paragraph_index: int) -> str:
+ """Delete a paragraph from a document.
+
+ Args:
+ filename: Path to the Word document
+ paragraph_index: Index of the paragraph to delete (0-based)
+ """
+ filename = ensure_docx_extension(filename)
+
+ if not os.path.exists(filename):
+ return f"Document {filename} does not exist"
+
+ # Check if file is writeable
+ is_writeable, error_message = check_file_writeable(filename)
+ if not is_writeable:
+ return f"Cannot modify document: {error_message}. Consider creating a copy first."
+
+ try:
+ doc = Document(filename)
+
+ # Validate paragraph index
+ if paragraph_index < 0 or paragraph_index >= len(doc.paragraphs):
+ return f"Invalid paragraph index. Document has {len(doc.paragraphs)} paragraphs (0-{len(doc.paragraphs)-1})."
+
+ # Delete the paragraph (by removing its content and setting it empty)
+ # Note: python-docx doesn't support true paragraph deletion, this is a workaround
+ paragraph = doc.paragraphs[paragraph_index]
+ p = paragraph._p
+ p.getparent().remove(p)
+
+ doc.save(filename)
+ return f"Paragraph at index {paragraph_index} deleted successfully."
+ except Exception as e:
+ return f"Failed to delete paragraph: {str(e)}"
+
+
+async def search_and_replace(filename: str, find_text: str, replace_text: str) -> str:
+ """Search for text and replace all occurrences.
+
+ Args:
+ filename: Path to the Word document
+ find_text: Text to search for
+ replace_text: Text to replace with
+ """
+ filename = ensure_docx_extension(filename)
+
+ if not os.path.exists(filename):
+ return f"Document {filename} does not exist"
+
+ # Check if file is writeable
+ is_writeable, error_message = check_file_writeable(filename)
+ if not is_writeable:
+ return f"Cannot modify document: {error_message}. Consider creating a copy first."
+
+ try:
+ doc = Document(filename)
+
+ # Perform find and replace
+ count = find_and_replace_text(doc, find_text, replace_text)
+
+ if count > 0:
+ doc.save(filename)
+ return f"Replaced {count} occurrence(s) of '{find_text}' with '{replace_text}'."
+ else:
+ return f"No occurrences of '{find_text}' found."
+ except Exception as e:
+ return f"Failed to search and replace: {str(e)}"
+
+async def insert_header_near_text_tool(filename: str, target_text: str = None, header_title: str = "", position: str = 'after', header_style: str = 'Heading 1', target_paragraph_index: int = None) -> str:
+ """Insert a header (with specified style) before or after the target paragraph. Specify by text or paragraph index."""
+ return insert_header_near_text(filename, target_text, header_title, position, header_style, target_paragraph_index)
+
+async def insert_numbered_list_near_text_tool(filename: str, target_text: str = None, list_items: list = None, position: str = 'after', target_paragraph_index: int = None, bullet_type: str = 'bullet') -> str:
+ """Insert a bulleted or numbered list before or after the target paragraph. Specify by text or paragraph index."""
+ return insert_numbered_list_near_text(filename, target_text, list_items, position, target_paragraph_index, bullet_type)
+
+async def insert_line_or_paragraph_near_text_tool(filename: str, target_text: str = None, line_text: str = "", position: str = 'after', line_style: str = None, target_paragraph_index: int = None) -> str:
+ """Insert a new line or paragraph (with specified or matched style) before or after the target paragraph. Specify by text or paragraph index."""
+ return insert_line_or_paragraph_near_text(filename, target_text, line_text, position, line_style, target_paragraph_index)
+
+async def replace_paragraph_block_below_header_tool(filename: str, header_text: str, new_paragraphs: list, detect_block_end_fn=None) -> str:
+ """Reemplaza el bloque de párrafos debajo de un encabezado, evitando modificar TOC."""
+ return replace_paragraph_block_below_header(filename, header_text, new_paragraphs, detect_block_end_fn)
+
+async def replace_block_between_manual_anchors_tool(filename: str, start_anchor_text: str, new_paragraphs: list, end_anchor_text: str = None, match_fn=None, new_paragraph_style: str = None) -> str:
+ """Replace all content between start_anchor_text and end_anchor_text (or next logical header if not provided)."""
+ return replace_block_between_manual_anchors(filename, start_anchor_text, new_paragraphs, end_anchor_text, match_fn, new_paragraph_style)
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/tools/document_tools.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/tools/document_tools.py
new file mode 100644
index 00000000..c15ad38d
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/tools/document_tools.py
@@ -0,0 +1,214 @@
+"""
+Document creation and manipulation tools for Word Document Server.
+"""
+import os
+import json
+from typing import Dict, List, Optional, Any
+from docx import Document
+
+from word_document_server.utils.file_utils import check_file_writeable, ensure_docx_extension, create_document_copy
+from word_document_server.utils.document_utils import get_document_properties, extract_document_text, get_document_structure, get_document_xml, insert_header_near_text, insert_line_or_paragraph_near_text
+from word_document_server.core.styles import ensure_heading_style, ensure_table_style
+
+
+async def create_document(filename: str, title: Optional[str] = None, author: Optional[str] = None) -> str:
+ """Create a new Word document with optional metadata.
+
+ Args:
+ filename: Name of the document to create (with or without .docx extension)
+ title: Optional title for the document metadata
+ author: Optional author for the document metadata
+ """
+ filename = ensure_docx_extension(filename)
+
+ # Check if file is writeable
+ is_writeable, error_message = check_file_writeable(filename)
+ if not is_writeable:
+ return f"Cannot create document: {error_message}"
+
+ try:
+ doc = Document()
+
+ # Set properties if provided
+ if title:
+ doc.core_properties.title = title
+ if author:
+ doc.core_properties.author = author
+
+ # Ensure necessary styles exist
+ ensure_heading_style(doc)
+ ensure_table_style(doc)
+
+ # Save the document
+ doc.save(filename)
+
+ return f"Document {filename} created successfully"
+ except Exception as e:
+ return f"Failed to create document: {str(e)}"
+
+
+async def get_document_info(filename: str) -> str:
+ """Get information about a Word document.
+
+ Args:
+ filename: Path to the Word document
+ """
+ filename = ensure_docx_extension(filename)
+
+ if not os.path.exists(filename):
+ return f"Document {filename} does not exist"
+
+ try:
+ properties = get_document_properties(filename)
+ return json.dumps(properties, indent=2)
+ except Exception as e:
+ return f"Failed to get document info: {str(e)}"
+
+
+async def get_document_text(filename: str) -> str:
+ """Extract all text from a Word document.
+
+ Args:
+ filename: Path to the Word document
+ """
+ filename = ensure_docx_extension(filename)
+
+ return extract_document_text(filename)
+
+
+async def get_document_outline(filename: str) -> str:
+ """Get the structure of a Word document.
+
+ Args:
+ filename: Path to the Word document
+ """
+ filename = ensure_docx_extension(filename)
+
+ structure = get_document_structure(filename)
+ return json.dumps(structure, indent=2)
+
+
+async def list_available_documents(directory: str = ".") -> str:
+ """List all .docx files in the specified directory.
+
+ Args:
+ directory: Directory to search for Word documents
+ """
+ try:
+ if not os.path.exists(directory):
+ return f"Directory {directory} does not exist"
+
+ docx_files = [f for f in os.listdir(directory) if f.endswith('.docx')]
+
+ if not docx_files:
+ return f"No Word documents found in {directory}"
+
+ result = f"Found {len(docx_files)} Word documents in {directory}:\n"
+ for file in docx_files:
+ file_path = os.path.join(directory, file)
+ size = os.path.getsize(file_path) / 1024 # KB
+ result += f"- {file} ({size:.2f} KB)\n"
+
+ return result
+ except Exception as e:
+ return f"Failed to list documents: {str(e)}"
+
+
+async def copy_document(source_filename: str, destination_filename: Optional[str] = None) -> str:
+ """Create a copy of a Word document.
+
+ Args:
+ source_filename: Path to the source document
+ destination_filename: Optional path for the copy. If not provided, a default name will be generated.
+ """
+ source_filename = ensure_docx_extension(source_filename)
+
+ if destination_filename:
+ destination_filename = ensure_docx_extension(destination_filename)
+
+ success, message, new_path = create_document_copy(source_filename, destination_filename)
+ if success:
+ return message
+ else:
+ return f"Failed to copy document: {message}"
+
+
+async def merge_documents(target_filename: str, source_filenames: List[str], add_page_breaks: bool = True) -> str:
+ """Merge multiple Word documents into a single document.
+
+ Args:
+ target_filename: Path to the target document (will be created or overwritten)
+ source_filenames: List of paths to source documents to merge
+ add_page_breaks: If True, add page breaks between documents
+ """
+ from word_document_server.core.tables import copy_table
+
+ target_filename = ensure_docx_extension(target_filename)
+
+ # Check if target file is writeable
+ is_writeable, error_message = check_file_writeable(target_filename)
+ if not is_writeable:
+ return f"Cannot create target document: {error_message}"
+
+ # Validate all source documents exist
+ missing_files = []
+ for filename in source_filenames:
+ doc_filename = ensure_docx_extension(filename)
+ if not os.path.exists(doc_filename):
+ missing_files.append(doc_filename)
+
+ if missing_files:
+ return f"Cannot merge documents. The following source files do not exist: {', '.join(missing_files)}"
+
+ try:
+ # Create a new document for the merged result
+ target_doc = Document()
+
+ # Process each source document
+ for i, filename in enumerate(source_filenames):
+ doc_filename = ensure_docx_extension(filename)
+ source_doc = Document(doc_filename)
+
+ # Add page break between documents (except before the first one)
+ if add_page_breaks and i > 0:
+ target_doc.add_page_break()
+
+ # Copy all paragraphs
+ for paragraph in source_doc.paragraphs:
+ # Create a new paragraph with the same text and style
+ new_paragraph = target_doc.add_paragraph(paragraph.text)
+ new_paragraph.style = target_doc.styles['Normal'] # Default style
+
+ # Try to match the style if possible
+ try:
+ if paragraph.style and paragraph.style.name in target_doc.styles:
+ new_paragraph.style = target_doc.styles[paragraph.style.name]
+ except:
+ pass
+
+ # Copy run formatting
+ for i, run in enumerate(paragraph.runs):
+ if i < len(new_paragraph.runs):
+ new_run = new_paragraph.runs[i]
+ # Copy basic formatting
+ new_run.bold = run.bold
+ new_run.italic = run.italic
+ new_run.underline = run.underline
+ # Font size if specified
+ if run.font.size:
+ new_run.font.size = run.font.size
+
+ # Copy all tables
+ for table in source_doc.tables:
+ copy_table(table, target_doc)
+
+ # Save the merged document
+ target_doc.save(target_filename)
+ return f"Successfully merged {len(source_filenames)} documents into {target_filename}"
+ except Exception as e:
+ return f"Failed to merge documents: {str(e)}"
+
+
+async def get_document_xml_tool(filename: str) -> str:
+ """Get the raw XML structure of a Word document."""
+ return get_document_xml(filename)
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/tools/extended_document_tools.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/tools/extended_document_tools.py
new file mode 100644
index 00000000..8e51b230
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/tools/extended_document_tools.py
@@ -0,0 +1,184 @@
+"""
+Extended document tools for Word Document Server.
+
+These tools provide enhanced document content extraction and search capabilities.
+"""
+import os
+import json
+import subprocess
+import platform
+import shutil
+from typing import Dict, List, Optional, Any, Union, Tuple
+from docx import Document
+
+from word_document_server.utils.file_utils import check_file_writeable, ensure_docx_extension
+from word_document_server.utils.extended_document_utils import get_paragraph_text, find_text
+
+
+async def get_paragraph_text_from_document(filename: str, paragraph_index: int) -> str:
+ """Get text from a specific paragraph in a Word document.
+
+ Args:
+ filename: Path to the Word document
+ paragraph_index: Index of the paragraph to retrieve (0-based)
+ """
+ filename = ensure_docx_extension(filename)
+
+ if not os.path.exists(filename):
+ return f"Document {filename} does not exist"
+
+
+ if paragraph_index < 0:
+ return "Invalid parameter: paragraph_index must be a non-negative integer"
+
+ try:
+ result = get_paragraph_text(filename, paragraph_index)
+ return json.dumps(result, indent=2)
+ except Exception as e:
+ return f"Failed to get paragraph text: {str(e)}"
+
+
+async def find_text_in_document(filename: str, text_to_find: str, match_case: bool = True, whole_word: bool = False) -> str:
+ """Find occurrences of specific text in a Word document.
+
+ Args:
+ filename: Path to the Word document
+ text_to_find: Text to search for in the document
+ match_case: Whether to match case (True) or ignore case (False)
+ whole_word: Whether to match whole words only (True) or substrings (False)
+ """
+ filename = ensure_docx_extension(filename)
+
+ if not os.path.exists(filename):
+ return f"Document {filename} does not exist"
+
+ if not text_to_find:
+ return "Search text cannot be empty"
+
+ try:
+
+ result = find_text(filename, text_to_find, match_case, whole_word)
+ return json.dumps(result, indent=2)
+ except Exception as e:
+ return f"Failed to search for text: {str(e)}"
+
+
+async def convert_to_pdf(filename: str, output_filename: Optional[str] = None) -> str:
+ """Convert a Word document to PDF format.
+
+ Args:
+ filename: Path to the Word document
+ output_filename: Optional path for the output PDF. If not provided,
+ will use the same name with .pdf extension
+ """
+ filename = ensure_docx_extension(filename)
+
+ if not os.path.exists(filename):
+ return f"Document {filename} does not exist"
+
+ # Generate output filename if not provided
+ if not output_filename:
+ base_name, _ = os.path.splitext(filename)
+ output_filename = f"{base_name}.pdf"
+ elif not output_filename.lower().endswith('.pdf'):
+ output_filename = f"{output_filename}.pdf"
+
+ # Convert to absolute path if not already
+ if not os.path.isabs(output_filename):
+ output_filename = os.path.abspath(output_filename)
+
+ # Ensure the output directory exists
+ output_dir = os.path.dirname(output_filename)
+ if not output_dir:
+ output_dir = os.path.abspath('.')
+
+ # Create the directory if it doesn't exist
+ os.makedirs(output_dir, exist_ok=True)
+
+ # Check if output file can be written
+ is_writeable, error_message = check_file_writeable(output_filename)
+ if not is_writeable:
+ return f"Cannot create PDF: {error_message} (Path: {output_filename}, Dir: {output_dir})"
+
+ try:
+ # Determine platform for appropriate conversion method
+ system = platform.system()
+
+ if system == "Windows":
+ # On Windows, try docx2pdf which uses Microsoft Word
+ try:
+ from docx2pdf import convert
+ convert(filename, output_filename)
+ return f"Document successfully converted to PDF: {output_filename}"
+ except (ImportError, Exception) as e:
+ return f"Failed to convert document to PDF: {str(e)}\nNote: docx2pdf requires Microsoft Word to be installed."
+
+ elif system in ["Linux", "Darwin"]: # Linux or macOS
+ errors = []
+
+ # --- Attempt 1: LibreOffice ---
+ lo_commands = []
+ if system == "Darwin": # macOS
+ lo_commands = ["soffice", "/Applications/LibreOffice.app/Contents/MacOS/soffice"]
+ else: # Linux
+ lo_commands = ["libreoffice", "soffice"]
+
+ for cmd_name in lo_commands:
+ try:
+ output_dir_for_lo = os.path.dirname(output_filename) or '.'
+ os.makedirs(output_dir_for_lo, exist_ok=True)
+
+ cmd = [cmd_name, '--headless', '--convert-to', 'pdf', '--outdir', output_dir_for_lo, filename]
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=60, check=False)
+
+ if result.returncode == 0:
+ # LibreOffice typically creates a PDF with the same base name as the source file.
+ # e.g., 'mydoc.docx' -> 'mydoc.pdf'
+ base_name = os.path.splitext(os.path.basename(filename))[0]
+ created_pdf_name = f"{base_name}.pdf"
+ created_pdf_path = os.path.join(output_dir_for_lo, created_pdf_name)
+
+ # If the created file exists, move it to the desired output_filename if necessary.
+ if os.path.exists(created_pdf_path):
+ if created_pdf_path != output_filename:
+ shutil.move(created_pdf_path, output_filename)
+
+ # Final check: does the target file now exist?
+ if os.path.exists(output_filename):
+ return f"Document successfully converted to PDF via {cmd_name}: {output_filename}"
+
+ # If we get here, soffice returned 0 but the expected file wasn't created.
+ errors.append(f"{cmd_name} returned success code, but output file '{created_pdf_path}' was not found.")
+ # Continue to the next command or fallback.
+ else:
+ errors.append(f"{cmd_name} failed. Stderr: {result.stderr.strip()}")
+ except FileNotFoundError:
+ errors.append(f"Command '{cmd_name}' not found.")
+ except (subprocess.SubprocessError, Exception) as e:
+ errors.append(f"An error occurred with {cmd_name}: {str(e)}")
+
+ # --- Attempt 2: docx2pdf (Fallback) ---
+ try:
+ from docx2pdf import convert
+ convert(filename, output_filename)
+ if os.path.exists(output_filename) and os.path.getsize(output_filename) > 0:
+ return f"Document successfully converted to PDF via docx2pdf: {output_filename}"
+ else:
+ errors.append("docx2pdf fallback was executed but failed to create a valid output file.")
+ except ImportError:
+ errors.append("docx2pdf is not installed, skipping fallback.")
+ except Exception as e:
+ errors.append(f"docx2pdf fallback failed with an exception: {str(e)}")
+
+ # --- If all attempts failed ---
+ error_summary = "Failed to convert document to PDF using all available methods.\n"
+ error_summary += "Recorded errors: " + "; ".join(errors) + "\n"
+ error_summary += "To convert documents to PDF, please install either:\n"
+ error_summary += "1. LibreOffice (recommended for Linux/macOS)\n"
+ error_summary += "2. Microsoft Word (required for docx2pdf on Windows/macOS)"
+ return error_summary
+ else:
+ return f"PDF conversion not supported on {system} platform"
+
+ except Exception as e:
+ return f"Failed to convert document to PDF: {str(e)}"
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/tools/footnote_tools.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/tools/footnote_tools.py
new file mode 100644
index 00000000..72b0190d
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/tools/footnote_tools.py
@@ -0,0 +1,709 @@
+"""
+Footnote and endnote tools for Word Document Server.
+
+These tools handle footnote and endnote functionality,
+including adding, customizing, and converting between them.
+
+This module combines both standard and robust implementations:
+- String-return functions for backward compatibility
+- Dict-return robust functions for structured responses
+"""
+import os
+from typing import Optional, Dict, Any
+from docx import Document
+from docx.shared import Pt
+from docx.enum.style import WD_STYLE_TYPE
+
+from word_document_server.utils.file_utils import check_file_writeable, ensure_docx_extension
+from word_document_server.core.footnotes import (
+ find_footnote_references,
+ get_format_symbols,
+ customize_footnote_formatting,
+ add_footnote_robust,
+ delete_footnote_robust,
+ validate_document_footnotes,
+ add_footnote_at_paragraph_end # Compatibility function
+)
+
+
+async def add_footnote_to_document(filename: str, paragraph_index: int, footnote_text: str) -> str:
+ """Add a footnote to a specific paragraph in a Word document.
+
+ Args:
+ filename: Path to the Word document
+ paragraph_index: Index of the paragraph to add footnote to (0-based)
+ footnote_text: Text content of the footnote
+ """
+ filename = ensure_docx_extension(filename)
+
+ # Ensure paragraph_index is an integer
+ try:
+ paragraph_index = int(paragraph_index)
+ except (ValueError, TypeError):
+ return "Invalid parameter: paragraph_index must be an integer"
+
+ if not os.path.exists(filename):
+ return f"Document {filename} does not exist"
+
+ # Check if file is writeable
+ is_writeable, error_message = check_file_writeable(filename)
+ if not is_writeable:
+ return f"Cannot modify document: {error_message}. Consider creating a copy first."
+
+ try:
+ doc = Document(filename)
+
+ # Validate paragraph index
+ if paragraph_index < 0 or paragraph_index >= len(doc.paragraphs):
+ return f"Invalid paragraph index. Document has {len(doc.paragraphs)} paragraphs (0-{len(doc.paragraphs)-1})."
+
+ paragraph = doc.paragraphs[paragraph_index]
+
+ # In python-docx, we'd use paragraph.add_footnote(), but we'll use a more robust approach
+ try:
+ footnote = paragraph.add_run()
+ footnote.text = ""
+
+ # Create the footnote reference
+ reference = footnote.add_footnote(footnote_text)
+
+ doc.save(filename)
+ return f"Footnote added to paragraph {paragraph_index} in {filename}"
+ except AttributeError:
+ # Fall back to a simpler approach if direct footnote addition fails
+ last_run = paragraph.add_run()
+ last_run.text = "¹" # Unicode superscript 1
+ last_run.font.superscript = True
+
+ # Add a footnote section at the end if it doesn't exist
+ found_footnote_section = False
+ for p in doc.paragraphs:
+ if p.text.startswith("Footnotes:"):
+ found_footnote_section = True
+ break
+
+ if not found_footnote_section:
+ doc.add_paragraph("\n").add_run()
+ doc.add_paragraph("Footnotes:").bold = True
+
+ # Add footnote text
+ footnote_para = doc.add_paragraph("¹ " + footnote_text)
+ footnote_para.style = "Footnote Text" if "Footnote Text" in doc.styles else "Normal"
+
+ doc.save(filename)
+ return f"Footnote added to paragraph {paragraph_index} in {filename} (simplified approach)"
+ except Exception as e:
+ return f"Failed to add footnote: {str(e)}"
+
+
+async def add_endnote_to_document(filename: str, paragraph_index: int, endnote_text: str) -> str:
+ """Add an endnote to a specific paragraph in a Word document.
+
+ Args:
+ filename: Path to the Word document
+ paragraph_index: Index of the paragraph to add endnote to (0-based)
+ endnote_text: Text content of the endnote
+ """
+ filename = ensure_docx_extension(filename)
+
+ # Ensure paragraph_index is an integer
+ try:
+ paragraph_index = int(paragraph_index)
+ except (ValueError, TypeError):
+ return "Invalid parameter: paragraph_index must be an integer"
+
+ if not os.path.exists(filename):
+ return f"Document {filename} does not exist"
+
+ # Check if file is writeable
+ is_writeable, error_message = check_file_writeable(filename)
+ if not is_writeable:
+ return f"Cannot modify document: {error_message}. Consider creating a copy first."
+
+ try:
+ doc = Document(filename)
+
+ # Validate paragraph index
+ if paragraph_index < 0 or paragraph_index >= len(doc.paragraphs):
+ return f"Invalid paragraph index. Document has {len(doc.paragraphs)} paragraphs (0-{len(doc.paragraphs)-1})."
+
+ paragraph = doc.paragraphs[paragraph_index]
+
+ # Add endnote reference
+ last_run = paragraph.add_run()
+ last_run.text = "†" # Unicode dagger symbol common for endnotes
+ last_run.font.superscript = True
+
+ # Check if endnotes section exists, if not create it
+ endnotes_heading_found = False
+ for para in doc.paragraphs:
+ if para.text == "Endnotes:" or para.text == "ENDNOTES":
+ endnotes_heading_found = True
+ break
+
+ if not endnotes_heading_found:
+ # Add a page break before endnotes section
+ doc.add_page_break()
+ doc.add_heading("Endnotes:", level=1)
+
+ # Add the endnote text
+ endnote_para = doc.add_paragraph("† " + endnote_text)
+ endnote_para.style = "Endnote Text" if "Endnote Text" in doc.styles else "Normal"
+
+ doc.save(filename)
+ return f"Endnote added to paragraph {paragraph_index} in {filename}"
+ except Exception as e:
+ return f"Failed to add endnote: {str(e)}"
+
+
+async def convert_footnotes_to_endnotes_in_document(filename: str) -> str:
+ """Convert all footnotes to endnotes in a Word document.
+
+ Args:
+ filename: Path to the Word document
+ """
+ filename = ensure_docx_extension(filename)
+
+ if not os.path.exists(filename):
+ return f"Document {filename} does not exist"
+
+ # Check if file is writeable
+ is_writeable, error_message = check_file_writeable(filename)
+ if not is_writeable:
+ return f"Cannot modify document: {error_message}. Consider creating a copy first."
+
+ try:
+ doc = Document(filename)
+
+
+ # Find all runs that might be footnote references
+ footnote_references = []
+
+ for para_idx, para in enumerate(doc.paragraphs):
+ for run_idx, run in enumerate(para.runs):
+ # Check if this run is likely a footnote reference
+ # (superscript number or special character)
+ if run.font.superscript and (run.text.isdigit() or run.text in "¹²³⁴⁵⁶⁷⁸⁹"):
+ footnote_references.append({
+ "paragraph_index": para_idx,
+ "run_index": run_idx,
+ "text": run.text
+ })
+
+ if not footnote_references:
+ return f"No footnote references found in {filename}"
+
+ # Create endnotes section
+ doc.add_page_break()
+ doc.add_heading("Endnotes:", level=1)
+
+ # Create a placeholder for endnote content, we'll fill it later
+ endnote_content = []
+
+ # Find the footnote text at the bottom of the page
+
+ found_footnote_section = False
+ footnote_text = []
+
+ for para in doc.paragraphs:
+ if not found_footnote_section and para.text.startswith("Footnotes:"):
+ found_footnote_section = True
+ continue
+
+ if found_footnote_section:
+ footnote_text.append(para.text)
+
+ # Create endnotes based on footnote references
+ for i, ref in enumerate(footnote_references):
+ # Add a new endnote
+ endnote_para = doc.add_paragraph()
+
+ # Try to match with footnote text, or use placeholder
+ if i < len(footnote_text):
+ endnote_para.text = f"†{i+1} {footnote_text[i]}"
+ else:
+ endnote_para.text = f"†{i+1} Converted from footnote {ref['text']}"
+
+ # Change the footnote reference to an endnote reference
+ try:
+ paragraph = doc.paragraphs[ref["paragraph_index"]]
+ paragraph.runs[ref["run_index"]].text = f"†{i+1}"
+ except IndexError:
+ # Skip if we can't locate the reference
+ pass
+
+ # Save the document
+ doc.save(filename)
+
+ return f"Converted {len(footnote_references)} footnotes to endnotes in {filename}"
+ except Exception as e:
+ return f"Failed to convert footnotes to endnotes: {str(e)}"
+
+
+async def add_footnote_after_text(filename: str, search_text: str, footnote_text: str,
+ output_filename: Optional[str] = None) -> str:
+ """Add a footnote after specific text in a Word document with proper formatting.
+
+ This enhanced function ensures proper superscript formatting by managing styles at the XML level.
+
+ Args:
+ filename: Path to the Word document
+ search_text: Text to search for (footnote will be added after this text)
+ footnote_text: Content of the footnote
+ output_filename: Optional output filename (if None, modifies in place)
+ """
+ filename = ensure_docx_extension(filename)
+
+ if not os.path.exists(filename):
+ return f"Document {filename} does not exist"
+
+ # Check if file is writeable
+ is_writeable, error_message = check_file_writeable(filename)
+ if not is_writeable:
+ return f"Cannot modify document: {error_message}. Consider creating a copy first."
+
+ try:
+ # Use robust implementation
+ success, message, details = add_footnote_robust(
+ filename=filename,
+ search_text=search_text,
+ footnote_text=footnote_text,
+ output_filename=output_filename,
+ position="after",
+ validate_location=True
+ )
+ return message
+ except Exception as e:
+ return f"Failed to add footnote: {str(e)}"
+
+
+async def add_footnote_before_text(filename: str, search_text: str, footnote_text: str,
+ output_filename: Optional[str] = None) -> str:
+ """Add a footnote before specific text in a Word document with proper formatting.
+
+ This enhanced function ensures proper superscript formatting by managing styles at the XML level.
+
+ Args:
+ filename: Path to the Word document
+ search_text: Text to search for (footnote will be added before this text)
+ footnote_text: Content of the footnote
+ output_filename: Optional output filename (if None, modifies in place)
+ """
+ filename = ensure_docx_extension(filename)
+
+ if not os.path.exists(filename):
+ return f"Document {filename} does not exist"
+
+ # Check if file is writeable
+ is_writeable, error_message = check_file_writeable(filename)
+ if not is_writeable:
+ return f"Cannot modify document: {error_message}. Consider creating a copy first."
+
+ try:
+ # Use robust implementation
+ success, message, details = add_footnote_robust(
+ filename=filename,
+ search_text=search_text,
+ footnote_text=footnote_text,
+ output_filename=output_filename,
+ position="before",
+ validate_location=True
+ )
+ return message
+ except Exception as e:
+ return f"Failed to add footnote: {str(e)}"
+
+
+async def add_footnote_enhanced(filename: str, paragraph_index: int, footnote_text: str,
+ output_filename: Optional[str] = None) -> str:
+ """Enhanced version of add_footnote_to_document with proper superscript formatting.
+
+ Now uses the robust implementation for better reliability.
+
+ Args:
+ filename: Path to the Word document
+ paragraph_index: Index of the paragraph to add footnote to (0-based)
+ footnote_text: Text content of the footnote
+ output_filename: Optional output filename (if None, modifies in place)
+ """
+ filename = ensure_docx_extension(filename)
+
+ # Ensure paragraph_index is an integer
+ try:
+ paragraph_index = int(paragraph_index)
+ except (ValueError, TypeError):
+ return "Invalid parameter: paragraph_index must be an integer"
+
+ if not os.path.exists(filename):
+ return f"Document {filename} does not exist"
+
+ # Check if file is writeable
+ is_writeable, error_message = check_file_writeable(filename)
+ if not is_writeable:
+ return f"Cannot modify document: {error_message}. Consider creating a copy first."
+
+ try:
+ # Use robust implementation
+ success, message, details = add_footnote_robust(
+ filename=filename,
+ paragraph_index=paragraph_index,
+ footnote_text=footnote_text,
+ output_filename=output_filename,
+ validate_location=True
+ )
+ return message
+ except Exception as e:
+ return f"Failed to add footnote: {str(e)}"
+
+
+async def customize_footnote_style(filename: str, numbering_format: str = "1, 2, 3",
+ start_number: int = 1, font_name: Optional[str] = None,
+ font_size: Optional[int] = None) -> str:
+ """Customize footnote numbering and formatting in a Word document.
+
+ Args:
+ filename: Path to the Word document
+ numbering_format: Format for footnote numbers (e.g., "1, 2, 3", "i, ii, iii", "a, b, c")
+ start_number: Number to start footnote numbering from
+ font_name: Optional font name for footnotes
+ font_size: Optional font size for footnotes (in points)
+ """
+ filename = ensure_docx_extension(filename)
+
+ if not os.path.exists(filename):
+ return f"Document {filename} does not exist"
+
+ # Check if file is writeable
+ is_writeable, error_message = check_file_writeable(filename)
+ if not is_writeable:
+ return f"Cannot modify document: {error_message}. Consider creating a copy first."
+
+ try:
+ doc = Document(filename)
+
+ # Create or get footnote style
+ footnote_style_name = "Footnote Text"
+ footnote_style = None
+
+ try:
+ footnote_style = doc.styles[footnote_style_name]
+ except KeyError:
+ # Create the style if it doesn't exist
+ footnote_style = doc.styles.add_style(footnote_style_name, WD_STYLE_TYPE.PARAGRAPH)
+
+ # Apply formatting to footnote style
+ if footnote_style:
+ if font_name:
+ footnote_style.font.name = font_name
+ if font_size:
+ footnote_style.font.size = Pt(font_size)
+
+ # Find all existing footnote references
+ footnote_refs = find_footnote_references(doc)
+
+ # Generate format symbols for the specified numbering format
+ format_symbols = get_format_symbols(numbering_format, len(footnote_refs) + start_number)
+
+ # Apply custom formatting to footnotes
+ count = customize_footnote_formatting(doc, footnote_refs, format_symbols, start_number, footnote_style)
+
+ # Save the document
+ doc.save(filename)
+
+ return f"Footnote style and numbering customized in {filename}"
+ except Exception as e:
+ return f"Failed to customize footnote style: {str(e)}"
+
+
+async def delete_footnote_from_document(filename: str, footnote_id: Optional[int] = None,
+ search_text: Optional[str] = None,
+ output_filename: Optional[str] = None) -> str:
+ """Delete a footnote from a Word document.
+
+ You can identify the footnote to delete either by:
+ 1. footnote_id: The numeric ID of the footnote (1, 2, 3, etc.)
+ 2. search_text: Text near the footnote reference to find and delete
+
+ Args:
+ filename: Path to the Word document
+ footnote_id: Optional ID of the footnote to delete (1-based)
+ search_text: Optional text to search near the footnote reference
+ output_filename: Optional output filename (if None, modifies in place)
+ """
+ filename = ensure_docx_extension(filename)
+
+ if not os.path.exists(filename):
+ return f"Document {filename} does not exist"
+
+ # Check if file is writeable
+ is_writeable, error_message = check_file_writeable(filename)
+ if not is_writeable:
+ return f"Cannot modify document: {error_message}. Consider creating a copy first."
+
+ try:
+ # Use robust implementation with orphan cleanup
+ success, message, details = delete_footnote_robust(
+ filename=filename,
+ footnote_id=footnote_id,
+ search_text=search_text,
+ output_filename=output_filename,
+ clean_orphans=True
+ )
+ return message
+ except Exception as e:
+ return f"Failed to delete footnote: {str(e)}"
+
+
+# ============================================================================
+# Robust tool functions with Dict returns for structured responses
+# ============================================================================
+
+
+async def add_footnote_robust_tool(
+ filename: str,
+ search_text: Optional[str] = None,
+ paragraph_index: Optional[int] = None,
+ footnote_text: str = "",
+ validate_location: bool = True,
+ auto_repair: bool = False
+) -> Dict[str, Any]:
+ """
+ Add a footnote with robust validation and error handling.
+
+ This is the production-ready version with comprehensive Word compliance.
+
+ Args:
+ filename: Path to the Word document
+ search_text: Text to search for (mutually exclusive with paragraph_index)
+ paragraph_index: Index of paragraph (mutually exclusive with search_text)
+ footnote_text: Content of the footnote
+ validate_location: Whether to validate placement restrictions
+ auto_repair: Whether to attempt automatic document repair
+
+ Returns:
+ Dict with success status, message, and optional details
+ """
+ filename = ensure_docx_extension(filename)
+
+ # Check if file is writeable
+ is_writeable, error_message = check_file_writeable(filename)
+ if not is_writeable:
+ return {
+ "success": False,
+ "message": f"Cannot modify document: {error_message}",
+ "details": None
+ }
+
+ # Convert paragraph_index if provided as string
+ if paragraph_index is not None:
+ try:
+ paragraph_index = int(paragraph_index)
+ except (ValueError, TypeError):
+ return {
+ "success": False,
+ "message": "Invalid parameter: paragraph_index must be an integer",
+ "details": None
+ }
+
+ # Call robust implementation
+ success, message, details = add_footnote_robust(
+ filename=filename,
+ search_text=search_text,
+ paragraph_index=paragraph_index,
+ footnote_text=footnote_text,
+ validate_location=validate_location,
+ auto_repair=auto_repair
+ )
+
+ return {
+ "success": success,
+ "message": message,
+ "details": details
+ }
+
+
+async def delete_footnote_robust_tool(
+ filename: str,
+ footnote_id: Optional[int] = None,
+ search_text: Optional[str] = None,
+ clean_orphans: bool = True
+) -> Dict[str, Any]:
+ """
+ Delete a footnote with comprehensive cleanup.
+
+ Args:
+ filename: Path to the Word document
+ footnote_id: ID of footnote to delete
+ search_text: Text near footnote reference
+ clean_orphans: Whether to remove orphaned content
+
+ Returns:
+ Dict with success status, message, and optional details
+ """
+ filename = ensure_docx_extension(filename)
+
+ # Check if file is writeable
+ is_writeable, error_message = check_file_writeable(filename)
+ if not is_writeable:
+ return {
+ "success": False,
+ "message": f"Cannot modify document: {error_message}",
+ "details": None
+ }
+
+ # Convert footnote_id if provided as string
+ if footnote_id is not None:
+ try:
+ footnote_id = int(footnote_id)
+ except (ValueError, TypeError):
+ return {
+ "success": False,
+ "message": "Invalid parameter: footnote_id must be an integer",
+ "details": None
+ }
+
+ # Call robust implementation
+ success, message, details = delete_footnote_robust(
+ filename=filename,
+ footnote_id=footnote_id,
+ search_text=search_text,
+ clean_orphans=clean_orphans
+ )
+
+ return {
+ "success": success,
+ "message": message,
+ "details": details
+ }
+
+
+async def validate_footnotes_tool(filename: str) -> Dict[str, Any]:
+ """
+ Validate all footnotes in a document.
+
+ Provides comprehensive validation report including:
+ - ID conflicts
+ - Orphaned content
+ - Missing styles
+ - Invalid locations
+ - Coherence issues
+
+ Args:
+ filename: Path to the Word document
+
+ Returns:
+ Dict with validation status and detailed report
+ """
+ filename = ensure_docx_extension(filename)
+
+ if not os.path.exists(filename):
+ return {
+ "valid": False,
+ "message": f"Document {filename} does not exist",
+ "report": {}
+ }
+
+ # Call validation
+ is_valid, message, report = validate_document_footnotes(filename)
+
+ return {
+ "valid": is_valid,
+ "message": message,
+ "report": report
+ }
+
+
+# ============================================================================
+# Compatibility wrappers for robust tools (maintain backward compatibility)
+# ============================================================================
+
+async def add_footnote_to_document_robust(
+ filename: str,
+ paragraph_index: int,
+ footnote_text: str
+) -> str:
+ """
+ Robust version of add_footnote_to_document.
+ Maintains backward compatibility with existing API.
+ """
+ result = await add_footnote_robust_tool(
+ filename=filename,
+ paragraph_index=paragraph_index,
+ footnote_text=footnote_text
+ )
+ return result["message"]
+
+
+async def add_footnote_after_text_robust(
+ filename: str,
+ search_text: str,
+ footnote_text: str,
+ output_filename: Optional[str] = None
+) -> str:
+ """
+ Robust version of add_footnote_after_text.
+ Maintains backward compatibility with existing API.
+ """
+ # Handle output filename by copying first if needed
+ working_file = filename
+ if output_filename:
+ import shutil
+ shutil.copy2(filename, output_filename)
+ working_file = output_filename
+
+ result = await add_footnote_robust_tool(
+ filename=working_file,
+ search_text=search_text,
+ footnote_text=footnote_text
+ )
+ return result["message"]
+
+
+async def add_footnote_before_text_robust(
+ filename: str,
+ search_text: str,
+ footnote_text: str,
+ output_filename: Optional[str] = None
+) -> str:
+ """
+ Robust version of add_footnote_before_text.
+ Note: Current robust implementation defaults to 'after' position.
+ """
+ # Handle output filename
+ working_file = filename
+ if output_filename:
+ import shutil
+ shutil.copy2(filename, output_filename)
+ working_file = output_filename
+
+ result = await add_footnote_robust_tool(
+ filename=working_file,
+ search_text=search_text,
+ footnote_text=footnote_text
+ )
+ return result["message"]
+
+
+async def delete_footnote_from_document_robust(
+ filename: str,
+ footnote_id: Optional[int] = None,
+ search_text: Optional[str] = None,
+ output_filename: Optional[str] = None
+) -> str:
+ """
+ Robust version of delete_footnote_from_document.
+ Maintains backward compatibility with existing API.
+ """
+ # Handle output filename
+ working_file = filename
+ if output_filename:
+ import shutil
+ shutil.copy2(filename, output_filename)
+ working_file = output_filename
+
+ result = await delete_footnote_robust_tool(
+ filename=working_file,
+ footnote_id=footnote_id,
+ search_text=search_text
+ )
+ return result["message"]
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/tools/format_tools.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/tools/format_tools.py
new file mode 100644
index 00000000..a60fc0c4
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/tools/format_tools.py
@@ -0,0 +1,1112 @@
+"""
+Formatting tools for Word Document Server.
+
+These tools handle formatting operations for Word documents,
+including text formatting, table formatting, and custom styles.
+"""
+import os
+from typing import List, Optional, Dict, Any
+from docx import Document
+from docx.shared import Pt, RGBColor
+from docx.enum.text import WD_COLOR_INDEX
+from docx.enum.style import WD_STYLE_TYPE
+
+from word_document_server.utils.file_utils import check_file_writeable, ensure_docx_extension
+from word_document_server.core.styles import create_style
+from word_document_server.core.tables import (
+ apply_table_style, set_cell_shading_by_position, apply_alternating_row_shading,
+ highlight_header_row, merge_cells, merge_cells_horizontal, merge_cells_vertical,
+ set_cell_alignment_by_position, set_table_alignment, set_column_width_by_position,
+ set_column_widths, set_table_width as set_table_width_func, auto_fit_table,
+ format_cell_text_by_position, set_cell_padding_by_position
+)
+
+
+async def format_text(filename: str, paragraph_index: int, start_pos: int, end_pos: int,
+ bold: Optional[bool] = None, italic: Optional[bool] = None,
+ underline: Optional[bool] = None, color: Optional[str] = None,
+ font_size: Optional[int] = None, font_name: Optional[str] = None) -> str:
+ """Format a specific range of text within a paragraph.
+
+ Args:
+ filename: Path to the Word document
+ paragraph_index: Index of the paragraph (0-based)
+ start_pos: Start position within the paragraph text
+ end_pos: End position within the paragraph text
+ bold: Set text bold (True/False)
+ italic: Set text italic (True/False)
+ underline: Set text underlined (True/False)
+ color: Text color (e.g., 'red', 'blue', etc.)
+ font_size: Font size in points
+ font_name: Font name/family
+ """
+ filename = ensure_docx_extension(filename)
+
+ # Ensure numeric parameters are the correct type
+ try:
+ paragraph_index = int(paragraph_index)
+ start_pos = int(start_pos)
+ end_pos = int(end_pos)
+ if font_size is not None:
+ font_size = int(font_size)
+ except (ValueError, TypeError):
+ return "Invalid parameter: paragraph_index, start_pos, end_pos, and font_size must be integers"
+
+ if not os.path.exists(filename):
+ return f"Document {filename} does not exist"
+
+ # Check if file is writeable
+ is_writeable, error_message = check_file_writeable(filename)
+ if not is_writeable:
+ return f"Cannot modify document: {error_message}. Consider creating a copy first."
+
+ try:
+ doc = Document(filename)
+
+ # Validate paragraph index
+ if paragraph_index < 0 or paragraph_index >= len(doc.paragraphs):
+ return f"Invalid paragraph index. Document has {len(doc.paragraphs)} paragraphs (0-{len(doc.paragraphs)-1})."
+
+ paragraph = doc.paragraphs[paragraph_index]
+ text = paragraph.text
+
+ # Validate text positions
+ if start_pos < 0 or end_pos > len(text) or start_pos >= end_pos:
+ return f"Invalid text positions. Paragraph has {len(text)} characters."
+
+ # Get the text to format
+ target_text = text[start_pos:end_pos]
+
+ # Clear existing runs and create three runs: before, target, after
+ for run in paragraph.runs:
+ run.clear()
+
+ # Add text before target
+ if start_pos > 0:
+ run_before = paragraph.add_run(text[:start_pos])
+
+ # Add target text with formatting
+ run_target = paragraph.add_run(target_text)
+ if bold is not None:
+ run_target.bold = bold
+ if italic is not None:
+ run_target.italic = italic
+ if underline is not None:
+ run_target.underline = underline
+ if color:
+ # Define common RGB colors
+ color_map = {
+ 'red': RGBColor(255, 0, 0),
+ 'blue': RGBColor(0, 0, 255),
+ 'green': RGBColor(0, 128, 0),
+ 'yellow': RGBColor(255, 255, 0),
+ 'black': RGBColor(0, 0, 0),
+ 'gray': RGBColor(128, 128, 128),
+ 'white': RGBColor(255, 255, 255),
+ 'purple': RGBColor(128, 0, 128),
+ 'orange': RGBColor(255, 165, 0)
+ }
+
+ try:
+ if color.lower() in color_map:
+ # Use predefined RGB color
+ run_target.font.color.rgb = color_map[color.lower()]
+ else:
+ # Try to set color by name
+ run_target.font.color.rgb = RGBColor.from_string(color)
+ except Exception as e:
+ # If all else fails, default to black
+ run_target.font.color.rgb = RGBColor(0, 0, 0)
+ if font_size:
+ run_target.font.size = Pt(font_size)
+ if font_name:
+ run_target.font.name = font_name
+
+ # Add text after target
+ if end_pos < len(text):
+ run_after = paragraph.add_run(text[end_pos:])
+
+ doc.save(filename)
+ return f"Text '{target_text}' formatted successfully in paragraph {paragraph_index}."
+ except Exception as e:
+ return f"Failed to format text: {str(e)}"
+
+
+async def create_custom_style(filename: str, style_name: str,
+ bold: Optional[bool] = None, italic: Optional[bool] = None,
+ font_size: Optional[int] = None, font_name: Optional[str] = None,
+ color: Optional[str] = None, base_style: Optional[str] = None) -> str:
+ """Create a custom style in the document.
+
+ Args:
+ filename: Path to the Word document
+ style_name: Name for the new style
+ bold: Set text bold (True/False)
+ italic: Set text italic (True/False)
+ font_size: Font size in points
+ font_name: Font name/family
+ color: Text color (e.g., 'red', 'blue')
+ base_style: Optional existing style to base this on
+ """
+ filename = ensure_docx_extension(filename)
+
+ if not os.path.exists(filename):
+ return f"Document {filename} does not exist"
+
+ # Check if file is writeable
+ is_writeable, error_message = check_file_writeable(filename)
+ if not is_writeable:
+ return f"Cannot modify document: {error_message}. Consider creating a copy first."
+
+ try:
+ doc = Document(filename)
+
+ # Build font properties dictionary
+ font_properties = {}
+ if bold is not None:
+ font_properties['bold'] = bold
+ if italic is not None:
+ font_properties['italic'] = italic
+ if font_size is not None:
+ font_properties['size'] = font_size
+ if font_name is not None:
+ font_properties['name'] = font_name
+ if color is not None:
+ font_properties['color'] = color
+
+ # Create the style
+ new_style = create_style(
+ doc,
+ style_name,
+ WD_STYLE_TYPE.PARAGRAPH,
+ base_style=base_style,
+ font_properties=font_properties
+ )
+
+ doc.save(filename)
+ return f"Style '{style_name}' created successfully."
+ except Exception as e:
+ return f"Failed to create style: {str(e)}"
+
+
+async def format_table(filename: str, table_index: int,
+ has_header_row: Optional[bool] = None,
+ border_style: Optional[str] = None,
+ shading: Optional[List[List[str]]] = None) -> str:
+ """Format a table with borders, shading, and structure.
+
+ Args:
+ filename: Path to the Word document
+ table_index: Index of the table (0-based)
+ has_header_row: If True, formats the first row as a header
+ border_style: Style for borders ('none', 'single', 'double', 'thick')
+ shading: 2D list of cell background colors (by row and column)
+ """
+ filename = ensure_docx_extension(filename)
+
+ if not os.path.exists(filename):
+ return f"Document {filename} does not exist"
+
+ # Check if file is writeable
+ is_writeable, error_message = check_file_writeable(filename)
+ if not is_writeable:
+ return f"Cannot modify document: {error_message}. Consider creating a copy first."
+
+ try:
+ doc = Document(filename)
+
+ # Validate table index
+ if table_index < 0 or table_index >= len(doc.tables):
+ return f"Invalid table index. Document has {len(doc.tables)} tables (0-{len(doc.tables)-1})."
+
+ table = doc.tables[table_index]
+
+ # Apply formatting
+ success = apply_table_style(table, has_header_row or False, border_style, shading)
+
+ if success:
+ doc.save(filename)
+ return f"Table at index {table_index} formatted successfully."
+ else:
+ return f"Failed to format table at index {table_index}."
+ except Exception as e:
+ return f"Failed to format table: {str(e)}"
+
+
+async def set_table_cell_shading(filename: str, table_index: int, row_index: int,
+ col_index: int, fill_color: str, pattern: str = "clear") -> str:
+ """Apply shading/filling to a specific table cell.
+
+ Args:
+ filename: Path to the Word document
+ table_index: Index of the table (0-based)
+ row_index: Row index of the cell (0-based)
+ col_index: Column index of the cell (0-based)
+ fill_color: Background color (hex string like "FF0000" or "red")
+ pattern: Shading pattern ("clear", "solid", "pct10", "pct20", etc.)
+ """
+ filename = ensure_docx_extension(filename)
+
+ # Ensure numeric parameters are the correct type
+ try:
+ table_index = int(table_index)
+ row_index = int(row_index)
+ col_index = int(col_index)
+ except (ValueError, TypeError):
+ return "Invalid parameter: table_index, row_index, and col_index must be integers"
+
+ if not os.path.exists(filename):
+ return f"Document {filename} does not exist"
+
+ # Check if file is writeable
+ is_writeable, error_message = check_file_writeable(filename)
+ if not is_writeable:
+ return f"Cannot modify document: {error_message}. Consider creating a copy first."
+
+ try:
+ doc = Document(filename)
+
+ # Validate table index
+ if table_index < 0 or table_index >= len(doc.tables):
+ return f"Invalid table index. Document has {len(doc.tables)} tables (0-{len(doc.tables)-1})."
+
+ table = doc.tables[table_index]
+
+ # Validate row and column indices
+ if row_index < 0 or row_index >= len(table.rows):
+ return f"Invalid row index. Table has {len(table.rows)} rows (0-{len(table.rows)-1})."
+
+ if col_index < 0 or col_index >= len(table.rows[row_index].cells):
+ return f"Invalid column index. Row has {len(table.rows[row_index].cells)} cells (0-{len(table.rows[row_index].cells)-1})."
+
+ # Apply cell shading
+ success = set_cell_shading_by_position(table, row_index, col_index, fill_color, pattern)
+
+ if success:
+ doc.save(filename)
+ return f"Cell shading applied successfully to table {table_index}, row {row_index}, column {col_index}."
+ else:
+ return f"Failed to apply cell shading."
+ except Exception as e:
+ return f"Failed to apply cell shading: {str(e)}"
+
+
+async def apply_table_alternating_rows(filename: str, table_index: int,
+ color1: str = "FFFFFF", color2: str = "F2F2F2") -> str:
+ """Apply alternating row colors to a table for better readability.
+
+ Args:
+ filename: Path to the Word document
+ table_index: Index of the table (0-based)
+ color1: Color for odd rows (hex string, default white)
+ color2: Color for even rows (hex string, default light gray)
+ """
+ filename = ensure_docx_extension(filename)
+
+ # Ensure numeric parameters are the correct type
+ try:
+ table_index = int(table_index)
+ except (ValueError, TypeError):
+ return "Invalid parameter: table_index must be an integer"
+
+ if not os.path.exists(filename):
+ return f"Document {filename} does not exist"
+
+ # Check if file is writeable
+ is_writeable, error_message = check_file_writeable(filename)
+ if not is_writeable:
+ return f"Cannot modify document: {error_message}. Consider creating a copy first."
+
+ try:
+ doc = Document(filename)
+
+ # Validate table index
+ if table_index < 0 or table_index >= len(doc.tables):
+ return f"Invalid table index. Document has {len(doc.tables)} tables (0-{len(doc.tables)-1})."
+
+ table = doc.tables[table_index]
+
+ # Apply alternating row shading
+ success = apply_alternating_row_shading(table, color1, color2)
+
+ if success:
+ doc.save(filename)
+ return f"Alternating row shading applied successfully to table {table_index}."
+ else:
+ return f"Failed to apply alternating row shading."
+ except Exception as e:
+ return f"Failed to apply alternating row shading: {str(e)}"
+
+
+async def highlight_table_header(filename: str, table_index: int,
+ header_color: str = "4472C4", text_color: str = "FFFFFF") -> str:
+ """Apply special highlighting to table header row.
+
+ Args:
+ filename: Path to the Word document
+ table_index: Index of the table (0-based)
+ header_color: Background color for header (hex string, default blue)
+ text_color: Text color for header (hex string, default white)
+ """
+ filename = ensure_docx_extension(filename)
+
+ # Ensure numeric parameters are the correct type
+ try:
+ table_index = int(table_index)
+ except (ValueError, TypeError):
+ return "Invalid parameter: table_index must be an integer"
+
+ if not os.path.exists(filename):
+ return f"Document {filename} does not exist"
+
+ # Check if file is writeable
+ is_writeable, error_message = check_file_writeable(filename)
+ if not is_writeable:
+ return f"Cannot modify document: {error_message}. Consider creating a copy first."
+
+ try:
+ doc = Document(filename)
+
+ # Validate table index
+ if table_index < 0 or table_index >= len(doc.tables):
+ return f"Invalid table index. Document has {len(doc.tables)} tables (0-{len(doc.tables)-1})."
+
+ table = doc.tables[table_index]
+
+ # Apply header highlighting
+ success = highlight_header_row(table, header_color, text_color)
+
+ if success:
+ doc.save(filename)
+ return f"Header highlighting applied successfully to table {table_index}."
+ else:
+ return f"Failed to apply header highlighting."
+ except Exception as e:
+ return f"Failed to apply header highlighting: {str(e)}"
+
+
+async def merge_table_cells(filename: str, table_index: int, start_row: int, start_col: int,
+ end_row: int, end_col: int) -> str:
+ """Merge cells in a rectangular area of a table.
+
+ Args:
+ filename: Path to the Word document
+ table_index: Index of the table (0-based)
+ start_row: Starting row index (0-based)
+ start_col: Starting column index (0-based)
+ end_row: Ending row index (0-based, inclusive)
+ end_col: Ending column index (0-based, inclusive)
+ """
+ filename = ensure_docx_extension(filename)
+
+ # Ensure numeric parameters are the correct type
+ try:
+ table_index = int(table_index)
+ start_row = int(start_row)
+ start_col = int(start_col)
+ end_row = int(end_row)
+ end_col = int(end_col)
+ except (ValueError, TypeError):
+ return "Invalid parameter: all indices must be integers"
+
+ if not os.path.exists(filename):
+ return f"Document {filename} does not exist"
+
+ # Check if file is writeable
+ is_writeable, error_message = check_file_writeable(filename)
+ if not is_writeable:
+ return f"Cannot modify document: {error_message}. Consider creating a copy first."
+
+ try:
+ doc = Document(filename)
+
+ # Validate table index
+ if table_index < 0 or table_index >= len(doc.tables):
+ return f"Invalid table index. Document has {len(doc.tables)} tables (0-{len(doc.tables)-1})."
+
+ table = doc.tables[table_index]
+
+ # Validate merge parameters
+ if start_row > end_row or start_col > end_col:
+ return "Invalid merge range: start indices must be <= end indices"
+
+ if start_row == end_row and start_col == end_col:
+ return "Invalid merge range: cannot merge a single cell with itself"
+
+ # Apply cell merge
+ success = merge_cells(table, start_row, start_col, end_row, end_col)
+
+ if success:
+ doc.save(filename)
+ return f"Cells merged successfully in table {table_index} from ({start_row},{start_col}) to ({end_row},{end_col})."
+ else:
+ return f"Failed to merge cells. Check that indices are valid."
+ except Exception as e:
+ return f"Failed to merge cells: {str(e)}"
+
+
+async def merge_table_cells_horizontal(filename: str, table_index: int, row_index: int,
+ start_col: int, end_col: int) -> str:
+ """Merge cells horizontally in a single row.
+
+ Args:
+ filename: Path to the Word document
+ table_index: Index of the table (0-based)
+ row_index: Row index (0-based)
+ start_col: Starting column index (0-based)
+ end_col: Ending column index (0-based, inclusive)
+ """
+ filename = ensure_docx_extension(filename)
+
+ # Ensure numeric parameters are the correct type
+ try:
+ table_index = int(table_index)
+ row_index = int(row_index)
+ start_col = int(start_col)
+ end_col = int(end_col)
+ except (ValueError, TypeError):
+ return "Invalid parameter: all indices must be integers"
+
+ if not os.path.exists(filename):
+ return f"Document {filename} does not exist"
+
+ # Check if file is writeable
+ is_writeable, error_message = check_file_writeable(filename)
+ if not is_writeable:
+ return f"Cannot modify document: {error_message}. Consider creating a copy first."
+
+ try:
+ doc = Document(filename)
+
+ # Validate table index
+ if table_index < 0 or table_index >= len(doc.tables):
+ return f"Invalid table index. Document has {len(doc.tables)} tables (0-{len(doc.tables)-1})."
+
+ table = doc.tables[table_index]
+
+ # Apply horizontal cell merge
+ success = merge_cells_horizontal(table, row_index, start_col, end_col)
+
+ if success:
+ doc.save(filename)
+ return f"Cells merged horizontally in table {table_index}, row {row_index}, columns {start_col}-{end_col}."
+ else:
+ return f"Failed to merge cells horizontally. Check that indices are valid."
+ except Exception as e:
+ return f"Failed to merge cells horizontally: {str(e)}"
+
+
+async def merge_table_cells_vertical(filename: str, table_index: int, col_index: int,
+ start_row: int, end_row: int) -> str:
+ """Merge cells vertically in a single column.
+
+ Args:
+ filename: Path to the Word document
+ table_index: Index of the table (0-based)
+ col_index: Column index (0-based)
+ start_row: Starting row index (0-based)
+ end_row: Ending row index (0-based, inclusive)
+ """
+ filename = ensure_docx_extension(filename)
+
+ # Ensure numeric parameters are the correct type
+ try:
+ table_index = int(table_index)
+ col_index = int(col_index)
+ start_row = int(start_row)
+ end_row = int(end_row)
+ except (ValueError, TypeError):
+ return "Invalid parameter: all indices must be integers"
+
+ if not os.path.exists(filename):
+ return f"Document {filename} does not exist"
+
+ # Check if file is writeable
+ is_writeable, error_message = check_file_writeable(filename)
+ if not is_writeable:
+ return f"Cannot modify document: {error_message}. Consider creating a copy first."
+
+ try:
+ doc = Document(filename)
+
+ # Validate table index
+ if table_index < 0 or table_index >= len(doc.tables):
+ return f"Invalid table index. Document has {len(doc.tables)} tables (0-{len(doc.tables)-1})."
+
+ table = doc.tables[table_index]
+
+ # Apply vertical cell merge
+ success = merge_cells_vertical(table, col_index, start_row, end_row)
+
+ if success:
+ doc.save(filename)
+ return f"Cells merged vertically in table {table_index}, column {col_index}, rows {start_row}-{end_row}."
+ else:
+ return f"Failed to merge cells vertically. Check that indices are valid."
+ except Exception as e:
+ return f"Failed to merge cells vertically: {str(e)}"
+
+
+async def set_table_cell_alignment(filename: str, table_index: int, row_index: int, col_index: int,
+ horizontal: str = "left", vertical: str = "top") -> str:
+ """Set text alignment for a specific table cell.
+
+ Args:
+ filename: Path to the Word document
+ table_index: Index of the table (0-based)
+ row_index: Row index (0-based)
+ col_index: Column index (0-based)
+ horizontal: Horizontal alignment ("left", "center", "right", "justify")
+ vertical: Vertical alignment ("top", "center", "bottom")
+ """
+ filename = ensure_docx_extension(filename)
+
+ # Ensure numeric parameters are the correct type
+ try:
+ table_index = int(table_index)
+ row_index = int(row_index)
+ col_index = int(col_index)
+ except (ValueError, TypeError):
+ return "Invalid parameter: table_index, row_index, and col_index must be integers"
+
+ # Validate alignment parameters
+ valid_horizontal = ["left", "center", "right", "justify"]
+ valid_vertical = ["top", "center", "bottom"]
+
+ if horizontal.lower() not in valid_horizontal:
+ return f"Invalid horizontal alignment. Valid options: {', '.join(valid_horizontal)}"
+
+ if vertical.lower() not in valid_vertical:
+ return f"Invalid vertical alignment. Valid options: {', '.join(valid_vertical)}"
+
+ if not os.path.exists(filename):
+ return f"Document {filename} does not exist"
+
+ # Check if file is writeable
+ is_writeable, error_message = check_file_writeable(filename)
+ if not is_writeable:
+ return f"Cannot modify document: {error_message}. Consider creating a copy first."
+
+ try:
+ doc = Document(filename)
+
+ # Validate table index
+ if table_index < 0 or table_index >= len(doc.tables):
+ return f"Invalid table index. Document has {len(doc.tables)} tables (0-{len(doc.tables)-1})."
+
+ table = doc.tables[table_index]
+
+ # Apply cell alignment
+ success = set_cell_alignment_by_position(table, row_index, col_index, horizontal, vertical)
+
+ if success:
+ doc.save(filename)
+ return f"Cell alignment set successfully for table {table_index}, cell ({row_index},{col_index}) to {horizontal}/{vertical}."
+ else:
+ return f"Failed to set cell alignment. Check that indices are valid."
+ except Exception as e:
+ return f"Failed to set cell alignment: {str(e)}"
+
+
+async def set_table_alignment_all(filename: str, table_index: int,
+ horizontal: str = "left", vertical: str = "top") -> str:
+ """Set text alignment for all cells in a table.
+
+ Args:
+ filename: Path to the Word document
+ table_index: Index of the table (0-based)
+ horizontal: Horizontal alignment ("left", "center", "right", "justify")
+ vertical: Vertical alignment ("top", "center", "bottom")
+ """
+ filename = ensure_docx_extension(filename)
+
+ # Ensure numeric parameters are the correct type
+ try:
+ table_index = int(table_index)
+ except (ValueError, TypeError):
+ return "Invalid parameter: table_index must be an integer"
+
+ # Validate alignment parameters
+ valid_horizontal = ["left", "center", "right", "justify"]
+ valid_vertical = ["top", "center", "bottom"]
+
+ if horizontal.lower() not in valid_horizontal:
+ return f"Invalid horizontal alignment. Valid options: {', '.join(valid_horizontal)}"
+
+ if vertical.lower() not in valid_vertical:
+ return f"Invalid vertical alignment. Valid options: {', '.join(valid_vertical)}"
+
+ if not os.path.exists(filename):
+ return f"Document {filename} does not exist"
+
+ # Check if file is writeable
+ is_writeable, error_message = check_file_writeable(filename)
+ if not is_writeable:
+ return f"Cannot modify document: {error_message}. Consider creating a copy first."
+
+ try:
+ doc = Document(filename)
+
+ # Validate table index
+ if table_index < 0 or table_index >= len(doc.tables):
+ return f"Invalid table index. Document has {len(doc.tables)} tables (0-{len(doc.tables)-1})."
+
+ table = doc.tables[table_index]
+
+ # Apply table alignment
+ success = set_table_alignment(table, horizontal, vertical)
+
+ if success:
+ doc.save(filename)
+ return f"Table alignment set successfully for table {table_index} to {horizontal}/{vertical} for all cells."
+ else:
+ return f"Failed to set table alignment."
+ except Exception as e:
+ return f"Failed to set table alignment: {str(e)}"
+
+
+async def set_table_column_width(filename: str, table_index: int, col_index: int,
+ width: float, width_type: str = "points") -> str:
+ """Set the width of a specific table column.
+
+ Args:
+ filename: Path to the Word document
+ table_index: Index of the table (0-based)
+ col_index: Column index (0-based)
+ width: Column width value
+ width_type: Width type ("points", "inches", "cm", "percent", "auto")
+ """
+ filename = ensure_docx_extension(filename)
+
+ # Ensure numeric parameters are the correct type
+ try:
+ table_index = int(table_index)
+ col_index = int(col_index)
+ if width_type != "auto":
+ width = float(width)
+ except (ValueError, TypeError):
+ return "Invalid parameter: table_index and col_index must be integers, width must be a number"
+
+ # Validate width type
+ valid_width_types = ["points", "inches", "cm", "percent", "auto"]
+ if width_type.lower() not in valid_width_types:
+ return f"Invalid width type. Valid options: {', '.join(valid_width_types)}"
+
+ if not os.path.exists(filename):
+ return f"Document {filename} does not exist"
+
+ # Check if file is writeable
+ is_writeable, error_message = check_file_writeable(filename)
+ if not is_writeable:
+ return f"Cannot modify document: {error_message}. Consider creating a copy first."
+
+ try:
+ doc = Document(filename)
+
+ # Validate table index
+ if table_index < 0 or table_index >= len(doc.tables):
+ return f"Invalid table index. Document has {len(doc.tables)} tables (0-{len(doc.tables)-1})."
+
+ table = doc.tables[table_index]
+
+ # Validate column index
+ if col_index < 0 or col_index >= len(table.columns):
+ return f"Invalid column index. Table has {len(table.columns)} columns (0-{len(table.columns)-1})."
+
+ # Convert width and type for Word format
+ if width_type.lower() == "points":
+ # Points to DXA (twentieths of a point)
+ word_width = width
+ word_type = "dxa"
+ elif width_type.lower() == "inches":
+ # Inches to points, then to DXA
+ word_width = width * 72 # 72 points per inch
+ word_type = "dxa"
+ elif width_type.lower() == "cm":
+ # CM to points, then to DXA
+ word_width = width * 28.35 # ~28.35 points per cm
+ word_type = "dxa"
+ elif width_type.lower() == "percent":
+ # Percentage (Word uses 50x the percentage value)
+ word_width = width
+ word_type = "pct"
+ else: # auto
+ word_width = 0
+ word_type = "auto"
+
+ # Apply column width
+ success = set_column_width_by_position(table, col_index, word_width, word_type)
+
+ if success:
+ doc.save(filename)
+ return f"Column width set successfully for table {table_index}, column {col_index} to {width} {width_type}."
+ else:
+ return f"Failed to set column width. Check that indices are valid."
+ except Exception as e:
+ return f"Failed to set column width: {str(e)}"
+
+
+async def set_table_column_widths(filename: str, table_index: int, widths: list,
+ width_type: str = "points") -> str:
+ """Set the widths of multiple table columns.
+
+ Args:
+ filename: Path to the Word document
+ table_index: Index of the table (0-based)
+ widths: List of width values for each column
+ width_type: Width type ("points", "inches", "cm", "percent", "auto")
+ """
+ filename = ensure_docx_extension(filename)
+
+ # Ensure numeric parameters are the correct type
+ try:
+ table_index = int(table_index)
+ if width_type != "auto":
+ widths = [float(w) for w in widths]
+ except (ValueError, TypeError):
+ return "Invalid parameter: table_index must be an integer, widths must be a list of numbers"
+
+ # Validate width type
+ valid_width_types = ["points", "inches", "cm", "percent", "auto"]
+ if width_type.lower() not in valid_width_types:
+ return f"Invalid width type. Valid options: {', '.join(valid_width_types)}"
+
+ if not os.path.exists(filename):
+ return f"Document {filename} does not exist"
+
+ # Check if file is writeable
+ is_writeable, error_message = check_file_writeable(filename)
+ if not is_writeable:
+ return f"Cannot modify document: {error_message}. Consider creating a copy first."
+
+ try:
+ doc = Document(filename)
+
+ # Validate table index
+ if table_index < 0 or table_index >= len(doc.tables):
+ return f"Invalid table index. Document has {len(doc.tables)} tables (0-{len(doc.tables)-1})."
+
+ table = doc.tables[table_index]
+
+ # Convert widths and type for Word format
+ word_widths = []
+ for width in widths:
+ if width_type.lower() == "points":
+ word_widths.append(width)
+ elif width_type.lower() == "inches":
+ word_widths.append(width * 72) # 72 points per inch
+ elif width_type.lower() == "cm":
+ word_widths.append(width * 28.35) # ~28.35 points per cm
+ elif width_type.lower() == "percent":
+ word_widths.append(width)
+ else: # auto
+ word_widths.append(0)
+
+ # Determine Word type
+ if width_type.lower() == "percent":
+ word_type = "pct"
+ elif width_type.lower() == "auto":
+ word_type = "auto"
+ else:
+ word_type = "dxa"
+
+ # Apply column widths
+ success = set_column_widths(table, word_widths, word_type)
+
+ if success:
+ doc.save(filename)
+ return f"Column widths set successfully for table {table_index} with {len(widths)} columns in {width_type}."
+ else:
+ return f"Failed to set column widths."
+ except Exception as e:
+ return f"Failed to set column widths: {str(e)}"
+
+
+async def set_table_width(filename: str, table_index: int, width: float,
+ width_type: str = "points") -> str:
+ """Set the overall width of a table.
+
+ Args:
+ filename: Path to the Word document
+ table_index: Index of the table (0-based)
+ width: Table width value
+ width_type: Width type ("points", "inches", "cm", "percent", "auto")
+ """
+ filename = ensure_docx_extension(filename)
+
+ # Ensure numeric parameters are the correct type
+ try:
+ table_index = int(table_index)
+ if width_type != "auto":
+ width = float(width)
+ except (ValueError, TypeError):
+ return "Invalid parameter: table_index must be an integer, width must be a number"
+
+ # Validate width type
+ valid_width_types = ["points", "inches", "cm", "percent", "auto"]
+ if width_type.lower() not in valid_width_types:
+ return f"Invalid width type. Valid options: {', '.join(valid_width_types)}"
+
+ if not os.path.exists(filename):
+ return f"Document {filename} does not exist"
+
+ # Check if file is writeable
+ is_writeable, error_message = check_file_writeable(filename)
+ if not is_writeable:
+ return f"Cannot modify document: {error_message}. Consider creating a copy first."
+
+ try:
+ doc = Document(filename)
+
+ # Validate table index
+ if table_index < 0 or table_index >= len(doc.tables):
+ return f"Invalid table index. Document has {len(doc.tables)} tables (0-{len(doc.tables)-1})."
+
+ table = doc.tables[table_index]
+
+ # Convert width and type for Word format
+ if width_type.lower() == "points":
+ word_width = width
+ word_type = "dxa"
+ elif width_type.lower() == "inches":
+ word_width = width * 72 # 72 points per inch
+ word_type = "dxa"
+ elif width_type.lower() == "cm":
+ word_width = width * 28.35 # ~28.35 points per cm
+ word_type = "dxa"
+ elif width_type.lower() == "percent":
+ word_width = width
+ word_type = "pct"
+ else: # auto
+ word_width = 0
+ word_type = "auto"
+
+ # Apply table width
+ success = set_table_width_func(table, word_width, word_type)
+
+ if success:
+ doc.save(filename)
+ return f"Table width set successfully for table {table_index} to {width} {width_type}."
+ else:
+ return f"Failed to set table width."
+ except Exception as e:
+ return f"Failed to set table width: {str(e)}"
+
+
+async def auto_fit_table_columns(filename: str, table_index: int) -> str:
+ """Set table columns to auto-fit based on content.
+
+ Args:
+ filename: Path to the Word document
+ table_index: Index of the table (0-based)
+ """
+ filename = ensure_docx_extension(filename)
+
+ # Ensure numeric parameters are the correct type
+ try:
+ table_index = int(table_index)
+ except (ValueError, TypeError):
+ return "Invalid parameter: table_index must be an integer"
+
+ if not os.path.exists(filename):
+ return f"Document {filename} does not exist"
+
+ # Check if file is writeable
+ is_writeable, error_message = check_file_writeable(filename)
+ if not is_writeable:
+ return f"Cannot modify document: {error_message}. Consider creating a copy first."
+
+ try:
+ doc = Document(filename)
+
+ # Validate table index
+ if table_index < 0 or table_index >= len(doc.tables):
+ return f"Invalid table index. Document has {len(doc.tables)} tables (0-{len(doc.tables)-1})."
+
+ table = doc.tables[table_index]
+
+ # Apply auto-fit
+ success = auto_fit_table(table)
+
+ if success:
+ doc.save(filename)
+ return f"Table {table_index} set to auto-fit columns based on content."
+ else:
+ return f"Failed to set table auto-fit."
+ except Exception as e:
+ return f"Failed to set table auto-fit: {str(e)}"
+
+
+async def format_table_cell_text(filename: str, table_index: int, row_index: int, col_index: int,
+ text_content: Optional[str] = None, bold: Optional[bool] = None, italic: Optional[bool] = None,
+ underline: Optional[bool] = None, color: Optional[str] = None, font_size: Optional[int] = None,
+ font_name: Optional[str] = None) -> str:
+ """Format text within a specific table cell.
+
+ Args:
+ filename: Path to the Word document
+ table_index: Index of the table (0-based)
+ row_index: Row index (0-based)
+ col_index: Column index (0-based)
+ text_content: Optional new text content for the cell
+ bold: Set text bold (True/False)
+ italic: Set text italic (True/False)
+ underline: Set text underlined (True/False)
+ color: Text color (hex string like "FF0000" or color name like "red")
+ font_size: Font size in points
+ font_name: Font name/family
+ """
+ filename = ensure_docx_extension(filename)
+
+ # Ensure numeric parameters are the correct type
+ try:
+ table_index = int(table_index)
+ row_index = int(row_index)
+ col_index = int(col_index)
+ if font_size is not None:
+ font_size = int(font_size)
+ except (ValueError, TypeError):
+ return "Invalid parameter: table_index, row_index, col_index must be integers, font_size must be integer"
+
+ if not os.path.exists(filename):
+ return f"Document {filename} does not exist"
+
+ # Check if file is writeable
+ is_writeable, error_message = check_file_writeable(filename)
+ if not is_writeable:
+ return f"Cannot modify document: {error_message}. Consider creating a copy first."
+
+ try:
+ doc = Document(filename)
+
+ # Validate table index
+ if table_index < 0 or table_index >= len(doc.tables):
+ return f"Invalid table index. Document has {len(doc.tables)} tables (0-{len(doc.tables)-1})."
+
+ table = doc.tables[table_index]
+
+ # Validate row and column indices
+ if row_index < 0 or row_index >= len(table.rows):
+ return f"Invalid row index. Table has {len(table.rows)} rows (0-{len(table.rows)-1})."
+
+ if col_index < 0 or col_index >= len(table.rows[row_index].cells):
+ return f"Invalid column index. Row has {len(table.rows[row_index].cells)} cells (0-{len(table.rows[row_index].cells)-1})."
+
+ # Apply cell text formatting
+ success = format_cell_text_by_position(table, row_index, col_index, text_content,
+ bold, italic, underline, color, font_size, font_name)
+
+ if success:
+ doc.save(filename)
+ format_desc = []
+ if text_content is not None:
+ format_desc.append(f"content='{text_content[:30]}{'...' if len(text_content) > 30 else ''}'")
+ if bold is not None:
+ format_desc.append(f"bold={bold}")
+ if italic is not None:
+ format_desc.append(f"italic={italic}")
+ if underline is not None:
+ format_desc.append(f"underline={underline}")
+ if color is not None:
+ format_desc.append(f"color={color}")
+ if font_size is not None:
+ format_desc.append(f"size={font_size}pt")
+ if font_name is not None:
+ format_desc.append(f"font={font_name}")
+
+ format_str = ", ".join(format_desc) if format_desc else "no changes"
+ return f"Cell text formatted successfully in table {table_index}, cell ({row_index},{col_index}): {format_str}."
+ else:
+ return f"Failed to format cell text. Check that indices are valid."
+ except Exception as e:
+ return f"Failed to format cell text: {str(e)}"
+
+
+async def set_table_cell_padding(filename: str, table_index: int, row_index: int, col_index: int,
+ top: Optional[float] = None, bottom: Optional[float] = None, left: Optional[float] = None,
+ right: Optional[float] = None, unit: str = "points") -> str:
+ """Set padding/margins for a specific table cell.
+
+ Args:
+ filename: Path to the Word document
+ table_index: Index of the table (0-based)
+ row_index: Row index (0-based)
+ col_index: Column index (0-based)
+ top: Top padding in specified units
+ bottom: Bottom padding in specified units
+ left: Left padding in specified units
+ right: Right padding in specified units
+ unit: Unit type ("points" or "percent")
+ """
+ filename = ensure_docx_extension(filename)
+
+ # Ensure numeric parameters are the correct type
+ try:
+ table_index = int(table_index)
+ row_index = int(row_index)
+ col_index = int(col_index)
+ if top is not None:
+ top = float(top)
+ if bottom is not None:
+ bottom = float(bottom)
+ if left is not None:
+ left = float(left)
+ if right is not None:
+ right = float(right)
+ except (ValueError, TypeError):
+ return "Invalid parameter: indices must be integers, padding values must be numbers"
+
+ # Validate unit
+ valid_units = ["points", "percent"]
+ if unit.lower() not in valid_units:
+ return f"Invalid unit. Valid options: {', '.join(valid_units)}"
+
+ if not os.path.exists(filename):
+ return f"Document {filename} does not exist"
+
+ # Check if file is writeable
+ is_writeable, error_message = check_file_writeable(filename)
+ if not is_writeable:
+ return f"Cannot modify document: {error_message}. Consider creating a copy first."
+
+ try:
+ doc = Document(filename)
+
+ # Validate table index
+ if table_index < 0 or table_index >= len(doc.tables):
+ return f"Invalid table index. Document has {len(doc.tables)} tables (0-{len(doc.tables)-1})."
+
+ table = doc.tables[table_index]
+
+ # Validate row and column indices
+ if row_index < 0 or row_index >= len(table.rows):
+ return f"Invalid row index. Table has {len(table.rows)} rows (0-{len(table.rows)-1})."
+
+ if col_index < 0 or col_index >= len(table.rows[row_index].cells):
+ return f"Invalid column index. Row has {len(table.rows[row_index].cells)} cells (0-{len(table.rows[row_index].cells)-1})."
+
+ # Convert unit for Word format
+ word_unit = "dxa" if unit.lower() == "points" else "pct"
+
+ # Apply cell padding
+ success = set_cell_padding_by_position(table, row_index, col_index, top, bottom,
+ left, right, word_unit)
+
+ if success:
+ doc.save(filename)
+ padding_desc = []
+ if top is not None:
+ padding_desc.append(f"top={top}")
+ if bottom is not None:
+ padding_desc.append(f"bottom={bottom}")
+ if left is not None:
+ padding_desc.append(f"left={left}")
+ if right is not None:
+ padding_desc.append(f"right={right}")
+
+ padding_str = ", ".join(padding_desc) if padding_desc else "no padding"
+ return f"Cell padding set successfully for table {table_index}, cell ({row_index},{col_index}): {padding_str} {unit}."
+ else:
+ return f"Failed to set cell padding. Check that indices are valid."
+ except Exception as e:
+ return f"Failed to set cell padding: {str(e)}"
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/tools/protection_tools.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/tools/protection_tools.py
new file mode 100644
index 00000000..e52fd34a
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/tools/protection_tools.py
@@ -0,0 +1,275 @@
+"""
+Protection tools for Word Document Server.
+
+These tools handle document protection features such as
+password protection, restricted editing, and digital signatures.
+"""
+import os
+import hashlib
+import datetime
+import io
+from typing import List, Optional, Dict, Any
+from docx import Document
+import msoffcrypto
+
+from word_document_server.utils.file_utils import check_file_writeable, ensure_docx_extension
+
+
+
+from word_document_server.core.protection import (
+ add_protection_info,
+ verify_document_protection,
+ create_signature_info
+)
+
+
+async def protect_document(filename: str, password: str) -> str:
+ """Add password protection to a Word document.
+
+ Args:
+ filename: Path to the Word document
+ password: Password to protect the document with
+ """
+ filename = ensure_docx_extension(filename)
+
+ if not os.path.exists(filename):
+ return f"Document {filename} does not exist"
+
+ # Check if file is writeable
+ is_writeable, error_message = check_file_writeable(filename)
+ if not is_writeable:
+ return f"Cannot protect document: {error_message}"
+
+ try:
+ # Read the original file content
+ with open(filename, "rb") as infile:
+ original_data = infile.read()
+
+ # Create an msoffcrypto file object from the original data
+ file = msoffcrypto.OfficeFile(io.BytesIO(original_data))
+ file.load_key(password=password) # Set the password for encryption
+
+ # Encrypt the data into an in-memory buffer
+ encrypted_data_io = io.BytesIO()
+
+ file.encrypt(password=password, outfile=encrypted_data_io)
+
+ # Overwrite the original file with the encrypted data
+ with open(filename, "wb") as outfile:
+ outfile.write(encrypted_data_io.getvalue())
+
+
+ base_path, _ = os.path.splitext(filename)
+ metadata_path = f"{base_path}.protection"
+ if os.path.exists(metadata_path):
+ os.remove(metadata_path)
+
+ return f"Document {filename} encrypted successfully with password."
+
+ except Exception as e:
+ # Attempt to restore original file content on failure
+ try:
+ if 'original_data' in locals():
+ with open(filename, "wb") as outfile:
+ outfile.write(original_data)
+ return f"Failed to encrypt document {filename}: {str(e)}. Original file restored."
+ else:
+ return f"Failed to encrypt document {filename}: {str(e)}. Could not restore original file."
+ except Exception as restore_e:
+ return f"Failed to encrypt document {filename}: {str(e)}. Also failed to restore original file: {str(restore_e)}"
+
+
+async def add_restricted_editing(filename: str, password: str, editable_sections: List[str]) -> str:
+ """Add restricted editing to a Word document, allowing editing only in specified sections.
+
+ Args:
+ filename: Path to the Word document
+ password: Password to protect the document with
+ editable_sections: List of section names that can be edited
+ """
+ filename = ensure_docx_extension(filename)
+
+ if not os.path.exists(filename):
+ return f"Document {filename} does not exist"
+
+ # Check if file is writeable
+ is_writeable, error_message = check_file_writeable(filename)
+ if not is_writeable:
+ return f"Cannot protect document: {error_message}"
+
+ try:
+ # Hash the password for security
+ password_hash = hashlib.sha256(password.encode()).hexdigest()
+
+ # Add protection info to metadata
+ success = add_protection_info(
+ filename,
+ protection_type="restricted",
+ password_hash=password_hash,
+ sections=editable_sections
+ )
+
+ if not editable_sections:
+ return "No editable sections specified. Document will be fully protected."
+
+ if success:
+ return f"Document {filename} protected with restricted editing. Editable sections: {', '.join(editable_sections)}"
+ else:
+ return f"Failed to protect document {filename} with restricted editing"
+ except Exception as e:
+ return f"Failed to add restricted editing: {str(e)}"
+
+async def add_digital_signature(filename: str, signer_name: str, reason: Optional[str] = None) -> str:
+ """Add a digital signature to a Word document.
+
+ Args:
+ filename: Path to the Word document
+ signer_name: Name of the person signing the document
+ reason: Optional reason for signing
+ """
+ filename = ensure_docx_extension(filename)
+
+ if not os.path.exists(filename):
+ return f"Document {filename} does not exist"
+
+ # Check if file is writeable
+ is_writeable, error_message = check_file_writeable(filename)
+ if not is_writeable:
+ return f"Cannot add signature to document: {error_message}"
+
+ try:
+ doc = Document(filename)
+
+ # Create signature info
+ signature_info = create_signature_info(doc, signer_name, reason)
+
+ # Add protection info to metadata
+ success = add_protection_info(
+ filename,
+ protection_type="signature",
+ password_hash="", # No password for signature-only
+ signature_info=signature_info
+ )
+
+ if success:
+ # Add a visible signature block to the document
+ doc.add_paragraph("").add_run() # Add empty paragraph for spacing
+ signature_para = doc.add_paragraph()
+ signature_para.add_run(f"Digitally signed by: {signer_name}").bold = True
+ if reason:
+ signature_para.add_run(f"\nReason: {reason}")
+ signature_para.add_run(f"\nDate: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
+ signature_para.add_run(f"\nSignature ID: {signature_info['content_hash'][:8]}")
+
+ # Save the document with the visible signature
+ doc.save(filename)
+
+ return f"Digital signature added to document {filename}"
+ else:
+ return f"Failed to add digital signature to document {filename}"
+ except Exception as e:
+ return f"Failed to add digital signature: {str(e)}"
+
+async def verify_document(filename: str, password: Optional[str] = None) -> str:
+ """Verify document protection and/or digital signature.
+
+ Args:
+ filename: Path to the Word document
+ password: Optional password to verify
+ """
+ filename = ensure_docx_extension(filename)
+
+ if not os.path.exists(filename):
+ return f"Document {filename} does not exist"
+
+ try:
+ # Verify document protection
+ is_verified, message = verify_document_protection(filename, password)
+
+ if not is_verified and password:
+ return f"Document verification failed: {message}"
+
+ # If document has a digital signature, verify content integrity
+ base_path, _ = os.path.splitext(filename)
+ metadata_path = f"{base_path}.protection"
+
+ if os.path.exists(metadata_path):
+ try:
+ import json
+ with open(metadata_path, 'r') as f:
+ protection_data = json.load(f)
+
+ if protection_data.get("type") == "signature":
+ # Get the original content hash
+ signature_info = protection_data.get("signature", {})
+ original_hash = signature_info.get("content_hash")
+
+ if original_hash:
+ # Calculate current content hash
+ doc = Document(filename)
+ text_content = "\n".join([p.text for p in doc.paragraphs])
+ current_hash = hashlib.sha256(text_content.encode()).hexdigest()
+
+ # Compare hashes
+ if current_hash != original_hash:
+ return f"Document has been modified since it was signed by {signature_info.get('signer')}"
+ else:
+ return f"Document signature is valid. Signed by {signature_info.get('signer')} on {signature_info.get('timestamp')}"
+ except Exception as e:
+ return f"Error verifying signature: {str(e)}"
+
+ return message
+ except Exception as e:
+ return f"Failed to verify document: {str(e)}"
+
+async def unprotect_document(filename: str, password: str) -> str:
+ """Remove password protection from a Word document.
+
+ Args:
+ filename: Path to the Word document
+ password: Password that was used to protect the document
+ """
+ filename = ensure_docx_extension(filename)
+
+ if not os.path.exists(filename):
+ return f"Document {filename} does not exist"
+
+ # Check if file is writeable
+ is_writeable, error_message = check_file_writeable(filename)
+ if not is_writeable:
+ return f"Cannot modify document: {error_message}"
+
+ try:
+ # Read the encrypted file content
+ with open(filename, "rb") as infile:
+ encrypted_data = infile.read()
+
+ # Create an msoffcrypto file object from the encrypted data
+ file = msoffcrypto.OfficeFile(io.BytesIO(encrypted_data))
+ file.load_key(password=password) # Set the password for decryption
+
+ # Decrypt the data into an in-memory buffer
+ decrypted_data_io = io.BytesIO()
+ file.decrypt(outfile=decrypted_data_io) # Pass the buffer as the 'outfile' argument
+
+ # Overwrite the original file with the decrypted data
+ with open(filename, "wb") as outfile:
+ outfile.write(decrypted_data_io.getvalue())
+
+ return f"Document {filename} decrypted successfully."
+
+ except msoffcrypto.exceptions.InvalidKeyError:
+ return f"Failed to decrypt document {filename}: Incorrect password."
+ except msoffcrypto.exceptions.InvalidFormatError:
+ return f"Failed to decrypt document {filename}: File is not encrypted or is not a supported Office format."
+ except Exception as e:
+ # Attempt to restore encrypted file content on failure
+ try:
+ if 'encrypted_data' in locals():
+ with open(filename, "wb") as outfile:
+ outfile.write(encrypted_data)
+ return f"Failed to decrypt document {filename}: {str(e)}. Encrypted file restored."
+ else:
+ return f"Failed to decrypt document {filename}: {str(e)}. Could not restore encrypted file."
+ except Exception as restore_e:
+ return f"Failed to decrypt document {filename}: {str(e)}. Also failed to restore encrypted file: {str(restore_e)}"
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/utils/__init__.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/utils/__init__.py
new file mode 100644
index 00000000..1146a586
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/utils/__init__.py
@@ -0,0 +1,8 @@
+"""
+Utility functions for the Word Document Server.
+
+This package contains utility modules for file operations and document handling.
+"""
+
+from word_document_server.utils.file_utils import check_file_writeable, create_document_copy, ensure_docx_extension
+from word_document_server.utils.document_utils import get_document_properties, extract_document_text, get_document_structure, find_paragraph_by_text, find_and_replace_text
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/utils/document_utils.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/utils/document_utils.py
new file mode 100644
index 00000000..c3681c08
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/utils/document_utils.py
@@ -0,0 +1,618 @@
+"""
+Document utility functions for Word Document Server.
+"""
+import json
+from typing import Dict, List, Any
+from docx import Document
+from docx.oxml.table import CT_Tbl
+from docx.oxml.text.paragraph import CT_P
+from docx.oxml.ns import qn
+from docx.oxml import OxmlElement
+
+
+def get_document_properties(doc_path: str) -> Dict[str, Any]:
+ """Get properties of a Word document."""
+ import os
+ if not os.path.exists(doc_path):
+ return {"error": f"Document {doc_path} does not exist"}
+
+ try:
+ doc = Document(doc_path)
+ core_props = doc.core_properties
+
+ return {
+ "title": core_props.title or "",
+ "author": core_props.author or "",
+ "subject": core_props.subject or "",
+ "keywords": core_props.keywords or "",
+ "created": str(core_props.created) if core_props.created else "",
+ "modified": str(core_props.modified) if core_props.modified else "",
+ "last_modified_by": core_props.last_modified_by or "",
+ "revision": core_props.revision or 0,
+ "page_count": len(doc.sections),
+ "word_count": sum(len(paragraph.text.split()) for paragraph in doc.paragraphs),
+ "paragraph_count": len(doc.paragraphs),
+ "table_count": len(doc.tables)
+ }
+ except Exception as e:
+ return {"error": f"Failed to get document properties: {str(e)}"}
+
+
+def extract_document_text(doc_path: str) -> str:
+ """Extract all text from a Word document."""
+ import os
+ if not os.path.exists(doc_path):
+ return f"Document {doc_path} does not exist"
+
+ try:
+ doc = Document(doc_path)
+ text = []
+
+ for paragraph in doc.paragraphs:
+ text.append(paragraph.text)
+
+ for table in doc.tables:
+ for row in table.rows:
+ for cell in row.cells:
+ for paragraph in cell.paragraphs:
+ text.append(paragraph.text)
+
+ return "\n".join(text)
+ except Exception as e:
+ return f"Failed to extract text: {str(e)}"
+
+
+def get_document_structure(doc_path: str) -> Dict[str, Any]:
+ """Get the structure of a Word document."""
+ import os
+ if not os.path.exists(doc_path):
+ return {"error": f"Document {doc_path} does not exist"}
+
+ try:
+ doc = Document(doc_path)
+ structure = {
+ "paragraphs": [],
+ "tables": []
+ }
+
+ # Get paragraphs
+ for i, para in enumerate(doc.paragraphs):
+ structure["paragraphs"].append({
+ "index": i,
+ "text": para.text[:100] + ("..." if len(para.text) > 100 else ""),
+ "style": para.style.name if para.style else "Normal"
+ })
+
+ # Get tables
+ for i, table in enumerate(doc.tables):
+ table_data = {
+ "index": i,
+ "rows": len(table.rows),
+ "columns": len(table.columns),
+ "preview": []
+ }
+
+ # Get sample of table data
+ max_rows = min(3, len(table.rows))
+ for row_idx in range(max_rows):
+ row_data = []
+ max_cols = min(3, len(table.columns))
+ for col_idx in range(max_cols):
+ try:
+ cell_text = table.cell(row_idx, col_idx).text
+ row_data.append(cell_text[:20] + ("..." if len(cell_text) > 20 else ""))
+ except IndexError:
+ row_data.append("N/A")
+ table_data["preview"].append(row_data)
+
+ structure["tables"].append(table_data)
+
+ return structure
+ except Exception as e:
+ return {"error": f"Failed to get document structure: {str(e)}"}
+
+
+def find_paragraph_by_text(doc, text, partial_match=False):
+ """
+ Find paragraphs containing specific text.
+
+ Args:
+ doc: Document object
+ text: Text to search for
+ partial_match: If True, matches paragraphs containing the text; if False, matches exact text
+
+ Returns:
+ List of paragraph indices that match the criteria
+ """
+ matching_paragraphs = []
+
+ for i, para in enumerate(doc.paragraphs):
+ if partial_match and text in para.text:
+ matching_paragraphs.append(i)
+ elif not partial_match and para.text == text:
+ matching_paragraphs.append(i)
+
+ return matching_paragraphs
+
+
+def find_and_replace_text(doc, old_text, new_text):
+ """
+ Find and replace text throughout the document, skipping Table of Contents (TOC) paragraphs.
+
+ Args:
+ doc: Document object
+ old_text: Text to find
+ new_text: Text to replace with
+
+ Returns:
+ Number of replacements made
+ """
+ count = 0
+
+ # Search in paragraphs
+ for para in doc.paragraphs:
+ # Skip TOC paragraphs
+ if para.style and para.style.name.startswith("TOC"):
+ continue
+ if old_text in para.text:
+ for run in para.runs:
+ if old_text in run.text:
+ run.text = run.text.replace(old_text, new_text)
+ count += 1
+
+ # Search in tables
+ for table in doc.tables:
+ for row in table.rows:
+ for cell in row.cells:
+ for para in cell.paragraphs:
+ # Skip TOC paragraphs in tables
+ if para.style and para.style.name.startswith("TOC"):
+ continue
+ if old_text in para.text:
+ for run in para.runs:
+ if old_text in run.text:
+ run.text = run.text.replace(old_text, new_text)
+ count += 1
+
+ return count
+
+
+def get_document_xml(doc_path: str) -> str:
+ """Extract and return the raw XML structure of the Word document (word/document.xml)."""
+ import os
+ import zipfile
+ if not os.path.exists(doc_path):
+ return f"Document {doc_path} does not exist"
+ try:
+ with zipfile.ZipFile(doc_path) as docx_zip:
+ with docx_zip.open('word/document.xml') as xml_file:
+ return xml_file.read().decode('utf-8')
+ except Exception as e:
+ return f"Failed to extract XML: {str(e)}"
+
+
+def insert_header_near_text(doc_path: str, target_text: str = None, header_title: str = "", position: str = 'after', header_style: str = 'Heading 1', target_paragraph_index: int = None) -> str:
+ """Insert a header (with specified style) before or after the target paragraph. Specify by text or paragraph index. Skips TOC paragraphs in text search."""
+ import os
+ from docx import Document
+ if not os.path.exists(doc_path):
+ return f"Document {doc_path} does not exist"
+ try:
+ doc = Document(doc_path)
+ found = False
+ para = None
+ if target_paragraph_index is not None:
+ if target_paragraph_index < 0 or target_paragraph_index >= len(doc.paragraphs):
+ return f"Invalid target_paragraph_index: {target_paragraph_index}. Document has {len(doc.paragraphs)} paragraphs."
+ para = doc.paragraphs[target_paragraph_index]
+ found = True
+ else:
+ for i, p in enumerate(doc.paragraphs):
+ # Skip TOC paragraphs
+ if p.style and p.style.name.lower().startswith("toc"):
+ continue
+ if target_text and target_text in p.text:
+ para = p
+ found = True
+ break
+ if not found or para is None:
+ return f"Target paragraph not found (by index or text). (TOC paragraphs are skipped in text search)"
+ # Save anchor index before insertion
+ if target_paragraph_index is not None:
+ anchor_index = target_paragraph_index
+ else:
+ anchor_index = None
+ for i, p in enumerate(doc.paragraphs):
+ if p is para:
+ anchor_index = i
+ break
+ new_para = doc.add_paragraph(header_title, style=header_style)
+ if position == 'before':
+ para._element.addprevious(new_para._element)
+ else:
+ para._element.addnext(new_para._element)
+ doc.save(doc_path)
+ if anchor_index is not None:
+ return f"Header '{header_title}' (style: {header_style}) inserted {position} paragraph (index {anchor_index})."
+ else:
+ return f"Header '{header_title}' (style: {header_style}) inserted {position} the target paragraph."
+ except Exception as e:
+ return f"Failed to insert header: {str(e)}"
+
+
+def insert_line_or_paragraph_near_text(doc_path: str, target_text: str = None, line_text: str = "", position: str = 'after', line_style: str = None, target_paragraph_index: int = None) -> str:
+ """
+ Insert a new line or paragraph (with specified or matched style) before or after the target paragraph.
+ You can specify the target by text (first match) or by paragraph index.
+ Skips paragraphs whose style name starts with 'TOC' if using text search.
+ """
+ import os
+ from docx import Document
+ if not os.path.exists(doc_path):
+ return f"Document {doc_path} does not exist"
+ try:
+ doc = Document(doc_path)
+ found = False
+ para = None
+ if target_paragraph_index is not None:
+ if target_paragraph_index < 0 or target_paragraph_index >= len(doc.paragraphs):
+ return f"Invalid target_paragraph_index: {target_paragraph_index}. Document has {len(doc.paragraphs)} paragraphs."
+ para = doc.paragraphs[target_paragraph_index]
+ found = True
+ else:
+ for i, p in enumerate(doc.paragraphs):
+ # Skip TOC paragraphs
+ if p.style and p.style.name.lower().startswith("toc"):
+ continue
+ if target_text and target_text in p.text:
+ para = p
+ found = True
+ break
+ if not found or para is None:
+ return f"Target paragraph not found (by index or text). (TOC paragraphs are skipped in text search)"
+ # Save anchor index before insertion
+ if target_paragraph_index is not None:
+ anchor_index = target_paragraph_index
+ else:
+ anchor_index = None
+ for i, p in enumerate(doc.paragraphs):
+ if p is para:
+ anchor_index = i
+ break
+ # Determine style: use provided or match target
+ style = line_style if line_style else para.style
+ new_para = doc.add_paragraph(line_text, style=style)
+ if position == 'before':
+ para._element.addprevious(new_para._element)
+ else:
+ para._element.addnext(new_para._element)
+ doc.save(doc_path)
+ if anchor_index is not None:
+ return f"Line/paragraph inserted {position} paragraph (index {anchor_index}) with style '{style}'."
+ else:
+ return f"Line/paragraph inserted {position} the target paragraph with style '{style}'."
+ except Exception as e:
+ return f"Failed to insert line/paragraph: {str(e)}"
+
+
+def add_bullet_numbering(paragraph, num_id=1, level=0):
+ """
+ Add bullet/numbering XML to a paragraph.
+
+ Args:
+ paragraph: python-docx Paragraph object
+ num_id: Numbering definition ID (1=bullets, 2=numbers, etc.)
+ level: Indentation level (0=first level, 1=second level, etc.)
+
+ Returns:
+ The modified paragraph
+ """
+ # Get or create paragraph properties
+ pPr = paragraph._element.get_or_add_pPr()
+
+ # Remove existing numPr if any (to avoid duplicates)
+ existing_numPr = pPr.find(qn('w:numPr'))
+ if existing_numPr is not None:
+ pPr.remove(existing_numPr)
+
+ # Create numbering properties element
+ numPr = OxmlElement('w:numPr')
+
+ # Set indentation level
+ ilvl = OxmlElement('w:ilvl')
+ ilvl.set(qn('w:val'), str(level))
+ numPr.append(ilvl)
+
+ # Set numbering definition ID
+ numId = OxmlElement('w:numId')
+ numId.set(qn('w:val'), str(num_id))
+ numPr.append(numId)
+
+ # Add to paragraph properties
+ pPr.append(numPr)
+
+ return paragraph
+
+
+def insert_numbered_list_near_text(doc_path: str, target_text: str = None, list_items: list = None, position: str = 'after', target_paragraph_index: int = None, bullet_type: str = 'bullet') -> str:
+ """
+ Insert a bulleted or numbered list before or after the target paragraph. Specify by text or paragraph index. Skips TOC paragraphs in text search.
+ Args:
+ doc_path: Path to the Word document
+ target_text: Text to search for in paragraphs (optional if using index)
+ list_items: List of strings, each as a list item
+ position: 'before' or 'after' (default: 'after')
+ target_paragraph_index: Optional paragraph index to use as anchor
+ bullet_type: 'bullet' for bullets (•), 'number' for numbers (1,2,3) (default: 'bullet')
+ Returns:
+ Status message
+ """
+ import os
+ from docx import Document
+ if not os.path.exists(doc_path):
+ return f"Document {doc_path} does not exist"
+ try:
+ doc = Document(doc_path)
+ found = False
+ para = None
+ if target_paragraph_index is not None:
+ if target_paragraph_index < 0 or target_paragraph_index >= len(doc.paragraphs):
+ return f"Invalid target_paragraph_index: {target_paragraph_index}. Document has {len(doc.paragraphs)} paragraphs."
+ para = doc.paragraphs[target_paragraph_index]
+ found = True
+ else:
+ for i, p in enumerate(doc.paragraphs):
+ # Skip TOC paragraphs
+ if p.style and p.style.name.lower().startswith("toc"):
+ continue
+ if target_text and target_text in p.text:
+ para = p
+ found = True
+ break
+ if not found or para is None:
+ return f"Target paragraph not found (by index or text). (TOC paragraphs are skipped in text search)"
+ # Save anchor index before insertion
+ if target_paragraph_index is not None:
+ anchor_index = target_paragraph_index
+ else:
+ anchor_index = None
+ for i, p in enumerate(doc.paragraphs):
+ if p is para:
+ anchor_index = i
+ break
+ # Determine numbering ID based on bullet_type
+ num_id = 1 if bullet_type == 'bullet' else 2
+
+ # Use ListParagraph style for proper list formatting
+ style_name = None
+ for candidate in ['List Paragraph', 'ListParagraph', 'Normal']:
+ try:
+ _ = doc.styles[candidate]
+ style_name = candidate
+ break
+ except KeyError:
+ continue
+ if not style_name:
+ style_name = None # fallback to default
+
+ new_paras = []
+ for item in (list_items or []):
+ p = doc.add_paragraph(item, style=style_name)
+ # Add bullet numbering XML - this is the fix!
+ add_bullet_numbering(p, num_id=num_id, level=0)
+ new_paras.append(p)
+ # Move the new paragraphs to the correct position
+ for p in reversed(new_paras):
+ if position == 'before':
+ para._element.addprevious(p._element)
+ else:
+ para._element.addnext(p._element)
+ doc.save(doc_path)
+ list_type = "bulleted" if bullet_type == 'bullet' else "numbered"
+ if anchor_index is not None:
+ return f"{list_type.capitalize()} list with {len(new_paras)} items inserted {position} paragraph (index {anchor_index})."
+ else:
+ return f"{list_type.capitalize()} list with {len(new_paras)} items inserted {position} the target paragraph."
+ except Exception as e:
+ return f"Failed to insert numbered list: {str(e)}"
+
+
+def is_toc_paragraph(para):
+ """Devuelve True si el párrafo tiene un estilo de tabla de contenido (TOC)."""
+ return para.style and para.style.name.upper().startswith("TOC")
+
+
+def is_heading_paragraph(para):
+ """Devuelve True si el párrafo tiene un estilo de encabezado (Heading 1, Heading 2, etc)."""
+ return para.style and para.style.name.lower().startswith("heading")
+
+
+# --- Helper: Get style name from a element ---
+def get_paragraph_style(el):
+ from docx.oxml.ns import qn
+ pPr = el.find(qn('w:pPr'))
+ if pPr is not None:
+ pStyle = pPr.find(qn('w:pStyle'))
+ if pStyle is not None and 'w:val' in pStyle.attrib:
+ return pStyle.attrib['w:val']
+ return None
+
+# --- Main: Delete everything under a header until next heading/TOC ---
+def delete_block_under_header(doc, header_text):
+ """
+ Remove all elements (paragraphs, tables, etc.) after the header (by text) and before the next heading/TOC (by style).
+ Returns: (header_element, elements_removed)
+ """
+ # Find the header paragraph by text (like delete_paragraph finds by index)
+ header_para = None
+ header_idx = None
+
+ for i, para in enumerate(doc.paragraphs):
+ if para.text.strip().lower() == header_text.strip().lower():
+ header_para = para
+ header_idx = i
+ break
+
+ if header_para is None:
+ return None, 0
+
+ # Find the next heading/TOC paragraph to determine the end of the block
+ end_idx = None
+ for i in range(header_idx + 1, len(doc.paragraphs)):
+ para = doc.paragraphs[i]
+ if para.style and para.style.name.lower().startswith(('heading', 'título', 'toc')):
+ end_idx = i
+ break
+
+ # If no next heading found, delete until end of document
+ if end_idx is None:
+ end_idx = len(doc.paragraphs)
+
+ # Remove paragraphs by index (like delete_paragraph does)
+ removed_count = 0
+ for i in range(header_idx + 1, end_idx):
+ if i < len(doc.paragraphs): # Safety check
+ para = doc.paragraphs[header_idx + 1] # Always remove the first paragraph after header
+ p = para._p
+ p.getparent().remove(p)
+ removed_count += 1
+
+ return header_para._p, removed_count
+
+# --- Usage in replace_paragraph_block_below_header ---
+def replace_paragraph_block_below_header(
+ doc_path: str,
+ header_text: str,
+ new_paragraphs: list,
+ detect_block_end_fn=None,
+ new_paragraph_style: str = None
+) -> str:
+ """
+ Reemplaza todo el contenido debajo de una cabecera (por texto), hasta el siguiente encabezado/TOC (por estilo).
+ """
+ from docx import Document
+ import os
+ if not os.path.exists(doc_path):
+ return f"Document {doc_path} not found."
+
+ doc = Document(doc_path)
+
+ # Find the header paragraph first
+ header_para = None
+ header_idx = None
+ for i, para in enumerate(doc.paragraphs):
+ para_text = para.text.strip().lower()
+ is_toc = is_toc_paragraph(para)
+ if para_text == header_text.strip().lower() and not is_toc:
+ header_para = para
+ header_idx = i
+ break
+
+ if header_para is None:
+ return f"Header '{header_text}' not found in document."
+
+ # Delete everything under the header using the same document instance
+ header_el, removed_count = delete_block_under_header(doc, header_text)
+
+ # Now insert new paragraphs after the header (which should still be in the document)
+ style_to_use = new_paragraph_style or "Normal"
+
+ # Find the header again after deletion (it should still be there)
+ current_para = header_para
+ for text in new_paragraphs:
+ new_para = doc.add_paragraph(text, style=style_to_use)
+ current_para._element.addnext(new_para._element)
+ current_para = new_para
+
+ doc.save(doc_path)
+ return f"Replaced content under '{header_text}' with {len(new_paragraphs)} paragraph(s), style: {style_to_use}, removed {removed_count} elements."
+
+
+def replace_block_between_manual_anchors(
+ doc_path: str,
+ start_anchor_text: str,
+ new_paragraphs: list,
+ end_anchor_text: str = None,
+ match_fn=None,
+ new_paragraph_style: str = None
+) -> str:
+ """
+ Replace all content (paragraphs, tables, etc.) between start_anchor_text and end_anchor_text (or next logical header if not provided).
+ If end_anchor_text is None, deletes until next visually distinct paragraph (bold, all caps, or different font size), or end of document.
+ Inserts new_paragraphs after the start anchor.
+ """
+ from docx import Document
+ import os
+ if not os.path.exists(doc_path):
+ return f"Document {doc_path} not found."
+ doc = Document(doc_path)
+ body = doc.element.body
+ elements = list(body)
+ start_idx = None
+ end_idx = None
+ # Find start anchor
+ for i, el in enumerate(elements):
+ if el.tag == CT_P.tag:
+ p_text = "".join([node.text or '' for node in el.iter() if node.tag.endswith('}t')]).strip()
+ if match_fn:
+ if match_fn(p_text, el):
+ start_idx = i
+ break
+ elif p_text == start_anchor_text.strip():
+ start_idx = i
+ break
+ if start_idx is None:
+ return f"Start anchor '{start_anchor_text}' not found."
+ # Find end anchor
+ if end_anchor_text:
+ for i in range(start_idx + 1, len(elements)):
+ el = elements[i]
+ if el.tag == CT_P.tag:
+ p_text = "".join([node.text or '' for node in el.iter() if node.tag.endswith('}t')]).strip()
+ if match_fn:
+ if match_fn(p_text, el, is_end=True):
+ end_idx = i
+ break
+ elif p_text == end_anchor_text.strip():
+ end_idx = i
+ break
+ else:
+ # Heuristic: next visually distinct paragraph (bold, all caps, or different font size), or end of document
+ for i in range(start_idx + 1, len(elements)):
+ el = elements[i]
+ if el.tag == CT_P.tag:
+ # Check for bold, all caps, or font size
+ runs = [node for node in el.iter() if node.tag.endswith('}r')]
+ for run in runs:
+ rpr = run.find(qn('w:rPr'))
+ if rpr is not None:
+ if rpr.find(qn('w:b')) is not None or rpr.find(qn('w:caps')) is not None or rpr.find(qn('w:sz')) is not None:
+ end_idx = i
+ break
+ if end_idx is not None:
+ break
+ # Mark elements for removal
+ to_remove = []
+ for i in range(start_idx + 1, end_idx if end_idx is not None else len(elements)):
+ to_remove.append(elements[i])
+ for el in to_remove:
+ body.remove(el)
+ doc.save(doc_path)
+ # Reload and find start anchor for insertion
+ doc = Document(doc_path)
+ paras = doc.paragraphs
+ anchor_idx = None
+ for i, para in enumerate(paras):
+ if para.text.strip() == start_anchor_text.strip():
+ anchor_idx = i
+ break
+ if anchor_idx is None:
+ return f"Start anchor '{start_anchor_text}' not found after deletion (unexpected)."
+ anchor_para = paras[anchor_idx]
+ style_to_use = new_paragraph_style or "Normal"
+ for text in new_paragraphs:
+ new_para = doc.add_paragraph(text, style=style_to_use)
+ anchor_para._element.addnext(new_para._element)
+ anchor_para = new_para
+ doc.save(doc_path)
+ return f"Replaced content between '{start_anchor_text}' and '{end_anchor_text or 'next logical header'}' with {len(new_paragraphs)} paragraph(s), style: {style_to_use}, removed {len(to_remove)} elements."
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/utils/extended_document_utils.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/utils/extended_document_utils.py
new file mode 100644
index 00000000..007d5ce1
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/utils/extended_document_utils.py
@@ -0,0 +1,165 @@
+"""
+Extended document utilities for Word Document Server.
+"""
+from typing import Dict, List, Any, Tuple
+from docx import Document
+
+
+def get_paragraph_text(doc_path: str, paragraph_index: int) -> Dict[str, Any]:
+ """
+ Get text from a specific paragraph in a Word document.
+
+ Args:
+ doc_path: Path to the Word document
+ paragraph_index: Index of the paragraph to extract (0-based)
+
+ Returns:
+ Dictionary with paragraph text and metadata
+ """
+ import os
+ if not os.path.exists(doc_path):
+ return {"error": f"Document {doc_path} does not exist"}
+
+ try:
+ doc = Document(doc_path)
+
+ # Check if paragraph index is valid
+ if paragraph_index < 0 or paragraph_index >= len(doc.paragraphs):
+ return {"error": f"Invalid paragraph index: {paragraph_index}. Document has {len(doc.paragraphs)} paragraphs."}
+
+ paragraph = doc.paragraphs[paragraph_index]
+
+ return {
+ "index": paragraph_index,
+ "text": paragraph.text,
+ "style": paragraph.style.name if paragraph.style else "Normal",
+ "is_heading": paragraph.style.name.startswith("Heading") if paragraph.style else False
+ }
+ except Exception as e:
+ return {"error": f"Failed to get paragraph text: {str(e)}"}
+
+
+def find_text(doc_path: str, text_to_find: str, match_case: bool = True, whole_word: bool = False) -> Dict[str, Any]:
+ """
+ Find all occurrences of specific text in a Word document.
+
+ Args:
+ doc_path: Path to the Word document
+ text_to_find: Text to search for
+ match_case: Whether to perform case-sensitive search
+ whole_word: Whether to match whole words only
+
+ Returns:
+ Dictionary with search results
+ """
+ import os
+ if not os.path.exists(doc_path):
+ return {"error": f"Document {doc_path} does not exist"}
+
+ if not text_to_find:
+ return {"error": "Search text cannot be empty"}
+
+ try:
+ doc = Document(doc_path)
+ results = {
+ "query": text_to_find,
+ "match_case": match_case,
+ "whole_word": whole_word,
+ "occurrences": [],
+ "total_count": 0
+ }
+
+ # Search in paragraphs
+ for i, para in enumerate(doc.paragraphs):
+ # Prepare text for comparison
+ para_text = para.text
+ search_text = text_to_find
+
+ if not match_case:
+ para_text = para_text.lower()
+ search_text = search_text.lower()
+
+ # Find all occurrences (simple implementation)
+ start_pos = 0
+ while True:
+ if whole_word:
+ # For whole word search, we need to check word boundaries
+ words = para_text.split()
+ found = False
+ for word_idx, word in enumerate(words):
+ if (word == search_text or
+ (not match_case and word.lower() == search_text.lower())):
+ results["occurrences"].append({
+ "paragraph_index": i,
+ "position": word_idx,
+ "context": para.text[:100] + ("..." if len(para.text) > 100 else "")
+ })
+ results["total_count"] += 1
+ found = True
+
+ # Break after checking all words
+ break
+ else:
+ # For substring search
+ pos = para_text.find(search_text, start_pos)
+ if pos == -1:
+ break
+
+ results["occurrences"].append({
+ "paragraph_index": i,
+ "position": pos,
+ "context": para.text[:100] + ("..." if len(para.text) > 100 else "")
+ })
+ results["total_count"] += 1
+ start_pos = pos + len(search_text)
+
+ # Search in tables
+ for table_idx, table in enumerate(doc.tables):
+ for row_idx, row in enumerate(table.rows):
+ for col_idx, cell in enumerate(row.cells):
+ for para_idx, para in enumerate(cell.paragraphs):
+ # Prepare text for comparison
+ para_text = para.text
+ search_text = text_to_find
+
+ if not match_case:
+ para_text = para_text.lower()
+ search_text = search_text.lower()
+
+ # Find all occurrences (simple implementation)
+ start_pos = 0
+ while True:
+ if whole_word:
+ # For whole word search, check word boundaries
+ words = para_text.split()
+ found = False
+ for word_idx, word in enumerate(words):
+ if (word == search_text or
+ (not match_case and word.lower() == search_text.lower())):
+ results["occurrences"].append({
+ "location": f"Table {table_idx}, Row {row_idx}, Column {col_idx}",
+ "position": word_idx,
+ "context": para.text[:100] + ("..." if len(para.text) > 100 else "")
+ })
+ results["total_count"] += 1
+ found = True
+
+ # Break after checking all words
+ break
+ else:
+ # For substring search
+ pos = para_text.find(search_text, start_pos)
+ if pos == -1:
+ break
+
+ results["occurrences"].append({
+ "location": f"Table {table_idx}, Row {row_idx}, Column {col_idx}",
+ "position": pos,
+ "context": para.text[:100] + ("..." if len(para.text) > 100 else "")
+ })
+ results["total_count"] += 1
+ start_pos = pos + len(search_text)
+
+ return results
+ except Exception as e:
+ return {"error": f"Failed to search for text: {str(e)}"}
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/utils/file_utils.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/utils/file_utils.py
new file mode 100644
index 00000000..7974707c
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_document_server/utils/file_utils.py
@@ -0,0 +1,85 @@
+"""
+File utility functions for Word Document Server.
+"""
+import os
+from typing import Tuple, Optional
+import shutil
+
+
+def check_file_writeable(filepath: str) -> Tuple[bool, str]:
+ """
+ Check if a file can be written to.
+
+ Args:
+ filepath: Path to the file
+
+ Returns:
+ Tuple of (is_writeable, error_message)
+ """
+ # If file doesn't exist, check if directory is writeable
+ if not os.path.exists(filepath):
+ directory = os.path.dirname(filepath)
+ # If no directory is specified (empty string), use current directory
+ if directory == '':
+ directory = '.'
+ if not os.path.exists(directory):
+ return False, f"Directory {directory} does not exist"
+ if not os.access(directory, os.W_OK):
+ return False, f"Directory {directory} is not writeable"
+ return True, ""
+
+ # If file exists, check if it's writeable
+ if not os.access(filepath, os.W_OK):
+ return False, f"File {filepath} is not writeable (permission denied)"
+
+ # Try to open the file for writing to see if it's locked
+ try:
+ with open(filepath, 'a'):
+ pass
+ return True, ""
+ except IOError as e:
+ return False, f"File {filepath} is not writeable: {str(e)}"
+ except Exception as e:
+ return False, f"Unknown error checking file permissions: {str(e)}"
+
+
+def create_document_copy(source_path: str, dest_path: Optional[str] = None) -> Tuple[bool, str, Optional[str]]:
+ """
+ Create a copy of a document.
+
+ Args:
+ source_path: Path to the source document
+ dest_path: Optional path for the new document. If not provided, will use source_path + '_copy.docx'
+
+ Returns:
+ Tuple of (success, message, new_filepath)
+ """
+ if not os.path.exists(source_path):
+ return False, f"Source document {source_path} does not exist", None
+
+ if not dest_path:
+ # Generate a new filename if not provided
+ base, ext = os.path.splitext(source_path)
+ dest_path = f"{base}_copy{ext}"
+
+ try:
+ # Simple file copy
+ shutil.copy2(source_path, dest_path)
+ return True, f"Document copied to {dest_path}", dest_path
+ except Exception as e:
+ return False, f"Failed to copy document: {str(e)}", None
+
+
+def ensure_docx_extension(filename: str) -> str:
+ """
+ Ensure filename has .docx extension.
+
+ Args:
+ filename: The filename to check
+
+ Returns:
+ Filename with .docx extension
+ """
+ if not filename.endswith('.docx'):
+ return filename + '.docx'
+ return filename
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_mcp_server.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_mcp_server.py
new file mode 100644
index 00000000..cd92472c
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/Office-Word-MCP-Server/word_mcp_server.py
@@ -0,0 +1,11 @@
+#!/usr/bin/env python3
+"""
+Run script for the Word Document Server.
+
+This script provides a simple way to start the Word Document Server.
+"""
+
+from word_document_server.main import run_server
+
+if __name__ == "__main__":
+ run_server()
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-latex-mcp/.github/workflows/ci.yml b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-latex-mcp/.github/workflows/ci.yml
new file mode 100644
index 00000000..28be7262
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-latex-mcp/.github/workflows/ci.yml
@@ -0,0 +1,15 @@
+name: CI
+on: [push, pull_request]
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ python-version: ["3.10", "3.12", "3.13"]
+ steps:
+ - uses: actions/checkout@v4
+ - uses: astral-sh/setup-uv@v4
+ with:
+ python-version: ${{ matrix.python-version }}
+ - run: uv sync
+ - run: uv run python -c "import server.main; print('OK')"
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-latex-mcp/.gitignore b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-latex-mcp/.gitignore
new file mode 100644
index 00000000..c549ca8a
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-latex-mcp/.gitignore
@@ -0,0 +1,3 @@
+server/lib/
+server/__pycache__/
+*.dxt
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-latex-mcp/LICENSE b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-latex-mcp/LICENSE
new file mode 100644
index 00000000..14e9ee94
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-latex-mcp/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2025 Takashi Ishida
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-latex-mcp/README.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-latex-mcp/README.md
new file mode 100644
index 00000000..498d3a77
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-latex-mcp/README.md
@@ -0,0 +1,49 @@
+# arxiv-latex MCP Server
+[](https://opensource.org/licenses/MIT)
+[](https://github.com/takashiishida/arxiv-latex-mcp/releases)
+[](https://archestra.ai/mcp-catalog/takashiishida__arxiv-latex-mcp)
+
+
+An MCP server that enables [Claude Desktop](https://claude.ai/download), [Claude Code](https://docs.anthropic.com/en/docs/claude-code), [Cursor](https://www.cursor.com/), or other MCP clients to directly access and process arXiv papers by fetching the LaTeX source. It uses [arxiv-to-prompt](https://github.com/takashiishida/arxiv-to-prompt) under the hood to handle downloading and processing the LaTeX.
+
+Why use the LaTeX source instead of uploading PDFs? Many PDF chat applications often struggle with mathematical content and equation-heavy papers. By utilizing the original LaTeX source code from arXiv papers, the LLM can accurately understand and handle equations and notations. This approach is particularly valuable for fields like computer science, mathematics, and engineering where precise interpretation of mathematical expressions is crucial.
+
+## Installation
+
+If you are using Claude Desktop, you can utilize Desktop Extensions by double-clicking on the .dxt file to install.
+Download the .dxt file from [here](https://github.com/takashiishida/arxiv-latex-mcp/releases/).
+Supported on macOS and Windows (Windows support is experimental).
+
+Otherwise, you can manually add the following configuration to your config file:
+```json
+{
+ "mcpServers": {
+ "arxiv-latex-mcp": {
+ "command": "uv",
+ "args": [
+ "--directory",
+ "/ABSOLUTE/PATH/TO/arxiv-latex-mcp",
+ "run",
+ "server/main.py"
+ ]
+ }
+ }
+}
+```
+
+You may need to replace the `command` field with the full path of `uv`: check this by running `which uv` (MacOS/Linux) or `where uv` (Windows).
+
+Restart the application after saving the above.
+
+For Claude Desktop, click on the hammer icon, and you should see the following in the list of "Available MCP tools":
+- `get_paper_prompt` — Get the full flattened LaTeX of a paper
+- `get_paper_abstract` — Get just the abstract
+- `list_paper_sections` — List section headings of a paper
+- `get_paper_section` — Get a specific section by path
+
+## Example
+Try asking questions about a paper from arXiv, e.g., "Explain the first theorem in 2202.00395"
+
+
+
+
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-latex-mcp/example.png b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-latex-mcp/example.png
new file mode 100644
index 00000000..21d49194
Binary files /dev/null and b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-latex-mcp/example.png differ
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-latex-mcp/manifest.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-latex-mcp/manifest.json
new file mode 100644
index 00000000..7857303c
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-latex-mcp/manifest.json
@@ -0,0 +1,37 @@
+{
+ "dxt_version": "0.1",
+ "name": "arxiv-latex-mcp",
+ "version": "0.2.1",
+ "description": "MCP server that uses arxiv-to-prompt to fetch and process arXiv LaTeX sources for precise interpretation of mathematical expressions in scientific papers.",
+ "author": {
+ "name": "Takashi Ishida",
+ "url": "https://takashiishida.github.io"
+ },
+ "homepage": "https://github.com/takashiishida/arxiv-latex-mcp",
+ "documentation": "https://github.com/takashiishida/arxiv-latex-mcp",
+ "server": {
+ "type": "python",
+ "entry_point": "server/main.py",
+ "mcp_config": {
+ "command": "python3",
+ "args": [
+ "${__dirname}/server/main.py"
+ ],
+ "env": {
+ "PYTHONPATH": "${__dirname}/server/lib"
+ }
+ }
+ },
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/takashiishida/arxiv-latex-mcp"
+ },
+ "compatibility": {
+ "claude_desktop": ">=0.11.4",
+ "platforms": ["darwin", "win32"],
+ "runtimes": {
+ "python": ">=3.10"
+ }
+ }
+}
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-latex-mcp/pyproject.toml b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-latex-mcp/pyproject.toml
new file mode 100644
index 00000000..abb1e27e
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-latex-mcp/pyproject.toml
@@ -0,0 +1,12 @@
+[project]
+name = "arxiv-latex-mcp"
+version = "0.2.1"
+description = "An MCP server that fetches and processes arXiv papers using LaTeX source for accurate equation handling"
+readme = "README.md"
+requires-python = ">=3.10"
+dependencies = [
+ "httpx>=0.28.1",
+ "mcp[cli]>=1.6.0",
+ "arxiv-to-prompt>=0.10.0",
+ "psycopg2-binary>=2.9.11",
+]
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-latex-mcp/server/main.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-latex-mcp/server/main.py
new file mode 100644
index 00000000..aafc51b0
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-latex-mcp/server/main.py
@@ -0,0 +1,197 @@
+#!/usr/bin/env python3
+"""
+ArXiv LaTeX MCP Server
+
+This server provides tools to fetch and process arXiv papers' LaTeX source code
+for better mathematical expression interpretation.
+"""
+
+import asyncio
+import logging
+from typing import Any
+
+from mcp.server import Server, NotificationOptions
+from mcp.server.models import InitializationOptions
+import mcp.types as types
+from mcp.server.stdio import stdio_server
+
+from pg_adapter import process_latex_source, list_sections, extract_section
+
+# Configure logging
+logging.basicConfig(level=logging.INFO)
+logger = logging.getLogger("arxiv-latex-mcp")
+
+# Create server instance
+server = Server("arxiv-latex-mcp")
+
+# MCP logging level (can be changed by client via logging/setLevel)
+mcp_log_level: types.LoggingLevel = "info"
+
+
+@server.set_logging_level()
+async def handle_set_logging_level(level: types.LoggingLevel) -> None:
+ """Handle logging level changes from the client."""
+ global mcp_log_level
+ mcp_log_level = level
+ logger.info(f"MCP logging level set to: {level}")
+
+
+async def mcp_log(level: types.LoggingLevel, message: str) -> None:
+ """Send a log message to the MCP client."""
+ MCP_LEVEL_ORDER = ["debug", "info", "notice", "warning", "error", "critical", "alert", "emergency"]
+ if MCP_LEVEL_ORDER.index(level) >= MCP_LEVEL_ORDER.index(mcp_log_level):
+ try:
+ ctx = server.request_context
+ await ctx.session.send_log_message(level=level, data=message, logger="arxiv-latex-mcp")
+ except Exception:
+ pass # Fall back silently if no active session
+
+
+@server.list_tools()
+async def handle_list_tools() -> list[types.Tool]:
+ """List available tools."""
+ return [
+ types.Tool(
+ name="get_paper_prompt",
+ description="Get a flattened LaTeX code of a paper from arXiv ID for precise interpretation of mathematical expressions",
+ inputSchema={
+ "type": "object",
+ "properties": {
+ "arxiv_id": {
+ "type": "string",
+ "description": "The arXiv ID of the paper (e.g., '2403.12345')",
+ }
+ },
+ "required": ["arxiv_id"],
+ },
+ ),
+ types.Tool(
+ name="get_paper_abstract",
+ description="Get just the abstract of an arXiv paper (faster and cheaper than fetching the full paper)",
+ inputSchema={
+ "type": "object",
+ "properties": {
+ "arxiv_id": {
+ "type": "string",
+ "description": "The arXiv ID of the paper (e.g., '2403.12345')",
+ }
+ },
+ "required": ["arxiv_id"],
+ },
+ ),
+ types.Tool(
+ name="list_paper_sections",
+ description="List section headings of an arXiv paper to see its structure",
+ inputSchema={
+ "type": "object",
+ "properties": {
+ "arxiv_id": {
+ "type": "string",
+ "description": "The arXiv ID of the paper (e.g., '2403.12345')",
+ }
+ },
+ "required": ["arxiv_id"],
+ },
+ ),
+ types.Tool(
+ name="get_paper_section",
+ description="Get a specific section of an arXiv paper by section path (use list_paper_sections first to find available sections)",
+ inputSchema={
+ "type": "object",
+ "properties": {
+ "arxiv_id": {
+ "type": "string",
+ "description": "The arXiv ID of the paper (e.g., '2403.12345')",
+ },
+ "section_path": {
+ "type": "string",
+ "description": "The section path to extract (e.g., '1', '2.1', 'Introduction'). Use list_paper_sections to find available paths.",
+ },
+ },
+ "required": ["arxiv_id", "section_path"],
+ },
+ ),
+ ]
+
+
+LATEX_RENDER_INSTRUCTIONS = """
+
+IMPORTANT INSTRUCTIONS FOR RENDERING:
+When discussing this paper, please use dollar sign notation ($...$) for inline equations and double dollar signs ($$...$$) for display equations when providing responses that include LaTeX mathematical expressions.
+"""
+
+
+@server.call_tool()
+async def handle_call_tool(
+ name: str, arguments: dict[str, Any] | None
+) -> list[types.TextContent]:
+ """Handle tool calls."""
+ if not arguments or "arxiv_id" not in arguments:
+ raise ValueError("Missing required argument: arxiv_id")
+
+ arxiv_id = arguments["arxiv_id"]
+
+ try:
+ if name == "get_paper_prompt":
+ await mcp_log("info", f"Processing arXiv paper: {arxiv_id}")
+ prompt = process_latex_source(arxiv_id)
+ result = prompt + LATEX_RENDER_INSTRUCTIONS
+ await mcp_log("info", f"Successfully processed arXiv paper: {arxiv_id}")
+
+ elif name == "get_paper_abstract":
+ await mcp_log("info", f"Getting abstract for arXiv paper: {arxiv_id}")
+ result = process_latex_source(arxiv_id, abstract_only=True)
+ await mcp_log("info", f"Successfully got abstract for: {arxiv_id}")
+
+ elif name == "list_paper_sections":
+ await mcp_log("info", f"Listing sections for arXiv paper: {arxiv_id}")
+ text = process_latex_source(arxiv_id)
+ sections = list_sections(text)
+ result = "\n".join(sections)
+ await mcp_log("info", f"Successfully listed sections for: {arxiv_id}")
+
+ elif name == "get_paper_section":
+ if "section_path" not in arguments:
+ raise ValueError("Missing required argument: section_path")
+ section_path = arguments["section_path"]
+ await mcp_log("info", f"Getting section '{section_path}' for arXiv paper: {arxiv_id}")
+ text = process_latex_source(arxiv_id)
+ result = extract_section(text, section_path)
+ if result is None:
+ result = f"Section '{section_path}' not found. Use list_paper_sections to see available sections."
+ else:
+ result = result + LATEX_RENDER_INSTRUCTIONS
+ await mcp_log("info", f"Successfully got section for: {arxiv_id}")
+
+ else:
+ raise ValueError(f"Unknown tool: {name}")
+
+ return [types.TextContent(type="text", text=result)]
+
+ except Exception as e:
+ error_msg = f"Error processing arXiv paper {arxiv_id}: {str(e)}"
+ await mcp_log("error", error_msg)
+
+ return [types.TextContent(type="text", text=error_msg)]
+
+
+async def main():
+ """Main entry point for the server."""
+ # Run the server using stdio transport
+ async with stdio_server() as (read_stream, write_stream):
+ await server.run(
+ read_stream,
+ write_stream,
+ InitializationOptions(
+ server_name="arxiv-latex-mcp",
+ server_version="0.2.1",
+ capabilities=server.get_capabilities(
+ notification_options=NotificationOptions(),
+ experimental_capabilities={},
+ ),
+ ),
+ )
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-latex-mcp/server/pg_adapter.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-latex-mcp/server/pg_adapter.py
new file mode 100644
index 00000000..781e961a
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-latex-mcp/server/pg_adapter.py
@@ -0,0 +1,130 @@
+"""PostgreSQL adapter replacing arxiv_to_prompt for arxiv-latex-mcp."""
+
+import os
+import json
+import psycopg2
+import psycopg2.extras
+from typing import Optional, List
+
+
+def _get_conn():
+ return psycopg2.connect(
+ host=os.environ.get('PG_HOST', 'localhost'),
+ port=int(os.environ.get('PG_PORT', '5432')),
+ dbname=os.environ.get('PG_DATABASE', 'toolathlon'),
+ user=os.environ.get('PG_USER', 'postgres'),
+ password=os.environ.get('PG_PASSWORD', 'postgres'),
+ )
+
+
+def process_latex_source(arxiv_id: str, abstract_only: bool = False) -> str:
+ """Replace arxiv_to_prompt.process_latex_source with PG lookup."""
+ conn = _get_conn()
+ try:
+ with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur:
+ cur.execute(
+ "SELECT title, abstract, full_prompt, sections FROM arxiv_latex.papers WHERE id = %s",
+ (arxiv_id,)
+ )
+ row = cur.fetchone()
+
+ if not row:
+ raise ValueError(f"Paper {arxiv_id} not found in database")
+
+ if abstract_only:
+ return row['abstract'] or f"No abstract available for {arxiv_id}"
+
+ return row['full_prompt'] or f"No content available for {arxiv_id}"
+ finally:
+ conn.close()
+
+
+def list_sections(text: str) -> List[str]:
+ """List section headings from the processed text.
+
+ This mimics arxiv_to_prompt.list_sections by parsing section headers
+ from the full_prompt text. We look for patterns like:
+ # Section Title
+ ## Subsection Title
+ """
+ sections = []
+ for line in text.split('\n'):
+ stripped = line.strip()
+ if stripped.startswith('#'):
+ # Count heading level
+ level = 0
+ for ch in stripped:
+ if ch == '#':
+ level += 1
+ else:
+ break
+ title = stripped[level:].strip()
+ if title:
+ # Generate section path based on numbering
+ sections.append(title)
+ return sections
+
+
+def extract_section(text: str, section_path: str) -> Optional[str]:
+ """Extract a specific section from the processed text.
+
+ Mimics arxiv_to_prompt.extract_section.
+ """
+ lines = text.split('\n')
+ in_section = False
+ section_level = 0
+ result_lines = []
+
+ for line in lines:
+ stripped = line.strip()
+ if stripped.startswith('#'):
+ level = 0
+ for ch in stripped:
+ if ch == '#':
+ level += 1
+ else:
+ break
+ title = stripped[level:].strip()
+
+ if in_section:
+ # If we hit a heading at same or higher level, stop
+ if level <= section_level:
+ break
+ result_lines.append(line)
+ elif title.lower() == section_path.lower() or section_path in title:
+ in_section = True
+ section_level = level
+ result_lines.append(line)
+ elif in_section:
+ result_lines.append(line)
+
+ if result_lines:
+ return '\n'.join(result_lines)
+
+ # Try matching by section number (e.g., "1", "2.1")
+ # Look for patterns like "1 Introduction", "2.1 Methods"
+ for i, line in enumerate(lines):
+ stripped = line.strip()
+ if stripped.startswith('#'):
+ level = 0
+ for ch in stripped:
+ if ch == '#':
+ level += 1
+ else:
+ break
+ title = stripped[level:].strip()
+ # Check if section_path matches the number prefix
+ if title.startswith(section_path + ' ') or title.startswith(section_path + '.'):
+ in_section = True
+ section_level = level
+ result_lines.append(line)
+ for subsequent_line in lines[i+1:]:
+ sub_stripped = subsequent_line.strip()
+ if sub_stripped.startswith('#'):
+ sub_level = sum(1 for ch in sub_stripped if ch == '#')
+ if sub_level <= section_level:
+ break
+ result_lines.append(subsequent_line)
+ return '\n'.join(result_lines) if result_lines else None
+
+ return None
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-latex-mcp/uv.lock b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-latex-mcp/uv.lock
new file mode 100644
index 00000000..d3672599
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-latex-mcp/uv.lock
@@ -0,0 +1,1042 @@
+version = 1
+revision = 2
+requires-python = ">=3.10"
+
+[[package]]
+name = "annotated-doc"
+version = "0.0.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" },
+]
+
+[[package]]
+name = "annotated-types"
+version = "0.7.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
+]
+
+[[package]]
+name = "anyio"
+version = "4.12.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
+ { name = "idna" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" },
+]
+
+[[package]]
+name = "arxiv-latex-mcp"
+version = "0.2.1"
+source = { virtual = "." }
+dependencies = [
+ { name = "arxiv-to-prompt" },
+ { name = "httpx" },
+ { name = "mcp", extra = ["cli"] },
+ { name = "psycopg2-binary" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "arxiv-to-prompt", specifier = ">=0.10.0" },
+ { name = "httpx", specifier = ">=0.28.1" },
+ { name = "mcp", extras = ["cli"], specifier = ">=1.6.0" },
+ { name = "psycopg2-binary", specifier = ">=2.9.11" },
+]
+
+[[package]]
+name = "arxiv-to-prompt"
+version = "0.10.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "filelock" },
+ { name = "pyperclip" },
+ { name = "requests" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/21/2d/9927ef07075c4d11a130551ebf4d008173966d12705648867742b0767efc/arxiv_to_prompt-0.10.0.tar.gz", hash = "sha256:17a612386dfd8b723784452ff751d7782f5fda9ff79bf0a2faf9eaa6790ae61d", size = 23800, upload-time = "2026-02-15T02:21:51.35Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/66/a0/0576a17e1baf903a1998a4f854cc17739db12332d871d123c478d40cc6c9/arxiv_to_prompt-0.10.0-py3-none-any.whl", hash = "sha256:b7cb77901d857edd1c5424c77a0065cfd1bebad9e393f9dc5ec60af17c0101c1", size = 14837, upload-time = "2026-02-15T02:21:49.792Z" },
+]
+
+[[package]]
+name = "attrs"
+version = "25.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" },
+]
+
+[[package]]
+name = "certifi"
+version = "2026.1.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" },
+]
+
+[[package]]
+name = "cffi"
+version = "2.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pycparser", marker = "implementation_name != 'PyPy'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" },
+ { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" },
+ { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" },
+ { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" },
+ { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" },
+ { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" },
+ { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" },
+ { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" },
+ { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" },
+ { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" },
+ { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" },
+ { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" },
+ { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" },
+ { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" },
+ { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" },
+ { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" },
+ { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" },
+ { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" },
+ { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" },
+ { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" },
+ { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" },
+ { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" },
+ { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" },
+ { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" },
+ { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
+]
+
+[[package]]
+name = "charset-normalizer"
+version = "3.4.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" },
+ { url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" },
+ { url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" },
+ { url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" },
+ { url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" },
+ { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" },
+ { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" },
+ { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" },
+ { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" },
+ { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" },
+ { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" },
+ { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" },
+ { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" },
+ { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" },
+ { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" },
+ { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" },
+ { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" },
+ { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" },
+ { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" },
+ { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" },
+ { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" },
+ { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" },
+ { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" },
+ { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" },
+ { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" },
+ { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" },
+ { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" },
+ { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" },
+ { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" },
+ { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" },
+ { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" },
+ { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" },
+ { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" },
+ { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" },
+]
+
+[[package]]
+name = "click"
+version = "8.3.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" },
+]
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
+]
+
+[[package]]
+name = "cryptography"
+version = "46.0.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/60/04/ee2a9e8542e4fa2773b81771ff8349ff19cdd56b7258a0cc442639052edb/cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d", size = 750064, upload-time = "2026-02-10T19:18:38.255Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f7/81/b0bb27f2ba931a65409c6b8a8b358a7f03c0e46eceacddff55f7c84b1f3b/cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad", size = 7176289, upload-time = "2026-02-10T19:17:08.274Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/9e/6b4397a3e3d15123de3b1806ef342522393d50736c13b20ec4c9ea6693a6/cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b", size = 4275637, upload-time = "2026-02-10T19:17:10.53Z" },
+ { url = "https://files.pythonhosted.org/packages/63/e7/471ab61099a3920b0c77852ea3f0ea611c9702f651600397ac567848b897/cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b", size = 4424742, upload-time = "2026-02-10T19:17:12.388Z" },
+ { url = "https://files.pythonhosted.org/packages/37/53/a18500f270342d66bf7e4d9f091114e31e5ee9e7375a5aba2e85a91e0044/cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263", size = 4277528, upload-time = "2026-02-10T19:17:13.853Z" },
+ { url = "https://files.pythonhosted.org/packages/22/29/c2e812ebc38c57b40e7c583895e73c8c5adb4d1e4a0cc4c5a4fdab2b1acc/cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d", size = 4947993, upload-time = "2026-02-10T19:17:15.618Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/e7/237155ae19a9023de7e30ec64e5d99a9431a567407ac21170a046d22a5a3/cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed", size = 4456855, upload-time = "2026-02-10T19:17:17.221Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/87/fc628a7ad85b81206738abbd213b07702bcbdada1dd43f72236ef3cffbb5/cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2", size = 3984635, upload-time = "2026-02-10T19:17:18.792Z" },
+ { url = "https://files.pythonhosted.org/packages/84/29/65b55622bde135aedf4565dc509d99b560ee4095e56989e815f8fd2aa910/cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2", size = 4277038, upload-time = "2026-02-10T19:17:20.256Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/36/45e76c68d7311432741faf1fbf7fac8a196a0a735ca21f504c75d37e2558/cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0", size = 4912181, upload-time = "2026-02-10T19:17:21.825Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/1a/c1ba8fead184d6e3d5afcf03d569acac5ad063f3ac9fb7258af158f7e378/cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731", size = 4456482, upload-time = "2026-02-10T19:17:25.133Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/e5/3fb22e37f66827ced3b902cf895e6a6bc1d095b5b26be26bd13c441fdf19/cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82", size = 4405497, upload-time = "2026-02-10T19:17:26.66Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/df/9d58bb32b1121a8a2f27383fabae4d63080c7ca60b9b5c88be742be04ee7/cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1", size = 4667819, upload-time = "2026-02-10T19:17:28.569Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/ed/325d2a490c5e94038cdb0117da9397ece1f11201f425c4e9c57fe5b9f08b/cryptography-46.0.5-cp311-abi3-win32.whl", hash = "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48", size = 3028230, upload-time = "2026-02-10T19:17:30.518Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/5a/ac0f49e48063ab4255d9e3b79f5def51697fce1a95ea1370f03dc9db76f6/cryptography-46.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4", size = 3480909, upload-time = "2026-02-10T19:17:32.083Z" },
+ { url = "https://files.pythonhosted.org/packages/00/13/3d278bfa7a15a96b9dc22db5a12ad1e48a9eb3d40e1827ef66a5df75d0d0/cryptography-46.0.5-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2", size = 7119287, upload-time = "2026-02-10T19:17:33.801Z" },
+ { url = "https://files.pythonhosted.org/packages/67/c8/581a6702e14f0898a0848105cbefd20c058099e2c2d22ef4e476dfec75d7/cryptography-46.0.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678", size = 4265728, upload-time = "2026-02-10T19:17:35.569Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/4a/ba1a65ce8fc65435e5a849558379896c957870dd64fecea97b1ad5f46a37/cryptography-46.0.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87", size = 4408287, upload-time = "2026-02-10T19:17:36.938Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/67/8ffdbf7b65ed1ac224d1c2df3943553766914a8ca718747ee3871da6107e/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee", size = 4270291, upload-time = "2026-02-10T19:17:38.748Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/e5/f52377ee93bc2f2bba55a41a886fd208c15276ffbd2569f2ddc89d50e2c5/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981", size = 4927539, upload-time = "2026-02-10T19:17:40.241Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/02/cfe39181b02419bbbbcf3abdd16c1c5c8541f03ca8bda240debc467d5a12/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9", size = 4442199, upload-time = "2026-02-10T19:17:41.789Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/96/2fcaeb4873e536cf71421a388a6c11b5bc846e986b2b069c79363dc1648e/cryptography-46.0.5-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648", size = 3960131, upload-time = "2026-02-10T19:17:43.379Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/d2/b27631f401ddd644e94c5cf33c9a4069f72011821cf3dc7309546b0642a0/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4", size = 4270072, upload-time = "2026-02-10T19:17:45.481Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/a7/60d32b0370dae0b4ebe55ffa10e8599a2a59935b5ece1b9f06edb73abdeb/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0", size = 4892170, upload-time = "2026-02-10T19:17:46.997Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/b9/cf73ddf8ef1164330eb0b199a589103c363afa0cf794218c24d524a58eab/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663", size = 4441741, upload-time = "2026-02-10T19:17:48.661Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/eb/eee00b28c84c726fe8fa0158c65afe312d9c3b78d9d01daf700f1f6e37ff/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826", size = 4396728, upload-time = "2026-02-10T19:17:50.058Z" },
+ { url = "https://files.pythonhosted.org/packages/65/f4/6bc1a9ed5aef7145045114b75b77c2a8261b4d38717bd8dea111a63c3442/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d", size = 4652001, upload-time = "2026-02-10T19:17:51.54Z" },
+ { url = "https://files.pythonhosted.org/packages/86/ef/5d00ef966ddd71ac2e6951d278884a84a40ffbd88948ef0e294b214ae9e4/cryptography-46.0.5-cp314-cp314t-win32.whl", hash = "sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a", size = 3003637, upload-time = "2026-02-10T19:17:52.997Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/57/f3f4160123da6d098db78350fdfd9705057aad21de7388eacb2401dceab9/cryptography-46.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4", size = 3469487, upload-time = "2026-02-10T19:17:54.549Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/fa/a66aa722105ad6a458bebd64086ca2b72cdd361fed31763d20390f6f1389/cryptography-46.0.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31", size = 7170514, upload-time = "2026-02-10T19:17:56.267Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/04/c85bdeab78c8bc77b701bf0d9bdcf514c044e18a46dcff330df5448631b0/cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18", size = 4275349, upload-time = "2026-02-10T19:17:58.419Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/32/9b87132a2f91ee7f5223b091dc963055503e9b442c98fc0b8a5ca765fab0/cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235", size = 4420667, upload-time = "2026-02-10T19:18:00.619Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/a6/a7cb7010bec4b7c5692ca6f024150371b295ee1c108bdc1c400e4c44562b/cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a", size = 4276980, upload-time = "2026-02-10T19:18:02.379Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/7c/c4f45e0eeff9b91e3f12dbd0e165fcf2a38847288fcfd889deea99fb7b6d/cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76", size = 4939143, upload-time = "2026-02-10T19:18:03.964Z" },
+ { url = "https://files.pythonhosted.org/packages/37/19/e1b8f964a834eddb44fa1b9a9976f4e414cbb7aa62809b6760c8803d22d1/cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614", size = 4453674, upload-time = "2026-02-10T19:18:05.588Z" },
+ { url = "https://files.pythonhosted.org/packages/db/ed/db15d3956f65264ca204625597c410d420e26530c4e2943e05a0d2f24d51/cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229", size = 3978801, upload-time = "2026-02-10T19:18:07.167Z" },
+ { url = "https://files.pythonhosted.org/packages/41/e2/df40a31d82df0a70a0daf69791f91dbb70e47644c58581d654879b382d11/cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1", size = 4276755, upload-time = "2026-02-10T19:18:09.813Z" },
+ { url = "https://files.pythonhosted.org/packages/33/45/726809d1176959f4a896b86907b98ff4391a8aa29c0aaaf9450a8a10630e/cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d", size = 4901539, upload-time = "2026-02-10T19:18:11.263Z" },
+ { url = "https://files.pythonhosted.org/packages/99/0f/a3076874e9c88ecb2ecc31382f6e7c21b428ede6f55aafa1aa272613e3cd/cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c", size = 4452794, upload-time = "2026-02-10T19:18:12.914Z" },
+ { url = "https://files.pythonhosted.org/packages/02/ef/ffeb542d3683d24194a38f66ca17c0a4b8bf10631feef44a7ef64e631b1a/cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4", size = 4404160, upload-time = "2026-02-10T19:18:14.375Z" },
+ { url = "https://files.pythonhosted.org/packages/96/93/682d2b43c1d5f1406ed048f377c0fc9fc8f7b0447a478d5c65ab3d3a66eb/cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9", size = 4667123, upload-time = "2026-02-10T19:18:15.886Z" },
+ { url = "https://files.pythonhosted.org/packages/45/2d/9c5f2926cb5300a8eefc3f4f0b3f3df39db7f7ce40c8365444c49363cbda/cryptography-46.0.5-cp38-abi3-win32.whl", hash = "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72", size = 3010220, upload-time = "2026-02-10T19:18:17.361Z" },
+ { url = "https://files.pythonhosted.org/packages/48/ef/0c2f4a8e31018a986949d34a01115dd057bf536905dca38897bacd21fac3/cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595", size = 3467050, upload-time = "2026-02-10T19:18:18.899Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/dd/2d9fdb07cebdf3d51179730afb7d5e576153c6744c3ff8fded23030c204e/cryptography-46.0.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:3b4995dc971c9fb83c25aa44cf45f02ba86f71ee600d81091c2f0cbae116b06c", size = 3476964, upload-time = "2026-02-10T19:18:20.687Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/6f/6cc6cc9955caa6eaf83660b0da2b077c7fe8ff9950a3c5e45d605038d439/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bc84e875994c3b445871ea7181d424588171efec3e185dced958dad9e001950a", size = 4218321, upload-time = "2026-02-10T19:18:22.349Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/5d/c4da701939eeee699566a6c1367427ab91a8b7088cc2328c09dbee940415/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:2ae6971afd6246710480e3f15824ed3029a60fc16991db250034efd0b9fb4356", size = 4381786, upload-time = "2026-02-10T19:18:24.529Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/97/a538654732974a94ff96c1db621fa464f455c02d4bb7d2652f4edc21d600/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d861ee9e76ace6cf36a6a89b959ec08e7bc2493ee39d07ffe5acb23ef46d27da", size = 4217990, upload-time = "2026-02-10T19:18:25.957Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/11/7e500d2dd3ba891197b9efd2da5454b74336d64a7cc419aa7327ab74e5f6/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:2b7a67c9cd56372f3249b39699f2ad479f6991e62ea15800973b956f4b73e257", size = 4381252, upload-time = "2026-02-10T19:18:27.496Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/58/6b3d24e6b9bc474a2dcdee65dfd1f008867015408a271562e4b690561a4d/cryptography-46.0.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8456928655f856c6e1533ff59d5be76578a7157224dbd9ce6872f25055ab9ab7", size = 3407605, upload-time = "2026-02-10T19:18:29.233Z" },
+]
+
+[[package]]
+name = "exceptiongroup"
+version = "1.3.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
+]
+
+[[package]]
+name = "filelock"
+version = "3.24.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/73/92/a8e2479937ff39185d20dd6a851c1a63e55849e447a55e798cc2e1f49c65/filelock-3.24.3.tar.gz", hash = "sha256:011a5644dc937c22699943ebbfc46e969cdde3e171470a6e40b9533e5a72affa", size = 37935, upload-time = "2026-02-19T00:48:20.543Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9c/0f/5d0c71a1aefeb08efff26272149e07ab922b64f46c63363756224bd6872e/filelock-3.24.3-py3-none-any.whl", hash = "sha256:426e9a4660391f7f8a810d71b0555bce9008b0a1cc342ab1f6947d37639e002d", size = 24331, upload-time = "2026-02-19T00:48:18.465Z" },
+]
+
+[[package]]
+name = "h11"
+version = "0.16.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
+]
+
+[[package]]
+name = "httpcore"
+version = "1.0.9"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "h11" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
+]
+
+[[package]]
+name = "httpx"
+version = "0.28.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "certifi" },
+ { name = "httpcore" },
+ { name = "idna" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
+]
+
+[[package]]
+name = "httpx-sse"
+version = "0.4.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" },
+]
+
+[[package]]
+name = "idna"
+version = "3.11"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
+]
+
+[[package]]
+name = "jsonschema"
+version = "4.26.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "attrs" },
+ { name = "jsonschema-specifications" },
+ { name = "referencing" },
+ { name = "rpds-py" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" },
+]
+
+[[package]]
+name = "jsonschema-specifications"
+version = "2025.9.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "referencing" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" },
+]
+
+[[package]]
+name = "markdown-it-py"
+version = "4.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "mdurl" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
+]
+
+[[package]]
+name = "mcp"
+version = "1.26.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "httpx" },
+ { name = "httpx-sse" },
+ { name = "jsonschema" },
+ { name = "pydantic" },
+ { name = "pydantic-settings" },
+ { name = "pyjwt", extra = ["crypto"] },
+ { name = "python-multipart" },
+ { name = "pywin32", marker = "sys_platform == 'win32'" },
+ { name = "sse-starlette" },
+ { name = "starlette" },
+ { name = "typing-extensions" },
+ { name = "typing-inspection" },
+ { name = "uvicorn", marker = "sys_platform != 'emscripten'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/fc/6d/62e76bbb8144d6ed86e202b5edd8a4cb631e7c8130f3f4893c3f90262b10/mcp-1.26.0.tar.gz", hash = "sha256:db6e2ef491eecc1a0d93711a76f28dec2e05999f93afd48795da1c1137142c66", size = 608005, upload-time = "2026-01-24T19:40:32.468Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fd/d9/eaa1f80170d2b7c5ba23f3b59f766f3a0bb41155fbc32a69adfa1adaaef9/mcp-1.26.0-py3-none-any.whl", hash = "sha256:904a21c33c25aa98ddbeb47273033c435e595bbacfdb177f4bd87f6dceebe1ca", size = 233615, upload-time = "2026-01-24T19:40:30.652Z" },
+]
+
+[package.optional-dependencies]
+cli = [
+ { name = "python-dotenv" },
+ { name = "typer" },
+]
+
+[[package]]
+name = "mdurl"
+version = "0.1.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
+]
+
+[[package]]
+name = "psycopg2-binary"
+version = "2.9.11"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ac/6c/8767aaa597ba424643dc87348c6f1754dd9f48e80fdc1b9f7ca5c3a7c213/psycopg2-binary-2.9.11.tar.gz", hash = "sha256:b6aed9e096bf63f9e75edf2581aa9a7e7186d97ab5c177aa6c87797cd591236c", size = 379620, upload-time = "2025-10-10T11:14:48.041Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6a/f2/8e377d29c2ecf99f6062d35ea606b036e8800720eccfec5fe3dd672c2b24/psycopg2_binary-2.9.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d6fe6b47d0b42ce1c9f1fa3e35bb365011ca22e39db37074458f27921dca40f2", size = 3756506, upload-time = "2025-10-10T11:10:30.144Z" },
+ { url = "https://files.pythonhosted.org/packages/24/cc/dc143ea88e4ec9d386106cac05023b69668bd0be20794c613446eaefafe5/psycopg2_binary-2.9.11-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a6c0e4262e089516603a09474ee13eabf09cb65c332277e39af68f6233911087", size = 3863943, upload-time = "2025-10-10T11:10:34.586Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/df/16848771155e7c419c60afeb24950b8aaa3ab09c0a091ec3ccca26a574d0/psycopg2_binary-2.9.11-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c47676e5b485393f069b4d7a811267d3168ce46f988fa602658b8bb901e9e64d", size = 4410873, upload-time = "2025-10-10T11:10:38.951Z" },
+ { url = "https://files.pythonhosted.org/packages/43/79/5ef5f32621abd5a541b89b04231fe959a9b327c874a1d41156041c75494b/psycopg2_binary-2.9.11-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:a28d8c01a7b27a1e3265b11250ba7557e5f72b5ee9e5f3a2fa8d2949c29bf5d2", size = 4468016, upload-time = "2025-10-10T11:10:43.319Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/9b/d7542d0f7ad78f57385971f426704776d7b310f5219ed58da5d605b1892e/psycopg2_binary-2.9.11-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5f3f2732cf504a1aa9e9609d02f79bea1067d99edf844ab92c247bbca143303b", size = 4164996, upload-time = "2025-10-10T11:10:46.705Z" },
+ { url = "https://files.pythonhosted.org/packages/14/ed/e409388b537fa7414330687936917c522f6a77a13474e4238219fcfd9a84/psycopg2_binary-2.9.11-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:865f9945ed1b3950d968ec4690ce68c55019d79e4497366d36e090327ce7db14", size = 3981881, upload-time = "2025-10-30T02:54:57.182Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/30/50e330e63bb05efc6fa7c1447df3e08954894025ca3dcb396ecc6739bc26/psycopg2_binary-2.9.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:91537a8df2bde69b1c1db01d6d944c831ca793952e4f57892600e96cee95f2cd", size = 3650857, upload-time = "2025-10-10T11:10:50.112Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/e0/4026e4c12bb49dd028756c5b0bc4c572319f2d8f1c9008e0dad8cc9addd7/psycopg2_binary-2.9.11-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4dca1f356a67ecb68c81a7bc7809f1569ad9e152ce7fd02c2f2036862ca9f66b", size = 3296063, upload-time = "2025-10-10T11:10:54.089Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/34/eb172be293c886fef5299fe5c3fcf180a05478be89856067881007934a7c/psycopg2_binary-2.9.11-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0da4de5c1ac69d94ed4364b6cbe7190c1a70d325f112ba783d83f8440285f152", size = 3043464, upload-time = "2025-10-30T02:55:02.483Z" },
+ { url = "https://files.pythonhosted.org/packages/18/1c/532c5d2cb11986372f14b798a95f2eaafe5779334f6a80589a68b5fcf769/psycopg2_binary-2.9.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37d8412565a7267f7d79e29ab66876e55cb5e8e7b3bbf94f8206f6795f8f7e7e", size = 3345378, upload-time = "2025-10-10T11:11:01.039Z" },
+ { url = "https://files.pythonhosted.org/packages/70/e7/de420e1cf16f838e1fa17b1120e83afff374c7c0130d088dba6286fcf8ea/psycopg2_binary-2.9.11-cp310-cp310-win_amd64.whl", hash = "sha256:c665f01ec8ab273a61c62beeb8cce3014c214429ced8a308ca1fc410ecac3a39", size = 2713904, upload-time = "2025-10-10T11:11:04.81Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/ae/8d8266f6dd183ab4d48b95b9674034e1b482a3f8619b33a0d86438694577/psycopg2_binary-2.9.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0e8480afd62362d0a6a27dd09e4ca2def6fa50ed3a4e7c09165266106b2ffa10", size = 3756452, upload-time = "2025-10-10T11:11:11.583Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/34/aa03d327739c1be70e09d01182619aca8ebab5970cd0cfa50dd8b9cec2ac/psycopg2_binary-2.9.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:763c93ef1df3da6d1a90f86ea7f3f806dc06b21c198fa87c3c25504abec9404a", size = 3863957, upload-time = "2025-10-10T11:11:16.932Z" },
+ { url = "https://files.pythonhosted.org/packages/48/89/3fdb5902bdab8868bbedc1c6e6023a4e08112ceac5db97fc2012060e0c9a/psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e164359396576a3cc701ba8af4751ae68a07235d7a380c631184a611220d9a4", size = 4410955, upload-time = "2025-10-10T11:11:21.21Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/24/e18339c407a13c72b336e0d9013fbbbde77b6fd13e853979019a1269519c/psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:d57c9c387660b8893093459738b6abddbb30a7eab058b77b0d0d1c7d521ddfd7", size = 4468007, upload-time = "2025-10-10T11:11:24.831Z" },
+ { url = "https://files.pythonhosted.org/packages/91/7e/b8441e831a0f16c159b5381698f9f7f7ed54b77d57bc9c5f99144cc78232/psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2c226ef95eb2250974bf6fa7a842082b31f68385c4f3268370e3f3870e7859ee", size = 4165012, upload-time = "2025-10-10T11:11:29.51Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/61/4aa89eeb6d751f05178a13da95516c036e27468c5d4d2509bb1e15341c81/psycopg2_binary-2.9.11-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a311f1edc9967723d3511ea7d2708e2c3592e3405677bf53d5c7246753591fbb", size = 3981881, upload-time = "2025-10-30T02:55:07.332Z" },
+ { url = "https://files.pythonhosted.org/packages/76/a1/2f5841cae4c635a9459fe7aca8ed771336e9383b6429e05c01267b0774cf/psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ebb415404821b6d1c47353ebe9c8645967a5235e6d88f914147e7fd411419e6f", size = 3650985, upload-time = "2025-10-10T11:11:34.975Z" },
+ { url = "https://files.pythonhosted.org/packages/84/74/4defcac9d002bca5709951b975173c8c2fa968e1a95dc713f61b3a8d3b6a/psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f07c9c4a5093258a03b28fab9b4f151aa376989e7f35f855088234e656ee6a94", size = 3296039, upload-time = "2025-10-10T11:11:40.432Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/c2/782a3c64403d8ce35b5c50e1b684412cf94f171dc18111be8c976abd2de1/psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:00ce1830d971f43b667abe4a56e42c1e2d594b32da4802e44a73bacacb25535f", size = 3043477, upload-time = "2025-10-30T02:55:11.182Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/31/36a1d8e702aa35c38fc117c2b8be3f182613faa25d794b8aeaab948d4c03/psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cffe9d7697ae7456649617e8bb8d7a45afb71cd13f7ab22af3e5c61f04840908", size = 3345842, upload-time = "2025-10-10T11:11:45.366Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/b4/a5375cda5b54cb95ee9b836930fea30ae5a8f14aa97da7821722323d979b/psycopg2_binary-2.9.11-cp311-cp311-win_amd64.whl", hash = "sha256:304fd7b7f97eef30e91b8f7e720b3db75fee010b520e434ea35ed1ff22501d03", size = 2713894, upload-time = "2025-10-10T11:11:48.775Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/91/f870a02f51be4a65987b45a7de4c2e1897dd0d01051e2b559a38fa634e3e/psycopg2_binary-2.9.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:be9b840ac0525a283a96b556616f5b4820e0526addb8dcf6525a0fa162730be4", size = 3756603, upload-time = "2025-10-10T11:11:52.213Z" },
+ { url = "https://files.pythonhosted.org/packages/27/fa/cae40e06849b6c9a95eb5c04d419942f00d9eaac8d81626107461e268821/psycopg2_binary-2.9.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f090b7ddd13ca842ebfe301cd587a76a4cf0913b1e429eb92c1be5dbeb1a19bc", size = 3864509, upload-time = "2025-10-10T11:11:56.452Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/75/364847b879eb630b3ac8293798e380e441a957c53657995053c5ec39a316/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ab8905b5dcb05bf3fb22e0cf90e10f469563486ffb6a96569e51f897c750a76a", size = 4411159, upload-time = "2025-10-10T11:12:00.49Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/a0/567f7ea38b6e1c62aafd58375665a547c00c608a471620c0edc364733e13/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf940cd7e7fec19181fdbc29d76911741153d51cab52e5c21165f3262125685e", size = 4468234, upload-time = "2025-10-10T11:12:04.892Z" },
+ { url = "https://files.pythonhosted.org/packages/30/da/4e42788fb811bbbfd7b7f045570c062f49e350e1d1f3df056c3fb5763353/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa0f693d3c68ae925966f0b14b8edda71696608039f4ed61b1fe9ffa468d16db", size = 4166236, upload-time = "2025-10-10T11:12:11.674Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/94/c1777c355bc560992af848d98216148be5f1be001af06e06fc49cbded578/psycopg2_binary-2.9.11-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a1cf393f1cdaf6a9b57c0a719a1068ba1069f022a59b8b1fe44b006745b59757", size = 3983083, upload-time = "2025-10-30T02:55:15.73Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/42/c9a21edf0e3daa7825ed04a4a8588686c6c14904344344a039556d78aa58/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7a6beb4beaa62f88592ccc65df20328029d721db309cb3250b0aae0fa146c3", size = 3652281, upload-time = "2025-10-10T11:12:17.713Z" },
+ { url = "https://files.pythonhosted.org/packages/12/22/dedfbcfa97917982301496b6b5e5e6c5531d1f35dd2b488b08d1ebc52482/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:31b32c457a6025e74d233957cc9736742ac5a6cb196c6b68499f6bb51390bd6a", size = 3298010, upload-time = "2025-10-10T11:12:22.671Z" },
+ { url = "https://files.pythonhosted.org/packages/66/ea/d3390e6696276078bd01b2ece417deac954dfdd552d2edc3d03204416c0c/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:edcb3aeb11cb4bf13a2af3c53a15b3d612edeb6409047ea0b5d6a21a9d744b34", size = 3044641, upload-time = "2025-10-30T02:55:19.929Z" },
+ { url = "https://files.pythonhosted.org/packages/12/9a/0402ded6cbd321da0c0ba7d34dc12b29b14f5764c2fc10750daa38e825fc/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b6d93d7c0b61a1dd6197d208ab613eb7dcfdcca0a49c42ceb082257991de9d", size = 3347940, upload-time = "2025-10-10T11:12:26.529Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/d2/99b55e85832ccde77b211738ff3925a5d73ad183c0b37bcbbe5a8ff04978/psycopg2_binary-2.9.11-cp312-cp312-win_amd64.whl", hash = "sha256:b33fabeb1fde21180479b2d4667e994de7bbf0eec22832ba5d9b5e4cf65b6c6d", size = 2714147, upload-time = "2025-10-10T11:12:29.535Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/a8/a2709681b3ac11b0b1786def10006b8995125ba268c9a54bea6f5ae8bd3e/psycopg2_binary-2.9.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b8fb3db325435d34235b044b199e56cdf9ff41223a4b9752e8576465170bb38c", size = 3756572, upload-time = "2025-10-10T11:12:32.873Z" },
+ { url = "https://files.pythonhosted.org/packages/62/e1/c2b38d256d0dafd32713e9f31982a5b028f4a3651f446be70785f484f472/psycopg2_binary-2.9.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:366df99e710a2acd90efed3764bb1e28df6c675d33a7fb40df9b7281694432ee", size = 3864529, upload-time = "2025-10-10T11:12:36.791Z" },
+ { url = "https://files.pythonhosted.org/packages/11/32/b2ffe8f3853c181e88f0a157c5fb4e383102238d73c52ac6d93a5c8bffe6/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c55b385daa2f92cb64b12ec4536c66954ac53654c7f15a203578da4e78105c0", size = 4411242, upload-time = "2025-10-10T11:12:42.388Z" },
+ { url = "https://files.pythonhosted.org/packages/10/04/6ca7477e6160ae258dc96f67c371157776564679aefd247b66f4661501a2/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c0377174bf1dd416993d16edc15357f6eb17ac998244cca19bc67cdc0e2e5766", size = 4468258, upload-time = "2025-10-10T11:12:48.654Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/7e/6a1a38f86412df101435809f225d57c1a021307dd0689f7a5e7fe83588b1/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c6ff3335ce08c75afaed19e08699e8aacf95d4a260b495a4a8545244fe2ceb3", size = 4166295, upload-time = "2025-10-10T11:12:52.525Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/7d/c07374c501b45f3579a9eb761cbf2604ddef3d96ad48679112c2c5aa9c25/psycopg2_binary-2.9.11-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:84011ba3109e06ac412f95399b704d3d6950e386b7994475b231cf61eec2fc1f", size = 3983133, upload-time = "2025-10-30T02:55:24.329Z" },
+ { url = "https://files.pythonhosted.org/packages/82/56/993b7104cb8345ad7d4516538ccf8f0d0ac640b1ebd8c754a7b024e76878/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ba34475ceb08cccbdd98f6b46916917ae6eeb92b5ae111df10b544c3a4621dc4", size = 3652383, upload-time = "2025-10-10T11:12:56.387Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/ac/eaeb6029362fd8d454a27374d84c6866c82c33bfc24587b4face5a8e43ef/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b31e90fdd0f968c2de3b26ab014314fe814225b6c324f770952f7d38abf17e3c", size = 3298168, upload-time = "2025-10-10T11:13:00.403Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/39/50c3facc66bded9ada5cbc0de867499a703dc6bca6be03070b4e3b65da6c/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d526864e0f67f74937a8fce859bd56c979f5e2ec57ca7c627f5f1071ef7fee60", size = 3044712, upload-time = "2025-10-30T02:55:27.975Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/8e/b7de019a1f562f72ada81081a12823d3c1590bedc48d7d2559410a2763fe/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04195548662fa544626c8ea0f06561eb6203f1984ba5b4562764fbeb4c3d14b1", size = 3347549, upload-time = "2025-10-10T11:13:03.971Z" },
+ { url = "https://files.pythonhosted.org/packages/80/2d/1bb683f64737bbb1f86c82b7359db1eb2be4e2c0c13b947f80efefa7d3e5/psycopg2_binary-2.9.11-cp313-cp313-win_amd64.whl", hash = "sha256:efff12b432179443f54e230fdf60de1f6cc726b6c832db8701227d089310e8aa", size = 2714215, upload-time = "2025-10-10T11:13:07.14Z" },
+ { url = "https://files.pythonhosted.org/packages/64/12/93ef0098590cf51d9732b4f139533732565704f45bdc1ffa741b7c95fb54/psycopg2_binary-2.9.11-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:92e3b669236327083a2e33ccfa0d320dd01b9803b3e14dd986a4fc54aa00f4e1", size = 3756567, upload-time = "2025-10-10T11:13:11.885Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/a9/9d55c614a891288f15ca4b5209b09f0f01e3124056924e17b81b9fa054cc/psycopg2_binary-2.9.11-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e0deeb03da539fa3577fcb0b3f2554a97f7e5477c246098dbb18091a4a01c16f", size = 3864755, upload-time = "2025-10-10T11:13:17.727Z" },
+ { url = "https://files.pythonhosted.org/packages/13/1e/98874ce72fd29cbde93209977b196a2edae03f8490d1bd8158e7f1daf3a0/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9b52a3f9bb540a3e4ec0f6ba6d31339727b2950c9772850d6545b7eae0b9d7c5", size = 4411646, upload-time = "2025-10-10T11:13:24.432Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/bd/a335ce6645334fb8d758cc358810defca14a1d19ffbc8a10bd38a2328565/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:db4fd476874ccfdbb630a54426964959e58da4c61c9feba73e6094d51303d7d8", size = 4468701, upload-time = "2025-10-10T11:13:29.266Z" },
+ { url = "https://files.pythonhosted.org/packages/44/d6/c8b4f53f34e295e45709b7568bf9b9407a612ea30387d35eb9fa84f269b4/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47f212c1d3be608a12937cc131bd85502954398aaa1320cb4c14421a0ffccf4c", size = 4166293, upload-time = "2025-10-10T11:13:33.336Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/e0/f8cc36eadd1b716ab36bb290618a3292e009867e5c97ce4aba908cb99644/psycopg2_binary-2.9.11-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e35b7abae2b0adab776add56111df1735ccc71406e56203515e228a8dc07089f", size = 3983184, upload-time = "2025-10-30T02:55:32.483Z" },
+ { url = "https://files.pythonhosted.org/packages/53/3e/2a8fe18a4e61cfb3417da67b6318e12691772c0696d79434184a511906dc/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fcf21be3ce5f5659daefd2b3b3b6e4727b028221ddc94e6c1523425579664747", size = 3652650, upload-time = "2025-10-10T11:13:38.181Z" },
+ { url = "https://files.pythonhosted.org/packages/76/36/03801461b31b29fe58d228c24388f999fe814dfc302856e0d17f97d7c54d/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9bd81e64e8de111237737b29d68039b9c813bdf520156af36d26819c9a979e5f", size = 3298663, upload-time = "2025-10-10T11:13:44.878Z" },
+ { url = "https://files.pythonhosted.org/packages/97/77/21b0ea2e1a73aa5fa9222b2a6b8ba325c43c3a8d54272839c991f2345656/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:32770a4d666fbdafab017086655bcddab791d7cb260a16679cc5a7338b64343b", size = 3044737, upload-time = "2025-10-30T02:55:35.69Z" },
+ { url = "https://files.pythonhosted.org/packages/67/69/f36abe5f118c1dca6d3726ceae164b9356985805480731ac6712a63f24f0/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c3cb3a676873d7506825221045bd70e0427c905b9c8ee8d6acd70cfcbd6e576d", size = 3347643, upload-time = "2025-10-10T11:13:53.499Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/36/9c0c326fe3a4227953dfb29f5d0c8ae3b8eb8c1cd2967aa569f50cb3c61f/psycopg2_binary-2.9.11-cp314-cp314-win_amd64.whl", hash = "sha256:4012c9c954dfaccd28f94e84ab9f94e12df76b4afb22331b1f0d3154893a6316", size = 2803913, upload-time = "2025-10-10T11:13:57.058Z" },
+]
+
+[[package]]
+name = "pycparser"
+version = "3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" },
+]
+
+[[package]]
+name = "pydantic"
+version = "2.12.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "annotated-types" },
+ { name = "pydantic-core" },
+ { name = "typing-extensions" },
+ { name = "typing-inspection" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" },
+]
+
+[[package]]
+name = "pydantic-core"
+version = "2.41.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" },
+ { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" },
+ { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" },
+ { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" },
+ { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" },
+ { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" },
+ { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" },
+ { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" },
+ { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" },
+ { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" },
+ { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" },
+ { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" },
+ { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" },
+ { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" },
+ { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" },
+ { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" },
+ { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" },
+ { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" },
+ { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" },
+ { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" },
+ { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" },
+ { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" },
+ { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" },
+ { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" },
+ { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" },
+ { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" },
+ { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" },
+ { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" },
+ { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" },
+ { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" },
+ { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" },
+ { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" },
+ { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" },
+ { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" },
+ { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" },
+ { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" },
+ { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" },
+ { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" },
+ { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" },
+ { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" },
+ { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" },
+ { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" },
+ { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" },
+]
+
+[[package]]
+name = "pydantic-settings"
+version = "2.13.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pydantic" },
+ { name = "python-dotenv" },
+ { name = "typing-inspection" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/52/6d/fffca34caecc4a3f97bda81b2098da5e8ab7efc9a66e819074a11955d87e/pydantic_settings-2.13.1.tar.gz", hash = "sha256:b4c11847b15237fb0171e1462bf540e294affb9b86db4d9aa5c01730bdbe4025", size = 223826, upload-time = "2026-02-19T13:45:08.055Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/00/4b/ccc026168948fec4f7555b9164c724cf4125eac006e176541483d2c959be/pydantic_settings-2.13.1-py3-none-any.whl", hash = "sha256:d56fd801823dbeae7f0975e1f8c8e25c258eb75d278ea7abb5d9cebb01b56237", size = 58929, upload-time = "2026-02-19T13:45:06.034Z" },
+]
+
+[[package]]
+name = "pygments"
+version = "2.19.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
+]
+
+[[package]]
+name = "pyjwt"
+version = "2.11.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/5c/5a/b46fa56bf322901eee5b0454a34343cdbdae202cd421775a8ee4e42fd519/pyjwt-2.11.0.tar.gz", hash = "sha256:35f95c1f0fbe5d5ba6e43f00271c275f7a1a4db1dab27bf708073b75318ea623", size = 98019, upload-time = "2026-01-30T19:59:55.694Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6f/01/c26ce75ba460d5cd503da9e13b21a33804d38c2165dec7b716d06b13010c/pyjwt-2.11.0-py3-none-any.whl", hash = "sha256:94a6bde30eb5c8e04fee991062b534071fd1439ef58d2adc9ccb823e7bcd0469", size = 28224, upload-time = "2026-01-30T19:59:54.539Z" },
+]
+
+[package.optional-dependencies]
+crypto = [
+ { name = "cryptography" },
+]
+
+[[package]]
+name = "pyperclip"
+version = "1.11.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e8/52/d87eba7cb129b81563019d1679026e7a112ef76855d6159d24754dbd2a51/pyperclip-1.11.0.tar.gz", hash = "sha256:244035963e4428530d9e3a6101a1ef97209c6825edab1567beac148ccc1db1b6", size = 12185, upload-time = "2025-09-26T14:40:37.245Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/df/80/fc9d01d5ed37ba4c42ca2b55b4339ae6e200b456be3a1aaddf4a9fa99b8c/pyperclip-1.11.0-py3-none-any.whl", hash = "sha256:299403e9ff44581cb9ba2ffeed69c7aa96a008622ad0c46cb575ca75b5b84273", size = 11063, upload-time = "2025-09-26T14:40:36.069Z" },
+]
+
+[[package]]
+name = "python-dotenv"
+version = "1.2.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" },
+]
+
+[[package]]
+name = "python-multipart"
+version = "0.0.22"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" },
+]
+
+[[package]]
+name = "pywin32"
+version = "311"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7b/40/44efbb0dfbd33aca6a6483191dae0716070ed99e2ecb0c53683f400a0b4f/pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3", size = 8760432, upload-time = "2025-07-14T20:13:05.9Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/bf/360243b1e953bd254a82f12653974be395ba880e7ec23e3731d9f73921cc/pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b", size = 9590103, upload-time = "2025-07-14T20:13:07.698Z" },
+ { url = "https://files.pythonhosted.org/packages/57/38/d290720e6f138086fb3d5ffe0b6caa019a791dd57866940c82e4eeaf2012/pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b", size = 8778557, upload-time = "2025-07-14T20:13:11.11Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" },
+ { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" },
+ { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" },
+ { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" },
+ { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" },
+]
+
+[[package]]
+name = "referencing"
+version = "0.37.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "attrs" },
+ { name = "rpds-py" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" },
+]
+
+[[package]]
+name = "requests"
+version = "2.32.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "charset-normalizer" },
+ { name = "idna" },
+ { name = "urllib3" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
+]
+
+[[package]]
+name = "rich"
+version = "14.3.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markdown-it-py" },
+ { name = "pygments" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" },
+]
+
+[[package]]
+name = "rpds-py"
+version = "0.30.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/06/0c/0c411a0ec64ccb6d104dcabe0e713e05e153a9a2c3c2bd2b32ce412166fe/rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288", size = 370490, upload-time = "2025-11-30T20:21:33.256Z" },
+ { url = "https://files.pythonhosted.org/packages/19/6a/4ba3d0fb7297ebae71171822554abe48d7cab29c28b8f9f2c04b79988c05/rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00", size = 359751, upload-time = "2025-11-30T20:21:34.591Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/7c/e4933565ef7f7a0818985d87c15d9d273f1a649afa6a52ea35ad011195ea/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6", size = 389696, upload-time = "2025-11-30T20:21:36.122Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/01/6271a2511ad0815f00f7ed4390cf2567bec1d4b1da39e2c27a41e6e3b4de/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7", size = 403136, upload-time = "2025-11-30T20:21:37.728Z" },
+ { url = "https://files.pythonhosted.org/packages/55/64/c857eb7cd7541e9b4eee9d49c196e833128a55b89a9850a9c9ac33ccf897/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324", size = 524699, upload-time = "2025-11-30T20:21:38.92Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/ed/94816543404078af9ab26159c44f9e98e20fe47e2126d5d32c9d9948d10a/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df", size = 412022, upload-time = "2025-11-30T20:21:40.407Z" },
+ { url = "https://files.pythonhosted.org/packages/61/b5/707f6cf0066a6412aacc11d17920ea2e19e5b2f04081c64526eb35b5c6e7/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3", size = 390522, upload-time = "2025-11-30T20:21:42.17Z" },
+ { url = "https://files.pythonhosted.org/packages/13/4e/57a85fda37a229ff4226f8cbcf09f2a455d1ed20e802ce5b2b4a7f5ed053/rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221", size = 404579, upload-time = "2025-11-30T20:21:43.769Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/da/c9339293513ec680a721e0e16bf2bac3db6e5d7e922488de471308349bba/rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7", size = 421305, upload-time = "2025-11-30T20:21:44.994Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/be/522cb84751114f4ad9d822ff5a1aa3c98006341895d5f084779b99596e5c/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff", size = 572503, upload-time = "2025-11-30T20:21:46.91Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/9b/de879f7e7ceddc973ea6e4629e9b380213a6938a249e94b0cdbcc325bb66/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7", size = 598322, upload-time = "2025-11-30T20:21:48.709Z" },
+ { url = "https://files.pythonhosted.org/packages/48/ac/f01fc22efec3f37d8a914fc1b2fb9bcafd56a299edbe96406f3053edea5a/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139", size = 560792, upload-time = "2025-11-30T20:21:50.024Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/da/4e2b19d0f131f35b6146425f846563d0ce036763e38913d917187307a671/rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464", size = 221901, upload-time = "2025-11-30T20:21:51.32Z" },
+ { url = "https://files.pythonhosted.org/packages/96/cb/156d7a5cf4f78a7cc571465d8aec7a3c447c94f6749c5123f08438bcf7bc/rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169", size = 235823, upload-time = "2025-11-30T20:21:52.505Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" },
+ { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" },
+ { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" },
+ { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" },
+ { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" },
+ { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" },
+ { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" },
+ { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" },
+ { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" },
+ { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" },
+ { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" },
+ { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" },
+ { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" },
+ { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" },
+ { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" },
+ { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" },
+ { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" },
+ { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" },
+ { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" },
+ { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" },
+ { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" },
+ { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" },
+ { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" },
+ { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" },
+ { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" },
+ { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" },
+ { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" },
+ { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" },
+ { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" },
+ { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" },
+ { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" },
+ { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" },
+ { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" },
+ { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" },
+ { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" },
+ { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" },
+ { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" },
+ { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" },
+ { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" },
+ { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" },
+ { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" },
+ { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" },
+ { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" },
+ { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" },
+ { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" },
+ { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" },
+]
+
+[[package]]
+name = "shellingham"
+version = "1.5.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
+]
+
+[[package]]
+name = "sse-starlette"
+version = "3.2.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "starlette" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/8b/8d/00d280c03ffd39aaee0e86ec81e2d3b9253036a0f93f51d10503adef0e65/sse_starlette-3.2.0.tar.gz", hash = "sha256:8127594edfb51abe44eac9c49e59b0b01f1039d0c7461c6fd91d4e03b70da422", size = 27253, upload-time = "2026-01-17T13:11:05.62Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/96/7f/832f015020844a8b8f7a9cbc103dd76ba8e3875004c41e08440ea3a2b41a/sse_starlette-3.2.0-py3-none-any.whl", hash = "sha256:5876954bd51920fc2cd51baee47a080eb88a37b5b784e615abb0b283f801cdbf", size = 12763, upload-time = "2026-01-17T13:11:03.775Z" },
+]
+
+[[package]]
+name = "starlette"
+version = "0.52.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c4/68/79977123bb7be889ad680d79a40f339082c1978b5cfcf62c2d8d196873ac/starlette-0.52.1.tar.gz", hash = "sha256:834edd1b0a23167694292e94f597773bc3f89f362be6effee198165a35d62933", size = 2653702, upload-time = "2026-01-18T13:34:11.062Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl", hash = "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74", size = 74272, upload-time = "2026-01-18T13:34:09.188Z" },
+]
+
+[[package]]
+name = "typer"
+version = "0.24.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "annotated-doc" },
+ { name = "click" },
+ { name = "rich" },
+ { name = "shellingham" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/f5/24/cb09efec5cc954f7f9b930bf8279447d24618bb6758d4f6adf2574c41780/typer-0.24.1.tar.gz", hash = "sha256:e39b4732d65fbdcde189ae76cf7cd48aeae72919dea1fdfc16593be016256b45", size = 118613, upload-time = "2026-02-21T16:54:40.609Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl", hash = "sha256:112c1f0ce578bfb4cab9ffdabc68f031416ebcc216536611ba21f04e9aa84c9e", size = 56085, upload-time = "2026-02-21T16:54:41.616Z" },
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.15.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
+]
+
+[[package]]
+name = "typing-inspection"
+version = "0.4.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
+]
+
+[[package]]
+name = "urllib3"
+version = "2.6.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
+]
+
+[[package]]
+name = "uvicorn"
+version = "0.41.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+ { name = "h11" },
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/32/ce/eeb58ae4ac36fe09e3842eb02e0eb676bf2c53ae062b98f1b2531673efdd/uvicorn-0.41.0.tar.gz", hash = "sha256:09d11cf7008da33113824ee5a1c6422d89fbc2ff476540d69a34c87fab8b571a", size = 82633, upload-time = "2026-02-16T23:07:24.1Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/83/e4/d04a086285c20886c0daad0e026f250869201013d18f81d9ff5eada73a88/uvicorn-0.41.0-py3-none-any.whl", hash = "sha256:29e35b1d2c36a04b9e180d4007ede3bcb32a85fbdfd6c6aeb3f26839de088187", size = 68783, upload-time = "2026-02-16T23:07:22.357Z" },
+]
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/.github/FUNDING.yml b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/.github/FUNDING.yml
new file mode 100644
index 00000000..47b92047
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/.github/FUNDING.yml
@@ -0,0 +1,3 @@
+# These are supported funding model platforms
+
+github: blazickjp # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/.github/workflows/lint.yml b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/.github/workflows/lint.yml
new file mode 100644
index 00000000..b66e502c
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/.github/workflows/lint.yml
@@ -0,0 +1,27 @@
+name: Lint
+
+on:
+ push:
+ branches: [ main, master ]
+ pull_request:
+ branches: [ main, master ]
+ types: [opened, synchronize, reopened]
+
+jobs:
+ lint:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: '3.11'
+
+ - name: Install dependencies
+ run: |
+ pip install black
+
+ - name: Check code formatting with Black
+ run: |
+ black --check .
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/.github/workflows/publish.yml b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/.github/workflows/publish.yml
new file mode 100644
index 00000000..1e39e311
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/.github/workflows/publish.yml
@@ -0,0 +1,30 @@
+name: Publish to PyPI
+
+on:
+ release:
+ types: [published]
+
+jobs:
+ publish:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: '3.11'
+
+ - name: Install build dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install build hatchling
+
+ - name: Build package
+ run: python -m build
+
+ - name: Publish to PyPI
+ uses: pypa/gh-action-pypi-publish@v1.8.11
+ with:
+ user: __token__
+ password: ${{ secrets.PYPI_API_TOKEN }}
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/.github/workflows/tests.yml b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/.github/workflows/tests.yml
new file mode 100644
index 00000000..0ac3ce4b
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/.github/workflows/tests.yml
@@ -0,0 +1,61 @@
+name: Run Tests
+
+on:
+ push:
+ branches: [ main, master ]
+ pull_request:
+ branches: [ main, master ]
+ types: [opened, synchronize, reopened]
+
+jobs:
+ test:
+ strategy:
+ matrix:
+ python-version: ["3.11", "3.12"]
+ os: [ubuntu-latest, windows-latest, macos-latest]
+
+ runs-on: ${{ matrix.os }}
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v5
+ with:
+ python-version: ${{ matrix.python-version }}
+
+ - name: Install uv (Linux/macOS)
+ if: runner.os != 'Windows'
+ run: |
+ curl -LsSf https://astral.sh/uv/install.sh | sh
+ echo "$HOME/.cargo/bin" >> $GITHUB_PATH
+
+ - name: Install uv (Windows)
+ if: runner.os == 'Windows'
+ run: |
+ # Install uv
+ iwr -useb https://astral.sh/uv/install.ps1 | iex
+ # Add uv to PATH
+ echo "$HOME\.uv\bin" >> $GITHUB_PATH
+
+ - name: Install dependencies
+ run: |
+ uv pip install --system pytest pytest-cov pytest-asyncio
+ uv pip install --system -e ".[test]"
+ # If you don't have a [test] extra, use:
+ # uv pip install --system -r requirements-test.txt
+ # or just:
+ # uv pip install --system -e .
+
+ # Run tests differently based on platform
+ - name: Run tests on Linux/macOS
+ if: runner.os != 'Windows'
+ run: |
+ pytest --cov=./ --cov-report=xml -v
+
+ - name: Run tests on Windows
+ if: runner.os == 'Windows'
+ run: |
+ pytest --cov=./ --cov-report=xml -v
+
+
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/.gitignore b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/.gitignore
new file mode 100644
index 00000000..6033070f
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/.gitignore
@@ -0,0 +1,40 @@
+# Python
+__pycache__/
+*.py[cod]
+*$py.class
+*.so
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+*.coverage
+*.DS_Store
+
+# Virtual Environment
+venv/
+env/
+ENV/
+
+# IDE
+.idea/
+.vscode/
+*.swp
+*.swo
+
+# Logs
+*.log
+
+# Local development settings
+.env
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/.pre-commit-config.yaml b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/.pre-commit-config.yaml
new file mode 100644
index 00000000..0f7c5c96
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/.pre-commit-config.yaml
@@ -0,0 +1,6 @@
+repos:
+- repo: https://github.com/psf/black
+ rev: 23.3.0
+ hooks:
+ - id: black
+ language_version: python3.11
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/.python-version b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/.python-version
new file mode 100644
index 00000000..2c073331
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/.python-version
@@ -0,0 +1 @@
+3.11
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/CLAUDE.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/CLAUDE.md
new file mode 100644
index 00000000..561df1a4
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/CLAUDE.md
@@ -0,0 +1,72 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## Development Commands
+
+### Environment Setup
+```bash
+# Create and activate virtual environment
+uv venv
+source .venv/bin/activate
+
+# Install with test dependencies
+uv pip install -e ".[test]"
+```
+
+### Testing
+```bash
+# Run all tests with coverage
+python -m pytest
+
+# Run specific test file
+python -m pytest tests/tools/test_search.py
+
+# Run tests with verbose output
+python -m pytest -v
+```
+
+### Running the Server
+```bash
+# Run as module
+python -m arxiv_mcp_server
+
+# Or via entry point
+arxiv-mcp-server
+```
+
+## Architecture Overview
+
+This is an **MCP (Message Control Protocol) server** that provides AI models access to arXiv research papers. The codebase follows a modular architecture with four main layers:
+
+### Core Components
+
+1. **Server Layer** (`server.py`): Main MCP server implementation that handles tool registration and request routing
+2. **Tools Layer** (`tools/`): Individual MCP tools for paper operations:
+ - `search.py`: Advanced arXiv paper search with filtering
+ - `download.py`: Paper download and storage management
+ - `list_papers.py`: List locally stored papers
+ - `read_paper.py`: Read paper content from storage
+3. **Resource Management** (`resources/papers.py`): `PaperManager` class handles paper storage, PDF-to-markdown conversion using pymupdf4llm, and local caching
+4. **Configuration** (`config.py`): Pydantic-based settings with environment variable support
+
+### Key Design Patterns
+
+- **MCP Protocol Compliance**: All tools follow MCP specification with proper type definitions
+- **Async-First**: Built on asyncio with aiofiles for non-blocking I/O operations
+- **Storage Strategy**: Papers downloaded as PDFs, converted to markdown, stored locally with PDF cleanup
+- **Error Handling**: Comprehensive error handling with user-friendly messages throughout tool chain
+
+### Configuration
+
+Environment variables (all optional with sensible defaults):
+- `ARXIV_STORAGE_PATH`: Paper storage location (default: `~/.arxiv-mcp-server/papers`)
+- `ARXIV_MAX_RESULTS`: Search results limit (default: 50)
+- `ARXIV_REQUEST_TIMEOUT`: API timeout in seconds (default: 60)
+
+### Testing Strategy
+
+Tests use pytest with async support and comprehensive mocking:
+- `conftest.py` provides shared fixtures for mock arXiv papers and HTTP responses
+- Tests cover both unit-level tool functionality and integration scenarios
+- Mock-based approach avoids external API calls during testing
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/Dockerfile b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/Dockerfile
new file mode 100644
index 00000000..7929997d
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/Dockerfile
@@ -0,0 +1,37 @@
+# Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile
+# Use a Python base image with uv pre-installed
+FROM ghcr.io/astral-sh/uv:python3.11-slim AS uv
+
+# Set the working directory in the container
+WORKDIR /app
+
+# Enable bytecode compilation for better performance
+ENV UV_COMPILE_BYTECODE=1
+
+# Use copy mode for mounting cache to avoid linking issues
+ENV UV_LINK_MODE=copy
+
+# Install project dependencies using uv
+RUN --mount=type=cache,target=/root/.cache/uv --mount=type=bind,source=pyproject.toml,target=pyproject.toml --mount=type=bind,source=uv.lock,target=uv.lock uv sync --frozen --no-install-project --no-dev --no-editable
+
+# Copy the rest of the application code
+ADD . /app
+
+# Install the application
+RUN --mount=type=cache,target=/root/.cache/uv uv sync --frozen --no-dev --no-editable
+
+# Create a minimal Python environment
+FROM python:3.11-slim-bookworm
+
+# Set the working directory in the container
+WORKDIR /app
+
+# Copy the installed dependencies and the virtual environment
+COPY --from=uv /root/.local /root/.local
+COPY --from=uv --chown=app:app /app/.venv /app/.venv
+
+# Set the PATH to include the virtual environment
+ENV PATH="/app/.venv/bin:$PATH"
+
+# Set the default entrypoint
+ENTRYPOINT ["python", "-m", "arxiv_mcp_server"]
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/LICENSE b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/LICENSE
new file mode 100644
index 00000000..261eeb9e
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/README.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/README.md
new file mode 100644
index 00000000..e12fd22a
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/README.md
@@ -0,0 +1,195 @@
+[](https://twitter.com/JoeBlazick)
+[](https://smithery.ai/server/arxiv-mcp-server)
+[](https://www.python.org/downloads/)
+[](https://github.com/blazickjp/arxiv-mcp-server/actions/workflows/tests.yml)
+[](https://opensource.org/licenses/MIT)
+[](https://pypi.org/project/arxiv-mcp-server/)
+[](https://pypi.org/project/arxiv-mcp-server/)
+
+# ArXiv MCP Server
+
+> 🔍 Enable AI assistants to search and access arXiv papers through a simple MCP interface.
+
+The ArXiv MCP Server provides a bridge between AI assistants and arXiv's research repository through the Model Context Protocol (MCP). It allows AI models to search for papers and access their content in a programmatic way.
+
+
+
+🤝 **[Contribute](https://github.com/blazickjp/arxiv-mcp-server/blob/main/CONTRIBUTING.md)** •
+📝 **[Report Bug](https://github.com/blazickjp/arxiv-mcp-server/issues)**
+
+
+
+
+## ✨ Core Features
+
+- 🔎 **Paper Search**: Query arXiv papers with filters for date ranges and categories
+- 📄 **Paper Access**: Download and read paper content
+- 📋 **Paper Listing**: View all downloaded papers
+- 🗃️ **Local Storage**: Papers are saved locally for faster access
+- 📝 **Prompts**: A Set of Research Prompts
+
+## 🚀 Quick Start
+
+### Installing via Smithery
+
+To install ArXiv Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/arxiv-mcp-server):
+
+```bash
+npx -y @smithery/cli install arxiv-mcp-server --client claude
+```
+
+### Installing Manually
+Install using uv:
+
+```bash
+uv tool install arxiv-mcp-server
+```
+
+For development:
+
+```bash
+# Clone and set up development environment
+git clone https://github.com/blazickjp/arxiv-mcp-server.git
+cd arxiv-mcp-server
+
+# Create and activate virtual environment
+uv venv
+source .venv/bin/activate
+
+# Install with test dependencies
+uv pip install -e ".[test]"
+```
+
+### 🔌 MCP Integration
+
+Add this configuration to your MCP client config file:
+
+```json
+{
+ "mcpServers": {
+ "arxiv-mcp-server": {
+ "command": "uv",
+ "args": [
+ "tool",
+ "run",
+ "arxiv-mcp-server",
+ "--storage-path", "/path/to/paper/storage"
+ ]
+ }
+ }
+}
+```
+
+For Development:
+
+```json
+{
+ "mcpServers": {
+ "arxiv-mcp-server": {
+ "command": "uv",
+ "args": [
+ "--directory",
+ "path/to/cloned/arxiv-mcp-server",
+ "run",
+ "arxiv-mcp-server",
+ "--storage-path", "/path/to/paper/storage"
+ ]
+ }
+ }
+}
+```
+
+## 💡 Available Tools
+
+The server provides four main tools:
+
+### 1. Paper Search
+Search for papers with optional filters:
+
+```python
+result = await call_tool("search_papers", {
+ "query": "transformer architecture",
+ "max_results": 10,
+ "date_from": "2023-01-01",
+ "categories": ["cs.AI", "cs.LG"]
+})
+```
+
+### 2. Paper Download
+Download a paper by its arXiv ID:
+
+```python
+result = await call_tool("download_paper", {
+ "paper_id": "2401.12345"
+})
+```
+
+### 3. List Papers
+View all downloaded papers:
+
+```python
+result = await call_tool("list_papers", {})
+```
+
+### 4. Read Paper
+Access the content of a downloaded paper:
+
+```python
+result = await call_tool("read_paper", {
+ "paper_id": "2401.12345"
+})
+```
+
+## 📝 Research Prompts
+
+The server offers specialized prompts to help analyze academic papers:
+
+### Paper Analysis Prompt
+A comprehensive workflow for analyzing academic papers that only requires a paper ID:
+
+```python
+result = await call_prompt("deep-paper-analysis", {
+ "paper_id": "2401.12345"
+})
+```
+
+This prompt includes:
+- Detailed instructions for using available tools (list_papers, download_paper, read_paper, search_papers)
+- A systematic workflow for paper analysis
+- Comprehensive analysis structure covering:
+ - Executive summary
+ - Research context
+ - Methodology analysis
+ - Results evaluation
+ - Practical and theoretical implications
+ - Future research directions
+ - Broader impacts
+
+## ⚙️ Configuration
+
+Configure through environment variables:
+
+| Variable | Purpose | Default |
+|----------|---------|---------|
+| `ARXIV_STORAGE_PATH` | Paper storage location | ~/.arxiv-mcp-server/papers |
+
+## 🧪 Testing
+
+Run the test suite:
+
+```bash
+python -m pytest
+```
+
+## 📄 License
+
+Released under the MIT License. See the LICENSE file for details.
+
+---
+
+
+
+Made with ❤️ by the Pearl Labs Team
+
+
+
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/pyproject.toml b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/pyproject.toml
new file mode 100644
index 00000000..dd4737be
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/pyproject.toml
@@ -0,0 +1,77 @@
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[project]
+name = "arxiv-mcp-server"
+version = "0.3.2"
+description = "A flexible arXiv search and analysis service with MCP protocol support"
+readme = "README.md"
+requires-python = ">=3.11"
+license = { text = "MIT" }
+authors = [
+ { name = "Joseph Blazick", email = "blazickjp@amazon.com" }
+]
+dependencies = [
+ "arxiv>=2.1.0",
+ "httpx>=0.24.0",
+ "python-dateutil>=2.8.2",
+ "pydantic>=2.8.0",
+ "mcp>=1.2.0",
+ "pymupdf4llm>=0.0.17",
+ "aiohttp>=3.9.1",
+ "python-dotenv>=1.0.0",
+ "pydantic-settings>=2.1.0",
+ "aiofiles>=23.2.1",
+ "uvicorn>=0.30.0",
+ "sse-starlette>=1.8.2",
+ "anyio>=4.2.0",
+ "black>=25.1.0",
+ "pymupdf-layout>=1.26.6",
+ "psycopg2-binary>=2.9.11",
+]
+
+[project.optional-dependencies]
+test = [
+ "pytest>=8.0.0",
+ "pytest-asyncio>=0.23.5",
+ "pytest-cov>=4.1.0",
+ "pytest-mock>=3.10.0",
+ "aioresponses>=0.7.6"
+]
+dev = [
+ "black>=23.3.0"
+]
+
+[tool.pytest.ini_options]
+asyncio_mode = "auto"
+asyncio_fixture_loop_scope = "function" # Added this line
+testpaths = ["tests"]
+addopts = "-v --cov=arxiv_mcp_server"
+
+[project.scripts]
+arxiv-mcp-server = "arxiv_mcp_server:main"
+
+[tool.hatch.build.targets.wheel]
+packages = ["src/arxiv_mcp_server"]
+
+[tool.hatch.metadata]
+allow-direct-references = true
+
+[tool.black]
+line-length = 88
+target-version = ["py311"]
+include = '\.pyi?$'
+exclude = '''
+/(
+ \.git
+ | \.hg
+ | \.mypy_cache
+ | \.tox
+ | \.venv
+ | _build
+ | buck-out
+ | build
+ | dist
+)/
+'''
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/smithery.yaml b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/smithery.yaml
new file mode 100644
index 00000000..d9164a5c
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/smithery.yaml
@@ -0,0 +1,17 @@
+# Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml
+
+startCommand:
+ type: stdio
+ configSchema:
+ # JSON Schema defining the configuration options for the MCP.
+ type: object
+ required:
+ - arxivStoragePath
+ properties:
+ arxivStoragePath:
+ type: string
+ description: The path to store downloaded papers.
+ commandFunction:
+ # A function that produces the CLI command to start the MCP on stdio.
+ |-
+ (config) => ({ command: 'python', args: ['-m', 'arxiv_mcp_server'], env: { ARXIV_STORAGE_PATH: config.arxivStoragePath } })
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/__init__.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/__init__.py
new file mode 100644
index 00000000..46fd7d7e
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/__init__.py
@@ -0,0 +1,14 @@
+"""
+Arxiv MCP Server initialization
+"""
+
+from . import server
+import asyncio
+
+
+def main():
+ """Main entry point for the package."""
+ asyncio.run(server.main())
+
+
+__all__ = ["main", "server"]
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/__main__.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/__main__.py
new file mode 100644
index 00000000..2f44a441
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/__main__.py
@@ -0,0 +1,6 @@
+"""Main entry point for the arxiv-mcp-server package."""
+
+from . import main
+
+if __name__ == "__main__":
+ main()
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/config.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/config.py
new file mode 100644
index 00000000..3ba45b23
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/config.py
@@ -0,0 +1,72 @@
+"""Configuration settings for the arXiv MCP server."""
+
+import sys
+from pydantic_settings import BaseSettings, SettingsConfigDict
+from pathlib import Path
+import logging
+
+logger = logging.getLogger(__name__)
+
+
+class Settings(BaseSettings):
+ """Server configuration settings."""
+
+ APP_NAME: str = "arxiv-mcp-server"
+ APP_VERSION: str = "0.3.2"
+ MAX_RESULTS: int = 50
+ BATCH_SIZE: int = 20
+ REQUEST_TIMEOUT: int = 60
+ HOST: str = "0.0.0.0"
+ PORT: int = 8000
+ model_config = SettingsConfigDict(extra="allow")
+
+ @property
+ def STORAGE_PATH(self) -> Path:
+ """Get the resolved storage path and ensure it exists.
+
+ Returns:
+ Path: The absolute storage path.
+ """
+ path = (
+ self._get_storage_path_from_args()
+ or Path.home() / ".arxiv-mcp-server" / "papers"
+ )
+ path = path.resolve()
+ path.mkdir(parents=True, exist_ok=True)
+ return path
+
+ def _get_storage_path_from_args(self) -> Path | None:
+ """Extract storage path from command line arguments.
+
+ Returns:
+ Path | None: The storage path if specified in arguments, None otherwise.
+ """
+ args = sys.argv[1:]
+
+ # If not enough arguments
+ if len(args) < 2:
+ return None
+
+ # Look for the --storage-path option
+ try:
+ storage_path_index = args.index("--storage-path")
+ except ValueError:
+ return None
+
+ # Early return if --storage-path is the last argument
+ if storage_path_index + 1 >= len(args):
+ return None
+
+ # Try to resolve the path
+ try:
+ path = Path(args[storage_path_index + 1])
+ return path.resolve()
+ except (TypeError, ValueError) as e:
+ # TypeError: If the path argument is not string-like
+ # ValueError: If the path string is malformed
+ logger.warning(f"Invalid storage path format: {e}")
+ except OSError as e:
+ # OSError: If the path contains invalid characters or is too long
+ logger.warning(f"Invalid storage path: {e}")
+
+ return None
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/pg_adapter.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/pg_adapter.py
new file mode 100644
index 00000000..a8a09f98
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/pg_adapter.py
@@ -0,0 +1,296 @@
+"""PostgreSQL adapter replacing arxiv API calls for the arxiv-mcp-server."""
+
+import os
+import json
+import logging
+import psycopg2
+import psycopg2.extras
+from typing import Dict, Any, List
+import mcp.types as types
+from .config import Settings
+from .tools import search_tool, download_tool, list_tool, read_tool # noqa: F401
+
+logger = logging.getLogger("arxiv-mcp-server")
+settings = Settings()
+
+
+def _get_conn():
+ return psycopg2.connect(
+ host=os.environ.get('PG_HOST', 'localhost'),
+ port=int(os.environ.get('PG_PORT', '5432')),
+ dbname=os.environ.get('PG_DATABASE', 'toolathlon'),
+ user=os.environ.get('PG_USER', 'postgres'),
+ password=os.environ.get('PG_PASSWORD', 'postgres'),
+ )
+
+
+# ---- search.py replacement ----
+
+async def handle_search(arguments: Dict[str, Any]) -> List[types.TextContent]:
+ """Handle paper search using PostgreSQL instead of arXiv API."""
+ try:
+ max_results = min(int(arguments.get("max_results", 10)), settings.MAX_RESULTS)
+ base_query = arguments["query"]
+ date_from = arguments.get("date_from")
+ date_to = arguments.get("date_to")
+ categories = arguments.get("categories")
+ sort_by = arguments.get("sort_by", "relevance")
+
+ conn = _get_conn()
+ try:
+ with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur:
+ conditions = []
+ params = []
+
+ # Full-text search
+ if base_query.strip():
+ conditions.append(
+ "to_tsvector('english', coalesce(title,'') || ' ' || coalesce(summary,'')) "
+ "@@ plainto_tsquery('english', %s)"
+ )
+ params.append(base_query)
+
+ # Date filtering
+ if date_from:
+ conditions.append("published >= %s::timestamptz")
+ params.append(date_from)
+ if date_to:
+ conditions.append("published <= %s::timestamptz")
+ params.append(date_to + "T23:59:59Z" if "T" not in date_to else date_to)
+
+ # Category filtering
+ if categories:
+ cat_conditions = []
+ for cat in categories:
+ cat_conditions.append("categories @> %s::jsonb")
+ params.append(json.dumps([cat]))
+ conditions.append("(" + " OR ".join(cat_conditions) + ")")
+
+ where_clause = " AND ".join(conditions) if conditions else "TRUE"
+
+ # Sort
+ if sort_by == "date":
+ order_clause = "published DESC NULLS LAST"
+ else:
+ if base_query.strip():
+ order_clause = (
+ "ts_rank(to_tsvector('english', coalesce(title,'') || ' ' || coalesce(summary,'')), "
+ "plainto_tsquery('english', %s)) DESC"
+ )
+ params.append(base_query)
+ else:
+ order_clause = "published DESC NULLS LAST"
+
+ params.append(max_results)
+
+ sql = f"""
+ SELECT id, title, authors, summary, categories, primary_category,
+ published, doi, journal_ref, comment, pdf_url, links
+ FROM arxiv.papers
+ WHERE {where_clause}
+ ORDER BY {order_clause}
+ LIMIT %s
+ """
+ cur.execute(sql, params)
+ rows = cur.fetchall()
+
+ # Fallback to ILIKE if FTS returns nothing
+ if not rows and base_query.strip():
+ with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur:
+ cur.execute("""
+ SELECT id, title, authors, summary, categories, primary_category,
+ published, doi, journal_ref, comment, pdf_url, links
+ FROM arxiv.papers
+ WHERE title ILIKE %s OR summary ILIKE %s
+ LIMIT %s
+ """, (f'%{base_query}%', f'%{base_query}%', max_results))
+ rows = cur.fetchall()
+
+ results = []
+ for r in rows:
+ authors = r['authors'] or []
+ if isinstance(authors, list):
+ authors = [a['name'] if isinstance(a, dict) else str(a) for a in authors]
+ else:
+ authors = [str(authors)]
+
+ paper_id = r['id']
+ short_id = paper_id.split("v")[0] if "v" in paper_id else paper_id
+
+ results.append({
+ "id": short_id,
+ "title": r['title'],
+ "authors": authors,
+ "abstract": r['summary'] or '',
+ "categories": r['categories'] or [],
+ "published": r['published'].isoformat() if r['published'] else '',
+ "url": r.get('pdf_url') or f"http://arxiv.org/pdf/{paper_id}",
+ "resource_uri": f"arxiv://{short_id}",
+ })
+
+ response_data = {"total_results": len(results), "papers": results}
+ return [types.TextContent(type="text", text=json.dumps(response_data, indent=2))]
+
+ finally:
+ conn.close()
+
+ except Exception as e:
+ logger.error(f"PG search error: {e}")
+ return [types.TextContent(type="text", text=f"Error: {str(e)}")]
+
+
+# ---- download.py replacement ----
+
+conversion_statuses: Dict[str, Any] = {}
+
+
+async def handle_download(arguments: Dict[str, Any]) -> List[types.TextContent]:
+ """Handle paper download using PostgreSQL (mark as downloaded)."""
+ try:
+ paper_id = arguments["paper_id"]
+ check_status = arguments.get("check_status", False)
+
+ conn = _get_conn()
+ try:
+ with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur:
+ cur.execute("SELECT id, is_downloaded, markdown_content FROM arxiv.papers WHERE id = %s", (paper_id,))
+ row = cur.fetchone()
+
+ if not row:
+ return [types.TextContent(type="text", text=json.dumps({
+ "status": "error",
+ "message": f"Paper {paper_id} not found in database",
+ }))]
+
+ if check_status:
+ if row['is_downloaded']:
+ return [types.TextContent(type="text", text=json.dumps({
+ "status": "success",
+ "message": "Paper is ready",
+ "resource_uri": f"arxiv://{paper_id}",
+ }))]
+ else:
+ return [types.TextContent(type="text", text=json.dumps({
+ "status": "unknown",
+ "message": "Paper not yet downloaded",
+ }))]
+
+ if row['is_downloaded']:
+ return [types.TextContent(type="text", text=json.dumps({
+ "status": "success",
+ "message": "Paper already available",
+ "resource_uri": f"arxiv://{paper_id}",
+ }))]
+
+ # Mark as downloaded
+ with conn.cursor() as cur:
+ cur.execute(
+ "UPDATE arxiv.papers SET is_downloaded = TRUE WHERE id = %s",
+ (paper_id,)
+ )
+ conn.commit()
+
+ return [types.TextContent(type="text", text=json.dumps({
+ "status": "success",
+ "message": "Paper downloaded and converted successfully",
+ "resource_uri": f"arxiv://{paper_id}",
+ }))]
+
+ finally:
+ conn.close()
+
+ except Exception as e:
+ return [types.TextContent(type="text", text=json.dumps({
+ "status": "error",
+ "message": f"Error: {str(e)}",
+ }))]
+
+
+# ---- list_papers.py replacement ----
+
+async def handle_list_papers(arguments=None) -> List[types.TextContent]:
+ """List all downloaded papers from PostgreSQL."""
+ try:
+ conn = _get_conn()
+ try:
+ with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur:
+ cur.execute("""
+ SELECT id, title, summary, authors, pdf_url, links
+ FROM arxiv.papers
+ WHERE is_downloaded = TRUE
+ """)
+ rows = cur.fetchall()
+
+ papers = []
+ for r in rows:
+ authors = r['authors'] or []
+ if isinstance(authors, list):
+ authors = [a['name'] if isinstance(a, dict) else str(a) for a in authors]
+ links_data = r.get('links') or []
+ if isinstance(links_data, list):
+ links = [l['href'] if isinstance(l, dict) else str(l) for l in links_data]
+ else:
+ links = []
+
+ papers.append({
+ "title": r['title'],
+ "summary": r['summary'] or '',
+ "authors": authors,
+ "links": links,
+ "pdf_url": r.get('pdf_url') or f"http://arxiv.org/pdf/{r['id']}",
+ })
+
+ response_data = {"total_papers": len(papers), "papers": papers}
+ return [types.TextContent(type="text", text=json.dumps(response_data, indent=2))]
+
+ finally:
+ conn.close()
+
+ except Exception as e:
+ return [types.TextContent(type="text", text=f"Error: {str(e)}")]
+
+
+# ---- read_paper.py replacement ----
+
+async def handle_read_paper(arguments: Dict[str, Any]) -> List[types.TextContent]:
+ """Read paper content from PostgreSQL."""
+ try:
+ paper_id = arguments["paper_id"]
+
+ conn = _get_conn()
+ try:
+ with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur:
+ cur.execute("""
+ SELECT id, is_downloaded, markdown_content
+ FROM arxiv.papers
+ WHERE id = %s
+ """, (paper_id,))
+ row = cur.fetchone()
+
+ if not row:
+ return [types.TextContent(type="text", text=json.dumps({
+ "status": "error",
+ "message": f"Paper {paper_id} not found in storage. You may need to download it first using download_paper.",
+ }))]
+
+ if not row['is_downloaded']:
+ return [types.TextContent(type="text", text=json.dumps({
+ "status": "error",
+ "message": f"Paper {paper_id} not found in storage. You may need to download it first using download_paper.",
+ }))]
+
+ content = row['markdown_content'] or ''
+ return [types.TextContent(type="text", text=json.dumps({
+ "status": "success",
+ "paper_id": paper_id,
+ "content": content,
+ }))]
+
+ finally:
+ conn.close()
+
+ except Exception as e:
+ return [types.TextContent(type="text", text=json.dumps({
+ "status": "error",
+ "message": f"Error reading paper: {str(e)}",
+ }))]
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/prompts/__init__.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/prompts/__init__.py
new file mode 100644
index 00000000..81ce52cb
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/prompts/__init__.py
@@ -0,0 +1,5 @@
+"""Prompt handling functionality for arXiv MCP server."""
+
+from .handlers import list_prompts, get_prompt
+
+__all__ = ["list_prompts", "get_prompt"]
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/prompts/deep_research_analysis_prompt.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/prompts/deep_research_analysis_prompt.py
new file mode 100644
index 00000000..5a7e2725
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/prompts/deep_research_analysis_prompt.py
@@ -0,0 +1,84 @@
+"""Deep research analysis prompt for the arXiv MCP server."""
+
+# Consolidated comprehensive paper analysis prompt
+PAPER_ANALYSIS_PROMPT = """
+You are an AI research assistant tasked with analyzing academic papers from arXiv. You have access to several tools to help with this analysis:
+
+AVAILABLE TOOLS:
+1. read_paper: Use this tool to retrieve the full content of the paper with the provided arXiv ID
+2. download_paper: If the paper is not already available locally, use this tool to download it first
+3. search_papers: Find related papers on the same topic to provide context
+4. list_papers: Check which papers are already downloaded and available for reading
+
+
+
+ - First, use the list_papers tool to check if the paper is already downloaded
+ - If not found, use the download_paper tool to retrieve it
+ - Then use the read_paper tool with the paper_id to get the full content
+ - If the paper is not found, use the search_papers tool to find related papers while you wait
+ - If you find related papers, use the download_paper tool to get the full content of the related papers and read those too
+
+
+ - Executive Summary:
+ * Summarize the paper in 2-3 sentences
+ * What is the main contribution of the paper?
+ * What is the main problem that the paper solves?
+ * What is the main methodology used in the paper?
+ * What are the main results of the paper?
+ * What is the main conclusion of the paper?
+
+
+ * Research area and specific problem addressed
+ * Key prior approaches and their limitations
+ * How this paper aims to advance the field
+ * How does this paper compare to other papers in the field?
+
+
+ * Step-by-step breakdown of the approach
+ * Key innovations in the methodology
+ * Theoretical foundations and assumptions
+ * Technical implementation details
+ * Algorithmic complexity and performance characteristics
+ * Anything the reader should know about the methodology if they wanted to replicate the paper
+
+
+ * Experimental setup (datasets, benchmarks, metrics)
+ * Main experimental results and their significance
+ * Statistical validity and robustness of results
+ * How results support or challenge the paper's claims
+ * Comparison to state-of-the-art approaches
+
+
+ * How could this be implemented or applied?
+ * Required resources and potential challenges
+ * Available code, datasets, or resources
+
+
+ * How this work advances fundamental understanding
+ * New concepts or paradigms introduced
+ * Challenges to existing theories or assumptions
+ * Open questions raised
+
+
+ * Limitations that future work could address
+ * Promising follow-up research questions
+ * Potential for integration with other approaches
+ * Long-term research agenda this work enables
+
+
+ * Societal, ethical, or policy implications
+ * Environmental or economic considerations
+ * Potential real-world applications and timeframe
+
+
+
+ * Use the search_papers tool to find related work or papers building on this work
+ * Cross-reference findings with other papers you've analyzed
+ * Use your artifacts to create diagrams, pseudocode, and other visualizations to illustrate key concepts
+ * Summarize key results in tables for easy reference
+
+
+Structure your analysis with clear headings, maintain technical accuracy while being accessible, and include your critical assessment where appropriate.
+Your analysis should be comprehensive but concise. Be sure to critically evaluate the statistical significance and
+reproducibility of any reported results.
+"""
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/prompts/handlers.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/prompts/handlers.py
new file mode 100644
index 00000000..9efea12e
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/prompts/handlers.py
@@ -0,0 +1,104 @@
+"""Handlers for prompt-related requests with paper analysis functionality."""
+
+from typing import List, Dict, Optional
+from mcp.types import Prompt, PromptMessage, TextContent, GetPromptResult
+from .prompts import PROMPTS
+from .deep_research_analysis_prompt import PAPER_ANALYSIS_PROMPT
+
+
+# Legacy global research context - used as fallback when no session_id is provided
+class ResearchContext:
+ """Maintains context throughout a research session."""
+
+ def __init__(self):
+ self.expertise_level = "intermediate" # default
+ self.explored_papers = {} # paper_id -> basic metadata
+ self.paper_analyses = {} # paper_id -> analysis focus and summary
+
+ def update_from_arguments(self, args: Dict[str, str]) -> None:
+ """Update context based on new arguments."""
+ if "expertise_level" in args:
+ self.expertise_level = args["expertise_level"]
+ if "paper_id" in args and args["paper_id"] not in self.explored_papers:
+ self.explored_papers[args["paper_id"]] = {"id": args["paper_id"]}
+
+
+# Global research context for backward compatibility
+_research_context = ResearchContext()
+
+# Output structure for deep paper analysis
+OUTPUT_STRUCTURE = """
+Present your analysis with the following structure:
+1. Executive Summary: 3-5 sentence overview of key contributions
+2. Detailed Analysis: Following the specific focus requested
+3. Visual Breakdown: Describe key figures/tables and their significance
+4. Related Work Map: Position this paper within the research landscape
+5. Implementation Notes: Practical considerations for applying these findings
+"""
+
+
+async def list_prompts() -> List[Prompt]:
+ """Handle prompts/list request."""
+ # Filter to only include deep-paper-analysis
+ return [PROMPTS["deep-paper-analysis"]] if "deep-paper-analysis" in PROMPTS else []
+
+
+async def get_prompt(
+ name: str, arguments: Dict[str, str] | None = None, session_id: Optional[str] = None
+) -> GetPromptResult:
+ """Handle prompts/get request for paper analysis.
+
+ Args:
+ name: The name of the prompt to get
+ arguments: The arguments to use with the prompt
+ session_id: Optional user session ID for context persistence
+
+ Returns:
+ GetPromptResult: The resulting prompt with messages
+
+ Raises:
+ ValueError: If prompt not found or arguments invalid
+ """
+ if name != "deep-paper-analysis":
+ raise ValueError(f"Prompt not found: {name}")
+
+ prompt = PROMPTS[name]
+ if arguments is None:
+ raise ValueError(f"No arguments provided for prompt: {name}")
+
+ # Validate required arguments
+ for arg in prompt.arguments:
+ if arg.required and (arg.name not in arguments or not arguments.get(arg.name)):
+ raise ValueError(f"Missing required argument: {arg.name}")
+
+ # Use only global research context since research sessions are removed
+ _research_context.update_from_arguments(arguments)
+
+ # Process deep-paper-analysis prompt
+ paper_id = arguments.get("paper_id", "")
+
+ # Add context from previous papers if available
+ previous_papers_context = ""
+
+ # Use global context
+ if len(_research_context.explored_papers) > 1:
+ previous_ids = [
+ pid for pid in _research_context.explored_papers.keys() if pid != paper_id
+ ]
+ if previous_ids:
+ previous_papers_context = f"\nI've previously analyzed papers: {', '.join(previous_ids)}. If relevant, note connections to these works."
+
+ # Track this analysis in context (for global context only)
+ _research_context.paper_analyses[paper_id] = {"analysis": "complete"}
+
+ return GetPromptResult(
+ messages=[
+ PromptMessage(
+ role="user",
+ content=TextContent(
+ type="text",
+ text=f"Analyze paper {paper_id}.{previous_papers_context}\n\n{OUTPUT_STRUCTURE}\n\n{PAPER_ANALYSIS_PROMPT}",
+ ),
+ )
+ ]
+ )
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/prompts/prompt_manager.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/prompts/prompt_manager.py
new file mode 100644
index 00000000..0e71c2bf
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/prompts/prompt_manager.py
@@ -0,0 +1,31 @@
+"""Research journey prompt management for the arXiv MCP server."""
+
+from typing import Dict, Optional
+from mcp.types import Prompt
+from .prompts import PROMPTS
+
+# Global prompt manager instance
+_prompt_manager: Optional[Dict[str, Prompt]] = None
+
+
+def get_prompt_manager() -> Dict[str, Prompt]:
+ """Get or create the global prompt manager dictionary.
+
+ Returns:
+ Dict[str, Prompt]: Dictionary of available prompts
+ """
+ global _prompt_manager
+ if _prompt_manager is None:
+ _prompt_manager = PROMPTS
+
+ return _prompt_manager
+
+
+def register_prompt(prompt: Prompt) -> None:
+ """Register a new prompt in the prompt manager.
+
+ Args:
+ prompt (Prompt): The prompt to register
+ """
+ manager = get_prompt_manager()
+ manager[prompt.name] = prompt
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/prompts/prompts.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/prompts/prompts.py
new file mode 100644
index 00000000..12863f54
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/prompts/prompts.py
@@ -0,0 +1,83 @@
+"""Prompt definitions for arXiv MCP server with research journey support."""
+
+from mcp.types import (
+ Prompt,
+ PromptArgument,
+)
+
+# Define all prompts
+PROMPTS = {
+ "research-discovery": Prompt(
+ name="research-discovery",
+ description="Begin research exploration on a specific topic",
+ arguments=[
+ PromptArgument(
+ name="topic", description="Research topic or question", required=True
+ ),
+ PromptArgument(
+ name="expertise_level",
+ description="User's familiarity (beginner/intermediate/expert)",
+ required=False,
+ ),
+ PromptArgument(
+ name="time_period",
+ description="Time period for search (e.g., '2023-present')",
+ required=False,
+ ),
+ PromptArgument(
+ name="domain",
+ description="Academic domain (e.g., computer_science/physics/biology)",
+ required=False,
+ ),
+ ],
+ ),
+ "deep-paper-analysis": Prompt(
+ name="deep-paper-analysis",
+ description="Analyze a specific paper in detail",
+ arguments=[
+ PromptArgument(
+ name="paper_id", description="arXiv paper ID", required=True
+ ),
+ ],
+ ),
+ "literature-synthesis": Prompt(
+ name="literature-synthesis",
+ description="Synthesize findings across multiple papers",
+ arguments=[
+ PromptArgument(
+ name="paper_ids",
+ description="Comma-separated list of arXiv paper IDs",
+ required=True,
+ ),
+ PromptArgument(
+ name="synthesis_type",
+ description="Synthesis type (themes/methods/timeline/gaps/comprehensive)",
+ required=False,
+ ),
+ PromptArgument(
+ name="domain",
+ description="Academic domain (e.g., computer_science/physics/biology)",
+ required=False,
+ ),
+ ],
+ ),
+ "research-question": Prompt(
+ name="research-question",
+ description="Formulate research questions based on literature",
+ arguments=[
+ PromptArgument(
+ name="paper_ids",
+ description="Comma-separated list of arXiv paper IDs",
+ required=True,
+ ),
+ PromptArgument(
+ name="topic", description="Research topic or question", required=True
+ ),
+ PromptArgument(
+ name="domain",
+ description="Academic domain (e.g., computer_science/physics/biology)",
+ required=False,
+ ),
+ ],
+ ),
+}
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/resources/__init__.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/resources/__init__.py
new file mode 100644
index 00000000..eab9db79
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/resources/__init__.py
@@ -0,0 +1,5 @@
+"""Resource management for the arXiv MCP server."""
+
+from .papers import PaperManager
+
+__all__ = ["PaperManager"]
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/resources/papers.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/resources/papers.py
new file mode 100644
index 00000000..662d13ae
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/resources/papers.py
@@ -0,0 +1,101 @@
+"""Resource management and storage for arXiv papers."""
+
+from pathlib import Path
+from typing import List
+import arxiv
+import pymupdf4llm
+import aiofiles
+import logging
+from pydantic import AnyUrl
+import mcp.types as types
+from ..config import Settings
+
+logger = logging.getLogger("arxiv-mcp-server")
+
+
+class PaperManager:
+ """Manages the storage, retrieval, and resource handling of arXiv papers."""
+
+ def __init__(self):
+ """Initialize the paper management system."""
+ settings = Settings()
+ self.storage_path = Path(settings.STORAGE_PATH)
+ self.storage_path.mkdir(parents=True, exist_ok=True)
+ self.client = arxiv.Client()
+
+ def _get_paper_path(self, paper_id: str) -> Path:
+ """Get the absolute file path for a paper."""
+ return self.storage_path / f"{paper_id}.md"
+
+ async def store_paper(self, paper_id: str, pdf_url: str) -> bool:
+ """Download and store a paper from arXiv."""
+ paper_md_path = self._get_paper_path(paper_id)
+ paper_pdf_path = paper_md_path.with_suffix(".pdf")
+
+ if paper_md_path.exists():
+ return True
+
+ try:
+ paper = next(self.client.results(arxiv.Search(id_list=[paper_id])))
+ paper.download_pdf(dirpath=self.storage_path, filename=paper_pdf_path)
+ markdown = pymupdf4llm.to_markdown(paper_pdf_path, show_progress=False)
+
+ async with aiofiles.open(paper_md_path, "w", encoding="utf-8") as f:
+ await f.write(markdown)
+
+ return True
+
+ except StopIteration:
+ raise ValueError(f"Paper with ID {paper_id} not found on arXiv.")
+ except arxiv.ArxivError as e:
+ raise ValueError(
+ f"Error: Failed to download paper {paper_id} from arXiv. Details: {str(e)}"
+ )
+ except Exception as e:
+ raise ValueError(
+ f"Error: An unexpected error occurred while storing paper {paper_id}. Details: {str(e)}"
+ )
+
+ async def has_paper(self, paper_id: str) -> bool:
+ """Check if a paper is available in storage."""
+ return self._get_paper_path(paper_id).exists()
+
+ async def list_papers(self) -> list[str]:
+ """List all stored paper IDs."""
+ logger.info(f"Listing papers in {self.storage_path}")
+ paper_ids = [p.stem for p in self.storage_path.glob("*.md")]
+ logger.info(f"Found {len(paper_ids)} papers")
+ return paper_ids
+
+ async def list_resources(self) -> List[types.Resource]:
+ """List all papers as MCP resources with metadata."""
+ paper_ids = await self.list_papers()
+ resources = []
+
+ for paper_id in paper_ids:
+ search = arxiv.Search(id_list=[paper_id])
+ papers = list(self.client.results(search))
+
+ if papers:
+ paper = papers[0]
+ paper_path = self._get_paper_path(paper_id)
+ resources.append(
+ types.Resource(
+ uri=AnyUrl(f"file://{str(paper_path)}"),
+ name=paper.title,
+ description=paper.summary,
+ mimeType="text/markdown",
+ )
+ )
+
+ logger.info(f"Found {len(resources)} resources")
+ return resources
+
+ async def get_paper_content(self, paper_id: str) -> str:
+ """Get the markdown content of a stored paper."""
+ paper_path = self._get_paper_path(paper_id)
+ if not paper_path.exists():
+ raise ValueError(f"Paper {paper_id} not found in storage")
+
+ async with aiofiles.open(paper_path, "r", encoding="utf-8") as f:
+ return await f.read()
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/server.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/server.py
new file mode 100644
index 00000000..ea4df470
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/server.py
@@ -0,0 +1,81 @@
+"""
+Arxiv MCP Server
+===============
+
+This module implements an MCP server for interacting with arXiv.
+"""
+
+import logging
+import mcp.types as types
+from typing import Dict, Any, List
+from mcp.server import Server
+from mcp.server.models import InitializationOptions
+from mcp.server import NotificationOptions
+from mcp.server.stdio import stdio_server
+from .config import Settings
+from .pg_adapter import handle_search, handle_download, handle_list_papers, handle_read_paper
+from .tools import search_tool, download_tool, list_tool, read_tool
+from .prompts.handlers import list_prompts as handler_list_prompts
+from .prompts.handlers import get_prompt as handler_get_prompt
+
+settings = Settings()
+logger = logging.getLogger("arxiv-mcp-server")
+logger.setLevel(logging.INFO)
+server = Server(settings.APP_NAME)
+
+
+@server.list_prompts()
+async def list_prompts() -> List[types.Prompt]:
+ """List available prompts."""
+ return await handler_list_prompts()
+
+
+@server.get_prompt()
+async def get_prompt(
+ name: str, arguments: Dict[str, str] | None = None
+) -> types.GetPromptResult:
+ """Get a specific prompt with arguments."""
+ return await handler_get_prompt(name, arguments)
+
+
+@server.list_tools()
+async def list_tools() -> List[types.Tool]:
+ """List available arXiv research tools."""
+ return [search_tool, download_tool, list_tool, read_tool]
+
+
+@server.call_tool()
+async def call_tool(name: str, arguments: Dict[str, Any]) -> List[types.TextContent]:
+ """Handle tool calls for arXiv research functionality."""
+ logger.debug(f"Calling tool {name} with arguments {arguments}")
+ try:
+ if name == "search_papers":
+ return await handle_search(arguments)
+ elif name == "download_paper":
+ return await handle_download(arguments)
+ elif name == "list_papers":
+ return await handle_list_papers(arguments)
+ elif name == "read_paper":
+ return await handle_read_paper(arguments)
+ else:
+ return [types.TextContent(type="text", text=f"Error: Unknown tool {name}")]
+ except Exception as e:
+ logger.error(f"Tool error: {str(e)}")
+ return [types.TextContent(type="text", text=f"Error: {str(e)}")]
+
+
+async def main():
+ """Run the server async context."""
+ async with stdio_server() as streams:
+ await server.run(
+ streams[0],
+ streams[1],
+ InitializationOptions(
+ server_name=settings.APP_NAME,
+ server_version=settings.APP_VERSION,
+ capabilities=server.get_capabilities(
+ notification_options=NotificationOptions(resources_changed=True),
+ experimental_capabilities={},
+ ),
+ ),
+ )
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/tool_definitions.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/tool_definitions.py
new file mode 100644
index 00000000..3cb422b0
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/tool_definitions.py
@@ -0,0 +1,31 @@
+"""Tool definitions for the arXiv MCP server.
+
+This module contains ONLY tool schema definitions (no external dependencies like arxiv).
+Both the original tools and the PG adapter import from here to ensure a single source of truth.
+"""
+
+import mcp.types as types
+
+search_tool = types.Tool(
+ name='search_papers',
+ description='Search for papers on arXiv with advanced filtering and query optimization.\n\nQUERY CONSTRUCTION GUIDELINES:\n- Use QUOTED PHRASES for exact matches: "multi-agent systems", "neural networks", "machine learning"\n- Combine related concepts with OR: "AI agents" OR "software agents" OR "intelligent agents" \n- Use field-specific searches for precision:\n - ti:"exact title phrase" - search in titles only\n - au:"author name" - search by author\n - abs:"keyword" - search in abstracts only\n- Use ANDNOT to exclude unwanted results: "machine learning" ANDNOT "survey"\n- For best results, use 2-4 core concepts rather than long keyword lists\n\nADVANCED SEARCH PATTERNS:\n- Field + phrase: ti:"transformer architecture" for papers with exact title phrase\n- Multiple fields: au:"Smith" AND ti:"quantum" for author Smith\'s quantum papers \n- Exclusions: "deep learning" ANDNOT ("survey" OR "review") to exclude survey papers\n- Broad + narrow: "artificial intelligence" AND (robotics OR "computer vision")\n\nCATEGORY FILTERING (highly recommended for relevance):\n- cs.AI: Artificial Intelligence\n- cs.MA: Multi-Agent Systems \n- cs.LG: Machine Learning\n- cs.CL: Computation and Language (NLP)\n- cs.CV: Computer Vision\n- cs.RO: Robotics\n- cs.HC: Human-Computer Interaction\n- cs.CR: Cryptography and Security\n- cs.DB: Databases\n\nEXAMPLES OF EFFECTIVE QUERIES:\n- ti:"reinforcement learning" with categories: ["cs.LG", "cs.AI"] - for RL papers by title\n- au:"Hinton" AND "deep learning" with categories: ["cs.LG"] - for Hinton\'s deep learning work\n- "multi-agent" ANDNOT "survey" with categories: ["cs.MA"] - exclude survey papers\n- abs:"transformer" AND ti:"attention" with categories: ["cs.CL"] - attention papers with transformer abstracts\n\nDATE FILTERING: Use YYYY-MM-DD format for historical research:\n- date_to: "2015-12-31" - for foundational/classic work (pre-2016)\n- date_from: "2020-01-01" - for recent developments (post-2020)\n- Both together for specific time periods\n\nRESULT QUALITY: Results sorted by RELEVANCE (most relevant papers first), not just newest papers.\nThis ensures you get the most pertinent results regardless of publication date.\n\nTIPS FOR FOUNDATIONAL RESEARCH:\n- Use date_to: "2010-12-31" to find classic papers on BDI, SOAR, ACT-R\n- Combine with field searches: ti:"BDI" AND abs:"belief desire intention" \n- Try author searches: au:"Rao" AND "BDI" for Anand Rao\'s foundational BDI work',
+ inputSchema={'type': 'object', 'properties': {'query': {'type': 'string', 'description': 'Search query using quoted phrases for exact matches (e.g., \'"machine learning" OR "deep learning"\') or specific technical terms. Avoid overly broad or generic terms.'}, 'max_results': {'type': 'integer', 'description': 'Maximum number of results to return (default: 10, max: 50). Use 15-20 for comprehensive searches.'}, 'date_from': {'type': 'string', 'description': "Start date for papers (YYYY-MM-DD format). Use to find recent work, e.g., '2023-01-01' for last 2 years."}, 'date_to': {'type': 'string', 'description': "End date for papers (YYYY-MM-DD format). Use with date_from to find historical work, e.g., '2020-12-31' for older research."}, 'categories': {'type': 'array', 'items': {'type': 'string'}, 'description': "Strongly recommended: arXiv categories to focus search (e.g., ['cs.AI', 'cs.MA'] for agent research, ['cs.LG'] for ML, ['cs.CL'] for NLP, ['cs.CV'] for vision). Greatly improves relevance."}, 'sort_by': {'type': 'string', 'enum': ['relevance', 'date'], 'description': "Sort results by 'relevance' (most relevant first, default) or 'date' (newest first). Use 'relevance' for focused searches, 'date' for recent developments."}}, 'required': ['query']},
+)
+
+download_tool = types.Tool(
+ name='download_paper',
+ description='Download a paper and create a resource for it',
+ inputSchema={'type': 'object', 'properties': {'paper_id': {'type': 'string', 'description': 'The arXiv ID of the paper to download'}, 'check_status': {'type': 'boolean', 'description': 'If true, only check conversion status without downloading', 'default': False}}, 'required': ['paper_id']},
+)
+
+list_tool = types.Tool(
+ name='list_papers',
+ description='List all existing papers available as resources',
+ inputSchema={'type': 'object', 'properties': {}, 'required': []},
+)
+
+read_tool = types.Tool(
+ name='read_paper',
+ description='Read the full content of a stored paper in markdown format',
+ inputSchema={'type': 'object', 'properties': {'paper_id': {'type': 'string', 'description': 'The arXiv ID of the paper to read'}}, 'required': ['paper_id']},
+)
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/tools/__init__.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/tools/__init__.py
new file mode 100644
index 00000000..a89cb8c1
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/tools/__init__.py
@@ -0,0 +1,17 @@
+"""Tool definitions for the arXiv MCP server."""
+
+from .search import search_tool, handle_search
+from .download import download_tool, handle_download
+from .list_papers import list_tool, handle_list_papers
+from .read_paper import read_tool, handle_read_paper
+
+__all__ = [
+ "search_tool",
+ "download_tool",
+ "read_tool",
+ "handle_search",
+ "handle_download",
+ "handle_read_paper",
+ "list_tool",
+ "handle_list_papers",
+]
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/tools/download.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/tools/download.py
new file mode 100644
index 00000000..32c4b80c
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/tools/download.py
@@ -0,0 +1,230 @@
+"""Download functionality for the arXiv MCP server."""
+
+import arxiv
+import json
+import asyncio
+from pathlib import Path
+from typing import Dict, Any, List, Optional
+from dataclasses import dataclass
+from datetime import datetime
+import mcp.types as types
+from ..config import Settings
+import pymupdf4llm
+import fitz
+import logging
+
+logger = logging.getLogger("arxiv-mcp-server")
+settings = Settings()
+
+# Global dictionary to track conversion status
+conversion_statuses: Dict[str, Any] = {}
+
+fitz.TOOLS.mupdf_display_errors(False)
+fitz.TOOLS.mupdf_display_warnings(False)
+
+
+@dataclass
+class ConversionStatus:
+ """Track the status of a PDF to Markdown conversion."""
+
+ paper_id: str
+ status: str # 'downloading', 'converting', 'success', 'error'
+ started_at: datetime
+ completed_at: Optional[datetime] = None
+ error: Optional[str] = None
+
+
+download_tool = types.Tool(
+ name="download_paper",
+ description="Download a paper and create a resource for it",
+ inputSchema={
+ "type": "object",
+ "properties": {
+ "paper_id": {
+ "type": "string",
+ "description": "The arXiv ID of the paper to download",
+ },
+ "check_status": {
+ "type": "boolean",
+ "description": "If true, only check conversion status without downloading",
+ "default": False,
+ },
+ },
+ "required": ["paper_id"],
+ },
+)
+
+
+def get_paper_path(paper_id: str, suffix: str = ".md") -> Path:
+ """Get the absolute file path for a paper with given suffix."""
+ storage_path = Path(settings.STORAGE_PATH)
+ storage_path.mkdir(parents=True, exist_ok=True)
+ return storage_path / f"{paper_id}{suffix}"
+
+
+def convert_pdf_to_markdown(paper_id: str, pdf_path: Path) -> None:
+ """Convert PDF to Markdown in a separate thread."""
+ try:
+ logger.info(f"Starting conversion for {paper_id}")
+ markdown = pymupdf4llm.to_markdown(pdf_path, show_progress=False)
+ md_path = get_paper_path(paper_id, ".md")
+
+ with open(md_path, "w", encoding="utf-8") as f:
+ f.write(markdown)
+
+ status = conversion_statuses.get(paper_id)
+ if status:
+ status.status = "success"
+ status.completed_at = datetime.now()
+
+ # Clean up PDF after successful conversion
+ logger.info(f"Conversion completed for {paper_id}")
+
+ except Exception as e:
+ logger.error(f"Conversion failed for {paper_id}: {str(e)}")
+ status = conversion_statuses.get(paper_id)
+ if status:
+ status.status = "error"
+ status.completed_at = datetime.now()
+ status.error = str(e)
+
+
+async def handle_download(arguments: Dict[str, Any]) -> List[types.TextContent]:
+ """Handle paper download and conversion requests."""
+ try:
+ paper_id = arguments["paper_id"]
+ check_status = arguments.get("check_status", False)
+
+ # If only checking status
+ if check_status:
+ status = conversion_statuses.get(paper_id)
+ if not status:
+ if get_paper_path(paper_id, ".md").exists():
+ return [
+ types.TextContent(
+ type="text",
+ text=json.dumps(
+ {
+ "status": "success",
+ "message": "Paper is ready",
+ "resource_uri": f"file://{get_paper_path(paper_id, '.md')}",
+ }
+ ),
+ )
+ ]
+ return [
+ types.TextContent(
+ type="text",
+ text=json.dumps(
+ {
+ "status": "unknown",
+ "message": "No download or conversion in progress",
+ }
+ ),
+ )
+ ]
+
+ return [
+ types.TextContent(
+ type="text",
+ text=json.dumps(
+ {
+ "status": status.status,
+ "started_at": status.started_at.isoformat(),
+ "completed_at": (
+ status.completed_at.isoformat()
+ if status.completed_at
+ else None
+ ),
+ "error": status.error,
+ "message": f"Paper conversion {status.status}",
+ }
+ ),
+ )
+ ]
+
+ # Check if paper is already converted
+ if get_paper_path(paper_id, ".md").exists():
+ return [
+ types.TextContent(
+ type="text",
+ text=json.dumps(
+ {
+ "status": "success",
+ "message": "Paper already available",
+ "resource_uri": f"file://{get_paper_path(paper_id, '.md')}",
+ }
+ ),
+ )
+ ]
+
+ # Check if already in progress
+ if paper_id in conversion_statuses:
+ status = conversion_statuses[paper_id]
+ return [
+ types.TextContent(
+ type="text",
+ text=json.dumps(
+ {
+ "status": status.status,
+ "message": f"Paper conversion {status.status}",
+ "started_at": status.started_at.isoformat(),
+ }
+ ),
+ )
+ ]
+
+ # Start new download and conversion
+ pdf_path = get_paper_path(paper_id, ".pdf")
+ client = arxiv.Client()
+
+ # Initialize status
+ conversion_statuses[paper_id] = ConversionStatus(
+ paper_id=paper_id, status="downloading", started_at=datetime.now()
+ )
+
+ # Download PDF
+ paper = next(client.results(arxiv.Search(id_list=[paper_id])))
+ paper.download_pdf(dirpath=pdf_path.parent, filename=pdf_path.name)
+
+ # Update status and start conversion
+ status = conversion_statuses[paper_id]
+ status.status = "converting"
+
+ # Start conversion in thread
+ asyncio.create_task(
+ asyncio.to_thread(convert_pdf_to_markdown, paper_id, pdf_path)
+ )
+
+ return [
+ types.TextContent(
+ type="text",
+ text=json.dumps(
+ {
+ "status": "converting",
+ "message": "Paper downloaded, conversion started",
+ "started_at": status.started_at.isoformat(),
+ }
+ ),
+ )
+ ]
+
+ except StopIteration:
+ return [
+ types.TextContent(
+ type="text",
+ text=json.dumps(
+ {
+ "status": "error",
+ "message": f"Paper {paper_id} not found on arXiv",
+ }
+ ),
+ )
+ ]
+ except Exception as e:
+ return [
+ types.TextContent(
+ type="text",
+ text=json.dumps({"status": "error", "message": f"Error: {str(e)}"}),
+ )
+ ]
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/tools/list_papers.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/tools/list_papers.py
new file mode 100644
index 00000000..f9760711
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/tools/list_papers.py
@@ -0,0 +1,58 @@
+"""List functionality for the arXiv MCP server."""
+
+import json
+from pathlib import Path
+import arxiv
+from typing import Dict, Any, List, Optional
+import mcp.types as types
+from ..config import Settings
+
+settings = Settings()
+
+list_tool = types.Tool(
+ name="list_papers",
+ description="List all existing papers available as resources",
+ inputSchema={
+ "type": "object",
+ "properties": {},
+ "required": [],
+ },
+)
+
+
+def list_papers() -> list[str]:
+ """List all stored paper IDs."""
+ return [p.stem for p in Path(settings.STORAGE_PATH).glob("*.md")]
+
+
+async def handle_list_papers(
+ arguments: Optional[Dict[str, Any]] = None,
+) -> List[types.TextContent]:
+ """Handle requests to list all stored papers."""
+ try:
+ papers = list_papers()
+
+ client = arxiv.Client()
+
+ results = client.results(arxiv.Search(id_list=papers))
+
+ response_data = {
+ "total_papers": len(papers),
+ "papers": [
+ {
+ "title": result.title,
+ "summary": result.summary,
+ "authors": [author.name for author in result.authors],
+ "links": [link.href for link in result.links],
+ "pdf_url": result.pdf_url,
+ }
+ for result in results
+ ],
+ }
+
+ return [
+ types.TextContent(type="text", text=json.dumps(response_data, indent=2))
+ ]
+
+ except Exception as e:
+ return [types.TextContent(type="text", text=f"Error: {str(e)}")]
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/tools/read_paper.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/tools/read_paper.py
new file mode 100644
index 00000000..cb216d43
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/tools/read_paper.py
@@ -0,0 +1,80 @@
+"""Read functionality for the arXiv MCP server."""
+
+import json
+from pathlib import Path
+from typing import Dict, Any, List
+import mcp.types as types
+from ..config import Settings
+
+settings = Settings()
+
+read_tool = types.Tool(
+ name="read_paper",
+ description="Read the full content of a stored paper in markdown format",
+ inputSchema={
+ "type": "object",
+ "properties": {
+ "paper_id": {
+ "type": "string",
+ "description": "The arXiv ID of the paper to read",
+ }
+ },
+ "required": ["paper_id"],
+ },
+)
+
+
+def list_papers() -> list[str]:
+ """List all stored paper IDs."""
+ return [p.stem for p in Path(settings.STORAGE_PATH).glob("*.md")]
+
+
+async def handle_read_paper(arguments: Dict[str, Any]) -> List[types.TextContent]:
+ """Handle requests to read a paper's content."""
+ try:
+ paper_ids = list_papers()
+ paper_id = arguments["paper_id"]
+ # Check if paper exists
+ if paper_id not in paper_ids:
+ return [
+ types.TextContent(
+ type="text",
+ text=json.dumps(
+ {
+ "status": "error",
+ "message": f"Paper {paper_id} not found in storage. You may need to download it first using download_paper.",
+ }
+ ),
+ )
+ ]
+
+ # Get paper content
+ content = Path(settings.STORAGE_PATH, f"{paper_id}.md").read_text(
+ encoding="utf-8"
+ )
+
+ return [
+ types.TextContent(
+ type="text",
+ text=json.dumps(
+ {
+ "status": "success",
+ "paper_id": paper_id,
+ "content": content,
+ }
+ ),
+ )
+ ]
+
+ except Exception as e:
+ return [
+ types.TextContent(
+ type="text",
+ text=json.dumps(
+ {
+ "status": "error",
+ "message": f"Error reading paper: {str(e)}",
+ }
+ ),
+ )
+ ]
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/tools/search.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/tools/search.py
new file mode 100644
index 00000000..a3fa7139
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/src/arxiv_mcp_server/tools/search.py
@@ -0,0 +1,500 @@
+"""Search functionality for the arXiv MCP server."""
+
+import arxiv
+import json
+import logging
+import httpx
+import xml.etree.ElementTree as ET
+from typing import Dict, Any, List, Optional
+from datetime import datetime, timezone
+from dateutil import parser
+import mcp.types as types
+from ..config import Settings
+
+logger = logging.getLogger("arxiv-mcp-server")
+settings = Settings()
+
+# arXiv API endpoint for raw queries (bypasses arxiv package URL encoding issues)
+# Use HTTPS to avoid redirect from http -> https
+ARXIV_API_URL = "https://export.arxiv.org/api/query"
+
+# XML namespaces used in arXiv Atom feed
+ARXIV_NS = {
+ "atom": "http://www.w3.org/2005/Atom",
+ "arxiv": "http://arxiv.org/schemas/atom",
+}
+
+# Valid arXiv category prefixes for validation
+VALID_CATEGORIES = {
+ "cs",
+ "econ",
+ "eess",
+ "math",
+ "physics",
+ "q-bio",
+ "q-fin",
+ "stat",
+ "astro-ph",
+ "cond-mat",
+ "gr-qc",
+ "hep-ex",
+ "hep-lat",
+ "hep-ph",
+ "hep-th",
+ "math-ph",
+ "nlin",
+ "nucl-ex",
+ "nucl-th",
+ "quant-ph",
+}
+
+
+async def _raw_arxiv_search(
+ query: str,
+ max_results: int = 10,
+ sort_by: str = "relevance",
+ date_from: Optional[str] = None,
+ date_to: Optional[str] = None,
+ categories: Optional[List[str]] = None,
+) -> List[Dict[str, Any]]:
+ """
+ Perform arXiv search using raw HTTP requests.
+
+ This bypasses the arxiv Python package to avoid URL encoding issues
+ with date filters. The arxiv package encodes '+' as '%2B' which breaks
+ the submittedDate:[YYYYMMDD+TO+YYYYMMDD] syntax.
+ """
+ # Build query components
+ query_parts = []
+
+ if query.strip():
+ query_parts.append(f"({query})")
+
+ # Add category filtering
+ if categories:
+ category_filter = " OR ".join(f"cat:{cat}" for cat in categories)
+ query_parts.append(f"({category_filter})")
+
+ # Add date filtering using arXiv API syntax
+ if date_from or date_to:
+ try:
+ if date_from:
+ start_date = parser.parse(date_from).strftime("%Y%m%d0000")
+ else:
+ start_date = "199107010000" # arXiv started July 1991
+
+ if date_to:
+ end_date = parser.parse(date_to).strftime("%Y%m%d2359")
+ else:
+ end_date = datetime.now().strftime("%Y%m%d2359")
+
+ # CRITICAL: This must NOT be URL-encoded. The '+' in '+TO+' must remain literal.
+ date_filter = f"submittedDate:[{start_date}+TO+{end_date}]"
+ query_parts.append(date_filter)
+ logger.debug(f"Added date filter: {date_filter}")
+ except (ValueError, TypeError) as e:
+ logger.error(f"Error parsing dates: {e}")
+ raise ValueError(f"Invalid date format. Use YYYY-MM-DD format: {e}")
+
+ if not query_parts:
+ raise ValueError("No search criteria provided")
+
+ # Combine query parts with AND (space in arXiv = AND)
+ final_query = " AND ".join(query_parts)
+ logger.debug(f"Raw API query: {final_query}")
+
+ # Map sort parameter to arXiv API values
+ sort_map = {
+ "relevance": "relevance",
+ "date": "submittedDate",
+ }
+ sort_order = "descending"
+
+ # Build the URL manually to avoid encoding the '+' in date ranges
+ # We encode most parameters but carefully preserve '+TO+' in date filters
+ base_params = f"max_results={max_results}&sortBy={sort_map.get(sort_by, 'relevance')}&sortOrder={sort_order}"
+
+ # Manually construct search_query parameter
+ # We need to encode spaces and special chars BUT NOT the '+' in '+TO+'
+ # Strategy: encode the query parts separately, then join with encoded AND
+ encoded_query = (
+ final_query.replace(" AND ", "+AND+").replace(" OR ", "+OR+").replace(" ", "+")
+ )
+ # But we need to be careful about existing '+TO+' - it should stay as-is
+ # Since we built the date filter with literal '+TO+', it's already correct
+
+ url = f"{ARXIV_API_URL}?search_query={encoded_query}&{base_params}"
+ logger.debug(f"Raw API URL: {url}")
+
+ # Make the request
+ async with httpx.AsyncClient(timeout=30.0) as client:
+ response = await client.get(url)
+ response.raise_for_status()
+
+ # Parse the Atom XML response
+ return _parse_arxiv_atom_response(response.text)
+
+
+def _parse_arxiv_atom_response(xml_text: str) -> List[Dict[str, Any]]:
+ """Parse arXiv Atom XML response into paper dictionaries."""
+ results = []
+
+ try:
+ root = ET.fromstring(xml_text)
+
+ for entry in root.findall("atom:entry", ARXIV_NS):
+ # Extract paper ID from the id URL
+ id_elem = entry.find("atom:id", ARXIV_NS)
+ if id_elem is None or id_elem.text is None:
+ continue
+
+ # ID format: http://arxiv.org/abs/XXXX.XXXXX or http://arxiv.org/abs/category/XXXXXXX
+ paper_id = id_elem.text.split("/abs/")[-1]
+ # Remove version suffix for short ID
+ short_id = paper_id.split("v")[0] if "v" in paper_id else paper_id
+
+ # Title
+ title_elem = entry.find("atom:title", ARXIV_NS)
+ title = (
+ title_elem.text.strip().replace("\n", " ")
+ if title_elem is not None and title_elem.text
+ else ""
+ )
+
+ # Authors
+ authors = []
+ for author in entry.findall("atom:author", ARXIV_NS):
+ name_elem = author.find("atom:name", ARXIV_NS)
+ if name_elem is not None and name_elem.text:
+ authors.append(name_elem.text)
+
+ # Abstract/Summary
+ summary_elem = entry.find("atom:summary", ARXIV_NS)
+ abstract = (
+ summary_elem.text.strip().replace("\n", " ")
+ if summary_elem is not None and summary_elem.text
+ else ""
+ )
+
+ # Categories
+ categories = []
+ for cat in entry.findall("arxiv:primary_category", ARXIV_NS):
+ term = cat.get("term")
+ if term:
+ categories.append(term)
+ for cat in entry.findall("atom:category", ARXIV_NS):
+ term = cat.get("term")
+ if term and term not in categories:
+ categories.append(term)
+
+ # Published date
+ published_elem = entry.find("atom:published", ARXIV_NS)
+ published = (
+ published_elem.text
+ if published_elem is not None and published_elem.text
+ else ""
+ )
+
+ # PDF URL
+ pdf_url = None
+ for link in entry.findall("atom:link", ARXIV_NS):
+ if link.get("title") == "pdf":
+ pdf_url = link.get("href")
+ break
+ if not pdf_url:
+ pdf_url = f"http://arxiv.org/pdf/{paper_id}"
+
+ results.append(
+ {
+ "id": short_id,
+ "title": title,
+ "authors": authors,
+ "abstract": abstract,
+ "categories": categories,
+ "published": published,
+ "url": pdf_url,
+ "resource_uri": f"arxiv://{short_id}",
+ }
+ )
+
+ except ET.ParseError as e:
+ logger.error(f"Failed to parse arXiv XML response: {e}")
+ raise ValueError(f"Failed to parse arXiv API response: {e}")
+
+ return results
+
+
+search_tool = types.Tool(
+ name="search_papers",
+ description="""Search for papers on arXiv with advanced filtering and query optimization.
+
+QUERY CONSTRUCTION GUIDELINES:
+- Use QUOTED PHRASES for exact matches: "multi-agent systems", "neural networks", "machine learning"
+- Combine related concepts with OR: "AI agents" OR "software agents" OR "intelligent agents"
+- Use field-specific searches for precision:
+ - ti:"exact title phrase" - search in titles only
+ - au:"author name" - search by author
+ - abs:"keyword" - search in abstracts only
+- Use ANDNOT to exclude unwanted results: "machine learning" ANDNOT "survey"
+- For best results, use 2-4 core concepts rather than long keyword lists
+
+ADVANCED SEARCH PATTERNS:
+- Field + phrase: ti:"transformer architecture" for papers with exact title phrase
+- Multiple fields: au:"Smith" AND ti:"quantum" for author Smith's quantum papers
+- Exclusions: "deep learning" ANDNOT ("survey" OR "review") to exclude survey papers
+- Broad + narrow: "artificial intelligence" AND (robotics OR "computer vision")
+
+CATEGORY FILTERING (highly recommended for relevance):
+- cs.AI: Artificial Intelligence
+- cs.MA: Multi-Agent Systems
+- cs.LG: Machine Learning
+- cs.CL: Computation and Language (NLP)
+- cs.CV: Computer Vision
+- cs.RO: Robotics
+- cs.HC: Human-Computer Interaction
+- cs.CR: Cryptography and Security
+- cs.DB: Databases
+
+EXAMPLES OF EFFECTIVE QUERIES:
+- ti:"reinforcement learning" with categories: ["cs.LG", "cs.AI"] - for RL papers by title
+- au:"Hinton" AND "deep learning" with categories: ["cs.LG"] - for Hinton's deep learning work
+- "multi-agent" ANDNOT "survey" with categories: ["cs.MA"] - exclude survey papers
+- abs:"transformer" AND ti:"attention" with categories: ["cs.CL"] - attention papers with transformer abstracts
+
+DATE FILTERING: Use YYYY-MM-DD format for historical research:
+- date_to: "2015-12-31" - for foundational/classic work (pre-2016)
+- date_from: "2020-01-01" - for recent developments (post-2020)
+- Both together for specific time periods
+
+RESULT QUALITY: Results sorted by RELEVANCE (most relevant papers first), not just newest papers.
+This ensures you get the most pertinent results regardless of publication date.
+
+TIPS FOR FOUNDATIONAL RESEARCH:
+- Use date_to: "2010-12-31" to find classic papers on BDI, SOAR, ACT-R
+- Combine with field searches: ti:"BDI" AND abs:"belief desire intention"
+- Try author searches: au:"Rao" AND "BDI" for Anand Rao's foundational BDI work""",
+ inputSchema={
+ "type": "object",
+ "properties": {
+ "query": {
+ "type": "string",
+ "description": 'Search query using quoted phrases for exact matches (e.g., \'"machine learning" OR "deep learning"\') or specific technical terms. Avoid overly broad or generic terms.',
+ },
+ "max_results": {
+ "type": "integer",
+ "description": "Maximum number of results to return (default: 10, max: 50). Use 15-20 for comprehensive searches.",
+ },
+ "date_from": {
+ "type": "string",
+ "description": "Start date for papers (YYYY-MM-DD format). Use to find recent work, e.g., '2023-01-01' for last 2 years.",
+ },
+ "date_to": {
+ "type": "string",
+ "description": "End date for papers (YYYY-MM-DD format). Use with date_from to find historical work, e.g., '2020-12-31' for older research.",
+ },
+ "categories": {
+ "type": "array",
+ "items": {"type": "string"},
+ "description": "Strongly recommended: arXiv categories to focus search (e.g., ['cs.AI', 'cs.MA'] for agent research, ['cs.LG'] for ML, ['cs.CL'] for NLP, ['cs.CV'] for vision). Greatly improves relevance.",
+ },
+ "sort_by": {
+ "type": "string",
+ "enum": ["relevance", "date"],
+ "description": "Sort results by 'relevance' (most relevant first, default) or 'date' (newest first). Use 'relevance' for focused searches, 'date' for recent developments.",
+ },
+ },
+ "required": ["query"],
+ },
+)
+
+
+def _validate_categories(categories: List[str]) -> bool:
+ """Validate that all provided categories are valid arXiv categories."""
+ for category in categories:
+ if "." in category:
+ prefix = category.split(".")[0]
+ else:
+ prefix = category
+ if prefix not in VALID_CATEGORIES:
+ logger.warning(f"Unknown category prefix: {prefix}")
+ return False
+ return True
+
+
+def _optimize_query(query: str) -> str:
+ """Minimal query optimization - preserve user intent while fixing obvious issues."""
+
+ # Don't modify queries with existing field specifiers (ti:, au:, abs:, cat:)
+ if any(
+ field in query
+ for field in ["ti:", "au:", "abs:", "cat:", "AND", "OR", "ANDNOT"]
+ ):
+ logger.debug("Field-specific or boolean query detected - no optimization")
+ return query
+
+ # Don't modify queries that are already quoted
+ if query.startswith('"') and query.endswith('"'):
+ logger.debug("Pre-quoted query detected - no optimization")
+ return query
+
+ # For very long queries (>10 terms), suggest user be more specific rather than auto-converting
+ terms = query.split()
+ if len(terms) > 10:
+ logger.warning(
+ f"Very long query ({len(terms)} terms) - consider using quotes for phrases or field-specific searches"
+ )
+
+ # Only optimization: preserve the original query exactly as intended
+ return query
+
+
+def _process_paper(paper: arxiv.Result) -> Dict[str, Any]:
+ """Process paper information with resource URI."""
+ return {
+ "id": paper.get_short_id(),
+ "title": paper.title,
+ "authors": [author.name for author in paper.authors],
+ "abstract": paper.summary,
+ "categories": paper.categories,
+ "published": paper.published.isoformat(),
+ "url": paper.pdf_url,
+ "resource_uri": f"arxiv://{paper.get_short_id()}",
+ }
+
+
+async def handle_search(arguments: Dict[str, Any]) -> List[types.TextContent]:
+ """Handle paper search requests with improved arXiv API integration.
+
+ Uses raw HTTP requests when date filtering is requested to avoid URL encoding
+ issues with the arxiv Python package. Falls back to the arxiv package for
+ non-date queries for better compatibility.
+ """
+ try:
+ max_results = min(int(arguments.get("max_results", 10)), settings.MAX_RESULTS)
+ base_query = arguments["query"]
+ date_from_arg = arguments.get("date_from")
+ date_to_arg = arguments.get("date_to")
+ categories = arguments.get("categories")
+ sort_by_arg = arguments.get("sort_by", "relevance")
+
+ logger.debug(
+ f"Starting search with query: '{base_query}', max_results: {max_results}"
+ )
+
+ # Validate categories if provided
+ if categories and not _validate_categories(categories):
+ return [
+ types.TextContent(
+ type="text",
+ text="Error: Invalid category provided. Please check arXiv category names.",
+ )
+ ]
+
+ # Use raw HTTP API when date filtering is requested
+ # This bypasses the arxiv package's URL encoding which breaks date syntax
+ if date_from_arg or date_to_arg:
+ logger.debug(
+ f"Date filtering requested - using raw API: {date_from_arg} to {date_to_arg}"
+ )
+
+ try:
+ optimized_query = (
+ _optimize_query(base_query) if base_query.strip() else ""
+ )
+ results = await _raw_arxiv_search(
+ query=optimized_query,
+ max_results=max_results,
+ sort_by=sort_by_arg,
+ date_from=date_from_arg,
+ date_to=date_to_arg,
+ categories=categories,
+ )
+
+ logger.info(
+ f"Raw API search completed: {len(results)} results returned"
+ )
+ response_data = {"total_results": len(results), "papers": results}
+
+ return [
+ types.TextContent(
+ type="text", text=json.dumps(response_data, indent=2)
+ )
+ ]
+
+ except httpx.HTTPStatusError as e:
+ logger.error(f"arXiv API HTTP error: {e}")
+ return [
+ types.TextContent(
+ type="text", text=f"Error: arXiv API HTTP error - {str(e)}"
+ )
+ ]
+ except ValueError as e:
+ return [types.TextContent(type="text", text=f"Error: {str(e)}")]
+
+ # For non-date queries, use the arxiv package (more robust parsing)
+ client = arxiv.Client()
+
+ # Build query components
+ query_parts = []
+
+ # Add base query with optimization
+ if base_query.strip():
+ optimized_query = _optimize_query(base_query)
+ query_parts.append(f"({optimized_query})")
+ if optimized_query != base_query:
+ logger.debug(f"Optimized query: '{base_query}' -> '{optimized_query}'")
+
+ # Add category filtering
+ if categories:
+ category_filter = " OR ".join(f"cat:{cat}" for cat in categories)
+ query_parts.append(f"({category_filter})")
+ logger.debug(f"Added category filter: {category_filter}")
+
+ # Combine query parts
+ if not query_parts:
+ return [
+ types.TextContent(
+ type="text", text="Error: No search criteria provided"
+ )
+ ]
+
+ # Combine query parts - arXiv uses space for AND by default
+ final_query = " ".join(query_parts)
+ logger.debug(f"Final arXiv query: {final_query}")
+
+ # Determine sort method
+ if sort_by_arg == "date":
+ sort_criterion = arxiv.SortCriterion.SubmittedDate
+ logger.debug("Using date sorting (newest first)")
+ else:
+ sort_criterion = arxiv.SortCriterion.Relevance
+ logger.debug("Using relevance sorting (most relevant first)")
+
+ search = arxiv.Search(
+ query=final_query,
+ max_results=max_results,
+ sort_by=sort_criterion,
+ )
+
+ # Process results
+ results = []
+ for paper in client.results(search):
+ if len(results) >= max_results:
+ break
+ results.append(_process_paper(paper))
+
+ logger.info(f"Search completed: {len(results)} results returned")
+ response_data = {"total_results": len(results), "papers": results}
+
+ return [
+ types.TextContent(type="text", text=json.dumps(response_data, indent=2))
+ ]
+
+ except arxiv.ArxivError as e:
+ logger.error(f"ArXiv API error: {e}")
+ return [
+ types.TextContent(type="text", text=f"Error: ArXiv API error - {str(e)}")
+ ]
+ except Exception as e:
+ logger.error(f"Unexpected search error: {e}")
+ return [types.TextContent(type="text", text=f"Error: {str(e)}")]
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/tests/conftest.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/tests/conftest.py
new file mode 100644
index 00000000..ea872f2e
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/tests/conftest.py
@@ -0,0 +1,76 @@
+"""Shared test fixtures for the arXiv MCP server test suite."""
+
+import pytest
+import tempfile
+from datetime import datetime, timezone
+from unittest.mock import MagicMock, AsyncMock
+import arxiv
+from pathlib import Path
+
+
+class MockAuthor:
+ def __init__(self, name):
+ self.name = name
+
+
+class MockLink:
+ def __init__(self, href):
+ self.href = href
+
+
+@pytest.fixture
+def mock_paper():
+ """Create a properly structured mock paper with all required attributes."""
+ paper = MagicMock(spec=arxiv.Result)
+ paper.get_short_id.return_value = "2103.12345"
+ paper.title = "Test Paper"
+ paper.authors = [MockAuthor("John Doe"), MockAuthor("Jane Smith")]
+ paper.summary = "Test abstract"
+ paper.categories = ["cs.AI", "cs.LG"]
+ paper.published = datetime(2023, 1, 1, tzinfo=timezone.utc)
+ paper.pdf_url = "https://arxiv.org/pdf/2103.12345"
+ paper.comment = "Test comment"
+ paper.journal_ref = "Test Journal 2023"
+ paper.primary_category = "cs.AI"
+ paper.links = [MockLink("https://arxiv.org/abs/2103.12345")]
+ return paper
+
+
+@pytest.fixture
+def mock_client(mock_paper):
+ """Create a mock arxiv client with predefined behavior."""
+ client = MagicMock(spec=arxiv.Client)
+ client.results.return_value = [mock_paper]
+ return client
+
+
+@pytest.fixture
+def temp_storage_path():
+ """Create a temporary directory for paper storage during tests."""
+ with tempfile.TemporaryDirectory() as tmpdir:
+ yield Path(tmpdir)
+
+
+@pytest.fixture
+def mock_pdf_content():
+ """Create mock PDF content for testing."""
+ return b"Mock PDF Content"
+
+
+@pytest.fixture
+def mock_http_response():
+ """Create a mock HTTP response for testing paper downloads."""
+ response = AsyncMock()
+ response.status = 200
+ response.__aenter__.return_value = response
+ response.read.return_value = b"Mock PDF Content"
+ return response
+
+
+@pytest.fixture
+def mock_http_session(mock_http_response):
+ """Create a mock HTTP session for testing."""
+ session = AsyncMock()
+ session.get.return_value = mock_http_response
+ session.__aenter__.return_value = session
+ return session
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/tests/prompts/conftest.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/tests/prompts/conftest.py
new file mode 100644
index 00000000..1bc07680
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/tests/prompts/conftest.py
@@ -0,0 +1,62 @@
+"""Test fixtures for prompt tests."""
+
+import pytest
+from typing import Dict, Any
+
+
+@pytest.fixture
+def mock_paper_content() -> str:
+ """Sample paper content for testing."""
+ return """# Test Paper Title
+
+## Abstract
+This is a test paper abstract.
+
+## Introduction
+Test introduction content.
+
+## Methods
+Test methodology section.
+
+## Results
+Test results section.
+
+## Discussion
+Test discussion section.
+
+## References
+1. Test reference
+"""
+
+
+@pytest.fixture
+def research_discovery_args() -> Dict[str, Any]:
+ """Sample arguments for research discovery prompt."""
+ return {
+ "topic": "machine learning",
+ "expertise_level": "intermediate",
+ "time_period": "2023-present",
+ }
+
+
+@pytest.fixture
+def paper_analysis_args() -> Dict[str, Any]:
+ """Sample arguments for paper analysis prompt."""
+ return {"paper_id": "2401.12345", "focus_area": "methodology"}
+
+
+@pytest.fixture
+def literature_synthesis_args() -> Dict[str, Any]:
+ """Sample arguments for literature synthesis prompt."""
+ return {"paper_ids": ["2401.12345", "2401.67890"], "synthesis_type": "themes"}
+
+
+@pytest.fixture(autouse=True)
+def clean_paper_manager():
+ """Reset the paper manager singleton between tests."""
+ # Reset before each test
+ global paper_manager
+ paper_manager = None
+ yield
+ # Reset after each test
+ paper_manager = None
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/tests/prompts/test_prompt_integration.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/tests/prompts/test_prompt_integration.py
new file mode 100644
index 00000000..4760459a
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/tests/prompts/test_prompt_integration.py
@@ -0,0 +1,42 @@
+"""Integration tests for prompt functionality."""
+
+import pytest
+from arxiv_mcp_server.prompts.handlers import list_prompts, get_prompt
+
+
+@pytest.mark.asyncio
+async def test_server_list_prompts():
+ """Test server list_prompts endpoint."""
+ prompts = await list_prompts()
+ assert len(prompts) == 1
+
+ # Check that all prompts have required fields
+ for prompt in prompts:
+ assert prompt.name
+ assert prompt.description
+ assert prompt.arguments is not None
+
+
+@pytest.mark.asyncio
+async def test_server_get_analysis_prompt():
+ """Test server get_prompt endpoint with analysis prompt."""
+ result = await get_prompt("deep-paper-analysis", {"paper_id": "2401.00123"})
+
+ assert len(result.messages) == 1
+ message = result.messages[0]
+ assert message.role == "user"
+ assert "2401.00123" in message.content.text
+
+
+@pytest.mark.asyncio
+async def test_server_get_prompt_invalid_name():
+ """Test server get_prompt endpoint with invalid prompt name."""
+ with pytest.raises(ValueError, match="Prompt not found"):
+ await get_prompt("invalid-prompt", {})
+
+
+@pytest.mark.asyncio
+async def test_server_get_prompt_missing_args():
+ """Test server get_prompt endpoint with missing required arguments."""
+ with pytest.raises(ValueError, match="Missing required argument"):
+ await get_prompt("deep-paper-analysis", {})
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/tests/prompts/test_prompts.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/tests/prompts/test_prompts.py
new file mode 100644
index 00000000..7ccdae2f
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/tests/prompts/test_prompts.py
@@ -0,0 +1,53 @@
+"""Unit tests for prompt handlers."""
+
+import pytest
+from typing import Dict
+from arxiv_mcp_server.prompts.handlers import list_prompts, get_prompt
+from mcp.types import GetPromptResult, PromptMessage, TextContent
+
+
+@pytest.mark.asyncio
+async def test_list_prompts():
+ """Test listing available prompts."""
+ prompts = await list_prompts()
+ assert len(prompts) == 1
+
+ prompt_names = {p.name for p in prompts}
+ expected_names = {"deep-paper-analysis"}
+ assert prompt_names == expected_names
+
+
+@pytest.mark.asyncio
+async def test_get_paper_analysis_prompt():
+ """Test getting paper analysis prompt."""
+ result = await get_prompt("deep-paper-analysis", {"paper_id": "2401.00123"})
+
+ assert isinstance(result, GetPromptResult)
+ assert len(result.messages) == 1
+ message = result.messages[0]
+
+ assert isinstance(message, PromptMessage)
+ assert message.role == "user"
+ assert isinstance(message.content, TextContent)
+ assert "2401.00123" in message.content.text
+
+
+@pytest.mark.asyncio
+async def test_get_prompt_with_invalid_name():
+ """Test getting prompt with invalid name."""
+ with pytest.raises(ValueError, match="Prompt not found"):
+ await get_prompt("invalid-prompt", {})
+
+
+@pytest.mark.asyncio
+async def test_get_prompt_with_no_arguments():
+ """Test getting prompt with no arguments."""
+ with pytest.raises(ValueError, match="No arguments provided"):
+ await get_prompt("deep-paper-analysis", None)
+
+
+@pytest.mark.asyncio
+async def test_get_prompt_with_missing_required_argument():
+ """Test getting prompt with missing required argument."""
+ with pytest.raises(ValueError, match="Missing required argument"):
+ await get_prompt("deep-paper-analysis", {})
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/tests/test_config.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/tests/test_config.py
new file mode 100644
index 00000000..54e862eb
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/tests/test_config.py
@@ -0,0 +1,131 @@
+"""Tests for the configuration module."""
+
+import os
+import sys
+from pathlib import Path
+from arxiv_mcp_server.config import Settings
+from unittest.mock import patch
+
+
+@patch.object(Path, "mkdir")
+@patch.object(Path, "resolve")
+def test_storage_path_default(mock_resolve, mock_mkdir):
+ """Test that the default storage path is correctly constructed."""
+ # Setup the mock to return the path itself when resolved
+ mock_resolve.side_effect = lambda: Path.home() / ".arxiv-mcp-server" / "papers"
+
+ settings = Settings()
+ expected_path = Path.home() / ".arxiv-mcp-server" / "papers"
+ assert settings.STORAGE_PATH == expected_path.resolve()
+ # Verify mkdir was called with parents=True and exist_ok=True
+ mock_mkdir.assert_called_once_with(parents=True, exist_ok=True)
+
+
+@patch.object(Path, "mkdir")
+@patch.object(Path, "resolve")
+def test_storage_path_from_args(mock_resolve, mock_mkdir):
+ """Test that the storage path from command line args is correctly parsed."""
+ test_path = "/tmp/test_storage"
+ mock_resolve.side_effect = lambda: Path(test_path)
+
+ with patch.object(sys, "argv", ["program", "--storage-path", test_path]):
+ settings = Settings()
+ assert settings.STORAGE_PATH == Path(test_path).resolve()
+ mock_mkdir.assert_called_once_with(parents=True, exist_ok=True)
+
+
+@patch.object(Path, "mkdir")
+@patch.object(Path, "resolve")
+def test_storage_path_platform_compatibility(mock_resolve, mock_mkdir):
+ """Test that the storage path works correctly on different platforms."""
+ # Test with a path format that would be valid on both Windows and Unix
+ test_paths = [
+ # Unix-style path
+ "/path/to/storage",
+ # Windows-style path
+ "C:\\path\\to\\storage",
+ # Path with spaces
+ "/path with spaces/to/storage",
+ # Path with non-ASCII characters
+ "/path/to/störâgè",
+ ]
+
+ for test_path in test_paths:
+ # Reset mocks for each iteration
+ mock_resolve.reset_mock()
+ mock_mkdir.reset_mock()
+
+ # Set up the mock to return the path itself
+ mock_resolve.side_effect = lambda: Path(test_path)
+
+ with patch.object(sys, "argv", ["program", "--storage-path", test_path]):
+ settings = Settings()
+ resolved_path = settings.STORAGE_PATH
+
+ # Verify that Path constructor was called with the test path
+ assert resolved_path == Path(test_path).resolve()
+
+ # Verify that mkdir was called
+ mock_mkdir.assert_called_once_with(parents=True, exist_ok=True)
+
+
+def test_storage_path_creates_missing_directory():
+ """Test that directories are actually created for the storage path."""
+ import tempfile
+
+ # Create a temporary directory for our test
+ with tempfile.TemporaryDirectory() as tmpdir:
+ # Create a path that doesn't exist yet
+ test_path = os.path.join(tmpdir, "deeply", "nested", "directory", "structure")
+
+ # Make sure it doesn't exist yet
+ assert not os.path.exists(test_path)
+
+ # Patch the arguments to use this path
+ with patch.object(sys, "argv", ["program", "--storage-path", test_path]):
+ # Access the STORAGE_PATH property which should create the directories
+ settings = Settings()
+ storage_path = settings.STORAGE_PATH
+
+ # Verify the directory was created
+ assert os.path.exists(test_path)
+ assert os.path.isdir(test_path)
+
+ # Verify the paths refer to the same location
+ # Use Path.samefile to handle symlinks (like /var -> /private/var on macOS)
+ assert Path(storage_path).samefile(test_path)
+
+
+def test_path_normalization_with_windows_paths():
+ """Test Windows-specific path handling using string operations only."""
+ # Windows-style paths - we'll test the normalization and joining logic
+ windows_style_paths = [
+ # Drive letter with backslashes
+ "C:\\Users\\username\\Documents\\Papers",
+ # UNC path (network share)
+ "\\\\server\\share\\papers",
+ # Drive letter with forward slashes (also valid on Windows)
+ "C:/Users/username/Documents/Papers",
+ # Windows-style path with spaces
+ "C:\\Program Files\\arXiv\\papers",
+ # Windows-style path with mixed slashes
+ "C:\\Users/username\\Documents/Papers",
+ ]
+
+ # Test that our config works with these path formats
+ for windows_path in windows_style_paths:
+ assert Path(windows_path) # This should not raise an error
+
+ # Test path joining logic works correctly
+ subpath = Path(windows_path) / "subdir"
+ assert str(subpath).endswith("subdir")
+
+ # The following check is problematic on real Windows systems
+ # where the path separator may be different
+ # Check only that the base path is contained in the result (ignoring separator differences)
+ base_path_norm = windows_path.replace("\\", "/").replace("//", "/")
+ subpath_norm = str(subpath).replace("\\", "/").replace("//", "/")
+ assert base_path_norm in subpath_norm
+
+ # Instead of checking exact string equality, verify the Path objects are equivalent
+ assert subpath == Path(windows_path).joinpath("subdir")
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/tests/tools/test_download.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/tests/tools/test_download.py
new file mode 100644
index 00000000..234d4133
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/tests/tools/test_download.py
@@ -0,0 +1,81 @@
+"""Tests for paper download functionality."""
+
+import pytest
+import json
+from datetime import datetime
+from arxiv_mcp_server.tools.download import (
+ handle_download,
+ get_paper_path,
+ conversion_statuses,
+)
+
+
+@pytest.mark.asyncio
+async def test_download_paper_lifecycle(mocker, temp_storage_path):
+ """Test the complete lifecycle of downloading and converting a paper."""
+ paper_id = "2103.12345"
+ # Mock arxiv client and PDF download
+ mocker.patch("arxiv.Client.results")
+ mocker.patch("arxiv.Result.download_pdf")
+
+ # Mock PDF to markdown conversion to happen immediately
+ async def mock_convert(paper_id, pdf_path):
+ md_path = get_paper_path(paper_id, ".md")
+ with open(md_path, "w", encoding="utf-8") as f:
+ f.write("# Test Paper\nConverted content")
+ if paper_id in conversion_statuses:
+ status = conversion_statuses[paper_id]
+ status.status = "success"
+ status.completed_at = datetime.now()
+ pdf_path.unlink() # Cleanup PDF
+
+ mocker.patch("asyncio.to_thread", side_effect=mock_convert)
+
+ # Initial download request
+ response = await handle_download({"paper_id": paper_id})
+ status = json.loads(response[0].text)
+ assert status["status"] in ["converting", "success"]
+
+ # Check final status
+ response = await handle_download({"paper_id": paper_id, "check_status": True})
+ final_status = json.loads(response[0].text)
+ assert final_status["status"] in ["success", "converting"]
+
+ # Verify markdown file exists
+ if final_status["status"] == "success":
+ assert get_paper_path(paper_id, ".md").exists()
+
+
+@pytest.mark.asyncio
+async def test_download_existing_paper(temp_storage_path):
+ """Test downloading a paper that's already available."""
+ paper_id = "2103.12345"
+ md_path = get_paper_path(paper_id, ".md")
+
+ # Create test markdown file
+ md_path.parent.mkdir(parents=True, exist_ok=True)
+ with open(md_path, "w", encoding="utf-8") as f:
+ f.write("# Existing Paper\nTest content")
+
+ response = await handle_download({"paper_id": paper_id})
+ status = json.loads(response[0].text)
+ assert status["status"] == "success"
+
+
+@pytest.mark.asyncio
+async def test_download_nonexistent_paper(mocker):
+ """Test downloading a paper that doesn't exist."""
+ mocker.patch("arxiv.Client.results", side_effect=StopIteration())
+
+ response = await handle_download({"paper_id": "invalid.12345"})
+ status = json.loads(response[0].text)
+ assert status["status"] == "error"
+ assert "not found on arXiv" in status["message"]
+
+
+@pytest.mark.asyncio
+async def test_check_unknown_status():
+ """Test checking status of unknown paper."""
+ response = await handle_download({"paper_id": "2103.99999", "check_status": True})
+ status = json.loads(response[0].text)
+ assert status["status"] == "unknown"
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/tests/tools/test_search.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/tests/tools/test_search.py
new file mode 100644
index 00000000..b02907ea
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/tests/tools/test_search.py
@@ -0,0 +1,261 @@
+"""Tests for paper search functionality."""
+
+import pytest
+import json
+from unittest.mock import patch, MagicMock, AsyncMock
+from arxiv_mcp_server.tools import handle_search
+from arxiv_mcp_server.tools.search import (
+ _validate_categories,
+ _raw_arxiv_search,
+ _parse_arxiv_atom_response,
+)
+
+
+@pytest.mark.asyncio
+async def test_basic_search(mock_client):
+ """Test basic paper search functionality."""
+ with patch("arxiv.Client", return_value=mock_client):
+ result = await handle_search({"query": "test query", "max_results": 1})
+
+ assert len(result) == 1
+ content = json.loads(result[0].text)
+ assert content["total_results"] == 1
+ paper = content["papers"][0]
+ assert paper["id"] == "2103.12345"
+ assert paper["title"] == "Test Paper"
+ assert "resource_uri" in paper
+
+
+@pytest.mark.asyncio
+async def test_search_with_categories(mock_client):
+ """Test paper search with category filtering."""
+ with patch("arxiv.Client", return_value=mock_client):
+ result = await handle_search(
+ {"query": "test query", "categories": ["cs.AI", "cs.LG"], "max_results": 1}
+ )
+
+ content = json.loads(result[0].text)
+ assert content["papers"][0]["categories"] == ["cs.AI", "cs.LG"]
+
+
+@pytest.mark.asyncio
+async def test_search_with_dates():
+ """Test paper search with date filtering uses raw API."""
+ mock_xml_response = """
+
+
+ http://arxiv.org/abs/2301.00001v1
+ Test Paper
+ Test abstract
+ 2023-06-15T00:00:00Z
+ Test Author
+
+
+
+ """
+
+ mock_response = MagicMock()
+ mock_response.text = mock_xml_response
+ mock_response.raise_for_status = MagicMock()
+
+ with patch("httpx.AsyncClient") as mock_client_class:
+ mock_client = AsyncMock()
+ mock_client.get = AsyncMock(return_value=mock_response)
+ mock_client.__aenter__ = AsyncMock(return_value=mock_client)
+ mock_client.__aexit__ = AsyncMock(return_value=None)
+ mock_client_class.return_value = mock_client
+
+ result = await handle_search(
+ {
+ "query": "test query",
+ "date_from": "2022-01-01",
+ "date_to": "2024-01-01",
+ "max_results": 1,
+ }
+ )
+
+ content = json.loads(result[0].text)
+ assert content["total_results"] == 1
+ assert len(content["papers"]) == 1
+
+
+@pytest.mark.asyncio
+async def test_search_with_invalid_dates():
+ """Test search with invalid date formats."""
+ result = await handle_search(
+ {"query": "test query", "date_from": "invalid-date", "max_results": 1}
+ )
+
+ assert "Error:" in result[0].text
+
+
+def test_validate_categories():
+ """Test category validation function."""
+ # Valid categories
+ assert _validate_categories(["cs.AI", "cs.LG"])
+ assert _validate_categories(["math.CO", "physics.gen-ph"])
+
+ # Invalid categories
+ assert not _validate_categories(["invalid.category"])
+ assert not _validate_categories(["cs.AI", "invalid.test"])
+
+
+def test_parse_arxiv_atom_response():
+ """Test parsing of arXiv Atom XML response."""
+ sample_xml = """
+
+
+ http://arxiv.org/abs/2301.00001v1
+ Test Paper Title
+ This is a test abstract.
+ 2023-01-01T00:00:00Z
+ John Doe
+ Jane Smith
+
+
+
+
+
+ """
+
+ results = _parse_arxiv_atom_response(sample_xml)
+ assert len(results) == 1
+ paper = results[0]
+ assert paper["id"] == "2301.00001"
+ assert paper["title"] == "Test Paper Title"
+ assert paper["abstract"] == "This is a test abstract."
+ assert paper["authors"] == ["John Doe", "Jane Smith"]
+ assert "cs.AI" in paper["categories"]
+ assert paper["resource_uri"] == "arxiv://2301.00001"
+
+
+@pytest.mark.asyncio
+async def test_raw_arxiv_search_builds_correct_url():
+ """Test that raw search builds correct URL with date filters."""
+ import httpx
+
+ # Mock the httpx client
+ mock_response = MagicMock()
+ mock_response.text = """
+
+ """
+ mock_response.raise_for_status = MagicMock()
+
+ with patch("httpx.AsyncClient") as mock_client_class:
+ mock_client = AsyncMock()
+ mock_client.get = AsyncMock(return_value=mock_response)
+ mock_client.__aenter__ = AsyncMock(return_value=mock_client)
+ mock_client.__aexit__ = AsyncMock(return_value=None)
+ mock_client_class.return_value = mock_client
+
+ await _raw_arxiv_search(
+ query="LLM",
+ max_results=5,
+ date_from="2023-01-01",
+ date_to="2023-12-31",
+ categories=["cs.AI"],
+ )
+
+ # Check that the URL was constructed with unencoded +TO+
+ call_args = mock_client.get.call_args
+ url = call_args[0][0]
+ assert "+TO+" in url # Critical: must not be encoded as %2B
+ assert "submittedDate:" in url
+ assert "20230101" in url
+ assert "20231231" in url
+
+
+@pytest.mark.asyncio
+async def test_search_with_invalid_categories(mock_client):
+ """Test search with invalid categories."""
+ with patch("arxiv.Client", return_value=mock_client):
+ result = await handle_search(
+ {
+ "query": "test query",
+ "categories": ["invalid.category"],
+ "max_results": 1,
+ }
+ )
+
+ assert "Error: Invalid category" in result[0].text
+
+
+@pytest.mark.asyncio
+async def test_search_empty_query(mock_client):
+ """Test search with empty query but categories."""
+ with patch("arxiv.Client", return_value=mock_client):
+ result = await handle_search(
+ {"query": "", "categories": ["cs.AI"], "max_results": 1}
+ )
+
+ # Should still work with just categories
+ content = json.loads(result[0].text)
+ assert "papers" in content
+
+
+@pytest.mark.asyncio
+async def test_search_arxiv_error(mock_client):
+ """Test handling of arXiv API errors."""
+ import arxiv
+
+ # Create proper ArxivError with required parameters
+ error = arxiv.ArxivError("http://example.com", retry=3, message="API Error")
+ mock_client.results.side_effect = error
+
+ with patch("arxiv.Client", return_value=mock_client):
+ result = await handle_search({"query": "test", "max_results": 1})
+
+ assert "ArXiv API error" in result[0].text
+
+
+@pytest.mark.asyncio
+async def test_search_max_results_limiting(mock_client):
+ """Test that max_results is properly limited."""
+ with patch("arxiv.Client", return_value=mock_client):
+ # Test that very large max_results gets capped
+ result = await handle_search({"query": "test", "max_results": 1000})
+
+ # Should not fail and should be limited by settings.MAX_RESULTS
+ content = json.loads(result[0].text)
+ assert "papers" in content
+
+
+@pytest.mark.asyncio
+async def test_search_sort_by_relevance(mock_client):
+ """Test search with relevance sorting (default)."""
+ with patch("arxiv.Client", return_value=mock_client):
+ result = await handle_search({"query": "test", "sort_by": "relevance"})
+
+ content = json.loads(result[0].text)
+ assert "papers" in content
+
+
+@pytest.mark.asyncio
+async def test_search_sort_by_date(mock_client):
+ """Test search with date sorting."""
+ with patch("arxiv.Client", return_value=mock_client):
+ result = await handle_search({"query": "test", "sort_by": "date"})
+
+ content = json.loads(result[0].text)
+ assert "papers" in content
+
+
+@pytest.mark.asyncio
+async def test_search_no_query_optimization(mock_client):
+ """Test that queries are not automatically modified."""
+ from arxiv_mcp_server.tools.search import _optimize_query
+
+ # Test that complex queries are not mangled
+ complex_query = "graph neural networks message passing attention mechanism"
+ optimized = _optimize_query(complex_query)
+ assert optimized == complex_query
+
+ # Test that field-specific queries are preserved
+ field_query = 'ti:"graph neural networks"'
+ optimized = _optimize_query(field_query)
+ assert optimized == field_query
+
+ # Test that boolean queries are preserved
+ bool_query = "machine learning AND deep learning"
+ optimized = _optimize_query(bool_query)
+ assert optimized == bool_query
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/uv.lock b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/uv.lock
new file mode 100644
index 00000000..d33a1dd4
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/arxiv-mcp-server/uv.lock
@@ -0,0 +1,2127 @@
+version = 1
+revision = 2
+requires-python = ">=3.11"
+
+[[package]]
+name = "aiofiles"
+version = "25.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/41/c3/534eac40372d8ee36ef40df62ec129bee4fdb5ad9706e58a29be53b2c970/aiofiles-25.1.0.tar.gz", hash = "sha256:a8d728f0a29de45dc521f18f07297428d56992a742f0cd2701ba86e44d23d5b2", size = 46354, upload-time = "2025-10-09T20:51:04.358Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/bc/8a/340a1555ae33d7354dbca4faa54948d76d89a27ceef032c8c3bc661d003e/aiofiles-25.1.0-py3-none-any.whl", hash = "sha256:abe311e527c862958650f9438e859c1fa7568a141b22abcd015e120e86a85695", size = 14668, upload-time = "2025-10-09T20:51:03.174Z" },
+]
+
+[[package]]
+name = "aiohappyeyeballs"
+version = "2.6.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" },
+]
+
+[[package]]
+name = "aiohttp"
+version = "3.13.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "aiohappyeyeballs" },
+ { name = "aiosignal" },
+ { name = "attrs" },
+ { name = "frozenlist" },
+ { name = "multidict" },
+ { name = "propcache" },
+ { name = "yarl" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/50/42/32cf8e7704ceb4481406eb87161349abb46a57fee3f008ba9cb610968646/aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88", size = 7844556, upload-time = "2026-01-03T17:33:05.204Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f1/4c/a164164834f03924d9a29dc3acd9e7ee58f95857e0b467f6d04298594ebb/aiohttp-3.13.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5b6073099fb654e0a068ae678b10feff95c5cae95bbfcbfa7af669d361a8aa6b", size = 746051, upload-time = "2026-01-03T17:29:43.287Z" },
+ { url = "https://files.pythonhosted.org/packages/82/71/d5c31390d18d4f58115037c432b7e0348c60f6f53b727cad33172144a112/aiohttp-3.13.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cb93e166e6c28716c8c6aeb5f99dfb6d5ccf482d29fe9bf9a794110e6d0ab64", size = 499234, upload-time = "2026-01-03T17:29:44.822Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/c9/741f8ac91e14b1d2e7100690425a5b2b919a87a5075406582991fb7de920/aiohttp-3.13.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:28e027cf2f6b641693a09f631759b4d9ce9165099d2b5d92af9bd4e197690eea", size = 494979, upload-time = "2026-01-03T17:29:46.405Z" },
+ { url = "https://files.pythonhosted.org/packages/75/b5/31d4d2e802dfd59f74ed47eba48869c1c21552c586d5e81a9d0d5c2ad640/aiohttp-3.13.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3b61b7169ababd7802f9568ed96142616a9118dd2be0d1866e920e77ec8fa92a", size = 1748297, upload-time = "2026-01-03T17:29:48.083Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/3e/eefad0ad42959f226bb79664826883f2687d602a9ae2941a18e0484a74d3/aiohttp-3.13.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:80dd4c21b0f6237676449c6baaa1039abae86b91636b6c91a7f8e61c87f89540", size = 1707172, upload-time = "2026-01-03T17:29:49.648Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/3a/54a64299fac2891c346cdcf2aa6803f994a2e4beeaf2e5a09dcc54acc842/aiohttp-3.13.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65d2ccb7eabee90ce0503c17716fc77226be026dcc3e65cce859a30db715025b", size = 1805405, upload-time = "2026-01-03T17:29:51.244Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/70/ddc1b7169cf64075e864f64595a14b147a895a868394a48f6a8031979038/aiohttp-3.13.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b179331a481cb5529fca8b432d8d3c7001cb217513c94cd72d668d1248688a3", size = 1899449, upload-time = "2026-01-03T17:29:53.938Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/7e/6815aab7d3a56610891c76ef79095677b8b5be6646aaf00f69b221765021/aiohttp-3.13.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d4c940f02f49483b18b079d1c27ab948721852b281f8b015c058100e9421dd1", size = 1748444, upload-time = "2026-01-03T17:29:55.484Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/f2/073b145c4100da5511f457dc0f7558e99b2987cf72600d42b559db856fbc/aiohttp-3.13.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9444f105664c4ce47a2a7171a2418bce5b7bae45fb610f4e2c36045d85911d3", size = 1606038, upload-time = "2026-01-03T17:29:57.179Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/c1/778d011920cae03ae01424ec202c513dc69243cf2db303965615b81deeea/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:694976222c711d1d00ba131904beb60534f93966562f64440d0c9d41b8cdb440", size = 1724156, upload-time = "2026-01-03T17:29:58.914Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/cb/3419eabf4ec1e9ec6f242c32b689248365a1cf621891f6f0386632525494/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f33ed1a2bf1997a36661874b017f5c4b760f41266341af36febaf271d179f6d7", size = 1722340, upload-time = "2026-01-03T17:30:01.962Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/e5/76cf77bdbc435bf233c1f114edad39ed4177ccbfab7c329482b179cff4f4/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e636b3c5f61da31a92bf0d91da83e58fdfa96f178ba682f11d24f31944cdd28c", size = 1783041, upload-time = "2026-01-03T17:30:03.609Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/d4/dd1ca234c794fd29c057ce8c0566b8ef7fd6a51069de5f06fa84b9a1971c/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5d2d94f1f5fcbe40838ac51a6ab5704a6f9ea42e72ceda48de5e6b898521da51", size = 1596024, upload-time = "2026-01-03T17:30:05.132Z" },
+ { url = "https://files.pythonhosted.org/packages/55/58/4345b5f26661a6180afa686c473620c30a66afdf120ed3dd545bbc809e85/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2be0e9ccf23e8a94f6f0650ce06042cefc6ac703d0d7ab6c7a917289f2539ad4", size = 1804590, upload-time = "2026-01-03T17:30:07.135Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/06/05950619af6c2df7e0a431d889ba2813c9f0129cec76f663e547a5ad56f2/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9af5e68ee47d6534d36791bbe9b646d2a7c7deb6fc24d7943628edfbb3581f29", size = 1740355, upload-time = "2026-01-03T17:30:09.083Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/80/958f16de79ba0422d7c1e284b2abd0c84bc03394fbe631d0a39ffa10e1eb/aiohttp-3.13.3-cp311-cp311-win32.whl", hash = "sha256:a2212ad43c0833a873d0fb3c63fa1bacedd4cf6af2fee62bf4b739ceec3ab239", size = 433701, upload-time = "2026-01-03T17:30:10.869Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/f2/27cdf04c9851712d6c1b99df6821a6623c3c9e55956d4b1e318c337b5a48/aiohttp-3.13.3-cp311-cp311-win_amd64.whl", hash = "sha256:642f752c3eb117b105acbd87e2c143de710987e09860d674e068c4c2c441034f", size = 457678, upload-time = "2026-01-03T17:30:12.719Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/be/4fc11f202955a69e0db803a12a062b8379c970c7c84f4882b6da17337cc1/aiohttp-3.13.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b903a4dfee7d347e2d87697d0713be59e0b87925be030c9178c5faa58ea58d5c", size = 739732, upload-time = "2026-01-03T17:30:14.23Z" },
+ { url = "https://files.pythonhosted.org/packages/97/2c/621d5b851f94fa0bb7430d6089b3aa970a9d9b75196bc93bb624b0db237a/aiohttp-3.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a45530014d7a1e09f4a55f4f43097ba0fd155089372e105e4bff4ca76cb1b168", size = 494293, upload-time = "2026-01-03T17:30:15.96Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/43/4be01406b78e1be8320bb8316dc9c42dbab553d281c40364e0f862d5661c/aiohttp-3.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27234ef6d85c914f9efeb77ff616dbf4ad2380be0cda40b4db086ffc7ddd1b7d", size = 493533, upload-time = "2026-01-03T17:30:17.431Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/a8/5a35dc56a06a2c90d4742cbf35294396907027f80eea696637945a106f25/aiohttp-3.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d32764c6c9aafb7fb55366a224756387cd50bfa720f32b88e0e6fa45b27dcf29", size = 1737839, upload-time = "2026-01-03T17:30:19.422Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/62/4b9eeb331da56530bf2e198a297e5303e1c1ebdceeb00fe9b568a65c5a0c/aiohttp-3.13.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b1a6102b4d3ebc07dad44fbf07b45bb600300f15b552ddf1851b5390202ea2e3", size = 1703932, upload-time = "2026-01-03T17:30:21.756Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/f6/af16887b5d419e6a367095994c0b1332d154f647e7dc2bd50e61876e8e3d/aiohttp-3.13.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c014c7ea7fb775dd015b2d3137378b7be0249a448a1612268b5a90c2d81de04d", size = 1771906, upload-time = "2026-01-03T17:30:23.932Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/83/397c634b1bcc24292fa1e0c7822800f9f6569e32934bdeef09dae7992dfb/aiohttp-3.13.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2b8d8ddba8f95ba17582226f80e2de99c7a7948e66490ef8d947e272a93e9463", size = 1871020, upload-time = "2026-01-03T17:30:26Z" },
+ { url = "https://files.pythonhosted.org/packages/86/f6/a62cbbf13f0ac80a70f71b1672feba90fdb21fd7abd8dbf25c0105fb6fa3/aiohttp-3.13.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ae8dd55c8e6c4257eae3a20fd2c8f41edaea5992ed67156642493b8daf3cecc", size = 1755181, upload-time = "2026-01-03T17:30:27.554Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/87/20a35ad487efdd3fba93d5843efdfaa62d2f1479eaafa7453398a44faf13/aiohttp-3.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01ad2529d4b5035578f5081606a465f3b814c542882804e2e8cda61adf5c71bf", size = 1561794, upload-time = "2026-01-03T17:30:29.254Z" },
+ { url = "https://files.pythonhosted.org/packages/de/95/8fd69a66682012f6716e1bc09ef8a1a2a91922c5725cb904689f112309c4/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bb4f7475e359992b580559e008c598091c45b5088f28614e855e42d39c2f1033", size = 1697900, upload-time = "2026-01-03T17:30:31.033Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/66/7b94b3b5ba70e955ff597672dad1691333080e37f50280178967aff68657/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c19b90316ad3b24c69cd78d5c9b4f3aa4497643685901185b65166293d36a00f", size = 1728239, upload-time = "2026-01-03T17:30:32.703Z" },
+ { url = "https://files.pythonhosted.org/packages/47/71/6f72f77f9f7d74719692ab65a2a0252584bf8d5f301e2ecb4c0da734530a/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:96d604498a7c782cb15a51c406acaea70d8c027ee6b90c569baa6e7b93073679", size = 1740527, upload-time = "2026-01-03T17:30:34.695Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/b4/75ec16cbbd5c01bdaf4a05b19e103e78d7ce1ef7c80867eb0ace42ff4488/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:084911a532763e9d3dd95adf78a78f4096cd5f58cdc18e6fdbc1b58417a45423", size = 1554489, upload-time = "2026-01-03T17:30:36.864Z" },
+ { url = "https://files.pythonhosted.org/packages/52/8f/bc518c0eea29f8406dcf7ed1f96c9b48e3bc3995a96159b3fc11f9e08321/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7a4a94eb787e606d0a09404b9c38c113d3b099d508021faa615d70a0131907ce", size = 1767852, upload-time = "2026-01-03T17:30:39.433Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/f2/a07a75173124f31f11ea6f863dc44e6f09afe2bca45dd4e64979490deab1/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87797e645d9d8e222e04160ee32aa06bc5c163e8499f24db719e7852ec23093a", size = 1722379, upload-time = "2026-01-03T17:30:41.081Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/4a/1a3fee7c21350cac78e5c5cef711bac1b94feca07399f3d406972e2d8fcd/aiohttp-3.13.3-cp312-cp312-win32.whl", hash = "sha256:b04be762396457bef43f3597c991e192ee7da460a4953d7e647ee4b1c28e7046", size = 428253, upload-time = "2026-01-03T17:30:42.644Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/b7/76175c7cb4eb73d91ad63c34e29fc4f77c9386bba4a65b53ba8e05ee3c39/aiohttp-3.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:e3531d63d3bdfa7e3ac5e9b27b2dd7ec9df3206a98e0b3445fa906f233264c57", size = 455407, upload-time = "2026-01-03T17:30:44.195Z" },
+ { url = "https://files.pythonhosted.org/packages/97/8a/12ca489246ca1faaf5432844adbfce7ff2cc4997733e0af120869345643a/aiohttp-3.13.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5dff64413671b0d3e7d5918ea490bdccb97a4ad29b3f311ed423200b2203e01c", size = 734190, upload-time = "2026-01-03T17:30:45.832Z" },
+ { url = "https://files.pythonhosted.org/packages/32/08/de43984c74ed1fca5c014808963cc83cb00d7bb06af228f132d33862ca76/aiohttp-3.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:87b9aab6d6ed88235aa2970294f496ff1a1f9adcd724d800e9b952395a80ffd9", size = 491783, upload-time = "2026-01-03T17:30:47.466Z" },
+ { url = "https://files.pythonhosted.org/packages/17/f8/8dd2cf6112a5a76f81f81a5130c57ca829d101ad583ce57f889179accdda/aiohttp-3.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:425c126c0dc43861e22cb1c14ba4c8e45d09516d0a3ae0a3f7494b79f5f233a3", size = 490704, upload-time = "2026-01-03T17:30:49.373Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/40/a46b03ca03936f832bc7eaa47cfbb1ad012ba1be4790122ee4f4f8cba074/aiohttp-3.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f9120f7093c2a32d9647abcaf21e6ad275b4fbec5b55969f978b1a97c7c86bf", size = 1720652, upload-time = "2026-01-03T17:30:50.974Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/7e/917fe18e3607af92657e4285498f500dca797ff8c918bd7d90b05abf6c2a/aiohttp-3.13.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:697753042d57f4bf7122cab985bf15d0cef23c770864580f5af4f52023a56bd6", size = 1692014, upload-time = "2026-01-03T17:30:52.729Z" },
+ { url = "https://files.pythonhosted.org/packages/71/b6/cefa4cbc00d315d68973b671cf105b21a609c12b82d52e5d0c9ae61d2a09/aiohttp-3.13.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6de499a1a44e7de70735d0b39f67c8f25eb3d91eb3103be99ca0fa882cdd987d", size = 1759777, upload-time = "2026-01-03T17:30:54.537Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/e3/e06ee07b45e59e6d81498b591fc589629be1553abb2a82ce33efe2a7b068/aiohttp-3.13.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37239e9f9a7ea9ac5bf6b92b0260b01f8a22281996da609206a84df860bc1261", size = 1861276, upload-time = "2026-01-03T17:30:56.512Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/24/75d274228acf35ceeb2850b8ce04de9dd7355ff7a0b49d607ee60c29c518/aiohttp-3.13.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f76c1e3fe7d7c8afad7ed193f89a292e1999608170dcc9751a7462a87dfd5bc0", size = 1743131, upload-time = "2026-01-03T17:30:58.256Z" },
+ { url = "https://files.pythonhosted.org/packages/04/98/3d21dde21889b17ca2eea54fdcff21b27b93f45b7bb94ca029c31ab59dc3/aiohttp-3.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fc290605db2a917f6e81b0e1e0796469871f5af381ce15c604a3c5c7e51cb730", size = 1556863, upload-time = "2026-01-03T17:31:00.445Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/84/da0c3ab1192eaf64782b03971ab4055b475d0db07b17eff925e8c93b3aa5/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4021b51936308aeea0367b8f006dc999ca02bc118a0cc78c303f50a2ff6afb91", size = 1682793, upload-time = "2026-01-03T17:31:03.024Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/0f/5802ada182f575afa02cbd0ec5180d7e13a402afb7c2c03a9aa5e5d49060/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:49a03727c1bba9a97d3e93c9f93ca03a57300f484b6e935463099841261195d3", size = 1716676, upload-time = "2026-01-03T17:31:04.842Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/8c/714d53bd8b5a4560667f7bbbb06b20c2382f9c7847d198370ec6526af39c/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3d9908a48eb7416dc1f4524e69f1d32e5d90e3981e4e37eb0aa1cd18f9cfa2a4", size = 1733217, upload-time = "2026-01-03T17:31:06.868Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/79/e2176f46d2e963facea939f5be2d26368ce543622be6f00a12844d3c991f/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2712039939ec963c237286113c68dbad80a82a4281543f3abf766d9d73228998", size = 1552303, upload-time = "2026-01-03T17:31:08.958Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/6a/28ed4dea1759916090587d1fe57087b03e6c784a642b85ef48217b0277ae/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7bfdc049127717581866fa4708791220970ce291c23e28ccf3922c700740fdc0", size = 1763673, upload-time = "2026-01-03T17:31:10.676Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/35/4a3daeb8b9fab49240d21c04d50732313295e4bd813a465d840236dd0ce1/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8057c98e0c8472d8846b9c79f56766bcc57e3e8ac7bfd510482332366c56c591", size = 1721120, upload-time = "2026-01-03T17:31:12.575Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/9f/d643bb3c5fb99547323e635e251c609fbbc660d983144cfebec529e09264/aiohttp-3.13.3-cp313-cp313-win32.whl", hash = "sha256:1449ceddcdbcf2e0446957863af03ebaaa03f94c090f945411b61269e2cb5daf", size = 427383, upload-time = "2026-01-03T17:31:14.382Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/f1/ab0395f8a79933577cdd996dd2f9aa6014af9535f65dddcf88204682fe62/aiohttp-3.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:693781c45a4033d31d4187d2436f5ac701e7bbfe5df40d917736108c1cc7436e", size = 453899, upload-time = "2026-01-03T17:31:15.958Z" },
+ { url = "https://files.pythonhosted.org/packages/99/36/5b6514a9f5d66f4e2597e40dea2e3db271e023eb7a5d22defe96ba560996/aiohttp-3.13.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:ea37047c6b367fd4bd632bff8077449b8fa034b69e812a18e0132a00fae6e808", size = 737238, upload-time = "2026-01-03T17:31:17.909Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/49/459327f0d5bcd8c6c9ca69e60fdeebc3622861e696490d8674a6d0cb90a6/aiohttp-3.13.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6fc0e2337d1a4c3e6acafda6a78a39d4c14caea625124817420abceed36e2415", size = 492292, upload-time = "2026-01-03T17:31:19.919Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/0b/b97660c5fd05d3495b4eb27f2d0ef18dc1dc4eff7511a9bf371397ff0264/aiohttp-3.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c685f2d80bb67ca8c3837823ad76196b3694b0159d232206d1e461d3d434666f", size = 493021, upload-time = "2026-01-03T17:31:21.636Z" },
+ { url = "https://files.pythonhosted.org/packages/54/d4/438efabdf74e30aeceb890c3290bbaa449780583b1270b00661126b8aae4/aiohttp-3.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e377758516d262bde50c2584fc6c578af272559c409eecbdd2bae1601184d6", size = 1717263, upload-time = "2026-01-03T17:31:23.296Z" },
+ { url = "https://files.pythonhosted.org/packages/71/f2/7bddc7fd612367d1459c5bcf598a9e8f7092d6580d98de0e057eb42697ad/aiohttp-3.13.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:34749271508078b261c4abb1767d42b8d0c0cc9449c73a4df494777dc55f0687", size = 1669107, upload-time = "2026-01-03T17:31:25.334Z" },
+ { url = "https://files.pythonhosted.org/packages/00/5a/1aeaecca40e22560f97610a329e0e5efef5e0b5afdf9f857f0d93839ab2e/aiohttp-3.13.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82611aeec80eb144416956ec85b6ca45a64d76429c1ed46ae1b5f86c6e0c9a26", size = 1760196, upload-time = "2026-01-03T17:31:27.394Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/f8/0ff6992bea7bd560fc510ea1c815f87eedd745fe035589c71ce05612a19a/aiohttp-3.13.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2fff83cfc93f18f215896e3a190e8e5cb413ce01553901aca925176e7568963a", size = 1843591, upload-time = "2026-01-03T17:31:29.238Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/d1/e30e537a15f53485b61f5be525f2157da719819e8377298502aebac45536/aiohttp-3.13.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbe7d4cecacb439e2e2a8a1a7b935c25b812af7a5fd26503a66dadf428e79ec1", size = 1720277, upload-time = "2026-01-03T17:31:31.053Z" },
+ { url = "https://files.pythonhosted.org/packages/84/45/23f4c451d8192f553d38d838831ebbc156907ea6e05557f39563101b7717/aiohttp-3.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b928f30fe49574253644b1ca44b1b8adbd903aa0da4b9054a6c20fc7f4092a25", size = 1548575, upload-time = "2026-01-03T17:31:32.87Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/ed/0a42b127a43712eda7807e7892c083eadfaf8429ca8fb619662a530a3aab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b5e8fe4de30df199155baaf64f2fcd604f4c678ed20910db8e2c66dc4b11603", size = 1679455, upload-time = "2026-01-03T17:31:34.76Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/b5/c05f0c2b4b4fe2c9d55e73b6d3ed4fd6c9dc2684b1d81cbdf77e7fad9adb/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:8542f41a62bcc58fc7f11cf7c90e0ec324ce44950003feb70640fc2a9092c32a", size = 1687417, upload-time = "2026-01-03T17:31:36.699Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/6b/915bc5dad66aef602b9e459b5a973529304d4e89ca86999d9d75d80cbd0b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5e1d8c8b8f1d91cd08d8f4a3c2b067bfca6ec043d3ff36de0f3a715feeedf926", size = 1729968, upload-time = "2026-01-03T17:31:38.622Z" },
+ { url = "https://files.pythonhosted.org/packages/11/3b/e84581290a9520024a08640b63d07673057aec5ca548177a82026187ba73/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:90455115e5da1c3c51ab619ac57f877da8fd6d73c05aacd125c5ae9819582aba", size = 1545690, upload-time = "2026-01-03T17:31:40.57Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/04/0c3655a566c43fd647c81b895dfe361b9f9ad6d58c19309d45cff52d6c3b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:042e9e0bcb5fba81886c8b4fbb9a09d6b8a00245fd8d88e4d989c1f96c74164c", size = 1746390, upload-time = "2026-01-03T17:31:42.857Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/53/71165b26978f719c3419381514c9690bd5980e764a09440a10bb816ea4ab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2eb752b102b12a76ca02dff751a801f028b4ffbbc478840b473597fc91a9ed43", size = 1702188, upload-time = "2026-01-03T17:31:44.984Z" },
+ { url = "https://files.pythonhosted.org/packages/29/a7/cbe6c9e8e136314fa1980da388a59d2f35f35395948a08b6747baebb6aa6/aiohttp-3.13.3-cp314-cp314-win32.whl", hash = "sha256:b556c85915d8efaed322bf1bdae9486aa0f3f764195a0fb6ee962e5c71ef5ce1", size = 433126, upload-time = "2026-01-03T17:31:47.463Z" },
+ { url = "https://files.pythonhosted.org/packages/de/56/982704adea7d3b16614fc5936014e9af85c0e34b58f9046655817f04306e/aiohttp-3.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9bf9f7a65e7aa20dd764151fb3d616c81088f91f8df39c3893a536e279b4b984", size = 459128, upload-time = "2026-01-03T17:31:49.2Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/2a/3c79b638a9c3d4658d345339d22070241ea341ed4e07b5ac60fb0f418003/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:05861afbbec40650d8a07ea324367cb93e9e8cc7762e04dd4405df99fa65159c", size = 769512, upload-time = "2026-01-03T17:31:51.134Z" },
+ { url = "https://files.pythonhosted.org/packages/29/b9/3e5014d46c0ab0db8707e0ac2711ed28c4da0218c358a4e7c17bae0d8722/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2fc82186fadc4a8316768d61f3722c230e2c1dcab4200d52d2ebdf2482e47592", size = 506444, upload-time = "2026-01-03T17:31:52.85Z" },
+ { url = "https://files.pythonhosted.org/packages/90/03/c1d4ef9a054e151cd7839cdc497f2638f00b93cbe8043983986630d7a80c/aiohttp-3.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0add0900ff220d1d5c5ebbf99ed88b0c1bbf87aa7e4262300ed1376a6b13414f", size = 510798, upload-time = "2026-01-03T17:31:54.91Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/76/8c1e5abbfe8e127c893fe7ead569148a4d5a799f7cf958d8c09f3eedf097/aiohttp-3.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:568f416a4072fbfae453dcf9a99194bbb8bdeab718e08ee13dfa2ba0e4bebf29", size = 1868835, upload-time = "2026-01-03T17:31:56.733Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/ac/984c5a6f74c363b01ff97adc96a3976d9c98940b8969a1881575b279ac5d/aiohttp-3.13.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:add1da70de90a2569c5e15249ff76a631ccacfe198375eead4aadf3b8dc849dc", size = 1720486, upload-time = "2026-01-03T17:31:58.65Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/9a/b7039c5f099c4eb632138728828b33428585031a1e658d693d41d07d89d1/aiohttp-3.13.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b47b7ba335d2e9b1239fa571131a87e2d8ec96b333e68b2a305e7a98b0bae2", size = 1847951, upload-time = "2026-01-03T17:32:00.989Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/02/3bec2b9a1ba3c19ff89a43a19324202b8eb187ca1e928d8bdac9bbdddebd/aiohttp-3.13.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4dce1c718e38081c8f35f323209d4c1df7d4db4bab1b5c88a6b4d12b74587", size = 1941001, upload-time = "2026-01-03T17:32:03.122Z" },
+ { url = "https://files.pythonhosted.org/packages/37/df/d879401cedeef27ac4717f6426c8c36c3091c6e9f08a9178cc87549c537f/aiohttp-3.13.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34bac00a67a812570d4a460447e1e9e06fae622946955f939051e7cc895cfab8", size = 1797246, upload-time = "2026-01-03T17:32:05.255Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/15/be122de1f67e6953add23335c8ece6d314ab67c8bebb3f181063010795a7/aiohttp-3.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a19884d2ee70b06d9204b2727a7b9f983d0c684c650254679e716b0b77920632", size = 1627131, upload-time = "2026-01-03T17:32:07.607Z" },
+ { url = "https://files.pythonhosted.org/packages/12/12/70eedcac9134cfa3219ab7af31ea56bc877395b1ac30d65b1bc4b27d0438/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ca7f2bb6ba8348a3614c7918cc4bb73268c5ac2a207576b7afea19d3d9f64", size = 1795196, upload-time = "2026-01-03T17:32:09.59Z" },
+ { url = "https://files.pythonhosted.org/packages/32/11/b30e1b1cd1f3054af86ebe60df96989c6a414dd87e27ad16950eee420bea/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b0d95340658b9d2f11d9697f59b3814a9d3bb4b7a7c20b131df4bcef464037c0", size = 1782841, upload-time = "2026-01-03T17:32:11.445Z" },
+ { url = "https://files.pythonhosted.org/packages/88/0d/d98a9367b38912384a17e287850f5695c528cff0f14f791ce8ee2e4f7796/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1e53262fd202e4b40b70c3aff944a8155059beedc8a89bba9dc1f9ef06a1b56", size = 1795193, upload-time = "2026-01-03T17:32:13.705Z" },
+ { url = "https://files.pythonhosted.org/packages/43/a5/a2dfd1f5ff5581632c7f6a30e1744deda03808974f94f6534241ef60c751/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d60ac9663f44168038586cab2157e122e46bdef09e9368b37f2d82d354c23f72", size = 1621979, upload-time = "2026-01-03T17:32:15.965Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/f0/12973c382ae7c1cccbc4417e129c5bf54c374dfb85af70893646e1f0e749/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:90751b8eed69435bac9ff4e3d2f6b3af1f57e37ecb0fbeee59c0174c9e2d41df", size = 1822193, upload-time = "2026-01-03T17:32:18.219Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/5f/24155e30ba7f8c96918af1350eb0663e2430aad9e001c0489d89cd708ab1/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fc353029f176fd2b3ec6cfc71be166aba1936fe5d73dd1992ce289ca6647a9aa", size = 1769801, upload-time = "2026-01-03T17:32:20.25Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/f8/7314031ff5c10e6ece114da79b338ec17eeff3a079e53151f7e9f43c4723/aiohttp-3.13.3-cp314-cp314t-win32.whl", hash = "sha256:2e41b18a58da1e474a057b3d35248d8320029f61d70a37629535b16a0c8f3767", size = 466523, upload-time = "2026-01-03T17:32:22.215Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/63/278a98c715ae467624eafe375542d8ba9b4383a016df8fdefe0ae28382a7/aiohttp-3.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:44531a36aa2264a1860089ffd4dce7baf875ee5a6079d5fb42e261c704ef7344", size = 499694, upload-time = "2026-01-03T17:32:24.546Z" },
+]
+
+[[package]]
+name = "aioresponses"
+version = "0.7.8"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "aiohttp" },
+ { name = "packaging" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/de/03/532bbc645bdebcf3b6af3b25d46655259d66ce69abba7720b71ebfabbade/aioresponses-0.7.8.tar.gz", hash = "sha256:b861cdfe5dc58f3b8afac7b0a6973d5d7b2cb608dd0f6253d16b8ee8eaf6df11", size = 40253, upload-time = "2025-01-19T18:14:03.222Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/12/b7/584157e43c98aa89810bc2f7099e7e01c728ecf905a66cf705106009228f/aioresponses-0.7.8-py2.py3-none-any.whl", hash = "sha256:b73bd4400d978855e55004b23a3a84cb0f018183bcf066a85ad392800b5b9a94", size = 12518, upload-time = "2025-01-19T18:13:59.633Z" },
+]
+
+[[package]]
+name = "aiosignal"
+version = "1.4.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "frozenlist" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" },
+]
+
+[[package]]
+name = "annotated-types"
+version = "0.7.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
+]
+
+[[package]]
+name = "anyio"
+version = "4.12.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "idna" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" },
+]
+
+[[package]]
+name = "arxiv"
+version = "2.4.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "feedparser" },
+ { name = "requests" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/8d/aa/dc1c6c633f63fce090e7c067af8c528a5e61218a61c266ff615d46cbde0a/arxiv-2.4.0.tar.gz", hash = "sha256:cabe5470d031aa3f22d2744a7600391c62c3489653f0c62bec9019e62bb0554b", size = 74546, upload-time = "2026-01-05T02:43:16.823Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/11/63/9e71153b2d48c98f8079c90d7211bc65515cc1ad18c3328c3c0472e68f44/arxiv-2.4.0-py3-none-any.whl", hash = "sha256:c02ccb09a777aaadd75d3bc1d2627894ef9c987c651d0dacd864b9f69fb0569f", size = 12065, upload-time = "2026-01-05T02:43:12.542Z" },
+]
+
+[[package]]
+name = "arxiv-mcp-server"
+version = "0.3.2"
+source = { editable = "." }
+dependencies = [
+ { name = "aiofiles" },
+ { name = "aiohttp" },
+ { name = "anyio" },
+ { name = "arxiv" },
+ { name = "black" },
+ { name = "httpx" },
+ { name = "mcp" },
+ { name = "psycopg2-binary" },
+ { name = "pydantic" },
+ { name = "pydantic-settings" },
+ { name = "pymupdf-layout" },
+ { name = "pymupdf4llm" },
+ { name = "python-dateutil" },
+ { name = "python-dotenv" },
+ { name = "sse-starlette" },
+ { name = "uvicorn" },
+]
+
+[package.optional-dependencies]
+dev = [
+ { name = "black" },
+]
+test = [
+ { name = "aioresponses" },
+ { name = "pytest" },
+ { name = "pytest-asyncio" },
+ { name = "pytest-cov" },
+ { name = "pytest-mock" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "aiofiles", specifier = ">=23.2.1" },
+ { name = "aiohttp", specifier = ">=3.9.1" },
+ { name = "aioresponses", marker = "extra == 'test'", specifier = ">=0.7.6" },
+ { name = "anyio", specifier = ">=4.2.0" },
+ { name = "arxiv", specifier = ">=2.1.0" },
+ { name = "black", specifier = ">=25.1.0" },
+ { name = "black", marker = "extra == 'dev'", specifier = ">=23.3.0" },
+ { name = "httpx", specifier = ">=0.24.0" },
+ { name = "mcp", specifier = ">=1.2.0" },
+ { name = "psycopg2-binary", specifier = ">=2.9.11" },
+ { name = "pydantic", specifier = ">=2.8.0" },
+ { name = "pydantic-settings", specifier = ">=2.1.0" },
+ { name = "pymupdf-layout", specifier = ">=1.26.6" },
+ { name = "pymupdf4llm", specifier = ">=0.0.17" },
+ { name = "pytest", marker = "extra == 'test'", specifier = ">=8.0.0" },
+ { name = "pytest-asyncio", marker = "extra == 'test'", specifier = ">=0.23.5" },
+ { name = "pytest-cov", marker = "extra == 'test'", specifier = ">=4.1.0" },
+ { name = "pytest-mock", marker = "extra == 'test'", specifier = ">=3.10.0" },
+ { name = "python-dateutil", specifier = ">=2.8.2" },
+ { name = "python-dotenv", specifier = ">=1.0.0" },
+ { name = "sse-starlette", specifier = ">=1.8.2" },
+ { name = "uvicorn", specifier = ">=0.30.0" },
+]
+provides-extras = ["test", "dev"]
+
+[[package]]
+name = "attrs"
+version = "25.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" },
+]
+
+[[package]]
+name = "black"
+version = "26.1.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+ { name = "mypy-extensions" },
+ { name = "packaging" },
+ { name = "pathspec" },
+ { name = "platformdirs" },
+ { name = "pytokens" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/13/88/560b11e521c522440af991d46848a2bde64b5f7202ec14e1f46f9509d328/black-26.1.0.tar.gz", hash = "sha256:d294ac3340eef9c9eb5d29288e96dc719ff269a88e27b396340459dd85da4c58", size = 658785, upload-time = "2026-01-18T04:50:11.993Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/30/83/f05f22ff13756e1a8ce7891db517dbc06200796a16326258268f4658a745/black-26.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3cee1487a9e4c640dc7467aaa543d6c0097c391dc8ac74eb313f2fbf9d7a7cb5", size = 1831956, upload-time = "2026-01-18T04:59:21.38Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/f2/b2c570550e39bedc157715e43927360312d6dd677eed2cc149a802577491/black-26.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d62d14ca31c92adf561ebb2e5f2741bf8dea28aef6deb400d49cca011d186c68", size = 1672499, upload-time = "2026-01-18T04:59:23.257Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/d7/990d6a94dc9e169f61374b1c3d4f4dd3037e93c2cc12b6f3b12bc663aa7b/black-26.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb1dafbbaa3b1ee8b4550a84425aac8874e5f390200f5502cf3aee4a2acb2f14", size = 1735431, upload-time = "2026-01-18T04:59:24.729Z" },
+ { url = "https://files.pythonhosted.org/packages/36/1c/cbd7bae7dd3cb315dfe6eeca802bb56662cc92b89af272e014d98c1f2286/black-26.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:101540cb2a77c680f4f80e628ae98bd2bd8812fb9d72ade4f8995c5ff019e82c", size = 1400468, upload-time = "2026-01-18T04:59:27.381Z" },
+ { url = "https://files.pythonhosted.org/packages/59/b1/9fe6132bb2d0d1f7094613320b56297a108ae19ecf3041d9678aec381b37/black-26.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:6f3977a16e347f1b115662be07daa93137259c711e526402aa444d7a88fdc9d4", size = 1207332, upload-time = "2026-01-18T04:59:28.711Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/13/710298938a61f0f54cdb4d1c0baeb672c01ff0358712eddaf29f76d32a0b/black-26.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6eeca41e70b5f5c84f2f913af857cf2ce17410847e1d54642e658e078da6544f", size = 1878189, upload-time = "2026-01-18T04:59:30.682Z" },
+ { url = "https://files.pythonhosted.org/packages/79/a6/5179beaa57e5dbd2ec9f1c64016214057b4265647c62125aa6aeffb05392/black-26.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dd39eef053e58e60204f2cdf059e2442e2eb08f15989eefe259870f89614c8b6", size = 1700178, upload-time = "2026-01-18T04:59:32.387Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/04/c96f79d7b93e8f09d9298b333ca0d31cd9b2ee6c46c274fd0f531de9dc61/black-26.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9459ad0d6cd483eacad4c6566b0f8e42af5e8b583cee917d90ffaa3778420a0a", size = 1777029, upload-time = "2026-01-18T04:59:33.767Z" },
+ { url = "https://files.pythonhosted.org/packages/49/f9/71c161c4c7aa18bdda3776b66ac2dc07aed62053c7c0ff8bbda8c2624fe2/black-26.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:a19915ec61f3a8746e8b10adbac4a577c6ba9851fa4a9e9fbfbcf319887a5791", size = 1406466, upload-time = "2026-01-18T04:59:35.177Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/8b/a7b0f974e473b159d0ac1b6bcefffeb6bec465898a516ee5cc989503cbc7/black-26.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:643d27fb5facc167c0b1b59d0315f2674a6e950341aed0fc05cf307d22bf4954", size = 1216393, upload-time = "2026-01-18T04:59:37.18Z" },
+ { url = "https://files.pythonhosted.org/packages/79/04/fa2f4784f7237279332aa735cdfd5ae2e7730db0072fb2041dadda9ae551/black-26.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ba1d768fbfb6930fc93b0ecc32a43d8861ded16f47a40f14afa9bb04ab93d304", size = 1877781, upload-time = "2026-01-18T04:59:39.054Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/ad/5a131b01acc0e5336740a039628c0ab69d60cf09a2c87a4ec49f5826acda/black-26.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2b807c240b64609cb0e80d2200a35b23c7df82259f80bef1b2c96eb422b4aac9", size = 1699670, upload-time = "2026-01-18T04:59:41.005Z" },
+ { url = "https://files.pythonhosted.org/packages/da/7c/b05f22964316a52ab6b4265bcd52c0ad2c30d7ca6bd3d0637e438fc32d6e/black-26.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1de0f7d01cc894066a1153b738145b194414cc6eeaad8ef4397ac9abacf40f6b", size = 1775212, upload-time = "2026-01-18T04:59:42.545Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/a3/e8d1526bea0446e040193185353920a9506eab60a7d8beb062029129c7d2/black-26.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:91a68ae46bf07868963671e4d05611b179c2313301bd756a89ad4e3b3db2325b", size = 1409953, upload-time = "2026-01-18T04:59:44.357Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/5a/d62ebf4d8f5e3a1daa54adaab94c107b57be1b1a2f115a0249b41931e188/black-26.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:be5e2fe860b9bd9edbf676d5b60a9282994c03fbbd40fe8f5e75d194f96064ca", size = 1217707, upload-time = "2026-01-18T04:59:45.719Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/83/be35a175aacfce4b05584ac415fd317dd6c24e93a0af2dcedce0f686f5d8/black-26.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9dc8c71656a79ca49b8d3e2ce8103210c9481c57798b48deeb3a8bb02db5f115", size = 1871864, upload-time = "2026-01-18T04:59:47.586Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/f5/d33696c099450b1274d925a42b7a030cd3ea1f56d72e5ca8bbed5f52759c/black-26.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b22b3810451abe359a964cc88121d57f7bce482b53a066de0f1584988ca36e79", size = 1701009, upload-time = "2026-01-18T04:59:49.443Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/87/670dd888c537acb53a863bc15abbd85b22b429237d9de1b77c0ed6b79c42/black-26.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:53c62883b3f999f14e5d30b5a79bd437236658ad45b2f853906c7cbe79de00af", size = 1767806, upload-time = "2026-01-18T04:59:50.769Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/9c/cd3deb79bfec5bcf30f9d2100ffeec63eecce826eb63e3961708b9431ff1/black-26.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:f016baaadc423dc960cdddf9acae679e71ee02c4c341f78f3179d7e4819c095f", size = 1433217, upload-time = "2026-01-18T04:59:52.218Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/29/f3be41a1cf502a283506f40f5d27203249d181f7a1a2abce1c6ce188035a/black-26.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:66912475200b67ef5a0ab665011964bf924745103f51977a78b4fb92a9fc1bf0", size = 1245773, upload-time = "2026-01-18T04:59:54.457Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/3d/51bdb3ecbfadfaf825ec0c75e1de6077422b4afa2091c6c9ba34fbfc0c2d/black-26.1.0-py3-none-any.whl", hash = "sha256:1054e8e47ebd686e078c0bb0eaf31e6ce69c966058d122f2c0c950311f9f3ede", size = 204010, upload-time = "2026-01-18T04:50:09.978Z" },
+]
+
+[[package]]
+name = "certifi"
+version = "2026.1.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" },
+]
+
+[[package]]
+name = "cffi"
+version = "2.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pycparser", marker = "implementation_name != 'PyPy'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" },
+ { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" },
+ { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" },
+ { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" },
+ { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" },
+ { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" },
+ { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" },
+ { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" },
+ { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" },
+ { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" },
+ { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" },
+ { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" },
+ { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" },
+ { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" },
+ { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" },
+ { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" },
+ { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
+]
+
+[[package]]
+name = "charset-normalizer"
+version = "3.4.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" },
+ { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" },
+ { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" },
+ { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" },
+ { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" },
+ { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" },
+ { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" },
+ { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" },
+ { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" },
+ { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" },
+ { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" },
+ { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" },
+ { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" },
+ { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" },
+ { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" },
+ { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" },
+ { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" },
+ { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" },
+ { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" },
+ { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" },
+ { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" },
+ { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" },
+ { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" },
+ { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" },
+ { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" },
+ { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" },
+ { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" },
+ { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" },
+ { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" },
+ { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" },
+]
+
+[[package]]
+name = "click"
+version = "8.3.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" },
+]
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
+]
+
+[[package]]
+name = "coverage"
+version = "7.13.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ad/49/349848445b0e53660e258acbcc9b0d014895b6739237920886672240f84b/coverage-7.13.2.tar.gz", hash = "sha256:044c6951ec37146b72a50cc81ef02217d27d4c3640efd2640311393cbbf143d3", size = 826523, upload-time = "2026-01-25T13:00:04.889Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6c/01/abca50583a8975bb6e1c59eff67ed8e48bb127c07dad5c28d9e96ccc09ec/coverage-7.13.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:060ebf6f2c51aff5ba38e1f43a2095e087389b1c69d559fde6049a4b0001320e", size = 218971, upload-time = "2026-01-25T12:57:36.953Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/0e/b6489f344d99cd1e5b4d5e1be52dfd3f8a3dc5112aa6c33948da8cabad4e/coverage-7.13.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c1ea8ca9db5e7469cd364552985e15911548ea5b69c48a17291f0cac70484b2e", size = 219473, upload-time = "2026-01-25T12:57:38.934Z" },
+ { url = "https://files.pythonhosted.org/packages/17/11/db2f414915a8e4ec53f60b17956c27f21fb68fcf20f8a455ce7c2ccec638/coverage-7.13.2-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b780090d15fd58f07cf2011943e25a5f0c1c894384b13a216b6c86c8a8a7c508", size = 249896, upload-time = "2026-01-25T12:57:40.365Z" },
+ { url = "https://files.pythonhosted.org/packages/80/06/0823fe93913663c017e508e8810c998c8ebd3ec2a5a85d2c3754297bdede/coverage-7.13.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:88a800258d83acb803c38175b4495d293656d5fac48659c953c18e5f539a274b", size = 251810, upload-time = "2026-01-25T12:57:42.045Z" },
+ { url = "https://files.pythonhosted.org/packages/61/dc/b151c3cc41b28cdf7f0166c5fa1271cbc305a8ec0124cce4b04f74791a18/coverage-7.13.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6326e18e9a553e674d948536a04a80d850a5eeefe2aae2e6d7cf05d54046c01b", size = 253920, upload-time = "2026-01-25T12:57:44.026Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/35/e83de0556e54a4729a2b94ea816f74ce08732e81945024adee46851c2264/coverage-7.13.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:59562de3f797979e1ff07c587e2ac36ba60ca59d16c211eceaa579c266c5022f", size = 250025, upload-time = "2026-01-25T12:57:45.624Z" },
+ { url = "https://files.pythonhosted.org/packages/39/67/af2eb9c3926ce3ea0d58a0d2516fcbdacf7a9fc9559fe63076beaf3f2596/coverage-7.13.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:27ba1ed6f66b0e2d61bfa78874dffd4f8c3a12f8e2b5410e515ab345ba7bc9c3", size = 251612, upload-time = "2026-01-25T12:57:47.713Z" },
+ { url = "https://files.pythonhosted.org/packages/26/62/5be2e25f3d6c711d23b71296f8b44c978d4c8b4e5b26871abfc164297502/coverage-7.13.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8be48da4d47cc68754ce643ea50b3234557cbefe47c2f120495e7bd0a2756f2b", size = 249670, upload-time = "2026-01-25T12:57:49.378Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/51/400d1b09a8344199f9b6a6fc1868005d766b7ea95e7882e494fa862ca69c/coverage-7.13.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2a47a4223d3361b91176aedd9d4e05844ca67d7188456227b6bf5e436630c9a1", size = 249395, upload-time = "2026-01-25T12:57:50.86Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/36/f02234bc6e5230e2f0a63fd125d0a2093c73ef20fdf681c7af62a140e4e7/coverage-7.13.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c6f141b468740197d6bd38f2b26ade124363228cc3f9858bd9924ab059e00059", size = 250298, upload-time = "2026-01-25T12:57:52.287Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/06/713110d3dd3151b93611c9cbfc65c15b4156b44f927fced49ac0b20b32a4/coverage-7.13.2-cp311-cp311-win32.whl", hash = "sha256:89567798404af067604246e01a49ef907d112edf2b75ef814b1364d5ce267031", size = 221485, upload-time = "2026-01-25T12:57:53.876Z" },
+ { url = "https://files.pythonhosted.org/packages/16/0c/3ae6255fa1ebcb7dec19c9a59e85ef5f34566d1265c70af5b2fc981da834/coverage-7.13.2-cp311-cp311-win_amd64.whl", hash = "sha256:21dd57941804ae2ac7e921771a5e21bbf9aabec317a041d164853ad0a96ce31e", size = 222421, upload-time = "2026-01-25T12:57:55.433Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/37/fabc3179af4d61d89ea47bd04333fec735cd5e8b59baad44fed9fc4170d7/coverage-7.13.2-cp311-cp311-win_arm64.whl", hash = "sha256:10758e0586c134a0bafa28f2d37dd2cdb5e4a90de25c0fc0c77dabbad46eca28", size = 221088, upload-time = "2026-01-25T12:57:57.41Z" },
+ { url = "https://files.pythonhosted.org/packages/46/39/e92a35f7800222d3f7b2cbb7bbc3b65672ae8d501cb31801b2d2bd7acdf1/coverage-7.13.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f106b2af193f965d0d3234f3f83fc35278c7fb935dfbde56ae2da3dd2c03b84d", size = 219142, upload-time = "2026-01-25T12:58:00.448Z" },
+ { url = "https://files.pythonhosted.org/packages/45/7a/8bf9e9309c4c996e65c52a7c5a112707ecdd9fbaf49e10b5a705a402bbb4/coverage-7.13.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78f45d21dc4d5d6bd29323f0320089ef7eae16e4bef712dff79d184fa7330af3", size = 219503, upload-time = "2026-01-25T12:58:02.451Z" },
+ { url = "https://files.pythonhosted.org/packages/87/93/17661e06b7b37580923f3f12406ac91d78aeed293fb6da0b69cc7957582f/coverage-7.13.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fae91dfecd816444c74531a9c3d6ded17a504767e97aa674d44f638107265b99", size = 251006, upload-time = "2026-01-25T12:58:04.059Z" },
+ { url = "https://files.pythonhosted.org/packages/12/f0/f9e59fb8c310171497f379e25db060abef9fa605e09d63157eebec102676/coverage-7.13.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:264657171406c114787b441484de620e03d8f7202f113d62fcd3d9688baa3e6f", size = 253750, upload-time = "2026-01-25T12:58:05.574Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/b1/1935e31add2232663cf7edd8269548b122a7d100047ff93475dbaaae673e/coverage-7.13.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae47d8dcd3ded0155afbb59c62bd8ab07ea0fd4902e1c40567439e6db9dcaf2f", size = 254862, upload-time = "2026-01-25T12:58:07.647Z" },
+ { url = "https://files.pythonhosted.org/packages/af/59/b5e97071ec13df5f45da2b3391b6cdbec78ba20757bc92580a5b3d5fa53c/coverage-7.13.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8a0b33e9fd838220b007ce8f299114d406c1e8edb21336af4c97a26ecfd185aa", size = 251420, upload-time = "2026-01-25T12:58:09.309Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/75/9495932f87469d013dc515fb0ce1aac5fa97766f38f6b1a1deb1ee7b7f3a/coverage-7.13.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b3becbea7f3ce9a2d4d430f223ec15888e4deb31395840a79e916368d6004cce", size = 252786, upload-time = "2026-01-25T12:58:10.909Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/59/af550721f0eb62f46f7b8cb7e6f1860592189267b1c411a4e3a057caacee/coverage-7.13.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f819c727a6e6eeb8711e4ce63d78c620f69630a2e9d53bc95ca5379f57b6ba94", size = 250928, upload-time = "2026-01-25T12:58:12.449Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/b1/21b4445709aae500be4ab43bbcfb4e53dc0811c3396dcb11bf9f23fd0226/coverage-7.13.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:4f7b71757a3ab19f7ba286e04c181004c1d61be921795ee8ba6970fd0ec91da5", size = 250496, upload-time = "2026-01-25T12:58:14.047Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/b1/0f5d89dfe0392990e4f3980adbde3eb34885bc1effb2dc369e0bf385e389/coverage-7.13.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b7fc50d2afd2e6b4f6f2f403b70103d280a8e0cb35320cbbe6debcda02a1030b", size = 252373, upload-time = "2026-01-25T12:58:15.976Z" },
+ { url = "https://files.pythonhosted.org/packages/01/c9/0cf1a6a57a9968cc049a6b896693faa523c638a5314b1fc374eb2b2ac904/coverage-7.13.2-cp312-cp312-win32.whl", hash = "sha256:292250282cf9bcf206b543d7608bda17ca6fc151f4cbae949fc7e115112fbd41", size = 221696, upload-time = "2026-01-25T12:58:17.517Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/05/d7540bf983f09d32803911afed135524570f8c47bb394bf6206c1dc3a786/coverage-7.13.2-cp312-cp312-win_amd64.whl", hash = "sha256:eeea10169fac01549a7921d27a3e517194ae254b542102267bef7a93ed38c40e", size = 222504, upload-time = "2026-01-25T12:58:19.115Z" },
+ { url = "https://files.pythonhosted.org/packages/15/8b/1a9f037a736ced0a12aacf6330cdaad5008081142a7070bc58b0f7930cbc/coverage-7.13.2-cp312-cp312-win_arm64.whl", hash = "sha256:2a5b567f0b635b592c917f96b9a9cb3dbd4c320d03f4bf94e9084e494f2e8894", size = 221120, upload-time = "2026-01-25T12:58:21.334Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/f0/3d3eac7568ab6096ff23791a526b0048a1ff3f49d0e236b2af6fb6558e88/coverage-7.13.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ed75de7d1217cf3b99365d110975f83af0528c849ef5180a12fd91b5064df9d6", size = 219168, upload-time = "2026-01-25T12:58:23.376Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/a6/f8b5cfeddbab95fdef4dcd682d82e5dcff7a112ced57a959f89537ee9995/coverage-7.13.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:97e596de8fa9bada4d88fde64a3f4d37f1b6131e4faa32bad7808abc79887ddc", size = 219537, upload-time = "2026-01-25T12:58:24.932Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/e6/8d8e6e0c516c838229d1e41cadcec91745f4b1031d4db17ce0043a0423b4/coverage-7.13.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:68c86173562ed4413345410c9480a8d64864ac5e54a5cda236748031e094229f", size = 250528, upload-time = "2026-01-25T12:58:26.567Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/78/befa6640f74092b86961f957f26504c8fba3d7da57cc2ab7407391870495/coverage-7.13.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7be4d613638d678b2b3773b8f687537b284d7074695a43fe2fbbfc0e31ceaed1", size = 253132, upload-time = "2026-01-25T12:58:28.251Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/10/1630db1edd8ce675124a2ee0f7becc603d2bb7b345c2387b4b95c6907094/coverage-7.13.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7f63ce526a96acd0e16c4af8b50b64334239550402fb1607ce6a584a6d62ce9", size = 254374, upload-time = "2026-01-25T12:58:30.294Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/1d/0d9381647b1e8e6d310ac4140be9c428a0277330991e0c35bdd751e338a4/coverage-7.13.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:406821f37f864f968e29ac14c3fccae0fec9fdeba48327f0341decf4daf92d7c", size = 250762, upload-time = "2026-01-25T12:58:32.036Z" },
+ { url = "https://files.pythonhosted.org/packages/43/e4/5636dfc9a7c871ee8776af83ee33b4c26bc508ad6cee1e89b6419a366582/coverage-7.13.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ee68e5a4e3e5443623406b905db447dceddffee0dceb39f4e0cd9ec2a35004b5", size = 252502, upload-time = "2026-01-25T12:58:33.961Z" },
+ { url = "https://files.pythonhosted.org/packages/02/2a/7ff2884d79d420cbb2d12fed6fff727b6d0ef27253140d3cdbbd03187ee0/coverage-7.13.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2ee0e58cca0c17dd9c6c1cdde02bb705c7b3fbfa5f3b0b5afeda20d4ebff8ef4", size = 250463, upload-time = "2026-01-25T12:58:35.529Z" },
+ { url = "https://files.pythonhosted.org/packages/91/c0/ba51087db645b6c7261570400fc62c89a16278763f36ba618dc8657a187b/coverage-7.13.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6e5bbb5018bf76a56aabdb64246b5288d5ae1b7d0dd4d0534fe86df2c2992d1c", size = 250288, upload-time = "2026-01-25T12:58:37.226Z" },
+ { url = "https://files.pythonhosted.org/packages/03/07/44e6f428551c4d9faf63ebcefe49b30e5c89d1be96f6a3abd86a52da9d15/coverage-7.13.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a55516c68ef3e08e134e818d5e308ffa6b1337cc8b092b69b24287bf07d38e31", size = 252063, upload-time = "2026-01-25T12:58:38.821Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/67/35b730ad7e1859dd57e834d1bc06080d22d2f87457d53f692fce3f24a5a9/coverage-7.13.2-cp313-cp313-win32.whl", hash = "sha256:5b20211c47a8abf4abc3319d8ce2464864fa9f30c5fcaf958a3eed92f4f1fef8", size = 221716, upload-time = "2026-01-25T12:58:40.484Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/82/e5fcf5a97c72f45fc14829237a6550bf49d0ab882ac90e04b12a69db76b4/coverage-7.13.2-cp313-cp313-win_amd64.whl", hash = "sha256:14f500232e521201cf031549fb1ebdfc0a40f401cf519157f76c397e586c3beb", size = 222522, upload-time = "2026-01-25T12:58:43.247Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/f1/25d7b2f946d239dd2d6644ca2cc060d24f97551e2af13b6c24c722ae5f97/coverage-7.13.2-cp313-cp313-win_arm64.whl", hash = "sha256:9779310cb5a9778a60c899f075a8514c89fa6d10131445c2207fc893e0b14557", size = 221145, upload-time = "2026-01-25T12:58:45Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/f7/080376c029c8f76fadfe43911d0daffa0cbdc9f9418a0eead70c56fb7f4b/coverage-7.13.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e64fa5a1e41ce5df6b547cbc3d3699381c9e2c2c369c67837e716ed0f549d48e", size = 219861, upload-time = "2026-01-25T12:58:46.586Z" },
+ { url = "https://files.pythonhosted.org/packages/42/11/0b5e315af5ab35f4c4a70e64d3314e4eec25eefc6dec13be3a7d5ffe8ac5/coverage-7.13.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b01899e82a04085b6561eb233fd688474f57455e8ad35cd82286463ba06332b7", size = 220207, upload-time = "2026-01-25T12:58:48.277Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/0c/0874d0318fb1062117acbef06a09cf8b63f3060c22265adaad24b36306b7/coverage-7.13.2-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:838943bea48be0e2768b0cf7819544cdedc1bbb2f28427eabb6eb8c9eb2285d3", size = 261504, upload-time = "2026-01-25T12:58:49.904Z" },
+ { url = "https://files.pythonhosted.org/packages/83/5e/1cd72c22ecb30751e43a72f40ba50fcef1b7e93e3ea823bd9feda8e51f9a/coverage-7.13.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:93d1d25ec2b27e90bcfef7012992d1f5121b51161b8bffcda756a816cf13c2c3", size = 263582, upload-time = "2026-01-25T12:58:51.582Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/da/8acf356707c7a42df4d0657020308e23e5a07397e81492640c186268497c/coverage-7.13.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93b57142f9621b0d12349c43fc7741fe578e4bc914c1e5a54142856cfc0bf421", size = 266008, upload-time = "2026-01-25T12:58:53.234Z" },
+ { url = "https://files.pythonhosted.org/packages/41/41/ea1730af99960309423c6ea8d6a4f1fa5564b2d97bd1d29dda4b42611f04/coverage-7.13.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f06799ae1bdfff7ccb8665d75f8291c69110ba9585253de254688aa8a1ccc6c5", size = 260762, upload-time = "2026-01-25T12:58:55.372Z" },
+ { url = "https://files.pythonhosted.org/packages/22/fa/02884d2080ba71db64fdc127b311db60e01fe6ba797d9c8363725e39f4d5/coverage-7.13.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:7f9405ab4f81d490811b1d91c7a20361135a2df4c170e7f0b747a794da5b7f23", size = 263571, upload-time = "2026-01-25T12:58:57.52Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/6b/4083aaaeba9b3112f55ac57c2ce7001dc4d8fa3fcc228a39f09cc84ede27/coverage-7.13.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f9ab1d5b86f8fbc97a5b3cd6280a3fd85fef3b028689d8a2c00918f0d82c728c", size = 261200, upload-time = "2026-01-25T12:58:59.255Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/d2/aea92fa36d61955e8c416ede9cf9bf142aa196f3aea214bb67f85235a050/coverage-7.13.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:f674f59712d67e841525b99e5e2b595250e39b529c3bda14764e4f625a3fa01f", size = 260095, upload-time = "2026-01-25T12:59:01.066Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/ae/04ffe96a80f107ea21b22b2367175c621da920063260a1c22f9452fd7866/coverage-7.13.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c6cadac7b8ace1ba9144feb1ae3cb787a6065ba6d23ffc59a934b16406c26573", size = 262284, upload-time = "2026-01-25T12:59:02.802Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/7a/6f354dcd7dfc41297791d6fb4e0d618acb55810bde2c1fd14b3939e05c2b/coverage-7.13.2-cp313-cp313t-win32.whl", hash = "sha256:14ae4146465f8e6e6253eba0cccd57423e598a4cb925958b240c805300918343", size = 222389, upload-time = "2026-01-25T12:59:04.563Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/d5/080ad292a4a3d3daf411574be0a1f56d6dee2c4fdf6b005342be9fac807f/coverage-7.13.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9074896edd705a05769e3de0eac0a8388484b503b68863dd06d5e473f874fd47", size = 223450, upload-time = "2026-01-25T12:59:06.677Z" },
+ { url = "https://files.pythonhosted.org/packages/88/96/df576fbacc522e9fb8d1c4b7a7fc62eb734be56e2cba1d88d2eabe08ea3f/coverage-7.13.2-cp313-cp313t-win_arm64.whl", hash = "sha256:69e526e14f3f854eda573d3cf40cffd29a1a91c684743d904c33dbdcd0e0f3e7", size = 221707, upload-time = "2026-01-25T12:59:08.363Z" },
+ { url = "https://files.pythonhosted.org/packages/55/53/1da9e51a0775634b04fcc11eb25c002fc58ee4f92ce2e8512f94ac5fc5bf/coverage-7.13.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:387a825f43d680e7310e6f325b2167dd093bc8ffd933b83e9aa0983cf6e0a2ef", size = 219213, upload-time = "2026-01-25T12:59:11.909Z" },
+ { url = "https://files.pythonhosted.org/packages/46/35/b3caac3ebbd10230fea5a33012b27d19e999a17c9285c4228b4b2e35b7da/coverage-7.13.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f0d7fea9d8e5d778cd5a9e8fc38308ad688f02040e883cdc13311ef2748cb40f", size = 219549, upload-time = "2026-01-25T12:59:13.638Z" },
+ { url = "https://files.pythonhosted.org/packages/76/9c/e1cf7def1bdc72c1907e60703983a588f9558434a2ff94615747bd73c192/coverage-7.13.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e080afb413be106c95c4ee96b4fffdc9e2fa56a8bbf90b5c0918e5c4449412f5", size = 250586, upload-time = "2026-01-25T12:59:15.808Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/49/f54ec02ed12be66c8d8897270505759e057b0c68564a65c429ccdd1f139e/coverage-7.13.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a7fc042ba3c7ce25b8a9f097eb0f32a5ce1ccdb639d9eec114e26def98e1f8a4", size = 253093, upload-time = "2026-01-25T12:59:17.491Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/5e/aaf86be3e181d907e23c0f61fccaeb38de8e6f6b47aed92bf57d8fc9c034/coverage-7.13.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d0ba505e021557f7f8173ee8cd6b926373d8653e5ff7581ae2efce1b11ef4c27", size = 254446, upload-time = "2026-01-25T12:59:19.752Z" },
+ { url = "https://files.pythonhosted.org/packages/28/c8/a5fa01460e2d75b0c853b392080d6829d3ca8b5ab31e158fa0501bc7c708/coverage-7.13.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7de326f80e3451bd5cc7239ab46c73ddb658fe0b7649476bc7413572d36cd548", size = 250615, upload-time = "2026-01-25T12:59:21.928Z" },
+ { url = "https://files.pythonhosted.org/packages/86/0b/6d56315a55f7062bb66410732c24879ccb2ec527ab6630246de5fe45a1df/coverage-7.13.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:abaea04f1e7e34841d4a7b343904a3f59481f62f9df39e2cd399d69a187a9660", size = 252452, upload-time = "2026-01-25T12:59:23.592Z" },
+ { url = "https://files.pythonhosted.org/packages/30/19/9bc550363ebc6b0ea121977ee44d05ecd1e8bf79018b8444f1028701c563/coverage-7.13.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9f93959ee0c604bccd8e0697be21de0887b1f73efcc3aa73a3ec0fd13feace92", size = 250418, upload-time = "2026-01-25T12:59:25.392Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/53/580530a31ca2f0cc6f07a8f2ab5460785b02bb11bdf815d4c4d37a4c5169/coverage-7.13.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:13fe81ead04e34e105bf1b3c9f9cdf32ce31736ee5d90a8d2de02b9d3e1bcb82", size = 250231, upload-time = "2026-01-25T12:59:27.888Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/42/dd9093f919dc3088cb472893651884bd675e3df3d38a43f9053656dca9a2/coverage-7.13.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d6d16b0f71120e365741bca2cb473ca6fe38930bc5431c5e850ba949f708f892", size = 251888, upload-time = "2026-01-25T12:59:29.636Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/a6/0af4053e6e819774626e133c3d6f70fae4d44884bfc4b126cb647baee8d3/coverage-7.13.2-cp314-cp314-win32.whl", hash = "sha256:9b2f4714bb7d99ba3790ee095b3b4ac94767e1347fe424278a0b10acb3ff04fe", size = 221968, upload-time = "2026-01-25T12:59:31.424Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/cc/5aff1e1f80d55862442855517bb8ad8ad3a68639441ff6287dde6a58558b/coverage-7.13.2-cp314-cp314-win_amd64.whl", hash = "sha256:e4121a90823a063d717a96e0a0529c727fb31ea889369a0ee3ec00ed99bf6859", size = 222783, upload-time = "2026-01-25T12:59:33.118Z" },
+ { url = "https://files.pythonhosted.org/packages/de/20/09abafb24f84b3292cc658728803416c15b79f9ee5e68d25238a895b07d9/coverage-7.13.2-cp314-cp314-win_arm64.whl", hash = "sha256:6873f0271b4a15a33e7590f338d823f6f66f91ed147a03938d7ce26efd04eee6", size = 221348, upload-time = "2026-01-25T12:59:34.939Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/60/a3820c7232db63be060e4019017cd3426751c2699dab3c62819cdbcea387/coverage-7.13.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f61d349f5b7cd95c34017f1927ee379bfbe9884300d74e07cf630ccf7a610c1b", size = 219950, upload-time = "2026-01-25T12:59:36.624Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/37/e4ef5975fdeb86b1e56db9a82f41b032e3d93a840ebaf4064f39e770d5c5/coverage-7.13.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a43d34ce714f4ca674c0d90beb760eb05aad906f2c47580ccee9da8fe8bfb417", size = 220209, upload-time = "2026-01-25T12:59:38.339Z" },
+ { url = "https://files.pythonhosted.org/packages/54/df/d40e091d00c51adca1e251d3b60a8b464112efa3004949e96a74d7c19a64/coverage-7.13.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bff1b04cb9d4900ce5c56c4942f047dc7efe57e2608cb7c3c8936e9970ccdbee", size = 261576, upload-time = "2026-01-25T12:59:40.446Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/44/5259c4bed54e3392e5c176121af9f71919d96dde853386e7730e705f3520/coverage-7.13.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6ae99e4560963ad8e163e819e5d77d413d331fd00566c1e0856aa252303552c1", size = 263704, upload-time = "2026-01-25T12:59:42.346Z" },
+ { url = "https://files.pythonhosted.org/packages/16/bd/ae9f005827abcbe2c70157459ae86053971c9fa14617b63903abbdce26d9/coverage-7.13.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e79a8c7d461820257d9aa43716c4efc55366d7b292e46b5b37165be1d377405d", size = 266109, upload-time = "2026-01-25T12:59:44.073Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/c0/8e279c1c0f5b1eaa3ad9b0fb7a5637fc0379ea7d85a781c0fe0bb3cfc2ab/coverage-7.13.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:060ee84f6a769d40c492711911a76811b4befb6fba50abb450371abb720f5bd6", size = 260686, upload-time = "2026-01-25T12:59:45.804Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/47/3a8112627e9d863e7cddd72894171c929e94491a597811725befdcd76bce/coverage-7.13.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3bca209d001fd03ea2d978f8a4985093240a355c93078aee3f799852c23f561a", size = 263568, upload-time = "2026-01-25T12:59:47.929Z" },
+ { url = "https://files.pythonhosted.org/packages/92/bc/7ea367d84afa3120afc3ce6de294fd2dcd33b51e2e7fbe4bbfd200f2cb8c/coverage-7.13.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:6b8092aa38d72f091db61ef83cb66076f18f02da3e1a75039a4f218629600e04", size = 261174, upload-time = "2026-01-25T12:59:49.717Z" },
+ { url = "https://files.pythonhosted.org/packages/33/b7/f1092dcecb6637e31cc2db099581ee5c61a17647849bae6b8261a2b78430/coverage-7.13.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4a3158dc2dcce5200d91ec28cd315c999eebff355437d2765840555d765a6e5f", size = 260017, upload-time = "2026-01-25T12:59:51.463Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/cd/f3d07d4b95fbe1a2ef0958c15da614f7e4f557720132de34d2dc3aa7e911/coverage-7.13.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3973f353b2d70bd9796cc12f532a05945232ccae966456c8ed7034cb96bbfd6f", size = 262337, upload-time = "2026-01-25T12:59:53.407Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/db/b0d5b2873a07cb1e06a55d998697c0a5a540dcefbf353774c99eb3874513/coverage-7.13.2-cp314-cp314t-win32.whl", hash = "sha256:79f6506a678a59d4ded048dc72f1859ebede8ec2b9a2d509ebe161f01c2879d3", size = 222749, upload-time = "2026-01-25T12:59:56.316Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/2f/838a5394c082ac57d85f57f6aba53093b30d9089781df72412126505716f/coverage-7.13.2-cp314-cp314t-win_amd64.whl", hash = "sha256:196bfeabdccc5a020a57d5a368c681e3a6ceb0447d153aeccc1ab4d70a5032ba", size = 223857, upload-time = "2026-01-25T12:59:58.201Z" },
+ { url = "https://files.pythonhosted.org/packages/44/d4/b608243e76ead3a4298824b50922b89ef793e50069ce30316a65c1b4d7ef/coverage-7.13.2-cp314-cp314t-win_arm64.whl", hash = "sha256:69269ab58783e090bfbf5b916ab3d188126e22d6070bbfc93098fdd474ef937c", size = 221881, upload-time = "2026-01-25T13:00:00.449Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/db/d291e30fdf7ea617a335531e72294e0c723356d7fdde8fba00610a76bda9/coverage-7.13.2-py3-none-any.whl", hash = "sha256:40ce1ea1e25125556d8e76bd0b61500839a07944cc287ac21d5626f3e620cad5", size = 210943, upload-time = "2026-01-25T13:00:02.388Z" },
+]
+
+[package.optional-dependencies]
+toml = [
+ { name = "tomli", marker = "python_full_version <= '3.11'" },
+]
+
+[[package]]
+name = "cryptography"
+version = "46.0.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258, upload-time = "2025-10-15T23:18:31.74Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1d/42/9c391dd801d6cf0d561b5890549d4b27bafcc53b39c31a817e69d87c625b/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a", size = 7225004, upload-time = "2025-10-15T23:16:52.239Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667, upload-time = "2025-10-15T23:16:54.369Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807, upload-time = "2025-10-15T23:16:56.414Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615, upload-time = "2025-10-15T23:16:58.442Z" },
+ { url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800, upload-time = "2025-10-15T23:17:00.378Z" },
+ { url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707, upload-time = "2025-10-15T23:17:01.98Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541, upload-time = "2025-10-15T23:17:04.078Z" },
+ { url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464, upload-time = "2025-10-15T23:17:05.483Z" },
+ { url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838, upload-time = "2025-10-15T23:17:07.425Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596, upload-time = "2025-10-15T23:17:09.343Z" },
+ { url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782, upload-time = "2025-10-15T23:17:11.22Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381, upload-time = "2025-10-15T23:17:12.829Z" },
+ { url = "https://files.pythonhosted.org/packages/96/92/8a6a9525893325fc057a01f654d7efc2c64b9de90413adcf605a85744ff4/cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018", size = 3055988, upload-time = "2025-10-15T23:17:14.65Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/bf/80fbf45253ea585a1e492a6a17efcb93467701fa79e71550a430c5e60df0/cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb", size = 3514451, upload-time = "2025-10-15T23:17:16.142Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/af/9b302da4c87b0beb9db4e756386a7c6c5b8003cd0e742277888d352ae91d/cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c", size = 2928007, upload-time = "2025-10-15T23:17:18.04Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/e2/a510aa736755bffa9d2f75029c229111a1d02f8ecd5de03078f4c18d91a3/cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217", size = 7158012, upload-time = "2025-10-15T23:17:19.982Z" },
+ { url = "https://files.pythonhosted.org/packages/73/dc/9aa866fbdbb95b02e7f9d086f1fccfeebf8953509b87e3f28fff927ff8a0/cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5", size = 4288728, upload-time = "2025-10-15T23:17:21.527Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/fd/bc1daf8230eaa075184cbbf5f8cd00ba9db4fd32d63fb83da4671b72ed8a/cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715", size = 4435078, upload-time = "2025-10-15T23:17:23.042Z" },
+ { url = "https://files.pythonhosted.org/packages/82/98/d3bd5407ce4c60017f8ff9e63ffee4200ab3e23fe05b765cab805a7db008/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54", size = 4293460, upload-time = "2025-10-15T23:17:24.885Z" },
+ { url = "https://files.pythonhosted.org/packages/26/e9/e23e7900983c2b8af7a08098db406cf989d7f09caea7897e347598d4cd5b/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459", size = 3995237, upload-time = "2025-10-15T23:17:26.449Z" },
+ { url = "https://files.pythonhosted.org/packages/91/15/af68c509d4a138cfe299d0d7ddb14afba15233223ebd933b4bbdbc7155d3/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422", size = 4967344, upload-time = "2025-10-15T23:17:28.06Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/e3/8643d077c53868b681af077edf6b3cb58288b5423610f21c62aadcbe99f4/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7", size = 4466564, upload-time = "2025-10-15T23:17:29.665Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/43/c1e8726fa59c236ff477ff2b5dc071e54b21e5a1e51aa2cee1676f1c986f/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044", size = 4292415, upload-time = "2025-10-15T23:17:31.686Z" },
+ { url = "https://files.pythonhosted.org/packages/42/f9/2f8fefdb1aee8a8e3256a0568cffc4e6d517b256a2fe97a029b3f1b9fe7e/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665", size = 4931457, upload-time = "2025-10-15T23:17:33.478Z" },
+ { url = "https://files.pythonhosted.org/packages/79/30/9b54127a9a778ccd6d27c3da7563e9f2d341826075ceab89ae3b41bf5be2/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3", size = 4466074, upload-time = "2025-10-15T23:17:35.158Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/68/b4f4a10928e26c941b1b6a179143af9f4d27d88fe84a6a3c53592d2e76bf/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20", size = 4420569, upload-time = "2025-10-15T23:17:37.188Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/49/3746dab4c0d1979888f125226357d3262a6dd40e114ac29e3d2abdf1ec55/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de", size = 4681941, upload-time = "2025-10-15T23:17:39.236Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/30/27654c1dbaf7e4a3531fa1fc77986d04aefa4d6d78259a62c9dc13d7ad36/cryptography-46.0.3-cp314-cp314t-win32.whl", hash = "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914", size = 3022339, upload-time = "2025-10-15T23:17:40.888Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/30/640f34ccd4d2a1bc88367b54b926b781b5a018d65f404d409aba76a84b1c/cryptography-46.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db", size = 3494315, upload-time = "2025-10-15T23:17:42.769Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/8b/88cc7e3bd0a8e7b861f26981f7b820e1f46aa9d26cc482d0feba0ecb4919/cryptography-46.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21", size = 2919331, upload-time = "2025-10-15T23:17:44.468Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/23/45fe7f376a7df8daf6da3556603b36f53475a99ce4faacb6ba2cf3d82021/cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936", size = 7218248, upload-time = "2025-10-15T23:17:46.294Z" },
+ { url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089, upload-time = "2025-10-15T23:17:48.269Z" },
+ { url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029, upload-time = "2025-10-15T23:17:49.837Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222, upload-time = "2025-10-15T23:17:51.357Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280, upload-time = "2025-10-15T23:17:52.964Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958, upload-time = "2025-10-15T23:17:54.965Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714, upload-time = "2025-10-15T23:17:56.754Z" },
+ { url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970, upload-time = "2025-10-15T23:17:58.588Z" },
+ { url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236, upload-time = "2025-10-15T23:18:00.897Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642, upload-time = "2025-10-15T23:18:02.749Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126, upload-time = "2025-10-15T23:18:04.85Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573, upload-time = "2025-10-15T23:18:06.908Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", size = 3036695, upload-time = "2025-10-15T23:18:08.672Z" },
+ { url = "https://files.pythonhosted.org/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", size = 3501720, upload-time = "2025-10-15T23:18:10.632Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740, upload-time = "2025-10-15T23:18:12.277Z" },
+ { url = "https://files.pythonhosted.org/packages/06/8a/e60e46adab4362a682cf142c7dcb5bf79b782ab2199b0dcb81f55970807f/cryptography-46.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7ce938a99998ed3c8aa7e7272dca1a610401ede816d36d0693907d863b10d9ea", size = 3698132, upload-time = "2025-10-15T23:18:17.056Z" },
+ { url = "https://files.pythonhosted.org/packages/da/38/f59940ec4ee91e93d3311f7532671a5cef5570eb04a144bf203b58552d11/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:191bb60a7be5e6f54e30ba16fdfae78ad3a342a0599eb4193ba88e3f3d6e185b", size = 4243992, upload-time = "2025-10-15T23:18:18.695Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/0c/35b3d92ddebfdfda76bb485738306545817253d0a3ded0bfe80ef8e67aa5/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c70cc23f12726be8f8bc72e41d5065d77e4515efae3690326764ea1b07845cfb", size = 4409944, upload-time = "2025-10-15T23:18:20.597Z" },
+ { url = "https://files.pythonhosted.org/packages/99/55/181022996c4063fc0e7666a47049a1ca705abb9c8a13830f074edb347495/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:9394673a9f4de09e28b5356e7fff97d778f8abad85c9d5ac4a4b7e25a0de7717", size = 4242957, upload-time = "2025-10-15T23:18:22.18Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/af/72cd6ef29f9c5f731251acadaeb821559fe25f10852f44a63374c9ca08c1/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94cd0549accc38d1494e1f8de71eca837d0509d0d44bf11d158524b0e12cebf9", size = 4409447, upload-time = "2025-10-15T23:18:24.209Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/c3/e90f4a4feae6410f914f8ebac129b9ae7a8c92eb60a638012dde42030a9d/cryptography-46.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6b5063083824e5509fdba180721d55909ffacccc8adbec85268b48439423d78c", size = 3438528, upload-time = "2025-10-15T23:18:26.227Z" },
+]
+
+[[package]]
+name = "feedparser"
+version = "6.0.12"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "sgmllib3k" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/dc/79/db7edb5e77d6dfbc54d7d9df72828be4318275b2e580549ff45a962f6461/feedparser-6.0.12.tar.gz", hash = "sha256:64f76ce90ae3e8ef5d1ede0f8d3b50ce26bcce71dd8ae5e82b1cd2d4a5f94228", size = 286579, upload-time = "2025-09-10T13:33:59.486Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/4e/eb/c96d64137e29ae17d83ad2552470bafe3a7a915e85434d9942077d7fd011/feedparser-6.0.12-py3-none-any.whl", hash = "sha256:6bbff10f5a52662c00a2e3f86a38928c37c48f77b3c511aedcd51de933549324", size = 81480, upload-time = "2025-09-10T13:33:58.022Z" },
+]
+
+[[package]]
+name = "flatbuffers"
+version = "25.12.19"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl", hash = "sha256:7634f50c427838bb021c2d66a3d1168e9d199b0607e6329399f04846d42e20b4", size = 26661, upload-time = "2025-12-19T23:16:13.622Z" },
+]
+
+[[package]]
+name = "frozenlist"
+version = "1.8.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/bc/03/077f869d540370db12165c0aa51640a873fb661d8b315d1d4d67b284d7ac/frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84", size = 86912, upload-time = "2025-10-06T05:35:45.98Z" },
+ { url = "https://files.pythonhosted.org/packages/df/b5/7610b6bd13e4ae77b96ba85abea1c8cb249683217ef09ac9e0ae93f25a91/frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9", size = 50046, upload-time = "2025-10-06T05:35:47.009Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/ef/0e8f1fe32f8a53dd26bdd1f9347efe0778b0fddf62789ea683f4cc7d787d/frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93", size = 50119, upload-time = "2025-10-06T05:35:48.38Z" },
+ { url = "https://files.pythonhosted.org/packages/11/b1/71a477adc7c36e5fb628245dfbdea2166feae310757dea848d02bd0689fd/frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f", size = 231067, upload-time = "2025-10-06T05:35:49.97Z" },
+ { url = "https://files.pythonhosted.org/packages/45/7e/afe40eca3a2dc19b9904c0f5d7edfe82b5304cb831391edec0ac04af94c2/frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695", size = 233160, upload-time = "2025-10-06T05:35:51.729Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/aa/7416eac95603ce428679d273255ffc7c998d4132cfae200103f164b108aa/frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52", size = 228544, upload-time = "2025-10-06T05:35:53.246Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/3d/2a2d1f683d55ac7e3875e4263d28410063e738384d3adc294f5ff3d7105e/frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581", size = 243797, upload-time = "2025-10-06T05:35:54.497Z" },
+ { url = "https://files.pythonhosted.org/packages/78/1e/2d5565b589e580c296d3bb54da08d206e797d941a83a6fdea42af23be79c/frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567", size = 247923, upload-time = "2025-10-06T05:35:55.861Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/c3/65872fcf1d326a7f101ad4d86285c403c87be7d832b7470b77f6d2ed5ddc/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b", size = 230886, upload-time = "2025-10-06T05:35:57.399Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/76/ac9ced601d62f6956f03cc794f9e04c81719509f85255abf96e2510f4265/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92", size = 245731, upload-time = "2025-10-06T05:35:58.563Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/49/ecccb5f2598daf0b4a1415497eba4c33c1e8ce07495eb07d2860c731b8d5/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d", size = 241544, upload-time = "2025-10-06T05:35:59.719Z" },
+ { url = "https://files.pythonhosted.org/packages/53/4b/ddf24113323c0bbcc54cb38c8b8916f1da7165e07b8e24a717b4a12cbf10/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd", size = 241806, upload-time = "2025-10-06T05:36:00.959Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/fb/9b9a084d73c67175484ba2789a59f8eebebd0827d186a8102005ce41e1ba/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967", size = 229382, upload-time = "2025-10-06T05:36:02.22Z" },
+ { url = "https://files.pythonhosted.org/packages/95/a3/c8fb25aac55bf5e12dae5c5aa6a98f85d436c1dc658f21c3ac73f9fa95e5/frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25", size = 39647, upload-time = "2025-10-06T05:36:03.409Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/f5/603d0d6a02cfd4c8f2a095a54672b3cf967ad688a60fb9faf04fc4887f65/frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b", size = 44064, upload-time = "2025-10-06T05:36:04.368Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/16/c2c9ab44e181f043a86f9a8f84d5124b62dbcb3a02c0977ec72b9ac1d3e0/frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a", size = 39937, upload-time = "2025-10-06T05:36:05.669Z" },
+ { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" },
+ { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" },
+ { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" },
+ { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" },
+ { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" },
+ { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" },
+ { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" },
+ { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" },
+ { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" },
+ { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" },
+ { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" },
+ { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" },
+ { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" },
+ { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" },
+ { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" },
+ { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" },
+ { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" },
+ { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" },
+ { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" },
+ { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" },
+ { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" },
+]
+
+[[package]]
+name = "h11"
+version = "0.16.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
+]
+
+[[package]]
+name = "httpcore"
+version = "1.0.9"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "h11" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
+]
+
+[[package]]
+name = "httpx"
+version = "0.28.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "certifi" },
+ { name = "httpcore" },
+ { name = "idna" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
+]
+
+[[package]]
+name = "httpx-sse"
+version = "0.4.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" },
+]
+
+[[package]]
+name = "idna"
+version = "3.11"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
+]
+
+[[package]]
+name = "iniconfig"
+version = "2.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
+]
+
+[[package]]
+name = "jsonschema"
+version = "4.26.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "attrs" },
+ { name = "jsonschema-specifications" },
+ { name = "referencing" },
+ { name = "rpds-py" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" },
+]
+
+[[package]]
+name = "jsonschema-specifications"
+version = "2025.9.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "referencing" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" },
+]
+
+[[package]]
+name = "mcp"
+version = "1.26.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "httpx" },
+ { name = "httpx-sse" },
+ { name = "jsonschema" },
+ { name = "pydantic" },
+ { name = "pydantic-settings" },
+ { name = "pyjwt", extra = ["crypto"] },
+ { name = "python-multipart" },
+ { name = "pywin32", marker = "sys_platform == 'win32'" },
+ { name = "sse-starlette" },
+ { name = "starlette" },
+ { name = "typing-extensions" },
+ { name = "typing-inspection" },
+ { name = "uvicorn", marker = "sys_platform != 'emscripten'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/fc/6d/62e76bbb8144d6ed86e202b5edd8a4cb631e7c8130f3f4893c3f90262b10/mcp-1.26.0.tar.gz", hash = "sha256:db6e2ef491eecc1a0d93711a76f28dec2e05999f93afd48795da1c1137142c66", size = 608005, upload-time = "2026-01-24T19:40:32.468Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fd/d9/eaa1f80170d2b7c5ba23f3b59f766f3a0bb41155fbc32a69adfa1adaaef9/mcp-1.26.0-py3-none-any.whl", hash = "sha256:904a21c33c25aa98ddbeb47273033c435e595bbacfdb177f4bd87f6dceebe1ca", size = 233615, upload-time = "2026-01-24T19:40:30.652Z" },
+]
+
+[[package]]
+name = "mpmath"
+version = "1.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" },
+]
+
+[[package]]
+name = "multidict"
+version = "6.7.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ce/f1/a90635c4f88fb913fbf4ce660b83b7445b7a02615bda034b2f8eb38fd597/multidict-6.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d", size = 76626, upload-time = "2026-01-26T02:43:26.485Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/9b/267e64eaf6fc637a15b35f5de31a566634a2740f97d8d094a69d34f524a4/multidict-6.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e", size = 44706, upload-time = "2026-01-26T02:43:27.607Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/a4/d45caf2b97b035c57267791ecfaafbd59c68212004b3842830954bb4b02e/multidict-6.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855", size = 44356, upload-time = "2026-01-26T02:43:28.661Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/d2/0a36c8473f0cbaeadd5db6c8b72d15bbceeec275807772bfcd059bef487d/multidict-6.7.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3", size = 244355, upload-time = "2026-01-26T02:43:31.165Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/16/8c65be997fd7dd311b7d39c7b6e71a0cb449bad093761481eccbbe4b42a2/multidict-6.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e", size = 246433, upload-time = "2026-01-26T02:43:32.581Z" },
+ { url = "https://files.pythonhosted.org/packages/01/fb/4dbd7e848d2799c6a026ec88ad39cf2b8416aa167fcc903baa55ecaa045c/multidict-6.7.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a", size = 225376, upload-time = "2026-01-26T02:43:34.417Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/8a/4a3a6341eac3830f6053062f8fbc9a9e54407c80755b3f05bc427295c2d0/multidict-6.7.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8", size = 257365, upload-time = "2026-01-26T02:43:35.741Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/a2/dd575a69c1aa206e12d27d0770cdf9b92434b48a9ef0cd0d1afdecaa93c4/multidict-6.7.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0", size = 254747, upload-time = "2026-01-26T02:43:36.976Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/56/21b27c560c13822ed93133f08aa6372c53a8e067f11fbed37b4adcdac922/multidict-6.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144", size = 246293, upload-time = "2026-01-26T02:43:38.258Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/a4/23466059dc3854763423d0ad6c0f3683a379d97673b1b89ec33826e46728/multidict-6.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49", size = 242962, upload-time = "2026-01-26T02:43:40.034Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/67/51dd754a3524d685958001e8fa20a0f5f90a6a856e0a9dcabff69be3dbb7/multidict-6.7.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71", size = 237360, upload-time = "2026-01-26T02:43:41.752Z" },
+ { url = "https://files.pythonhosted.org/packages/64/3f/036dfc8c174934d4b55d86ff4f978e558b0e585cef70cfc1ad01adc6bf18/multidict-6.7.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3", size = 245940, upload-time = "2026-01-26T02:43:43.042Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/20/6214d3c105928ebc353a1c644a6ef1408bc5794fcb4f170bb524a3c16311/multidict-6.7.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c", size = 253502, upload-time = "2026-01-26T02:43:44.371Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/e2/c653bc4ae1be70a0f836b82172d643fcf1dade042ba2676ab08ec08bff0f/multidict-6.7.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0", size = 247065, upload-time = "2026-01-26T02:43:45.745Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/11/a854b4154cd3bd8b1fd375e8a8ca9d73be37610c361543d56f764109509b/multidict-6.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa", size = 241870, upload-time = "2026-01-26T02:43:47.054Z" },
+ { url = "https://files.pythonhosted.org/packages/13/bf/9676c0392309b5fdae322333d22a829715b570edb9baa8016a517b55b558/multidict-6.7.1-cp311-cp311-win32.whl", hash = "sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a", size = 41302, upload-time = "2026-01-26T02:43:48.753Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/68/f16a3a8ba6f7b6dc92a1f19669c0810bd2c43fc5a02da13b1cbf8e253845/multidict-6.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b", size = 45981, upload-time = "2026-01-26T02:43:49.921Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/ad/9dd5305253fa00cd3c7555dbef69d5bf4133debc53b87ab8d6a44d411665/multidict-6.7.1-cp311-cp311-win_arm64.whl", hash = "sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6", size = 43159, upload-time = "2026-01-26T02:43:51.635Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456, upload-time = "2026-01-26T02:43:53.893Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018, upload-time = "2026-01-26T02:43:56.198Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883, upload-time = "2026-01-26T02:43:57.499Z" },
+ { url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413, upload-time = "2026-01-26T02:43:58.755Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404, upload-time = "2026-01-26T02:44:00.216Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456, upload-time = "2026-01-26T02:44:02.202Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322, upload-time = "2026-01-26T02:44:03.56Z" },
+ { url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955, upload-time = "2026-01-26T02:44:04.845Z" },
+ { url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254, upload-time = "2026-01-26T02:44:06.133Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059, upload-time = "2026-01-26T02:44:07.518Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588, upload-time = "2026-01-26T02:44:09.382Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642, upload-time = "2026-01-26T02:44:10.73Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377, upload-time = "2026-01-26T02:44:12.042Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887, upload-time = "2026-01-26T02:44:14.245Z" },
+ { url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053, upload-time = "2026-01-26T02:44:15.371Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307, upload-time = "2026-01-26T02:44:16.852Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/22/929c141d6c0dba87d3e1d38fbdf1ba8baba86b7776469f2bc2d3227a1e67/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", size = 76174, upload-time = "2026-01-26T02:44:18.509Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", size = 45116, upload-time = "2026-01-26T02:44:19.745Z" },
+ { url = "https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", size = 43524, upload-time = "2026-01-26T02:44:21.571Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/3c/414842ef8d5a1628d68edee29ba0e5bcf235dbfb3ccd3ea303a7fe8c72ff/multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", size = 249368, upload-time = "2026-01-26T02:44:22.803Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", size = 256952, upload-time = "2026-01-26T02:44:24.306Z" },
+ { url = "https://files.pythonhosted.org/packages/03/d6/c878a44ba877f366630c860fdf74bfb203c33778f12b6ac274936853c451/multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", size = 240317, upload-time = "2026-01-26T02:44:25.772Z" },
+ { url = "https://files.pythonhosted.org/packages/68/49/57421b4d7ad2e9e60e25922b08ceb37e077b90444bde6ead629095327a6f/multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", size = 267132, upload-time = "2026-01-26T02:44:27.648Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/fe/ec0edd52ddbcea2a2e89e174f0206444a61440b40f39704e64dc807a70bd/multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", size = 268140, upload-time = "2026-01-26T02:44:29.588Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", size = 254277, upload-time = "2026-01-26T02:44:30.902Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/b2/5fb8c124d7561a4974c342bc8c778b471ebbeb3cc17df696f034a7e9afe7/multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", size = 252291, upload-time = "2026-01-26T02:44:32.31Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/96/51d4e4e06bcce92577fcd488e22600bd38e4fd59c20cb49434d054903bd2/multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", size = 250156, upload-time = "2026-01-26T02:44:33.734Z" },
+ { url = "https://files.pythonhosted.org/packages/db/6b/420e173eec5fba721a50e2a9f89eda89d9c98fded1124f8d5c675f7a0c0f/multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", size = 249742, upload-time = "2026-01-26T02:44:35.222Z" },
+ { url = "https://files.pythonhosted.org/packages/44/a3/ec5b5bd98f306bc2aa297b8c6f11a46714a56b1e6ef5ebda50a4f5d7c5fb/multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", size = 262221, upload-time = "2026-01-26T02:44:36.604Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/f7/e8c0d0da0cd1e28d10e624604e1a36bcc3353aaebdfdc3a43c72bc683a12/multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", size = 258664, upload-time = "2026-01-26T02:44:38.008Z" },
+ { url = "https://files.pythonhosted.org/packages/52/da/151a44e8016dd33feed44f730bd856a66257c1ee7aed4f44b649fb7edeb3/multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", size = 249490, upload-time = "2026-01-26T02:44:39.386Z" },
+ { url = "https://files.pythonhosted.org/packages/87/af/a3b86bf9630b732897f6fc3f4c4714b90aa4361983ccbdcd6c0339b21b0c/multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3", size = 41695, upload-time = "2026-01-26T02:44:41.318Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5", size = 45884, upload-time = "2026-01-26T02:44:42.488Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/61/42d3e5dbf661242a69c97ea363f2d7b46c567da8eadef8890022be6e2ab0/multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df", size = 43122, upload-time = "2026-01-26T02:44:43.664Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/b3/e6b21c6c4f314bb956016b0b3ef2162590a529b84cb831c257519e7fde44/multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", size = 83175, upload-time = "2026-01-26T02:44:44.894Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/76/23ecd2abfe0957b234f6c960f4ade497f55f2c16aeb684d4ecdbf1c95791/multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", size = 48460, upload-time = "2026-01-26T02:44:46.106Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/57/a0ed92b23f3a042c36bc4227b72b97eca803f5f1801c1ab77c8a212d455e/multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", size = 46930, upload-time = "2026-01-26T02:44:47.278Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/66/02ec7ace29162e447f6382c495dc95826bf931d3818799bbef11e8f7df1a/multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", size = 242582, upload-time = "2026-01-26T02:44:48.604Z" },
+ { url = "https://files.pythonhosted.org/packages/58/18/64f5a795e7677670e872673aca234162514696274597b3708b2c0d276cce/multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", size = 250031, upload-time = "2026-01-26T02:44:50.544Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/ed/e192291dbbe51a8290c5686f482084d31bcd9d09af24f63358c3d42fd284/multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", size = 228596, upload-time = "2026-01-26T02:44:51.951Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/7e/3562a15a60cf747397e7f2180b0a11dc0c38d9175a650e75fa1b4d325e15/multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", size = 257492, upload-time = "2026-01-26T02:44:53.902Z" },
+ { url = "https://files.pythonhosted.org/packages/24/02/7d0f9eae92b5249bb50ac1595b295f10e263dd0078ebb55115c31e0eaccd/multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", size = 255899, upload-time = "2026-01-26T02:44:55.316Z" },
+ { url = "https://files.pythonhosted.org/packages/00/e3/9b60ed9e23e64c73a5cde95269ef1330678e9c6e34dd4eb6b431b85b5a10/multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", size = 247970, upload-time = "2026-01-26T02:44:56.783Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/06/538e58a63ed5cfb0bd4517e346b91da32fde409d839720f664e9a4ae4f9d/multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", size = 245060, upload-time = "2026-01-26T02:44:58.195Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/2f/d743a3045a97c895d401e9bd29aaa09b94f5cbdf1bd561609e5a6c431c70/multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", size = 235888, upload-time = "2026-01-26T02:44:59.57Z" },
+ { url = "https://files.pythonhosted.org/packages/38/83/5a325cac191ab28b63c52f14f1131f3b0a55ba3b9aa65a6d0bf2a9b921a0/multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", size = 243554, upload-time = "2026-01-26T02:45:01.054Z" },
+ { url = "https://files.pythonhosted.org/packages/20/1f/9d2327086bd15da2725ef6aae624208e2ef828ed99892b17f60c344e57ed/multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", size = 252341, upload-time = "2026-01-26T02:45:02.484Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/2c/2a1aa0280cf579d0f6eed8ee5211c4f1730bd7e06c636ba2ee6aafda302e/multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", size = 246391, upload-time = "2026-01-26T02:45:03.862Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/03/7ca022ffc36c5a3f6e03b179a5ceb829be9da5783e6fe395f347c0794680/multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", size = 243422, upload-time = "2026-01-26T02:45:05.296Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/1d/b31650eab6c5778aceed46ba735bd97f7c7d2f54b319fa916c0f96e7805b/multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", size = 47770, upload-time = "2026-01-26T02:45:06.754Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/5b/2d2d1d522e51285bd61b1e20df8f47ae1a9d80839db0b24ea783b3832832/multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", size = 53109, upload-time = "2026-01-26T02:45:08.044Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/a3/cc409ba012c83ca024a308516703cf339bdc4b696195644a7215a5164a24/multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", size = 45573, upload-time = "2026-01-26T02:45:09.349Z" },
+ { url = "https://files.pythonhosted.org/packages/91/cc/db74228a8be41884a567e88a62fd589a913708fcf180d029898c17a9a371/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", size = 75190, upload-time = "2026-01-26T02:45:10.651Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/22/492f2246bb5b534abd44804292e81eeaf835388901f0c574bac4eeec73c5/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", size = 44486, upload-time = "2026-01-26T02:45:11.938Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", size = 43219, upload-time = "2026-01-26T02:45:14.346Z" },
+ { url = "https://files.pythonhosted.org/packages/24/bb/2c0c2287963f4259c85e8bcbba9182ced8d7fca65c780c38e99e61629d11/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", size = 245132, upload-time = "2026-01-26T02:45:15.712Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/f9/44d4b3064c65079d2467888794dea218d1601898ac50222ab8a9a8094460/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", size = 252420, upload-time = "2026-01-26T02:45:17.293Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/13/78f7275e73fa17b24c9a51b0bd9d73ba64bb32d0ed51b02a746eb876abe7/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", size = 233510, upload-time = "2026-01-26T02:45:19.356Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/25/8167187f62ae3cbd52da7893f58cb036b47ea3fb67138787c76800158982/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", size = 264094, upload-time = "2026-01-26T02:45:20.834Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/e7/69a3a83b7b030cf283fb06ce074a05a02322359783424d7edf0f15fe5022/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", size = 260786, upload-time = "2026-01-26T02:45:22.818Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", size = 248483, upload-time = "2026-01-26T02:45:24.368Z" },
+ { url = "https://files.pythonhosted.org/packages/48/5a/d5a99e3acbca0e29c5d9cba8f92ceb15dce78bab963b308ae692981e3a5d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", size = 248403, upload-time = "2026-01-26T02:45:25.982Z" },
+ { url = "https://files.pythonhosted.org/packages/35/48/e58cd31f6c7d5102f2a4bf89f96b9cf7e00b6c6f3d04ecc44417c00a5a3c/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", size = 240315, upload-time = "2026-01-26T02:45:27.487Z" },
+ { url = "https://files.pythonhosted.org/packages/94/33/1cd210229559cb90b6786c30676bb0c58249ff42f942765f88793b41fdce/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", size = 245528, upload-time = "2026-01-26T02:45:28.991Z" },
+ { url = "https://files.pythonhosted.org/packages/64/f2/6e1107d226278c876c783056b7db43d800bb64c6131cec9c8dfb6903698e/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", size = 258784, upload-time = "2026-01-26T02:45:30.503Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/c1/11f664f14d525e4a1b5327a82d4de61a1db604ab34c6603bb3c2cc63ad34/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", size = 251980, upload-time = "2026-01-26T02:45:32.603Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/9f/75a9ac888121d0c5bbd4ecf4eead45668b1766f6baabfb3b7f66a410e231/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", size = 243602, upload-time = "2026-01-26T02:45:34.043Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/e7/50bf7b004cc8525d80dbbbedfdc7aed3e4c323810890be4413e589074032/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", size = 40930, upload-time = "2026-01-26T02:45:36.278Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", size = 45074, upload-time = "2026-01-26T02:45:37.546Z" },
+ { url = "https://files.pythonhosted.org/packages/97/ab/22803b03285fa3a525f48217963da3a65ae40f6a1b6f6cf2768879e208f9/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", size = 42471, upload-time = "2026-01-26T02:45:38.889Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/6d/f9293baa6146ba9507e360ea0292b6422b016907c393e2f63fc40ab7b7b5/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", size = 82401, upload-time = "2026-01-26T02:45:40.254Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/68/53b5494738d83558d87c3c71a486504d8373421c3e0dbb6d0db48ad42ee0/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", size = 48143, upload-time = "2026-01-26T02:45:41.635Z" },
+ { url = "https://files.pythonhosted.org/packages/37/e8/5284c53310dcdc99ce5d66563f6e5773531a9b9fe9ec7a615e9bc306b05f/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", size = 46507, upload-time = "2026-01-26T02:45:42.99Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/fc/6800d0e5b3875568b4083ecf5f310dcf91d86d52573160834fb4bfcf5e4f/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", size = 239358, upload-time = "2026-01-26T02:45:44.376Z" },
+ { url = "https://files.pythonhosted.org/packages/41/75/4ad0973179361cdf3a113905e6e088173198349131be2b390f9fa4da5fc6/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", size = 246884, upload-time = "2026-01-26T02:45:47.167Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/9c/095bb28b5da139bd41fb9a5d5caff412584f377914bd8787c2aa98717130/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", size = 225878, upload-time = "2026-01-26T02:45:48.698Z" },
+ { url = "https://files.pythonhosted.org/packages/07/d0/c0a72000243756e8f5a277b6b514fa005f2c73d481b7d9e47cd4568aa2e4/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", size = 253542, upload-time = "2026-01-26T02:45:50.164Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/6b/f69da15289e384ecf2a68837ec8b5ad8c33e973aa18b266f50fe55f24b8c/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", size = 252403, upload-time = "2026-01-26T02:45:51.779Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", size = 244889, upload-time = "2026-01-26T02:45:53.27Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/a9/a50d2669e506dad33cfc45b5d574a205587b7b8a5f426f2fbb2e90882588/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", size = 241982, upload-time = "2026-01-26T02:45:54.919Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/bb/1609558ad8b456b4827d3c5a5b775c93b87878fd3117ed3db3423dfbce1b/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", size = 232415, upload-time = "2026-01-26T02:45:56.981Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/59/6f61039d2aa9261871e03ab9dc058a550d240f25859b05b67fd70f80d4b3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", size = 240337, upload-time = "2026-01-26T02:45:58.698Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/29/fdc6a43c203890dc2ae9249971ecd0c41deaedfe00d25cb6564b2edd99eb/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", size = 248788, upload-time = "2026-01-26T02:46:00.862Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/14/a153a06101323e4cf086ecee3faadba52ff71633d471f9685c42e3736163/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", size = 242842, upload-time = "2026-01-26T02:46:02.824Z" },
+ { url = "https://files.pythonhosted.org/packages/41/5f/604ae839e64a4a6efc80db94465348d3b328ee955e37acb24badbcd24d83/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", size = 240237, upload-time = "2026-01-26T02:46:05.898Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/60/c3a5187bf66f6fb546ff4ab8fb5a077cbdd832d7b1908d4365c7f74a1917/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", size = 48008, upload-time = "2026-01-26T02:46:07.468Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", size = 53542, upload-time = "2026-01-26T02:46:08.809Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", size = 44719, upload-time = "2026-01-26T02:46:11.146Z" },
+ { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" },
+]
+
+[[package]]
+name = "mypy-extensions"
+version = "1.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
+]
+
+[[package]]
+name = "networkx"
+version = "3.6.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" },
+]
+
+[[package]]
+name = "numpy"
+version = "2.4.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/57/fd/0005efbd0af48e55eb3c7208af93f2862d4b1a56cd78e84309a2d959208d/numpy-2.4.2.tar.gz", hash = "sha256:659a6107e31a83c4e33f763942275fd278b21d095094044eb35569e86a21ddae", size = 20723651, upload-time = "2026-01-31T23:13:10.135Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d3/44/71852273146957899753e69986246d6a176061ea183407e95418c2aa4d9a/numpy-2.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7e88598032542bd49af7c4747541422884219056c268823ef6e5e89851c8825", size = 16955478, upload-time = "2026-01-31T23:10:25.623Z" },
+ { url = "https://files.pythonhosted.org/packages/74/41/5d17d4058bd0cd96bcbd4d9ff0fb2e21f52702aab9a72e4a594efa18692f/numpy-2.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7edc794af8b36ca37ef5fcb5e0d128c7e0595c7b96a2318d1badb6fcd8ee86b1", size = 14965467, upload-time = "2026-01-31T23:10:28.186Z" },
+ { url = "https://files.pythonhosted.org/packages/49/48/fb1ce8136c19452ed15f033f8aee91d5defe515094e330ce368a0647846f/numpy-2.4.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:6e9f61981ace1360e42737e2bae58b27bf28a1b27e781721047d84bd754d32e7", size = 5475172, upload-time = "2026-01-31T23:10:30.848Z" },
+ { url = "https://files.pythonhosted.org/packages/40/a9/3feb49f17bbd1300dd2570432961f5c8a4ffeff1db6f02c7273bd020a4c9/numpy-2.4.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:cb7bbb88aa74908950d979eeaa24dbdf1a865e3c7e45ff0121d8f70387b55f73", size = 6805145, upload-time = "2026-01-31T23:10:32.352Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/39/fdf35cbd6d6e2fcad42fcf85ac04a85a0d0fbfbf34b30721c98d602fd70a/numpy-2.4.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f069069931240b3fc703f1e23df63443dbd6390614c8c44a87d96cd0ec81eb1", size = 15966084, upload-time = "2026-01-31T23:10:34.502Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/46/6fa4ea94f1ddf969b2ee941290cca6f1bfac92b53c76ae5f44afe17ceb69/numpy-2.4.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c02ef4401a506fb60b411467ad501e1429a3487abca4664871d9ae0b46c8ba32", size = 16899477, upload-time = "2026-01-31T23:10:37.075Z" },
+ { url = "https://files.pythonhosted.org/packages/09/a1/2a424e162b1a14a5bd860a464ab4e07513916a64ab1683fae262f735ccd2/numpy-2.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2653de5c24910e49c2b106499803124dde62a5a1fe0eedeaecf4309a5f639390", size = 17323429, upload-time = "2026-01-31T23:10:39.704Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/a2/73014149ff250628df72c58204822ac01d768697913881aacf839ff78680/numpy-2.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1ae241bbfc6ae276f94a170b14785e561cb5e7f626b6688cf076af4110887413", size = 18635109, upload-time = "2026-01-31T23:10:41.924Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/0c/73e8be2f1accd56df74abc1c5e18527822067dced5ec0861b5bb882c2ce0/numpy-2.4.2-cp311-cp311-win32.whl", hash = "sha256:df1b10187212b198dd45fa943d8985a3c8cf854aed4923796e0e019e113a1bda", size = 6237915, upload-time = "2026-01-31T23:10:45.26Z" },
+ { url = "https://files.pythonhosted.org/packages/76/ae/e0265e0163cf127c24c3969d29f1c4c64551a1e375d95a13d32eab25d364/numpy-2.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:b9c618d56a29c9cb1c4da979e9899be7578d2e0b3c24d52079c166324c9e8695", size = 12607972, upload-time = "2026-01-31T23:10:47.021Z" },
+ { url = "https://files.pythonhosted.org/packages/29/a5/c43029af9b8014d6ea157f192652c50042e8911f4300f8f6ed3336bf437f/numpy-2.4.2-cp311-cp311-win_arm64.whl", hash = "sha256:47c5a6ed21d9452b10227e5e8a0e1c22979811cad7dcc19d8e3e2fb8fa03f1a3", size = 10485763, upload-time = "2026-01-31T23:10:50.087Z" },
+ { url = "https://files.pythonhosted.org/packages/51/6e/6f394c9c77668153e14d4da83bcc247beb5952f6ead7699a1a2992613bea/numpy-2.4.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:21982668592194c609de53ba4933a7471880ccbaadcc52352694a59ecc860b3a", size = 16667963, upload-time = "2026-01-31T23:10:52.147Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/f8/55483431f2b2fd015ae6ed4fe62288823ce908437ed49db5a03d15151678/numpy-2.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40397bda92382fcec844066efb11f13e1c9a3e2a8e8f318fb72ed8b6db9f60f1", size = 14693571, upload-time = "2026-01-31T23:10:54.789Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/20/18026832b1845cdc82248208dd929ca14c9d8f2bac391f67440707fff27c/numpy-2.4.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:b3a24467af63c67829bfaa61eecf18d5432d4f11992688537be59ecd6ad32f5e", size = 5203469, upload-time = "2026-01-31T23:10:57.343Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/33/2eb97c8a77daaba34eaa3fa7241a14ac5f51c46a6bd5911361b644c4a1e2/numpy-2.4.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:805cc8de9fd6e7a22da5aed858e0ab16be5a4db6c873dde1d7451c541553aa27", size = 6550820, upload-time = "2026-01-31T23:10:59.429Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/91/b97fdfd12dc75b02c44e26c6638241cc004d4079a0321a69c62f51470c4c/numpy-2.4.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d82351358ffbcdcd7b686b90742a9b86632d6c1c051016484fa0b326a0a1548", size = 15663067, upload-time = "2026-01-31T23:11:01.291Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/c6/a18e59f3f0b8071cc85cbc8d80cd02d68aa9710170b2553a117203d46936/numpy-2.4.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e35d3e0144137d9fdae62912e869136164534d64a169f86438bc9561b6ad49f", size = 16619782, upload-time = "2026-01-31T23:11:03.669Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/83/9751502164601a79e18847309f5ceec0b1446d7b6aa12305759b72cf98b2/numpy-2.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adb6ed2ad29b9e15321d167d152ee909ec73395901b70936f029c3bc6d7f4460", size = 17013128, upload-time = "2026-01-31T23:11:05.913Z" },
+ { url = "https://files.pythonhosted.org/packages/61/c4/c4066322256ec740acc1c8923a10047818691d2f8aec254798f3dd90f5f2/numpy-2.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8906e71fd8afcb76580404e2a950caef2685df3d2a57fe82a86ac8d33cc007ba", size = 18345324, upload-time = "2026-01-31T23:11:08.248Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/af/6157aa6da728fa4525a755bfad486ae7e3f76d4c1864138003eb84328497/numpy-2.4.2-cp312-cp312-win32.whl", hash = "sha256:ec055f6dae239a6299cace477b479cca2fc125c5675482daf1dd886933a1076f", size = 5960282, upload-time = "2026-01-31T23:11:10.497Z" },
+ { url = "https://files.pythonhosted.org/packages/92/0f/7ceaaeaacb40567071e94dbf2c9480c0ae453d5bb4f52bea3892c39dc83c/numpy-2.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:209fae046e62d0ce6435fcfe3b1a10537e858249b3d9b05829e2a05218296a85", size = 12314210, upload-time = "2026-01-31T23:11:12.176Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/a3/56c5c604fae6dd40fa2ed3040d005fca97e91bd320d232ac9931d77ba13c/numpy-2.4.2-cp312-cp312-win_arm64.whl", hash = "sha256:fbde1b0c6e81d56f5dccd95dd4a711d9b95df1ae4009a60887e56b27e8d903fa", size = 10220171, upload-time = "2026-01-31T23:11:14.684Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/22/815b9fe25d1d7ae7d492152adbc7226d3eff731dffc38fe970589fcaaa38/numpy-2.4.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:25f2059807faea4b077a2b6837391b5d830864b3543627f381821c646f31a63c", size = 16663696, upload-time = "2026-01-31T23:11:17.516Z" },
+ { url = "https://files.pythonhosted.org/packages/09/f0/817d03a03f93ba9c6c8993de509277d84e69f9453601915e4a69554102a1/numpy-2.4.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bd3a7a9f5847d2fb8c2c6d1c862fa109c31a9abeca1a3c2bd5a64572955b2979", size = 14688322, upload-time = "2026-01-31T23:11:19.883Z" },
+ { url = "https://files.pythonhosted.org/packages/da/b4/f805ab79293c728b9a99438775ce51885fd4f31b76178767cfc718701a39/numpy-2.4.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8e4549f8a3c6d13d55041925e912bfd834285ef1dd64d6bc7d542583355e2e98", size = 5198157, upload-time = "2026-01-31T23:11:22.375Z" },
+ { url = "https://files.pythonhosted.org/packages/74/09/826e4289844eccdcd64aac27d13b0fd3f32039915dd5b9ba01baae1f436c/numpy-2.4.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:aea4f66ff44dfddf8c2cffd66ba6538c5ec67d389285292fe428cb2c738c8aef", size = 6546330, upload-time = "2026-01-31T23:11:23.958Z" },
+ { url = "https://files.pythonhosted.org/packages/19/fb/cbfdbfa3057a10aea5422c558ac57538e6acc87ec1669e666d32ac198da7/numpy-2.4.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3cd545784805de05aafe1dde61752ea49a359ccba9760c1e5d1c88a93bbf2b7", size = 15660968, upload-time = "2026-01-31T23:11:25.713Z" },
+ { url = "https://files.pythonhosted.org/packages/04/dc/46066ce18d01645541f0186877377b9371b8fa8017fa8262002b4ef22612/numpy-2.4.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0d9b7c93578baafcbc5f0b83eaf17b79d345c6f36917ba0c67f45226911d499", size = 16607311, upload-time = "2026-01-31T23:11:28.117Z" },
+ { url = "https://files.pythonhosted.org/packages/14/d9/4b5adfc39a43fa6bf918c6d544bc60c05236cc2f6339847fc5b35e6cb5b0/numpy-2.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f74f0f7779cc7ae07d1810aab8ac6b1464c3eafb9e283a40da7309d5e6e48fbb", size = 17012850, upload-time = "2026-01-31T23:11:30.888Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/20/adb6e6adde6d0130046e6fdfb7675cc62bc2f6b7b02239a09eb58435753d/numpy-2.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c7ac672d699bf36275c035e16b65539931347d68b70667d28984c9fb34e07fa7", size = 18334210, upload-time = "2026-01-31T23:11:33.214Z" },
+ { url = "https://files.pythonhosted.org/packages/78/0e/0a73b3dff26803a8c02baa76398015ea2a5434d9b8265a7898a6028c1591/numpy-2.4.2-cp313-cp313-win32.whl", hash = "sha256:8e9afaeb0beff068b4d9cd20d322ba0ee1cecfb0b08db145e4ab4dd44a6b5110", size = 5958199, upload-time = "2026-01-31T23:11:35.385Z" },
+ { url = "https://files.pythonhosted.org/packages/43/bc/6352f343522fcb2c04dbaf94cb30cca6fd32c1a750c06ad6231b4293708c/numpy-2.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:7df2de1e4fba69a51c06c28f5a3de36731eb9639feb8e1cf7e4a7b0daf4cf622", size = 12310848, upload-time = "2026-01-31T23:11:38.001Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/8d/6da186483e308da5da1cc6918ce913dcfe14ffde98e710bfeff2a6158d4e/numpy-2.4.2-cp313-cp313-win_arm64.whl", hash = "sha256:0fece1d1f0a89c16b03442eae5c56dc0be0c7883b5d388e0c03f53019a4bfd71", size = 10221082, upload-time = "2026-01-31T23:11:40.392Z" },
+ { url = "https://files.pythonhosted.org/packages/25/a1/9510aa43555b44781968935c7548a8926274f815de42ad3997e9e83680dd/numpy-2.4.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5633c0da313330fd20c484c78cdd3f9b175b55e1a766c4a174230c6b70ad8262", size = 14815866, upload-time = "2026-01-31T23:11:42.495Z" },
+ { url = "https://files.pythonhosted.org/packages/36/30/6bbb5e76631a5ae46e7923dd16ca9d3f1c93cfa8d4ed79a129814a9d8db3/numpy-2.4.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d9f64d786b3b1dd742c946c42d15b07497ed14af1a1f3ce840cce27daa0ce913", size = 5325631, upload-time = "2026-01-31T23:11:44.7Z" },
+ { url = "https://files.pythonhosted.org/packages/46/00/3a490938800c1923b567b3a15cd17896e68052e2145d8662aaf3e1ffc58f/numpy-2.4.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:b21041e8cb6a1eb5312dd1d2f80a94d91efffb7a06b70597d44f1bd2dfc315ab", size = 6646254, upload-time = "2026-01-31T23:11:46.341Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/e9/fac0890149898a9b609caa5af7455a948b544746e4b8fe7c212c8edd71f8/numpy-2.4.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:00ab83c56211a1d7c07c25e3217ea6695e50a3e2f255053686b081dc0b091a82", size = 15720138, upload-time = "2026-01-31T23:11:48.082Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/5c/08887c54e68e1e28df53709f1893ce92932cc6f01f7c3d4dc952f61ffd4e/numpy-2.4.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fb882da679409066b4603579619341c6d6898fc83a8995199d5249f986e8e8f", size = 16655398, upload-time = "2026-01-31T23:11:50.293Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/89/253db0fa0e66e9129c745e4ef25631dc37d5f1314dad2b53e907b8538e6d/numpy-2.4.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:66cb9422236317f9d44b67b4d18f44efe6e9c7f8794ac0462978513359461554", size = 17079064, upload-time = "2026-01-31T23:11:52.927Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/d5/cbade46ce97c59c6c3da525e8d95b7abe8a42974a1dc5c1d489c10433e88/numpy-2.4.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0f01dcf33e73d80bd8dc0f20a71303abbafa26a19e23f6b68d1aa9990af90257", size = 18379680, upload-time = "2026-01-31T23:11:55.22Z" },
+ { url = "https://files.pythonhosted.org/packages/40/62/48f99ae172a4b63d981babe683685030e8a3df4f246c893ea5c6ef99f018/numpy-2.4.2-cp313-cp313t-win32.whl", hash = "sha256:52b913ec40ff7ae845687b0b34d8d93b60cb66dcee06996dd5c99f2fc9328657", size = 6082433, upload-time = "2026-01-31T23:11:58.096Z" },
+ { url = "https://files.pythonhosted.org/packages/07/38/e054a61cfe48ad9f1ed0d188e78b7e26859d0b60ef21cd9de4897cdb5326/numpy-2.4.2-cp313-cp313t-win_amd64.whl", hash = "sha256:5eea80d908b2c1f91486eb95b3fb6fab187e569ec9752ab7d9333d2e66bf2d6b", size = 12451181, upload-time = "2026-01-31T23:11:59.782Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/a4/a05c3a6418575e185dd84d0b9680b6bb2e2dc3e4202f036b7b4e22d6e9dc/numpy-2.4.2-cp313-cp313t-win_arm64.whl", hash = "sha256:fd49860271d52127d61197bb50b64f58454e9f578cb4b2c001a6de8b1f50b0b1", size = 10290756, upload-time = "2026-01-31T23:12:02.438Z" },
+ { url = "https://files.pythonhosted.org/packages/18/88/b7df6050bf18fdcfb7046286c6535cabbdd2064a3440fca3f069d319c16e/numpy-2.4.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:444be170853f1f9d528428eceb55f12918e4fda5d8805480f36a002f1415e09b", size = 16663092, upload-time = "2026-01-31T23:12:04.521Z" },
+ { url = "https://files.pythonhosted.org/packages/25/7a/1fee4329abc705a469a4afe6e69b1ef7e915117747886327104a8493a955/numpy-2.4.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d1240d50adff70c2a88217698ca844723068533f3f5c5fa6ee2e3220e3bdb000", size = 14698770, upload-time = "2026-01-31T23:12:06.96Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/0b/f9e49ba6c923678ad5bc38181c08ac5e53b7a5754dbca8e581aa1a56b1ff/numpy-2.4.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:7cdde6de52fb6664b00b056341265441192d1291c130e99183ec0d4b110ff8b1", size = 5208562, upload-time = "2026-01-31T23:12:09.632Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/12/d7de8f6f53f9bb76997e5e4c069eda2051e3fe134e9181671c4391677bb2/numpy-2.4.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:cda077c2e5b780200b6b3e09d0b42205a3d1c68f30c6dceb90401c13bff8fe74", size = 6543710, upload-time = "2026-01-31T23:12:11.969Z" },
+ { url = "https://files.pythonhosted.org/packages/09/63/c66418c2e0268a31a4cf8a8b512685748200f8e8e8ec6c507ce14e773529/numpy-2.4.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d30291931c915b2ab5717c2974bb95ee891a1cf22ebc16a8006bd59cd210d40a", size = 15677205, upload-time = "2026-01-31T23:12:14.33Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/6c/7f237821c9642fb2a04d2f1e88b4295677144ca93285fd76eff3bcba858d/numpy-2.4.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bba37bc29d4d85761deed3954a1bc62be7cf462b9510b51d367b769a8c8df325", size = 16611738, upload-time = "2026-01-31T23:12:16.525Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/a7/39c4cdda9f019b609b5c473899d87abff092fc908cfe4d1ecb2fcff453b0/numpy-2.4.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b2f0073ed0868db1dcd86e052d37279eef185b9c8db5bf61f30f46adac63c909", size = 17028888, upload-time = "2026-01-31T23:12:19.306Z" },
+ { url = "https://files.pythonhosted.org/packages/da/b3/e84bb64bdfea967cc10950d71090ec2d84b49bc691df0025dddb7c26e8e3/numpy-2.4.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7f54844851cdb630ceb623dcec4db3240d1ac13d4990532446761baede94996a", size = 18339556, upload-time = "2026-01-31T23:12:21.816Z" },
+ { url = "https://files.pythonhosted.org/packages/88/f5/954a291bc1192a27081706862ac62bb5920fbecfbaa302f64682aa90beed/numpy-2.4.2-cp314-cp314-win32.whl", hash = "sha256:12e26134a0331d8dbd9351620f037ec470b7c75929cb8a1537f6bfe411152a1a", size = 6006899, upload-time = "2026-01-31T23:12:24.14Z" },
+ { url = "https://files.pythonhosted.org/packages/05/cb/eff72a91b2efdd1bc98b3b8759f6a1654aa87612fc86e3d87d6fe4f948c4/numpy-2.4.2-cp314-cp314-win_amd64.whl", hash = "sha256:068cdb2d0d644cdb45670810894f6a0600797a69c05f1ac478e8d31670b8ee75", size = 12443072, upload-time = "2026-01-31T23:12:26.33Z" },
+ { url = "https://files.pythonhosted.org/packages/37/75/62726948db36a56428fce4ba80a115716dc4fad6a3a4352487f8bb950966/numpy-2.4.2-cp314-cp314-win_arm64.whl", hash = "sha256:6ed0be1ee58eef41231a5c943d7d1375f093142702d5723ca2eb07db9b934b05", size = 10494886, upload-time = "2026-01-31T23:12:28.488Z" },
+ { url = "https://files.pythonhosted.org/packages/36/2f/ee93744f1e0661dc267e4b21940870cabfae187c092e1433b77b09b50ac4/numpy-2.4.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:98f16a80e917003a12c0580f97b5f875853ebc33e2eaa4bccfc8201ac6869308", size = 14818567, upload-time = "2026-01-31T23:12:30.709Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/24/6535212add7d76ff938d8bdc654f53f88d35cddedf807a599e180dcb8e66/numpy-2.4.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:20abd069b9cda45874498b245c8015b18ace6de8546bf50dfa8cea1696ed06ef", size = 5328372, upload-time = "2026-01-31T23:12:32.962Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/9d/c48f0a035725f925634bf6b8994253b43f2047f6778a54147d7e213bc5a7/numpy-2.4.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:e98c97502435b53741540a5717a6749ac2ada901056c7db951d33e11c885cc7d", size = 6649306, upload-time = "2026-01-31T23:12:34.797Z" },
+ { url = "https://files.pythonhosted.org/packages/81/05/7c73a9574cd4a53a25907bad38b59ac83919c0ddc8234ec157f344d57d9a/numpy-2.4.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da6cad4e82cb893db4b69105c604d805e0c3ce11501a55b5e9f9083b47d2ffe8", size = 15722394, upload-time = "2026-01-31T23:12:36.565Z" },
+ { url = "https://files.pythonhosted.org/packages/35/fa/4de10089f21fc7d18442c4a767ab156b25c2a6eaf187c0db6d9ecdaeb43f/numpy-2.4.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e4424677ce4b47fe73c8b5556d876571f7c6945d264201180db2dc34f676ab5", size = 16653343, upload-time = "2026-01-31T23:12:39.188Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/f9/d33e4ffc857f3763a57aa85650f2e82486832d7492280ac21ba9efda80da/numpy-2.4.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2b8f157c8a6f20eb657e240f8985cc135598b2b46985c5bccbde7616dc9c6b1e", size = 17078045, upload-time = "2026-01-31T23:12:42.041Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/b8/54bdb43b6225badbea6389fa038c4ef868c44f5890f95dd530a218706da3/numpy-2.4.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5daf6f3914a733336dab21a05cdec343144600e964d2fcdabaac0c0269874b2a", size = 18380024, upload-time = "2026-01-31T23:12:44.331Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/55/6e1a61ded7af8df04016d81b5b02daa59f2ea9252ee0397cb9f631efe9e5/numpy-2.4.2-cp314-cp314t-win32.whl", hash = "sha256:8c50dd1fc8826f5b26a5ee4d77ca55d88a895f4e4819c7ecc2a9f5905047a443", size = 6153937, upload-time = "2026-01-31T23:12:47.229Z" },
+ { url = "https://files.pythonhosted.org/packages/45/aa/fa6118d1ed6d776b0983f3ceac9b1a5558e80df9365b1c3aa6d42bf9eee4/numpy-2.4.2-cp314-cp314t-win_amd64.whl", hash = "sha256:fcf92bee92742edd401ba41135185866f7026c502617f422eb432cfeca4fe236", size = 12631844, upload-time = "2026-01-31T23:12:48.997Z" },
+ { url = "https://files.pythonhosted.org/packages/32/0a/2ec5deea6dcd158f254a7b372fb09cfba5719419c8d66343bab35237b3fb/numpy-2.4.2-cp314-cp314t-win_arm64.whl", hash = "sha256:1f92f53998a17265194018d1cc321b2e96e900ca52d54c7c77837b71b9465181", size = 10565379, upload-time = "2026-01-31T23:12:51.345Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/f8/50e14d36d915ef64d8f8bc4a087fc8264d82c785eda6711f80ab7e620335/numpy-2.4.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:89f7268c009bc492f506abd6f5265defa7cb3f7487dc21d357c3d290add45082", size = 16833179, upload-time = "2026-01-31T23:12:53.5Z" },
+ { url = "https://files.pythonhosted.org/packages/17/17/809b5cad63812058a8189e91a1e2d55a5a18fd04611dbad244e8aeae465c/numpy-2.4.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6dee3bb76aa4009d5a912180bf5b2de012532998d094acee25d9cb8dee3e44a", size = 14889755, upload-time = "2026-01-31T23:12:55.933Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/ea/181b9bcf7627fc8371720316c24db888dcb9829b1c0270abf3d288b2e29b/numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:cd2bd2bbed13e213d6b55dc1d035a4f91748a7d3edc9480c13898b0353708920", size = 5399500, upload-time = "2026-01-31T23:12:58.671Z" },
+ { url = "https://files.pythonhosted.org/packages/33/9f/413adf3fc955541ff5536b78fcf0754680b3c6d95103230252a2c9408d23/numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:cf28c0c1d4c4bf00f509fa7eb02c58d7caf221b50b467bcb0d9bbf1584d5c821", size = 6714252, upload-time = "2026-01-31T23:13:00.518Z" },
+ { url = "https://files.pythonhosted.org/packages/91/da/643aad274e29ccbdf42ecd94dafe524b81c87bcb56b83872d54827f10543/numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e04ae107ac591763a47398bb45b568fc38f02dbc4aa44c063f67a131f99346cb", size = 15797142, upload-time = "2026-01-31T23:13:02.219Z" },
+ { url = "https://files.pythonhosted.org/packages/66/27/965b8525e9cb5dc16481b30a1b3c21e50c7ebf6e9dbd48d0c4d0d5089c7e/numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:602f65afdef699cda27ec0b9224ae5dc43e328f4c24c689deaf77133dbee74d0", size = 16727979, upload-time = "2026-01-31T23:13:04.62Z" },
+ { url = "https://files.pythonhosted.org/packages/de/e5/b7d20451657664b07986c2f6e3be564433f5dcaf3482d68eaecd79afaf03/numpy-2.4.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be71bf1edb48ebbbf7f6337b5bfd2f895d1902f6335a5830b20141fc126ffba0", size = 12502577, upload-time = "2026-01-31T23:13:07.08Z" },
+]
+
+[[package]]
+name = "onnxruntime"
+version = "1.24.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "flatbuffers" },
+ { name = "numpy" },
+ { name = "packaging" },
+ { name = "protobuf" },
+ { name = "sympy" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/15/41/3253db975a90c3ce1d475e2a230773a21cd7998537f0657947df6fb79861/onnxruntime-1.24.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3e6456801c66b095c5cd68e690ca25db970ea5202bd0c5b84a2c3ef7731c5a3c", size = 17332766, upload-time = "2026-03-05T17:18:59.714Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/c5/3af6b325f1492d691b23844d88ed26844c1164620860c5efe95c0e22782d/onnxruntime-1.24.3-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b2ebc54c6d8281dccff78d4b06e47d4cf07535937584ab759448390a70f4978", size = 15130330, upload-time = "2026-03-05T16:34:53.831Z" },
+ { url = "https://files.pythonhosted.org/packages/03/4b/f96b46c1866a293ed23ca2cf5e5a63d413ad3a951da60dd877e3c56cbbca/onnxruntime-1.24.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb56575d7794bf0781156955610c9e651c9504c64d42ec880784b6106244882d", size = 17213247, upload-time = "2026-03-05T17:17:59.812Z" },
+ { url = "https://files.pythonhosted.org/packages/36/13/27cf4d8df2578747584e8758aeb0b673b60274048510257f1f084b15e80e/onnxruntime-1.24.3-cp311-cp311-win_amd64.whl", hash = "sha256:c958222ef9eff54018332beecd32d5d94a3ab079d8821937b333811bf4da0d39", size = 12595530, upload-time = "2026-03-05T17:18:49.356Z" },
+ { url = "https://files.pythonhosted.org/packages/19/8c/6d9f31e6bae72a8079be12ed8ba36c4126a571fad38ded0a1b96f60f6896/onnxruntime-1.24.3-cp311-cp311-win_arm64.whl", hash = "sha256:a8f761857ebaf58a85b9e42422d03207f1d39e6bb8fecfdbf613bac5b9710723", size = 12261715, upload-time = "2026-03-05T17:18:39.699Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/7f/dfdc4e52600fde4c02d59bfe98c4b057931c1114b701e175aee311a9bc11/onnxruntime-1.24.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:0d244227dc5e00a9ae15a7ac1eba4c4460d7876dfecafe73fb00db9f1d914d91", size = 17342578, upload-time = "2026-03-05T17:19:02.403Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/dc/1f5489f7b21817d4ad352bf7a92a252bd5b438bcbaa7ad20ea50814edc79/onnxruntime-1.24.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a9847b870b6cb462652b547bc98c49e0efb67553410a082fde1918a38707452", size = 15150105, upload-time = "2026-03-05T16:34:56.897Z" },
+ { url = "https://files.pythonhosted.org/packages/28/7c/fd253da53594ab8efbefdc85b3638620ab1a6aab6eb7028a513c853559ce/onnxruntime-1.24.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b354afce3333f2859c7e8706d84b6c552beac39233bcd3141ce7ab77b4cabb5d", size = 17237101, upload-time = "2026-03-05T17:18:02.561Z" },
+ { url = "https://files.pythonhosted.org/packages/71/5f/eaabc5699eeed6a9188c5c055ac1948ae50138697a0428d562ac970d7db5/onnxruntime-1.24.3-cp312-cp312-win_amd64.whl", hash = "sha256:44ea708c34965439170d811267c51281d3897ecfc4aa0087fa25d4a4c3eb2e4a", size = 12597638, upload-time = "2026-03-05T17:18:52.141Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/5c/d8066c320b90610dbeb489a483b132c3b3879b2f93f949fb5d30cfa9b119/onnxruntime-1.24.3-cp312-cp312-win_arm64.whl", hash = "sha256:48d1092b44ca2ba6f9543892e7c422c15a568481403c10440945685faf27a8d8", size = 12270943, upload-time = "2026-03-05T17:18:42.006Z" },
+ { url = "https://files.pythonhosted.org/packages/51/8d/487ece554119e2991242d4de55de7019ac6e47ee8dfafa69fcf41d37f8ed/onnxruntime-1.24.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:34a0ea5ff191d8420d9c1332355644148b1bf1a0d10c411af890a63a9f662aa7", size = 17342706, upload-time = "2026-03-05T16:35:10.813Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/25/8b444f463c1ac6106b889f6235c84f01eec001eaf689c3eff8c69cf48fae/onnxruntime-1.24.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fd2ec7bb0fabe42f55e8337cfc9b1969d0d14622711aac73d69b4bd5abb5ed7", size = 15149956, upload-time = "2026-03-05T16:34:59.264Z" },
+ { url = "https://files.pythonhosted.org/packages/34/fc/c9182a3e1ab46940dd4f30e61071f59eee8804c1f641f37ce6e173633fb6/onnxruntime-1.24.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df8e70e732fe26346faaeec9147fa38bef35d232d2495d27e93dd221a2d473a9", size = 17237370, upload-time = "2026-03-05T17:18:05.258Z" },
+ { url = "https://files.pythonhosted.org/packages/05/7e/3b549e1f4538514118bff98a1bcd6481dd9a17067f8c9af77151621c9a5c/onnxruntime-1.24.3-cp313-cp313-win_amd64.whl", hash = "sha256:2d3706719be6ad41d38a2250998b1d87758a20f6ea4546962e21dc79f1f1fd2b", size = 12597939, upload-time = "2026-03-05T17:18:54.772Z" },
+ { url = "https://files.pythonhosted.org/packages/80/41/9696a5c4631a0caa75cc8bc4efd30938fd483694aa614898d087c3ee6d29/onnxruntime-1.24.3-cp313-cp313-win_arm64.whl", hash = "sha256:b082f3ba9519f0a1a1e754556bc7e635c7526ef81b98b3f78da4455d25f0437b", size = 12270705, upload-time = "2026-03-05T17:18:44.774Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/65/a26c5e59e3b210852ee04248cf8843c81fe7d40d94cf95343b66efe7eec9/onnxruntime-1.24.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72f956634bc2e4bd2e8b006bef111849bd42c42dea37bd0a4c728404fdaf4d34", size = 15161796, upload-time = "2026-03-05T16:35:02.871Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/25/2035b4aa2ccb5be6acf139397731ec507c5f09e199ab39d3262b22ffa1ac/onnxruntime-1.24.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78d1f25eed4ab9959db70a626ed50ee24cf497e60774f59f1207ac8556399c4d", size = 17240936, upload-time = "2026-03-05T17:18:09.534Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/a4/b3240ea84b92a3efb83d49cc16c04a17ade1ab47a6a95c4866d15bf0ac35/onnxruntime-1.24.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:a6b4bce87d96f78f0a9bf5cefab3303ae95d558c5bfea53d0bf7f9ea207880a8", size = 17344149, upload-time = "2026-03-05T16:35:13.382Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/4a/4b56757e51a56265e8c56764d9c36d7b435045e05e3b8a38bedfc5aedba3/onnxruntime-1.24.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d48f36c87b25ab3b2b4c88826c96cf1399a5631e3c2c03cc27d6a1e5d6b18eb4", size = 15151571, upload-time = "2026-03-05T16:35:05.679Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/14/c6fb84980cec8f682a523fcac7c2bdd6b311e7f342c61ce48d3a9cb87fc6/onnxruntime-1.24.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e104d33a409bf6e3f30f0e8198ec2aaf8d445b8395490a80f6e6ad56da98e400", size = 17238951, upload-time = "2026-03-05T17:18:12.394Z" },
+ { url = "https://files.pythonhosted.org/packages/57/14/447e1400165aca8caf35dabd46540eb943c92f3065927bb4d9bcbc91e221/onnxruntime-1.24.3-cp314-cp314-win_amd64.whl", hash = "sha256:e785d73fbd17421c2513b0bb09eb25d88fa22c8c10c3f5d6060589efa5537c5b", size = 12903820, upload-time = "2026-03-05T17:18:57.123Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/ec/6b2fa5702e4bbba7339ca5787a9d056fc564a16079f8833cc6ba4798da1c/onnxruntime-1.24.3-cp314-cp314-win_arm64.whl", hash = "sha256:951e897a275f897a05ffbcaa615d98777882decaeb80c9216c68cdc62f849f53", size = 12594089, upload-time = "2026-03-05T17:18:47.169Z" },
+ { url = "https://files.pythonhosted.org/packages/12/dc/cd06cba3ddad92ceb17b914a8e8d49836c79e38936e26bde6e368b62c1fe/onnxruntime-1.24.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d4e70ce578aa214c74c7a7a9226bc8e229814db4a5b2d097333b81279ecde36", size = 15162789, upload-time = "2026-03-05T16:35:08.282Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/d6/413e98ab666c6fb9e8be7d1c6eb3bd403b0bea1b8d42db066dab98c7df07/onnxruntime-1.24.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02aaf6ddfa784523b6873b4176a79d508e599efe12ab0ea1a3a6e7314408b7aa", size = 17240738, upload-time = "2026-03-05T17:18:15.203Z" },
+]
+
+[[package]]
+name = "packaging"
+version = "26.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
+]
+
+[[package]]
+name = "pathspec"
+version = "1.0.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/4c/b2/bb8e495d5262bfec41ab5cb18f522f1012933347fb5d9e62452d446baca2/pathspec-1.0.3.tar.gz", hash = "sha256:bac5cf97ae2c2876e2d25ebb15078eb04d76e4b98921ee31c6f85ade8b59444d", size = 130841, upload-time = "2026-01-09T15:46:46.009Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/32/2b/121e912bd60eebd623f873fd090de0e84f322972ab25a7f9044c056804ed/pathspec-1.0.3-py3-none-any.whl", hash = "sha256:e80767021c1cc524aa3fb14bedda9c34406591343cc42797b386ce7b9354fb6c", size = 55021, upload-time = "2026-01-09T15:46:44.652Z" },
+]
+
+[[package]]
+name = "platformdirs"
+version = "4.5.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" },
+]
+
+[[package]]
+name = "pluggy"
+version = "1.6.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
+]
+
+[[package]]
+name = "propcache"
+version = "0.4.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8c/d4/4e2c9aaf7ac2242b9358f98dccd8f90f2605402f5afeff6c578682c2c491/propcache-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf", size = 80208, upload-time = "2025-10-08T19:46:24.597Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/21/d7b68e911f9c8e18e4ae43bdbc1e1e9bbd971f8866eb81608947b6f585ff/propcache-0.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5", size = 45777, upload-time = "2025-10-08T19:46:25.733Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/1d/11605e99ac8ea9435651ee71ab4cb4bf03f0949586246476a25aadfec54a/propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e", size = 47647, upload-time = "2025-10-08T19:46:27.304Z" },
+ { url = "https://files.pythonhosted.org/packages/58/1a/3c62c127a8466c9c843bccb503d40a273e5cc69838805f322e2826509e0d/propcache-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566", size = 214929, upload-time = "2025-10-08T19:46:28.62Z" },
+ { url = "https://files.pythonhosted.org/packages/56/b9/8fa98f850960b367c4b8fe0592e7fc341daa7a9462e925228f10a60cf74f/propcache-0.4.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165", size = 221778, upload-time = "2025-10-08T19:46:30.358Z" },
+ { url = "https://files.pythonhosted.org/packages/46/a6/0ab4f660eb59649d14b3d3d65c439421cf2f87fe5dd68591cbe3c1e78a89/propcache-0.4.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc", size = 228144, upload-time = "2025-10-08T19:46:32.607Z" },
+ { url = "https://files.pythonhosted.org/packages/52/6a/57f43e054fb3d3a56ac9fc532bc684fc6169a26c75c353e65425b3e56eef/propcache-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48", size = 210030, upload-time = "2025-10-08T19:46:33.969Z" },
+ { url = "https://files.pythonhosted.org/packages/40/e2/27e6feebb5f6b8408fa29f5efbb765cd54c153ac77314d27e457a3e993b7/propcache-0.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570", size = 208252, upload-time = "2025-10-08T19:46:35.309Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/f8/91c27b22ccda1dbc7967f921c42825564fa5336a01ecd72eb78a9f4f53c2/propcache-0.4.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85", size = 202064, upload-time = "2025-10-08T19:46:36.993Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/26/7f00bd6bd1adba5aafe5f4a66390f243acab58eab24ff1a08bebb2ef9d40/propcache-0.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e", size = 212429, upload-time = "2025-10-08T19:46:38.398Z" },
+ { url = "https://files.pythonhosted.org/packages/84/89/fd108ba7815c1117ddca79c228f3f8a15fc82a73bca8b142eb5de13b2785/propcache-0.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757", size = 216727, upload-time = "2025-10-08T19:46:39.732Z" },
+ { url = "https://files.pythonhosted.org/packages/79/37/3ec3f7e3173e73f1d600495d8b545b53802cbf35506e5732dd8578db3724/propcache-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f", size = 205097, upload-time = "2025-10-08T19:46:41.025Z" },
+ { url = "https://files.pythonhosted.org/packages/61/b0/b2631c19793f869d35f47d5a3a56fb19e9160d3c119f15ac7344fc3ccae7/propcache-0.4.1-cp311-cp311-win32.whl", hash = "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1", size = 38084, upload-time = "2025-10-08T19:46:42.693Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/78/6cce448e2098e9f3bfc91bb877f06aa24b6ccace872e39c53b2f707c4648/propcache-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6", size = 41637, upload-time = "2025-10-08T19:46:43.778Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/e9/754f180cccd7f51a39913782c74717c581b9cc8177ad0e949f4d51812383/propcache-0.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239", size = 38064, upload-time = "2025-10-08T19:46:44.872Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061, upload-time = "2025-10-08T19:46:46.075Z" },
+ { url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037, upload-time = "2025-10-08T19:46:47.23Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324, upload-time = "2025-10-08T19:46:48.384Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/d3/6c7ee328b39a81ee877c962469f1e795f9db87f925251efeb0545e0020d0/propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", size = 225505, upload-time = "2025-10-08T19:46:50.055Z" },
+ { url = "https://files.pythonhosted.org/packages/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242, upload-time = "2025-10-08T19:46:51.815Z" },
+ { url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474, upload-time = "2025-10-08T19:46:53.208Z" },
+ { url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575, upload-time = "2025-10-08T19:46:54.511Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736, upload-time = "2025-10-08T19:46:56.212Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019, upload-time = "2025-10-08T19:46:57.595Z" },
+ { url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376, upload-time = "2025-10-08T19:46:59.067Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988, upload-time = "2025-10-08T19:47:00.544Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615, upload-time = "2025-10-08T19:47:01.968Z" },
+ { url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066, upload-time = "2025-10-08T19:47:03.503Z" },
+ { url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655, upload-time = "2025-10-08T19:47:04.973Z" },
+ { url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789, upload-time = "2025-10-08T19:47:06.077Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750, upload-time = "2025-10-08T19:47:07.648Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780, upload-time = "2025-10-08T19:47:08.851Z" },
+ { url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308, upload-time = "2025-10-08T19:47:09.982Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182, upload-time = "2025-10-08T19:47:11.319Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215, upload-time = "2025-10-08T19:47:13.146Z" },
+ { url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112, upload-time = "2025-10-08T19:47:14.913Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442, upload-time = "2025-10-08T19:47:16.277Z" },
+ { url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398, upload-time = "2025-10-08T19:47:17.962Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920, upload-time = "2025-10-08T19:47:19.355Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748, upload-time = "2025-10-08T19:47:21.338Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877, upload-time = "2025-10-08T19:47:23.059Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437, upload-time = "2025-10-08T19:47:24.445Z" },
+ { url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586, upload-time = "2025-10-08T19:47:25.736Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790, upload-time = "2025-10-08T19:47:26.847Z" },
+ { url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158, upload-time = "2025-10-08T19:47:27.961Z" },
+ { url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451, upload-time = "2025-10-08T19:47:29.445Z" },
+ { url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374, upload-time = "2025-10-08T19:47:30.579Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396, upload-time = "2025-10-08T19:47:31.79Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950, upload-time = "2025-10-08T19:47:33.481Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856, upload-time = "2025-10-08T19:47:34.906Z" },
+ { url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420, upload-time = "2025-10-08T19:47:36.338Z" },
+ { url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254, upload-time = "2025-10-08T19:47:37.692Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205, upload-time = "2025-10-08T19:47:39.659Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873, upload-time = "2025-10-08T19:47:41.084Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739, upload-time = "2025-10-08T19:47:42.51Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514, upload-time = "2025-10-08T19:47:43.927Z" },
+ { url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781, upload-time = "2025-10-08T19:47:45.448Z" },
+ { url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396, upload-time = "2025-10-08T19:47:47.202Z" },
+ { url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897, upload-time = "2025-10-08T19:47:48.336Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789, upload-time = "2025-10-08T19:47:49.876Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152, upload-time = "2025-10-08T19:47:51.051Z" },
+ { url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869, upload-time = "2025-10-08T19:47:52.594Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596, upload-time = "2025-10-08T19:47:54.073Z" },
+ { url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981, upload-time = "2025-10-08T19:47:55.715Z" },
+ { url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490, upload-time = "2025-10-08T19:47:57.499Z" },
+ { url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371, upload-time = "2025-10-08T19:47:59.317Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424, upload-time = "2025-10-08T19:48:00.67Z" },
+ { url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566, upload-time = "2025-10-08T19:48:02.604Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130, upload-time = "2025-10-08T19:48:04.499Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625, upload-time = "2025-10-08T19:48:06.213Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209, upload-time = "2025-10-08T19:48:08.432Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797, upload-time = "2025-10-08T19:48:09.968Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140, upload-time = "2025-10-08T19:48:11.232Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257, upload-time = "2025-10-08T19:48:12.707Z" },
+ { url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097, upload-time = "2025-10-08T19:48:13.923Z" },
+ { url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455, upload-time = "2025-10-08T19:48:15.16Z" },
+ { url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372, upload-time = "2025-10-08T19:48:16.424Z" },
+ { url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411, upload-time = "2025-10-08T19:48:17.577Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712, upload-time = "2025-10-08T19:48:18.901Z" },
+ { url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557, upload-time = "2025-10-08T19:48:20.762Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015, upload-time = "2025-10-08T19:48:22.592Z" },
+ { url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880, upload-time = "2025-10-08T19:48:23.947Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938, upload-time = "2025-10-08T19:48:25.656Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641, upload-time = "2025-10-08T19:48:27.207Z" },
+ { url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510, upload-time = "2025-10-08T19:48:28.65Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161, upload-time = "2025-10-08T19:48:30.133Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393, upload-time = "2025-10-08T19:48:31.567Z" },
+ { url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546, upload-time = "2025-10-08T19:48:32.872Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259, upload-time = "2025-10-08T19:48:34.226Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428, upload-time = "2025-10-08T19:48:35.441Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" },
+]
+
+[[package]]
+name = "protobuf"
+version = "7.34.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f2/00/04a2ab36b70a52d0356852979e08b44edde0435f2115dc66e25f2100f3ab/protobuf-7.34.0.tar.gz", hash = "sha256:3871a3df67c710aaf7bb8d214cc997342e63ceebd940c8c7fc65c9b3d697591a", size = 454726, upload-time = "2026-02-27T00:30:25.421Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/13/c4/6322ab5c8f279c4c358bc14eb8aefc0550b97222a39f04eb3c1af7a830fa/protobuf-7.34.0-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:8e329966799f2c271d5e05e236459fe1cbfdb8755aaa3b0914fa60947ddea408", size = 429248, upload-time = "2026-02-27T00:30:14.924Z" },
+ { url = "https://files.pythonhosted.org/packages/45/99/b029bbbc61e8937545da5b79aa405ab2d9cf307a728f8c9459ad60d7a481/protobuf-7.34.0-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:9d7a5005fb96f3c1e64f397f91500b0eb371b28da81296ae73a6b08a5b76cdd6", size = 325753, upload-time = "2026-02-27T00:30:17.247Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/79/09f02671eb75b251c5550a1c48e7b3d4b0623efd7c95a15a50f6f9fc1e2e/protobuf-7.34.0-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:4a72a8ec94e7a9f7ef7fe818ed26d073305f347f8b3b5ba31e22f81fd85fca02", size = 340200, upload-time = "2026-02-27T00:30:18.672Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/57/89727baef7578897af5ed166735ceb315819f1c184da8c3441271dbcfde7/protobuf-7.34.0-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:964cf977e07f479c0697964e83deda72bcbc75c3badab506fb061b352d991b01", size = 324268, upload-time = "2026-02-27T00:30:20.088Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/3e/38ff2ddee5cc946f575c9d8cc822e34bde205cf61acf8099ad88ef19d7d2/protobuf-7.34.0-cp310-abi3-win32.whl", hash = "sha256:f791ec509707a1d91bd02e07df157e75e4fb9fbdad12a81b7396201ec244e2e3", size = 426628, upload-time = "2026-02-27T00:30:21.555Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/71/7c32eaf34a61a1bae1b62a2ac4ffe09b8d1bb0cf93ad505f42040023db89/protobuf-7.34.0-cp310-abi3-win_amd64.whl", hash = "sha256:9f9079f1dde4e32342ecbd1c118d76367090d4aaa19da78230c38101c5b3dd40", size = 437901, upload-time = "2026-02-27T00:30:22.836Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/e7/14dc9366696dcb53a413449881743426ed289d687bcf3d5aee4726c32ebb/protobuf-7.34.0-py3-none-any.whl", hash = "sha256:e3b914dd77fa33fa06ab2baa97937746ab25695f389869afdf03e81f34e45dc7", size = 170716, upload-time = "2026-02-27T00:30:23.994Z" },
+]
+
+[[package]]
+name = "psycopg2-binary"
+version = "2.9.11"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ac/6c/8767aaa597ba424643dc87348c6f1754dd9f48e80fdc1b9f7ca5c3a7c213/psycopg2-binary-2.9.11.tar.gz", hash = "sha256:b6aed9e096bf63f9e75edf2581aa9a7e7186d97ab5c177aa6c87797cd591236c", size = 379620, upload-time = "2025-10-10T11:14:48.041Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c7/ae/8d8266f6dd183ab4d48b95b9674034e1b482a3f8619b33a0d86438694577/psycopg2_binary-2.9.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0e8480afd62362d0a6a27dd09e4ca2def6fa50ed3a4e7c09165266106b2ffa10", size = 3756452, upload-time = "2025-10-10T11:11:11.583Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/34/aa03d327739c1be70e09d01182619aca8ebab5970cd0cfa50dd8b9cec2ac/psycopg2_binary-2.9.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:763c93ef1df3da6d1a90f86ea7f3f806dc06b21c198fa87c3c25504abec9404a", size = 3863957, upload-time = "2025-10-10T11:11:16.932Z" },
+ { url = "https://files.pythonhosted.org/packages/48/89/3fdb5902bdab8868bbedc1c6e6023a4e08112ceac5db97fc2012060e0c9a/psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e164359396576a3cc701ba8af4751ae68a07235d7a380c631184a611220d9a4", size = 4410955, upload-time = "2025-10-10T11:11:21.21Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/24/e18339c407a13c72b336e0d9013fbbbde77b6fd13e853979019a1269519c/psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:d57c9c387660b8893093459738b6abddbb30a7eab058b77b0d0d1c7d521ddfd7", size = 4468007, upload-time = "2025-10-10T11:11:24.831Z" },
+ { url = "https://files.pythonhosted.org/packages/91/7e/b8441e831a0f16c159b5381698f9f7f7ed54b77d57bc9c5f99144cc78232/psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2c226ef95eb2250974bf6fa7a842082b31f68385c4f3268370e3f3870e7859ee", size = 4165012, upload-time = "2025-10-10T11:11:29.51Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/61/4aa89eeb6d751f05178a13da95516c036e27468c5d4d2509bb1e15341c81/psycopg2_binary-2.9.11-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a311f1edc9967723d3511ea7d2708e2c3592e3405677bf53d5c7246753591fbb", size = 3981881, upload-time = "2025-10-30T02:55:07.332Z" },
+ { url = "https://files.pythonhosted.org/packages/76/a1/2f5841cae4c635a9459fe7aca8ed771336e9383b6429e05c01267b0774cf/psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ebb415404821b6d1c47353ebe9c8645967a5235e6d88f914147e7fd411419e6f", size = 3650985, upload-time = "2025-10-10T11:11:34.975Z" },
+ { url = "https://files.pythonhosted.org/packages/84/74/4defcac9d002bca5709951b975173c8c2fa968e1a95dc713f61b3a8d3b6a/psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f07c9c4a5093258a03b28fab9b4f151aa376989e7f35f855088234e656ee6a94", size = 3296039, upload-time = "2025-10-10T11:11:40.432Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/c2/782a3c64403d8ce35b5c50e1b684412cf94f171dc18111be8c976abd2de1/psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:00ce1830d971f43b667abe4a56e42c1e2d594b32da4802e44a73bacacb25535f", size = 3043477, upload-time = "2025-10-30T02:55:11.182Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/31/36a1d8e702aa35c38fc117c2b8be3f182613faa25d794b8aeaab948d4c03/psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cffe9d7697ae7456649617e8bb8d7a45afb71cd13f7ab22af3e5c61f04840908", size = 3345842, upload-time = "2025-10-10T11:11:45.366Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/b4/a5375cda5b54cb95ee9b836930fea30ae5a8f14aa97da7821722323d979b/psycopg2_binary-2.9.11-cp311-cp311-win_amd64.whl", hash = "sha256:304fd7b7f97eef30e91b8f7e720b3db75fee010b520e434ea35ed1ff22501d03", size = 2713894, upload-time = "2025-10-10T11:11:48.775Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/91/f870a02f51be4a65987b45a7de4c2e1897dd0d01051e2b559a38fa634e3e/psycopg2_binary-2.9.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:be9b840ac0525a283a96b556616f5b4820e0526addb8dcf6525a0fa162730be4", size = 3756603, upload-time = "2025-10-10T11:11:52.213Z" },
+ { url = "https://files.pythonhosted.org/packages/27/fa/cae40e06849b6c9a95eb5c04d419942f00d9eaac8d81626107461e268821/psycopg2_binary-2.9.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f090b7ddd13ca842ebfe301cd587a76a4cf0913b1e429eb92c1be5dbeb1a19bc", size = 3864509, upload-time = "2025-10-10T11:11:56.452Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/75/364847b879eb630b3ac8293798e380e441a957c53657995053c5ec39a316/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ab8905b5dcb05bf3fb22e0cf90e10f469563486ffb6a96569e51f897c750a76a", size = 4411159, upload-time = "2025-10-10T11:12:00.49Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/a0/567f7ea38b6e1c62aafd58375665a547c00c608a471620c0edc364733e13/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf940cd7e7fec19181fdbc29d76911741153d51cab52e5c21165f3262125685e", size = 4468234, upload-time = "2025-10-10T11:12:04.892Z" },
+ { url = "https://files.pythonhosted.org/packages/30/da/4e42788fb811bbbfd7b7f045570c062f49e350e1d1f3df056c3fb5763353/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa0f693d3c68ae925966f0b14b8edda71696608039f4ed61b1fe9ffa468d16db", size = 4166236, upload-time = "2025-10-10T11:12:11.674Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/94/c1777c355bc560992af848d98216148be5f1be001af06e06fc49cbded578/psycopg2_binary-2.9.11-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a1cf393f1cdaf6a9b57c0a719a1068ba1069f022a59b8b1fe44b006745b59757", size = 3983083, upload-time = "2025-10-30T02:55:15.73Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/42/c9a21edf0e3daa7825ed04a4a8588686c6c14904344344a039556d78aa58/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7a6beb4beaa62f88592ccc65df20328029d721db309cb3250b0aae0fa146c3", size = 3652281, upload-time = "2025-10-10T11:12:17.713Z" },
+ { url = "https://files.pythonhosted.org/packages/12/22/dedfbcfa97917982301496b6b5e5e6c5531d1f35dd2b488b08d1ebc52482/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:31b32c457a6025e74d233957cc9736742ac5a6cb196c6b68499f6bb51390bd6a", size = 3298010, upload-time = "2025-10-10T11:12:22.671Z" },
+ { url = "https://files.pythonhosted.org/packages/66/ea/d3390e6696276078bd01b2ece417deac954dfdd552d2edc3d03204416c0c/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:edcb3aeb11cb4bf13a2af3c53a15b3d612edeb6409047ea0b5d6a21a9d744b34", size = 3044641, upload-time = "2025-10-30T02:55:19.929Z" },
+ { url = "https://files.pythonhosted.org/packages/12/9a/0402ded6cbd321da0c0ba7d34dc12b29b14f5764c2fc10750daa38e825fc/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b6d93d7c0b61a1dd6197d208ab613eb7dcfdcca0a49c42ceb082257991de9d", size = 3347940, upload-time = "2025-10-10T11:12:26.529Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/d2/99b55e85832ccde77b211738ff3925a5d73ad183c0b37bcbbe5a8ff04978/psycopg2_binary-2.9.11-cp312-cp312-win_amd64.whl", hash = "sha256:b33fabeb1fde21180479b2d4667e994de7bbf0eec22832ba5d9b5e4cf65b6c6d", size = 2714147, upload-time = "2025-10-10T11:12:29.535Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/a8/a2709681b3ac11b0b1786def10006b8995125ba268c9a54bea6f5ae8bd3e/psycopg2_binary-2.9.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b8fb3db325435d34235b044b199e56cdf9ff41223a4b9752e8576465170bb38c", size = 3756572, upload-time = "2025-10-10T11:12:32.873Z" },
+ { url = "https://files.pythonhosted.org/packages/62/e1/c2b38d256d0dafd32713e9f31982a5b028f4a3651f446be70785f484f472/psycopg2_binary-2.9.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:366df99e710a2acd90efed3764bb1e28df6c675d33a7fb40df9b7281694432ee", size = 3864529, upload-time = "2025-10-10T11:12:36.791Z" },
+ { url = "https://files.pythonhosted.org/packages/11/32/b2ffe8f3853c181e88f0a157c5fb4e383102238d73c52ac6d93a5c8bffe6/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c55b385daa2f92cb64b12ec4536c66954ac53654c7f15a203578da4e78105c0", size = 4411242, upload-time = "2025-10-10T11:12:42.388Z" },
+ { url = "https://files.pythonhosted.org/packages/10/04/6ca7477e6160ae258dc96f67c371157776564679aefd247b66f4661501a2/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c0377174bf1dd416993d16edc15357f6eb17ac998244cca19bc67cdc0e2e5766", size = 4468258, upload-time = "2025-10-10T11:12:48.654Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/7e/6a1a38f86412df101435809f225d57c1a021307dd0689f7a5e7fe83588b1/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c6ff3335ce08c75afaed19e08699e8aacf95d4a260b495a4a8545244fe2ceb3", size = 4166295, upload-time = "2025-10-10T11:12:52.525Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/7d/c07374c501b45f3579a9eb761cbf2604ddef3d96ad48679112c2c5aa9c25/psycopg2_binary-2.9.11-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:84011ba3109e06ac412f95399b704d3d6950e386b7994475b231cf61eec2fc1f", size = 3983133, upload-time = "2025-10-30T02:55:24.329Z" },
+ { url = "https://files.pythonhosted.org/packages/82/56/993b7104cb8345ad7d4516538ccf8f0d0ac640b1ebd8c754a7b024e76878/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ba34475ceb08cccbdd98f6b46916917ae6eeb92b5ae111df10b544c3a4621dc4", size = 3652383, upload-time = "2025-10-10T11:12:56.387Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/ac/eaeb6029362fd8d454a27374d84c6866c82c33bfc24587b4face5a8e43ef/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b31e90fdd0f968c2de3b26ab014314fe814225b6c324f770952f7d38abf17e3c", size = 3298168, upload-time = "2025-10-10T11:13:00.403Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/39/50c3facc66bded9ada5cbc0de867499a703dc6bca6be03070b4e3b65da6c/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d526864e0f67f74937a8fce859bd56c979f5e2ec57ca7c627f5f1071ef7fee60", size = 3044712, upload-time = "2025-10-30T02:55:27.975Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/8e/b7de019a1f562f72ada81081a12823d3c1590bedc48d7d2559410a2763fe/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04195548662fa544626c8ea0f06561eb6203f1984ba5b4562764fbeb4c3d14b1", size = 3347549, upload-time = "2025-10-10T11:13:03.971Z" },
+ { url = "https://files.pythonhosted.org/packages/80/2d/1bb683f64737bbb1f86c82b7359db1eb2be4e2c0c13b947f80efefa7d3e5/psycopg2_binary-2.9.11-cp313-cp313-win_amd64.whl", hash = "sha256:efff12b432179443f54e230fdf60de1f6cc726b6c832db8701227d089310e8aa", size = 2714215, upload-time = "2025-10-10T11:13:07.14Z" },
+ { url = "https://files.pythonhosted.org/packages/64/12/93ef0098590cf51d9732b4f139533732565704f45bdc1ffa741b7c95fb54/psycopg2_binary-2.9.11-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:92e3b669236327083a2e33ccfa0d320dd01b9803b3e14dd986a4fc54aa00f4e1", size = 3756567, upload-time = "2025-10-10T11:13:11.885Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/a9/9d55c614a891288f15ca4b5209b09f0f01e3124056924e17b81b9fa054cc/psycopg2_binary-2.9.11-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e0deeb03da539fa3577fcb0b3f2554a97f7e5477c246098dbb18091a4a01c16f", size = 3864755, upload-time = "2025-10-10T11:13:17.727Z" },
+ { url = "https://files.pythonhosted.org/packages/13/1e/98874ce72fd29cbde93209977b196a2edae03f8490d1bd8158e7f1daf3a0/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9b52a3f9bb540a3e4ec0f6ba6d31339727b2950c9772850d6545b7eae0b9d7c5", size = 4411646, upload-time = "2025-10-10T11:13:24.432Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/bd/a335ce6645334fb8d758cc358810defca14a1d19ffbc8a10bd38a2328565/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:db4fd476874ccfdbb630a54426964959e58da4c61c9feba73e6094d51303d7d8", size = 4468701, upload-time = "2025-10-10T11:13:29.266Z" },
+ { url = "https://files.pythonhosted.org/packages/44/d6/c8b4f53f34e295e45709b7568bf9b9407a612ea30387d35eb9fa84f269b4/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47f212c1d3be608a12937cc131bd85502954398aaa1320cb4c14421a0ffccf4c", size = 4166293, upload-time = "2025-10-10T11:13:33.336Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/e0/f8cc36eadd1b716ab36bb290618a3292e009867e5c97ce4aba908cb99644/psycopg2_binary-2.9.11-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e35b7abae2b0adab776add56111df1735ccc71406e56203515e228a8dc07089f", size = 3983184, upload-time = "2025-10-30T02:55:32.483Z" },
+ { url = "https://files.pythonhosted.org/packages/53/3e/2a8fe18a4e61cfb3417da67b6318e12691772c0696d79434184a511906dc/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fcf21be3ce5f5659daefd2b3b3b6e4727b028221ddc94e6c1523425579664747", size = 3652650, upload-time = "2025-10-10T11:13:38.181Z" },
+ { url = "https://files.pythonhosted.org/packages/76/36/03801461b31b29fe58d228c24388f999fe814dfc302856e0d17f97d7c54d/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9bd81e64e8de111237737b29d68039b9c813bdf520156af36d26819c9a979e5f", size = 3298663, upload-time = "2025-10-10T11:13:44.878Z" },
+ { url = "https://files.pythonhosted.org/packages/97/77/21b0ea2e1a73aa5fa9222b2a6b8ba325c43c3a8d54272839c991f2345656/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:32770a4d666fbdafab017086655bcddab791d7cb260a16679cc5a7338b64343b", size = 3044737, upload-time = "2025-10-30T02:55:35.69Z" },
+ { url = "https://files.pythonhosted.org/packages/67/69/f36abe5f118c1dca6d3726ceae164b9356985805480731ac6712a63f24f0/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c3cb3a676873d7506825221045bd70e0427c905b9c8ee8d6acd70cfcbd6e576d", size = 3347643, upload-time = "2025-10-10T11:13:53.499Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/36/9c0c326fe3a4227953dfb29f5d0c8ae3b8eb8c1cd2967aa569f50cb3c61f/psycopg2_binary-2.9.11-cp314-cp314-win_amd64.whl", hash = "sha256:4012c9c954dfaccd28f94e84ab9f94e12df76b4afb22331b1f0d3154893a6316", size = 2803913, upload-time = "2025-10-10T11:13:57.058Z" },
+]
+
+[[package]]
+name = "pycparser"
+version = "3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" },
+]
+
+[[package]]
+name = "pydantic"
+version = "2.12.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "annotated-types" },
+ { name = "pydantic-core" },
+ { name = "typing-extensions" },
+ { name = "typing-inspection" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" },
+]
+
+[[package]]
+name = "pydantic-core"
+version = "2.41.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" },
+ { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" },
+ { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" },
+ { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" },
+ { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" },
+ { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" },
+ { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" },
+ { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" },
+ { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" },
+ { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" },
+ { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" },
+ { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" },
+ { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" },
+ { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" },
+ { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" },
+ { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" },
+ { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" },
+ { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" },
+ { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" },
+ { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" },
+ { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" },
+ { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" },
+ { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" },
+ { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" },
+ { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" },
+ { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" },
+ { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" },
+ { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" },
+ { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" },
+ { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" },
+ { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" },
+ { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" },
+ { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" },
+ { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" },
+ { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" },
+ { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" },
+ { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" },
+]
+
+[[package]]
+name = "pydantic-settings"
+version = "2.12.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pydantic" },
+ { name = "python-dotenv" },
+ { name = "typing-inspection" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/43/4b/ac7e0aae12027748076d72a8764ff1c9d82ca75a7a52622e67ed3f765c54/pydantic_settings-2.12.0.tar.gz", hash = "sha256:005538ef951e3c2a68e1c08b292b5f2e71490def8589d4221b95dab00dafcfd0", size = 194184, upload-time = "2025-11-10T14:25:47.013Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809", size = 51880, upload-time = "2025-11-10T14:25:45.546Z" },
+]
+
+[[package]]
+name = "pygments"
+version = "2.19.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
+]
+
+[[package]]
+name = "pyjwt"
+version = "2.10.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785, upload-time = "2024-11-28T03:43:29.933Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" },
+]
+
+[package.optional-dependencies]
+crypto = [
+ { name = "cryptography" },
+]
+
+[[package]]
+name = "pymupdf"
+version = "1.27.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1b/0c/40dda0cc4bd2220a2ef75f8c53dd7d8ed1e29681fcb3df75db6ee9677a7e/pymupdf-1.27.1.tar.gz", hash = "sha256:4afbde0769c336717a149ab0de3330dcb75378f795c1a8c5af55c1a628b17d55", size = 85303479, upload-time = "2026-02-12T08:29:17.682Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/13/19/fde6ea4712a904b65e8f41124a0e4233879b87a770fe6a8ce857964de6d5/pymupdf-1.27.1-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:bee9f95512f9556dbf2cacfd1413c61b29a55baa07fa7f8fc83d221d8419888a", size = 23986707, upload-time = "2026-02-11T15:03:24.025Z" },
+ { url = "https://files.pythonhosted.org/packages/75/c2/070dff91ad3f1bc16fd6c6ceff23495601fcce4c92d28be534417596418a/pymupdf-1.27.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:3de95a0889395b0966fafd11b94980b7543a816e89dd1c218597a08543ac3415", size = 23263493, upload-time = "2026-02-11T15:03:45.528Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/db/937377f4b3e0fbf6273c17436a49f7db17df1a46b1be9e26653b6fafc0e1/pymupdf-1.27.1-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:2c9d9353b840040cbc724341f4095fb7e2cc1a12a9147d0ec1a0a79f5d773147", size = 24317651, upload-time = "2026-02-11T22:33:38.967Z" },
+ { url = "https://files.pythonhosted.org/packages/72/d5/c701cf2d0cdd6e5d6bca3ca9188d7f5d7ce3ae67dd1368d658cd4bae2707/pymupdf-1.27.1-cp310-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:aeaed76e72cbc061149a825ab0811c5f4752970c56591c2938c5042ec06b26e1", size = 24945742, upload-time = "2026-02-11T15:04:06.21Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/29/690202b38b93cf77b73a29c25a63a2b6f3fcb36b1f75006e50b8dee7c108/pymupdf-1.27.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4f1837554134fb45d390a44de8844b2ca9b6c901c82ccc90b340e3b7f3b126ca", size = 25167965, upload-time = "2026-02-11T22:36:35.478Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/81/f937e6aa606fd263c3a45d0ff0f0bbdbf3fb779933091fc0f6179513cc93/pymupdf-1.27.1-cp310-abi3-win32.whl", hash = "sha256:fa33b512d82c6c4852edadf57f22d5f27d16243bb33dac0fbe4eb0f281c5b17e", size = 18006253, upload-time = "2026-02-12T13:48:07.129Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/99/fe4a7752990bf65277718fffbead4478de9afd1c7288d7a6d643f79a6fa7/pymupdf-1.27.1-cp310-abi3-win_amd64.whl", hash = "sha256:4b6268dff3a9d713034eba5c2ffce0da37c62443578941ac5df433adcde57b2f", size = 19236703, upload-time = "2026-02-11T15:04:19.607Z" },
+]
+
+[[package]]
+name = "pymupdf-layout"
+version = "1.27.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "networkx" },
+ { name = "numpy" },
+ { name = "onnxruntime" },
+ { name = "pymupdf" },
+ { name = "pyyaml" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b7/ac/574f537fa0199b31755eb0859d8e6ed8fcc669f7b460eba78611d1d78c8a/pymupdf_layout-1.27.1-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:e60b7238f7652a825a884641fc1e9070e3b385905891ae194dfe91e5932b2342", size = 12323338, upload-time = "2026-02-11T16:23:52.453Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/0b/b83178bb88cc8933f61dac113c89be9be426fd7c3b9deca3b628e96eb99a/pymupdf_layout-1.27.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:888923391e155cb6a4e0498709c8f4da750858dc273417a7386aeb8b81009b83", size = 12318712, upload-time = "2026-02-11T16:24:01.194Z" },
+ { url = "https://files.pythonhosted.org/packages/66/e4/d11ebba2e950606713495620503d0e1b627c879c708fc488f7167cfb35a3/pymupdf_layout-1.27.1-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:05d88e0060b5875d21ee53c1dc50a2454aa32e59048b971847782318f47dca11", size = 12328732, upload-time = "2026-02-11T22:33:46.923Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/b6/0278408e2f6db993e3c9d4ee7be89f01b0022233298aa20fe54b95a0169c/pymupdf_layout-1.27.1-cp310-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:e9e10d96f06ef5f511523a7caf5b707596a60c5c0a4735f4c5f84728cdb75ffd", size = 12329763, upload-time = "2026-02-11T16:24:09.825Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/2c/f96920afb5a17226e7475f5db9eb7755a019623984caedcb47cb16660bd2/pymupdf_layout-1.27.1-cp310-abi3-win_amd64.whl", hash = "sha256:e456bcdc8760caadcaecb57e96277de11f9cdde785b04bf535448df4fb1e25fc", size = 12332980, upload-time = "2026-02-11T16:24:17.523Z" },
+]
+
+[[package]]
+name = "pymupdf4llm"
+version = "0.2.9"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pymupdf" },
+ { name = "tabulate" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/cb/25/40ce2f199d97439bff28d9fb3723d68995905b98cba113873d9ff6cfd013/pymupdf4llm-0.2.9.tar.gz", hash = "sha256:6a3e156d1f5724353ae44c18b86b4c5eefcc0d8599eb73a0e92f8137fcbce012", size = 70453, upload-time = "2026-01-10T10:18:00.2Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/52/6f/7312cb3d220b252defb1205a5b9d7e074e1c0883d91c461227b2bd3b7fa8/pymupdf4llm-0.2.9-py3-none-any.whl", hash = "sha256:ea9eb3b72765e1970374a37ba7bdc6b1117b0e3ceea5d48b530f49654d8adc94", size = 72286, upload-time = "2026-01-10T10:18:03.805Z" },
+]
+
+[[package]]
+name = "pytest"
+version = "9.0.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+ { name = "iniconfig" },
+ { name = "packaging" },
+ { name = "pluggy" },
+ { name = "pygments" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
+]
+
+[[package]]
+name = "pytest-asyncio"
+version = "1.3.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pytest" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" },
+]
+
+[[package]]
+name = "pytest-cov"
+version = "7.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "coverage", extra = ["toml"] },
+ { name = "pluggy" },
+ { name = "pytest" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" },
+]
+
+[[package]]
+name = "pytest-mock"
+version = "3.15.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pytest" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036, upload-time = "2025-09-16T16:37:27.081Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" },
+]
+
+[[package]]
+name = "python-dateutil"
+version = "2.9.0.post0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "six" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
+]
+
+[[package]]
+name = "python-dotenv"
+version = "1.2.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" },
+]
+
+[[package]]
+name = "python-multipart"
+version = "0.0.22"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" },
+]
+
+[[package]]
+name = "pytokens"
+version = "0.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e5/16/4b9cfd90d55e66ffdb277d7ebe3bc25250c2311336ec3fc73b2673c794d5/pytokens-0.4.0.tar.gz", hash = "sha256:6b0b03e6ea7c9f9d47c5c61164b69ad30f4f0d70a5d9fe7eac4d19f24f77af2d", size = 15039, upload-time = "2026-01-19T07:59:50.623Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b4/05/3196399a353dd4cd99138a88f662810979ee2f1a1cdb0b417cb2f4507836/pytokens-0.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:92eb3ef88f27c22dc9dbab966ace4d61f6826e02ba04dac8e2d65ea31df56c8e", size = 160075, upload-time = "2026-01-19T07:59:00.316Z" },
+ { url = "https://files.pythonhosted.org/packages/28/1d/c8fc4ed0a1c4f660391b201cda00b1d5bbcc00e2998e8bcd48b15eefd708/pytokens-0.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f4b77858a680635ee9904306f54b0ee4781effb89e211ba0a773d76539537165", size = 247318, upload-time = "2026-01-19T07:59:01.636Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/0e/53e55ba01f3e858d229cd84b02481542f42ba59050483a78bf2447ee1af7/pytokens-0.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25cacc20c2ad90acb56f3739d87905473c54ca1fa5967ffcd675463fe965865e", size = 259752, upload-time = "2026-01-19T07:59:04.229Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/56/2d930d7f899e3f21868ca6e8ec739ac31e8fc532f66e09cbe45d3df0a84f/pytokens-0.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:628fab535ebc9079e4db35cd63cb401901c7ce8720a9834f9ad44b9eb4e0f1d4", size = 262842, upload-time = "2026-01-19T07:59:06.14Z" },
+ { url = "https://files.pythonhosted.org/packages/42/dd/4e7e6920d23deffaf66e6f40d45f7610dcbc132ca5d90ab4faccef22f624/pytokens-0.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:4d0f568d7e82b7e96be56d03b5081de40e43c904eb6492bf09aaca47cd55f35b", size = 102620, upload-time = "2026-01-19T07:59:07.839Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/65/65460ebbfefd0bc1b160457904370d44f269e6e4582e0a9b6cba7c267b04/pytokens-0.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cd8da894e5a29ba6b6da8be06a4f7589d7220c099b5e363cb0643234b9b38c2a", size = 159864, upload-time = "2026-01-19T07:59:08.908Z" },
+ { url = "https://files.pythonhosted.org/packages/25/70/a46669ec55876c392036b4da9808b5c3b1c5870bbca3d4cc923bf68bdbc1/pytokens-0.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:237ba7cfb677dbd3b01b09860810aceb448871150566b93cd24501d5734a04b1", size = 254448, upload-time = "2026-01-19T07:59:10.594Z" },
+ { url = "https://files.pythonhosted.org/packages/62/0b/c486fc61299c2fc3b7f88ee4e115d4c8b6ffd1a7f88dc94b398b5b1bc4b8/pytokens-0.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01d1a61e36812e4e971cfe2c0e4c1f2d66d8311031dac8bf168af8a249fa04dd", size = 268863, upload-time = "2026-01-19T07:59:12.31Z" },
+ { url = "https://files.pythonhosted.org/packages/79/92/b036af846707d25feaff7cafbd5280f1bd6a1034c16bb06a7c910209c1ab/pytokens-0.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e47e2ef3ec6ee86909e520d79f965f9b23389fda47460303cf715d510a6fe544", size = 267181, upload-time = "2026-01-19T07:59:13.856Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/c0/6d011fc00fefa74ce34816c84a923d2dd7c46b8dbc6ee52d13419786834c/pytokens-0.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:3d36954aba4557fd5a418a03cf595ecbb1cdcce119f91a49b19ef09d691a22ae", size = 102814, upload-time = "2026-01-19T07:59:15.288Z" },
+ { url = "https://files.pythonhosted.org/packages/98/63/627b7e71d557383da5a97f473ad50f8d9c2c1f55c7d3c2531a120c796f6e/pytokens-0.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73eff3bdd8ad08da679867992782568db0529b887bed4c85694f84cdf35eafc6", size = 159744, upload-time = "2026-01-19T07:59:16.88Z" },
+ { url = "https://files.pythonhosted.org/packages/28/d7/16f434c37ec3824eba6bcb6e798e5381a8dc83af7a1eda0f95c16fe3ade5/pytokens-0.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d97cc1f91b1a8e8ebccf31c367f28225699bea26592df27141deade771ed0afb", size = 253207, upload-time = "2026-01-19T07:59:18.069Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/96/04102856b9527701ae57d74a6393d1aca5bad18a1b1ca48ccffb3c93b392/pytokens-0.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2c8952c537cb73a1a74369501a83b7f9d208c3cf92c41dd88a17814e68d48ce", size = 267452, upload-time = "2026-01-19T07:59:19.328Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/ef/0936eb472b89ab2d2c2c24bb81c50417e803fa89c731930d9fb01176fe9f/pytokens-0.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5dbf56f3c748aed9310b310d5b8b14e2c96d3ad682ad5a943f381bdbbdddf753", size = 265965, upload-time = "2026-01-19T07:59:20.613Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/f5/64f3d6f7df4a9e92ebda35ee85061f6260e16eac82df9396020eebbca775/pytokens-0.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:e131804513597f2dff2b18f9911d9b6276e21ef3699abeffc1c087c65a3d975e", size = 102813, upload-time = "2026-01-19T07:59:22.012Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/f1/d07e6209f18ef378fc2ae9dee8d1dfe91fd2447c2e2dbfa32867b6dd30cf/pytokens-0.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0d7374c917197106d3c4761374718bc55ea2e9ac0fb94171588ef5840ee1f016", size = 159968, upload-time = "2026-01-19T07:59:23.07Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/73/0eb111400abd382a04f253b269819db9fcc748aa40748441cebdcb6d068f/pytokens-0.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cd3fa1caf9e47a72ee134a29ca6b5bea84712724bba165d6628baa190c6ea5b", size = 253373, upload-time = "2026-01-19T07:59:24.381Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/8d/9e4e2fdb5bcaba679e54afcc304e9f13f488eb4d626e6b613f9553e03dbd/pytokens-0.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c6986576b7b07fe9791854caa5347923005a80b079d45b63b0be70d50cce5f1", size = 267024, upload-time = "2026-01-19T07:59:25.74Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/b7/e0a370321af2deb772cff14ff337e1140d1eac2c29a8876bfee995f486f0/pytokens-0.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9940f7c2e2f54fb1cb5fe17d0803c54da7a2bf62222704eb4217433664a186a7", size = 270912, upload-time = "2026-01-19T07:59:27.072Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/54/4348f916c440d4c3e68b53b4ed0e66b292d119e799fa07afa159566dcc86/pytokens-0.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:54691cf8f299e7efabcc25adb4ce715d3cef1491e1c930eaf555182f898ef66a", size = 103836, upload-time = "2026-01-19T07:59:28.112Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/f8/a693c0cfa9c783a2a8c4500b7b2a8bab420f8ca4f2d496153226bf1c12e3/pytokens-0.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:94ff5db97a0d3cd7248a5b07ba2167bd3edc1db92f76c6db00137bbaf068ddf8", size = 167643, upload-time = "2026-01-19T07:59:29.292Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/dd/a64eb1e9f3ec277b69b33ef1b40ffbcc8f0a3bafcde120997efc7bdefebf/pytokens-0.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d0dd6261cd9cc95fae1227b1b6ebee023a5fd4a4b6330b071c73a516f5f59b63", size = 289553, upload-time = "2026-01-19T07:59:30.537Z" },
+ { url = "https://files.pythonhosted.org/packages/df/22/06c1079d93dbc3bca5d013e1795f3d8b9ed6c87290acd6913c1c526a6bb2/pytokens-0.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0cdca8159df407dbd669145af4171a0d967006e0be25f3b520896bc7068f02c4", size = 302490, upload-time = "2026-01-19T07:59:32.352Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/de/a6f5e43115b4fbf4b93aa87d6c83c79932cdb084f9711daae04549e1e4ad/pytokens-0.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4b5770abeb2a24347380a1164a558f0ebe06e98aedbd54c45f7929527a5fb26e", size = 305652, upload-time = "2026-01-19T07:59:33.685Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/3d/c136e057cb622e36e0c3ff7a8aaa19ff9720050c4078235691da885fe6ee/pytokens-0.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:74500d72c561dad14c037a9e86a657afd63e277dd5a3bb7570932ab7a3b12551", size = 115472, upload-time = "2026-01-19T07:59:34.734Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/3c/6941a82f4f130af6e1c68c076b6789069ef10c04559bd4733650f902fd3b/pytokens-0.4.0-py3-none-any.whl", hash = "sha256:0508d11b4de157ee12063901603be87fb0253e8f4cb9305eb168b1202ab92068", size = 13224, upload-time = "2026-01-19T07:59:49.822Z" },
+]
+
+[[package]]
+name = "pywin32"
+version = "311"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" },
+ { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" },
+ { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" },
+ { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" },
+ { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" },
+]
+
+[[package]]
+name = "pyyaml"
+version = "6.0.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" },
+ { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" },
+ { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" },
+ { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" },
+ { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" },
+ { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
+ { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
+ { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
+ { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
+ { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
+ { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
+ { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
+ { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
+ { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
+ { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
+ { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
+ { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
+ { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
+ { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
+ { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
+ { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
+ { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
+ { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
+]
+
+[[package]]
+name = "referencing"
+version = "0.37.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "attrs" },
+ { name = "rpds-py" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" },
+]
+
+[[package]]
+name = "requests"
+version = "2.32.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "charset-normalizer" },
+ { name = "idna" },
+ { name = "urllib3" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
+]
+
+[[package]]
+name = "rpds-py"
+version = "0.30.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" },
+ { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" },
+ { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" },
+ { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" },
+ { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" },
+ { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" },
+ { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" },
+ { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" },
+ { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" },
+ { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" },
+ { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" },
+ { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" },
+ { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" },
+ { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" },
+ { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" },
+ { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" },
+ { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" },
+ { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" },
+ { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" },
+ { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" },
+ { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" },
+ { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" },
+ { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" },
+ { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" },
+ { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" },
+ { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" },
+ { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" },
+ { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" },
+ { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" },
+ { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" },
+ { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" },
+ { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" },
+ { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" },
+ { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" },
+ { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" },
+ { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" },
+ { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" },
+ { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" },
+ { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" },
+ { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" },
+ { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" },
+ { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" },
+ { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" },
+ { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" },
+ { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" },
+ { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" },
+]
+
+[[package]]
+name = "sgmllib3k"
+version = "1.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/9e/bd/3704a8c3e0942d711c1299ebf7b9091930adae6675d7c8f476a7ce48653c/sgmllib3k-1.0.0.tar.gz", hash = "sha256:7868fb1c8bfa764c1ac563d3cf369c381d1325d36124933a726f29fcdaa812e9", size = 5750, upload-time = "2010-08-24T14:33:52.445Z" }
+
+[[package]]
+name = "six"
+version = "1.17.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
+]
+
+[[package]]
+name = "sse-starlette"
+version = "3.2.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "starlette" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/8b/8d/00d280c03ffd39aaee0e86ec81e2d3b9253036a0f93f51d10503adef0e65/sse_starlette-3.2.0.tar.gz", hash = "sha256:8127594edfb51abe44eac9c49e59b0b01f1039d0c7461c6fd91d4e03b70da422", size = 27253, upload-time = "2026-01-17T13:11:05.62Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/96/7f/832f015020844a8b8f7a9cbc103dd76ba8e3875004c41e08440ea3a2b41a/sse_starlette-3.2.0-py3-none-any.whl", hash = "sha256:5876954bd51920fc2cd51baee47a080eb88a37b5b784e615abb0b283f801cdbf", size = 12763, upload-time = "2026-01-17T13:11:03.775Z" },
+]
+
+[[package]]
+name = "starlette"
+version = "0.52.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c4/68/79977123bb7be889ad680d79a40f339082c1978b5cfcf62c2d8d196873ac/starlette-0.52.1.tar.gz", hash = "sha256:834edd1b0a23167694292e94f597773bc3f89f362be6effee198165a35d62933", size = 2653702, upload-time = "2026-01-18T13:34:11.062Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl", hash = "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74", size = 74272, upload-time = "2026-01-18T13:34:09.188Z" },
+]
+
+[[package]]
+name = "sympy"
+version = "1.14.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "mpmath" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" },
+]
+
+[[package]]
+name = "tabulate"
+version = "0.9.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ec/fe/802052aecb21e3797b8f7902564ab6ea0d60ff8ca23952079064155d1ae1/tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c", size = 81090, upload-time = "2022-10-06T17:21:48.54Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252, upload-time = "2022-10-06T17:21:44.262Z" },
+]
+
+[[package]]
+name = "tomli"
+version = "2.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" },
+ { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" },
+ { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" },
+ { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" },
+ { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" },
+ { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" },
+ { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" },
+ { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" },
+ { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" },
+ { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" },
+ { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" },
+ { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" },
+ { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" },
+ { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" },
+ { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" },
+ { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" },
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.15.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
+]
+
+[[package]]
+name = "typing-inspection"
+version = "0.4.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
+]
+
+[[package]]
+name = "urllib3"
+version = "2.6.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
+]
+
+[[package]]
+name = "uvicorn"
+version = "0.40.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+ { name = "h11" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c3/d1/8f3c683c9561a4e6689dd3b1d345c815f10f86acd044ee1fb9a4dcd0b8c5/uvicorn-0.40.0.tar.gz", hash = "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea", size = 81761, upload-time = "2025-12-21T14:16:22.45Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3d/d8/2083a1daa7439a66f3a48589a57d576aa117726762618f6bb09fe3798796/uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee", size = 68502, upload-time = "2025-12-21T14:16:21.041Z" },
+]
+
+[[package]]
+name = "yarl"
+version = "1.22.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "idna" },
+ { name = "multidict" },
+ { name = "propcache" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/57/63/0c6ebca57330cd313f6102b16dd57ffaf3ec4c83403dcb45dbd15c6f3ea1/yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71", size = 187169, upload-time = "2025-10-06T14:12:55.963Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/4d/27/5ab13fc84c76a0250afd3d26d5936349a35be56ce5785447d6c423b26d92/yarl-1.22.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ab72135b1f2db3fed3997d7e7dc1b80573c67138023852b6efb336a5eae6511", size = 141607, upload-time = "2025-10-06T14:09:16.298Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/a1/d065d51d02dc02ce81501d476b9ed2229d9a990818332242a882d5d60340/yarl-1.22.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:669930400e375570189492dc8d8341301578e8493aec04aebc20d4717f899dd6", size = 94027, upload-time = "2025-10-06T14:09:17.786Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/da/8da9f6a53f67b5106ffe902c6fa0164e10398d4e150d85838b82f424072a/yarl-1.22.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:792a2af6d58177ef7c19cbf0097aba92ca1b9cb3ffdd9c7470e156c8f9b5e028", size = 94963, upload-time = "2025-10-06T14:09:19.662Z" },
+ { url = "https://files.pythonhosted.org/packages/68/fe/2c1f674960c376e29cb0bec1249b117d11738db92a6ccc4a530b972648db/yarl-1.22.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ea66b1c11c9150f1372f69afb6b8116f2dd7286f38e14ea71a44eee9ec51b9d", size = 368406, upload-time = "2025-10-06T14:09:21.402Z" },
+ { url = "https://files.pythonhosted.org/packages/95/26/812a540e1c3c6418fec60e9bbd38e871eaba9545e94fa5eff8f4a8e28e1e/yarl-1.22.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3e2daa88dc91870215961e96a039ec73e4937da13cf77ce17f9cad0c18df3503", size = 336581, upload-time = "2025-10-06T14:09:22.98Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/f5/5777b19e26fdf98563985e481f8be3d8a39f8734147a6ebf459d0dab5a6b/yarl-1.22.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba440ae430c00eee41509353628600212112cd5018d5def7e9b05ea7ac34eb65", size = 388924, upload-time = "2025-10-06T14:09:24.655Z" },
+ { url = "https://files.pythonhosted.org/packages/86/08/24bd2477bd59c0bbd994fe1d93b126e0472e4e3df5a96a277b0a55309e89/yarl-1.22.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e6438cc8f23a9c1478633d216b16104a586b9761db62bfacb6425bac0a36679e", size = 392890, upload-time = "2025-10-06T14:09:26.617Z" },
+ { url = "https://files.pythonhosted.org/packages/46/00/71b90ed48e895667ecfb1eaab27c1523ee2fa217433ed77a73b13205ca4b/yarl-1.22.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c52a6e78aef5cf47a98ef8e934755abf53953379b7d53e68b15ff4420e6683d", size = 365819, upload-time = "2025-10-06T14:09:28.544Z" },
+ { url = "https://files.pythonhosted.org/packages/30/2d/f715501cae832651d3282387c6a9236cd26bd00d0ff1e404b3dc52447884/yarl-1.22.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3b06bcadaac49c70f4c88af4ffcfbe3dc155aab3163e75777818092478bcbbe7", size = 363601, upload-time = "2025-10-06T14:09:30.568Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/f9/a678c992d78e394e7126ee0b0e4e71bd2775e4334d00a9278c06a6cce96a/yarl-1.22.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:6944b2dc72c4d7f7052683487e3677456050ff77fcf5e6204e98caf785ad1967", size = 358072, upload-time = "2025-10-06T14:09:32.528Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/d1/b49454411a60edb6fefdcad4f8e6dbba7d8019e3a508a1c5836cba6d0781/yarl-1.22.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d5372ca1df0f91a86b047d1277c2aaf1edb32d78bbcefffc81b40ffd18f027ed", size = 385311, upload-time = "2025-10-06T14:09:34.634Z" },
+ { url = "https://files.pythonhosted.org/packages/87/e5/40d7a94debb8448c7771a916d1861d6609dddf7958dc381117e7ba36d9e8/yarl-1.22.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:51af598701f5299012b8416486b40fceef8c26fc87dc6d7d1f6fc30609ea0aa6", size = 381094, upload-time = "2025-10-06T14:09:36.268Z" },
+ { url = "https://files.pythonhosted.org/packages/35/d8/611cc282502381ad855448643e1ad0538957fc82ae83dfe7762c14069e14/yarl-1.22.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b266bd01fedeffeeac01a79ae181719ff848a5a13ce10075adbefc8f1daee70e", size = 370944, upload-time = "2025-10-06T14:09:37.872Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/df/fadd00fb1c90e1a5a8bd731fa3d3de2e165e5a3666a095b04e31b04d9cb6/yarl-1.22.0-cp311-cp311-win32.whl", hash = "sha256:a9b1ba5610a4e20f655258d5a1fdc7ebe3d837bb0e45b581398b99eb98b1f5ca", size = 81804, upload-time = "2025-10-06T14:09:39.359Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/f7/149bb6f45f267cb5c074ac40c01c6b3ea6d8a620d34b337f6321928a1b4d/yarl-1.22.0-cp311-cp311-win_amd64.whl", hash = "sha256:078278b9b0b11568937d9509b589ee83ef98ed6d561dfe2020e24a9fd08eaa2b", size = 86858, upload-time = "2025-10-06T14:09:41.068Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/13/88b78b93ad3f2f0b78e13bfaaa24d11cbc746e93fe76d8c06bf139615646/yarl-1.22.0-cp311-cp311-win_arm64.whl", hash = "sha256:b6a6f620cfe13ccec221fa312139135166e47ae169f8253f72a0abc0dae94376", size = 81637, upload-time = "2025-10-06T14:09:42.712Z" },
+ { url = "https://files.pythonhosted.org/packages/75/ff/46736024fee3429b80a165a732e38e5d5a238721e634ab41b040d49f8738/yarl-1.22.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f", size = 142000, upload-time = "2025-10-06T14:09:44.631Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/9a/b312ed670df903145598914770eb12de1bac44599549b3360acc96878df8/yarl-1.22.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2", size = 94338, upload-time = "2025-10-06T14:09:46.372Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/f5/0601483296f09c3c65e303d60c070a5c19fcdbc72daa061e96170785bc7d/yarl-1.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74", size = 94909, upload-time = "2025-10-06T14:09:48.648Z" },
+ { url = "https://files.pythonhosted.org/packages/60/41/9a1fe0b73dbcefce72e46cf149b0e0a67612d60bfc90fb59c2b2efdfbd86/yarl-1.22.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1651bf8e0398574646744c1885a41198eba53dc8a9312b954073f845c90a8df", size = 372940, upload-time = "2025-10-06T14:09:50.089Z" },
+ { url = "https://files.pythonhosted.org/packages/17/7a/795cb6dfee561961c30b800f0ed616b923a2ec6258b5def2a00bf8231334/yarl-1.22.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b8a0588521a26bf92a57a1705b77b8b59044cdceccac7151bd8d229e66b8dedb", size = 345825, upload-time = "2025-10-06T14:09:52.142Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/93/a58f4d596d2be2ae7bab1a5846c4d270b894958845753b2c606d666744d3/yarl-1.22.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42188e6a615c1a75bcaa6e150c3fe8f3e8680471a6b10150c5f7e83f47cc34d2", size = 386705, upload-time = "2025-10-06T14:09:54.128Z" },
+ { url = "https://files.pythonhosted.org/packages/61/92/682279d0e099d0e14d7fd2e176bd04f48de1484f56546a3e1313cd6c8e7c/yarl-1.22.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82", size = 396518, upload-time = "2025-10-06T14:09:55.762Z" },
+ { url = "https://files.pythonhosted.org/packages/db/0f/0d52c98b8a885aeda831224b78f3be7ec2e1aa4a62091f9f9188c3c65b56/yarl-1.22.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50678a3b71c751d58d7908edc96d332af328839eea883bb554a43f539101277a", size = 377267, upload-time = "2025-10-06T14:09:57.958Z" },
+ { url = "https://files.pythonhosted.org/packages/22/42/d2685e35908cbeaa6532c1fc73e89e7f2efb5d8a7df3959ea8e37177c5a3/yarl-1.22.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e8fbaa7cec507aa24ea27a01456e8dd4b6fab829059b69844bd348f2d467124", size = 365797, upload-time = "2025-10-06T14:09:59.527Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/83/cf8c7bcc6355631762f7d8bdab920ad09b82efa6b722999dfb05afa6cfac/yarl-1.22.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:433885ab5431bc3d3d4f2f9bd15bfa1614c522b0f1405d62c4f926ccd69d04fa", size = 365535, upload-time = "2025-10-06T14:10:01.139Z" },
+ { url = "https://files.pythonhosted.org/packages/25/e1/5302ff9b28f0c59cac913b91fe3f16c59a033887e57ce9ca5d41a3a94737/yarl-1.22.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7", size = 382324, upload-time = "2025-10-06T14:10:02.756Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/cd/4617eb60f032f19ae3a688dc990d8f0d89ee0ea378b61cac81ede3e52fae/yarl-1.22.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d", size = 383803, upload-time = "2025-10-06T14:10:04.552Z" },
+ { url = "https://files.pythonhosted.org/packages/59/65/afc6e62bb506a319ea67b694551dab4a7e6fb7bf604e9bd9f3e11d575fec/yarl-1.22.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520", size = 374220, upload-time = "2025-10-06T14:10:06.489Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/3d/68bf18d50dc674b942daec86a9ba922d3113d8399b0e52b9897530442da2/yarl-1.22.0-cp312-cp312-win32.whl", hash = "sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8", size = 81589, upload-time = "2025-10-06T14:10:09.254Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/9a/6ad1a9b37c2f72874f93e691b2e7ecb6137fb2b899983125db4204e47575/yarl-1.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c", size = 87213, upload-time = "2025-10-06T14:10:11.369Z" },
+ { url = "https://files.pythonhosted.org/packages/44/c5/c21b562d1680a77634d748e30c653c3ca918beb35555cff24986fff54598/yarl-1.22.0-cp312-cp312-win_arm64.whl", hash = "sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74", size = 81330, upload-time = "2025-10-06T14:10:13.112Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/f3/d67de7260456ee105dc1d162d43a019ecad6b91e2f51809d6cddaa56690e/yarl-1.22.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53", size = 139980, upload-time = "2025-10-06T14:10:14.601Z" },
+ { url = "https://files.pythonhosted.org/packages/01/88/04d98af0b47e0ef42597b9b28863b9060bb515524da0a65d5f4db160b2d5/yarl-1.22.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a", size = 93424, upload-time = "2025-10-06T14:10:16.115Z" },
+ { url = "https://files.pythonhosted.org/packages/18/91/3274b215fd8442a03975ce6bee5fe6aa57a8326b29b9d3d56234a1dca244/yarl-1.22.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c", size = 93821, upload-time = "2025-10-06T14:10:17.993Z" },
+ { url = "https://files.pythonhosted.org/packages/61/3a/caf4e25036db0f2da4ca22a353dfeb3c9d3c95d2761ebe9b14df8fc16eb0/yarl-1.22.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601", size = 373243, upload-time = "2025-10-06T14:10:19.44Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/9e/51a77ac7516e8e7803b06e01f74e78649c24ee1021eca3d6a739cb6ea49c/yarl-1.22.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a", size = 342361, upload-time = "2025-10-06T14:10:21.124Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/f8/33b92454789dde8407f156c00303e9a891f1f51a0330b0fad7c909f87692/yarl-1.22.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df", size = 387036, upload-time = "2025-10-06T14:10:22.902Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/9a/c5db84ea024f76838220280f732970aa4ee154015d7f5c1bfb60a267af6f/yarl-1.22.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2", size = 397671, upload-time = "2025-10-06T14:10:24.523Z" },
+ { url = "https://files.pythonhosted.org/packages/11/c9/cd8538dc2e7727095e0c1d867bad1e40c98f37763e6d995c1939f5fdc7b1/yarl-1.22.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b", size = 377059, upload-time = "2025-10-06T14:10:26.406Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/b9/ab437b261702ced75122ed78a876a6dec0a1b0f5e17a4ac7a9a2482d8abe/yarl-1.22.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273", size = 365356, upload-time = "2025-10-06T14:10:28.461Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/9d/8e1ae6d1d008a9567877b08f0ce4077a29974c04c062dabdb923ed98e6fe/yarl-1.22.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a", size = 361331, upload-time = "2025-10-06T14:10:30.541Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/5a/09b7be3905962f145b73beb468cdd53db8aa171cf18c80400a54c5b82846/yarl-1.22.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d", size = 382590, upload-time = "2025-10-06T14:10:33.352Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/7f/59ec509abf90eda5048b0bc3e2d7b5099dffdb3e6b127019895ab9d5ef44/yarl-1.22.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02", size = 385316, upload-time = "2025-10-06T14:10:35.034Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/84/891158426bc8036bfdfd862fabd0e0fa25df4176ec793e447f4b85cf1be4/yarl-1.22.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67", size = 374431, upload-time = "2025-10-06T14:10:37.76Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/49/03da1580665baa8bef5e8ed34c6df2c2aca0a2f28bf397ed238cc1bbc6f2/yarl-1.22.0-cp313-cp313-win32.whl", hash = "sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95", size = 81555, upload-time = "2025-10-06T14:10:39.649Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/ee/450914ae11b419eadd067c6183ae08381cfdfcb9798b90b2b713bbebddda/yarl-1.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d", size = 86965, upload-time = "2025-10-06T14:10:41.313Z" },
+ { url = "https://files.pythonhosted.org/packages/98/4d/264a01eae03b6cf629ad69bae94e3b0e5344741e929073678e84bf7a3e3b/yarl-1.22.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b", size = 81205, upload-time = "2025-10-06T14:10:43.167Z" },
+ { url = "https://files.pythonhosted.org/packages/88/fc/6908f062a2f77b5f9f6d69cecb1747260831ff206adcbc5b510aff88df91/yarl-1.22.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10", size = 146209, upload-time = "2025-10-06T14:10:44.643Z" },
+ { url = "https://files.pythonhosted.org/packages/65/47/76594ae8eab26210b4867be6f49129861ad33da1f1ebdf7051e98492bf62/yarl-1.22.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3", size = 95966, upload-time = "2025-10-06T14:10:46.554Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/ce/05e9828a49271ba6b5b038b15b3934e996980dd78abdfeb52a04cfb9467e/yarl-1.22.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9", size = 97312, upload-time = "2025-10-06T14:10:48.007Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/c5/7dffad5e4f2265b29c9d7ec869c369e4223166e4f9206fc2243ee9eea727/yarl-1.22.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f", size = 361967, upload-time = "2025-10-06T14:10:49.997Z" },
+ { url = "https://files.pythonhosted.org/packages/50/b2/375b933c93a54bff7fc041e1a6ad2c0f6f733ffb0c6e642ce56ee3b39970/yarl-1.22.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0", size = 323949, upload-time = "2025-10-06T14:10:52.004Z" },
+ { url = "https://files.pythonhosted.org/packages/66/50/bfc2a29a1d78644c5a7220ce2f304f38248dc94124a326794e677634b6cf/yarl-1.22.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e", size = 361818, upload-time = "2025-10-06T14:10:54.078Z" },
+ { url = "https://files.pythonhosted.org/packages/46/96/f3941a46af7d5d0f0498f86d71275696800ddcdd20426298e572b19b91ff/yarl-1.22.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708", size = 372626, upload-time = "2025-10-06T14:10:55.767Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/42/8b27c83bb875cd89448e42cd627e0fb971fa1675c9ec546393d18826cb50/yarl-1.22.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f", size = 341129, upload-time = "2025-10-06T14:10:57.985Z" },
+ { url = "https://files.pythonhosted.org/packages/49/36/99ca3122201b382a3cf7cc937b95235b0ac944f7e9f2d5331d50821ed352/yarl-1.22.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d", size = 346776, upload-time = "2025-10-06T14:10:59.633Z" },
+ { url = "https://files.pythonhosted.org/packages/85/b4/47328bf996acd01a4c16ef9dcd2f59c969f495073616586f78cd5f2efb99/yarl-1.22.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8", size = 334879, upload-time = "2025-10-06T14:11:01.454Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/ad/b77d7b3f14a4283bffb8e92c6026496f6de49751c2f97d4352242bba3990/yarl-1.22.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5", size = 350996, upload-time = "2025-10-06T14:11:03.452Z" },
+ { url = "https://files.pythonhosted.org/packages/81/c8/06e1d69295792ba54d556f06686cbd6a7ce39c22307100e3fb4a2c0b0a1d/yarl-1.22.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f", size = 356047, upload-time = "2025-10-06T14:11:05.115Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/b8/4c0e9e9f597074b208d18cef227d83aac36184bfbc6eab204ea55783dbc5/yarl-1.22.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62", size = 342947, upload-time = "2025-10-06T14:11:08.137Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/e5/11f140a58bf4c6ad7aca69a892bff0ee638c31bea4206748fc0df4ebcb3a/yarl-1.22.0-cp313-cp313t-win32.whl", hash = "sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03", size = 86943, upload-time = "2025-10-06T14:11:10.284Z" },
+ { url = "https://files.pythonhosted.org/packages/31/74/8b74bae38ed7fe6793d0c15a0c8207bbb819cf287788459e5ed230996cdd/yarl-1.22.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249", size = 93715, upload-time = "2025-10-06T14:11:11.739Z" },
+ { url = "https://files.pythonhosted.org/packages/69/66/991858aa4b5892d57aef7ee1ba6b4d01ec3b7eb3060795d34090a3ca3278/yarl-1.22.0-cp313-cp313t-win_arm64.whl", hash = "sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b", size = 83857, upload-time = "2025-10-06T14:11:13.586Z" },
+ { url = "https://files.pythonhosted.org/packages/46/b3/e20ef504049f1a1c54a814b4b9bed96d1ac0e0610c3b4da178f87209db05/yarl-1.22.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4", size = 140520, upload-time = "2025-10-06T14:11:15.465Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/04/3532d990fdbab02e5ede063676b5c4260e7f3abea2151099c2aa745acc4c/yarl-1.22.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683", size = 93504, upload-time = "2025-10-06T14:11:17.106Z" },
+ { url = "https://files.pythonhosted.org/packages/11/63/ff458113c5c2dac9a9719ac68ee7c947cb621432bcf28c9972b1c0e83938/yarl-1.22.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b", size = 94282, upload-time = "2025-10-06T14:11:19.064Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/bc/315a56aca762d44a6aaaf7ad253f04d996cb6b27bad34410f82d76ea8038/yarl-1.22.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e", size = 372080, upload-time = "2025-10-06T14:11:20.996Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/3f/08e9b826ec2e099ea6e7c69a61272f4f6da62cb5b1b63590bb80ca2e4a40/yarl-1.22.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590", size = 338696, upload-time = "2025-10-06T14:11:22.847Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/9f/90360108e3b32bd76789088e99538febfea24a102380ae73827f62073543/yarl-1.22.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2", size = 387121, upload-time = "2025-10-06T14:11:24.889Z" },
+ { url = "https://files.pythonhosted.org/packages/98/92/ab8d4657bd5b46a38094cfaea498f18bb70ce6b63508fd7e909bd1f93066/yarl-1.22.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da", size = 394080, upload-time = "2025-10-06T14:11:27.307Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/e7/d8c5a7752fef68205296201f8ec2bf718f5c805a7a7e9880576c67600658/yarl-1.22.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784", size = 372661, upload-time = "2025-10-06T14:11:29.387Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/2e/f4d26183c8db0bb82d491b072f3127fb8c381a6206a3a56332714b79b751/yarl-1.22.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b", size = 364645, upload-time = "2025-10-06T14:11:31.423Z" },
+ { url = "https://files.pythonhosted.org/packages/80/7c/428e5812e6b87cd00ee8e898328a62c95825bf37c7fa87f0b6bb2ad31304/yarl-1.22.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694", size = 355361, upload-time = "2025-10-06T14:11:33.055Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/2a/249405fd26776f8b13c067378ef4d7dd49c9098d1b6457cdd152a99e96a9/yarl-1.22.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d", size = 381451, upload-time = "2025-10-06T14:11:35.136Z" },
+ { url = "https://files.pythonhosted.org/packages/67/a8/fb6b1adbe98cf1e2dd9fad71003d3a63a1bc22459c6e15f5714eb9323b93/yarl-1.22.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd", size = 383814, upload-time = "2025-10-06T14:11:37.094Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/f9/3aa2c0e480fb73e872ae2814c43bc1e734740bb0d54e8cb2a95925f98131/yarl-1.22.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da", size = 370799, upload-time = "2025-10-06T14:11:38.83Z" },
+ { url = "https://files.pythonhosted.org/packages/50/3c/af9dba3b8b5eeb302f36f16f92791f3ea62e3f47763406abf6d5a4a3333b/yarl-1.22.0-cp314-cp314-win32.whl", hash = "sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2", size = 82990, upload-time = "2025-10-06T14:11:40.624Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/30/ac3a0c5bdc1d6efd1b41fa24d4897a4329b3b1e98de9449679dd327af4f0/yarl-1.22.0-cp314-cp314-win_amd64.whl", hash = "sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79", size = 88292, upload-time = "2025-10-06T14:11:42.578Z" },
+ { url = "https://files.pythonhosted.org/packages/df/0a/227ab4ff5b998a1b7410abc7b46c9b7a26b0ca9e86c34ba4b8d8bc7c63d5/yarl-1.22.0-cp314-cp314-win_arm64.whl", hash = "sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33", size = 82888, upload-time = "2025-10-06T14:11:44.863Z" },
+ { url = "https://files.pythonhosted.org/packages/06/5e/a15eb13db90abd87dfbefb9760c0f3f257ac42a5cac7e75dbc23bed97a9f/yarl-1.22.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1", size = 146223, upload-time = "2025-10-06T14:11:46.796Z" },
+ { url = "https://files.pythonhosted.org/packages/18/82/9665c61910d4d84f41a5bf6837597c89e665fa88aa4941080704645932a9/yarl-1.22.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca", size = 95981, upload-time = "2025-10-06T14:11:48.845Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/9a/2f65743589809af4d0a6d3aa749343c4b5f4c380cc24a8e94a3c6625a808/yarl-1.22.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53", size = 97303, upload-time = "2025-10-06T14:11:50.897Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/ab/5b13d3e157505c43c3b43b5a776cbf7b24a02bc4cccc40314771197e3508/yarl-1.22.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c", size = 361820, upload-time = "2025-10-06T14:11:52.549Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/76/242a5ef4677615cf95330cfc1b4610e78184400699bdda0acb897ef5e49a/yarl-1.22.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf", size = 323203, upload-time = "2025-10-06T14:11:54.225Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/96/475509110d3f0153b43d06164cf4195c64d16999e0c7e2d8a099adcd6907/yarl-1.22.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face", size = 363173, upload-time = "2025-10-06T14:11:56.069Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/66/59db471aecfbd559a1fd48aedd954435558cd98c7d0da8b03cc6c140a32c/yarl-1.22.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b", size = 373562, upload-time = "2025-10-06T14:11:58.783Z" },
+ { url = "https://files.pythonhosted.org/packages/03/1f/c5d94abc91557384719da10ff166b916107c1b45e4d0423a88457071dd88/yarl-1.22.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486", size = 339828, upload-time = "2025-10-06T14:12:00.686Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/97/aa6a143d3afba17b6465733681c70cf175af89f76ec8d9286e08437a7454/yarl-1.22.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138", size = 347551, upload-time = "2025-10-06T14:12:02.628Z" },
+ { url = "https://files.pythonhosted.org/packages/43/3c/45a2b6d80195959239a7b2a8810506d4eea5487dce61c2a3393e7fc3c52e/yarl-1.22.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a", size = 334512, upload-time = "2025-10-06T14:12:04.871Z" },
+ { url = "https://files.pythonhosted.org/packages/86/a0/c2ab48d74599c7c84cb104ebd799c5813de252bea0f360ffc29d270c2caa/yarl-1.22.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529", size = 352400, upload-time = "2025-10-06T14:12:06.624Z" },
+ { url = "https://files.pythonhosted.org/packages/32/75/f8919b2eafc929567d3d8411f72bdb1a2109c01caaab4ebfa5f8ffadc15b/yarl-1.22.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093", size = 357140, upload-time = "2025-10-06T14:12:08.362Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/72/6a85bba382f22cf78add705d8c3731748397d986e197e53ecc7835e76de7/yarl-1.22.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c", size = 341473, upload-time = "2025-10-06T14:12:10.994Z" },
+ { url = "https://files.pythonhosted.org/packages/35/18/55e6011f7c044dc80b98893060773cefcfdbf60dfefb8cb2f58b9bacbd83/yarl-1.22.0-cp314-cp314t-win32.whl", hash = "sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e", size = 89056, upload-time = "2025-10-06T14:12:13.317Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/86/0f0dccb6e59a9e7f122c5afd43568b1d31b8ab7dda5f1b01fb5c7025c9a9/yarl-1.22.0-cp314-cp314t-win_amd64.whl", hash = "sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27", size = 96292, upload-time = "2025-10-06T14:12:15.398Z" },
+ { url = "https://files.pythonhosted.org/packages/48/b7/503c98092fb3b344a179579f55814b613c1fbb1c23b3ec14a7b008a66a6e/yarl-1.22.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1", size = 85171, upload-time = "2025-10-06T14:12:16.935Z" },
+ { url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814, upload-time = "2025-10-06T14:12:53.872Z" },
+]
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/.github/workflows/python-tests.yml b/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/.github/workflows/python-tests.yml
new file mode 100644
index 00000000..c6b8e46b
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/.github/workflows/python-tests.yml
@@ -0,0 +1,19 @@
+name: Python Tests
+
+on:
+ push:
+ branches:
+ - main
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - uses: actions/setup-python@v4
+ with:
+ python-version: '3.10'
+ - run: |
+ python -m pip install --upgrade pip
+ python -m pip install .
+ - run: python -m unittest discover -v
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/.gitignore b/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/.gitignore
new file mode 100644
index 00000000..76118a5c
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/.gitignore
@@ -0,0 +1,13 @@
+# Python-generated files
+__pycache__/
+*.py[oc]
+build/
+dist/
+wheels/
+*.egg-info
+
+# Virtual environments
+.venv
+
+# PyCharm
+.idea/
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/.python-version b/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/.python-version
new file mode 100644
index 00000000..24ee5b1b
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/.python-version
@@ -0,0 +1 @@
+3.13
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/CLAUDE.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/CLAUDE.md
new file mode 100644
index 00000000..3d3b6da0
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/CLAUDE.md
@@ -0,0 +1,112 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## Project Overview
+
+This is a secure Model Context Protocol (MCP) server implementation that provides controlled command-line execution capabilities. The server enables LLM applications to execute CLI commands with comprehensive security features including command whitelisting, path validation, and shell operator protection.
+
+## Development Commands
+
+### Testing
+```bash
+# Run all tests
+python -m pytest tests/
+
+# Run specific test file
+python -m unittest tests.test_cli_mcp_server
+
+# Run tests with verbose output
+python -m unittest -v tests.test_cli_mcp_server
+```
+
+### Building and Publishing
+```bash
+# Sync dependencies and update lockfile
+uv sync
+
+# Build package distributions
+uv build
+
+# Publish to PyPI (requires API token)
+uv publish --token {YOUR_PYPI_API_TOKEN}
+```
+
+### Running the Server
+```bash
+# Run locally for development
+uv run cli-mcp-server
+
+# Run with MCP Inspector for debugging
+npx @modelcontextprotocol/inspector uv --directory . run cli-mcp-server
+```
+
+## Architecture
+
+### Core Components
+
+- **`server.py`**: Main MCP server implementation containing:
+ - `CommandExecutor`: Secure command execution engine with path validation and security controls
+ - `SecurityConfig`: Configuration dataclass for security policies
+ - Custom exception classes: `CommandSecurityError`, `CommandExecutionError`, `CommandTimeoutError`
+ - Two MCP tools: `run_command` and `show_security_rules`
+
+- **`__init__.py`**: Package entry point that exposes the `main()` function
+
+### Security Architecture
+
+The server implements multiple security layers:
+
+1. **Command Whitelisting**: Only allowed commands can be executed (configurable via `ALLOWED_COMMANDS`)
+2. **Flag Validation**: Command flags must be in the allowed list (configurable via `ALLOWED_FLAGS`)
+3. **Path Normalization**: All file paths are validated to prevent directory traversal attacks
+4. **Shell Operator Control**: Shell operators (&&, ||, |, >, etc.) are disabled by default but can be enabled
+5. **Execution Limits**: Configurable timeouts and command length limits
+6. **Working Directory Restriction**: Commands execute only within the specified allowed directory
+
+### Environment Configuration
+
+Required:
+- `ALLOWED_DIR`: Base directory for command execution (must exist)
+
+Optional (with defaults):
+- `ALLOWED_COMMANDS`: Comma-separated commands or "all" (default: "ls,cat,pwd")
+- `ALLOWED_FLAGS`: Comma-separated flags or "all" (default: "-l,-a,--help")
+- `MAX_COMMAND_LENGTH`: Maximum command string length (default: 1024)
+- `COMMAND_TIMEOUT`: Execution timeout in seconds (default: 30)
+- `ALLOW_SHELL_OPERATORS`: Enable shell operators like &&, ||, | (default: false)
+
+**Output Control:**
+- `MAX_OUTPUT_LENGTH`: Maximum total output length (default: 10240)
+- `MAX_STDOUT_LENGTH`: Maximum stdout length (default: 8192)
+- `MAX_STDERR_LENGTH`: Maximum stderr length (default: 2048)
+- `OUTPUT_TRUNCATE_MESSAGE`: Message shown when output is truncated (default: "...[output truncated]")
+
+**Proxy Support:**
+- `CLI_PROXY_ENABLED`: Enable proxy support (default: false)
+- `CLI_PROXY_URL`: Proxy URL (also checks HTTP_PROXY if not set)
+
+Example proxy configuration:
+```bash
+CLI_PROXY_ENABLED=true
+CLI_PROXY_URL=http://proxy.company.com:8080
+```
+
+## Testing Strategy
+
+The test suite in `tests/test_cli_mcp_server.py` covers:
+
+- Basic command execution (pwd, ls)
+- Security validation (shell operator blocking)
+- Shell operator functionality when enabled
+- Path handling and file operations
+- Network operations (curl test when available)
+
+Tests use a temporary directory and reload the server module to test different configurations.
+
+## Package Management
+
+- Uses `uv` for dependency management and building
+- Python 3.10+ required
+- Single dependency: `mcp>=1.10.1`
+- Entry point defined in `pyproject.toml` as `cli-mcp-server = "cli_mcp_server:main"`
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/LICENSE b/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/LICENSE
new file mode 100644
index 00000000..0d26613a
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024 Mladen
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/README.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/README.md
new file mode 100644
index 00000000..e929c66d
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/README.md
@@ -0,0 +1,318 @@
+# CLI MCP Server
+
+---
+
+A secure Model Context Protocol (MCP) server implementation for executing controlled command-line operations with
+comprehensive security features.
+
+
+
+
+[](https://smithery.ai/protocol/cli-mcp-server)
+[](https://github.com/MladenSU/cli-mcp-server/actions/workflows/python-tests.yml)
+
+
+
+---
+
+# Table of Contents
+
+1. [Overview](#overview)
+2. [Features](#features)
+3. [Configuration](#configuration)
+4. [Available Tools](#available-tools)
+ - [run_command](#run_command)
+ - [show_security_rules](#show_security_rules)
+5. [Usage with Claude Desktop](#usage-with-claude-desktop)
+ - [Development/Unpublished Servers Configuration](#developmentunpublished-servers-configuration)
+ - [Published Servers Configuration](#published-servers-configuration)
+6. [Security Features](#security-features)
+7. [Error Handling](#error-handling)
+8. [Development](#development)
+ - [Prerequisites](#prerequisites)
+ - [Building and Publishing](#building-and-publishing)
+ - [Debugging](#debugging)
+9. [License](#license)
+
+---
+
+## Overview
+
+This MCP server enables secure command-line execution with robust security measures including command whitelisting, path
+validation, and execution controls. Perfect for providing controlled CLI access to LLM applications while maintaining security.
+
+## Features
+
+- 🔒 Secure command execution with strict validation
+- ⚙️ Configurable command and flag whitelisting with 'all' option
+- 🛡️ Path traversal prevention and validation
+- 🚫 Shell operator injection protection
+- ⏱️ Execution timeouts and length limits
+- 📝 Detailed error reporting
+- 🔄 Async operation support
+- 🎯 Working directory restriction and validation
+- 🌐 HTTP/HTTPS proxy support for network commands
+- 📏 Configurable output length limits with truncation
+- 🔧 Robust configuration with error handling and fallbacks
+
+## Configuration
+
+Configure the server using environment variables:
+
+### Basic Security Configuration
+
+| Variable | Description | Default |
+|---------------------|------------------------------------------------------|-------------------|
+| `ALLOWED_DIR` | Base directory for command execution (Required) | None (Required) |
+| `ALLOWED_COMMANDS` | Comma-separated list of allowed commands or 'all' | `ls,cat,pwd` |
+| `ALLOWED_FLAGS` | Comma-separated list of allowed flags or 'all' | `-l,-a,--help` |
+| `MAX_COMMAND_LENGTH`| Maximum command string length | `1024` |
+| `COMMAND_TIMEOUT` | Command execution timeout (seconds) | `30` |
+| `ALLOW_SHELL_OPERATORS` | Allow shell operators (&&, \|\|, \|, >, etc.) | `false` |
+
+### Output Control Configuration
+
+| Variable | Description | Default |
+|-------------------------|---------------------------------------------------|------------------------|
+| `MAX_OUTPUT_LENGTH` | Maximum total output length (characters) | `10240` |
+| `MAX_STDOUT_LENGTH` | Maximum stdout length (characters) | `8192` |
+| `MAX_STDERR_LENGTH` | Maximum stderr length (characters) | `2048` |
+| `OUTPUT_TRUNCATE_MESSAGE` | Message shown when output is truncated | `...[output truncated]` |
+
+### Proxy Configuration
+
+| Variable | Description | Default |
+|---------------------|------------------------------------------------------|-------------------|
+| `CLI_PROXY_ENABLED` | Enable proxy support for HTTP/HTTPS requests | `false` |
+| `CLI_PROXY_URL` | Proxy URL (also checks HTTP_PROXY if not set) | None |
+
+**Proxy Usage Examples:**
+```bash
+# Enable proxy with custom URL
+CLI_PROXY_ENABLED=true
+CLI_PROXY_URL=http://proxy.company.com:8080
+
+# Or use standard HTTP_PROXY environment variable
+CLI_PROXY_ENABLED=true
+HTTP_PROXY=http://proxy.company.com:8080
+```
+
+**Output Control Examples:**
+```bash
+# Limit stdout to 4KB and stderr to 1KB
+MAX_STDOUT_LENGTH=4096
+MAX_STDERR_LENGTH=1024
+OUTPUT_TRUNCATE_MESSAGE="... [Output truncated due to length limit]"
+
+# Disable output limiting (set to very large values)
+MAX_STDOUT_LENGTH=1000000
+MAX_STDERR_LENGTH=1000000
+```
+
+Note: Setting `ALLOWED_COMMANDS` or `ALLOWED_FLAGS` to 'all' will allow any command or flag respectively.
+
+## Common Use Cases
+
+### Corporate Environment with Proxy
+```bash
+# Enable proxy for all HTTP/HTTPS commands like curl, wget
+CLI_PROXY_ENABLED=true
+CLI_PROXY_URL=http://proxy.corporate.com:8080
+ALLOWED_COMMANDS=ls,cat,pwd,curl,wget
+ALLOWED_FLAGS=all
+```
+
+### Limited Output for Performance
+```bash
+# Prevent memory issues with large command outputs
+MAX_STDOUT_LENGTH=4096
+MAX_STDERR_LENGTH=1024
+OUTPUT_TRUNCATE_MESSAGE="... [Output limited for performance]"
+```
+
+### Development Environment
+```bash
+# Allow most commands but limit output size
+ALLOWED_COMMANDS=all
+ALLOWED_FLAGS=all
+ALLOW_SHELL_OPERATORS=true
+MAX_STDOUT_LENGTH=16384
+CLI_PROXY_ENABLED=true
+```
+
+## Installation
+
+To install CLI MCP Server for Claude Desktop automatically via [Smithery](https://smithery.ai/protocol/cli-mcp-server):
+
+```bash
+npx @smithery/cli install cli-mcp-server --client claude
+```
+
+## Available Tools
+
+### run_command
+
+Executes whitelisted CLI commands within allowed directories.
+
+**Input Schema:**
+```json
+{
+ "command": {
+ "type": "string",
+ "description": "Single command to execute (e.g., 'ls -l' or 'cat file.txt')"
+ }
+}
+```
+
+**Security Notes:**
+- Shell operators (&&, |, >, >>) are not supported by default, but can be enabled with `ALLOW_SHELL_OPERATORS=true`
+- Commands must be whitelisted unless ALLOWED_COMMANDS='all'
+- Flags must be whitelisted unless ALLOWED_FLAGS='all'
+- All paths are validated to be within ALLOWED_DIR
+
+### show_security_rules
+
+Displays current security configuration and restrictions, including:
+- Working directory
+- Allowed commands
+- Allowed flags
+- Security limits (max command length and timeout)
+- Output length limits
+- Proxy configuration status
+
+## Usage with Claude Desktop
+
+Add to your `~/Library/Application\ Support/Claude/claude_desktop_config.json`:
+
+> Development/Unpublished Servers Configuration
+
+```json
+{
+ "mcpServers": {
+ "cli-mcp-server": {
+ "command": "uv",
+ "args": [
+ "--directory",
+ "/cli-mcp-server",
+ "run",
+ "cli-mcp-server"
+ ],
+ "env": {
+ "ALLOWED_DIR": "",
+ "ALLOWED_COMMANDS": "ls,cat,pwd,echo,curl",
+ "ALLOWED_FLAGS": "-l,-a,--help,--version,-s,-G",
+ "MAX_COMMAND_LENGTH": "1024",
+ "COMMAND_TIMEOUT": "30",
+ "ALLOW_SHELL_OPERATORS": "false",
+ "MAX_STDOUT_LENGTH": "8192",
+ "MAX_STDERR_LENGTH": "2048",
+ "CLI_PROXY_ENABLED": "false",
+ "CLI_PROXY_URL": "http://proxy.company.com:8080",
+ "OUTPUT_TRUNCATE_MESSAGE": "...[output truncated]"
+ }
+ }
+ }
+}
+```
+
+> Published Servers Configuration
+
+```json
+{
+ "mcpServers": {
+ "cli-mcp-server": {
+ "command": "uvx",
+ "args": [
+ "cli-mcp-server"
+ ],
+ "env": {
+ "ALLOWED_DIR": "",
+ "ALLOWED_COMMANDS": "ls,cat,pwd,echo,curl",
+ "ALLOWED_FLAGS": "-l,-a,--help,--version,-s,-G",
+ "MAX_COMMAND_LENGTH": "1024",
+ "COMMAND_TIMEOUT": "30",
+ "ALLOW_SHELL_OPERATORS": "false",
+ "MAX_STDOUT_LENGTH": "8192",
+ "MAX_STDERR_LENGTH": "2048",
+ "CLI_PROXY_ENABLED": "false"
+ }
+ }
+ }
+}
+```
+> In case it's not working or showing in the UI, clear your cache via `uv clean`.
+
+## Security Features
+
+- ✅ Command whitelist enforcement with 'all' option
+- ✅ Flag validation with 'all' option
+- ✅ Path traversal prevention and normalization
+- ✅ Shell operator blocking (with opt-in support via `ALLOW_SHELL_OPERATORS=true`)
+- ✅ Command length limits
+- ✅ Execution timeouts
+- ✅ Working directory restrictions
+- ✅ Symlink resolution and validation
+- ✅ Output length limiting with configurable truncation
+- ✅ Robust environment variable validation with fallbacks
+- ✅ Proxy support with secure environment variable handling
+
+## Error Handling
+
+The server provides detailed error messages for:
+
+- Security violations (CommandSecurityError)
+- Command timeouts (CommandTimeoutError)
+- Invalid command formats
+- Path security violations
+- Execution failures (CommandExecutionError)
+- General command errors (CommandError)
+
+## Development
+
+### Prerequisites
+
+- Python 3.10+
+- MCP protocol library
+
+### Building and Publishing
+
+To prepare the package for distribution:
+
+1. Sync dependencies and update lockfile:
+ ```bash
+ uv sync
+ ```
+
+2. Build package distributions:
+ ```bash
+ uv build
+ ```
+
+ > This will create source and wheel distributions in the `dist/` directory.
+
+3. Publish to PyPI:
+ ```bash
+ uv publish --token {{YOUR_PYPI_API_TOKEN}}
+ ```
+
+### Debugging
+
+Since MCP servers run over stdio, debugging can be challenging. For the best debugging
+experience, we strongly recommend using the [MCP Inspector](https://github.com/modelcontextprotocol/inspector).
+
+You can launch the MCP Inspector via [`npm`](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) with
+this command:
+
+```bash
+npx @modelcontextprotocol/inspector uv --directory {{your source code local directory}}/cli-mcp-server run cli-mcp-server
+```
+
+Upon launching, the Inspector will display a URL that you can access in your browser to begin debugging.
+
+## License
+
+This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
+
+---
+
+For more information or support, please open an issue on the project repository.
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/glama.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/glama.json
new file mode 100644
index 00000000..61649d65
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/glama.json
@@ -0,0 +1,6 @@
+{
+ "$schema": "https://glama.ai/mcp/schemas/server.json",
+ "maintainers": [
+ "MladenSU"
+ ]
+}
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/pyproject.toml b/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/pyproject.toml
new file mode 100644
index 00000000..8c297021
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/pyproject.toml
@@ -0,0 +1,23 @@
+[project]
+name = "cli-mcp-server"
+version = "0.2.5"
+description = "Command line interface for MCP clients with secure execution and customizable security policies"
+readme = "README.md"
+requires-python = ">=3.10"
+dependencies = ["mcp>=1.10.1"]
+authors = [
+ { name = "Mladen", email = "fangs-lever6n@icloud.com" },
+]
+
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[project.scripts]
+cli-mcp-server = "cli_mcp_server:main"
+
+[project.urls]
+Homepage = "https://github.com/MladenSU/cli-mcp-server"
+Documentation = "https://github.com/MladenSU/cli-mcp-server#readme"
+Repository = "https://github.com/MladenSU/cli-mcp-server.git"
+"Bug Tracker" = "https://github.com/MladenSU/cli-mcp-server/issues"
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/src/cli_mcp_server/__init__.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/src/cli_mcp_server/__init__.py
new file mode 100644
index 00000000..04b915eb
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/src/cli_mcp_server/__init__.py
@@ -0,0 +1,12 @@
+import asyncio
+
+from . import server
+
+
+def main():
+ """Main entry point for the package."""
+ asyncio.run(server.main())
+
+
+# Optionally expose other important items at package level
+__all__ = ["main", "server"]
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/src/cli_mcp_server/server.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/src/cli_mcp_server/server.py
new file mode 100644
index 00000000..38a219d1
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/src/cli_mcp_server/server.py
@@ -0,0 +1,690 @@
+import os
+import re
+import shlex
+import subprocess
+from dataclasses import dataclass
+from typing import List, Dict, Any, Optional
+
+import mcp.server.stdio
+import mcp.types as types
+from mcp.server import NotificationOptions, Server
+from mcp.server.models import InitializationOptions
+
+server = Server("cli-mcp-server")
+
+
+class CommandError(Exception):
+ """Base exception for command-related errors"""
+
+ pass
+
+
+class CommandSecurityError(CommandError):
+ """Security violation errors"""
+
+ pass
+
+
+class CommandExecutionError(CommandError):
+ """Command execution errors"""
+
+ pass
+
+
+class CommandTimeoutError(CommandError):
+ """Command timeout errors"""
+
+ pass
+
+
+@dataclass
+class SecurityConfig:
+ """
+ Security configuration for command execution
+ """
+
+ allowed_commands: set[str]
+ allowed_flags: set[str]
+ max_command_length: int
+ command_timeout: int
+ allow_all_commands: bool = False
+ allow_all_flags: bool = False
+ allow_shell_operators: bool = False
+ max_output_length: int = 10240
+ max_stdout_length: int = 8192
+ max_stderr_length: int = 2048
+ proxy_url: Optional[str] = None
+ proxy_enabled: bool = False
+ truncate_message: str = "...[output truncated]"
+
+
+class CommandExecutor:
+ def __init__(self, allowed_dir: str, security_config: SecurityConfig):
+ if not allowed_dir or not os.path.exists(allowed_dir):
+ raise ValueError("Valid ALLOWED_DIR is required")
+ self.allowed_dir = os.path.abspath(os.path.realpath(allowed_dir))
+ self.security_config = security_config
+
+ def _truncate_output(self, text: str, max_length: int) -> str:
+ """
+ Truncates output text if it exceeds the maximum length.
+
+ Args:
+ text (str): The text to potentially truncate
+ max_length (int): Maximum allowed length
+
+ Returns:
+ str: Original text if within limit, or truncated text with message
+ """
+ if not text or len(text) <= max_length:
+ return text
+ return text[:max_length] + self.security_config.truncate_message
+
+ def _normalize_path(self, path: str) -> str:
+ """
+ Normalizes a path and ensures it's within allowed directory.
+ """
+ try:
+ if os.path.isabs(path):
+ # If absolute path, check directly
+ real_path = os.path.abspath(os.path.realpath(path))
+ else:
+ # If relative path, combine with allowed_dir first
+ real_path = os.path.abspath(
+ os.path.realpath(os.path.join(self.allowed_dir, path))
+ )
+
+ if not self._is_path_safe(real_path):
+ raise CommandSecurityError(
+ f"Path '{path}' is outside of allowed directory: {self.allowed_dir}"
+ )
+
+ return real_path
+ except CommandSecurityError:
+ raise
+ except Exception as e:
+ raise CommandSecurityError(f"Invalid path '{path}': {str(e)}")
+
+ def validate_command(self, command_string: str) -> tuple[str, List[str]]:
+ """
+ Validates and parses a command string for security and formatting.
+
+ Checks if the command string contains shell operators. If it does, splits the command
+ by operators and validates each part individually. If all parts are valid, returns
+ the original command string to be executed with shell=True.
+
+ For commands without shell operators, splits into command and arguments and validates
+ each part according to security rules.
+
+ Args:
+ command_string (str): The command string to validate and parse.
+
+ Returns:
+ tuple[str, List[str]]: A tuple containing:
+ - For regular commands: The command name (str) and list of arguments (List[str])
+ - For commands with shell operators: The full command string and empty args list
+
+ Raises:
+ CommandSecurityError: If any part of the command fails security validation.
+ """
+
+ # Define shell operators
+ shell_operators = ["&&", "||", "|", ">", ">>", "<", "<<", ";"]
+
+ # Check if command contains shell operators
+ contains_shell_operator = any(
+ operator in command_string for operator in shell_operators
+ )
+
+ if contains_shell_operator:
+ # Check if shell operators are allowed
+ if not self.security_config.allow_shell_operators:
+ # If shell operators are not allowed, raise an error
+ for operator in shell_operators:
+ if operator in command_string:
+ raise CommandSecurityError(
+ f"Shell operator '{operator}' is not supported. Set ALLOW_SHELL_OPERATORS=true to enable."
+ )
+
+ # Split the command by shell operators and validate each part
+ return self._validate_command_with_operators(
+ command_string, shell_operators
+ )
+
+ # Process single command without shell operators
+ return self._validate_single_command(command_string)
+
+ def _is_url_path(self, path: str) -> bool:
+ """
+ Checks if a given path is a URL of type http or https.
+
+ Args:
+ path (str): The path to check.
+
+ Returns:
+ bool: True if the path is a URL, False otherwise.
+ """
+ url_pattern = re.compile(r"^https?://")
+ return bool(url_pattern.match(path))
+
+ def _is_path_safe(self, path: str) -> bool:
+ """
+ Checks if a given path is safe to access within allowed directory boundaries.
+
+ Validates that the absolute resolved path is within the allowed directory
+ to prevent directory traversal attacks.
+
+ Args:
+ path (str): The path to validate.
+
+ Returns:
+ bool: True if path is within allowed directory, False otherwise.
+ Returns False if path resolution fails for any reason.
+
+ Private method intended for internal use only.
+ """
+ try:
+ # Resolve any symlinks and get absolute path
+ real_path = os.path.abspath(os.path.realpath(path))
+ allowed_dir_real = os.path.abspath(os.path.realpath(self.allowed_dir))
+
+ # Check if the path starts with allowed_dir
+ return real_path.startswith(allowed_dir_real)
+ except Exception:
+ return False
+
+ def _validate_single_command(self, command_string: str) -> tuple[str, List[str]]:
+ """
+ Validates a single command without shell operators.
+
+ Args:
+ command_string (str): The command string to validate.
+
+ Returns:
+ tuple[str, List[str]]: A tuple containing the command and validated arguments.
+
+ Raises:
+ CommandSecurityError: If the command fails validation.
+ """
+ try:
+ parts = shlex.split(command_string)
+ if not parts:
+ raise CommandSecurityError("Empty command")
+
+ command, args = parts[0], parts[1:]
+
+ # Validate command if not in allow-all mode
+ if (
+ not self.security_config.allow_all_commands
+ and command not in self.security_config.allowed_commands
+ ):
+ raise CommandSecurityError(f"Command '{command}' is not allowed")
+
+ # Process and validate arguments
+ validated_args = []
+ for arg in args:
+ is_explicit_path = (arg.startswith(("./", "../", "/")) and not arg.startswith("//")) or arg == "."
+
+ if arg.startswith("-"):
+ if (
+ not self.security_config.allow_all_flags
+ and arg not in self.security_config.allowed_flags
+ ):
+ raise CommandSecurityError(f"Flag '{arg}' is not allowed")
+ validated_args.append(arg)
+ continue
+ # For any path-like argument, validate it
+ if is_explicit_path or ("/" in arg and os.path.exists(os.path.join(self.allowed_dir, arg))):
+ if self._is_url_path(arg):
+ # If it's a URL, we don't need to normalize it
+ validated_args.append(arg)
+ continue
+
+ normalized_path = self._normalize_path(arg)
+ validated_args.append(normalized_path)
+ else:
+ # For non-path arguments, add them as-is
+ validated_args.append(arg)
+
+ return command, validated_args
+
+ except ValueError as e:
+ raise CommandSecurityError(f"Invalid command format: {str(e)}")
+
+ def _validate_command_with_operators(
+ self, command_string: str, shell_operators: List[str]
+ ) -> tuple[str, List[str]]:
+ """
+ Validates a command string that contains shell operators.
+
+ FIXED VERSION: Properly handles redirection operators by understanding shell syntax context.
+ Filenames after redirection operators (>, >>, <, <<) are not validated as commands.
+
+ Args:
+ command_string (str): The command string containing shell operators.
+ shell_operators (List[str]): List of shell operators to split by.
+
+ Returns:
+ tuple[str, List[str]]: A tuple containing the command and empty args list
+ (since the command will be executed with shell=True)
+
+ Raises:
+ CommandSecurityError: If any part of the command fails validation.
+ """
+ # Define redirection operators that take filenames as arguments
+ redirection_operators = [">", ">>", "<", "<<"]
+ command_separators = ["&&", "||", "|", ";"]
+
+ # Create a regex pattern to split by any of the shell operators
+ # We need to escape special regex characters in the operators
+ escaped_operators = [re.escape(op) for op in shell_operators]
+ pattern = "|".join(escaped_operators)
+
+ # Split the command string by shell operators, keeping the operators
+ parts = re.split(f"({pattern})", command_string)
+
+ # Filter out empty parts and whitespace-only parts
+ parts = [part.strip() for part in parts if part.strip()]
+
+ # Parse commands with context awareness
+ i = 0
+ while i < len(parts):
+ current_part = parts[i]
+
+ # Skip if this part is an operator
+ if current_part in shell_operators:
+ i += 1
+ continue
+
+ # Check if this part should be treated as a command or a filename
+ is_filename_context = False
+
+ # Look at the previous operator to determine context
+ if i > 0:
+ prev_operator = parts[i - 1]
+ if prev_operator in redirection_operators:
+ # This part is a filename after a redirection operator
+ is_filename_context = True
+
+ # Validate only if this is a command, not a filename
+ if not is_filename_context:
+ try:
+ # This should be a command - validate it
+ self._validate_single_command(current_part)
+ except CommandSecurityError as e:
+ raise CommandSecurityError(f"Invalid command part '{current_part}': {str(e)}")
+ except ValueError as e:
+ raise CommandSecurityError(
+ f"Invalid command format in '{current_part}': {str(e)}"
+ )
+ else:
+ # This is a filename after redirection - just check if it's a safe path
+ if not self._is_url_path(current_part):
+ try:
+ # Validate that the filename path is safe (within allowed directory)
+ self._normalize_path(current_part)
+ except CommandSecurityError as e:
+ raise CommandSecurityError(f"Invalid file path '{current_part}': {str(e)}")
+
+ i += 1
+
+ # If we get here, all parts passed validation
+ # Return the original command string to be executed with shell=True
+ return command_string, []
+
+ def execute(self, command_string: str) -> subprocess.CompletedProcess:
+ """
+ Executes a command string in a secure, controlled environment.
+
+ Runs the command after validating it against security constraints including length limits
+ and shell operator restrictions. Executes with controlled parameters for safety.
+
+ Args:
+ command_string (str): The command string to execute.
+
+ Returns:
+ subprocess.CompletedProcess: The result of the command execution containing
+ stdout, stderr, and return code.
+
+ Raises:
+ CommandSecurityError: If the command:
+ - Exceeds maximum length
+ - Fails security validation
+ - Fails during execution
+
+ Notes:
+ - Uses shell=True for commands with shell operators, shell=False otherwise
+ - Uses timeout and working directory constraints
+ - Captures both stdout and stderr
+ """
+ if len(command_string) > self.security_config.max_command_length:
+ raise CommandSecurityError(
+ f"Command exceeds maximum length of {self.security_config.max_command_length}"
+ )
+
+ try:
+ command, args = self.validate_command(command_string)
+
+ # Prepare environment variables for proxy support
+ env = os.environ.copy()
+ if self.security_config.proxy_enabled and self.security_config.proxy_url:
+ env.update({
+ 'HTTP_PROXY': self.security_config.proxy_url,
+ 'HTTPS_PROXY': self.security_config.proxy_url,
+ 'http_proxy': self.security_config.proxy_url,
+ 'https_proxy': self.security_config.proxy_url,
+ })
+
+ # Check if this is a command with shell operators
+ shell_operators = ["&&", "||", "|", ">", ">>", "<", "<<", ";"]
+ use_shell = any(operator in command_string for operator in shell_operators)
+
+ # Double-check that shell operators are allowed if they are present
+ if use_shell and not self.security_config.allow_shell_operators:
+ for operator in shell_operators:
+ if operator in command_string:
+ raise CommandSecurityError(
+ f"Shell operator '{operator}' is not supported. Set ALLOW_SHELL_OPERATORS=true to enable."
+ )
+
+ if use_shell:
+ # For commands with shell operators, execute with shell=True
+ return subprocess.run(
+ command, # command is the full command string in this case
+ shell=True,
+ text=True,
+ capture_output=True,
+ timeout=self.security_config.command_timeout,
+ cwd=self.allowed_dir,
+ env=env,
+ )
+ else:
+ # For regular commands, execute with shell=False
+ return subprocess.run(
+ [command] + args,
+ shell=False,
+ text=True,
+ capture_output=True,
+ timeout=self.security_config.command_timeout,
+ cwd=self.allowed_dir,
+ env=env,
+ )
+ except subprocess.TimeoutExpired:
+ raise CommandTimeoutError(
+ f"Command timed out after {self.security_config.command_timeout} seconds"
+ )
+ except CommandError:
+ raise
+ except Exception as e:
+ raise CommandExecutionError(f"Command execution failed: {str(e)}")
+
+
+# Load security configuration from environment
+def load_security_config() -> SecurityConfig:
+ """
+ Loads security configuration from environment variables with default fallbacks.
+
+ Creates a SecurityConfig instance using environment variables to configure allowed
+ commands, flags, patterns, and execution constraints. Uses predefined defaults if
+ environment variables are not set.
+
+ Returns:
+ SecurityConfig: Configuration object containing:
+ - allowed_commands: Set of permitted command names
+ - allowed_flags: Set of permitted command flags/options
+ - max_command_length: Maximum length of command string
+ - command_timeout: Maximum execution time in seconds
+ - allow_all_commands: Whether all commands are allowed
+ - allow_all_flags: Whether all flags are allowed
+ - allow_shell_operators: Whether shell operators (&&, ||, |, etc.) are allowed
+ - max_output_length: Maximum total output length
+ - max_stdout_length: Maximum stdout length
+ - max_stderr_length: Maximum stderr length
+ - proxy_url: Proxy URL for HTTP/HTTPS requests
+ - proxy_enabled: Whether proxy is enabled
+ - truncate_message: Message shown when output is truncated
+
+ Environment Variables:
+ ALLOWED_COMMANDS: Comma-separated list of allowed commands or 'all' (default: "ls,cat,pwd")
+ ALLOWED_FLAGS: Comma-separated list of allowed flags or 'all' (default: "-l,-a,--help")
+ MAX_COMMAND_LENGTH: Maximum command string length (default: 1024)
+ COMMAND_TIMEOUT: Command timeout in seconds (default: 30)
+ ALLOW_SHELL_OPERATORS: Whether to allow shell operators like &&, ||, |, >, etc. (default: false)
+ Set to "true" or "1" to enable, any other value to disable.
+ MAX_OUTPUT_LENGTH: Maximum total output length (default: 10240)
+ MAX_STDOUT_LENGTH: Maximum stdout length (default: 8192)
+ MAX_STDERR_LENGTH: Maximum stderr length (default: 2048)
+ CLI_PROXY_ENABLED: Enable proxy support (default: false)
+ CLI_PROXY_URL: Proxy URL (also checks HTTP_PROXY if not set)
+ OUTPUT_TRUNCATE_MESSAGE: Message shown when output is truncated (default: "...[output truncated]")
+ """
+ allowed_commands = os.getenv("ALLOWED_COMMANDS", "ls,cat,pwd")
+ allowed_flags = os.getenv("ALLOWED_FLAGS", "-l,-a,--help")
+ allow_shell_operators_env = os.getenv("ALLOW_SHELL_OPERATORS", "false")
+
+ allow_all_commands = allowed_commands.lower() == "all"
+ allow_all_flags = allowed_flags.lower() == "all"
+ allow_shell_operators = allow_shell_operators_env.lower() in ("true", "1")
+
+ # Proxy configuration
+ proxy_url = os.getenv("CLI_PROXY_URL") or os.getenv("HTTP_PROXY")
+ proxy_enabled = os.getenv("CLI_PROXY_ENABLED", "false").lower() in ("true", "1")
+
+ # Output length limits with error handling
+ try:
+ max_command_length = int(os.getenv("MAX_COMMAND_LENGTH", "1024"))
+ if max_command_length <= 0:
+ max_command_length = 1024
+ except ValueError:
+ max_command_length = 1024
+
+ try:
+ command_timeout = int(os.getenv("COMMAND_TIMEOUT", "30"))
+ if command_timeout <= 0:
+ command_timeout = 30
+ except ValueError:
+ command_timeout = 30
+
+ try:
+ max_output_length = int(os.getenv("MAX_OUTPUT_LENGTH", "10240"))
+ if max_output_length <= 0:
+ max_output_length = 10240
+ except ValueError:
+ max_output_length = 10240
+
+ try:
+ max_stdout_length = int(os.getenv("MAX_STDOUT_LENGTH", "8192"))
+ if max_stdout_length <= 0:
+ max_stdout_length = 8192
+ except ValueError:
+ max_stdout_length = 8192
+
+ try:
+ max_stderr_length = int(os.getenv("MAX_STDERR_LENGTH", "2048"))
+ if max_stderr_length <= 0:
+ max_stderr_length = 2048
+ except ValueError:
+ max_stderr_length = 2048
+
+ return SecurityConfig(
+ allowed_commands=(
+ set() if allow_all_commands else set(allowed_commands.split(","))
+ ),
+ allowed_flags=set() if allow_all_flags else set(allowed_flags.split(",")),
+ max_command_length=max_command_length,
+ command_timeout=command_timeout,
+ allow_all_commands=allow_all_commands,
+ allow_all_flags=allow_all_flags,
+ allow_shell_operators=allow_shell_operators,
+ max_output_length=max_output_length,
+ max_stdout_length=max_stdout_length,
+ max_stderr_length=max_stderr_length,
+ proxy_url=proxy_url,
+ proxy_enabled=proxy_enabled,
+ truncate_message=os.getenv("OUTPUT_TRUNCATE_MESSAGE", "...[output truncated]"),
+ )
+
+
+executor = CommandExecutor(
+ allowed_dir=os.getenv("ALLOWED_DIR", ""), security_config=load_security_config()
+)
+
+
+@server.list_tools()
+async def handle_list_tools() -> list[types.Tool]:
+ commands_desc = (
+ "all commands"
+ if executor.security_config.allow_all_commands
+ else ", ".join(executor.security_config.allowed_commands)
+ )
+ flags_desc = (
+ "all flags"
+ if executor.security_config.allow_all_flags
+ else ", ".join(executor.security_config.allowed_flags)
+ )
+
+ return [
+ types.Tool(
+ name="run_command",
+ description=(
+ f"Allows command (CLI) execution in the directory: {executor.allowed_dir}\n\n"
+ f"Available commands: {commands_desc}\n"
+ f"Available flags: {flags_desc}\n\n"
+ f"Shell operators (&&, ||, |, >, >>, <, <<, ;) are {'supported' if executor.security_config.allow_shell_operators else 'not supported'}. Set ALLOW_SHELL_OPERATORS=true to enable."
+ ),
+ inputSchema={
+ "type": "object",
+ "properties": {
+ "command": {
+ "type": "string",
+ "description": "Single command to execute (example: 'ls -l' or 'cat file.txt')",
+ }
+ },
+ "required": ["command"],
+ },
+ ),
+ types.Tool(
+ name="show_security_rules",
+ description=(
+ "Show what commands and operations are allowed in this environment.\n"
+ ),
+ inputSchema={
+ "type": "object",
+ "properties": {},
+ },
+ ),
+ ]
+
+
+@server.call_tool()
+async def handle_call_tool(
+ name: str, arguments: Optional[Dict[str, Any]]
+) -> List[types.TextContent]:
+ if name == "run_command":
+ if not arguments or "command" not in arguments:
+ return [
+ types.TextContent(type="text", text="No command provided", error=True)
+ ]
+
+ try:
+ result = executor.execute(arguments["command"])
+
+ response = []
+ if result.stdout:
+ stdout_truncated = executor._truncate_output(
+ result.stdout,
+ executor.security_config.max_stdout_length
+ )
+ response.append(types.TextContent(type="text", text=stdout_truncated))
+
+ if result.stderr:
+ stderr_truncated = executor._truncate_output(
+ result.stderr,
+ executor.security_config.max_stderr_length
+ )
+ response.append(
+ types.TextContent(type="text", text=stderr_truncated, error=True)
+ )
+
+ response.append(
+ types.TextContent(
+ type="text",
+ text=f"\nCommand completed with return code: {result.returncode}",
+ )
+ )
+
+ return response
+
+ except CommandSecurityError as e:
+ return [
+ types.TextContent(
+ type="text", text=f"Security violation: {str(e)}", error=True
+ )
+ ]
+ except subprocess.TimeoutExpired:
+ return [
+ types.TextContent(
+ type="text",
+ text=f"Command timed out after {executor.security_config.command_timeout} seconds",
+ error=True,
+ )
+ ]
+ except Exception as e:
+ return [types.TextContent(type="text", text=f"Error: {str(e)}", error=True)]
+
+ elif name == "show_security_rules":
+ commands_desc = (
+ "All commands allowed"
+ if executor.security_config.allow_all_commands
+ else ", ".join(sorted(executor.security_config.allowed_commands))
+ )
+ flags_desc = (
+ "All flags allowed"
+ if executor.security_config.allow_all_flags
+ else ", ".join(sorted(executor.security_config.allowed_flags))
+ )
+
+ security_info = (
+ "Security Configuration:\n"
+ f"==================\n"
+ f"Working Directory: {executor.allowed_dir}\n"
+ f"\nAllowed Commands:\n"
+ f"----------------\n"
+ f"{commands_desc}\n"
+ f"\nAllowed Flags:\n"
+ f"-------------\n"
+ f"{flags_desc}\n"
+ f"\nSecurity Limits:\n"
+ f"---------------\n"
+ f"Max Command Length: {executor.security_config.max_command_length} characters\n"
+ f"Command Timeout: {executor.security_config.command_timeout} seconds\n"
+ f"Max Stdout Length: {executor.security_config.max_stdout_length} characters\n"
+ f"Max Stderr Length: {executor.security_config.max_stderr_length} characters\n"
+ f"Shell Operators: {'Enabled' if executor.security_config.allow_shell_operators else 'Disabled'}\n"
+ f"\nProxy Configuration:\n"
+ f"-------------------\n"
+ f"Proxy Enabled: {'Yes' if executor.security_config.proxy_enabled else 'No'}\n"
+ f"Proxy URL: {executor.security_config.proxy_url or 'Not configured'}\n"
+ f"\nOutput Settings:\n"
+ f"---------------\n"
+ f"Truncate Message: {executor.security_config.truncate_message}\n"
+ )
+ return [types.TextContent(type="text", text=security_info)]
+
+ raise ValueError(f"Unknown tool: {name}")
+
+
+async def main():
+ async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
+ await server.run(
+ read_stream,
+ write_stream,
+ InitializationOptions(
+ server_name="cli-mcp-server",
+ server_version="0.2.1",
+ capabilities=server.get_capabilities(
+ notification_options=NotificationOptions(),
+ experimental_capabilities={},
+ ),
+ ),
+ )
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/tests/__init__.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/tests/__init__.py
new file mode 100644
index 00000000..65140f2e
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/tests/__init__.py
@@ -0,0 +1 @@
+# tests package
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/tests/test_cli_mcp_server.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/tests/test_cli_mcp_server.py
new file mode 100644
index 00000000..be6e4dd7
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/tests/test_cli_mcp_server.py
@@ -0,0 +1,224 @@
+import os
+import importlib
+import asyncio
+import shutil
+import tempfile
+import unittest
+
+
+# Helper to print results in a simple table format
+def print_results_table(name: str, results: list) -> None:
+ print(f"\n[{name}] Results Table:")
+ print("Idx | Type | Error | Text")
+ print("-----|--------|-------|-----")
+ for idx, tc in enumerate(results):
+ error_flag = getattr(tc, "error", False)
+ # Replace newlines in text for single-line display
+ text = tc.text.strip().replace("\n", "\\n")
+ print(f"{idx:<3} | {tc.type:<6} | {error_flag!s:<5} | {text}")
+
+
+class TestCLIMCPServer(unittest.TestCase):
+ def setUp(self):
+ # Create a temporary directory for allowed_dir
+ self.tempdir = tempfile.TemporaryDirectory()
+ os.environ["ALLOWED_DIR"] = self.tempdir.name
+ # Remove custom allowed commands/flags to use defaults
+ os.environ.pop("ALLOWED_COMMANDS", None)
+ os.environ.pop("ALLOWED_FLAGS", None)
+ # Ensure shell operators are disabled by default
+ os.environ.pop("ALLOW_SHELL_OPERATORS", None)
+ # Reload server module to pick up env changes
+ try:
+ import cli_mcp_server.server as server_module
+
+ self.server = importlib.reload(server_module)
+ except ImportError:
+ import cli_mcp_server.server as server_module
+
+ self.server = server_module
+
+ def tearDown(self):
+ self.tempdir.cleanup()
+
+ def test_run_pwd(self):
+ # Run 'pwd' command
+ result = asyncio.run(
+ self.server.handle_call_tool("run_command", {"command": "pwd"})
+ )
+ texts = [tc.text for tc in result]
+ # Debug print: show results in table form
+ print_results_table("test_run_pwd", result)
+ self.assertTrue(texts, "No output returned")
+ self.assertEqual(texts[0].strip(), self.tempdir.name)
+ self.assertTrue(any("return code: 0" in text for text in texts))
+
+ def test_run_ls(self):
+ # Create a file in the allowed directory
+ file_path = os.path.join(self.tempdir.name, "foo.txt")
+ with open(file_path, "w") as f:
+ f.write("test")
+ result = asyncio.run(
+ self.server.handle_call_tool("run_command", {"command": "ls"})
+ )
+ texts = [tc.text for tc in result]
+ # Debug print: show results in table form
+ print_results_table("test_run_ls", result)
+ self.assertTrue(
+ any("foo.txt" in text for text in texts),
+ f"Output did not contain 'foo.txt': {texts}",
+ )
+ self.assertTrue(any("return code: 0" in text for text in texts))
+
+ def test_run_curl_ifconfig(self):
+ # Skip test if curl is not available
+ if not shutil.which("curl"):
+ self.skipTest("curl is not available on PATH")
+ # Allow all commands and flags
+ os.environ["ALLOWED_COMMANDS"] = "all"
+ os.environ["ALLOWED_FLAGS"] = "all"
+ # Reload server to pick up new settings
+ import cli_mcp_server.server as server_module
+
+ self.server = importlib.reload(server_module)
+ result = asyncio.run(
+ self.server.handle_call_tool(
+ "run_command", {"command": "curl -sG ifconfig.me"}
+ )
+ )
+ texts = [tc.text for tc in result]
+ # Debug print: show results in table form
+ print_results_table("test_run_curl_ifconfig", result)
+ output_texts = [t for t in texts if "return code" not in t]
+ self.assertTrue(
+ any(t.strip() for t in output_texts), f"No IP address retrieved: {texts}"
+ )
+ self.assertTrue(any("return code: 0" in text for text in texts))
+
+ def test_shell_operator_disallowed(self):
+ # Ensure shell operators are disabled by default
+ result = asyncio.run(
+ self.server.handle_call_tool("run_command", {"command": "echo 1 && echo 2"})
+ )
+ texts = [tc.text for tc in result]
+ print_results_table("test_shell_operator_disallowed", result)
+ self.assertTrue(
+ any("Security violation" in text for text in texts),
+ f"Expected security violation for shell operators, got: {texts}",
+ )
+ self.assertTrue(
+ any("Shell operator '&&' is not supported" in text for text in texts),
+ f"Expected '&&' not supported message, got: {texts}",
+ )
+
+ def test_shell_operator_allowed_and_executes_commands(self):
+ # Enable shell operators and allow all commands/flags
+ os.environ["ALLOW_SHELL_OPERATORS"] = "true"
+ os.environ["ALLOWED_COMMANDS"] = "all"
+ os.environ["ALLOWED_FLAGS"] = "all"
+ # Reload server to pick up new settings
+ import cli_mcp_server.server as server_module
+
+ server = importlib.reload(server_module)
+ # Execute a compound command with '&&'
+ result = asyncio.run(
+ server.handle_call_tool("run_command", {"command": "echo 3 && echo 4"})
+ )
+ texts = [tc.text for tc in result]
+ print_results_table("test_shell_operator_allowed", result)
+ # The first element should contain the combined stdout from both commands
+ self.assertEqual(
+ texts[0].strip(),
+ "3\n4",
+ f"Unexpected combined output, got: {texts[0]!r}",
+ )
+ self.assertTrue(any("return code: 0" in text for text in texts))
+
+ def test_shell_operator_semicolon(self):
+ # Enable shell operators and allow all commands/flags
+ os.environ["ALLOW_SHELL_OPERATORS"] = "true"
+ os.environ["ALLOWED_COMMANDS"] = "all"
+ os.environ["ALLOWED_FLAGS"] = "all"
+ # Reload server to pick up new settings
+ import cli_mcp_server.server as server_module
+
+ server = importlib.reload(server_module)
+ # Execute a compound command with ';'
+ result = asyncio.run(
+ server.handle_call_tool("run_command", {"command": "echo 5; echo 6"})
+ )
+ texts = [tc.text for tc in result]
+ print_results_table("test_shell_operator_semicolon", result)
+ self.assertEqual(
+ texts[0].strip(),
+ "5\n6",
+ f"Unexpected combined output, got: {texts[0]!r}",
+ )
+ self.assertTrue(any("return code: 0" in text for text in texts))
+
+ def test_shell_operator_append_redirection(self):
+ # Enable shell operators and allow all commands/flags
+ os.environ["ALLOW_SHELL_OPERATORS"] = "true"
+ os.environ["ALLOWED_COMMANDS"] = "all"
+ os.environ["ALLOWED_FLAGS"] = "all"
+ # Reload server to pick up new settings
+ import cli_mcp_server.server as server_module
+
+ server = importlib.reload(server_module)
+ # Create an output file and append text using '>>'
+ file_name = "append.txt"
+ file_path = os.path.join(self.tempdir.name, file_name)
+ # Ensure the file exists
+ open(file_path, "w").close()
+ result = asyncio.run(
+ server.handle_call_tool("run_command", {"command": f"echo hello >> {file_name}"})
+ )
+ texts = [tc.text for tc in result]
+ print_results_table("test_shell_operator_append_redirection", result)
+ # After redirection, file should contain 'hello'
+ with open(file_path, "r") as f:
+ content = f.read().strip()
+ self.assertEqual(content, "hello", f"Unexpected file content: {content!r}")
+ self.assertTrue(any("return code: 0" in text for text in texts))
+
+ def test_shell_operator_pipe(self):
+ # Enable shell operators and allow all commands/flags
+ os.environ["ALLOW_SHELL_OPERATORS"] = "true"
+ os.environ["ALLOWED_COMMANDS"] = "all"
+ os.environ["ALLOWED_FLAGS"] = "all"
+ # Reload server to pick up new settings
+ import cli_mcp_server.server as server_module
+
+ server = importlib.reload(server_module)
+ # Execute a simple pipeline to filter output
+ result = asyncio.run(
+ server.handle_call_tool("run_command", {"command": "echo 123 | grep 123"})
+ )
+ texts = [tc.text for tc in result]
+ print_results_table("test_shell_operator_pipe", result)
+ # The pipeline should output '123'
+ self.assertEqual(texts[0].strip(), "123", f"Unexpected pipeline output: {texts[0]!r}")
+ self.assertTrue(any("return code: 0" in text for text in texts))
+
+ def test_shell_operator_or(self):
+ # Enable shell operators and allow all commands/flags
+ os.environ["ALLOW_SHELL_OPERATORS"] = "true"
+ os.environ["ALLOWED_COMMANDS"] = "all"
+ os.environ["ALLOWED_FLAGS"] = "all"
+ # Reload server to pick up new settings
+ import cli_mcp_server.server as server_module
+
+ server = importlib.reload(server_module)
+ # Use '||' to fallback on failure
+ result = asyncio.run(
+ server.handle_call_tool("run_command", {"command": "false || echo OR_OK"})
+ )
+ texts = [tc.text for tc in result]
+ print_results_table("test_shell_operator_or", result)
+ # The OR operation should output 'OR_OK'
+ self.assertEqual(texts[0].strip(), "OR_OK", f"Unexpected OR output: {texts[0]!r}")
+ self.assertTrue(any("return code: 0" in text for text in texts))
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/uv.lock b/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/uv.lock
new file mode 100644
index 00000000..d1b16810
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/cli-mcp-server/uv.lock
@@ -0,0 +1,532 @@
+version = 1
+revision = 1
+requires-python = ">=3.10"
+
+[[package]]
+name = "annotated-types"
+version = "0.7.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 },
+]
+
+[[package]]
+name = "anyio"
+version = "4.9.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
+ { name = "idna" },
+ { name = "sniffio" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916 },
+]
+
+[[package]]
+name = "attrs"
+version = "25.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815 },
+]
+
+[[package]]
+name = "certifi"
+version = "2025.1.31"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 },
+]
+
+[[package]]
+name = "cli-mcp-server"
+version = "0.2.5"
+source = { editable = "." }
+dependencies = [
+ { name = "mcp" },
+]
+
+[package.metadata]
+requires-dist = [{ name = "mcp", specifier = ">=1.10.1" }]
+
+[[package]]
+name = "click"
+version = "8.1.8"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 },
+]
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
+]
+
+[[package]]
+name = "exceptiongroup"
+version = "1.2.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 },
+]
+
+[[package]]
+name = "h11"
+version = "0.14.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 },
+]
+
+[[package]]
+name = "httpcore"
+version = "1.0.8"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "h11" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/9f/45/ad3e1b4d448f22c0cff4f5692f5ed0666658578e358b8d58a19846048059/httpcore-1.0.8.tar.gz", hash = "sha256:86e94505ed24ea06514883fd44d2bc02d90e77e7979c8eb71b90f41d364a1bad", size = 85385 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/18/8d/f052b1e336bb2c1fc7ed1aaed898aa570c0b61a09707b108979d9fc6e308/httpcore-1.0.8-py3-none-any.whl", hash = "sha256:5254cf149bcb5f75e9d1b2b9f729ea4a4b883d1ad7379fc632b727cec23674be", size = 78732 },
+]
+
+[[package]]
+name = "httpx"
+version = "0.28.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "certifi" },
+ { name = "httpcore" },
+ { name = "idna" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 },
+]
+
+[[package]]
+name = "httpx-sse"
+version = "0.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819 },
+]
+
+[[package]]
+name = "idna"
+version = "3.10"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
+]
+
+[[package]]
+name = "jsonschema"
+version = "4.24.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "attrs" },
+ { name = "jsonschema-specifications" },
+ { name = "referencing" },
+ { name = "rpds-py" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/bf/d3/1cf5326b923a53515d8f3a2cd442e6d7e94fcc444716e879ea70a0ce3177/jsonschema-4.24.0.tar.gz", hash = "sha256:0b4e8069eb12aedfa881333004bccaec24ecef5a8a6a4b6df142b2cc9599d196", size = 353480 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a2/3d/023389198f69c722d039351050738d6755376c8fd343e91dc493ea485905/jsonschema-4.24.0-py3-none-any.whl", hash = "sha256:a462455f19f5faf404a7902952b6f0e3ce868f3ee09a359b05eca6673bd8412d", size = 88709 },
+]
+
+[[package]]
+name = "jsonschema-specifications"
+version = "2025.4.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "referencing" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/bf/ce/46fbd9c8119cfc3581ee5643ea49464d168028cfb5caff5fc0596d0cf914/jsonschema_specifications-2025.4.1.tar.gz", hash = "sha256:630159c9f4dbea161a6a2205c3011cc4f18ff381b189fff48bb39b9bf26ae608", size = 15513 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/01/0e/b27cdbaccf30b890c40ed1da9fd4a3593a5cf94dae54fb34f8a4b74fcd3f/jsonschema_specifications-2025.4.1-py3-none-any.whl", hash = "sha256:4653bffbd6584f7de83a67e0d620ef16900b390ddc7939d56684d6c81e33f1af", size = 18437 },
+]
+
+[[package]]
+name = "mcp"
+version = "1.10.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "httpx" },
+ { name = "httpx-sse" },
+ { name = "jsonschema" },
+ { name = "pydantic" },
+ { name = "pydantic-settings" },
+ { name = "python-multipart" },
+ { name = "sse-starlette" },
+ { name = "starlette" },
+ { name = "uvicorn", marker = "sys_platform != 'emscripten'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/7c/68/63045305f29ff680a9cd5be360c755270109e6b76f696ea6824547ddbc30/mcp-1.10.1.tar.gz", hash = "sha256:aaa0957d8307feeff180da2d9d359f2b801f35c0c67f1882136239055ef034c2", size = 392969 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d7/3f/435a5b3d10ae242a9d6c2b33175551173c3c61fe637dc893be05c4ed0aaf/mcp-1.10.1-py3-none-any.whl", hash = "sha256:4d08301aefe906dce0fa482289db55ce1db831e3e67212e65b5e23ad8454b3c5", size = 150878 },
+]
+
+[[package]]
+name = "pydantic"
+version = "2.11.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "annotated-types" },
+ { name = "pydantic-core" },
+ { name = "typing-extensions" },
+ { name = "typing-inspection" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/10/2e/ca897f093ee6c5f3b0bee123ee4465c50e75431c3d5b6a3b44a47134e891/pydantic-2.11.3.tar.gz", hash = "sha256:7471657138c16adad9322fe3070c0116dd6c3ad8d649300e3cbdfe91f4db4ec3", size = 785513 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b0/1d/407b29780a289868ed696d1616f4aad49d6388e5a77f567dcd2629dcd7b8/pydantic-2.11.3-py3-none-any.whl", hash = "sha256:a082753436a07f9ba1289c6ffa01cd93db3548776088aa917cc43b63f68fa60f", size = 443591 },
+]
+
+[[package]]
+name = "pydantic-core"
+version = "2.33.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/17/19/ed6a078a5287aea7922de6841ef4c06157931622c89c2a47940837b5eecd/pydantic_core-2.33.1.tar.gz", hash = "sha256:bcc9c6fdb0ced789245b02b7d6603e17d1563064ddcfc36f046b61c0c05dd9df", size = 434395 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/38/ea/5f572806ab4d4223d11551af814d243b0e3e02cc6913def4d1fe4a5ca41c/pydantic_core-2.33.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3077cfdb6125cc8dab61b155fdd714663e401f0e6883f9632118ec12cf42df26", size = 2044021 },
+ { url = "https://files.pythonhosted.org/packages/8c/d1/f86cc96d2aa80e3881140d16d12ef2b491223f90b28b9a911346c04ac359/pydantic_core-2.33.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8ffab8b2908d152e74862d276cf5017c81a2f3719f14e8e3e8d6b83fda863927", size = 1861742 },
+ { url = "https://files.pythonhosted.org/packages/37/08/fbd2cd1e9fc735a0df0142fac41c114ad9602d1c004aea340169ae90973b/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5183e4f6a2d468787243ebcd70cf4098c247e60d73fb7d68d5bc1e1beaa0c4db", size = 1910414 },
+ { url = "https://files.pythonhosted.org/packages/7f/73/3ac217751decbf8d6cb9443cec9b9eb0130eeada6ae56403e11b486e277e/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:398a38d323f37714023be1e0285765f0a27243a8b1506b7b7de87b647b517e48", size = 1996848 },
+ { url = "https://files.pythonhosted.org/packages/9a/f5/5c26b265cdcff2661e2520d2d1e9db72d117ea00eb41e00a76efe68cb009/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87d3776f0001b43acebfa86f8c64019c043b55cc5a6a2e313d728b5c95b46969", size = 2141055 },
+ { url = "https://files.pythonhosted.org/packages/5d/14/a9c3cee817ef2f8347c5ce0713e91867a0dceceefcb2973942855c917379/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c566dd9c5f63d22226409553531f89de0cac55397f2ab8d97d6f06cfce6d947e", size = 2753806 },
+ { url = "https://files.pythonhosted.org/packages/f2/68/866ce83a51dd37e7c604ce0050ff6ad26de65a7799df89f4db87dd93d1d6/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0d5f3acc81452c56895e90643a625302bd6be351e7010664151cc55b7b97f89", size = 2007777 },
+ { url = "https://files.pythonhosted.org/packages/b6/a8/36771f4404bb3e49bd6d4344da4dede0bf89cc1e01f3b723c47248a3761c/pydantic_core-2.33.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d3a07fadec2a13274a8d861d3d37c61e97a816beae717efccaa4b36dfcaadcde", size = 2122803 },
+ { url = "https://files.pythonhosted.org/packages/18/9c/730a09b2694aa89360d20756369822d98dc2f31b717c21df33b64ffd1f50/pydantic_core-2.33.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f99aeda58dce827f76963ee87a0ebe75e648c72ff9ba1174a253f6744f518f65", size = 2086755 },
+ { url = "https://files.pythonhosted.org/packages/54/8e/2dccd89602b5ec31d1c58138d02340ecb2ebb8c2cac3cc66b65ce3edb6ce/pydantic_core-2.33.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:902dbc832141aa0ec374f4310f1e4e7febeebc3256f00dc359a9ac3f264a45dc", size = 2257358 },
+ { url = "https://files.pythonhosted.org/packages/d1/9c/126e4ac1bfad8a95a9837acdd0963695d69264179ba4ede8b8c40d741702/pydantic_core-2.33.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fe44d56aa0b00d66640aa84a3cbe80b7a3ccdc6f0b1ca71090696a6d4777c091", size = 2257916 },
+ { url = "https://files.pythonhosted.org/packages/7d/ba/91eea2047e681a6853c81c20aeca9dcdaa5402ccb7404a2097c2adf9d038/pydantic_core-2.33.1-cp310-cp310-win32.whl", hash = "sha256:ed3eb16d51257c763539bde21e011092f127a2202692afaeaccb50db55a31383", size = 1923823 },
+ { url = "https://files.pythonhosted.org/packages/94/c0/fcdf739bf60d836a38811476f6ecd50374880b01e3014318b6e809ddfd52/pydantic_core-2.33.1-cp310-cp310-win_amd64.whl", hash = "sha256:694ad99a7f6718c1a498dc170ca430687a39894a60327f548e02a9c7ee4b6504", size = 1952494 },
+ { url = "https://files.pythonhosted.org/packages/d6/7f/c6298830cb780c46b4f46bb24298d01019ffa4d21769f39b908cd14bbd50/pydantic_core-2.33.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6e966fc3caaf9f1d96b349b0341c70c8d6573bf1bac7261f7b0ba88f96c56c24", size = 2044224 },
+ { url = "https://files.pythonhosted.org/packages/a8/65/6ab3a536776cad5343f625245bd38165d6663256ad43f3a200e5936afd6c/pydantic_core-2.33.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bfd0adeee563d59c598ceabddf2c92eec77abcb3f4a391b19aa7366170bd9e30", size = 1858845 },
+ { url = "https://files.pythonhosted.org/packages/e9/15/9a22fd26ba5ee8c669d4b8c9c244238e940cd5d818649603ca81d1c69861/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91815221101ad3c6b507804178a7bb5cb7b2ead9ecd600041669c8d805ebd595", size = 1910029 },
+ { url = "https://files.pythonhosted.org/packages/d5/33/8cb1a62818974045086f55f604044bf35b9342900318f9a2a029a1bec460/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9fea9c1869bb4742d174a57b4700c6dadea951df8b06de40c2fedb4f02931c2e", size = 1997784 },
+ { url = "https://files.pythonhosted.org/packages/c0/ca/49958e4df7715c71773e1ea5be1c74544923d10319173264e6db122543f9/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d20eb4861329bb2484c021b9d9a977566ab16d84000a57e28061151c62b349a", size = 2141075 },
+ { url = "https://files.pythonhosted.org/packages/7b/a6/0b3a167a9773c79ba834b959b4e18c3ae9216b8319bd8422792abc8a41b1/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb935c5591573ae3201640579f30128ccc10739b45663f93c06796854405505", size = 2745849 },
+ { url = "https://files.pythonhosted.org/packages/0b/60/516484135173aa9e5861d7a0663dce82e4746d2e7f803627d8c25dfa5578/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c964fd24e6166420d18fb53996d8c9fd6eac9bf5ae3ec3d03015be4414ce497f", size = 2005794 },
+ { url = "https://files.pythonhosted.org/packages/86/70/05b1eb77459ad47de00cf78ee003016da0cedf8b9170260488d7c21e9181/pydantic_core-2.33.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:681d65e9011f7392db5aa002b7423cc442d6a673c635668c227c6c8d0e5a4f77", size = 2123237 },
+ { url = "https://files.pythonhosted.org/packages/c7/57/12667a1409c04ae7dc95d3b43158948eb0368e9c790be8b095cb60611459/pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e100c52f7355a48413e2999bfb4e139d2977a904495441b374f3d4fb4a170961", size = 2086351 },
+ { url = "https://files.pythonhosted.org/packages/57/61/cc6d1d1c1664b58fdd6ecc64c84366c34ec9b606aeb66cafab6f4088974c/pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:048831bd363490be79acdd3232f74a0e9951b11b2b4cc058aeb72b22fdc3abe1", size = 2258914 },
+ { url = "https://files.pythonhosted.org/packages/d1/0a/edb137176a1f5419b2ddee8bde6a0a548cfa3c74f657f63e56232df8de88/pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bdc84017d28459c00db6f918a7272a5190bec3090058334e43a76afb279eac7c", size = 2257385 },
+ { url = "https://files.pythonhosted.org/packages/26/3c/48ca982d50e4b0e1d9954919c887bdc1c2b462801bf408613ccc641b3daa/pydantic_core-2.33.1-cp311-cp311-win32.whl", hash = "sha256:32cd11c5914d1179df70406427097c7dcde19fddf1418c787540f4b730289896", size = 1923765 },
+ { url = "https://files.pythonhosted.org/packages/33/cd/7ab70b99e5e21559f5de38a0928ea84e6f23fdef2b0d16a6feaf942b003c/pydantic_core-2.33.1-cp311-cp311-win_amd64.whl", hash = "sha256:2ea62419ba8c397e7da28a9170a16219d310d2cf4970dbc65c32faf20d828c83", size = 1950688 },
+ { url = "https://files.pythonhosted.org/packages/4b/ae/db1fc237b82e2cacd379f63e3335748ab88b5adde98bf7544a1b1bd10a84/pydantic_core-2.33.1-cp311-cp311-win_arm64.whl", hash = "sha256:fc903512177361e868bc1f5b80ac8c8a6e05fcdd574a5fb5ffeac5a9982b9e89", size = 1908185 },
+ { url = "https://files.pythonhosted.org/packages/c8/ce/3cb22b07c29938f97ff5f5bb27521f95e2ebec399b882392deb68d6c440e/pydantic_core-2.33.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:1293d7febb995e9d3ec3ea09caf1a26214eec45b0f29f6074abb004723fc1de8", size = 2026640 },
+ { url = "https://files.pythonhosted.org/packages/19/78/f381d643b12378fee782a72126ec5d793081ef03791c28a0fd542a5bee64/pydantic_core-2.33.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:99b56acd433386c8f20be5c4000786d1e7ca0523c8eefc995d14d79c7a081498", size = 1852649 },
+ { url = "https://files.pythonhosted.org/packages/9d/2b/98a37b80b15aac9eb2c6cfc6dbd35e5058a352891c5cce3a8472d77665a6/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35a5ec3fa8c2fe6c53e1b2ccc2454398f95d5393ab398478f53e1afbbeb4d939", size = 1892472 },
+ { url = "https://files.pythonhosted.org/packages/4e/d4/3c59514e0f55a161004792b9ff3039da52448f43f5834f905abef9db6e4a/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b172f7b9d2f3abc0efd12e3386f7e48b576ef309544ac3a63e5e9cdd2e24585d", size = 1977509 },
+ { url = "https://files.pythonhosted.org/packages/a9/b6/c2c7946ef70576f79a25db59a576bce088bdc5952d1b93c9789b091df716/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9097b9f17f91eea659b9ec58148c0747ec354a42f7389b9d50701610d86f812e", size = 2128702 },
+ { url = "https://files.pythonhosted.org/packages/88/fe/65a880f81e3f2a974312b61f82a03d85528f89a010ce21ad92f109d94deb/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc77ec5b7e2118b152b0d886c7514a4653bcb58c6b1d760134a9fab915f777b3", size = 2679428 },
+ { url = "https://files.pythonhosted.org/packages/6f/ff/4459e4146afd0462fb483bb98aa2436d69c484737feaceba1341615fb0ac/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5e3d15245b08fa4a84cefc6c9222e6f37c98111c8679fbd94aa145f9a0ae23d", size = 2008753 },
+ { url = "https://files.pythonhosted.org/packages/7c/76/1c42e384e8d78452ededac8b583fe2550c84abfef83a0552e0e7478ccbc3/pydantic_core-2.33.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ef99779001d7ac2e2461d8ab55d3373fe7315caefdbecd8ced75304ae5a6fc6b", size = 2114849 },
+ { url = "https://files.pythonhosted.org/packages/00/72/7d0cf05095c15f7ffe0eb78914b166d591c0eed72f294da68378da205101/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:fc6bf8869e193855e8d91d91f6bf59699a5cdfaa47a404e278e776dd7f168b39", size = 2069541 },
+ { url = "https://files.pythonhosted.org/packages/b3/69/94a514066bb7d8be499aa764926937409d2389c09be0b5107a970286ef81/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:b1caa0bc2741b043db7823843e1bde8aaa58a55a58fda06083b0569f8b45693a", size = 2239225 },
+ { url = "https://files.pythonhosted.org/packages/84/b0/e390071eadb44b41f4f54c3cef64d8bf5f9612c92686c9299eaa09e267e2/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ec259f62538e8bf364903a7d0d0239447059f9434b284f5536e8402b7dd198db", size = 2248373 },
+ { url = "https://files.pythonhosted.org/packages/d6/b2/288b3579ffc07e92af66e2f1a11be3b056fe1214aab314748461f21a31c3/pydantic_core-2.33.1-cp312-cp312-win32.whl", hash = "sha256:e14f369c98a7c15772b9da98987f58e2b509a93235582838bd0d1d8c08b68fda", size = 1907034 },
+ { url = "https://files.pythonhosted.org/packages/02/28/58442ad1c22b5b6742b992ba9518420235adced665513868f99a1c2638a5/pydantic_core-2.33.1-cp312-cp312-win_amd64.whl", hash = "sha256:1c607801d85e2e123357b3893f82c97a42856192997b95b4d8325deb1cd0c5f4", size = 1956848 },
+ { url = "https://files.pythonhosted.org/packages/a1/eb/f54809b51c7e2a1d9f439f158b8dd94359321abcc98767e16fc48ae5a77e/pydantic_core-2.33.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d13f0276806ee722e70a1c93da19748594f19ac4299c7e41237fc791d1861ea", size = 1903986 },
+ { url = "https://files.pythonhosted.org/packages/7a/24/eed3466a4308d79155f1cdd5c7432c80ddcc4530ba8623b79d5ced021641/pydantic_core-2.33.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:70af6a21237b53d1fe7b9325b20e65cbf2f0a848cf77bed492b029139701e66a", size = 2033551 },
+ { url = "https://files.pythonhosted.org/packages/ab/14/df54b1a0bc9b6ded9b758b73139d2c11b4e8eb43e8ab9c5847c0a2913ada/pydantic_core-2.33.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:282b3fe1bbbe5ae35224a0dbd05aed9ccabccd241e8e6b60370484234b456266", size = 1852785 },
+ { url = "https://files.pythonhosted.org/packages/fa/96/e275f15ff3d34bb04b0125d9bc8848bf69f25d784d92a63676112451bfb9/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b315e596282bbb5822d0c7ee9d255595bd7506d1cb20c2911a4da0b970187d3", size = 1897758 },
+ { url = "https://files.pythonhosted.org/packages/b7/d8/96bc536e975b69e3a924b507d2a19aedbf50b24e08c80fb00e35f9baaed8/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1dfae24cf9921875ca0ca6a8ecb4bb2f13c855794ed0d468d6abbec6e6dcd44a", size = 1986109 },
+ { url = "https://files.pythonhosted.org/packages/90/72/ab58e43ce7e900b88cb571ed057b2fcd0e95b708a2e0bed475b10130393e/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6dd8ecfde08d8bfadaea669e83c63939af76f4cf5538a72597016edfa3fad516", size = 2129159 },
+ { url = "https://files.pythonhosted.org/packages/dc/3f/52d85781406886c6870ac995ec0ba7ccc028b530b0798c9080531b409fdb/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f593494876eae852dc98c43c6f260f45abdbfeec9e4324e31a481d948214764", size = 2680222 },
+ { url = "https://files.pythonhosted.org/packages/f4/56/6e2ef42f363a0eec0fd92f74a91e0ac48cd2e49b695aac1509ad81eee86a/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:948b73114f47fd7016088e5186d13faf5e1b2fe83f5e320e371f035557fd264d", size = 2006980 },
+ { url = "https://files.pythonhosted.org/packages/4c/c0/604536c4379cc78359f9ee0aa319f4aedf6b652ec2854953f5a14fc38c5a/pydantic_core-2.33.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e11f3864eb516af21b01e25fac915a82e9ddad3bb0fb9e95a246067398b435a4", size = 2120840 },
+ { url = "https://files.pythonhosted.org/packages/1f/46/9eb764814f508f0edfb291a0f75d10854d78113fa13900ce13729aaec3ae/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:549150be302428b56fdad0c23c2741dcdb5572413776826c965619a25d9c6bde", size = 2072518 },
+ { url = "https://files.pythonhosted.org/packages/42/e3/fb6b2a732b82d1666fa6bf53e3627867ea3131c5f39f98ce92141e3e3dc1/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:495bc156026efafd9ef2d82372bd38afce78ddd82bf28ef5276c469e57c0c83e", size = 2248025 },
+ { url = "https://files.pythonhosted.org/packages/5c/9d/fbe8fe9d1aa4dac88723f10a921bc7418bd3378a567cb5e21193a3c48b43/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ec79de2a8680b1a67a07490bddf9636d5c2fab609ba8c57597e855fa5fa4dacd", size = 2254991 },
+ { url = "https://files.pythonhosted.org/packages/aa/99/07e2237b8a66438d9b26482332cda99a9acccb58d284af7bc7c946a42fd3/pydantic_core-2.33.1-cp313-cp313-win32.whl", hash = "sha256:ee12a7be1742f81b8a65b36c6921022301d466b82d80315d215c4c691724986f", size = 1915262 },
+ { url = "https://files.pythonhosted.org/packages/8a/f4/e457a7849beeed1e5defbcf5051c6f7b3c91a0624dd31543a64fc9adcf52/pydantic_core-2.33.1-cp313-cp313-win_amd64.whl", hash = "sha256:ede9b407e39949d2afc46385ce6bd6e11588660c26f80576c11c958e6647bc40", size = 1956626 },
+ { url = "https://files.pythonhosted.org/packages/20/d0/e8d567a7cff7b04e017ae164d98011f1e1894269fe8e90ea187a3cbfb562/pydantic_core-2.33.1-cp313-cp313-win_arm64.whl", hash = "sha256:aa687a23d4b7871a00e03ca96a09cad0f28f443690d300500603bd0adba4b523", size = 1909590 },
+ { url = "https://files.pythonhosted.org/packages/ef/fd/24ea4302d7a527d672c5be06e17df16aabfb4e9fdc6e0b345c21580f3d2a/pydantic_core-2.33.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:401d7b76e1000d0dd5538e6381d28febdcacb097c8d340dde7d7fc6e13e9f95d", size = 1812963 },
+ { url = "https://files.pythonhosted.org/packages/5f/95/4fbc2ecdeb5c1c53f1175a32d870250194eb2fdf6291b795ab08c8646d5d/pydantic_core-2.33.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7aeb055a42d734c0255c9e489ac67e75397d59c6fbe60d155851e9782f276a9c", size = 1986896 },
+ { url = "https://files.pythonhosted.org/packages/71/ae/fe31e7f4a62431222d8f65a3bd02e3fa7e6026d154a00818e6d30520ea77/pydantic_core-2.33.1-cp313-cp313t-win_amd64.whl", hash = "sha256:338ea9b73e6e109f15ab439e62cb3b78aa752c7fd9536794112e14bee02c8d18", size = 1931810 },
+ { url = "https://files.pythonhosted.org/packages/9c/c7/8b311d5adb0fe00a93ee9b4e92a02b0ec08510e9838885ef781ccbb20604/pydantic_core-2.33.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c834f54f8f4640fd7e4b193f80eb25a0602bba9e19b3cd2fc7ffe8199f5ae02", size = 2041659 },
+ { url = "https://files.pythonhosted.org/packages/8a/d6/4f58d32066a9e26530daaf9adc6664b01875ae0691570094968aaa7b8fcc/pydantic_core-2.33.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:049e0de24cf23766f12cc5cc71d8abc07d4a9deb9061b334b62093dedc7cb068", size = 1873294 },
+ { url = "https://files.pythonhosted.org/packages/f7/3f/53cc9c45d9229da427909c751f8ed2bf422414f7664ea4dde2d004f596ba/pydantic_core-2.33.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a28239037b3d6f16916a4c831a5a0eadf856bdd6d2e92c10a0da3a59eadcf3e", size = 1903771 },
+ { url = "https://files.pythonhosted.org/packages/f0/49/bf0783279ce674eb9903fb9ae43f6c614cb2f1c4951370258823f795368b/pydantic_core-2.33.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d3da303ab5f378a268fa7d45f37d7d85c3ec19769f28d2cc0c61826a8de21fe", size = 2083558 },
+ { url = "https://files.pythonhosted.org/packages/9c/5b/0d998367687f986c7d8484a2c476d30f07bf5b8b1477649a6092bd4c540e/pydantic_core-2.33.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:25626fb37b3c543818c14821afe0fd3830bc327a43953bc88db924b68c5723f1", size = 2118038 },
+ { url = "https://files.pythonhosted.org/packages/b3/33/039287d410230ee125daee57373ac01940d3030d18dba1c29cd3089dc3ca/pydantic_core-2.33.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3ab2d36e20fbfcce8f02d73c33a8a7362980cff717926bbae030b93ae46b56c7", size = 2079315 },
+ { url = "https://files.pythonhosted.org/packages/1f/85/6d8b2646d99c062d7da2d0ab2faeb0d6ca9cca4c02da6076376042a20da3/pydantic_core-2.33.1-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:2f9284e11c751b003fd4215ad92d325d92c9cb19ee6729ebd87e3250072cdcde", size = 2249063 },
+ { url = "https://files.pythonhosted.org/packages/17/d7/c37d208d5738f7b9ad8f22ae8a727d88ebf9c16c04ed2475122cc3f7224a/pydantic_core-2.33.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:048c01eee07d37cbd066fc512b9d8b5ea88ceeb4e629ab94b3e56965ad655add", size = 2254631 },
+ { url = "https://files.pythonhosted.org/packages/13/e0/bafa46476d328e4553b85ab9b2f7409e7aaef0ce4c937c894821c542d347/pydantic_core-2.33.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5ccd429694cf26af7997595d627dd2637e7932214486f55b8a357edaac9dae8c", size = 2080877 },
+ { url = "https://files.pythonhosted.org/packages/0b/76/1794e440c1801ed35415238d2c728f26cd12695df9057154ad768b7b991c/pydantic_core-2.33.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3a371dc00282c4b84246509a5ddc808e61b9864aa1eae9ecc92bb1268b82db4a", size = 2042858 },
+ { url = "https://files.pythonhosted.org/packages/73/b4/9cd7b081fb0b1b4f8150507cd59d27b275c3e22ad60b35cb19ea0977d9b9/pydantic_core-2.33.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f59295ecc75a1788af8ba92f2e8c6eeaa5a94c22fc4d151e8d9638814f85c8fc", size = 1873745 },
+ { url = "https://files.pythonhosted.org/packages/e1/d7/9ddb7575d4321e40d0363903c2576c8c0c3280ebea137777e5ab58d723e3/pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08530b8ac922003033f399128505f513e30ca770527cc8bbacf75a84fcc2c74b", size = 1904188 },
+ { url = "https://files.pythonhosted.org/packages/d1/a8/3194ccfe461bb08da19377ebec8cb4f13c9bd82e13baebc53c5c7c39a029/pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bae370459da6a5466978c0eacf90690cb57ec9d533f8e63e564ef3822bfa04fe", size = 2083479 },
+ { url = "https://files.pythonhosted.org/packages/42/c7/84cb569555d7179ca0b3f838cef08f66f7089b54432f5b8599aac6e9533e/pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e3de2777e3b9f4d603112f78006f4ae0acb936e95f06da6cb1a45fbad6bdb4b5", size = 2118415 },
+ { url = "https://files.pythonhosted.org/packages/3b/67/72abb8c73e0837716afbb58a59cc9e3ae43d1aa8677f3b4bc72c16142716/pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3a64e81e8cba118e108d7126362ea30e021291b7805d47e4896e52c791be2761", size = 2079623 },
+ { url = "https://files.pythonhosted.org/packages/0b/cd/c59707e35a47ba4cbbf153c3f7c56420c58653b5801b055dc52cccc8e2dc/pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:52928d8c1b6bda03cc6d811e8923dffc87a2d3c8b3bfd2ce16471c7147a24850", size = 2250175 },
+ { url = "https://files.pythonhosted.org/packages/84/32/e4325a6676b0bed32d5b084566ec86ed7fd1e9bcbfc49c578b1755bde920/pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:1b30d92c9412beb5ac6b10a3eb7ef92ccb14e3f2a8d7732e2d739f58b3aa7544", size = 2254674 },
+ { url = "https://files.pythonhosted.org/packages/12/6f/5596dc418f2e292ffc661d21931ab34591952e2843e7168ea5a52591f6ff/pydantic_core-2.33.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f995719707e0e29f0f41a8aa3bcea6e761a36c9136104d3189eafb83f5cec5e5", size = 2080951 },
+]
+
+[[package]]
+name = "pydantic-settings"
+version = "2.9.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pydantic" },
+ { name = "python-dotenv" },
+ { name = "typing-inspection" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/67/1d/42628a2c33e93f8e9acbde0d5d735fa0850f3e6a2f8cb1eb6c40b9a732ac/pydantic_settings-2.9.1.tar.gz", hash = "sha256:c509bf79d27563add44e8446233359004ed85066cd096d8b510f715e6ef5d268", size = 163234 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b6/5f/d6d641b490fd3ec2c4c13b4244d68deea3a1b970a97be64f34fb5504ff72/pydantic_settings-2.9.1-py3-none-any.whl", hash = "sha256:59b4f431b1defb26fe620c71a7d3968a710d719f5f4cdbbdb7926edeb770f6ef", size = 44356 },
+]
+
+[[package]]
+name = "python-dotenv"
+version = "1.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256 },
+]
+
+[[package]]
+name = "python-multipart"
+version = "0.0.20"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546 },
+]
+
+[[package]]
+name = "referencing"
+version = "0.36.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "attrs" },
+ { name = "rpds-py" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775 },
+]
+
+[[package]]
+name = "rpds-py"
+version = "0.26.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a5/aa/4456d84bbb54adc6a916fb10c9b374f78ac840337644e4a5eda229c81275/rpds_py-0.26.0.tar.gz", hash = "sha256:20dae58a859b0906f0685642e591056f1e787f3a8b39c8e8749a45dc7d26bdb0", size = 27385 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b9/31/1459645f036c3dfeacef89e8e5825e430c77dde8489f3b99eaafcd4a60f5/rpds_py-0.26.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:4c70c70f9169692b36307a95f3d8c0a9fcd79f7b4a383aad5eaa0e9718b79b37", size = 372466 },
+ { url = "https://files.pythonhosted.org/packages/dd/ff/3d0727f35836cc8773d3eeb9a46c40cc405854e36a8d2e951f3a8391c976/rpds_py-0.26.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:777c62479d12395bfb932944e61e915741e364c843afc3196b694db3d669fcd0", size = 357825 },
+ { url = "https://files.pythonhosted.org/packages/bf/ce/badc5e06120a54099ae287fa96d82cbb650a5f85cf247ffe19c7b157fd1f/rpds_py-0.26.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec671691e72dff75817386aa02d81e708b5a7ec0dec6669ec05213ff6b77e1bd", size = 381530 },
+ { url = "https://files.pythonhosted.org/packages/1e/a5/fa5d96a66c95d06c62d7a30707b6a4cfec696ab8ae280ee7be14e961e118/rpds_py-0.26.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6a1cb5d6ce81379401bbb7f6dbe3d56de537fb8235979843f0d53bc2e9815a79", size = 396933 },
+ { url = "https://files.pythonhosted.org/packages/00/a7/7049d66750f18605c591a9db47d4a059e112a0c9ff8de8daf8fa0f446bba/rpds_py-0.26.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4f789e32fa1fb6a7bf890e0124e7b42d1e60d28ebff57fe806719abb75f0e9a3", size = 513973 },
+ { url = "https://files.pythonhosted.org/packages/0e/f1/528d02c7d6b29d29fac8fd784b354d3571cc2153f33f842599ef0cf20dd2/rpds_py-0.26.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c55b0a669976cf258afd718de3d9ad1b7d1fe0a91cd1ab36f38b03d4d4aeaaf", size = 402293 },
+ { url = "https://files.pythonhosted.org/packages/15/93/fde36cd6e4685df2cd08508f6c45a841e82f5bb98c8d5ecf05649522acb5/rpds_py-0.26.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c70d9ec912802ecfd6cd390dadb34a9578b04f9bcb8e863d0a7598ba5e9e7ccc", size = 383787 },
+ { url = "https://files.pythonhosted.org/packages/69/f2/5007553aaba1dcae5d663143683c3dfd03d9395289f495f0aebc93e90f24/rpds_py-0.26.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3021933c2cb7def39d927b9862292e0f4c75a13d7de70eb0ab06efed4c508c19", size = 416312 },
+ { url = "https://files.pythonhosted.org/packages/8f/a7/ce52c75c1e624a79e48a69e611f1c08844564e44c85db2b6f711d76d10ce/rpds_py-0.26.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8a7898b6ca3b7d6659e55cdac825a2e58c638cbf335cde41f4619e290dd0ad11", size = 558403 },
+ { url = "https://files.pythonhosted.org/packages/79/d5/e119db99341cc75b538bf4cb80504129fa22ce216672fb2c28e4a101f4d9/rpds_py-0.26.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:12bff2ad9447188377f1b2794772f91fe68bb4bbfa5a39d7941fbebdbf8c500f", size = 588323 },
+ { url = "https://files.pythonhosted.org/packages/93/94/d28272a0b02f5fe24c78c20e13bbcb95f03dc1451b68e7830ca040c60bd6/rpds_py-0.26.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:191aa858f7d4902e975d4cf2f2d9243816c91e9605070aeb09c0a800d187e323", size = 554541 },
+ { url = "https://files.pythonhosted.org/packages/93/e0/8c41166602f1b791da892d976057eba30685486d2e2c061ce234679c922b/rpds_py-0.26.0-cp310-cp310-win32.whl", hash = "sha256:b37a04d9f52cb76b6b78f35109b513f6519efb481d8ca4c321f6a3b9580b3f45", size = 220442 },
+ { url = "https://files.pythonhosted.org/packages/87/f0/509736bb752a7ab50fb0270c2a4134d671a7b3038030837e5536c3de0e0b/rpds_py-0.26.0-cp310-cp310-win_amd64.whl", hash = "sha256:38721d4c9edd3eb6670437d8d5e2070063f305bfa2d5aa4278c51cedcd508a84", size = 231314 },
+ { url = "https://files.pythonhosted.org/packages/09/4c/4ee8f7e512030ff79fda1df3243c88d70fc874634e2dbe5df13ba4210078/rpds_py-0.26.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:9e8cb77286025bdb21be2941d64ac6ca016130bfdcd228739e8ab137eb4406ed", size = 372610 },
+ { url = "https://files.pythonhosted.org/packages/fa/9d/3dc16be00f14fc1f03c71b1d67c8df98263ab2710a2fbd65a6193214a527/rpds_py-0.26.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5e09330b21d98adc8ccb2dbb9fc6cb434e8908d4c119aeaa772cb1caab5440a0", size = 358032 },
+ { url = "https://files.pythonhosted.org/packages/e7/5a/7f1bf8f045da2866324a08ae80af63e64e7bfaf83bd31f865a7b91a58601/rpds_py-0.26.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c9c1b92b774b2e68d11193dc39620d62fd8ab33f0a3c77ecdabe19c179cdbc1", size = 381525 },
+ { url = "https://files.pythonhosted.org/packages/45/8a/04479398c755a066ace10e3d158866beb600867cacae194c50ffa783abd0/rpds_py-0.26.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:824e6d3503ab990d7090768e4dfd9e840837bae057f212ff9f4f05ec6d1975e7", size = 397089 },
+ { url = "https://files.pythonhosted.org/packages/72/88/9203f47268db488a1b6d469d69c12201ede776bb728b9d9f29dbfd7df406/rpds_py-0.26.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8ad7fd2258228bf288f2331f0a6148ad0186b2e3643055ed0db30990e59817a6", size = 514255 },
+ { url = "https://files.pythonhosted.org/packages/f5/b4/01ce5d1e853ddf81fbbd4311ab1eff0b3cf162d559288d10fd127e2588b5/rpds_py-0.26.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0dc23bbb3e06ec1ea72d515fb572c1fea59695aefbffb106501138762e1e915e", size = 402283 },
+ { url = "https://files.pythonhosted.org/packages/34/a2/004c99936997bfc644d590a9defd9e9c93f8286568f9c16cdaf3e14429a7/rpds_py-0.26.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d80bf832ac7b1920ee29a426cdca335f96a2b5caa839811803e999b41ba9030d", size = 383881 },
+ { url = "https://files.pythonhosted.org/packages/05/1b/ef5fba4a8f81ce04c427bfd96223f92f05e6cd72291ce9d7523db3b03a6c/rpds_py-0.26.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0919f38f5542c0a87e7b4afcafab6fd2c15386632d249e9a087498571250abe3", size = 415822 },
+ { url = "https://files.pythonhosted.org/packages/16/80/5c54195aec456b292f7bd8aa61741c8232964063fd8a75fdde9c1e982328/rpds_py-0.26.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d422b945683e409000c888e384546dbab9009bb92f7c0b456e217988cf316107", size = 558347 },
+ { url = "https://files.pythonhosted.org/packages/f2/1c/1845c1b1fd6d827187c43afe1841d91678d7241cbdb5420a4c6de180a538/rpds_py-0.26.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:77a7711fa562ba2da1aa757e11024ad6d93bad6ad7ede5afb9af144623e5f76a", size = 587956 },
+ { url = "https://files.pythonhosted.org/packages/2e/ff/9e979329dd131aa73a438c077252ddabd7df6d1a7ad7b9aacf6261f10faa/rpds_py-0.26.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:238e8c8610cb7c29460e37184f6799547f7e09e6a9bdbdab4e8edb90986a2318", size = 554363 },
+ { url = "https://files.pythonhosted.org/packages/00/8b/d78cfe034b71ffbe72873a136e71acc7a831a03e37771cfe59f33f6de8a2/rpds_py-0.26.0-cp311-cp311-win32.whl", hash = "sha256:893b022bfbdf26d7bedb083efeea624e8550ca6eb98bf7fea30211ce95b9201a", size = 220123 },
+ { url = "https://files.pythonhosted.org/packages/94/c1/3c8c94c7dd3905dbfde768381ce98778500a80db9924731d87ddcdb117e9/rpds_py-0.26.0-cp311-cp311-win_amd64.whl", hash = "sha256:87a5531de9f71aceb8af041d72fc4cab4943648d91875ed56d2e629bef6d4c03", size = 231732 },
+ { url = "https://files.pythonhosted.org/packages/67/93/e936fbed1b734eabf36ccb5d93c6a2e9246fbb13c1da011624b7286fae3e/rpds_py-0.26.0-cp311-cp311-win_arm64.whl", hash = "sha256:de2713f48c1ad57f89ac25b3cb7daed2156d8e822cf0eca9b96a6f990718cc41", size = 221917 },
+ { url = "https://files.pythonhosted.org/packages/ea/86/90eb87c6f87085868bd077c7a9938006eb1ce19ed4d06944a90d3560fce2/rpds_py-0.26.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:894514d47e012e794f1350f076c427d2347ebf82f9b958d554d12819849a369d", size = 363933 },
+ { url = "https://files.pythonhosted.org/packages/63/78/4469f24d34636242c924626082b9586f064ada0b5dbb1e9d096ee7a8e0c6/rpds_py-0.26.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc921b96fa95a097add244da36a1d9e4f3039160d1d30f1b35837bf108c21136", size = 350447 },
+ { url = "https://files.pythonhosted.org/packages/ad/91/c448ed45efdfdade82348d5e7995e15612754826ea640afc20915119734f/rpds_py-0.26.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e1157659470aa42a75448b6e943c895be8c70531c43cb78b9ba990778955582", size = 384711 },
+ { url = "https://files.pythonhosted.org/packages/ec/43/e5c86fef4be7f49828bdd4ecc8931f0287b1152c0bb0163049b3218740e7/rpds_py-0.26.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:521ccf56f45bb3a791182dc6b88ae5f8fa079dd705ee42138c76deb1238e554e", size = 400865 },
+ { url = "https://files.pythonhosted.org/packages/55/34/e00f726a4d44f22d5c5fe2e5ddd3ac3d7fd3f74a175607781fbdd06fe375/rpds_py-0.26.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9def736773fd56b305c0eef698be5192c77bfa30d55a0e5885f80126c4831a15", size = 517763 },
+ { url = "https://files.pythonhosted.org/packages/52/1c/52dc20c31b147af724b16104500fba13e60123ea0334beba7b40e33354b4/rpds_py-0.26.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cdad4ea3b4513b475e027be79e5a0ceac8ee1c113a1a11e5edc3c30c29f964d8", size = 406651 },
+ { url = "https://files.pythonhosted.org/packages/2e/77/87d7bfabfc4e821caa35481a2ff6ae0b73e6a391bb6b343db2c91c2b9844/rpds_py-0.26.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82b165b07f416bdccf5c84546a484cc8f15137ca38325403864bfdf2b5b72f6a", size = 386079 },
+ { url = "https://files.pythonhosted.org/packages/e3/d4/7f2200c2d3ee145b65b3cddc4310d51f7da6a26634f3ac87125fd789152a/rpds_py-0.26.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d04cab0a54b9dba4d278fe955a1390da3cf71f57feb78ddc7cb67cbe0bd30323", size = 421379 },
+ { url = "https://files.pythonhosted.org/packages/ae/13/9fdd428b9c820869924ab62236b8688b122baa22d23efdd1c566938a39ba/rpds_py-0.26.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:79061ba1a11b6a12743a2b0f72a46aa2758613d454aa6ba4f5a265cc48850158", size = 562033 },
+ { url = "https://files.pythonhosted.org/packages/f3/e1/b69686c3bcbe775abac3a4c1c30a164a2076d28df7926041f6c0eb5e8d28/rpds_py-0.26.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f405c93675d8d4c5ac87364bb38d06c988e11028a64b52a47158a355079661f3", size = 591639 },
+ { url = "https://files.pythonhosted.org/packages/5c/c9/1e3d8c8863c84a90197ac577bbc3d796a92502124c27092413426f670990/rpds_py-0.26.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dafd4c44b74aa4bed4b250f1aed165b8ef5de743bcca3b88fc9619b6087093d2", size = 557105 },
+ { url = "https://files.pythonhosted.org/packages/9f/c5/90c569649057622959f6dcc40f7b516539608a414dfd54b8d77e3b201ac0/rpds_py-0.26.0-cp312-cp312-win32.whl", hash = "sha256:3da5852aad63fa0c6f836f3359647870e21ea96cf433eb393ffa45263a170d44", size = 223272 },
+ { url = "https://files.pythonhosted.org/packages/7d/16/19f5d9f2a556cfed454eebe4d354c38d51c20f3db69e7b4ce6cff904905d/rpds_py-0.26.0-cp312-cp312-win_amd64.whl", hash = "sha256:cf47cfdabc2194a669dcf7a8dbba62e37a04c5041d2125fae0233b720da6f05c", size = 234995 },
+ { url = "https://files.pythonhosted.org/packages/83/f0/7935e40b529c0e752dfaa7880224771b51175fce08b41ab4a92eb2fbdc7f/rpds_py-0.26.0-cp312-cp312-win_arm64.whl", hash = "sha256:20ab1ae4fa534f73647aad289003f1104092890849e0266271351922ed5574f8", size = 223198 },
+ { url = "https://files.pythonhosted.org/packages/6a/67/bb62d0109493b12b1c6ab00de7a5566aa84c0e44217c2d94bee1bd370da9/rpds_py-0.26.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:696764a5be111b036256c0b18cd29783fab22154690fc698062fc1b0084b511d", size = 363917 },
+ { url = "https://files.pythonhosted.org/packages/4b/f3/34e6ae1925a5706c0f002a8d2d7f172373b855768149796af87bd65dcdb9/rpds_py-0.26.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1e6c15d2080a63aaed876e228efe4f814bc7889c63b1e112ad46fdc8b368b9e1", size = 350073 },
+ { url = "https://files.pythonhosted.org/packages/75/83/1953a9d4f4e4de7fd0533733e041c28135f3c21485faaef56a8aadbd96b5/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:390e3170babf42462739a93321e657444f0862c6d722a291accc46f9d21ed04e", size = 384214 },
+ { url = "https://files.pythonhosted.org/packages/48/0e/983ed1b792b3322ea1d065e67f4b230f3b96025f5ce3878cc40af09b7533/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7da84c2c74c0f5bc97d853d9e17bb83e2dcafcff0dc48286916001cc114379a1", size = 400113 },
+ { url = "https://files.pythonhosted.org/packages/69/7f/36c0925fff6f660a80be259c5b4f5e53a16851f946eb080351d057698528/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c5fe114a6dd480a510b6d3661d09d67d1622c4bf20660a474507aaee7eeeee9", size = 515189 },
+ { url = "https://files.pythonhosted.org/packages/13/45/cbf07fc03ba7a9b54662c9badb58294ecfb24f828b9732970bd1a431ed5c/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3100b3090269f3a7ea727b06a6080d4eb7439dca4c0e91a07c5d133bb1727ea7", size = 406998 },
+ { url = "https://files.pythonhosted.org/packages/6c/b0/8fa5e36e58657997873fd6a1cf621285ca822ca75b4b3434ead047daa307/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c03c9b0c64afd0320ae57de4c982801271c0c211aa2d37f3003ff5feb75bb04", size = 385903 },
+ { url = "https://files.pythonhosted.org/packages/4b/f7/b25437772f9f57d7a9fbd73ed86d0dcd76b4c7c6998348c070d90f23e315/rpds_py-0.26.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5963b72ccd199ade6ee493723d18a3f21ba7d5b957017607f815788cef50eaf1", size = 419785 },
+ { url = "https://files.pythonhosted.org/packages/a7/6b/63ffa55743dfcb4baf2e9e77a0b11f7f97ed96a54558fcb5717a4b2cd732/rpds_py-0.26.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9da4e873860ad5bab3291438525cae80169daecbfafe5657f7f5fb4d6b3f96b9", size = 561329 },
+ { url = "https://files.pythonhosted.org/packages/2f/07/1f4f5e2886c480a2346b1e6759c00278b8a69e697ae952d82ae2e6ee5db0/rpds_py-0.26.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5afaddaa8e8c7f1f7b4c5c725c0070b6eed0228f705b90a1732a48e84350f4e9", size = 590875 },
+ { url = "https://files.pythonhosted.org/packages/cc/bc/e6639f1b91c3a55f8c41b47d73e6307051b6e246254a827ede730624c0f8/rpds_py-0.26.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4916dc96489616a6f9667e7526af8fa693c0fdb4f3acb0e5d9f4400eb06a47ba", size = 556636 },
+ { url = "https://files.pythonhosted.org/packages/05/4c/b3917c45566f9f9a209d38d9b54a1833f2bb1032a3e04c66f75726f28876/rpds_py-0.26.0-cp313-cp313-win32.whl", hash = "sha256:2a343f91b17097c546b93f7999976fd6c9d5900617aa848c81d794e062ab302b", size = 222663 },
+ { url = "https://files.pythonhosted.org/packages/e0/0b/0851bdd6025775aaa2365bb8de0697ee2558184c800bfef8d7aef5ccde58/rpds_py-0.26.0-cp313-cp313-win_amd64.whl", hash = "sha256:0a0b60701f2300c81b2ac88a5fb893ccfa408e1c4a555a77f908a2596eb875a5", size = 234428 },
+ { url = "https://files.pythonhosted.org/packages/ed/e8/a47c64ed53149c75fb581e14a237b7b7cd18217e969c30d474d335105622/rpds_py-0.26.0-cp313-cp313-win_arm64.whl", hash = "sha256:257d011919f133a4746958257f2c75238e3ff54255acd5e3e11f3ff41fd14256", size = 222571 },
+ { url = "https://files.pythonhosted.org/packages/89/bf/3d970ba2e2bcd17d2912cb42874107390f72873e38e79267224110de5e61/rpds_py-0.26.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:529c8156d7506fba5740e05da8795688f87119cce330c244519cf706a4a3d618", size = 360475 },
+ { url = "https://files.pythonhosted.org/packages/82/9f/283e7e2979fc4ec2d8ecee506d5a3675fce5ed9b4b7cb387ea5d37c2f18d/rpds_py-0.26.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f53ec51f9d24e9638a40cabb95078ade8c99251945dad8d57bf4aabe86ecee35", size = 346692 },
+ { url = "https://files.pythonhosted.org/packages/e3/03/7e50423c04d78daf391da3cc4330bdb97042fc192a58b186f2d5deb7befd/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab504c4d654e4a29558eaa5bb8cea5fdc1703ea60a8099ffd9c758472cf913f", size = 379415 },
+ { url = "https://files.pythonhosted.org/packages/57/00/d11ee60d4d3b16808432417951c63df803afb0e0fc672b5e8d07e9edaaae/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd0641abca296bc1a00183fe44f7fced8807ed49d501f188faa642d0e4975b83", size = 391783 },
+ { url = "https://files.pythonhosted.org/packages/08/b3/1069c394d9c0d6d23c5b522e1f6546b65793a22950f6e0210adcc6f97c3e/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69b312fecc1d017b5327afa81d4da1480f51c68810963a7336d92203dbb3d4f1", size = 512844 },
+ { url = "https://files.pythonhosted.org/packages/08/3b/c4fbf0926800ed70b2c245ceca99c49f066456755f5d6eb8863c2c51e6d0/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c741107203954f6fc34d3066d213d0a0c40f7bb5aafd698fb39888af277c70d8", size = 402105 },
+ { url = "https://files.pythonhosted.org/packages/1c/b0/db69b52ca07413e568dae9dc674627a22297abb144c4d6022c6d78f1e5cc/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc3e55a7db08dc9a6ed5fb7103019d2c1a38a349ac41901f9f66d7f95750942f", size = 383440 },
+ { url = "https://files.pythonhosted.org/packages/4c/e1/c65255ad5b63903e56b3bb3ff9dcc3f4f5c3badde5d08c741ee03903e951/rpds_py-0.26.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9e851920caab2dbcae311fd28f4313c6953993893eb5c1bb367ec69d9a39e7ed", size = 412759 },
+ { url = "https://files.pythonhosted.org/packages/e4/22/bb731077872377a93c6e93b8a9487d0406c70208985831034ccdeed39c8e/rpds_py-0.26.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dfbf280da5f876d0b00c81f26bedce274e72a678c28845453885a9b3c22ae632", size = 556032 },
+ { url = "https://files.pythonhosted.org/packages/e0/8b/393322ce7bac5c4530fb96fc79cc9ea2f83e968ff5f6e873f905c493e1c4/rpds_py-0.26.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:1cc81d14ddfa53d7f3906694d35d54d9d3f850ef8e4e99ee68bc0d1e5fed9a9c", size = 585416 },
+ { url = "https://files.pythonhosted.org/packages/49/ae/769dc372211835bf759319a7aae70525c6eb523e3371842c65b7ef41c9c6/rpds_py-0.26.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dca83c498b4650a91efcf7b88d669b170256bf8017a5db6f3e06c2bf031f57e0", size = 554049 },
+ { url = "https://files.pythonhosted.org/packages/6b/f9/4c43f9cc203d6ba44ce3146246cdc38619d92c7bd7bad4946a3491bd5b70/rpds_py-0.26.0-cp313-cp313t-win32.whl", hash = "sha256:4d11382bcaf12f80b51d790dee295c56a159633a8e81e6323b16e55d81ae37e9", size = 218428 },
+ { url = "https://files.pythonhosted.org/packages/7e/8b/9286b7e822036a4a977f2f1e851c7345c20528dbd56b687bb67ed68a8ede/rpds_py-0.26.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff110acded3c22c033e637dd8896e411c7d3a11289b2edf041f86663dbc791e9", size = 231524 },
+ { url = "https://files.pythonhosted.org/packages/55/07/029b7c45db910c74e182de626dfdae0ad489a949d84a468465cd0ca36355/rpds_py-0.26.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:da619979df60a940cd434084355c514c25cf8eb4cf9a508510682f6c851a4f7a", size = 364292 },
+ { url = "https://files.pythonhosted.org/packages/13/d1/9b3d3f986216b4d1f584878dca15ce4797aaf5d372d738974ba737bf68d6/rpds_py-0.26.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ea89a2458a1a75f87caabefe789c87539ea4e43b40f18cff526052e35bbb4fdf", size = 350334 },
+ { url = "https://files.pythonhosted.org/packages/18/98/16d5e7bc9ec715fa9668731d0cf97f6b032724e61696e2db3d47aeb89214/rpds_py-0.26.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feac1045b3327a45944e7dcbeb57530339f6b17baff154df51ef8b0da34c8c12", size = 384875 },
+ { url = "https://files.pythonhosted.org/packages/f9/13/aa5e2b1ec5ab0e86a5c464d53514c0467bec6ba2507027d35fc81818358e/rpds_py-0.26.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b818a592bd69bfe437ee8368603d4a2d928c34cffcdf77c2e761a759ffd17d20", size = 399993 },
+ { url = "https://files.pythonhosted.org/packages/17/03/8021810b0e97923abdbab6474c8b77c69bcb4b2c58330777df9ff69dc559/rpds_py-0.26.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a8b0dd8648709b62d9372fc00a57466f5fdeefed666afe3fea5a6c9539a0331", size = 516683 },
+ { url = "https://files.pythonhosted.org/packages/dc/b1/da8e61c87c2f3d836954239fdbbfb477bb7b54d74974d8f6fcb34342d166/rpds_py-0.26.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6d3498ad0df07d81112aa6ec6c95a7e7b1ae00929fb73e7ebee0f3faaeabad2f", size = 408825 },
+ { url = "https://files.pythonhosted.org/packages/38/bc/1fc173edaaa0e52c94b02a655db20697cb5fa954ad5a8e15a2c784c5cbdd/rpds_py-0.26.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24a4146ccb15be237fdef10f331c568e1b0e505f8c8c9ed5d67759dac58ac246", size = 387292 },
+ { url = "https://files.pythonhosted.org/packages/7c/eb/3a9bb4bd90867d21916f253caf4f0d0be7098671b6715ad1cead9fe7bab9/rpds_py-0.26.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a9a63785467b2d73635957d32a4f6e73d5e4df497a16a6392fa066b753e87387", size = 420435 },
+ { url = "https://files.pythonhosted.org/packages/cd/16/e066dcdb56f5632713445271a3f8d3d0b426d51ae9c0cca387799df58b02/rpds_py-0.26.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:de4ed93a8c91debfd5a047be327b7cc8b0cc6afe32a716bbbc4aedca9e2a83af", size = 562410 },
+ { url = "https://files.pythonhosted.org/packages/60/22/ddbdec7eb82a0dc2e455be44c97c71c232983e21349836ce9f272e8a3c29/rpds_py-0.26.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:caf51943715b12af827696ec395bfa68f090a4c1a1d2509eb4e2cb69abbbdb33", size = 590724 },
+ { url = "https://files.pythonhosted.org/packages/2c/b4/95744085e65b7187d83f2fcb0bef70716a1ea0a9e5d8f7f39a86e5d83424/rpds_py-0.26.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4a59e5bc386de021f56337f757301b337d7ab58baa40174fb150accd480bc953", size = 558285 },
+ { url = "https://files.pythonhosted.org/packages/37/37/6309a75e464d1da2559446f9c811aa4d16343cebe3dbb73701e63f760caa/rpds_py-0.26.0-cp314-cp314-win32.whl", hash = "sha256:92c8db839367ef16a662478f0a2fe13e15f2227da3c1430a782ad0f6ee009ec9", size = 223459 },
+ { url = "https://files.pythonhosted.org/packages/d9/6f/8e9c11214c46098b1d1391b7e02b70bb689ab963db3b19540cba17315291/rpds_py-0.26.0-cp314-cp314-win_amd64.whl", hash = "sha256:b0afb8cdd034150d4d9f53926226ed27ad15b7f465e93d7468caaf5eafae0d37", size = 236083 },
+ { url = "https://files.pythonhosted.org/packages/47/af/9c4638994dd623d51c39892edd9d08e8be8220a4b7e874fa02c2d6e91955/rpds_py-0.26.0-cp314-cp314-win_arm64.whl", hash = "sha256:ca3f059f4ba485d90c8dc75cb5ca897e15325e4e609812ce57f896607c1c0867", size = 223291 },
+ { url = "https://files.pythonhosted.org/packages/4d/db/669a241144460474aab03e254326b32c42def83eb23458a10d163cb9b5ce/rpds_py-0.26.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:5afea17ab3a126006dc2f293b14ffc7ef3c85336cf451564a0515ed7648033da", size = 361445 },
+ { url = "https://files.pythonhosted.org/packages/3b/2d/133f61cc5807c6c2fd086a46df0eb8f63a23f5df8306ff9f6d0fd168fecc/rpds_py-0.26.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:69f0c0a3df7fd3a7eec50a00396104bb9a843ea6d45fcc31c2d5243446ffd7a7", size = 347206 },
+ { url = "https://files.pythonhosted.org/packages/05/bf/0e8fb4c05f70273469eecf82f6ccf37248558526a45321644826555db31b/rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:801a71f70f9813e82d2513c9a96532551fce1e278ec0c64610992c49c04c2dad", size = 380330 },
+ { url = "https://files.pythonhosted.org/packages/d4/a8/060d24185d8b24d3923322f8d0ede16df4ade226a74e747b8c7c978e3dd3/rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:df52098cde6d5e02fa75c1f6244f07971773adb4a26625edd5c18fee906fa84d", size = 392254 },
+ { url = "https://files.pythonhosted.org/packages/b9/7b/7c2e8a9ee3e6bc0bae26bf29f5219955ca2fbb761dca996a83f5d2f773fe/rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9bc596b30f86dc6f0929499c9e574601679d0341a0108c25b9b358a042f51bca", size = 516094 },
+ { url = "https://files.pythonhosted.org/packages/75/d6/f61cafbed8ba1499b9af9f1777a2a199cd888f74a96133d8833ce5eaa9c5/rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9dfbe56b299cf5875b68eb6f0ebaadc9cac520a1989cac0db0765abfb3709c19", size = 402889 },
+ { url = "https://files.pythonhosted.org/packages/92/19/c8ac0a8a8df2dd30cdec27f69298a5c13e9029500d6d76718130f5e5be10/rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac64f4b2bdb4ea622175c9ab7cf09444e412e22c0e02e906978b3b488af5fde8", size = 384301 },
+ { url = "https://files.pythonhosted.org/packages/41/e1/6b1859898bc292a9ce5776016c7312b672da00e25cec74d7beced1027286/rpds_py-0.26.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:181ef9b6bbf9845a264f9aa45c31836e9f3c1f13be565d0d010e964c661d1e2b", size = 412891 },
+ { url = "https://files.pythonhosted.org/packages/ef/b9/ceb39af29913c07966a61367b3c08b4f71fad841e32c6b59a129d5974698/rpds_py-0.26.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:49028aa684c144ea502a8e847d23aed5e4c2ef7cadfa7d5eaafcb40864844b7a", size = 557044 },
+ { url = "https://files.pythonhosted.org/packages/2f/27/35637b98380731a521f8ec4f3fd94e477964f04f6b2f8f7af8a2d889a4af/rpds_py-0.26.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e5d524d68a474a9688336045bbf76cb0def88549c1b2ad9dbfec1fb7cfbe9170", size = 585774 },
+ { url = "https://files.pythonhosted.org/packages/52/d9/3f0f105420fecd18551b678c9a6ce60bd23986098b252a56d35781b3e7e9/rpds_py-0.26.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c1851f429b822831bd2edcbe0cfd12ee9ea77868f8d3daf267b189371671c80e", size = 554886 },
+ { url = "https://files.pythonhosted.org/packages/6b/c5/347c056a90dc8dd9bc240a08c527315008e1b5042e7a4cf4ac027be9d38a/rpds_py-0.26.0-cp314-cp314t-win32.whl", hash = "sha256:7bdb17009696214c3b66bb3590c6d62e14ac5935e53e929bcdbc5a495987a84f", size = 219027 },
+ { url = "https://files.pythonhosted.org/packages/75/04/5302cea1aa26d886d34cadbf2dc77d90d7737e576c0065f357b96dc7a1a6/rpds_py-0.26.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f14440b9573a6f76b4ee4770c13f0b5921f71dde3b6fcb8dabbefd13b7fe05d7", size = 232821 },
+ { url = "https://files.pythonhosted.org/packages/ef/9a/1f033b0b31253d03d785b0cd905bc127e555ab496ea6b4c7c2e1f951f2fd/rpds_py-0.26.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3c0909c5234543ada2515c05dc08595b08d621ba919629e94427e8e03539c958", size = 373226 },
+ { url = "https://files.pythonhosted.org/packages/58/29/5f88023fd6aaaa8ca3c4a6357ebb23f6f07da6079093ccf27c99efce87db/rpds_py-0.26.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:c1fb0cda2abcc0ac62f64e2ea4b4e64c57dfd6b885e693095460c61bde7bb18e", size = 359230 },
+ { url = "https://files.pythonhosted.org/packages/6c/6c/13eaebd28b439da6964dde22712b52e53fe2824af0223b8e403249d10405/rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84d142d2d6cf9b31c12aa4878d82ed3b2324226270b89b676ac62ccd7df52d08", size = 382363 },
+ { url = "https://files.pythonhosted.org/packages/55/fc/3bb9c486b06da19448646f96147796de23c5811ef77cbfc26f17307b6a9d/rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a547e21c5610b7e9093d870be50682a6a6cf180d6da0f42c47c306073bfdbbf6", size = 397146 },
+ { url = "https://files.pythonhosted.org/packages/15/18/9d1b79eb4d18e64ba8bba9e7dec6f9d6920b639f22f07ee9368ca35d4673/rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:35e9a70a0f335371275cdcd08bc5b8051ac494dd58bff3bbfb421038220dc871", size = 514804 },
+ { url = "https://files.pythonhosted.org/packages/4f/5a/175ad7191bdbcd28785204621b225ad70e85cdfd1e09cc414cb554633b21/rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0dfa6115c6def37905344d56fb54c03afc49104e2ca473d5dedec0f6606913b4", size = 402820 },
+ { url = "https://files.pythonhosted.org/packages/11/45/6a67ecf6d61c4d4aff4bc056e864eec4b2447787e11d1c2c9a0242c6e92a/rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:313cfcd6af1a55a286a3c9a25f64af6d0e46cf60bc5798f1db152d97a216ff6f", size = 384567 },
+ { url = "https://files.pythonhosted.org/packages/a1/ba/16589da828732b46454c61858950a78fe4c931ea4bf95f17432ffe64b241/rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f7bf2496fa563c046d05e4d232d7b7fd61346e2402052064b773e5c378bf6f73", size = 416520 },
+ { url = "https://files.pythonhosted.org/packages/81/4b/00092999fc7c0c266045e984d56b7314734cc400a6c6dc4d61a35f135a9d/rpds_py-0.26.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:aa81873e2c8c5aa616ab8e017a481a96742fdf9313c40f14338ca7dbf50cb55f", size = 559362 },
+ { url = "https://files.pythonhosted.org/packages/96/0c/43737053cde1f93ac4945157f7be1428724ab943e2132a0d235a7e161d4e/rpds_py-0.26.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:68ffcf982715f5b5b7686bdd349ff75d422e8f22551000c24b30eaa1b7f7ae84", size = 588113 },
+ { url = "https://files.pythonhosted.org/packages/46/46/8e38f6161466e60a997ed7e9951ae5de131dedc3cf778ad35994b4af823d/rpds_py-0.26.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:6188de70e190847bb6db3dc3981cbadff87d27d6fe9b4f0e18726d55795cee9b", size = 555429 },
+ { url = "https://files.pythonhosted.org/packages/2c/ac/65da605e9f1dd643ebe615d5bbd11b6efa1d69644fc4bf623ea5ae385a82/rpds_py-0.26.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:1c962145c7473723df9722ba4c058de12eb5ebedcb4e27e7d902920aa3831ee8", size = 231950 },
+ { url = "https://files.pythonhosted.org/packages/51/f2/b5c85b758a00c513bb0389f8fc8e61eb5423050c91c958cdd21843faa3e6/rpds_py-0.26.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f61a9326f80ca59214d1cceb0a09bb2ece5b2563d4e0cd37bfd5515c28510674", size = 373505 },
+ { url = "https://files.pythonhosted.org/packages/23/e0/25db45e391251118e915e541995bb5f5ac5691a3b98fb233020ba53afc9b/rpds_py-0.26.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:183f857a53bcf4b1b42ef0f57ca553ab56bdd170e49d8091e96c51c3d69ca696", size = 359468 },
+ { url = "https://files.pythonhosted.org/packages/0b/73/dd5ee6075bb6491be3a646b301dfd814f9486d924137a5098e61f0487e16/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:941c1cfdf4799d623cf3aa1d326a6b4fdb7a5799ee2687f3516738216d2262fb", size = 382680 },
+ { url = "https://files.pythonhosted.org/packages/2f/10/84b522ff58763a5c443f5bcedc1820240e454ce4e620e88520f04589e2ea/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72a8d9564a717ee291f554eeb4bfeafe2309d5ec0aa6c475170bdab0f9ee8e88", size = 397035 },
+ { url = "https://files.pythonhosted.org/packages/06/ea/8667604229a10a520fcbf78b30ccc278977dcc0627beb7ea2c96b3becef0/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:511d15193cbe013619dd05414c35a7dedf2088fcee93c6bbb7c77859765bd4e8", size = 514922 },
+ { url = "https://files.pythonhosted.org/packages/24/e6/9ed5b625c0661c4882fc8cdf302bf8e96c73c40de99c31e0b95ed37d508c/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aea1f9741b603a8d8fedb0ed5502c2bc0accbc51f43e2ad1337fe7259c2b77a5", size = 402822 },
+ { url = "https://files.pythonhosted.org/packages/8a/58/212c7b6fd51946047fb45d3733da27e2fa8f7384a13457c874186af691b1/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4019a9d473c708cf2f16415688ef0b4639e07abaa569d72f74745bbeffafa2c7", size = 384336 },
+ { url = "https://files.pythonhosted.org/packages/aa/f5/a40ba78748ae8ebf4934d4b88e77b98497378bc2c24ba55ebe87a4e87057/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:093d63b4b0f52d98ebae33b8c50900d3d67e0666094b1be7a12fffd7f65de74b", size = 416871 },
+ { url = "https://files.pythonhosted.org/packages/d5/a6/33b1fc0c9f7dcfcfc4a4353daa6308b3ece22496ceece348b3e7a7559a09/rpds_py-0.26.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2abe21d8ba64cded53a2a677e149ceb76dcf44284202d737178afe7ba540c1eb", size = 559439 },
+ { url = "https://files.pythonhosted.org/packages/71/2d/ceb3f9c12f8cfa56d34995097f6cd99da1325642c60d1b6680dd9df03ed8/rpds_py-0.26.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:4feb7511c29f8442cbbc28149a92093d32e815a28aa2c50d333826ad2a20fdf0", size = 588380 },
+ { url = "https://files.pythonhosted.org/packages/c8/ed/9de62c2150ca8e2e5858acf3f4f4d0d180a38feef9fdab4078bea63d8dba/rpds_py-0.26.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:e99685fc95d386da368013e7fb4269dd39c30d99f812a8372d62f244f662709c", size = 555334 },
+]
+
+[[package]]
+name = "sniffio"
+version = "1.3.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 },
+]
+
+[[package]]
+name = "sse-starlette"
+version = "2.2.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "starlette" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/71/a4/80d2a11af59fe75b48230846989e93979c892d3a20016b42bb44edb9e398/sse_starlette-2.2.1.tar.gz", hash = "sha256:54470d5f19274aeed6b2d473430b08b4b379ea851d953b11d7f1c4a2c118b419", size = 17376 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d9/e0/5b8bd393f27f4a62461c5cf2479c75a2cc2ffa330976f9f00f5f6e4f50eb/sse_starlette-2.2.1-py3-none-any.whl", hash = "sha256:6410a3d3ba0c89e7675d4c273a301d64649c03a5ef1ca101f10b47f895fd0e99", size = 10120 },
+]
+
+[[package]]
+name = "starlette"
+version = "0.46.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ce/20/08dfcd9c983f6a6f4a1000d934b9e6d626cff8d2eeb77a89a68eef20a2b7/starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5", size = 2580846 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35", size = 72037 },
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.13.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806 },
+]
+
+[[package]]
+name = "typing-inspection"
+version = "0.4.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/82/5c/e6082df02e215b846b4b8c0b887a64d7d08ffaba30605502639d44c06b82/typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122", size = 76222 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f", size = 14125 },
+]
+
+[[package]]
+name = "uvicorn"
+version = "0.34.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+ { name = "h11" },
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a6/ae/9bbb19b9e1c450cf9ecaef06463e40234d98d95bf572fab11b4f19ae5ded/uvicorn-0.34.2.tar.gz", hash = "sha256:0e929828f6186353a80b58ea719861d2629d766293b6d19baf086ba31d4f3328", size = 76815 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b1/4b/4cef6ce21a2aaca9d852a6e84ef4f135d99fcd74fa75105e2fc0c8308acd/uvicorn-0.34.2-py3-none-any.whl", hash = "sha256:deb49af569084536d269fe0a6d67e3754f104cf03aba7c11c40f01aadf33c403", size = 62483 },
+]
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/.gitignore b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/.gitignore
new file mode 100644
index 00000000..505a3b1c
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/.gitignore
@@ -0,0 +1,10 @@
+# Python-generated files
+__pycache__/
+*.py[oc]
+build/
+dist/
+wheels/
+*.egg-info
+
+# Virtual environments
+.venv
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/.python-version b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/.python-version
new file mode 100644
index 00000000..e4fba218
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/.python-version
@@ -0,0 +1 @@
+3.12
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/LICENSE b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/LICENSE
new file mode 100644
index 00000000..29dd7ce9
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2025 lockon-n
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/README.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/README.md
new file mode 100644
index 00000000..aa54eb00
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/README.md
@@ -0,0 +1,369 @@
+# Emails MCP Server
+
+A FastMCP-based email management server for IMAP/SMTP operations through the Model Context Protocol (MCP).
+
+## Features
+
+- 📧 **Email Management**: Get, read, search emails with pagination support
+- 📤 **Send & Reply**: Send emails with HTML/text, attachments, CC/BCC support
+- 📁 **Folder Operations**: List, create, delete email folders
+- 📋 **Draft Management**: Save, update, delete, and manage email drafts
+- 📦 **Import/Export**: Backup/restore emails with folder structure preservation
+- 🔗 **Attachment Handling**: Download email attachments to specified locations
+- 📊 **Statistics**: Get mailbox statistics and unread counts
+- 🛡️ **Security**: Workspace isolation and secure IMAP/SMTP connections
+
+## Installation
+
+### Uv is recommanded
+
+```bash
+uv tool install emails-mcp
+```
+
+### From source
+
+```bash
+git clone https://github.com/lockon-n/emails-mcp.git
+cd emails-mcp
+uv sync
+```
+
+## Usage
+
+### Configuration
+
+Create email configuration file (`/path/to/your/email/config.json`):
+
+**Single account format:**
+```json
+{
+ "email": "your-email@example.com",
+ "password": "your-password",
+ "name": "Your Name",
+ "imap_server": "imap.example.com",
+ "imap_port": yourport (typically 993),
+ "smtp_server": "smtp.example.com",
+ "smtp_port": yourport (typically 587),
+ "use_ssl": true/false,
+ "use_starttls": true/false
+}
+```
+
+### Usage with Claude Desktop
+
+Add to your `~/.config/claude/claude_desktop_config.json` (Linux/macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
+
+**Published Configuration:**
+```json
+{
+ "mcpServers": {
+ "emails-mcp": {
+ "command": "uvx",
+ "args": [
+ "emails-mcp",
+ "--config_file",
+ " ",
+ "--attachment_upload_path",
+ "",
+ "--attachment_download_path",
+ "",
+ "--email_export_path",
+ ""
+ ]
+ }
+ }
+}
+```
+
+**Development/Unpublished Configuration:**
+```json
+{
+ "mcpServers": {
+ "emails-mcp": {
+ "command": "uv",
+ "args": [
+ "--directory",
+ "",
+ "run",
+ "emails-mcp",
+ "--config_file",
+ " ",
+ "--attachment_upload_path",
+ "",
+ "--attachment_download_path",
+ "",
+ "--email_export_path",
+ ""
+ ]
+ }
+ }
+}
+```
+
+**Note**: For security, this tool restricts file operations to the specified directories:
+- `--attachment_upload_path`: Restricts attachment file selection
+- `--attachment_download_path`: Where downloaded attachments are saved
+- `--email_export_path`: Where email exports are saved
+
+### As a command line tool
+
+```bash
+# Basic usage (uses default config file: test_emils.json)
+emails-mcp
+
+# With custom configuration file
+emails-mcp --config_file config.json
+
+# With path restrictions for security
+emails-mcp --config_file config.json \
+ --attachment_download_path ./downloads \
+ --email_export_path ./exports \
+ --attachment_upload_path ./uploads
+
+# With debug logging
+emails-mcp --config_file config.json --debug
+```
+
+#### Supported Arguments
+
+- `--config_file`: Email configuration file path
+- `--attachment_upload_path`: Directory for attachment uploads (restricts file selection)
+- `--attachment_download_path`: Directory for attachment downloads (files saved here)
+- `--email_export_path`: Directory for email exports (exports saved here)
+- `--debug`: Enable debug logging
+
+## Available Tools
+
+
+📧 Email Operations
+
+### get_emails
+Get paginated list of emails from specified folder
+- `folder`: Email folder name (default: "INBOX")
+- `page`: Page number starting from 1 (default: 1)
+- `page_size`: Number of emails per page (default: 20)
+
+### read_email
+Read full content of a specific email
+- `email_id`: Email ID to read
+
+### search_emails
+Search emails with query string (sorted by date descending)
+- `query`: Search query (subject, from, body content)
+- `folder`: Folder to search in (optional)
+- `page`: Page number starting from 1 (default: 1)
+- `page_size`: Number of results per page (default: 20)
+
+### send_email
+Send an email with optional HTML body, CC, BCC, and attachments
+- `to`: Recipient email address(es), comma-separated
+- `subject`: Email subject
+- `body`: Plain text body
+- `html_body`: HTML body content (optional)
+- `cc`: CC recipients, comma-separated (optional)
+- `bcc`: BCC recipients, comma-separated (optional)
+- `attachments`: List of file paths to attach (optional)
+
+### reply_email
+Reply to an email
+- `email_id`: ID of email to reply to
+- `body`: Reply message body (plain text)
+- `html_body`: Reply message body (HTML, optional)
+- `cc`: Additional CC recipients (optional)
+- `bcc`: BCC recipients (optional)
+- `reply_all`: Whether to reply to all recipients (default: False)
+
+### forward_email
+Forward an email to other recipients
+- `email_id`: ID of email to forward
+- `to`: Recipients to forward to
+- `body`: Additional message body (optional)
+- `html_body`: Additional HTML message body (optional)
+- `cc`: CC recipients (optional)
+- `bcc`: BCC recipients (optional)
+
+### delete_email / delete_emails
+Delete single or multiple emails
+- `email_id`: Email ID to delete (single)
+- `email_ids`: List of email IDs to delete (batch)
+
+### move_email / move_emails
+Move single or multiple emails to another folder
+- `email_id`: Email ID to move (single)
+- `email_ids`: List of email IDs to move (batch)
+- `target_folder`: Target folder name
+
+### mark_emails
+Mark multiple emails with status
+- `email_ids`: List of email IDs to mark
+- `status`: Status to set (read, unread, important, not_important)
+
+
+
+
+📁 Folder Operations
+
+### get_folders
+Get list of available email folders
+
+### create_folder
+Create new email folder
+- `folder_name`: Name of folder to create
+
+### delete_folder
+Delete email folder
+- `folder_name`: Name of folder to delete
+
+### get_mailbox_stats
+Get mailbox statistics
+- `folder_name`: Specific folder name (optional, defaults to all folders)
+
+### get_unread_count
+Get unread message count
+- `folder_name`: Specific folder name (optional, defaults to all folders)
+
+
+
+
+📋 Draft Management
+
+### save_draft
+Save email draft
+- `subject`: Email subject
+- `body`: Plain text body
+- `html_body`: HTML body content (optional)
+- `to`: Recipient email address(es) (optional)
+- `cc`: CC recipients (optional)
+- `bcc`: BCC recipients (optional)
+
+### get_drafts
+Get list of saved drafts
+- `page`: Page number starting from 1 (default: 1)
+- `page_size`: Number of drafts per page (default: 20)
+
+### update_draft
+Update existing draft
+- `draft_id`: Draft ID to update
+- `subject`: Email subject (optional)
+- `body`: Plain text body (optional)
+- `html_body`: HTML body content (optional)
+- `to`: Recipient email address(es) (optional)
+- `cc`: CC recipients (optional)
+- `bcc`: BCC recipients (optional)
+
+### delete_draft
+Delete draft
+- `draft_id`: Draft ID to delete
+
+
+
+
+🔧 Management & Utilities
+
+### check_connection
+Check email server connection status
+
+### get_email_headers
+Get complete email headers for technical analysis
+- `email_id`: Email ID to get headers for
+
+### export_emails
+Export emails to file for backup
+- `folder`: Specific folder to export (optional)
+- `export_path`: Path where to save the export file (default: "emails_export.json")
+- `max_emails`: Maximum number of emails to export (optional)
+- `export_all_folders`: Export from all folders instead of just one (default: False)
+
+### import_emails
+Import emails from backup file to IMAP server
+- `import_path`: Path to import file
+- `target_folder`: Target folder for imported emails (if preserve_folders=False)
+- `preserve_folders`: Whether to preserve original folder structure (default: True)
+
+### download_attachment
+Download email attachment to specified path
+- `email_id`: Email ID containing the attachment
+- `attachment_filename`: Name of attachment to download
+- `download_path`: Directory path where to save the attachment
+
+
+
+## Configuration Options
+
+### Email Server Settings
+- **IMAP/SMTP servers**: Configure your email provider's servers
+- **Security**: Supports SSL/TLS and STARTTLS
+- **Authentication**: Standard username/password authentication
+
+### Workspace Security
+- **Path Restriction**: Limit file operations to specified directory
+- **Path Validation**: All file paths are validated for security
+
+### Pagination
+- **Default page size**: 20 items per page
+- **Maximum page size**: 50 items per page
+- **Auto-correction**: Invalid page parameters are automatically corrected
+
+## Error Handling
+
+The server includes comprehensive error handling:
+- **Connection errors**: Graceful handling of network issues
+- **Authentication errors**: Clear error messages for login failures
+- **Validation errors**: Input validation with helpful error messages
+- **File system errors**: Proper handling of file operation failures
+
+## Security Considerations
+
+- **Workspace isolation**: File operations can be restricted to a safe directory
+- **Input validation**: All user inputs are validated
+- **Connection security**: Supports SSL/TLS encryption
+- **Error messages**: Avoid exposing sensitive information in error messages
+
+## Development
+
+### Build
+
+```bash
+uv build
+```
+
+### Publish to PyPI
+
+```bash
+uv publish
+```
+
+### Local Development
+
+```bash
+# Install development dependencies
+uv sync
+
+# Run tests
+uv run python -m pytest
+
+# Run server
+uv run python -m emails_mcp.server
+```
+
+### Project Structure
+The codebase follows software engineering best practices:
+
+```
+- models/ # Data models and configurations
+- config/ # Configuration management
+- utils/ # Utilities, validators, and parsers
+- backends/ # IMAP, SMTP, and file operation backends
+- services/ # Business logic layer
+- tools/ # MCP tool definitions
+- server.py # Main MCP server entry point
+```
+
+## License
+
+MIT License
+
+## Contributing
+
+Issues and Pull Requests are welcome!
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/package-lock.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/package-lock.json
new file mode 100644
index 00000000..5579f75c
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/package-lock.json
@@ -0,0 +1,6 @@
+{
+ "name": "emails-mcp",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {}
+}
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/pyproject.toml b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/pyproject.toml
new file mode 100644
index 00000000..a8deae5e
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/pyproject.toml
@@ -0,0 +1,26 @@
+[project]
+name = "emails-mcp"
+version = "0.1.11"
+description = "A FastMCP-based email management server for IMAP/SMTP operations"
+readme = "README.md"
+authors = [
+ { name = "lockon-n", email = "lockonlvange@gmail.com" }
+]
+requires-python = ">=3.12"
+dependencies = [
+ "fastmcp>=2.10.5",
+ "mcp[cli]>=1.11.0",
+ "email-validator>=2.1.1",
+ "typing-extensions>=4.9.0",
+ "requests>=2.32.4",
+ "beautifulsoup4>=4.13.4",
+ "imapclient>=3.0.1",
+ "psycopg2-binary>=2.9",
+]
+
+[project.scripts]
+emails-mcp = "emails_mcp.server:main"
+
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/__init__.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/__init__.py
new file mode 100644
index 00000000..dbf888d2
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/__init__.py
@@ -0,0 +1,2 @@
+def main() -> None:
+ print("Hello from emails-mcp!")
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/__main__.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/__main__.py
new file mode 100644
index 00000000..1487999f
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/__main__.py
@@ -0,0 +1,8 @@
+"""
+Entry point for running emails_mcp as a module with python -m
+"""
+
+from .server import main
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/backends/__init__.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/backends/__init__.py
new file mode 100644
index 00000000..157b94a1
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/backends/__init__.py
@@ -0,0 +1,5 @@
+from .imap_backend import IMAPBackend
+from .smtp_backend import SMTPBackend
+from .file_backend import FileBackend
+
+__all__ = ['IMAPBackend', 'SMTPBackend', 'FileBackend']
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/backends/file_backend.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/backends/file_backend.py
new file mode 100644
index 00000000..c8950d84
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/backends/file_backend.py
@@ -0,0 +1,356 @@
+import json
+import os
+import base64
+from pathlib import Path
+from typing import List
+from datetime import datetime
+from ..models.email import EmailMessage
+from ..utils.exceptions import ValidationError
+from ..utils.validators import validate_file_path
+from ..utils.email_parser import parse_raw_email
+import logging
+from email import encoders
+
+
+class FileBackend:
+ """File backend for email import/export operations"""
+
+ def __init__(self, email_export_path: str = None, attachment_download_path: str = None):
+ self.email_export_path = Path(email_export_path) if email_export_path else None
+ self.attachment_download_path = Path(attachment_download_path) if attachment_download_path else None
+
+ def export_emails(self, emails: List[EmailMessage], filename_prefix: str = "emails_export",
+ format: str = 'json') -> str:
+ """Export emails to file using configured export path with date-based filename"""
+ from datetime import datetime
+
+ # Use configured export path or current directory
+ if self.email_export_path:
+ export_dir = self.email_export_path
+ else:
+ export_dir = Path.cwd()
+
+ try:
+ export_dir.mkdir(parents=True, exist_ok=True)
+
+ # Generate date-based filename
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
+ filename = f"{filename_prefix}_{timestamp}.{format.lower()}"
+ export_file = export_dir / filename
+
+ if format.lower() == 'json':
+ self._export_to_json(emails, export_file)
+ elif format.lower() == 'eml':
+ self._export_to_eml(emails, export_file)
+ else:
+ raise ValidationError(f"Unsupported export format: {format}")
+
+ return str(export_file) # Return the actual file path
+
+ except Exception as e:
+ raise ValidationError(f"Export failed: {str(e)}")
+
+ def import_emails(self, import_path: str) -> List[EmailMessage]:
+ """Import emails from file with time-based sorting (newest first)"""
+
+ # Validate import path
+ valid, error = validate_file_path(import_path, must_exist=True)
+ if not valid:
+ raise ValidationError(f"Invalid import path: {error}")
+
+ try:
+ import_file = Path(import_path)
+
+ emails = []
+ if import_file.suffix.lower() == '.json':
+ # logging.warning("Importing emails from JSON file")
+ emails = self._import_from_json(import_file)
+ elif import_file.suffix.lower() == '.eml':
+ # logging.warning("Importing emails from EML file")
+ emails = self._import_from_eml(import_file)
+ elif import_file.is_dir():
+ emails = self._import_from_directory(import_file)
+ else:
+ raise ValidationError(f"Unsupported import format: {import_file.suffix}")
+
+ # Sort emails by email_id (oldest first) for consistent import order
+ emails.sort(key=lambda email: int(email.email_id) if email.email_id.isdigit() else 0, reverse=False)
+
+ return emails
+
+ except Exception as e:
+ raise ValidationError(f"Import failed: {str(e)}")
+
+ def _parse_email_date(self, date_str: str) -> datetime:
+ """Parse email date string to datetime object for sorting"""
+ if not date_str:
+ return datetime.min.replace(tzinfo=None) # Put emails without dates at the end
+
+ try:
+ from email.utils import parsedate_to_datetime
+ parsed_date = parsedate_to_datetime(date_str)
+
+ # Convert to naive datetime for consistent comparison
+ if parsed_date.tzinfo is not None:
+ # Convert to UTC and then remove timezone info
+ parsed_date = parsed_date.utctimetuple()
+ parsed_date = datetime(*parsed_date[:6])
+
+ return parsed_date
+ except Exception:
+ # Fallback to current time if parsing fails (make it naive)
+ return datetime.now().replace(tzinfo=None)
+
+ def _export_to_json(self, emails: List[EmailMessage], export_file: Path):
+ """Export emails to JSON format"""
+ export_data = {
+ 'export_date': datetime.now().isoformat(),
+ 'total_emails': len(emails),
+ 'emails': []
+ }
+
+ for email_obj in emails:
+ email_data = {
+ 'email_id': email_obj.email_id,
+ 'subject': email_obj.subject,
+ 'from_addr': email_obj.from_addr,
+ 'to_addr': email_obj.to_addr,
+ 'cc_addr': email_obj.cc_addr,
+ 'bcc_addr': email_obj.bcc_addr,
+ 'date': email_obj.date,
+ 'message_id': email_obj.message_id,
+ 'body_text': email_obj.body_text,
+ 'body_html': email_obj.body_html,
+ 'is_read': email_obj.is_read,
+ 'is_important': email_obj.is_important,
+ 'folder': email_obj.folder,
+ 'attachments': [
+ {
+ 'filename': att.filename,
+ 'content_type': att.content_type,
+ 'size': att.size,
+ 'content': base64.b64encode(att.content).decode('utf-8') if att.content else None
+ }
+ for att in email_obj.attachments
+ ]
+ }
+ export_data['emails'].append(email_data)
+
+ with open(export_file, 'w', encoding='utf-8') as f:
+ json.dump(export_data, f, indent=2, ensure_ascii=False)
+
+ def _export_to_eml(self, emails: List[EmailMessage], export_path: Path):
+ """Export emails to EML format (directory of .eml files)"""
+ export_dir = export_path
+ if export_path.suffix:
+ export_dir = export_path.parent / export_path.stem
+
+ export_dir.mkdir(parents=True, exist_ok=True)
+
+ for i, email_obj in enumerate(emails):
+ if email_obj.raw_message:
+ filename = f"{i+1:04d}_{email_obj.email_id}.eml"
+ eml_file = export_dir / filename
+
+ with open(eml_file, 'wb') as f:
+ f.write(email_obj.raw_message.as_bytes())
+
+ def _import_from_json(self, import_file: Path) -> List[EmailMessage]:
+ """Import emails from JSON format"""
+ with open(import_file, 'r', encoding='utf-8') as f:
+ import_data = json.load(f)
+
+ if 'emails' not in import_data:
+ raise ValidationError("Invalid JSON format: missing 'emails' key")
+
+ emails = []
+ for email_data in import_data['emails']:
+ try:
+ # Create EmailMessage from JSON data
+ from ..models.email import EmailAttachment
+
+ attachments = []
+ for att_data in email_data.get('attachments', []):
+ # 解码附件内容(如果存在)
+ content = None
+ if att_data.get('content'):
+ try:
+ content = base64.b64decode(att_data['content'])
+ except Exception as decode_error: # 修正:给异常一个具体的变量名
+ # 如果解码失败,保持content为None
+ logging.debug(f"Failed to decode attachment content: {str(decode_error)}")
+ content = None
+
+ attachment = EmailAttachment(
+ filename=att_data['filename'],
+ content_type=att_data['content_type'],
+ size=att_data['size'],
+ content=content
+ )
+ attachments.append(attachment)
+
+
+
+ email_obj = EmailMessage(
+ email_id=email_data['email_id'],
+ subject=email_data['subject'],
+ from_addr=email_data['from_addr'],
+ to_addr=email_data['to_addr'],
+ cc_addr=email_data.get('cc_addr'),
+ bcc_addr=email_data.get('bcc_addr'),
+ date=email_data.get('date'),
+ message_id=email_data.get('message_id'),
+ body_text=email_data.get('body_text'),
+ body_html=email_data.get('body_html'),
+ is_read=email_data.get('is_read', False),
+ is_important=email_data.get('is_important', False),
+ folder=email_data.get('folder'),
+ attachments=attachments
+ )
+
+ # Only create raw_message if there are attachments (for attachment display support)
+ if email_data.get('attachments'):
+ # Create a minimal email.message.Message object for JSON imports to support attachment display
+ import email.message
+ from email.mime.multipart import MIMEMultipart
+ from email.mime.base import MIMEBase
+ from email.mime.text import MIMEText
+
+ msg = MIMEMultipart()
+ msg['Subject'] = email_data['subject']
+ msg['From'] = email_data['from_addr']
+ msg['To'] = email_data['to_addr']
+ if email_data.get('cc_addr'):
+ msg['Cc'] = email_data['cc_addr']
+ if email_data.get('message_id'):
+ msg['Message-ID'] = email_data['message_id']
+ if email_data.get('date'):
+ msg['Date'] = email_data['date']
+
+ # Add text body if exists
+ if email_data.get('body_text'):
+ text_part = MIMEText(email_data['body_text'], 'plain', 'utf-8')
+ msg.attach(text_part)
+
+ # Add HTML body if exists
+ if email_data.get('body_html'):
+ html_part = MIMEText(email_data['body_html'], 'html', 'utf-8')
+ msg.attach(html_part)
+
+ # Add attachments to the message object
+ for att_data in email_data.get('attachments', []):
+ if att_data.get('content'):
+ content_type_parts = att_data['content_type'].split('/')
+ if len(content_type_parts) == 2:
+ maintype, subtype = content_type_parts
+ else:
+ maintype, subtype = 'application', 'octet-stream'
+
+ part = MIMEBase(maintype, subtype)
+
+ # 方法A:直接使用(推荐)
+ # 验证 base64 格式但不解码
+ try:
+ # 只验证,不实际解码
+ base64.b64decode(att_data['content'])
+ # 如果验证通过,直接使用
+ part.set_payload(att_data['content'])
+ part['Content-Transfer-Encoding'] = 'base64'
+ except Exception as e:
+ logging.warning(f"Invalid base64 content: {e}")
+ # 设置空附件
+ part.set_payload('')
+ part['Content-Transfer-Encoding'] = 'base64'
+
+ # 处理文件名...
+ filename = att_data["filename"]
+ try:
+ filename.encode('ascii')
+ part.add_header('Content-Disposition', 'attachment',
+ filename=filename)
+ except UnicodeEncodeError:
+ part.add_header('Content-Disposition', 'attachment',
+ filename=('utf-8', '', filename))
+
+ msg.attach(part)
+
+ email_obj.raw_message = msg
+ else:
+ # No attachments, no need for raw_message
+ email_obj.raw_message = None
+
+ emails.append(email_obj)
+
+ except KeyError as e:
+ raise ValidationError(f"Missing required field in JSON: {str(e)}")
+
+ # Sort emails by email_id in descending order
+ emails.sort(key=lambda email_obj: int(email_obj.email_id) if email_obj.email_id.isdigit() else 0, reverse=True)
+
+ # import pickle
+ # os.makedirs("./json", exist_ok=True)
+ # with open("./json/all.pkl", "wb") as f:
+ # pickle.dump(emails, f)
+
+ return emails
+
+ def _import_from_eml(self, import_file: Path) -> List[EmailMessage]:
+ """Import single email from EML format"""
+ with open(import_file, 'rb') as f:
+ raw_email = f.read()
+
+ email_id = import_file.stem
+ email_obj = parse_raw_email(raw_email, email_id)
+ # import pickle
+ # os.makedirs("./eml", exist_ok=True)
+ # with open("./eml/all.pkl", "wb") as f:
+ # pickle.dump([email_obj], f)
+ return [email_obj]
+
+ def _import_from_directory(self, import_dir: Path) -> List[EmailMessage]:
+ """Import multiple emails from directory of EML files"""
+ emails = []
+
+ for eml_file in import_dir.glob('*.eml'):
+ try:
+ imported_emails = self._import_from_eml(eml_file)
+ emails.extend(imported_emails)
+ except Exception as e:
+ # Log warning but continue with other files
+ import logging
+ logging.warning(f"Failed to import {eml_file}: {str(e)}")
+
+ return emails
+
+ def save_attachment(self, attachment_data: bytes, filename: str) -> str:
+ """Save attachment data to file using configured download path"""
+
+ # Use configured download path or current directory
+ if self.attachment_download_path:
+ download_dir = self.attachment_download_path
+ else:
+ download_dir = Path.cwd()
+
+ try:
+ download_dir.mkdir(parents=True, exist_ok=True)
+
+ file_path = download_dir / filename
+
+ # Avoid overwriting existing files using (1), (2), etc. format
+ if file_path.exists():
+ name, ext = os.path.splitext(filename)
+ counter = 1
+ while True:
+ new_filename = f"{name}({counter}){ext}"
+ file_path = download_dir / new_filename
+ if not file_path.exists():
+ break
+ counter += 1
+
+ with open(file_path, 'wb') as f:
+ f.write(attachment_data)
+
+ return str(file_path)
+
+ except Exception as e:
+ raise ValidationError(f"Failed to save attachment: {str(e)}")
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/backends/imap_backend.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/backends/imap_backend.py
new file mode 100644
index 00000000..5a6a61ec
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/backends/imap_backend.py
@@ -0,0 +1,546 @@
+import imaplib
+import logging
+from typing import List, Optional, Tuple
+from datetime import datetime
+from ..models.config import EmailConfig
+from ..models.email import EmailFolder, EmailMessage
+from ..utils.exceptions import ConnectionError, AuthenticationError, FolderError
+from ..utils.email_parser import parse_raw_email
+from ..utils.encode_decode import encode_to_imap_utf7, decode_from_imap_utf7
+
+class IMAPBackend:
+ """IMAP backend for email operations"""
+
+ def __init__(self, config: EmailConfig):
+ self.config = config
+ self.connection: Optional[imaplib.IMAP4_SSL] = None
+ self.current_folder: Optional[str] = None
+ self.last_accessed = datetime.now()
+ self.utf8_enabled = False
+
+ def connect(self) -> bool:
+ """Establish IMAP connection"""
+ try:
+ if self.config.use_ssl:
+ self.connection = imaplib.IMAP4_SSL(
+ self.config.imap_server,
+ self.config.imap_port
+ )
+ else:
+ self.connection = imaplib.IMAP4(
+ self.config.imap_server,
+ self.config.imap_port
+ )
+
+ # Login
+ self.connection.login(self.config.email, self.config.password)
+ self.last_accessed = datetime.now()
+
+ # Try to enable UTF-8 support if available
+ self._enable_utf8_support()
+
+ logging.info(f"IMAP connected for {self.config.email}")
+ return True
+
+ except imaplib.IMAP4.error as e:
+ logging.error(f"IMAP authentication failed: {str(e)}")
+ raise AuthenticationError(f"IMAP login failed: {str(e)}")
+ except Exception as e:
+ logging.error(f"IMAP connection failed: {str(e)}")
+ raise ConnectionError(f"IMAP connection failed: {str(e)}")
+
+ def disconnect(self):
+ """Close IMAP connection"""
+ if self.connection:
+ try:
+ self.connection.close()
+ self.connection.logout()
+ except:
+ pass
+ finally:
+ self.connection = None
+ self.current_folder = None
+ self.utf8_enabled = False
+
+ def ensure_connected(self):
+ """Ensure IMAP connection is active"""
+ if not self.connection:
+ self.connect()
+
+ # Test connection with NOOP
+ try:
+ self.connection.noop()
+ self.last_accessed = datetime.now()
+ except:
+ logging.warning("IMAP connection lost, reconnecting...")
+ self.disconnect()
+ self.connect()
+
+ def _enable_utf8_support(self):
+ """Try to enable UTF-8 support on IMAP server"""
+ try:
+ # Check if server supports UTF8 capability
+ capabilities = self.connection.capability()
+ if capabilities[0] == 'OK':
+ capability_list = capabilities[1][0].decode().upper().split()
+ if 'UTF8=ACCEPT' in capability_list or 'UTF8=ONLY' in capability_list:
+ # Try to enable UTF-8 support
+ result = self.connection.enable('UTF8=ACCEPT')
+ if result[0] == 'OK':
+ self.utf8_enabled = True
+ logging.info("UTF-8 support enabled for IMAP connection")
+ else:
+ logging.warning("Failed to enable UTF-8 support")
+ else:
+ logging.info("Server does not support UTF8=ACCEPT capability")
+ except Exception as e:
+ logging.warning(f"Could not check/enable UTF-8 support: {str(e)}")
+ self.utf8_enabled = False
+
+ def _quote_folder_name(self, folder_name: str) -> str:
+ """Quote folder name if it contains spaces (excluding leading/trailing spaces)"""
+ # Strip leading/trailing spaces first
+ folder_name = folder_name.strip()
+
+ # Check if there are spaces in the middle of the folder name
+ if ' ' in folder_name:
+ # Escape any existing quotes in the folder name
+ folder_name = folder_name.replace('"', '\\"')
+ # Wrap with quotes
+ return f'"{folder_name}"'
+
+ return folder_name
+
+ def select_folder(self, folder: str) -> Tuple[int, int]:
+ """Select email folder and return (total_messages, unread_messages)"""
+ self.ensure_connected()
+ logging.info(f"尝试选中文件夹: {folder}")
+ try:
+ # Quote folder name if necessary
+ quoted_folder_name = self._quote_folder_name(folder)
+ utf7_quoted_folder_name = encode_to_imap_utf7(quoted_folder_name)
+
+ if self.utf8_enabled:
+ status, data = self.connection.select(quoted_folder_name)
+ else:
+ status, data = self.connection.select(utf7_quoted_folder_name)
+
+ if status != 'OK':
+ raise FolderError(f"Failed to select folder '{folder}': {status}")
+
+ self.current_folder = folder # Store the original folder name without quotes
+ total_messages = int(
+ data[0]) if data[0] else 0
+
+ # Get unread count
+ status, unread_data = self.connection.search(None, 'UNSEEN')
+ unread_messages = len(unread_data[0].split()) if status == 'OK' and unread_data[0] else 0
+
+ return total_messages, unread_messages
+
+ except Exception as e:
+ raise FolderError(f"Error selecting folder '{folder}': {str(e)}")
+
+ def list_folders(self) -> List[EmailFolder]:
+ """List all available folders"""
+ self.ensure_connected()
+
+ try:
+ status, folders = self.connection.list()
+ if status != 'OK':
+ raise FolderError(f"Failed to list folders: {status}")
+
+ folder_list = []
+ for folder in folders:
+ if self.utf8_enabled:
+ folder_info = folder.decode('utf-8')
+ else:
+ folder_info = decode_from_imap_utf7(folder.decode('utf-8'))
+ logging.debug(f"Parsing folder info: {folder_info}")
+
+ # Parse folder name from IMAP response
+ # Format can be: '(\\HasNoChildren) "." "INBOX"' or '(\\HasNoChildren) "." INBOX'
+ parts = folder_info.split('"')
+
+ folder_name = None
+ if len(parts) >= 3:
+ # If folder name is quoted: '(flags) "sep" "name"'
+ # The folder name is typically the last quoted part (parts[3] if exists, otherwise parts[2])
+ if len(parts) >= 4 and parts[3].strip():
+ folder_name = parts[3].strip()
+ elif len(parts) >= 3 and parts[2].strip() and parts[2].strip() not in ['.', '/', '\\']:
+ folder_name = parts[2].strip()
+ else:
+ # If folder name is not quoted: '(flags) "sep" name'
+ # Split by spaces and take the last part
+ space_parts = folder_info.split()
+ if len(space_parts) >= 3:
+ folder_name = space_parts[-1]
+
+ # Skip invalid folder names (root folders, empty names, etc.)
+ if not folder_name or folder_name in [".", ".."]:
+ continue
+
+ # Check if folder is selectable
+ is_noselect = '\\Noselect' in folder_info
+
+ # Try to get folder stats for selectable folders
+ if not is_noselect:
+ try:
+ total, unread = self.select_folder(folder_name)
+ folder_obj = EmailFolder(
+ name=folder_name,
+ total_messages=total,
+ unread_messages=unread,
+ can_select=True
+ )
+ except:
+ # If we can't select the folder, mark it as non-selectable
+ folder_obj = EmailFolder(
+ name=folder_name,
+ can_select=False
+ )
+ else:
+ # Folder marked as non-selectable by server
+ folder_obj = EmailFolder(
+ name=folder_name,
+ can_select=False
+ )
+
+ folder_list.append(folder_obj)
+
+ return folder_list
+
+ except Exception as e:
+ raise FolderError(f"Error listing folders: {str(e)}")
+
+ def get_email_ids(self, folder: str, limit: Optional[int] = None) -> List[str]:
+ """Get email IDs from folder (newest first)"""
+ quoted_folder_name = self._quote_folder_name(folder)
+ utf7_quoted_folder_name = encode_to_imap_utf7(quoted_folder_name)
+
+ if self.utf8_enabled:
+ total, _ = self.select_folder(quoted_folder_name)
+ else:
+ total, _ = self.select_folder(utf7_quoted_folder_name)
+
+ if total == 0:
+ return []
+
+ try:
+ # Get all email IDs
+ status, email_ids = self.connection.search(None, 'ALL')
+ if status != 'OK':
+ raise FolderError(f"Failed to search emails: {status}")
+
+ id_list = email_ids[0].split()
+ # Reverse to get newest first
+ id_list = [uid.decode() for uid in reversed(id_list)]
+
+ if limit:
+ id_list = id_list[:limit]
+
+ return id_list
+
+ except Exception as e:
+ raise FolderError(f"Error getting email IDs: {str(e)}")
+
+ def fetch_email(self, email_id: str) -> EmailMessage:
+ """Fetch single email by ID"""
+ self.ensure_connected()
+
+ try:
+ # Fetch email content and flags separately for better reliability
+ # First get the RFC822 content
+ status, content_data = self.connection.fetch(email_id, '(RFC822)')
+ if status != 'OK':
+ raise FolderError(f"Failed to fetch email content {email_id}: {status}")
+
+ # Then get current flags separately
+ status, flag_data = self.connection.fetch(email_id, '(FLAGS)')
+ if status != 'OK':
+ raise FolderError(f"Failed to fetch email flags {email_id}: {status}")
+
+ # Extract email content
+ raw_email = None
+ if content_data and len(content_data) > 0:
+ for item in content_data:
+ if isinstance(item, tuple) and len(item) == 2:
+ # item[1] should be the email content
+ if isinstance(item[1], bytes) and len(item[1]) > 0:
+ raw_email = item[1]
+ break
+
+ if not raw_email:
+ raise FolderError(f"No email content found for email {email_id}")
+
+ # Extract flags
+ flags = []
+ for item in flag_data:
+ if isinstance(item, bytes):
+ # Direct bytes response like b'6 (FLAGS (\\Seen \\Flagged))'
+ header = item.decode()
+ if 'FLAGS' in header:
+ import re
+ flag_match = re.search(r'FLAGS \(([^)]*)\)', header)
+ if flag_match:
+ flags_str = flag_match.group(1)
+ flags = flags_str.split() if flags_str.strip() else []
+ logging.debug(f"Extracted current flags for email {email_id}: {flags}")
+ break
+ elif isinstance(item, tuple) and len(item) == 2:
+ # Tuple response
+ header = item[0].decode() if isinstance(item[0], bytes) else str(item[0])
+ if 'FLAGS' in header:
+ import re
+ flag_match = re.search(r'FLAGS \(([^)]*)\)', header)
+ if flag_match:
+ flags_str = flag_match.group(1)
+ flags = flags_str.split() if flags_str.strip() else []
+ logging.debug(f"Extracted current flags for email {email_id}: {flags}")
+ break
+
+ email_obj = parse_raw_email(raw_email, email_id)
+ email_obj.folder = self.current_folder
+
+ # Set status based on current IMAP flags
+ email_obj.is_read = '\\Seen' in flags
+ email_obj.is_important = '\\Flagged' in flags
+
+ logging.debug(f"Email {email_id} status: read={email_obj.is_read}, important={email_obj.is_important}")
+
+ return email_obj
+
+ except Exception as e:
+ logging.error(f"Error fetching email {email_id}: {str(e)}")
+ raise
+
+ def search_emails(self, query: str, folder: str = None) -> List[str]:
+ """Search emails and return email IDs"""
+ self.ensure_connected()
+
+ # If no folder specified, use INBOX as default
+ if not folder:
+ folder = 'INBOX'
+
+ # Always select folder before searching
+ self.select_folder(folder)
+
+ try:
+ # Handle Unicode/Chinese characters in search query
+ # Based on StackOverflow solution: need to encode Unicode strings to UTF-8 bytes
+ query_bytes = query.encode('utf-8')
+
+ # Try UTF-8 search first if server supports it
+ # Use TEXT search which covers all text content (subject, body, headers)
+ # This is more reliable than OR combinations for UTF-8 content
+ if self.utf8_enabled:
+ # When UTF-8 is enabled, we should NOT specify charset parameter
+ # Use TEXT to search all text content
+ search_criteria = b'TEXT "' + query_bytes + b'"'
+ status, email_ids = self.connection.search(None, search_criteria)
+ else:
+ # For servers without UTF-8 support, try UTF-8 charset parameter
+ try:
+ # Use TEXT with UTF-8 charset
+ search_criteria = b'TEXT "' + query_bytes + b'"'
+ status, email_ids = self.connection.search('UTF-8', search_criteria)
+ except Exception as search_error:
+ logging.warning(f"UTF-8 charset search failed: {search_error}")
+ status = 'NO'
+
+ if status != 'OK':
+ # Fallback to ASCII search if UTF-8 search fails
+ logging.warning(f"UTF-8 search failed, trying ASCII fallback for query: {query}")
+ ascii_query = query.encode('ascii', errors='ignore').decode('ascii')
+ if ascii_query.strip(): # Only search if we have non-empty ASCII query
+ search_criteria_ascii = f'(OR SUBJECT "{ascii_query}" FROM "{ascii_query}" BODY "{ascii_query}")'
+ status, email_ids = self.connection.search(None, search_criteria_ascii)
+ else:
+ # If ASCII conversion results in empty string, return empty results
+ logging.warning(f"Query '{query}' contains only non-ASCII characters, no ASCII fallback possible")
+ return []
+
+ if status != 'OK':
+ raise FolderError(f"Search failed: {status}")
+
+ id_list = email_ids[0].split()
+ # Return newest first
+ return [uid.decode() for uid in reversed(id_list)]
+
+ except Exception as e:
+ logging.error(f"Error searching emails with query '{query}': {str(e)}")
+ raise FolderError(f"Error searching emails: {str(e)}")
+
+ def mark_as_read(self, email_id: str) -> bool:
+ """Mark email as read
+
+ Returns:
+ bool: True if operation was successful
+ """
+ self.ensure_connected()
+
+ try:
+ result = self.connection.store(email_id, '+FLAGS', '\\Seen')
+ if result[0] != 'OK':
+ logging.error(f"Failed to mark email {email_id} as read: {result[1]}")
+ return False
+
+ # Force synchronization to ensure the change is applied
+ self.connection.noop() # Send NOOP to sync with server
+ logging.debug(f"Successfully marked email {email_id} as read")
+ return True
+
+ except Exception as e:
+ logging.error(f"Error marking email {email_id} as read: {str(e)}")
+ return False
+
+ def mark_as_unread(self, email_id: str) -> bool:
+ """Mark email as unread
+
+ Returns:
+ bool: True if operation was successful
+ """
+ self.ensure_connected()
+
+ try:
+ result = self.connection.store(email_id, '-FLAGS', '\\Seen')
+ if result[0] != 'OK':
+ logging.error(f"Failed to mark email {email_id} as unread: {result[1]}")
+ return False
+
+ # Force synchronization to ensure the change is applied
+ self.connection.noop() # Send NOOP to sync with server
+ logging.debug(f"Successfully marked email {email_id} as unread")
+ return True
+
+ except Exception as e:
+ logging.error(f"Error marking email {email_id} as unread: {str(e)}")
+ return False
+
+ def mark_as_important(self, email_id: str) -> bool:
+ """Mark email as important (flagged)
+
+ Returns:
+ bool: True if operation was successful
+ """
+ self.ensure_connected()
+
+ try:
+ result = self.connection.store(email_id, '+FLAGS', '\\Flagged')
+ if result[0] != 'OK':
+ logging.error(f"Failed to mark email {email_id} as important: {result[1]}")
+ return False
+
+ # Force synchronization to ensure the change is applied
+ self.connection.noop() # Send NOOP to sync with server
+ logging.debug(f"Successfully marked email {email_id} as important")
+ return True
+
+ except Exception as e:
+ logging.error(f"Error marking email {email_id} as important: {str(e)}")
+ return False
+
+ def mark_as_not_important(self, email_id: str) -> bool:
+ """Remove important flag from email
+
+ Returns:
+ bool: True if operation was successful
+ """
+ self.ensure_connected()
+
+ try:
+ result = self.connection.store(email_id, '-FLAGS', '\\Flagged')
+ if result[0] != 'OK':
+ logging.error(f"Failed to remove important flag from email {email_id}: {result[1]}")
+ return False
+
+ # Force synchronization to ensure the change is applied
+ self.connection.noop() # Send NOOP to sync with server
+ logging.debug(f"Successfully removed important flag from email {email_id}")
+ return True
+
+ except Exception as e:
+ logging.error(f"Error removing important flag from email {email_id}: {str(e)}")
+ return False
+
+ def delete_email(self, email_id: str):
+ """Mark email for deletion"""
+ self.ensure_connected()
+
+ try:
+ self.connection.store(email_id, '+FLAGS', '\\Deleted')
+ self.connection.expunge()
+ except Exception as e:
+ logging.error(f"Error deleting email {email_id}: {str(e)}")
+ raise FolderError(f"Failed to delete email: {str(e)}")
+
+ def move_email(self, email_id: str, target_folder: str) -> Optional[str]:
+ """Move email to another folder with UTF-8 encoding support
+
+ Returns:
+ Optional[str]: New email ID in target folder, or None if move failed
+ """
+ self.ensure_connected()
+
+ try:
+ # First, verify the email exists in current folder
+ status, data = self.connection.fetch(email_id, '(FLAGS)')
+ if status != 'OK':
+ raise FolderError(f"Email {email_id} not found in current folder")
+
+ # Handle UTF-8 encoding for target folder name
+ quoted_target_folder = self._quote_folder_name(target_folder)
+ utf7_quoted_target_folder = encode_to_imap_utf7(quoted_target_folder)
+
+ # Copy to target folder
+ if self.utf8_enabled:
+ copy_result = self.connection.copy(email_id, quoted_target_folder)
+ else:
+ copy_result = self.connection.copy(email_id, utf7_quoted_target_folder)
+ if copy_result[0] != 'OK':
+ raise FolderError(f"Failed to copy email to {target_folder}: {copy_result[1]}")
+
+ # Mark as deleted in current folder
+ store_result = self.connection.store(email_id, '+FLAGS', '\\Deleted')
+ if store_result[0] != 'OK':
+ logging.warning(f"Failed to mark email {email_id} as deleted: {store_result[1]}")
+
+ # Expunge to actually remove from current folder
+ expunge_result = self.connection.expunge()
+ if expunge_result[0] != 'OK':
+ logging.warning(f"Failed to expunge deleted emails: {expunge_result[1]}")
+
+ logging.info(f"Successfully moved email {email_id} to {target_folder}")
+
+ # Note: We can't easily get the new UID without searching,
+ # but the move operation itself is successful
+ return None # Could be enhanced to return new UID if needed
+
+ except Exception as e:
+ logging.error(f"Error moving email {email_id} to {target_folder}: {str(e)}")
+ raise FolderError(f"Failed to move email: {str(e)}")
+
+ def append_message(self, folder: str, message: str, flags: str = '\\Seen') -> bool:
+ """Append a message to the specified folder with UTF-8 support"""
+ self.ensure_connected()
+
+ try:
+ # Encode folder name for IMAP operations
+ quoted_folder = self._quote_folder_name(folder)
+ utf7_quoted_folder = encode_to_imap_utf7(quoted_folder)
+
+ # Use IMAP APPEND command to add message to folder
+ if self.utf8_enabled:
+ result = self.connection.append(quoted_folder, flags, None, message.encode('utf-8'))
+ else:
+ result = self.connection.append(utf7_quoted_folder, flags, None, message.encode('utf-8'))
+ if result[0] == 'OK':
+ logging.info(f"Message appended to {folder}")
+ return True
+ else:
+ logging.error(f"Failed to append message to {folder}: {result}")
+ return False
+ except Exception as e:
+ logging.error(f"Error appending message to {folder}: {str(e)}")
+ raise FolderError(f"Failed to append message to {folder}: {str(e)}")
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/backends/pg_backend.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/backends/pg_backend.py
new file mode 100644
index 00000000..073bc9a7
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/backends/pg_backend.py
@@ -0,0 +1,818 @@
+import json
+import logging
+import os
+import uuid
+from datetime import datetime
+from email import policy
+from email.mime.text import MIMEText
+from email.mime.multipart import MIMEMultipart
+from email.mime.base import MIMEBase
+from email import encoders
+from email.parser import Parser
+from email.utils import formataddr, formatdate, make_msgid
+from typing import List, Optional, Tuple
+
+import psycopg2
+import psycopg2.extras
+
+from ..models.config import EmailConfig
+from ..models.email import EmailAttachment, EmailFolder, EmailMessage
+from ..utils.exceptions import ConnectionError, FolderError, SendEmailError
+
+
+def _get_pg_conn_params() -> dict:
+ """Get PostgreSQL connection parameters from environment variables."""
+ return {
+ "host": os.environ.get("PG_HOST", "localhost"),
+ "port": int(os.environ.get("PG_PORT", "5432")),
+ "database": os.environ.get("PG_DATABASE", "toolathlon"),
+ "user": os.environ.get("PG_USER", "postgres"),
+ "password": os.environ.get("PG_PASSWORD", "postgres"),
+ }
+
+
+def _jsonb_list_to_comma_str(jsonb_val) -> str:
+ """Convert a JSONB list (or Python list) to a comma-separated string."""
+ if jsonb_val is None:
+ return ""
+ if isinstance(jsonb_val, str):
+ try:
+ jsonb_val = json.loads(jsonb_val)
+ except (json.JSONDecodeError, TypeError):
+ return jsonb_val
+ if isinstance(jsonb_val, list):
+ return ", ".join(str(v) for v in jsonb_val if v)
+ return str(jsonb_val)
+
+
+def _comma_str_to_jsonb_list(comma_str: Optional[str]) -> list:
+ """Convert a comma-separated string to a JSON-serializable list."""
+ if not comma_str:
+ return []
+ return [addr.strip() for addr in comma_str.split(",") if addr.strip()]
+
+
+class PgIMAPBackend:
+ """PostgreSQL-backed IMAP replacement backend for email operations."""
+
+ def __init__(self, config: EmailConfig):
+ self.config = config
+ self.connection = None # psycopg2 connection
+ self.current_folder: Optional[str] = None
+ self.utf8_enabled = True # Always true for PG
+
+ # ------------------------------------------------------------------
+ # Connection management
+ # ------------------------------------------------------------------
+
+ def connect(self) -> bool:
+ """Establish PostgreSQL connection."""
+ try:
+ self.connection = psycopg2.connect(**_get_pg_conn_params())
+ self.connection.autocommit = True
+ logging.info("PgIMAPBackend connected to PostgreSQL")
+ return True
+ except Exception as e:
+ logging.error(f"PgIMAPBackend connection failed: {e}")
+ raise ConnectionError(f"PostgreSQL connection failed: {e}")
+
+ def disconnect(self):
+ """Close PostgreSQL connection."""
+ if self.connection:
+ try:
+ self.connection.close()
+ except Exception:
+ pass
+ finally:
+ self.connection = None
+ self.current_folder = None
+
+ def ensure_connected(self):
+ """Ensure PostgreSQL connection is active."""
+ if self.connection is None or self.connection.closed:
+ self.connect()
+ return
+ try:
+ with self.connection.cursor() as cur:
+ cur.execute("SELECT 1")
+ except Exception:
+ logging.warning("PgIMAPBackend connection lost, reconnecting...")
+ self.disconnect()
+ self.connect()
+
+ # ------------------------------------------------------------------
+ # Folder operations
+ # ------------------------------------------------------------------
+
+ def _get_or_create_folder(self, folder_name: str) -> int:
+ """Return folder id, creating the folder row if it doesn't exist."""
+ self.ensure_connected()
+ with self.connection.cursor() as cur:
+ cur.execute(
+ "INSERT INTO email.folders (name) VALUES (%s) "
+ "ON CONFLICT (name) DO UPDATE SET name = EXCLUDED.name "
+ "RETURNING id",
+ (folder_name,),
+ )
+ return cur.fetchone()[0]
+
+ def _refresh_folder_counts(self, folder_id: int):
+ """Refresh message_count and unread_count for a folder."""
+ with self.connection.cursor() as cur:
+ cur.execute(
+ "UPDATE email.folders SET "
+ "message_count = (SELECT COUNT(*) FROM email.messages WHERE folder_id = %s), "
+ "unread_count = (SELECT COUNT(*) FROM email.messages WHERE folder_id = %s AND is_read = FALSE) "
+ "WHERE id = %s",
+ (folder_id, folder_id, folder_id),
+ )
+
+ def select_folder(self, folder: str) -> Tuple[int, int]:
+ """Select email folder and return (total_messages, unread_messages)."""
+ self.ensure_connected()
+ folder = folder.strip()
+ folder_id = self._get_or_create_folder(folder)
+ self._refresh_folder_counts(folder_id)
+ self.current_folder = folder
+
+ with self.connection.cursor() as cur:
+ cur.execute(
+ "SELECT message_count, unread_count FROM email.folders WHERE id = %s",
+ (folder_id,),
+ )
+ row = cur.fetchone()
+ if row is None:
+ raise FolderError(f"Folder '{folder}' not found")
+ return row[0], row[1]
+
+ def list_folders(self) -> List[EmailFolder]:
+ """List all available folders."""
+ self.ensure_connected()
+ with self.connection.cursor() as cur:
+ cur.execute("SELECT id, name, message_count, unread_count FROM email.folders ORDER BY name")
+ rows = cur.fetchall()
+
+ folders = []
+ for row in rows:
+ folder_id, name, msg_count, unread_count = row
+ # Refresh counts for accuracy
+ self._refresh_folder_counts(folder_id)
+ with self.connection.cursor() as cur:
+ cur.execute(
+ "SELECT message_count, unread_count FROM email.folders WHERE id = %s",
+ (folder_id,),
+ )
+ updated = cur.fetchone()
+ folders.append(
+ EmailFolder(
+ name=name,
+ total_messages=updated[0] if updated else msg_count,
+ unread_messages=updated[1] if updated else unread_count,
+ can_select=True,
+ )
+ )
+ return folders
+
+ def create_folder(self, folder_name: str) -> bool:
+ """Create a new email folder."""
+ self.ensure_connected()
+ folder_name = folder_name.strip()
+ try:
+ self._get_or_create_folder(folder_name)
+ logging.info(f"Folder '{folder_name}' created (or already exists)")
+ return True
+ except Exception as e:
+ logging.error(f"Error creating folder '{folder_name}': {e}")
+ raise FolderError(f"Failed to create folder: {e}")
+
+ def delete_folder(self, folder_name: str) -> bool:
+ """Delete an email folder and all its messages."""
+ self.ensure_connected()
+ folder_name = folder_name.strip()
+ try:
+ with self.connection.cursor() as cur:
+ cur.execute("SELECT id FROM email.folders WHERE name = %s", (folder_name,))
+ row = cur.fetchone()
+ if row is None:
+ raise FolderError(f"Folder '{folder_name}' not found")
+ folder_id = row[0]
+ # Delete messages in folder (attachments cascade)
+ cur.execute("DELETE FROM email.messages WHERE folder_id = %s", (folder_id,))
+ cur.execute("DELETE FROM email.folders WHERE id = %s", (folder_id,))
+ logging.info(f"Folder '{folder_name}' deleted")
+ return True
+ except FolderError:
+ raise
+ except Exception as e:
+ logging.error(f"Error deleting folder '{folder_name}': {e}")
+ raise FolderError(f"Failed to delete folder: {e}")
+
+ # ------------------------------------------------------------------
+ # Email ID operations
+ # ------------------------------------------------------------------
+
+ def get_email_ids(self, folder: str, limit: Optional[int] = None) -> List[str]:
+ """Get email IDs from folder (newest first)."""
+ self.ensure_connected()
+ folder = folder.strip()
+ folder_id = self._get_or_create_folder(folder)
+
+ query = (
+ "SELECT m.id FROM email.messages m WHERE m.folder_id = %s ORDER BY m.date DESC, m.id DESC"
+ )
+ params: list = [folder_id]
+ if limit is not None:
+ query += " LIMIT %s"
+ params.append(limit)
+
+ with self.connection.cursor() as cur:
+ cur.execute(query, params)
+ rows = cur.fetchall()
+ return [str(r[0]) for r in rows]
+
+ # ------------------------------------------------------------------
+ # Fetch
+ # ------------------------------------------------------------------
+
+ def fetch_email(self, email_id: str) -> EmailMessage:
+ """Fetch single email by ID (primary key)."""
+ self.ensure_connected()
+ msg_pk = int(email_id)
+
+ with self.connection.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
+ cur.execute(
+ "SELECT m.*, f.name AS folder_name "
+ "FROM email.messages m "
+ "JOIN email.folders f ON f.id = m.folder_id "
+ "WHERE m.id = %s",
+ (msg_pk,),
+ )
+ row = cur.fetchone()
+ if row is None:
+ raise FolderError(f"Email {email_id} not found")
+
+ # Fetch attachments
+ cur.execute(
+ "SELECT id, filename, content_type, size, content FROM email.attachments WHERE message_id = %s",
+ (msg_pk,),
+ )
+ att_rows = cur.fetchall()
+
+ attachments = []
+ for a in att_rows:
+ attachments.append(
+ EmailAttachment(
+ filename=a["filename"],
+ content_type=a["content_type"] or "application/octet-stream",
+ size=a["size"] or 0,
+ attachment_id=str(a["id"]),
+ content=bytes(a["content"]) if a["content"] else None,
+ )
+ )
+
+ date_val = row["date"]
+ date_str = date_val.isoformat() if isinstance(date_val, datetime) else str(date_val) if date_val else None
+
+ email_obj = EmailMessage(
+ email_id=str(row["id"]),
+ subject=row["subject"] or "",
+ from_addr=row["from_addr"] or "",
+ to_addr=_jsonb_list_to_comma_str(row["to_addr"]),
+ cc_addr=_jsonb_list_to_comma_str(row["cc_addr"]) or None,
+ bcc_addr=_jsonb_list_to_comma_str(row["bcc_addr"]) or None,
+ date=date_str,
+ message_id=row["message_id"],
+ body_text=row["body_text"],
+ body_html=row["body_html"],
+ attachments=attachments,
+ is_read=row["is_read"],
+ is_important=row["is_important"],
+ folder=row["folder_name"],
+ )
+ return email_obj
+
+ # ------------------------------------------------------------------
+ # Search
+ # ------------------------------------------------------------------
+
+ def search_emails(self, query: str, folder: str = None) -> List[str]:
+ """Search emails using PostgreSQL full-text search on subject and body_text."""
+ self.ensure_connected()
+
+ # Build tsquery from the raw query string.
+ # Split into words and join with '&' for AND semantics.
+ words = query.strip().split()
+ if not words:
+ return []
+ ts_query_str = " & ".join(words)
+
+ sql = (
+ "SELECT m.id FROM email.messages m "
+ )
+ params: list = []
+
+ if folder:
+ folder = folder.strip()
+ folder_id = self._get_or_create_folder(folder)
+ sql += "WHERE m.folder_id = %s AND "
+ params.append(folder_id)
+ else:
+ sql += "WHERE "
+
+ sql += (
+ "to_tsvector('simple', COALESCE(m.subject, '') || ' ' || COALESCE(m.body_text, '')) "
+ "@@ to_tsquery('simple', %s) "
+ "ORDER BY m.date DESC, m.id DESC"
+ )
+ params.append(ts_query_str)
+
+ with self.connection.cursor() as cur:
+ cur.execute(sql, params)
+ rows = cur.fetchall()
+ return [str(r[0]) for r in rows]
+
+ # ------------------------------------------------------------------
+ # Flag operations
+ # ------------------------------------------------------------------
+
+ def mark_as_read(self, email_id: str) -> bool:
+ self.ensure_connected()
+ try:
+ with self.connection.cursor() as cur:
+ cur.execute(
+ "UPDATE email.messages SET is_read = TRUE WHERE id = %s", (int(email_id),)
+ )
+ return True
+ except Exception as e:
+ logging.error(f"Error marking email {email_id} as read: {e}")
+ return False
+
+ def mark_as_unread(self, email_id: str) -> bool:
+ self.ensure_connected()
+ try:
+ with self.connection.cursor() as cur:
+ cur.execute(
+ "UPDATE email.messages SET is_read = FALSE WHERE id = %s", (int(email_id),)
+ )
+ return True
+ except Exception as e:
+ logging.error(f"Error marking email {email_id} as unread: {e}")
+ return False
+
+ def mark_as_important(self, email_id: str) -> bool:
+ self.ensure_connected()
+ try:
+ with self.connection.cursor() as cur:
+ cur.execute(
+ "UPDATE email.messages SET is_important = TRUE WHERE id = %s", (int(email_id),)
+ )
+ return True
+ except Exception as e:
+ logging.error(f"Error marking email {email_id} as important: {e}")
+ return False
+
+ def mark_as_not_important(self, email_id: str) -> bool:
+ self.ensure_connected()
+ try:
+ with self.connection.cursor() as cur:
+ cur.execute(
+ "UPDATE email.messages SET is_important = FALSE WHERE id = %s", (int(email_id),)
+ )
+ return True
+ except Exception as e:
+ logging.error(f"Error removing important flag from email {email_id}: {e}")
+ return False
+
+ # ------------------------------------------------------------------
+ # Delete / Move / Append
+ # ------------------------------------------------------------------
+
+ def delete_email(self, email_id: str):
+ """Delete email from database."""
+ self.ensure_connected()
+ try:
+ with self.connection.cursor() as cur:
+ cur.execute("DELETE FROM email.messages WHERE id = %s", (int(email_id),))
+ except Exception as e:
+ logging.error(f"Error deleting email {email_id}: {e}")
+ raise FolderError(f"Failed to delete email: {e}")
+
+ def move_email(self, email_id: str, target_folder: str) -> Optional[str]:
+ """Move email to another folder. Returns None (consistent with IMAP backend)."""
+ self.ensure_connected()
+ target_folder = target_folder.strip()
+ target_folder_id = self._get_or_create_folder(target_folder)
+
+ try:
+ with self.connection.cursor() as cur:
+ cur.execute(
+ "UPDATE email.messages SET folder_id = %s WHERE id = %s",
+ (target_folder_id, int(email_id)),
+ )
+ logging.info(f"Moved email {email_id} to folder '{target_folder}'")
+ return None
+ except Exception as e:
+ logging.error(f"Error moving email {email_id} to {target_folder}: {e}")
+ raise FolderError(f"Failed to move email: {e}")
+
+ def append_message(self, folder: str, message: str, flags: str = "\\Seen") -> bool:
+ """Parse an RFC822 message string and insert it into the specified folder."""
+ self.ensure_connected()
+ folder = folder.strip()
+ folder_id = self._get_or_create_folder(folder)
+
+ try:
+ parser = Parser(policy=policy.default)
+ msg = parser.parsestr(message)
+
+ subject = msg.get("Subject", "")
+ from_addr = msg.get("From", "")
+ to_addr = msg.get("To", "")
+ cc_addr = msg.get("Cc", "")
+ bcc_addr = msg.get("Bcc", "")
+ message_id_hdr = msg.get("Message-ID", None)
+ in_reply_to = msg.get("In-Reply-To", None)
+ references_hdr = msg.get("References", None)
+ date_hdr = msg.get("Date", None)
+
+ # Extract body
+ body_text = None
+ body_html = None
+ if msg.is_multipart():
+ for part in msg.walk():
+ ct = part.get_content_type()
+ if ct == "text/plain" and body_text is None:
+ body_text = part.get_content()
+ elif ct == "text/html" and body_html is None:
+ body_html = part.get_content()
+ else:
+ ct = msg.get_content_type()
+ content = msg.get_content()
+ if ct == "text/html":
+ body_html = content
+ else:
+ body_text = content
+
+ is_read = "\\Seen" in flags
+ is_flagged = "\\Flagged" in flags
+
+ to_list = _comma_str_to_jsonb_list(to_addr)
+ cc_list = _comma_str_to_jsonb_list(cc_addr)
+ bcc_list = _comma_str_to_jsonb_list(bcc_addr)
+
+ with self.connection.cursor() as cur:
+ cur.execute(
+ "INSERT INTO email.messages "
+ "(folder_id, message_id, subject, from_addr, to_addr, cc_addr, bcc_addr, "
+ "date, body_text, body_html, is_read, is_flagged, in_reply_to, references_header, "
+ "size) "
+ "VALUES (%s, %s, %s, %s, %s, %s, %s, "
+ "COALESCE(%s::timestamptz, NOW()), %s, %s, %s, %s, %s, %s, %s) "
+ "RETURNING id",
+ (
+ folder_id,
+ message_id_hdr,
+ subject,
+ from_addr,
+ json.dumps(to_list),
+ json.dumps(cc_list),
+ json.dumps(bcc_list),
+ date_hdr,
+ body_text,
+ body_html,
+ is_read,
+ is_flagged,
+ in_reply_to,
+ references_hdr,
+ len(message),
+ ),
+ )
+ new_id = cur.fetchone()[0]
+
+ # Handle attachments from multipart message
+ if msg.is_multipart():
+ for part in msg.iter_attachments():
+ filename = part.get_filename() or "attachment"
+ content_type = part.get_content_type()
+ payload = part.get_payload(decode=True)
+ size = len(payload) if payload else 0
+ content_id = part.get("Content-ID", None)
+ with self.connection.cursor() as cur:
+ cur.execute(
+ "INSERT INTO email.attachments "
+ "(message_id, filename, content_type, size, content, content_id) "
+ "VALUES (%s, %s, %s, %s, %s, %s)",
+ (new_id, filename, content_type, size,
+ psycopg2.Binary(payload) if payload else None, content_id),
+ )
+
+ logging.info(f"Message appended to folder '{folder}' with id {new_id}")
+ return True
+
+ except Exception as e:
+ logging.error(f"Error appending message to folder '{folder}': {e}")
+ raise FolderError(f"Failed to append message to {folder}: {e}")
+
+
+class PgSMTPBackend:
+ """PostgreSQL-backed SMTP replacement backend.
+
+ Instead of actually sending emails over SMTP, this backend inserts
+ the message into the ``email.messages`` table (in the 'Sent' folder)
+ and logs the send in ``email.sent_log``.
+ """
+
+ def __init__(self, config: EmailConfig):
+ self.config = config
+ self.connection = None # psycopg2 connection
+
+ # ------------------------------------------------------------------
+ # Connection management
+ # ------------------------------------------------------------------
+
+ def connect(self) -> bool:
+ """Establish PostgreSQL connection."""
+ try:
+ self.connection = psycopg2.connect(**_get_pg_conn_params())
+ self.connection.autocommit = True
+ logging.info("PgSMTPBackend connected to PostgreSQL")
+ return True
+ except Exception as e:
+ logging.error(f"PgSMTPBackend connection failed: {e}")
+ raise ConnectionError(f"PostgreSQL connection failed: {e}")
+
+ def disconnect(self):
+ """Close PostgreSQL connection."""
+ if self.connection:
+ try:
+ self.connection.close()
+ except Exception:
+ pass
+ finally:
+ self.connection = None
+
+ def ensure_connected(self):
+ """Ensure PostgreSQL connection is active."""
+ if self.connection is None or self.connection.closed:
+ self.connect()
+ return
+ try:
+ with self.connection.cursor() as cur:
+ cur.execute("SELECT 1")
+ except Exception:
+ logging.warning("PgSMTPBackend connection lost, reconnecting...")
+ self.disconnect()
+ self.connect()
+
+ # ------------------------------------------------------------------
+ # Send (insert into DB)
+ # ------------------------------------------------------------------
+
+ def send_email(
+ self,
+ to: str,
+ subject: str,
+ body: str,
+ html_body: Optional[str] = None,
+ cc: Optional[str] = None,
+ bcc: Optional[str] = None,
+ attachments: Optional[List[str]] = None,
+ ) -> tuple:
+ """Compose and 'send' an email by inserting into the database.
+
+ Returns:
+ tuple[bool, Optional[str]]: (success, RFC822 message string)
+ """
+ self.ensure_connected()
+
+ try:
+ # ---- Build the MIME message (for the returned RFC822 string) ----
+ if html_body or attachments:
+ msg = MIMEMultipart("alternative" if html_body and not attachments else "mixed")
+ text_part = MIMEText(body, "plain", "utf-8")
+ msg.attach(text_part)
+ if html_body:
+ html_part = MIMEText(html_body, "html", "utf-8")
+ msg.attach(html_part)
+ else:
+ msg = MIMEText(body, "plain", "utf-8")
+
+ from email.header import Header
+
+ msg["Subject"] = Header(subject, "utf-8")
+ if self.config.name:
+ encoded_name = Header(self.config.name, "utf-8")
+ msg["From"] = formataddr((str(encoded_name), self.config.email))
+ else:
+ msg["From"] = self.config.email
+ msg["To"] = to
+ if cc:
+ msg["Cc"] = cc
+ if bcc:
+ msg["Bcc"] = bcc
+ msg["Date"] = formatdate(localtime=True)
+ msg["Message-ID"] = make_msgid()
+
+ # Attachments
+ attachment_data_list = []
+ if attachments:
+ if not isinstance(msg, MIMEMultipart):
+ original_msg = msg
+ msg = MIMEMultipart("mixed")
+ msg["Subject"] = Header(subject, "utf-8")
+ if self.config.name:
+ msg["From"] = formataddr((self.config.name, self.config.email))
+ else:
+ msg["From"] = self.config.email
+ msg["To"] = to
+ if cc:
+ msg["Cc"] = cc
+ if bcc:
+ msg["Bcc"] = bcc
+ msg.attach(original_msg)
+
+ for file_path in attachments:
+ filename = os.path.basename(file_path)
+ with open(file_path, "rb") as f:
+ file_data = f.read()
+ attachment_data_list.append((filename, "application/octet-stream", file_data))
+
+ att = MIMEBase("application", "octet-stream")
+ att.set_payload(file_data)
+ encoders.encode_base64(att)
+ att.add_header("Content-Disposition", f"attachment; filename= {filename}")
+ msg.attach(att)
+
+ message_string = msg.as_string()
+
+ # ---- Insert into database ----
+ to_list = _comma_str_to_jsonb_list(to)
+ cc_list = _comma_str_to_jsonb_list(cc)
+ bcc_list = _comma_str_to_jsonb_list(bcc)
+
+ # Ensure 'Sent' folder exists
+ with self.connection.cursor() as cur:
+ cur.execute(
+ "INSERT INTO email.folders (name) VALUES ('Sent') "
+ "ON CONFLICT (name) DO UPDATE SET name = EXCLUDED.name "
+ "RETURNING id"
+ )
+ sent_folder_id = cur.fetchone()[0]
+
+ cur.execute(
+ "INSERT INTO email.messages "
+ "(folder_id, message_id, subject, from_addr, to_addr, cc_addr, bcc_addr, "
+ "date, body_text, body_html, is_read, size) "
+ "VALUES (%s, %s, %s, %s, %s, %s, %s, NOW(), %s, %s, TRUE, %s) "
+ "RETURNING id",
+ (
+ sent_folder_id,
+ msg["Message-ID"],
+ subject,
+ msg["From"],
+ json.dumps(to_list),
+ json.dumps(cc_list),
+ json.dumps(bcc_list),
+ body,
+ html_body,
+ len(message_string),
+ ),
+ )
+ new_msg_id = cur.fetchone()[0]
+
+ # Insert attachments
+ for filename, content_type, file_data in attachment_data_list:
+ cur.execute(
+ "INSERT INTO email.attachments "
+ "(message_id, filename, content_type, size, content) "
+ "VALUES (%s, %s, %s, %s, %s)",
+ (new_msg_id, filename, content_type, len(file_data),
+ psycopg2.Binary(file_data)),
+ )
+
+ # Log to sent_log
+ cur.execute(
+ "INSERT INTO email.sent_log (message_id) VALUES (%s)",
+ (new_msg_id,),
+ )
+
+ logging.info(f"Email 'sent' (stored) to {to} with id {new_msg_id}")
+ return True, message_string
+
+ except Exception as e:
+ logging.error(f"Error sending email: {e}")
+ raise SendEmailError(f"Failed to send email: {e}")
+
+ # ------------------------------------------------------------------
+ # Test connection
+ # ------------------------------------------------------------------
+
+ def test_connection(self) -> bool:
+ """Test PostgreSQL connection."""
+ try:
+ self.ensure_connected()
+ return True
+ except Exception:
+ return False
+
+
+class PgDraftBackend:
+ """PostgreSQL-backed draft storage."""
+
+ def __init__(self):
+ self.connection = None
+
+ def _ensure_connected(self):
+ if self.connection is None or self.connection.closed:
+ self.connection = psycopg2.connect(**_get_pg_conn_params())
+ self.connection.autocommit = True
+
+ def save_draft(self, subject, body, html_body=None, to=None, cc=None, bcc=None, from_addr=None):
+ self._ensure_connected()
+ to_list = _comma_str_to_jsonb_list(to)
+ cc_list = _comma_str_to_jsonb_list(cc)
+ bcc_list = _comma_str_to_jsonb_list(bcc)
+ with self.connection.cursor() as cur:
+ cur.execute(
+ "INSERT INTO email.drafts (subject, from_addr, to_addr, cc_addr, bcc_addr, body_text, body_html) "
+ "VALUES (%s, %s, %s, %s, %s, %s, %s) RETURNING id",
+ (subject, from_addr, json.dumps(to_list), json.dumps(cc_list), json.dumps(bcc_list), body, html_body),
+ )
+ return str(cur.fetchone()[0])
+
+ def get_drafts(self, page=1, page_size=20):
+ self._ensure_connected()
+ with self.connection.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
+ cur.execute("SELECT COUNT(*) FROM email.drafts")
+ total = cur.fetchone()[0]
+ offset = (page - 1) * page_size
+ cur.execute(
+ "SELECT * FROM email.drafts ORDER BY updated_at DESC LIMIT %s OFFSET %s",
+ (page_size, offset),
+ )
+ rows = cur.fetchall()
+ total_pages = max(1, (total + page_size - 1) // page_size)
+ drafts = []
+ for r in rows:
+ drafts.append({
+ 'draft_id': str(r['id']),
+ 'subject': r['subject'] or '',
+ 'to': _jsonb_list_to_comma_str(r['to_addr']),
+ 'cc': _jsonb_list_to_comma_str(r['cc_addr']),
+ 'bcc': _jsonb_list_to_comma_str(r['bcc_addr']),
+ 'body': r['body_text'] or '',
+ 'html_body': r['body_html'],
+ 'created_at': r['created_at'].isoformat() if r['created_at'] else '',
+ 'updated_at': r['updated_at'].isoformat() if r['updated_at'] else '',
+ })
+ return {'drafts': drafts, 'total_drafts': total, 'current_page': page, 'total_pages': total_pages, 'page_size': page_size}
+
+ def get_draft(self, draft_id):
+ self._ensure_connected()
+ with self.connection.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
+ cur.execute("SELECT * FROM email.drafts WHERE id = %s", (int(draft_id),))
+ r = cur.fetchone()
+ if r is None:
+ raise FolderError(f"Draft not found: {draft_id}")
+ return {
+ 'draft_id': str(r['id']),
+ 'subject': r['subject'] or '',
+ 'to': _jsonb_list_to_comma_str(r['to_addr']),
+ 'cc': _jsonb_list_to_comma_str(r['cc_addr']),
+ 'bcc': _jsonb_list_to_comma_str(r['bcc_addr']),
+ 'body': r['body_text'] or '',
+ 'html_body': r['body_html'],
+ 'created_at': r['created_at'].isoformat() if r['created_at'] else '',
+ 'updated_at': r['updated_at'].isoformat() if r['updated_at'] else '',
+ }
+
+ def update_draft(self, draft_id, subject=None, body=None, html_body=None, to=None, cc=None, bcc=None):
+ self._ensure_connected()
+ sets = ["updated_at = NOW()"]
+ params = []
+ if subject is not None:
+ sets.append("subject = %s"); params.append(subject)
+ if body is not None:
+ sets.append("body_text = %s"); params.append(body)
+ if html_body is not None:
+ sets.append("body_html = %s"); params.append(html_body)
+ if to is not None:
+ sets.append("to_addr = %s"); params.append(json.dumps(_comma_str_to_jsonb_list(to)))
+ if cc is not None:
+ sets.append("cc_addr = %s"); params.append(json.dumps(_comma_str_to_jsonb_list(cc)))
+ if bcc is not None:
+ sets.append("bcc_addr = %s"); params.append(json.dumps(_comma_str_to_jsonb_list(bcc)))
+ params.append(int(draft_id))
+ with self.connection.cursor() as cur:
+ cur.execute(f"UPDATE email.drafts SET {', '.join(sets)} WHERE id = %s", params)
+ if cur.rowcount == 0:
+ raise FolderError(f"Draft not found: {draft_id}")
+ return True
+
+ def delete_draft(self, draft_id):
+ self._ensure_connected()
+ with self.connection.cursor() as cur:
+ cur.execute("DELETE FROM email.drafts WHERE id = %s", (int(draft_id),))
+ if cur.rowcount == 0:
+ raise FolderError(f"Draft not found: {draft_id}")
+ return True
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/backends/smtp_backend.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/backends/smtp_backend.py
new file mode 100644
index 00000000..cc4aee1b
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/backends/smtp_backend.py
@@ -0,0 +1,233 @@
+import smtplib
+import logging
+import os
+from email.mime.text import MIMEText
+from email.mime.multipart import MIMEMultipart
+from email.mime.base import MIMEBase
+from email import encoders
+from email.utils import formataddr
+from typing import List, Optional
+from ..models.config import EmailConfig
+from ..utils.exceptions import ConnectionError, AuthenticationError, SendEmailError
+from ..utils.validators import validate_email_list, validate_file_path
+from ..config.settings import ConfigManager
+
+
+class SMTPBackend:
+ """SMTP backend for sending emails"""
+
+ def __init__(self, config: EmailConfig):
+ self.config = config
+ self.connection: Optional[smtplib.SMTP] = None
+
+ def connect(self) -> bool:
+ """Establish SMTP connection"""
+ try:
+ self.connection = smtplib.SMTP(
+ self.config.smtp_server,
+ self.config.smtp_port
+ )
+
+ # Enable debugging for development
+ if logging.getLogger().isEnabledFor(logging.DEBUG):
+ self.connection.set_debuglevel(1)
+
+ # Start TLS if configured
+ if self.config.use_starttls:
+ self.connection.starttls()
+
+ # Login only if password is provided and server supports auth
+ if self.config.password and self.config.password.strip():
+ try:
+ # Check if server supports authentication
+ if 'auth' in self.connection.esmtp_features:
+ self.connection.login(self.config.email, self.config.password)
+ logging.info(f"SMTP authenticated for {self.config.email}")
+ else:
+ logging.info(f"SMTP server doesn't support auth, proceeding without authentication")
+ except smtplib.SMTPAuthenticationError as e:
+ # If auth fails but server might allow sending without auth, try to continue
+ logging.warning(f"SMTP authentication failed: {str(e)}, trying without auth")
+ else:
+ logging.info(f"No password provided, proceeding without authentication")
+
+ logging.info(f"SMTP connected for {self.config.email}")
+ return True
+
+ except Exception as e:
+ logging.error(f"SMTP connection failed: {str(e)}")
+ raise ConnectionError(f"SMTP connection failed: {str(e)}")
+
+ def disconnect(self):
+ """Close SMTP connection"""
+ if self.connection:
+ try:
+ self.connection.quit()
+ except:
+ pass
+ finally:
+ self.connection = None
+
+ def ensure_connected(self):
+ """Ensure SMTP connection is active"""
+ if not self.connection:
+ logging.debug("No SMTP connection, connecting...")
+ self.connect()
+
+ # Test connection with more robust checking
+ try:
+ status = self.connection.noop()
+ if status[0] != 250: # NOOP should return 250 OK
+ raise Exception(f"NOOP returned {status}")
+ except Exception as e:
+ logging.warning(f"SMTP connection test failed: {e}, reconnecting...")
+ self.disconnect()
+ self.connect()
+
+ def send_email(self, to: str, subject: str, body: str,
+ html_body: Optional[str] = None,
+ cc: Optional[str] = None,
+ bcc: Optional[str] = None,
+ attachments: Optional[List[str]] = None) -> tuple[bool, Optional[str]]:
+ """Send email with optional HTML, CC, BCC, and attachments
+
+ Returns:
+ tuple[bool, Optional[str]]: (success, message_string_for_saving)
+ """
+
+ # Validate recipients
+ valid, error = validate_email_list(to)
+ if not valid:
+ raise SendEmailError(f"Invalid TO addresses: {error}")
+
+ if cc:
+ valid, error = validate_email_list(cc)
+ if not valid:
+ raise SendEmailError(f"Invalid CC addresses: {error}")
+
+ if bcc:
+ valid, error = validate_email_list(bcc)
+ if not valid:
+ raise SendEmailError(f"Invalid BCC addresses: {error}")
+
+ self.ensure_connected()
+
+ try:
+ # Create message
+ if html_body or attachments:
+ msg = MIMEMultipart('alternative' if html_body else 'mixed')
+ else:
+ msg = MIMEText(body, 'plain', 'utf-8')
+
+ if isinstance(msg, MIMEMultipart):
+ # Add text part
+ text_part = MIMEText(body, 'plain', 'utf-8')
+ msg.attach(text_part)
+
+ # Add HTML part if provided
+ if html_body:
+ html_part = MIMEText(html_body, 'html', 'utf-8')
+ msg.attach(html_part)
+
+ # Set headers with proper Chinese encoding
+ from email.header import Header
+
+ # Encode subject for Chinese characters
+ msg['Subject'] = Header(subject, 'utf-8')
+
+ # Format From header with name if provided, with Chinese support
+ if self.config.name:
+ # Encode name for Chinese characters
+ encoded_name = Header(self.config.name, 'utf-8')
+ msg['From'] = formataddr((str(encoded_name), self.config.email))
+ else:
+ msg['From'] = self.config.email
+ msg['To'] = to
+
+ if cc:
+ msg['Cc'] = cc
+ if bcc:
+ msg['Bcc'] = bcc
+
+ # Add attachments
+ if attachments:
+ # Ensure we have a multipart message
+ if not isinstance(msg, MIMEMultipart):
+ original_msg = msg
+ msg = MIMEMultipart('mixed')
+ msg['Subject'] = subject
+ # Format From header with name if provided
+ if self.config.name:
+ msg['From'] = formataddr((self.config.name, self.config.email))
+ else:
+ msg['From'] = self.config.email
+ msg['To'] = to
+ if cc:
+ msg['Cc'] = cc
+ if bcc:
+ msg['Bcc'] = bcc
+ msg.attach(original_msg)
+
+ for file_path in attachments:
+ self._attach_file(msg, file_path)
+
+ # Prepare recipient list
+ recipients = [addr.strip() for addr in to.split(',')]
+ if cc:
+ recipients.extend([addr.strip() for addr in cc.split(',')])
+ if bcc:
+ recipients.extend([addr.strip() for addr in bcc.split(',')])
+
+ # Send email
+ self.connection.send_message(msg, to_addrs=recipients)
+ logging.info(f"Email sent successfully to {to}")
+
+ # Return success and the complete message for saving to Sent folder
+ return True, msg.as_string()
+
+ except Exception as e:
+ logging.error(f"Error sending email: {str(e)}")
+ raise SendEmailError(f"Failed to send email: {str(e)}")
+
+ def _attach_file(self, msg: MIMEMultipart, file_path: str):
+ """Attach file to message"""
+ # Validate file path
+ valid, error = validate_file_path(file_path, must_exist=True)
+ if not valid:
+ raise SendEmailError(f"Attachment error: {error}")
+
+ # Validate attachment upload path if configured
+ config_manager = ConfigManager()
+ path_valid, path_error = config_manager.validate_attachment_upload_path(file_path)
+ if not path_valid:
+ raise SendEmailError(f"Attachment path validation failed: {path_error}")
+
+ try:
+ with open(file_path, 'rb') as f:
+ attachment_data = f.read()
+
+ # Create attachment
+ attachment = MIMEBase('application', 'octet-stream')
+ attachment.set_payload(attachment_data)
+ encoders.encode_base64(attachment)
+
+ # Add header
+ filename = os.path.basename(file_path)
+ attachment.add_header(
+ 'Content-Disposition',
+ f'attachment; filename= {filename}'
+ )
+
+ msg.attach(attachment)
+ logging.debug(f"Attached file: {filename}")
+
+ except Exception as e:
+ raise SendEmailError(f"Failed to attach file {file_path}: {str(e)}")
+
+ def test_connection(self) -> bool:
+ """Test SMTP connection without sending email"""
+ try:
+ self.ensure_connected()
+ return True
+ except Exception:
+ return False
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/config/__init__.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/config/__init__.py
new file mode 100644
index 00000000..d2423dc9
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/config/__init__.py
@@ -0,0 +1,3 @@
+from .settings import ConfigManager, config_manager
+
+__all__ = ['ConfigManager', 'config_manager']
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/config/settings.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/config/settings.py
new file mode 100644
index 00000000..7a1834ef
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/config/settings.py
@@ -0,0 +1,155 @@
+import json
+import os
+from pathlib import Path
+from typing import Optional, List
+from ..models.config import EmailConfig, WorkspaceConfig
+
+
+class ConfigManager:
+ """Configuration manager for email MCP server"""
+
+ def __init__(self):
+ self.workspace_config: Optional[WorkspaceConfig] = None
+ self.email_config: Optional[EmailConfig] = None
+
+ def load_workspace_config(self, attachment_upload_path: str = None,
+ attachment_download_path: str = None,
+ email_export_path: str = None,
+ config_file: str = None) -> WorkspaceConfig:
+ """Load workspace configuration"""
+ self.workspace_config = WorkspaceConfig(
+ attachment_upload_path=attachment_upload_path,
+ attachment_download_path=attachment_download_path,
+ email_export_path=email_export_path,
+ config_file=config_file
+ )
+ return self.workspace_config
+
+ def load_email_config(self, config_file: str) -> EmailConfig:
+ """Load email configuration from JSON file (uses first account)"""
+ if not os.path.exists(config_file):
+ raise FileNotFoundError(f"Configuration file not found: {config_file}")
+
+ try:
+ with open(config_file, 'r', encoding='utf-8') as f:
+ config_data = json.load(f)
+
+ # if not isinstance(config_data, list):
+ # raise ValueError("Configuration file should contain a list of email accounts")
+
+ if not config_data:
+ raise ValueError("Configuration file is empty")
+
+ # Use first account only
+ if isinstance(config_data, list):
+ if not config_data:
+ raise ValueError("Configuration file is empty")
+ account_data = config_data[0] # Use first account
+ else:
+ account_data = config_data # Single account format
+
+ if not isinstance(account_data, dict):
+ raise ValueError("Invalid account data format")
+
+ email_config = EmailConfig(
+ email=account_data.get('email', ''),
+ password=account_data.get('password', ''),
+ name=account_data.get('name', ''),
+ imap_server=account_data.get('imap_server', 'localhost'),
+ imap_port=account_data.get('imap_port', 993),
+ smtp_server=account_data.get('smtp_server', 'localhost'),
+ smtp_port=account_data.get('smtp_port', 587),
+ use_ssl=account_data.get('use_ssl', True),
+ use_starttls=account_data.get('use_starttls', True)
+ )
+
+ # Validate required fields
+ if not email_config.email or not email_config.password:
+ raise ValueError("Email and password are required")
+
+ self.email_config = email_config
+ return email_config
+
+ except json.JSONDecodeError as e:
+ raise ValueError(f"Invalid JSON in configuration file: {str(e)}")
+ except Exception as e:
+ raise RuntimeError(f"Failed to load email configuration: {str(e)}")
+
+ def get_email_config(self) -> Optional[EmailConfig]:
+ """Get email configuration"""
+ return self.email_config
+
+ def validate_attachment_upload_path(self, file_path: str) -> tuple[bool, str]:
+ """Validate if file path is within attachment upload path"""
+ if not self.workspace_config or not self.workspace_config.attachment_upload_path:
+ return True, ""
+
+ try:
+ upload_path = Path(self.workspace_config.attachment_upload_path).resolve()
+ resolved_path = Path(file_path).resolve()
+
+ if not resolved_path.is_relative_to(upload_path):
+ return False, f"Error: Path '{file_path}' is outside attachment upload path '{upload_path}'"
+ return True, ""
+
+ except Exception as e:
+ return False, f"Error validating attachment upload path: {str(e)}"
+
+ def validate_attachment_download_path(self, file_path: str) -> tuple[bool, str]:
+ """Validate if file path is within attachment download path"""
+ if not self.workspace_config or not self.workspace_config.attachment_download_path:
+ return True, ""
+
+ try:
+ download_path = Path(self.workspace_config.attachment_download_path).resolve()
+ resolved_path = Path(file_path).resolve()
+
+ if not resolved_path.is_relative_to(download_path):
+ return False, f"Error: Path '{file_path}' is outside attachment download path '{download_path}'"
+ return True, ""
+
+ except Exception as e:
+ return False, f"Error validating attachment download path: {str(e)}"
+
+ def validate_email_export_path(self, file_path: str) -> tuple[bool, str]:
+ """Validate if file path is within email export path"""
+ if not self.workspace_config or not self.workspace_config.email_export_path:
+ return True, ""
+
+ try:
+ export_path = Path(self.workspace_config.email_export_path).resolve()
+ resolved_path = Path(file_path).resolve()
+
+ if not resolved_path.is_relative_to(export_path):
+ return False, f"Error: Path '{file_path}' is outside email export path '{export_path}'"
+ return True, ""
+
+ except Exception as e:
+ return False, f"Error validating email export path: {str(e)}"
+
+ def get_unique_download_path(self, filename: str) -> str:
+ """Get unique path for downloading file, adding (1), (2), etc. if file exists"""
+ if not self.workspace_config or not self.workspace_config.attachment_download_path:
+ return filename
+
+ base_path = Path(self.workspace_config.attachment_download_path)
+ file_path = base_path / filename
+
+ if not file_path.exists():
+ return str(file_path)
+
+ # Extract name and extension
+ stem = file_path.stem
+ suffix = file_path.suffix
+
+ counter = 1
+ while True:
+ new_filename = f"{stem}({counter}){suffix}"
+ new_path = base_path / new_filename
+ if not new_path.exists():
+ return str(new_path)
+ counter += 1
+
+
+# Global config manager instance
+config_manager = ConfigManager()
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/models/__init__.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/models/__init__.py
new file mode 100644
index 00000000..15d0f07c
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/models/__init__.py
@@ -0,0 +1,12 @@
+from .config import EmailConfig, WorkspaceConfig
+from .email import EmailMessage, EmailAttachment, EmailFolder, SearchResult, MailboxStats
+
+__all__ = [
+ 'EmailConfig',
+ 'WorkspaceConfig',
+ 'EmailMessage',
+ 'EmailAttachment',
+ 'EmailFolder',
+ 'SearchResult',
+ 'MailboxStats'
+]
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/models/config.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/models/config.py
new file mode 100644
index 00000000..88be16cf
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/models/config.py
@@ -0,0 +1,30 @@
+from dataclasses import dataclass
+from typing import Optional
+from datetime import datetime
+
+
+@dataclass
+class EmailConfig:
+ """Email server configuration"""
+ email: str
+ password: str
+ name: str = ""
+ imap_server: str = "localhost"
+ imap_port: int = 993
+ smtp_server: str = "localhost"
+ smtp_port: int = 587
+ use_ssl: bool = True
+ use_starttls: bool = True
+
+
+@dataclass
+class WorkspaceConfig:
+ """Workspace configuration"""
+ attachment_upload_path: Optional[str] = None # Path for uploading attachments
+ attachment_download_path: Optional[str] = None # Path for downloading attachments
+ email_export_path: Optional[str] = None # Path for exporting emails
+ config_file: Optional[str] = None
+ max_page_size: int = 50
+ default_page_size: int = 20
+ connection_timeout: int = 30
+ cache_timeout_minutes: int = 30
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/models/email.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/models/email.py
new file mode 100644
index 00000000..7ad530b7
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/models/email.py
@@ -0,0 +1,69 @@
+from dataclasses import dataclass
+from typing import List, Optional, Any
+
+
+@dataclass
+class EmailAttachment:
+ """Email attachment information"""
+ filename: str
+ content_type: str
+ size: int
+ attachment_id: Optional[str] = None
+ content: Optional[bytes] = None # 附件的实际内容数据
+
+
+@dataclass
+class EmailMessage:
+ """Email message data model"""
+ email_id: str
+ subject: str
+ from_addr: str
+ to_addr: str
+ cc_addr: Optional[str] = None
+ bcc_addr: Optional[str] = None
+ date: Optional[str] = None
+ message_id: Optional[str] = None
+ body_text: Optional[str] = None
+ body_html: Optional[str] = None
+ attachments: List[EmailAttachment] = None
+ is_read: bool = False
+ is_important: bool = False
+ folder: Optional[str] = None
+ raw_message: Optional[Any] = None
+
+ def __post_init__(self):
+ if self.attachments is None:
+ self.attachments = []
+
+
+@dataclass
+class EmailFolder:
+ """Email folder information"""
+ name: str
+ total_messages: int = 0
+ unread_messages: int = 0
+ can_select: bool = True
+
+
+@dataclass
+class SearchResult:
+ """Email search result"""
+ emails: List[EmailMessage]
+ total_results: int
+ current_page: int
+ page_size: int
+ query: str
+ folder: Optional[str] = None
+
+ @property
+ def total_pages(self) -> int:
+ return (self.total_results + self.page_size - 1) // self.page_size
+
+
+@dataclass
+class MailboxStats:
+ """Mailbox statistics"""
+ folder_name: str
+ total_messages: int
+ unread_messages: int
+ total_size_mb: Optional[float] = None
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/server.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/server.py
new file mode 100644
index 00000000..3e0d72da
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/server.py
@@ -0,0 +1,208 @@
+import argparse
+import json
+import logging
+import sys
+import os
+import tempfile
+from mcp.server.fastmcp import FastMCP
+from .config import config_manager
+from .services import EmailService, FolderService, SearchService, DraftService
+from .backends.pg_backend import PgIMAPBackend, PgSMTPBackend, PgDraftBackend
+from .backends import FileBackend
+from .models.config import EmailConfig
+from .tools import register_email_tools, register_folder_tools, register_management_tools
+
+
+def setup_logging(debug: bool = False):
+ """Setup logging configuration"""
+ level = logging.DEBUG if debug else logging.INFO
+ logging.basicConfig(
+ level=level,
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
+ handlers=[
+ logging.StreamHandler(sys.stderr)
+ ]
+ )
+
+
+def _make_default_email_config() -> EmailConfig:
+ """Create a default EmailConfig for PG mode (no real IMAP/SMTP needed)."""
+ return EmailConfig(
+ email=os.environ.get("EMAIL_ADDRESS", "user@example.com"),
+ name=os.environ.get("EMAIL_NAME", "PG Email User"),
+ imap_server="localhost",
+ imap_port=993,
+ smtp_server="localhost",
+ smtp_port=587,
+ password="unused",
+ )
+
+
+def create_services(email_config):
+ """Create service instances using PostgreSQL backends"""
+ # Create PG-backed backends
+ imap_backend = PgIMAPBackend(email_config)
+ smtp_backend = PgSMTPBackend(email_config)
+
+ email_export_path = config_manager.workspace_config.email_export_path if config_manager.workspace_config else None
+ attachment_download_path = config_manager.workspace_config.attachment_download_path if config_manager.workspace_config else None
+ file_backend = FileBackend(email_export_path, attachment_download_path)
+
+ # Create services, injecting PG backends into EmailService
+ email_service = EmailService(email_config)
+ # Replace the IMAP/SMTP backends that EmailService created internally
+ email_service.imap_backend = imap_backend
+ email_service.smtp_backend = smtp_backend
+
+ folder_service = FolderService(imap_backend)
+ search_service = SearchService(imap_backend)
+ draft_service = DraftService(file_backend)
+
+ return email_service, folder_service, search_service, draft_service
+
+
+def main():
+ """Main function to run the emails MCP server"""
+ parser = argparse.ArgumentParser(description='Emails MCP Server')
+ parser.add_argument(
+ '--attachment_upload_path',
+ type=str,
+ default=None,
+ help='Directory path for attachment uploads (restricts file selection to this path and subdirectories)'
+ )
+ parser.add_argument(
+ '--attachment_download_path',
+ type=str,
+ default=None,
+ help='Directory path for attachment downloads (files will be saved here with unique names)'
+ )
+ parser.add_argument(
+ '--email_export_path',
+ type=str,
+ default=None,
+ help='Directory path for email exports (exports will be saved here with date-based filenames)'
+ )
+ parser.add_argument(
+ '--config_file',
+ type=str,
+ default=None,
+ help='Email configuration file path (optional for PG mode)'
+ )
+ parser.add_argument(
+ '--debug',
+ action='store_true',
+ help='Enable debug logging'
+ )
+
+ args = parser.parse_args()
+
+ # Setup logging
+ setup_logging(args.debug)
+ logger = logging.getLogger(__name__)
+
+ try:
+ # Initialize MCP server
+ mcp = FastMCP("emails-mcp")
+
+ # If no config file provided or it doesn't exist, create a temporary
+ # dummy config so that config_manager doesn't complain.
+ config_file = args.config_file
+ _temp_config_file = None
+
+ if config_file and os.path.exists(config_file):
+ # Real config file supplied – use it normally
+ pass
+ else:
+ if config_file and not os.path.exists(config_file):
+ logger.info(
+ f"Config file '{config_file}' not found – running in PG-only mode"
+ )
+ else:
+ logger.info("No config file specified – running in PG-only mode")
+
+ # Write a minimal dummy config so config_manager.load_email_config works
+ dummy = {
+ "email": os.environ.get("EMAIL_ADDRESS", "user@example.com"),
+ "name": os.environ.get("EMAIL_NAME", "PG Email User"),
+ "imap_server": "localhost",
+ "imap_port": 993,
+ "smtp_server": "localhost",
+ "smtp_port": 587,
+ "password": "unused",
+ }
+ fd, _temp_config_file = tempfile.mkstemp(suffix=".json", prefix="email_cfg_")
+ with os.fdopen(fd, "w") as fh:
+ json.dump(dummy, fh)
+ config_file = _temp_config_file
+
+ # Load configuration
+ config_manager.load_workspace_config(
+ attachment_upload_path=args.attachment_upload_path,
+ attachment_download_path=args.attachment_download_path,
+ email_export_path=args.email_export_path,
+ config_file=config_file
+ )
+
+ email_config = config_manager.load_email_config(config_file)
+ if not email_config:
+ # Fallback: build a default config for PG mode
+ email_config = _make_default_email_config()
+
+ logger.info(f"Loaded configuration for: {email_config.email}")
+
+ # Create services (PG-backed)
+ email_service, folder_service, search_service, draft_service = create_services(email_config)
+
+ # Register MCP tools
+ register_email_tools(mcp, email_service)
+ register_folder_tools(mcp, folder_service)
+ register_management_tools(mcp, draft_service, email_service)
+
+ logger.info("All MCP tools registered successfully")
+
+ # Log path restrictions if set
+ if config_manager.workspace_config:
+ config = config_manager.workspace_config
+ if config.attachment_upload_path:
+ logger.info(f"Attachment uploads restricted to: {config.attachment_upload_path}")
+ if config.attachment_download_path:
+ logger.info(f"Attachment downloads will be saved to: {config.attachment_download_path}")
+ if config.email_export_path:
+ logger.info(f"Email exports will be saved to: {config.email_export_path}")
+
+ # Test PG connection on startup
+ try:
+ imap_ok, smtp_ok = email_service.check_connection()
+ if imap_ok and smtp_ok:
+ logger.info("All PG email connections verified successfully")
+ else:
+ logger.warning("PG email connection check returned partial failure")
+ except Exception as e:
+ logger.warning(f"PG connection test failed: {str(e)}")
+
+ # Start the MCP server
+ logger.info("Starting emails MCP server (PG backend)...")
+ mcp.run(transport='stdio')
+
+ except KeyboardInterrupt:
+ logger.info("Server shutdown requested")
+ except Exception as e:
+ logger.error(f"Server startup failed: {str(e)}")
+ sys.exit(1)
+ finally:
+ # Cleanup
+ try:
+ if 'email_service' in locals():
+ email_service.cleanup()
+ except:
+ pass
+ # Remove temporary config file if we created one
+ try:
+ if '_temp_config_file' in locals() and _temp_config_file and os.path.exists(_temp_config_file):
+ os.unlink(_temp_config_file)
+ except:
+ pass
+
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/services/__init__.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/services/__init__.py
new file mode 100644
index 00000000..fcf6aa2e
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/services/__init__.py
@@ -0,0 +1,6 @@
+from .email_service import EmailService
+from .folder_service import FolderService
+from .search_service import SearchService
+from .draft_service import DraftService
+
+__all__ = ['EmailService', 'FolderService', 'SearchService', 'DraftService']
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/services/draft_service.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/services/draft_service.py
new file mode 100644
index 00000000..b9b3d9d6
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/services/draft_service.py
@@ -0,0 +1,172 @@
+from typing import List, Optional, Dict, Any
+from datetime import datetime
+from ..models.email import EmailMessage
+from ..backends.file_backend import FileBackend
+from ..utils.exceptions import EmailMCPError
+
+
+class DraftService:
+ """Draft management service layer"""
+
+ def __init__(self, file_backend: FileBackend):
+ self.file_backend = file_backend
+ self.drafts: Dict[str, Dict[str, Any]] = {}
+ self._draft_counter = 1
+
+ def save_draft(self, subject: str, body: str,
+ html_body: Optional[str] = None,
+ to: Optional[str] = None,
+ cc: Optional[str] = None,
+ bcc: Optional[str] = None) -> str:
+ """Save email draft and return draft ID"""
+ try:
+ draft_id = f"draft_{self._draft_counter}"
+ self._draft_counter += 1
+
+ draft_data = {
+ 'draft_id': draft_id,
+ 'subject': subject,
+ 'body': body,
+ 'html_body': html_body,
+ 'to': to,
+ 'cc': cc,
+ 'bcc': bcc,
+ 'created_at': datetime.now().isoformat(),
+ 'updated_at': datetime.now().isoformat()
+ }
+
+ self.drafts[draft_id] = draft_data
+ return draft_id
+
+ except Exception as e:
+ raise EmailMCPError(f"Failed to save draft: {str(e)}")
+
+ def get_drafts(self, page: int = 1, page_size: int = 20) -> Dict[str, Any]:
+ """Get paginated list of drafts"""
+ try:
+ draft_list = list(self.drafts.values())
+ draft_list.sort(key=lambda x: x['updated_at'], reverse=True)
+
+ total_drafts = len(draft_list)
+ total_pages = (total_drafts + page_size - 1) // page_size if total_drafts > 0 else 1
+
+ if page < 1:
+ page = 1
+ elif page > total_pages:
+ page = total_pages
+
+ start_idx = (page - 1) * page_size
+ end_idx = start_idx + page_size
+ page_drafts = draft_list[start_idx:end_idx]
+
+ return {
+ 'drafts': page_drafts,
+ 'total_drafts': total_drafts,
+ 'current_page': page,
+ 'total_pages': total_pages,
+ 'page_size': page_size
+ }
+
+ except Exception as e:
+ raise EmailMCPError(f"Failed to get drafts: {str(e)}")
+
+ def get_draft(self, draft_id: str) -> Dict[str, Any]:
+ """Get specific draft by ID"""
+ if draft_id not in self.drafts:
+ raise EmailMCPError(f"Draft not found: {draft_id}")
+
+ return self.drafts[draft_id]
+
+ def update_draft(self, draft_id: str,
+ subject: Optional[str] = None,
+ body: Optional[str] = None,
+ html_body: Optional[str] = None,
+ to: Optional[str] = None,
+ cc: Optional[str] = None,
+ bcc: Optional[str] = None) -> bool:
+ """Update existing draft"""
+ if draft_id not in self.drafts:
+ raise EmailMCPError(f"Draft not found: {draft_id}")
+
+ try:
+ draft = self.drafts[draft_id]
+
+ # Update only provided fields
+ if subject is not None:
+ draft['subject'] = subject
+ if body is not None:
+ draft['body'] = body
+ if html_body is not None:
+ draft['html_body'] = html_body
+ if to is not None:
+ draft['to'] = to
+ if cc is not None:
+ draft['cc'] = cc
+ if bcc is not None:
+ draft['bcc'] = bcc
+
+ draft['updated_at'] = datetime.now().isoformat()
+
+ return True
+
+ except Exception as e:
+ raise EmailMCPError(f"Failed to update draft: {str(e)}")
+
+ def delete_draft(self, draft_id: str) -> bool:
+ """Delete draft"""
+ if draft_id not in self.drafts:
+ raise EmailMCPError(f"Draft not found: {draft_id}")
+
+ try:
+ del self.drafts[draft_id]
+ return True
+ except Exception as e:
+ raise EmailMCPError(f"Failed to delete draft: {str(e)}")
+
+ def export_drafts(self, export_path: str) -> bool:
+ """Export all drafts to file"""
+ try:
+ # Convert drafts to list for export
+ draft_emails = []
+ for draft_data in self.drafts.values():
+ # Create minimal EmailMessage for export
+ email_obj = EmailMessage(
+ email_id=draft_data['draft_id'],
+ subject=draft_data['subject'],
+ from_addr="", # Will be set when sending
+ to_addr=draft_data.get('to', ''),
+ cc_addr=draft_data.get('cc'),
+ bcc_addr=draft_data.get('bcc'),
+ body_text=draft_data['body'],
+ body_html=draft_data.get('html_body'),
+ folder="Drafts"
+ )
+ draft_emails.append(email_obj)
+
+ return self.file_backend.export_emails(draft_emails, export_path, 'json')
+
+ except Exception as e:
+ raise EmailMCPError(f"Failed to export drafts: {str(e)}")
+
+ def import_drafts(self, import_path: str) -> int:
+ """Import drafts from file"""
+ try:
+ imported_emails = self.file_backend.import_emails(import_path)
+ imported_count = 0
+
+ for email_obj in imported_emails:
+ # Convert EmailMessage back to draft format
+ self.save_draft(
+ subject=email_obj.subject,
+ body=email_obj.body_text or "",
+ html_body=email_obj.body_html,
+ to=email_obj.to_addr,
+ cc=email_obj.cc_addr,
+ bcc=email_obj.bcc_addr
+ )
+ imported_count += 1
+
+ return imported_count
+
+ except Exception as e:
+ raise EmailMCPError(f"Failed to import drafts: {str(e)}")
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/services/email_service.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/services/email_service.py
new file mode 100644
index 00000000..7cf4a692
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/services/email_service.py
@@ -0,0 +1,433 @@
+from typing import List, Optional, Tuple
+from datetime import datetime
+import logging
+from ..models.config import EmailConfig
+from ..models.email import EmailMessage, SearchResult
+from ..backends.pg_backend import PgIMAPBackend, PgSMTPBackend
+from ..utils.exceptions import EmailMCPError, ValidationError
+from ..utils.validators import validate_page_params, validate_search_query
+from ..utils.email_parser import format_email_summary
+
+
+class EmailService:
+ """Email operations service layer"""
+
+ def __init__(self, email_config: EmailConfig):
+ self.config = email_config
+ self.imap_backend = PgIMAPBackend(email_config)
+ self.smtp_backend = PgSMTPBackend(email_config)
+
+ def get_emails(self, folder: str = "INBOX", page: int = 1, page_size: int = 20) -> SearchResult:
+ """Get paginated emails from folder"""
+ try:
+ # Validate parameters
+ page, page_size, warning = validate_page_params(page, page_size)
+
+ # Get total count and select folder
+ total_messages, unread_count = self.imap_backend.select_folder(folder)
+
+ if total_messages == 0:
+ return SearchResult(
+ emails=[],
+ total_results=0,
+ current_page=1,
+ page_size=page_size,
+ query="",
+ folder=folder
+ )
+
+ # Calculate pagination
+ total_pages = (total_messages + page_size - 1) // page_size
+ if page > total_pages:
+ page = total_pages
+
+ # Get email IDs for current page
+ start_idx = (page - 1) * page_size
+ email_ids = self.imap_backend.get_email_ids(folder, limit=total_messages)
+ page_ids = email_ids[start_idx:start_idx + page_size]
+
+ # Fetch emails
+ emails = []
+ for email_id in page_ids:
+ try:
+ email_obj = self.imap_backend.fetch_email(email_id)
+ emails.append(email_obj)
+ except Exception as e:
+ # Log error but continue with other emails
+ import logging
+ logging.error(f"Failed to fetch email {email_id}: {str(e)}")
+
+ result = SearchResult(
+ emails=emails,
+ total_results=total_messages,
+ current_page=page,
+ page_size=page_size,
+ query="",
+ folder=folder
+ )
+
+ return result
+
+ except Exception as e:
+ raise EmailMCPError(f"Failed to get emails: {str(e)}")
+
+ def read_email(self, email_id: str) -> EmailMessage:
+ """Read specific email by ID"""
+ try:
+ email_obj = self.imap_backend.fetch_email(email_id)
+
+ # Mark as read
+ if self.imap_backend.mark_as_read(email_id):
+ email_obj.is_read = True
+ else:
+ logging.warning(f"Failed to mark email {email_id} as read")
+ # Continue anyway, as the email content was retrieved successfully
+
+ return email_obj
+
+ except Exception as e:
+ raise EmailMCPError(f"Failed to read email {email_id}: {str(e)}")
+
+ def search_emails(self, query: str, folder: Optional[str] = None,
+ page: int = 1, page_size: int = 20) -> SearchResult:
+ """Search emails with pagination"""
+ try:
+ # Validate query
+ valid, error = validate_search_query(query)
+ if not valid:
+ raise ValidationError(error)
+
+ # Validate parameters
+ page, page_size, warning = validate_page_params(page, page_size)
+
+ # Search emails
+ email_ids = self.imap_backend.search_emails(query, folder)
+ total_results = len(email_ids)
+
+ if total_results == 0:
+ return SearchResult(
+ emails=[],
+ total_results=0,
+ current_page=1,
+ page_size=page_size,
+ query=query,
+ folder=folder
+ )
+
+ # Calculate pagination
+ total_pages = (total_results + page_size - 1) // page_size
+ if page > total_pages:
+ page = total_pages
+
+ # Get page slice
+ start_idx = (page - 1) * page_size
+ page_ids = email_ids[start_idx:start_idx + page_size]
+
+ # Fetch emails
+ emails = []
+ for email_id in page_ids:
+ try:
+ email_obj = self.imap_backend.fetch_email(email_id)
+ emails.append(email_obj)
+ except Exception as e:
+ import logging
+ logging.error(f"Failed to fetch search result {email_id}: {str(e)}")
+
+ return SearchResult(
+ emails=emails,
+ total_results=total_results,
+ current_page=page,
+ page_size=page_size,
+ query=query,
+ folder=folder
+ )
+
+ except Exception as e:
+ raise EmailMCPError(f"Failed to search emails: {str(e)}")
+
+ def send_email(self, to: str, subject: str, body: str,
+ html_body: Optional[str] = None,
+ cc: Optional[str] = None,
+ bcc: Optional[str] = None,
+ attachments: Optional[List[str]] = None,
+ save_to_sent: bool = True) -> bool:
+ """Send email and optionally save to Sent folder"""
+ try:
+ # Send the email first
+ success, message_string = self.smtp_backend.send_email(
+ to=to,
+ subject=subject,
+ body=body,
+ html_body=html_body,
+ cc=cc,
+ bcc=bcc,
+ attachments=attachments
+ )
+
+ # If sending was successful and save_to_sent is True, save to Sent folder
+ # Skip if using PgSMTPBackend — it already saves to Sent internally
+ from emails_mcp.backends.pg_backend import PgSMTPBackend
+ is_pg = isinstance(self.smtp_backend, PgSMTPBackend)
+ if success and save_to_sent and message_string and not is_pg:
+ try:
+ # Try common sent folder names
+ sent_folders = ["Sent", "INBOX.Sent", "Sent Messages", "Sent Items"]
+ saved = False
+
+ for folder in sent_folders:
+ try:
+ self.imap_backend.append_message(folder, message_string)
+ saved = True
+ logging.info(f"Email saved to {folder} folder")
+ break
+ except Exception as e:
+ logging.debug(f"Failed to save to {folder}: {str(e)}")
+ continue
+
+ if not saved:
+ logging.warning("Could not save email to any Sent folder")
+
+ except Exception as e:
+ logging.error(f"Error saving email to Sent folder: {str(e)}")
+ # Don't fail the whole operation if saving to Sent fails
+
+ return success
+
+ except Exception as e:
+ raise EmailMCPError(f"Failed to send email: {str(e)}")
+
+ def reply_email(self, email_id: str, body: str,
+ html_body: Optional[str] = None,
+ cc: Optional[str] = None,
+ bcc: Optional[str] = None,
+ reply_all: bool = False) -> bool:
+ """Reply to email"""
+ try:
+ # Get original email
+ original_email = self.imap_backend.fetch_email(email_id)
+
+ # Prepare reply subject
+ original_subject = original_email.subject or ""
+ reply_subject = f"Re: {original_subject}" if not original_subject.startswith('Re:') else original_subject
+
+ # Determine recipients
+ reply_to = original_email.from_addr
+
+ if reply_all:
+ # Include original TO and CC recipients (excluding ourselves)
+ all_recipients = []
+ if original_email.to_addr:
+ all_recipients.extend([addr.strip() for addr in original_email.to_addr.split(',')])
+ if original_email.cc_addr:
+ all_recipients.extend([addr.strip() for addr in original_email.cc_addr.split(',')])
+
+ # Remove our own email (handle both "email" and "Name " formats)
+ our_email = self.config.email.lower()
+ all_recipients = [addr for addr in all_recipients
+ if our_email not in addr.lower()]
+ reply_cc = ','.join(all_recipients) if all_recipients else cc
+ else:
+ reply_cc = cc
+
+ # Prepare body with original message
+ original_body = original_email.body_text or ""
+ full_body = f"{body}\n\n--- Original Message ---\nFrom: {original_email.from_addr}\nDate: {original_email.date}\nSubject: {original_subject}\n\n{original_body}"
+
+ # Prepare HTML body if provided
+ full_html_body = None
+ if html_body:
+ original_html = original_email.body_html or original_body
+ full_html_body = f"{html_body}Original Message: From: {original_email.from_addr} Date: {original_email.date} Subject: {original_subject} {original_html}"
+
+ # Send reply
+ return self.send_email(
+ to=reply_to,
+ subject=reply_subject,
+ body=full_body,
+ html_body=full_html_body,
+ cc=reply_cc,
+ bcc=bcc
+ )
+
+ except Exception as e:
+ raise EmailMCPError(f"Failed to reply to email: {str(e)}")
+
+ def forward_email(self, email_id: str, to: str,
+ body: Optional[str] = None,
+ html_body: Optional[str] = None,
+ cc: Optional[str] = None,
+ bcc: Optional[str] = None) -> bool:
+ """Forward email with attachments"""
+ try:
+ # Get original email
+ original_email = self.imap_backend.fetch_email(email_id)
+
+ # Prepare forward subject
+ original_subject = original_email.subject or ""
+ forward_subject = f"Fwd: {original_subject}" if not original_subject.startswith('Fwd:') else original_subject
+
+ # Prepare body with forwarded content
+ original_body = original_email.body_text or ""
+ forward_body = f"{body or ''}\n\n--- Forwarded Message ---\nFrom: {original_email.from_addr}\nTo: {original_email.to_addr}\nDate: {original_email.date}\nSubject: {original_subject}\n\n{original_body}"
+
+ # Prepare HTML body if provided
+ forward_html_body = None
+ if html_body or original_email.body_html:
+ original_html = original_email.body_html or original_body
+ forward_html_body = f"{html_body or ''}Forwarded Message: From: {original_email.from_addr} To: {original_email.to_addr} Date: {original_email.date} Subject: {original_subject} {original_html}"
+
+ # Forward with attachments from the original email
+ return self._send_with_original_attachments(
+ to=to,
+ subject=forward_subject,
+ body=forward_body,
+ html_body=forward_html_body,
+ cc=cc,
+ bcc=bcc,
+ original_email=original_email
+ )
+
+ except Exception as e:
+ raise EmailMCPError(f"Failed to forward email: {str(e)}")
+
+ def _send_with_original_attachments(self, to: str, subject: str, body: str,
+ html_body: Optional[str] = None,
+ cc: Optional[str] = None,
+ bcc: Optional[str] = None,
+ original_email: EmailMessage = None) -> bool:
+ """Send email with attachments from original email"""
+ try:
+ if not original_email or not original_email.attachments:
+ # No attachments, use regular send_email
+ return self.send_email(
+ to=to,
+ subject=subject,
+ body=body,
+ html_body=html_body,
+ cc=cc,
+ bcc=bcc
+ )
+
+ # Extract attachment data from original email's raw message
+ import tempfile
+ import os
+ temp_files = []
+
+ try:
+ if original_email.raw_message:
+ for part in original_email.raw_message.walk():
+ if part.get_content_disposition() == 'attachment':
+ filename = part.get_filename()
+ if filename:
+ # Decode attachment data
+ attachment_data = part.get_payload(decode=True)
+ if attachment_data:
+ # Create temporary file with original filename
+ import tempfile
+ temp_dir = tempfile.mkdtemp()
+ temp_file_path = os.path.join(temp_dir, filename)
+ with open(temp_file_path, 'wb') as f:
+ f.write(attachment_data)
+ temp_files.append(temp_file_path)
+
+ # Send email with temporary attachment files
+ success = self.send_email(
+ to=to,
+ subject=subject,
+ body=body,
+ html_body=html_body,
+ cc=cc,
+ bcc=bcc,
+ attachments=temp_files
+ )
+
+ return success
+
+ finally:
+ # Clean up temporary files and directories
+ for temp_file_path in temp_files:
+ try:
+ os.unlink(temp_file_path)
+ # Also remove the temporary directory if it's empty
+ temp_dir = os.path.dirname(temp_file_path)
+ try:
+ os.rmdir(temp_dir)
+ except OSError:
+ pass # Directory not empty or already removed
+ except:
+ pass
+
+ except Exception as e:
+ raise EmailMCPError(f"Failed to send email with original attachments: {str(e)}")
+
+ def _check_email_exists(self, email_id: str) -> bool:
+ """Check if email exists in the database"""
+ try:
+ self.imap_backend.fetch_email(email_id)
+ return True
+ except Exception:
+ return False
+
+ def delete_email(self, email_id: str) -> bool:
+ """Delete email"""
+ try:
+ self.imap_backend.delete_email(email_id)
+ return True
+ except Exception as e:
+ raise EmailMCPError(f"Failed to delete email: {str(e)}")
+
+ def move_email(self, email_id: str, target_folder: str) -> bool:
+ """Move email to another folder"""
+ try:
+ self.imap_backend.move_email(email_id, target_folder)
+ return True
+ except Exception as e:
+ raise EmailMCPError(f"Failed to move email: {str(e)}")
+
+ def mark_emails(self, email_ids: List[str], status: str) -> int:
+ """Mark multiple emails with status (read/unread/important)"""
+ success_count = 0
+
+ for email_id in email_ids:
+ try:
+ success = False
+ if status == "read":
+ success = self.imap_backend.mark_as_read(email_id)
+ elif status == "unread":
+ success = self.imap_backend.mark_as_unread(email_id)
+ elif status == "important":
+ success = self.imap_backend.mark_as_important(email_id)
+ elif status == "not_important":
+ success = self.imap_backend.mark_as_not_important(email_id)
+
+ if success:
+ success_count += 1
+ else:
+ logging.warning(f"Failed to mark email {email_id} as {status}")
+ except Exception as e:
+ logging.error(f"Failed to mark email {email_id} as {status}: {str(e)}")
+
+ return success_count
+
+ def check_connection(self) -> Tuple[bool, bool]:
+ """Check IMAP and SMTP connections"""
+ imap_ok = False
+ smtp_ok = False
+
+ try:
+ self.imap_backend.ensure_connected()
+ imap_ok = True
+ except:
+ pass
+
+ try:
+ smtp_ok = self.smtp_backend.test_connection()
+ except:
+ pass
+
+ return imap_ok, smtp_ok
+
+ def cleanup(self):
+ """Cleanup connections"""
+ self.imap_backend.disconnect()
+ self.smtp_backend.disconnect()
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/services/folder_service.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/services/folder_service.py
new file mode 100644
index 00000000..70bbf62b
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/services/folder_service.py
@@ -0,0 +1,85 @@
+from typing import List
+import logging
+from ..models.email import EmailFolder, MailboxStats
+from ..utils.exceptions import EmailMCPError, FolderError
+from ..utils.validators import validate_folder_name
+
+class FolderService:
+ """Folder management service layer"""
+
+ def __init__(self, imap_backend):
+ self.imap_backend = imap_backend
+
+ def get_folders(self) -> List[EmailFolder]:
+ """Get list of all email folders"""
+ try:
+ return self.imap_backend.list_folders()
+ except Exception as e:
+ raise EmailMCPError(f"Failed to get folders: {str(e)}")
+
+ def create_folder(self, folder_name: str) -> bool:
+ """Create new email folder"""
+ # Validate folder name
+ valid, error = validate_folder_name(folder_name)
+ if not valid:
+ raise FolderError(error)
+
+ try:
+ return self.imap_backend.create_folder(folder_name)
+ except FolderError:
+ raise
+ except Exception as e:
+ raise FolderError(f"Failed to create folder: {str(e)}")
+
+ def delete_folder(self, folder_name: str) -> bool:
+ """Delete email folder"""
+ # Validate folder name
+ valid, error = validate_folder_name(folder_name)
+ if not valid:
+ raise FolderError(error)
+
+ # Prevent deletion of system folders
+ system_folders = ['INBOX', 'Sent', 'Drafts', 'Trash', 'Spam']
+ if folder_name.strip() in system_folders:
+ raise FolderError(f"Cannot delete system folder: {folder_name}")
+
+ try:
+ return self.imap_backend.delete_folder(folder_name)
+ except FolderError:
+ raise
+ except Exception as e:
+ raise FolderError(f"Failed to delete folder: {str(e)}")
+
+ def get_folder_stats(self, folder_name: str) -> MailboxStats:
+ """Get statistics for specific folder"""
+ try:
+ folder_name = folder_name.strip()
+ total_messages, unread_messages = self.imap_backend.select_folder(folder_name)
+
+ return MailboxStats(
+ folder_name=folder_name,
+ total_messages=total_messages,
+ unread_messages=unread_messages
+ )
+
+ except Exception as e:
+ raise EmailMCPError(f"Failed to get folder stats: {str(e)}")
+
+ def get_unread_count(self, folder_name: str = None) -> int:
+ """Get unread message count for folder or all folders"""
+ try:
+ if folder_name:
+ folder_name = folder_name.strip()
+ _, unread_count = self.imap_backend.select_folder(folder_name)
+ return unread_count
+ else:
+ # Get unread count for all folders
+ folders = self.get_folders()
+ total_unread = 0
+ for folder in folders:
+ if folder.can_select:
+ total_unread += folder.unread_messages
+ return total_unread
+
+ except Exception as e:
+ raise EmailMCPError(f"Failed to get unread count: {str(e)}")
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/services/search_service.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/services/search_service.py
new file mode 100644
index 00000000..f1fd8a07
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/services/search_service.py
@@ -0,0 +1,84 @@
+import logging
+from typing import List, Optional
+from ..models.email import EmailMessage
+from ..utils.exceptions import EmailMCPError
+
+
+class SearchService:
+ """Email search service layer"""
+
+ def __init__(self, imap_backend):
+ self.imap_backend = imap_backend
+
+ def search_emails_by_query(self, query: str, folder: Optional[str] = None) -> List[str]:
+ """Search emails and return email IDs"""
+ try:
+ # If no folder specified, use INBOX as default
+ if not folder:
+ folder = 'INBOX'
+ return self.imap_backend.search_emails(query, folder)
+ except Exception as e:
+ raise EmailMCPError(f"Failed to search emails: {str(e)}")
+
+ def search_by_sender(self, sender: str, folder: Optional[str] = None) -> List[str]:
+ """Search emails by sender using PG full-text search on from_addr"""
+ try:
+ if not folder:
+ folder = 'INBOX'
+ # Delegate to the backend's search which searches subject+body.
+ # For sender-specific search we query the DB directly.
+ self.imap_backend.ensure_connected()
+ folder_id = self.imap_backend._get_or_create_folder(folder)
+ with self.imap_backend.connection.cursor() as cur:
+ cur.execute(
+ "SELECT id FROM email.messages "
+ "WHERE folder_id = %s AND from_addr ILIKE %s "
+ "ORDER BY date DESC, id DESC",
+ (folder_id, f"%{sender}%"),
+ )
+ rows = cur.fetchall()
+ return [str(r[0]) for r in rows]
+ except Exception as e:
+ raise EmailMCPError(f"Failed to search by sender: {str(e)}")
+
+ def search_by_subject(self, subject: str, folder: Optional[str] = None) -> List[str]:
+ """Search emails by subject using PG ILIKE"""
+ try:
+ if not folder:
+ folder = 'INBOX'
+ self.imap_backend.ensure_connected()
+ folder_id = self.imap_backend._get_or_create_folder(folder)
+ with self.imap_backend.connection.cursor() as cur:
+ cur.execute(
+ "SELECT id FROM email.messages "
+ "WHERE folder_id = %s AND subject ILIKE %s "
+ "ORDER BY date DESC, id DESC",
+ (folder_id, f"%{subject}%"),
+ )
+ rows = cur.fetchall()
+ return [str(r[0]) for r in rows]
+ except Exception as e:
+ raise EmailMCPError(f"Failed to search by subject: {str(e)}")
+
+ def search_by_date_range(self, since_date: str, before_date: Optional[str] = None,
+ folder: Optional[str] = None) -> List[str]:
+ """Search emails by date range (YYYY-MM-DD format)"""
+ try:
+ if not folder:
+ folder = 'INBOX'
+ self.imap_backend.ensure_connected()
+ folder_id = self.imap_backend._get_or_create_folder(folder)
+
+ sql = "SELECT id FROM email.messages WHERE folder_id = %s AND date >= %s::date"
+ params: list = [folder_id, since_date]
+ if before_date:
+ sql += " AND date < %s::date"
+ params.append(before_date)
+ sql += " ORDER BY date DESC, id DESC"
+
+ with self.imap_backend.connection.cursor() as cur:
+ cur.execute(sql, params)
+ rows = cur.fetchall()
+ return [str(r[0]) for r in rows]
+ except Exception as e:
+ raise EmailMCPError(f"Failed to search by date: {str(e)}")
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/tools/__init__.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/tools/__init__.py
new file mode 100644
index 00000000..64456a07
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/tools/__init__.py
@@ -0,0 +1,5 @@
+from .email_tools import register_email_tools
+from .folder_tools import register_folder_tools
+from .management_tools import register_management_tools
+
+__all__ = ['register_email_tools', 'register_folder_tools', 'register_management_tools']
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/tools/email_tools.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/tools/email_tools.py
new file mode 100644
index 00000000..b162bf83
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/tools/email_tools.py
@@ -0,0 +1,382 @@
+import logging
+from typing import List, Optional
+from mcp.server.fastmcp import FastMCP
+from ..services.email_service import EmailService
+from ..utils.email_parser import format_email_summary
+
+
+def register_email_tools(mcp: FastMCP, email_service: EmailService):
+ """Register email-related MCP tools"""
+
+ @mcp.tool()
+ async def get_emails(folder: str = "INBOX", page: int = 1, page_size: int = 20) -> str:
+ """Get paginated list of emails from specified folder
+
+ Args:
+ folder: Email folder name (default: INBOX)
+ page: Page number starting from 1 (default: 1)
+ page_size: Number of emails per page (default: 20)
+ """
+ try:
+ result = email_service.get_emails(folder, page, page_size)
+
+ if not result.emails:
+ return f"Folder '{folder}' is empty or page {page} is out of range"
+
+ output = f"Folder: {folder}\n"
+ output += f"Page: {result.current_page}/{result.total_pages}\n"
+ output += f"Total emails: {result.total_results}\n\n"
+
+ for i, email in enumerate(result.emails, 1):
+ output += f"{(result.current_page-1)*result.page_size + i}. "
+ output += f"ID: {email.email_id}\n"
+ output += f" Subject: {email.subject}\n"
+ output += f" From: {email.from_addr}\n"
+ output += f" Date: {email.date}\n"
+ if email.attachments:
+ output += f" Attachments: {len(email.attachments)} files\n"
+ output += "\n"
+
+ return output
+
+ except Exception as e:
+ return f"Error getting emails: {str(e)}"
+
+ @mcp.tool()
+ async def read_email(email_id: str) -> str:
+ """Read full content of a specific email
+
+ Args:
+ email_id: Email ID to read
+ """
+ try:
+ email = email_service.read_email(email_id)
+
+ output = f"Email ID: {email.email_id}\n"
+ output += f"Subject: {email.subject}\n"
+ output += f"From: {email.from_addr}\n"
+ output += f"To: {email.to_addr}\n"
+
+ if email.cc_addr:
+ output += f"CC: {email.cc_addr}\n"
+
+ output += f"Date: {email.date}\n"
+ output += f"Message-ID: {email.message_id}\n\n"
+
+ if email.body_text:
+ output += "Text Content:\n"
+ output += f"{email.body_text}\n\n"
+
+ if email.body_html:
+ output += "HTML Content:\n"
+ output += f"{email.body_html}\n\n"
+
+ if email.attachments:
+ output += "Attachments:\n"
+ for i, att in enumerate(email.attachments, 1):
+ output += f"{i}. {att.filename} ({att.content_type}, {att.size} bytes)\n"
+
+ return output
+
+ except Exception as e:
+ return f"Error reading email: {str(e)}"
+
+ @mcp.tool()
+ async def search_emails(query: str, folder: str = "INBOX", page: int = 1, page_size: int = 20) -> str:
+ """Search emails with query string (sorted by date descending)
+
+ Args:
+ query: Search query (subject, from, body content)
+ folder: Folder to search in (default: INBOX)
+ page: Page number starting from 1 (default: 1)
+ page_size: Number of results per page (default: 20)
+ """
+ try:
+ result = email_service.search_emails(query, folder, page, page_size)
+
+ if not result.emails:
+ return f"No emails found matching query: {query}"
+
+ output = f"Search query: {query}\n"
+ output += f"Folder: {result.folder or 'current'}\n"
+ output += f"Page: {result.current_page}/{result.total_pages}\n"
+ output += f"Total results: {result.total_results}\n\n"
+
+ for i, email in enumerate(result.emails, 1):
+ output += f"{(result.current_page-1)*result.page_size + i}. "
+ output += f"ID: {email.email_id}\n"
+ output += f" Subject: {email.subject}\n"
+ output += f" From: {email.from_addr}\n"
+ output += f" Date: {email.date}\n\n"
+
+ return output
+
+ except Exception as e:
+ return f"Error searching emails: {str(e)}"
+
+ @mcp.tool()
+ async def send_email(to: str, subject: str, body: str, html_body: str = None,
+ cc: str = None, bcc: str = None, attachments: List[str] = None) -> str:
+ """Send an email with optional HTML body, CC, BCC, and attachments
+
+ Args:
+ to: Recipient email address(es), comma-separated
+ subject: Email subject
+ body: Plain text body
+ html_body: HTML body content (optional)
+ cc: CC recipients, comma-separated (optional)
+ bcc: BCC recipients, comma-separated (optional)
+ attachments: List of file paths to attach (optional)
+ """
+ try:
+ success = email_service.send_email(
+ to=to,
+ subject=subject,
+ body=body,
+ html_body=html_body,
+ cc=cc,
+ bcc=bcc,
+ attachments=attachments
+ )
+
+ if success:
+ attachment_info = f" with {len(attachments)} attachments" if attachments else ""
+ return f"Email sent successfully to {to}{attachment_info}"
+ else:
+ return "Email sending failed"
+
+ except Exception as e:
+ return f"Error sending email: {str(e)}"
+
+ @mcp.tool()
+ async def reply_email(email_id: str, body: str, html_body: str = None,
+ cc: str = None, bcc: str = None, reply_all: bool = False) -> str:
+ """Reply to an email
+
+ Args:
+ email_id: ID of email to reply to
+ body: Reply message body (plain text)
+ html_body: Reply message body (HTML, optional)
+ cc: Additional CC recipients (optional)
+ bcc: BCC recipients (optional)
+ reply_all: Whether to reply to all recipients (default: False)
+ """
+ try:
+ success = email_service.reply_email(
+ email_id=email_id,
+ body=body,
+ html_body=html_body,
+ cc=cc,
+ bcc=bcc,
+ reply_all=reply_all
+ )
+
+ if success:
+ reply_type = "all recipients" if reply_all else "sender"
+ return f"Reply sent successfully to {reply_type}"
+ else:
+ return "Reply sending failed"
+
+ except Exception as e:
+ return f"Error replying to email: {str(e)}"
+
+ @mcp.tool()
+ async def forward_email(email_id: str, to: str, body: str = None, html_body: str = None,
+ cc: str = None, bcc: str = None) -> str:
+ """Forward an email to other recipients
+
+ Args:
+ email_id: ID of email to forward
+ to: Recipients to forward to
+ body: Additional message body (optional)
+ html_body: Additional HTML message body (optional)
+ cc: CC recipients (optional)
+ bcc: BCC recipients (optional)
+ """
+ try:
+ success = email_service.forward_email(
+ email_id=email_id,
+ to=to,
+ body=body,
+ html_body=html_body,
+ cc=cc,
+ bcc=bcc
+ )
+
+ if success:
+ return f"Email forwarded successfully to {to}"
+ else:
+ return "Email forwarding failed"
+
+ except Exception as e:
+ return f"Error forwarding email: {str(e)}"
+
+ @mcp.tool()
+ async def delete_email(email_id: str) -> str:
+ """Delete an email
+
+ Args:
+ email_id: Email ID to delete
+ """
+ try:
+ success = email_service.delete_email(email_id)
+
+ if success:
+ return f"Email {email_id} deleted successfully"
+ else:
+ return "Email deletion failed"
+
+ except Exception as e:
+ return f"Error deleting email: {str(e)}"
+
+ @mcp.tool()
+ async def move_email(email_id: str, target_folder: str) -> str:
+ """Move email to another folder
+
+ Args:
+ email_id: Email ID to move
+ target_folder: Target folder name
+ """
+ try:
+ success = email_service.move_email(email_id, target_folder)
+
+ if success:
+ return f"Email {email_id} moved to {target_folder} successfully"
+ else:
+ return "Email move failed"
+
+ except Exception as e:
+ return f"Error moving email: {str(e)}"
+
+ @mcp.tool()
+ async def mark_emails(email_ids: List[str], status: str) -> str:
+ """Mark multiple emails with status (read/unread/important/not_important)
+
+ Args:
+ email_ids: List of email IDs to mark
+ status: Status to set (read, unread, important, not_important)
+ """
+ try:
+ if status not in ['read', 'unread', 'important', 'not_important']:
+ return "Error: Status must be 'read', 'unread', 'important', or 'not_important'"
+
+ success_count = email_service.mark_emails(email_ids, status)
+ total_count = len(email_ids)
+
+ return f"Successfully marked {success_count}/{total_count} emails as {status}"
+
+ except Exception as e:
+ return f"Error marking emails: {str(e)}"
+
+ @mcp.tool()
+ async def move_emails(email_ids: List[str], target_folder: str) -> str:
+ """Move multiple emails to another folder with improved ID synchronization
+
+ Args:
+ email_ids: List of email IDs to move
+ target_folder: Target folder name
+ """
+ try:
+ success_count = 0
+ failed_count = 0
+ failed_ids = []
+
+ # Sort email IDs in descending order to avoid ID renumbering issues
+ # When emails are deleted/moved, higher IDs remain stable
+ sorted_ids = sorted(email_ids, key=lambda x: int(x) if x.isdigit() else 0, reverse=True)
+
+ for email_id in sorted_ids:
+ try:
+ # Verify email exists before attempting move
+ email_exists = email_service._check_email_exists(email_id)
+ if not email_exists:
+ logging.warning(f"Email {email_id} no longer exists, skipping")
+ failed_count += 1
+ failed_ids.append(email_id)
+ continue
+
+ success = email_service.move_email(email_id, target_folder)
+ if success:
+ success_count += 1
+ logging.info(f"Successfully moved email {email_id}")
+ else:
+ failed_count += 1
+ failed_ids.append(email_id)
+ logging.warning(f"Failed to move email {email_id}")
+
+ except Exception as e:
+ failed_count += 1
+ failed_ids.append(email_id)
+ logging.error(f"Failed to move email {email_id}: {str(e)}")
+
+ total_count = len(email_ids)
+ result_msg = f"Successfully moved {success_count}/{total_count} emails to {target_folder}"
+
+ if failed_count > 0:
+ result_msg += f" ({failed_count} failed"
+ if failed_ids:
+ result_msg += f": {', '.join(failed_ids[:5])}" # Show first 5 failed IDs
+ if len(failed_ids) > 5:
+ result_msg += f" and {len(failed_ids)-5} more"
+ result_msg += ")"
+
+ return result_msg
+
+ except Exception as e:
+ return f"Error moving emails: {str(e)}"
+
+ @mcp.tool()
+ async def delete_emails(email_ids: List[str]) -> str:
+ """Delete multiple emails with improved ID synchronization
+
+ Args:
+ email_ids: List of email IDs to delete
+ """
+ try:
+ success_count = 0
+ failed_count = 0
+ failed_ids = []
+
+ # Sort email IDs in descending order to avoid ID renumbering issues
+ # When emails are deleted, higher IDs remain stable
+ sorted_ids = sorted(email_ids, key=lambda x: int(x) if x.isdigit() else 0, reverse=True)
+
+ for email_id in sorted_ids:
+ try:
+ # Verify email exists before attempting deletion
+ email_exists = email_service._check_email_exists(email_id)
+ if not email_exists:
+ logging.warning(f"Email {email_id} no longer exists, skipping")
+ failed_count += 1
+ failed_ids.append(email_id)
+ continue
+
+ success = email_service.delete_email(email_id)
+ if success:
+ success_count += 1
+ logging.info(f"Successfully deleted email {email_id}")
+ else:
+ failed_count += 1
+ failed_ids.append(email_id)
+ logging.warning(f"Failed to delete email {email_id}")
+
+ except Exception as e:
+ failed_count += 1
+ failed_ids.append(email_id)
+ logging.error(f"Failed to delete email {email_id}: {str(e)}")
+
+ total_count = len(email_ids)
+ result_msg = f"Successfully deleted {success_count}/{total_count} emails"
+
+ if failed_count > 0:
+ result_msg += f" ({failed_count} failed"
+ if failed_ids:
+ result_msg += f": {', '.join(failed_ids[:5])}" # Show first 5 failed IDs
+ if len(failed_ids) > 5:
+ result_msg += f" and {len(failed_ids)-5} more"
+ result_msg += ")"
+
+ return result_msg
+
+ except Exception as e:
+ return f"Error deleting emails: {str(e)}"
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/tools/folder_tools.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/tools/folder_tools.py
new file mode 100644
index 00000000..b9149433
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/tools/folder_tools.py
@@ -0,0 +1,117 @@
+from mcp.server.fastmcp import FastMCP
+from ..services.folder_service import FolderService
+
+
+def register_folder_tools(mcp: FastMCP, folder_service: FolderService):
+ """Register folder-related MCP tools"""
+
+ @mcp.tool()
+ async def get_folders() -> str:
+ """Get list of available email folders"""
+ try:
+ folders = folder_service.get_folders()
+
+ if not folders:
+ return "No folders found"
+
+ output = "Available folders:\n"
+ for i, folder in enumerate(folders, 1):
+ output += f"{i}. {folder.name}"
+ if folder.can_select:
+ output += f" ({folder.total_messages} total, {folder.unread_messages} unread)"
+ else:
+ output += " (cannot select)"
+ output += "\n"
+
+ return output
+
+ except Exception as e:
+ return f"Error getting folders: {str(e)}"
+
+ @mcp.tool()
+ async def create_folder(folder_name: str) -> str:
+ """Create new email folder
+
+ Args:
+ folder_name: Name of folder to create
+ """
+ try:
+ success = folder_service.create_folder(folder_name)
+
+ if success:
+ return f"Folder '{folder_name}' created successfully"
+ else:
+ return f"Failed to create folder '{folder_name}'"
+
+ except Exception as e:
+ return f"Error creating folder: {str(e)}"
+
+ @mcp.tool()
+ async def delete_folder(folder_name: str) -> str:
+ """Delete email folder
+
+ Args:
+ folder_name: Name of folder to delete
+ """
+ try:
+ success = folder_service.delete_folder(folder_name)
+
+ if success:
+ return f"Folder '{folder_name}' deleted successfully"
+ else:
+ return f"Failed to delete folder '{folder_name}'"
+
+ except Exception as e:
+ return f"Error deleting folder: {str(e)}"
+
+ @mcp.tool()
+ async def get_mailbox_stats(folder_name: str = None) -> str:
+ """Get mailbox statistics
+
+ Args:
+ folder_name: Specific folder name (optional, defaults to all folders)
+ """
+ try:
+ if folder_name:
+ stats = folder_service.get_folder_stats(folder_name)
+ output = f"Folder Statistics for '{stats.folder_name}':\n"
+ output += f"Total messages: {stats.total_messages}\n"
+ output += f"Unread messages: {stats.unread_messages}\n"
+ if stats.total_size_mb:
+ output += f"Total size: {stats.total_size_mb:.2f} MB\n"
+ else:
+ folders = folder_service.get_folders()
+ output = "Mailbox Statistics:\n"
+ total_messages = 0
+ total_unread = 0
+
+ for folder in folders:
+ if folder.can_select:
+ output += f" {folder.name}: {folder.total_messages} total, {folder.unread_messages} unread\n"
+ total_messages += folder.total_messages
+ total_unread += folder.unread_messages
+
+ output += f"\nOverall Total: {total_messages} messages, {total_unread} unread\n"
+
+ return output
+
+ except Exception as e:
+ return f"Error getting mailbox stats: {str(e)}"
+
+ @mcp.tool()
+ async def get_unread_count(folder_name: str = None) -> str:
+ """Get unread message count
+
+ Args:
+ folder_name: Specific folder name (optional, defaults to all folders)
+ """
+ try:
+ unread_count = folder_service.get_unread_count(folder_name)
+
+ if folder_name:
+ return f"Unread messages in '{folder_name}': {unread_count}"
+ else:
+ return f"Total unread messages: {unread_count}"
+
+ except Exception as e:
+ return f"Error getting unread count: {str(e)}"
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/tools/management_tools.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/tools/management_tools.py
new file mode 100644
index 00000000..492b38ad
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/tools/management_tools.py
@@ -0,0 +1,482 @@
+from typing import List
+from mcp.server.fastmcp import FastMCP
+from ..services.draft_service import DraftService
+
+
+def _reconstruct_email_message(email_obj) -> str:
+ """Reconstruct email message from EmailMessage object"""
+ from email.mime.multipart import MIMEMultipart
+ from email.mime.text import MIMEText
+ from email.utils import formatdate
+
+ # Create message
+ if email_obj.body_html:
+ msg = MIMEMultipart('alternative')
+ msg.attach(MIMEText(email_obj.body_text or '', 'plain', 'utf-8'))
+ msg.attach(MIMEText(email_obj.body_html, 'html', 'utf-8'))
+ else:
+ msg = MIMEText(email_obj.body_text or '', 'plain', 'utf-8')
+
+ # Set headers
+ msg['Subject'] = email_obj.subject or ''
+ msg['From'] = email_obj.from_addr or ''
+ msg['To'] = email_obj.to_addr or ''
+ if email_obj.cc_addr:
+ msg['Cc'] = email_obj.cc_addr
+ if email_obj.bcc_addr:
+ msg['Bcc'] = email_obj.bcc_addr
+ if email_obj.message_id:
+ msg['Message-ID'] = email_obj.message_id
+ if email_obj.date:
+ msg['Date'] = email_obj.date
+ else:
+ msg['Date'] = formatdate(localtime=True)
+
+ return msg.as_string()
+
+def register_management_tools(mcp: FastMCP, draft_service: DraftService, email_service):
+ """Register management and utility MCP tools"""
+
+ @mcp.tool()
+ async def check_connection() -> str:
+ """Check email server connection status"""
+ try:
+ imap_ok, smtp_ok = email_service.check_connection()
+
+ status = "Connection Status:\n"
+ status += f"IMAP: {'✓ Connected' if imap_ok else '✗ Failed'}\n"
+ status += f"SMTP: {'✓ Connected' if smtp_ok else '✗ Failed'}\n"
+
+ if imap_ok and smtp_ok:
+ status += "\nAll connections are working properly"
+ elif imap_ok:
+ status += "\nWarning: SMTP connection failed - cannot send emails"
+ elif smtp_ok:
+ status += "\nWarning: IMAP connection failed - cannot receive emails"
+ else:
+ status += "\nError: Both connections failed - check configuration"
+
+ return status
+
+ except Exception as e:
+ return f"Error checking connection: {str(e)}"
+
+ @mcp.tool()
+ async def get_email_headers(email_id: str) -> str:
+ """Get complete email headers for technical analysis
+
+ Args:
+ email_id: Email ID to get headers for
+ """
+ try:
+ email = email_service.imap_backend.fetch_email(email_id)
+
+ if not email.raw_message:
+ return f"No raw message data available for email {email_id}"
+
+ output = f"Email Headers for ID: {email_id}\n"
+ output += "=" * 50 + "\n"
+
+ # Get all headers from raw message
+ for header, value in email.raw_message.items():
+ output += f"{header}: {value}\n"
+
+ return output
+
+ except Exception as e:
+ return f"Error getting email headers: {str(e)}"
+
+ @mcp.tool()
+ async def save_draft(subject: str, body: str, html_body: str = None,
+ to: str = None, cc: str = None, bcc: str = None) -> str:
+ """Save email draft
+
+ Args:
+ subject: Email subject
+ body: Plain text body
+ html_body: HTML body content (optional)
+ to: Recipient email address(es) (optional)
+ cc: CC recipients (optional)
+ bcc: BCC recipients (optional)
+ """
+ try:
+ draft_id = draft_service.save_draft(
+ subject=subject,
+ body=body,
+ html_body=html_body,
+ to=to,
+ cc=cc,
+ bcc=bcc
+ )
+
+ return f"Draft saved successfully with ID: {draft_id}"
+
+ except Exception as e:
+ return f"Error saving draft: {str(e)}"
+
+ @mcp.tool()
+ async def get_drafts(page: int = 1, page_size: int = 20) -> str:
+ """Get list of saved drafts
+
+ Args:
+ page: Page number starting from 1 (default: 1)
+ page_size: Number of drafts per page (default: 20)
+ """
+ try:
+ result = draft_service.get_drafts(page, page_size)
+
+ if result['total_drafts'] == 0:
+ return "No drafts found"
+
+ output = f"Drafts (Page {result['current_page']}/{result['total_pages']}):\n"
+ output += f"Total drafts: {result['total_drafts']}\n\n"
+
+ for i, draft in enumerate(result['drafts'], 1):
+ output += f"{(result['current_page']-1)*result['page_size'] + i}. "
+ output += f"ID: {draft['draft_id']}\n"
+ output += f" Subject: {draft['subject']}\n"
+ output += f" To: {draft.get('to', 'Not set')}\n"
+ output += f" Updated: {draft['updated_at']}\n\n"
+
+ return output
+
+ except Exception as e:
+ return f"Error getting drafts: {str(e)}"
+
+ @mcp.tool()
+ async def update_draft(draft_id: str, subject: str = None, body: str = None,
+ html_body: str = None, to: str = None, cc: str = None, bcc: str = None) -> str:
+ """Update existing draft
+
+ Args:
+ draft_id: Draft ID to update
+ subject: Email subject (optional)
+ body: Plain text body (optional)
+ html_body: HTML body content (optional)
+ to: Recipient email address(es) (optional)
+ cc: CC recipients (optional)
+ bcc: BCC recipients (optional)
+ """
+ try:
+ success = draft_service.update_draft(
+ draft_id=draft_id,
+ subject=subject,
+ body=body,
+ html_body=html_body,
+ to=to,
+ cc=cc,
+ bcc=bcc
+ )
+
+ if success:
+ return f"Draft {draft_id} updated successfully"
+ else:
+ return f"Failed to update draft {draft_id}"
+
+ except Exception as e:
+ return f"Error updating draft: {str(e)}"
+
+ @mcp.tool()
+ async def delete_draft(draft_id: str) -> str:
+ """Delete draft
+
+ Args:
+ draft_id: Draft ID to delete
+ """
+ try:
+ success = draft_service.delete_draft(draft_id)
+
+ if success:
+ return f"Draft {draft_id} deleted successfully"
+ else:
+ return f"Failed to delete draft {draft_id}"
+
+ except Exception as e:
+ return f"Error deleting draft: {str(e)}"
+
+ @mcp.tool()
+ async def export_emails(folder: str = None, export_path: str = "emails_export.json", max_emails: int = None, export_all_folders: bool = False) -> str:
+ """Export emails to file for backup
+
+ Args:
+ folder: Specific folder to export (mutually exclusive with export_all_folders)
+ export_path: Path where to save the export file
+ max_emails: Maximum number of emails to export (optional, exports all if not specified)
+ export_all_folders: Export from all folders instead of just one (default: False)
+ """
+ try:
+ from ..backends.file_backend import FileBackend
+ from ..config import config_manager
+ from ..services.folder_service import FolderService
+
+ # Initialize services
+ folder_service = FolderService(email_service.imap_backend)
+
+ # Determine which folders to export from
+ if export_all_folders and folder:
+ return "Error: Cannot specify both 'folder' and 'export_all_folders=True'"
+
+ folders_to_export = []
+ if export_all_folders:
+ # Get all selectable folders
+ all_folders = folder_service.get_folders()
+ folders_to_export = [f.name for f in all_folders if f.can_select]
+ print(f"Exporting from all folders: {folders_to_export}")
+ else:
+ # Export from single folder
+ target_folder = folder or "INBOX"
+ folders_to_export = [target_folder]
+
+ # Export from all specified folders
+ all_emails = []
+ folder_stats = {}
+
+ for folder_name in folders_to_export:
+ print(f"Processing folder: {folder_name}")
+ folder_emails = []
+ page = 1
+ page_size = 100 # Process in smaller batches
+
+ while True:
+ try:
+ result = email_service.get_emails(folder_name, page=page, page_size=page_size)
+
+ if not result.emails:
+ break
+
+ folder_emails.extend(result.emails)
+
+ # Check if we've reached the global maximum
+ if max_emails and len(all_emails) + len(folder_emails) >= max_emails:
+ remaining = max_emails - len(all_emails)
+ folder_emails = folder_emails[:remaining]
+ break
+
+ # Check if we've got all emails from this folder
+ if page * page_size >= result.total_results:
+ break
+
+ page += 1
+
+ except Exception as e:
+ print(f"Error reading from folder {folder_name}, page {page}: {str(e)}")
+ break
+
+ # Add folder emails to total
+ all_emails.extend(folder_emails)
+ folder_stats[folder_name] = len(folder_emails)
+
+ # Check global limit
+ if max_emails and len(all_emails) >= max_emails:
+ break
+
+ if not all_emails:
+ folders_desc = "all folders" if export_all_folders else folders_to_export[0]
+ return f"No emails found to export from {folders_desc}"
+
+ # Validate export path
+ workspace_config = config_manager.workspace_config
+ file_backend = FileBackend(
+ email_export_path=workspace_config.email_export_path if workspace_config else None,
+ attachment_download_path=workspace_config.attachment_download_path if workspace_config else None
+ )
+
+ export_name = "all_folders_export" if export_all_folders else f"{folders_to_export[0]}_export"
+ exported_file = file_backend.export_emails(all_emails, export_name, 'json')
+
+ # Build result message
+ result_msg = f"Successfully exported {len(all_emails)} emails to {exported_file}\n"
+
+ if export_all_folders:
+ result_msg += "Export breakdown by folder:\n"
+ for folder_name, count in folder_stats.items():
+ result_msg += f" - {folder_name}: {count} emails\n"
+
+ return result_msg.rstrip()
+
+ except Exception as e:
+ return f"Error exporting emails: {str(e)}"
+
+ @mcp.tool()
+ async def import_emails(import_path: str, target_folder: str = None, preserve_folders: bool = True) -> str:
+ """Import emails from backup file to IMAP server
+
+ Args:
+ import_path: Path to import file (.json or .eml) or a directory
+ target_folder: Target folder for imported emails (if preserve_folders=False)
+ preserve_folders: Whether to preserve original folder structure (default: True)
+ """
+ try:
+ from ..backends.file_backend import FileBackend
+ from ..config import config_manager
+
+ # Validate import path
+ workspace_config = config_manager.workspace_config
+ file_backend = FileBackend(
+ email_export_path=workspace_config.email_export_path if workspace_config else None,
+ attachment_download_path=workspace_config.attachment_download_path if workspace_config else None
+ )
+
+ imported_emails = file_backend.import_emails(import_path)
+
+ if not imported_emails:
+ return f"No emails found in import file {import_path}"
+
+ # 按email_id从大到小排序,确保导入时保持原始顺序
+ # 因为get_emails返回的是newest first,所以ID越大的邮件越新
+ # 倒序导入可以保持原来的头部(最老)和尾部(最新)顺序
+ try:
+ imported_emails.sort(key=lambda x: int(x.email_id) if x.email_id.isdigit() else 0, reverse=False)
+ print(f"Sorted {len(imported_emails)} emails by ID for proper import order")
+ except Exception as e:
+ print(f"Warning: Could not sort emails by ID: {str(e)}, importing in original order")
+
+ # Import emails to IMAP server
+ success_count = 0
+ failed_count = 0
+ failed_reasons = []
+ folder_stats = {}
+
+ for email_obj in imported_emails:
+ try:
+ # Determine target folder
+ if preserve_folders and email_obj.folder:
+ import_folder = email_obj.folder
+ else:
+ import_folder = target_folder or "INBOX"
+
+ # Ensure target folder exists and is accessible
+ folder_created = False
+ try:
+ email_service.imap_backend.select_folder(import_folder)
+ except Exception as e:
+ # If folder doesn't exist, try to create it
+ if preserve_folders and email_obj.folder and email_obj.folder not in ["INBOX", "SENT", "DRAFTS", "TRASH"]:
+ try:
+ from ..services.folder_service import FolderService
+ folder_service = FolderService(email_service.imap_backend)
+
+ # Create folder and all necessary parent folders
+ success = folder_service.create_folder(import_folder)
+ if success:
+ folder_created = True
+ print(f"Created folder: {import_folder}")
+ # Re-select the newly created folder
+ email_service.imap_backend.select_folder(import_folder)
+ else:
+ raise Exception("Failed to create folder")
+
+ except Exception as create_error:
+ # If can't create custom folder, fall back to INBOX
+ print(f"Warning: Cannot create folder '{import_folder}': {str(create_error)}")
+ print(f"Importing email {email_obj.email_id} to INBOX instead")
+ import_folder = "INBOX"
+ email_service.imap_backend.select_folder(import_folder)
+ else:
+ # For system folders or when preserve_folders=False, fail if can't access
+ failed_count += 1
+ failed_reasons.append(f"Email {email_obj.email_id}: Cannot access folder '{import_folder}': {str(e)}")
+ continue
+
+ # Convert EmailMessage back to raw email format if needed
+ if email_obj.raw_message:
+ # Use existing raw message
+ message_string = email_obj.raw_message.as_string()
+ else:
+ # Reconstruct email from EmailMessage data
+ message_string = _reconstruct_email_message(email_obj)
+
+ # Import to IMAP server using APPEND command
+ success = email_service.imap_backend.append_message(
+ import_folder,
+ message_string,
+ flags='\\Seen' if email_obj.is_read else ''
+ )
+
+ if success:
+ success_count += 1
+ # Track folder statistics
+ if import_folder not in folder_stats:
+ folder_stats[import_folder] = 0
+ folder_stats[import_folder] += 1
+ else:
+ failed_count += 1
+ failed_reasons.append(f"Email {email_obj.email_id}: APPEND to '{import_folder}' failed")
+
+ except Exception as e:
+ failed_count += 1
+ failed_reasons.append(f"Email {email_obj.email_id}: {str(e)}")
+
+ # Build result message
+ result_msg = f"Successfully imported {success_count}/{len(imported_emails)} emails"
+
+ if preserve_folders and len(folder_stats) > 1:
+ result_msg += "\n\nImport breakdown by folder:"
+ for folder, count in folder_stats.items():
+ result_msg += f"\n - {folder}: {count} emails"
+ elif folder_stats:
+ folder_name = list(folder_stats.keys())[0]
+ result_msg += f" to {folder_name}"
+
+ if failed_count > 0:
+ result_msg += f"\n\n{failed_count} emails failed to import:"
+ for reason in failed_reasons[:5]: # Show first 5 failures
+ result_msg += f"\n - {reason}"
+ if len(failed_reasons) > 5:
+ result_msg += f"\n ... and {len(failed_reasons)-5} more"
+
+ return result_msg
+
+ except Exception as e:
+ return f"Error importing emails: {str(e)}"
+
+ @mcp.tool()
+ async def download_attachment(email_id: str, attachment_filename: str) -> str:
+ """Download email attachment to configured download path
+
+ Args:
+ email_id: Email ID containing the attachment
+ attachment_filename: Name of attachment to download
+ """
+ try:
+ email = email_service.imap_backend.fetch_email(email_id)
+
+ # Find the attachment
+ target_attachment = None
+ for attachment in email.attachments:
+ if attachment.filename == attachment_filename:
+ target_attachment = attachment
+ break
+
+ if not target_attachment:
+ return f"Attachment '{attachment_filename}' not found in email {email_id}"
+
+ # Extract attachment data from raw message
+ if not email.raw_message:
+ return f"No raw message data available for email {email_id}"
+
+ attachment_data = None
+ for part in email.raw_message.walk():
+ if part.get_filename() == attachment_filename:
+ attachment_data = part.get_payload(decode=True)
+ break
+
+ if not attachment_data:
+ return f"Could not extract attachment data for '{attachment_filename}'"
+
+ # Save attachment
+ from ..backends.file_backend import FileBackend
+ from ..config import config_manager
+
+ workspace_config = config_manager.workspace_config
+ file_backend = FileBackend(
+ email_export_path=workspace_config.email_export_path if workspace_config else None,
+ attachment_download_path=workspace_config.attachment_download_path if workspace_config else None
+ )
+
+ saved_path = file_backend.save_attachment(attachment_data, attachment_filename)
+
+ return f"Attachment '{attachment_filename}' saved to: {saved_path}"
+
+ except Exception as e:
+ return f"Error downloading attachment: {str(e)}"
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/utils/__init__.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/utils/__init__.py
new file mode 100644
index 00000000..20bb4042
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/utils/__init__.py
@@ -0,0 +1,33 @@
+from .exceptions import *
+from .validators import *
+from .email_parser import *
+
+__all__ = [
+ # Exceptions
+ 'EmailMCPError',
+ 'ConnectionError',
+ 'AuthenticationError',
+ 'ConfigurationError',
+ 'ValidationError',
+ 'FolderError',
+ 'EmailNotFoundError',
+ 'AttachmentError',
+ 'SendEmailError',
+
+ # Validators
+ 'validate_email_address',
+ 'validate_email_list',
+ 'validate_page_params',
+ 'validate_file_path',
+ 'validate_folder_name',
+ 'sanitize_subject',
+ 'validate_search_query',
+
+ # Email parser
+ 'decode_email_header',
+ 'parse_email_addresses',
+ 'extract_attachments_info',
+ 'extract_email_body',
+ 'parse_raw_email',
+ 'format_email_summary'
+]
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/utils/email_parser.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/utils/email_parser.py
new file mode 100644
index 00000000..8a79749e
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/utils/email_parser.py
@@ -0,0 +1,309 @@
+import email
+import email.message
+import logging
+from email.header import decode_header
+from email.utils import parseaddr, formataddr
+from typing import Dict, Any, List, Optional
+from ..models.email import EmailMessage, EmailAttachment
+from .exceptions import ValidationError
+
+
+def decode_email_header(header_value: str) -> str:
+ """Decode email header properly handling encoding with improved Chinese support"""
+ if header_value is None:
+ return ""
+
+ try:
+ decoded_parts = decode_header(header_value)
+ result = ""
+ for part, encoding in decoded_parts:
+ if isinstance(part, bytes):
+ if encoding:
+ try:
+ # Try the specified encoding first
+ result += part.decode(encoding)
+ except (UnicodeDecodeError, LookupError):
+ # Fallback to common Chinese encodings
+ for fallback_encoding in ['utf-8', 'gb2312', 'gbk', 'big5']:
+ try:
+ result += part.decode(fallback_encoding)
+ break
+ except (UnicodeDecodeError, LookupError):
+ continue
+ else:
+ # Final fallback with replacement characters
+ result += part.decode('utf-8', errors='replace')
+ else:
+ # No encoding specified, try UTF-8 first, then fallbacks
+ try:
+ result += part.decode('utf-8')
+ except UnicodeDecodeError:
+ for fallback_encoding in ['gb2312', 'gbk', 'big5', 'iso-8859-1']:
+ try:
+ result += part.decode(fallback_encoding)
+ break
+ except (UnicodeDecodeError, LookupError):
+ continue
+ else:
+ result += part.decode('utf-8', errors='replace')
+ else:
+ result += str(part)
+ return result.strip()
+ except Exception as e:
+ logging.warning(f"Failed to decode header: {str(e)}")
+ return str(header_value)
+
+
+def parse_email_address_with_name(addr_string: str) -> tuple[str, str]:
+ """Parse email address with Chinese display name support
+
+ Returns:
+ tuple[str, str]: (display_name, email_address)
+ """
+ if not addr_string:
+ return "", ""
+
+ try:
+ # First decode the header to handle Chinese names
+ decoded_addr = decode_email_header(addr_string)
+
+ # Parse the address
+ display_name, email_addr = parseaddr(decoded_addr)
+
+ # Clean up the display name and email
+ display_name = display_name.strip().strip('"').strip("'")
+ email_addr = email_addr.strip()
+
+ return display_name, email_addr
+ except Exception as e:
+ logging.warning(f"Failed to parse email address '{addr_string}': {str(e)}")
+ return "", addr_string.strip()
+
+
+def parse_email_addresses(addr_string: str) -> List[str]:
+ """Parse comma-separated email addresses with improved Chinese support"""
+ if not addr_string:
+ return []
+
+ addresses = []
+ # Split by comma but be careful about commas in quoted names
+ parts = []
+ current_part = ""
+ in_quotes = False
+ bracket_depth = 0
+
+ for char in addr_string:
+ if char == '"' and bracket_depth == 0:
+ in_quotes = not in_quotes
+ elif char == '<' and not in_quotes:
+ bracket_depth += 1
+ elif char == '>' and not in_quotes:
+ bracket_depth -= 1
+ elif char == ',' and not in_quotes and bracket_depth == 0:
+ parts.append(current_part.strip())
+ current_part = ""
+ continue
+ current_part += char
+
+ if current_part.strip():
+ parts.append(current_part.strip())
+
+ for addr in parts:
+ if addr:
+ # Extract just the email part if in "Name " format
+ display_name, email_addr = parse_email_address_with_name(addr)
+ if email_addr:
+ addresses.append(email_addr)
+
+ return addresses
+
+
+def extract_attachments_info(msg: email.message.Message) -> List[EmailAttachment]:
+ """Extract attachment information from email message"""
+ attachments = []
+
+ if not msg.is_multipart():
+ return attachments
+
+ for part in msg.walk():
+ disposition = part.get('Content-Disposition', '')
+ if 'attachment' in disposition:
+ filename = part.get_filename()
+ if filename:
+ # Decode filename if needed
+ filename = decode_email_header(filename)
+
+ # Get content info
+ content_type = part.get_content_type()
+ # logging.debug(f"Filename: {filename}")
+ # logging.debug(f"Content type: {content_type}")
+ payload = part.get_payload(decode=True)
+ size = len(payload) if payload else 0
+
+ # logging.debug(f"Payload: {payload}")
+ # logging.debug(f"Size: {size}")
+
+ attachment = EmailAttachment(
+ filename=filename,
+ content_type=content_type,
+ size=size,
+ content=payload # 保存附件的实际内容
+ )
+ attachments.append(attachment)
+
+ return attachments
+
+
+def detect_and_decode_content(payload: bytes, part: email.message.Message) -> str:
+ """Detect encoding and decode content with Chinese support"""
+ if not payload:
+ return ""
+
+ # Get charset from content type
+ charset = part.get_content_charset()
+
+ # Try the specified charset first
+ if charset:
+ try:
+ return payload.decode(charset)
+ except (UnicodeDecodeError, LookupError):
+ logging.debug(f"Failed to decode with specified charset: {charset}")
+
+ # Try common encodings in order of preference
+ encodings_to_try = [
+ 'utf-8',
+ 'gb2312',
+ 'gbk',
+ 'big5',
+ 'iso-8859-1',
+ 'windows-1252'
+ ]
+
+ for encoding in encodings_to_try:
+ try:
+ return payload.decode(encoding)
+ except (UnicodeDecodeError, LookupError):
+ continue
+
+ # Final fallback with replacement characters
+ return payload.decode('utf-8', errors='replace')
+
+
+def extract_email_body(msg: email.message.Message) -> tuple[str, str]:
+ """Extract text and HTML body from email message with improved Chinese encoding support"""
+ body_text = ""
+ body_html = ""
+
+ if msg.is_multipart():
+ for part in msg.walk():
+ content_type = part.get_content_type()
+ disposition = part.get('Content-Disposition', '')
+
+ # Skip attachments
+ if 'attachment' in disposition:
+ continue
+
+ payload = part.get_payload(decode=True)
+ if not payload:
+ continue
+
+ try:
+ content = detect_and_decode_content(payload, part)
+ except Exception as e:
+ logging.warning(f"Failed to decode email content: {str(e)}")
+ continue
+
+ if content_type == 'text/plain' and not body_text:
+ body_text = content
+ elif content_type == 'text/html' and not body_html:
+ body_html = content
+ else:
+ # Single part message
+ payload = msg.get_payload(decode=True)
+ if payload:
+ try:
+ content = detect_and_decode_content(payload, msg)
+ if msg.get_content_type() == 'text/html':
+ body_html = content
+ else:
+ body_text = content
+ except Exception as e:
+ logging.warning(f"Failed to decode single part message: {str(e)}")
+
+ return body_text, body_html
+
+
+def parse_raw_email(raw_email: bytes, email_id: str) -> EmailMessage:
+ """Parse raw email bytes into EmailMessage object with improved Chinese support"""
+ try:
+ msg = email.message_from_bytes(raw_email)
+
+ # Extract headers with proper Chinese decoding
+ subject = decode_email_header(msg.get('Subject', ''))
+
+ # Parse addresses with Chinese display name support
+ from_display_name, from_addr = parse_email_address_with_name(msg.get('From', ''))
+ to_display_name, to_addr = parse_email_address_with_name(msg.get('To', ''))
+ cc_addr = decode_email_header(msg.get('Cc', '')) or None
+
+ # Store the original from address with display name for reply functionality
+ original_from = decode_email_header(msg.get('From', ''))
+
+ date = msg.get('Date', '')
+ message_id = msg.get('Message-ID', '')
+
+ # Extract body content with improved encoding detection
+ body_text, body_html = extract_email_body(msg)
+
+ # Extract attachments
+ attachments = extract_attachments_info(msg)
+
+ # Create EmailMessage with additional metadata
+ email_msg = EmailMessage(
+ email_id=email_id,
+ subject=subject,
+ from_addr=from_addr,
+ to_addr=to_addr,
+ cc_addr=cc_addr,
+ date=date,
+ body_text=body_text,
+ body_html=body_html,
+ attachments=attachments,
+ message_id=message_id
+ )
+
+ # Store the raw message for attachment extraction
+ email_msg.raw_message = msg
+
+ # Add extra metadata for Chinese support
+ if hasattr(email_msg, '__dict__'):
+ email_msg.__dict__['from_display_name'] = from_display_name
+ email_msg.__dict__['to_display_name'] = to_display_name
+ email_msg.__dict__['original_from'] = original_from
+
+ return email_msg
+
+ except Exception as e:
+ logging.error(f"Failed to parse email {email_id}: {str(e)}")
+ raise ValidationError(f"Failed to parse email: {str(e)}")
+
+
+def format_email_summary(email: EmailMessage, include_body_preview: bool = False) -> str:
+ """Format email for display summary"""
+ result = f"Subject: {email.subject}\n"
+ result += f"From: {email.from_addr}\n"
+ result += f"To: {email.to_addr}\n"
+
+ if email.cc_addr:
+ result += f"CC: {email.cc_addr}\n"
+
+ result += f"Date: {email.date}\n"
+
+ if email.attachments:
+ result += f"Attachments: {len(email.attachments)} files\n"
+
+ if include_body_preview and email.body_text:
+ preview = email.body_text[:200] + "..." if len(email.body_text) > 200 else email.body_text
+ result += f"Preview: {preview}\n"
+
+ return result
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/utils/encode_decode.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/utils/encode_decode.py
new file mode 100644
index 00000000..6d486cd7
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/utils/encode_decode.py
@@ -0,0 +1,10 @@
+import imapclient
+
+def encode_to_imap_utf7(text):
+ """将文本编码为IMAP UTF-7格式"""
+ return imapclient.imap_utf7.encode(text).decode('ascii')
+
+
+def decode_from_imap_utf7(encoded_text):
+ """将IMAP UTF-7格式解码为文本"""
+ return imapclient.imap_utf7.decode(encoded_text.encode('ascii'))
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/utils/exceptions.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/utils/exceptions.py
new file mode 100644
index 00000000..bf063278
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/utils/exceptions.py
@@ -0,0 +1,43 @@
+class EmailMCPError(Exception):
+ """Base exception for Email MCP operations"""
+ pass
+
+
+class ConnectionError(EmailMCPError):
+ """Email server connection error"""
+ pass
+
+
+class AuthenticationError(EmailMCPError):
+ """Email authentication error"""
+ pass
+
+
+class ConfigurationError(EmailMCPError):
+ """Configuration error"""
+ pass
+
+
+class ValidationError(EmailMCPError):
+ """Data validation error"""
+ pass
+
+
+class FolderError(EmailMCPError):
+ """Email folder operation error"""
+ pass
+
+
+class EmailNotFoundError(EmailMCPError):
+ """Email not found error"""
+ pass
+
+
+class AttachmentError(EmailMCPError):
+ """Attachment operation error"""
+ pass
+
+
+class SendEmailError(EmailMCPError):
+ """Email sending error"""
+ pass
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/utils/validators.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/utils/validators.py
new file mode 100644
index 00000000..8460b18f
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/emails_mcp/utils/validators.py
@@ -0,0 +1,156 @@
+import re
+from typing import Optional
+from pathlib import Path
+from .exceptions import ValidationError
+
+
+def validate_email_address(email: str) -> bool:
+ """Validate email address format with international domain support"""
+ if not email or not isinstance(email, str):
+ return False
+
+ # More flexible pattern that supports international domains
+ # Split into local and domain parts for separate validation
+ if '@' not in email:
+ return False
+
+ local_part, domain_part = email.rsplit('@', 1)
+
+ # Validate local part (before @)
+ # Allow ASCII characters and common symbols, but not international characters in local part
+ # This follows RFC 5321 more closely
+ if not local_part or len(local_part) > 64:
+ return False
+
+ # Check for valid characters in local part (ASCII only for now)
+ # Allow letters, numbers, and common symbols
+ valid_local_chars = re.match(r'^[a-zA-Z0-9._%+-]+$', local_part)
+ if not valid_local_chars:
+ return False
+
+ # Basic validation: no consecutive dots, no start/end with dot
+ if local_part.startswith('.') or local_part.endswith('.') or '..' in local_part:
+ return False
+
+ # Validate domain part (after @)
+ if not domain_part or len(domain_part) > 255:
+ return False
+
+ # Domain should have at least one dot and valid structure
+ if '.' not in domain_part:
+ return False
+
+ # Split domain into parts
+ domain_parts = domain_part.split('.')
+ if len(domain_parts) < 2:
+ return False
+
+ # Each domain part should not be empty and should have reasonable length
+ for part in domain_parts:
+ if not part or len(part) > 63:
+ return False
+ # Allow international characters in domain names (IDN support)
+ if not re.match(r'^[a-zA-Z0-9\u00a1-\uffff-]+$', part):
+ return False
+
+ # TLD should be at least 2 characters (but allow international TLDs)
+ if len(domain_parts[-1]) < 2:
+ return False
+
+ return True
+
+
+def validate_email_list(email_list: str) -> tuple[bool, str]:
+ """Validate comma-separated email list"""
+ if not email_list:
+ return False, "Email list cannot be empty"
+
+ emails = [email.strip() for email in email_list.split(',')]
+ invalid_emails = []
+
+ for email in emails:
+ if not validate_email_address(email):
+ invalid_emails.append(email)
+
+ if invalid_emails:
+ return False, f"Invalid email addresses: {', '.join(invalid_emails)}"
+
+ return True, ""
+
+
+def validate_page_params(page: int, page_size: int, max_page_size: int = 50) -> tuple[int, int, str]:
+ """Validate and normalize pagination parameters"""
+ warning = ""
+
+ if page < 1:
+ page = 1
+ warning += "Page number adjusted to 1. "
+
+ if page_size < 1:
+ page_size = 20
+ warning += "Page size adjusted to 20. "
+ elif page_size > max_page_size:
+ page_size = max_page_size
+ warning += f"Page size limited to {max_page_size}. "
+
+ return page, page_size, warning
+
+
+def validate_file_path(file_path: str, must_exist: bool = True) -> tuple[bool, str]:
+ """Validate file path"""
+ if not file_path:
+ return False, "File path cannot be empty"
+
+ try:
+ path = Path(file_path)
+
+ if must_exist and not path.exists():
+ return False, f"File does not exist: {file_path}"
+
+ if must_exist and not path.is_file():
+ return False, f"Path is not a file: {file_path}"
+
+ return True, ""
+
+ except Exception as e:
+ return False, f"Invalid file path: {str(e)}"
+
+
+def validate_folder_name(folder_name: str) -> tuple[bool, str]:
+ """Validate email folder name"""
+ if not folder_name:
+ return False, "Folder name cannot be empty"
+
+ # Basic validation - no path separators or special chars
+ invalid_chars = ['/', '\\', '..', '<', '>', ':', '"', '|', '?', '*']
+ for char in invalid_chars:
+ if char in folder_name:
+ return False, f"Folder name contains invalid character: {char}"
+
+ if len(folder_name) > 255:
+ return False, "Folder name too long (max 255 characters)"
+
+ return True, ""
+
+
+def sanitize_subject(subject: str) -> str:
+ """Sanitize email subject"""
+ if not subject:
+ return ""
+
+ # Remove control characters and normalize whitespace
+ sanitized = re.sub(r'[\x00-\x1f\x7f-\x9f]', '', subject)
+ sanitized = ' '.join(sanitized.split())
+
+ return sanitized[:998] # RFC limit for subject length
+
+
+def validate_search_query(query: str) -> tuple[bool, str]:
+ """Validate search query"""
+ if not query or not query.strip():
+ return False, "Search query cannot be empty"
+
+ if len(query) > 1000:
+ return False, "Search query too long (max 1000 characters)"
+
+ return True, ""
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/test_connection.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/test_connection.py
new file mode 100644
index 00000000..cda4a408
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/src/test_connection.py
@@ -0,0 +1,215 @@
+#!/usr/bin/env python3
+"""
+Email Connection Tester
+Test IMAP and SMTP server connections
+"""
+
+import argparse
+import imaplib
+import smtplib
+import sys
+from datetime import datetime
+
+class EmailConnectionTester:
+ """Email Connection Tester"""
+
+ def __init__(self, email, password, imap_server, imap_port, smtp_server, smtp_port):
+ self.email = email
+ self.password = password
+ self.imap_server = imap_server
+ self.imap_port = imap_port
+ self.smtp_server = smtp_server
+ self.smtp_port = smtp_port
+
+ def _connect_imap(self):
+ """Connect to IMAP server"""
+ print(f"\n📥 Connecting to IMAP server {self.imap_server}:{self.imap_port}...")
+
+ try:
+ # Try normal connection first to check server capabilities
+ try:
+ print(" Trying SSL connection...")
+ imap = imaplib.IMAP4_SSL(self.imap_server, self.imap_port)
+ print(" ✅ SSL connection successful")
+ except:
+ print(" SSL connection failed, trying normal connection...")
+ imap = imaplib.IMAP4(self.imap_server, self.imap_port)
+ # Try STARTTLS
+ try:
+ print(" Trying to upgrade to TLS...")
+ imap.starttls()
+ print(" ✅ TLS upgrade successful")
+ except Exception as e:
+ print(f" ⚠️ STARTTLS failed: {e}, continuing with non-encrypted connection")
+
+ # Login
+ print(f" Logging in to {self.email}...")
+ imap.login(self.email, self.password)
+ print(" ✅ IMAP login successful!")
+
+ # Get email information
+ print("\n📊 Email information:")
+ # List all folders
+ status, folders = imap.list()
+ if status == 'OK':
+ print(f" 📁 Folder count: {len(folders)}")
+ print(" 📁 Folder list:")
+ max_display = 5
+ for folder in folders[:max_display]:
+ print(f" - {folder.decode()}")
+ if len(folders) > max_display:
+ print(f" ... {len(folders)-max_display} more folders")
+
+ # Select inbox
+ status, count = imap.select('INBOX')
+ if status == 'OK':
+ print(f" 📧 Inbox email count: {count[0].decode()}")
+
+ # Close connection
+ imap.close()
+ imap.logout()
+
+ return True
+
+ except imaplib.IMAP4.error as e:
+ print(f" ❌ IMAP error: {e}")
+ return False
+ except Exception as e:
+ print(f" ❌ Connection failed: {type(e).__name__}: {e}")
+ return False
+
+ def _connect_smtp(self):
+ """Connect to SMTP server"""
+ print(f"\n📤 Connecting to SMTP server {self.smtp_server}:{self.smtp_port}...")
+
+ try:
+ # Try SSL connection first, then normal connection if SSL fails
+ try:
+ print(" Trying SSL connection...")
+ smtp = smtplib.SMTP_SSL(self.smtp_server, self.smtp_port)
+ print(" ✅ SSL connection successful")
+ except:
+ print(" SSL connection failed, trying normal connection...")
+ smtp = smtplib.SMTP(self.smtp_server, self.smtp_port)
+ smtp.set_debuglevel(1) # Set to 1 to see detailed SMTP conversation
+
+ # 打招呼
+ smtp.ehlo()
+
+ # 检查服务器是否支持STARTTLS
+ if smtp.has_extn('STARTTLS'):
+ try:
+ print(" Detected STARTTLS support, trying to upgrade to TLS...")
+ smtp.starttls()
+ print(" Re-doing EHLO handshake...")
+ smtp.ehlo()
+ print(" ✅ TLS upgrade successful")
+ except Exception as e:
+ print(f" ⚠️ STARTTLS failed: {e}, continuing with non-encrypted connection")
+ else:
+ print(" ⚠️ Server does not support STARTTLS")
+
+ # Login
+ print(f" Logging in to {self.email}...")
+ smtp.login(self.email, self.password)
+ print(" ✅ SMTP login successful!")
+
+ # Get server information
+ print("\n📊 SMTP server features:")
+ if hasattr(smtp, 'esmtp_features'):
+ max_display = 5
+ features = list(smtp.esmtp_features.keys())[:max_display]
+ for feature in features:
+ print(f" - {feature}")
+ if len(smtp.esmtp_features) > max_display:
+ print(f" ... {len(smtp.esmtp_features)-max_display} more features")
+
+ # Close connection
+ smtp.quit()
+
+ return True
+
+ except smtplib.SMTPAuthenticationError:
+ print(" ❌ SMTP authentication failed: username or password error")
+ return False
+ except smtplib.SMTPException as e:
+ print(f" ❌ SMTP error: {e}")
+ return False
+ except Exception as e:
+ print(f" ❌ Connection failed: {type(e).__name__}: {e}")
+ return False
+
+ def test_all(self):
+ """Test all connections"""
+ print("=" * 50)
+ print(f"📧 Email connection test")
+ print(f"⏰ Test time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
+ print("=" * 50)
+
+ print(f"\n📋 Configuration:")
+ print(f" Email: {self.email}")
+ print(f" IMAP: {self.imap_server}:{self.imap_port}")
+ print(f" SMTP: {self.smtp_server}:{self.smtp_port}")
+
+ # Test IMAP
+ imap_success = self._connect_imap()
+
+ # Test SMTP
+ smtp_success = self._connect_smtp()
+
+ # Summary
+ print("\n" + "=" * 50)
+ print("📊 Test results summary:")
+ print(f" IMAP: {'✅ Success' if imap_success else '❌ Failed'}")
+ print(f" SMTP: {'✅ Success' if smtp_success else '❌ Failed'}")
+ print("=" * 50)
+
+ return imap_success and smtp_success
+
+def main():
+ """Main function"""
+ parser = argparse.ArgumentParser(
+ description='Test email server connections',
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ epilog="""
+Examples:
+ # Test Gmail
+ %(prog)s -e myemail@gmail.com -p mypassword -is imap.gmail.com -ip 993 -ss smtp.gmail.com -sp 587
+
+ # Test local Poste.io
+ %(prog)s -e user1@mcp.com -p password -is localhost -ip 1143 -ss localhost -sp 2525
+
+ # Simplified write (using short parameters)
+ %(prog)s -e test@test.com -p pass123 -is localhost -ip 143 -ss localhost -sp 25
+ """
+ )
+
+ # Add parameters
+ parser.add_argument('-e', '--email', required=True, help='Email address')
+ parser.add_argument('-p', '--password', required=True, help='Email password')
+ parser.add_argument('-is', '--imap-server', required=True, help='IMAP server address')
+ parser.add_argument('-ip', '--imap-port', type=int, required=True, help='IMAP port (143/993)')
+ parser.add_argument('-ss', '--smtp-server', required=True, help='SMTP server address')
+ parser.add_argument('-sp', '--smtp-port', type=int, required=True, help='SMTP port (25/465/587)')
+
+ # Parse parameters
+ args = parser.parse_args()
+
+ # Create tester and run
+ tester = EmailConnectionTester(
+ email=args.email,
+ password=args.password,
+ imap_server=args.imap_server,
+ imap_port=args.imap_port,
+ smtp_server=args.smtp_server,
+ smtp_port=args.smtp_port
+ )
+
+ # Run test
+ success = tester.test_all()
+
+ # Return status code
+ sys.exit(0 if success else 1)
+
+if __name__ == '__main__':
+ main()
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/uv.lock b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/uv.lock
new file mode 100644
index 00000000..69a7db37
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/emails-mcp/uv.lock
@@ -0,0 +1,1089 @@
+version = 1
+revision = 3
+requires-python = ">=3.12"
+
+[[package]]
+name = "annotated-types"
+version = "0.7.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
+]
+
+[[package]]
+name = "anyio"
+version = "4.10.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "idna" },
+ { name = "sniffio" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/f1/b4/636b3b65173d3ce9a38ef5f0522789614e590dab6a8d505340a4efe4c567/anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6", size = 213252, upload-time = "2025-08-04T08:54:26.451Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6f/12/e5e0282d673bb9746bacfb6e2dba8719989d3660cdb2ea79aee9a9651afb/anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1", size = 107213, upload-time = "2025-08-04T08:54:24.882Z" },
+]
+
+[[package]]
+name = "attrs"
+version = "25.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032, upload-time = "2025-03-13T11:10:22.779Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" },
+]
+
+[[package]]
+name = "authlib"
+version = "1.6.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cryptography" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/8e/a1/d8d1c6f8bc922c0b87ae0d933a8ed57be1bef6970894ed79c2852a153cd3/authlib-1.6.1.tar.gz", hash = "sha256:4dffdbb1460ba6ec8c17981a4c67af7d8af131231b5a36a88a1e8c80c111cdfd", size = 159988, upload-time = "2025-07-20T07:38:42.834Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f9/58/cc6a08053f822f98f334d38a27687b69c6655fb05cd74a7a5e70a2aeed95/authlib-1.6.1-py2.py3-none-any.whl", hash = "sha256:e9d2031c34c6309373ab845afc24168fe9e93dc52d252631f52642f21f5ed06e", size = 239299, upload-time = "2025-07-20T07:38:39.259Z" },
+]
+
+[[package]]
+name = "beautifulsoup4"
+version = "4.13.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "soupsieve" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/d8/e4/0c4c39e18fd76d6a628d4dd8da40543d136ce2d1752bd6eeeab0791f4d6b/beautifulsoup4-4.13.4.tar.gz", hash = "sha256:dbb3c4e1ceae6aefebdaf2423247260cd062430a410e38c66f2baa50a8437195", size = 621067, upload-time = "2025-04-15T17:05:13.836Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/50/cd/30110dc0ffcf3b131156077b90e9f60ed75711223f306da4db08eff8403b/beautifulsoup4-4.13.4-py3-none-any.whl", hash = "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b", size = 187285, upload-time = "2025-04-15T17:05:12.221Z" },
+]
+
+[[package]]
+name = "certifi"
+version = "2025.8.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" },
+]
+
+[[package]]
+name = "cffi"
+version = "1.17.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pycparser" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178, upload-time = "2024-09-04T20:44:12.232Z" },
+ { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840, upload-time = "2024-09-04T20:44:13.739Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload-time = "2024-09-04T20:44:15.231Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850, upload-time = "2024-09-04T20:44:17.188Z" },
+ { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729, upload-time = "2024-09-04T20:44:18.688Z" },
+ { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256, upload-time = "2024-09-04T20:44:20.248Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424, upload-time = "2024-09-04T20:44:21.673Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568, upload-time = "2024-09-04T20:44:23.245Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736, upload-time = "2024-09-04T20:44:24.757Z" },
+ { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448, upload-time = "2024-09-04T20:44:26.208Z" },
+ { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976, upload-time = "2024-09-04T20:44:27.578Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989, upload-time = "2024-09-04T20:44:28.956Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802, upload-time = "2024-09-04T20:44:30.289Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload-time = "2024-09-04T20:44:32.01Z" },
+ { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload-time = "2024-09-04T20:44:33.606Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload-time = "2024-09-04T20:44:35.191Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200, upload-time = "2024-09-04T20:44:36.743Z" },
+ { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447, upload-time = "2024-09-04T20:44:38.492Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358, upload-time = "2024-09-04T20:44:40.046Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469, upload-time = "2024-09-04T20:44:41.616Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475, upload-time = "2024-09-04T20:44:43.733Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009, upload-time = "2024-09-04T20:44:45.309Z" },
+]
+
+[[package]]
+name = "charset-normalizer"
+version = "3.4.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936, upload-time = "2025-05-02T08:32:33.712Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790, upload-time = "2025-05-02T08:32:35.768Z" },
+ { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924, upload-time = "2025-05-02T08:32:37.284Z" },
+ { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626, upload-time = "2025-05-02T08:32:38.803Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567, upload-time = "2025-05-02T08:32:40.251Z" },
+ { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957, upload-time = "2025-05-02T08:32:41.705Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408, upload-time = "2025-05-02T08:32:43.709Z" },
+ { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399, upload-time = "2025-05-02T08:32:46.197Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815, upload-time = "2025-05-02T08:32:48.105Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537, upload-time = "2025-05-02T08:32:49.719Z" },
+ { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload-time = "2025-05-02T08:32:51.404Z" },
+ { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload-time = "2025-05-02T08:32:53.079Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload-time = "2025-05-02T08:32:54.573Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" },
+ { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" },
+ { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" },
+ { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" },
+ { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" },
+ { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" },
+ { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" },
+ { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" },
+]
+
+[[package]]
+name = "click"
+version = "8.2.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" },
+]
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
+]
+
+[[package]]
+name = "cryptography"
+version = "45.0.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/95/1e/49527ac611af559665f71cbb8f92b332b5ec9c6fbc4e88b0f8e92f5e85df/cryptography-45.0.5.tar.gz", hash = "sha256:72e76caa004ab63accdf26023fccd1d087f6d90ec6048ff33ad0445abf7f605a", size = 744903, upload-time = "2025-07-02T13:06:25.941Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f0/fb/09e28bc0c46d2c547085e60897fea96310574c70fb21cd58a730a45f3403/cryptography-45.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:101ee65078f6dd3e5a028d4f19c07ffa4dd22cce6a20eaa160f8b5219911e7d8", size = 7043092, upload-time = "2025-07-02T13:05:01.514Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/05/2194432935e29b91fb649f6149c1a4f9e6d3d9fc880919f4ad1bcc22641e/cryptography-45.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3a264aae5f7fbb089dbc01e0242d3b67dffe3e6292e1f5182122bdf58e65215d", size = 4205926, upload-time = "2025-07-02T13:05:04.741Z" },
+ { url = "https://files.pythonhosted.org/packages/07/8b/9ef5da82350175e32de245646b1884fc01124f53eb31164c77f95a08d682/cryptography-45.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e74d30ec9c7cb2f404af331d5b4099a9b322a8a6b25c4632755c8757345baac5", size = 4429235, upload-time = "2025-07-02T13:05:07.084Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/e1/c809f398adde1994ee53438912192d92a1d0fc0f2d7582659d9ef4c28b0c/cryptography-45.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3af26738f2db354aafe492fb3869e955b12b2ef2e16908c8b9cb928128d42c57", size = 4209785, upload-time = "2025-07-02T13:05:09.321Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/8b/07eb6bd5acff58406c5e806eff34a124936f41a4fb52909ffa4d00815f8c/cryptography-45.0.5-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e6c00130ed423201c5bc5544c23359141660b07999ad82e34e7bb8f882bb78e0", size = 3893050, upload-time = "2025-07-02T13:05:11.069Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/ef/3333295ed58d900a13c92806b67e62f27876845a9a908c939f040887cca9/cryptography-45.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:dd420e577921c8c2d31289536c386aaa30140b473835e97f83bc71ea9d2baf2d", size = 4457379, upload-time = "2025-07-02T13:05:13.32Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/9d/44080674dee514dbb82b21d6fa5d1055368f208304e2ab1828d85c9de8f4/cryptography-45.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:d05a38884db2ba215218745f0781775806bde4f32e07b135348355fe8e4991d9", size = 4209355, upload-time = "2025-07-02T13:05:15.017Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/d8/0749f7d39f53f8258e5c18a93131919ac465ee1f9dccaf1b3f420235e0b5/cryptography-45.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:ad0caded895a00261a5b4aa9af828baede54638754b51955a0ac75576b831b27", size = 4456087, upload-time = "2025-07-02T13:05:16.945Z" },
+ { url = "https://files.pythonhosted.org/packages/09/d7/92acac187387bf08902b0bf0699816f08553927bdd6ba3654da0010289b4/cryptography-45.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9024beb59aca9d31d36fcdc1604dd9bbeed0a55bface9f1908df19178e2f116e", size = 4332873, upload-time = "2025-07-02T13:05:18.743Z" },
+ { url = "https://files.pythonhosted.org/packages/03/c2/840e0710da5106a7c3d4153c7215b2736151bba60bf4491bdb421df5056d/cryptography-45.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:91098f02ca81579c85f66df8a588c78f331ca19089763d733e34ad359f474174", size = 4564651, upload-time = "2025-07-02T13:05:21.382Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/92/cc723dd6d71e9747a887b94eb3827825c6c24b9e6ce2bb33b847d31d5eaa/cryptography-45.0.5-cp311-abi3-win32.whl", hash = "sha256:926c3ea71a6043921050eaa639137e13dbe7b4ab25800932a8498364fc1abec9", size = 2929050, upload-time = "2025-07-02T13:05:23.39Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/10/197da38a5911a48dd5389c043de4aec4b3c94cb836299b01253940788d78/cryptography-45.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:b85980d1e345fe769cfc57c57db2b59cff5464ee0c045d52c0df087e926fbe63", size = 3403224, upload-time = "2025-07-02T13:05:25.202Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/2b/160ce8c2765e7a481ce57d55eba1546148583e7b6f85514472b1d151711d/cryptography-45.0.5-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:f3562c2f23c612f2e4a6964a61d942f891d29ee320edb62ff48ffb99f3de9ae8", size = 7017143, upload-time = "2025-07-02T13:05:27.229Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/e7/2187be2f871c0221a81f55ee3105d3cf3e273c0a0853651d7011eada0d7e/cryptography-45.0.5-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3fcfbefc4a7f332dece7272a88e410f611e79458fab97b5efe14e54fe476f4fd", size = 4197780, upload-time = "2025-07-02T13:05:29.299Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/cf/84210c447c06104e6be9122661159ad4ce7a8190011669afceeaea150524/cryptography-45.0.5-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:460f8c39ba66af7db0545a8c6f2eabcbc5a5528fc1cf6c3fa9a1e44cec33385e", size = 4420091, upload-time = "2025-07-02T13:05:31.221Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/6a/cb8b5c8bb82fafffa23aeff8d3a39822593cee6e2f16c5ca5c2ecca344f7/cryptography-45.0.5-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9b4cf6318915dccfe218e69bbec417fdd7c7185aa7aab139a2c0beb7468c89f0", size = 4198711, upload-time = "2025-07-02T13:05:33.062Z" },
+ { url = "https://files.pythonhosted.org/packages/04/f7/36d2d69df69c94cbb2473871926daf0f01ad8e00fe3986ac3c1e8c4ca4b3/cryptography-45.0.5-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2089cc8f70a6e454601525e5bf2779e665d7865af002a5dec8d14e561002e135", size = 3883299, upload-time = "2025-07-02T13:05:34.94Z" },
+ { url = "https://files.pythonhosted.org/packages/82/c7/f0ea40f016de72f81288e9fe8d1f6748036cb5ba6118774317a3ffc6022d/cryptography-45.0.5-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0027d566d65a38497bc37e0dd7c2f8ceda73597d2ac9ba93810204f56f52ebc7", size = 4450558, upload-time = "2025-07-02T13:05:37.288Z" },
+ { url = "https://files.pythonhosted.org/packages/06/ae/94b504dc1a3cdf642d710407c62e86296f7da9e66f27ab12a1ee6fdf005b/cryptography-45.0.5-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:be97d3a19c16a9be00edf79dca949c8fa7eff621763666a145f9f9535a5d7f42", size = 4198020, upload-time = "2025-07-02T13:05:39.102Z" },
+ { url = "https://files.pythonhosted.org/packages/05/2b/aaf0adb845d5dabb43480f18f7ca72e94f92c280aa983ddbd0bcd6ecd037/cryptography-45.0.5-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:7760c1c2e1a7084153a0f68fab76e754083b126a47d0117c9ed15e69e2103492", size = 4449759, upload-time = "2025-07-02T13:05:41.398Z" },
+ { url = "https://files.pythonhosted.org/packages/91/e4/f17e02066de63e0100a3a01b56f8f1016973a1d67551beaf585157a86b3f/cryptography-45.0.5-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6ff8728d8d890b3dda5765276d1bc6fb099252915a2cd3aff960c4c195745dd0", size = 4319991, upload-time = "2025-07-02T13:05:43.64Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/2e/e2dbd629481b499b14516eed933f3276eb3239f7cee2dcfa4ee6b44d4711/cryptography-45.0.5-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7259038202a47fdecee7e62e0fd0b0738b6daa335354396c6ddebdbe1206af2a", size = 4554189, upload-time = "2025-07-02T13:05:46.045Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/ea/a78a0c38f4c8736287b71c2ea3799d173d5ce778c7d6e3c163a95a05ad2a/cryptography-45.0.5-cp37-abi3-win32.whl", hash = "sha256:1e1da5accc0c750056c556a93c3e9cb828970206c68867712ca5805e46dc806f", size = 2911769, upload-time = "2025-07-02T13:05:48.329Z" },
+ { url = "https://files.pythonhosted.org/packages/79/b3/28ac139109d9005ad3f6b6f8976ffede6706a6478e21c889ce36c840918e/cryptography-45.0.5-cp37-abi3-win_amd64.whl", hash = "sha256:90cb0a7bb35959f37e23303b7eed0a32280510030daba3f7fdfbb65defde6a97", size = 3390016, upload-time = "2025-07-02T13:05:50.811Z" },
+]
+
+[[package]]
+name = "cyclopts"
+version = "3.22.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "attrs" },
+ { name = "docstring-parser", marker = "python_full_version < '4'" },
+ { name = "rich" },
+ { name = "rich-rst" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a3/d5/24c6c894f3833bc93d4944c2064309dfd633c0becf93e16fc79d76edd388/cyclopts-3.22.5.tar.gz", hash = "sha256:fa2450b9840abc41c6aa37af5eaeafc7a1264e08054e3a2fe39d49aa154f592a", size = 74890, upload-time = "2025-07-31T18:18:37.336Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/df/e5/a7b6db64f08cfe065e531ec6b508fa7dac704fab70d05adb5bc0c2c1d1b6/cyclopts-3.22.5-py3-none-any.whl", hash = "sha256:92efb4a094d9812718d7efe0bffa319a19cb661f230dbf24406c18cd8809fb82", size = 84994, upload-time = "2025-07-31T18:18:35.939Z" },
+]
+
+[[package]]
+name = "dnspython"
+version = "2.7.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b5/4a/263763cb2ba3816dd94b08ad3a33d5fdae34ecb856678773cc40a3605829/dnspython-2.7.0.tar.gz", hash = "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1", size = 345197, upload-time = "2024-10-05T20:14:59.362Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/68/1b/e0a87d256e40e8c888847551b20a017a6b98139178505dc7ffb96f04e954/dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86", size = 313632, upload-time = "2024-10-05T20:14:57.687Z" },
+]
+
+[[package]]
+name = "docstring-parser"
+version = "0.17.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442, upload-time = "2025-07-21T07:35:01.868Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" },
+]
+
+[[package]]
+name = "docutils"
+version = "0.22"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e9/86/5b41c32ecedcfdb4c77b28b6cb14234f252075f8cdb254531727a35547dd/docutils-0.22.tar.gz", hash = "sha256:ba9d57750e92331ebe7c08a1bbf7a7f8143b86c476acd51528b042216a6aad0f", size = 2277984, upload-time = "2025-07-29T15:20:31.06Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/44/57/8db39bc5f98f042e0153b1de9fb88e1a409a33cda4dd7f723c2ed71e01f6/docutils-0.22-py3-none-any.whl", hash = "sha256:4ed966a0e96a0477d852f7af31bdcb3adc049fbb35ccba358c2ea8a03287615e", size = 630709, upload-time = "2025-07-29T15:20:28.335Z" },
+]
+
+[[package]]
+name = "email-validator"
+version = "2.2.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "dnspython" },
+ { name = "idna" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/48/ce/13508a1ec3f8bb981ae4ca79ea40384becc868bfae97fd1c942bb3a001b1/email_validator-2.2.0.tar.gz", hash = "sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7", size = 48967, upload-time = "2024-06-20T11:30:30.034Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d7/ee/bf0adb559ad3c786f12bcbc9296b3f5675f529199bef03e2df281fa1fadb/email_validator-2.2.0-py3-none-any.whl", hash = "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631", size = 33521, upload-time = "2024-06-20T11:30:28.248Z" },
+]
+
+[[package]]
+name = "emails-mcp"
+version = "0.1.11"
+source = { editable = "." }
+dependencies = [
+ { name = "beautifulsoup4" },
+ { name = "email-validator" },
+ { name = "fastmcp" },
+ { name = "imapclient" },
+ { name = "mcp", extra = ["cli"] },
+ { name = "psycopg2-binary" },
+ { name = "requests" },
+ { name = "typing-extensions" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "beautifulsoup4", specifier = ">=4.13.4" },
+ { name = "email-validator", specifier = ">=2.1.1" },
+ { name = "fastmcp", specifier = ">=2.10.5" },
+ { name = "imapclient", specifier = ">=3.0.1" },
+ { name = "mcp", extras = ["cli"], specifier = ">=1.11.0" },
+ { name = "psycopg2-binary", specifier = ">=2.9" },
+ { name = "requests", specifier = ">=2.32.4" },
+ { name = "typing-extensions", specifier = ">=4.9.0" },
+]
+
+[[package]]
+name = "exceptiongroup"
+version = "1.3.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" },
+]
+
+[[package]]
+name = "fastmcp"
+version = "2.11.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "authlib" },
+ { name = "cyclopts" },
+ { name = "exceptiongroup" },
+ { name = "httpx" },
+ { name = "mcp" },
+ { name = "openapi-core" },
+ { name = "openapi-pydantic" },
+ { name = "pydantic", extra = ["email"] },
+ { name = "pyperclip" },
+ { name = "python-dotenv" },
+ { name = "rich" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/92/89/d100073d15cdfa5fa029107b44ef55916b04ed6010ff2b0f7bed92a35ed9/fastmcp-2.11.1.tar.gz", hash = "sha256:2b5af21b093d4926fef17a9a162d5729a2fcb46f3b195699762fa01f61ac3c60", size = 2672724, upload-time = "2025-08-04T15:39:29.623Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a6/9f/f3703867a8be93f2a139f6664fa7ff46c5c844e28998ce288f7b919ed197/fastmcp-2.11.1-py3-none-any.whl", hash = "sha256:9f0b6a3f61dcf6f688a0a24b8b507be24bfae051a00b7d590c01395d63da8c00", size = 256573, upload-time = "2025-08-04T15:39:27.594Z" },
+]
+
+[[package]]
+name = "h11"
+version = "0.16.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
+]
+
+[[package]]
+name = "httpcore"
+version = "1.0.9"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "h11" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
+]
+
+[[package]]
+name = "httpx"
+version = "0.28.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "certifi" },
+ { name = "httpcore" },
+ { name = "idna" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
+]
+
+[[package]]
+name = "httpx-sse"
+version = "0.4.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6e/fa/66bd985dd0b7c109a3bcb89272ee0bfb7e2b4d06309ad7b38ff866734b2a/httpx_sse-0.4.1.tar.gz", hash = "sha256:8f44d34414bc7b21bf3602713005c5df4917884f76072479b21f68befa4ea26e", size = 12998, upload-time = "2025-06-24T13:21:05.71Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/25/0a/6269e3473b09aed2dab8aa1a600c70f31f00ae1349bee30658f7e358a159/httpx_sse-0.4.1-py3-none-any.whl", hash = "sha256:cba42174344c3a5b06f255ce65b350880f962d99ead85e776f23c6618a377a37", size = 8054, upload-time = "2025-06-24T13:21:04.772Z" },
+]
+
+[[package]]
+name = "idna"
+version = "3.10"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
+]
+
+[[package]]
+name = "imapclient"
+version = "3.0.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b6/63/0eea51c9c263c18021cdc5866def55c98393f3bd74bbb8e3053e36f0f81a/IMAPClient-3.0.1.zip", hash = "sha256:78e6d62fbfbbe233e1f0e0e993160fd665eb1fd35973acddc61c15719b22bc02", size = 244222, upload-time = "2023-12-02T08:24:15.344Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/de/8a/d1364c1c6d8f53ea390e8f1c6da220a4f9ee478ac8a473ae0669a2fb6f51/IMAPClient-3.0.1-py2.py3-none-any.whl", hash = "sha256:d77d77caa4123e0233b5cf2b9c54a078522e63270b88d3f48653a28637fd8828", size = 182490, upload-time = "2023-12-02T08:24:11.854Z" },
+]
+
+[[package]]
+name = "isodate"
+version = "0.7.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/54/4d/e940025e2ce31a8ce1202635910747e5a87cc3a6a6bb2d00973375014749/isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6", size = 29705, upload-time = "2024-10-08T23:04:11.5Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320, upload-time = "2024-10-08T23:04:09.501Z" },
+]
+
+[[package]]
+name = "jsonschema"
+version = "4.25.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "attrs" },
+ { name = "jsonschema-specifications" },
+ { name = "referencing" },
+ { name = "rpds-py" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/d5/00/a297a868e9d0784450faa7365c2172a7d6110c763e30ba861867c32ae6a9/jsonschema-4.25.0.tar.gz", hash = "sha256:e63acf5c11762c0e6672ffb61482bdf57f0876684d8d249c0fe2d730d48bc55f", size = 356830, upload-time = "2025-07-18T15:39:45.11Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fe/54/c86cd8e011fe98803d7e382fd67c0df5ceab8d2b7ad8c5a81524f791551c/jsonschema-4.25.0-py3-none-any.whl", hash = "sha256:24c2e8da302de79c8b9382fee3e76b355e44d2a4364bb207159ce10b517bd716", size = 89184, upload-time = "2025-07-18T15:39:42.956Z" },
+]
+
+[[package]]
+name = "jsonschema-path"
+version = "0.3.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pathable" },
+ { name = "pyyaml" },
+ { name = "referencing" },
+ { name = "requests" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/6e/45/41ebc679c2a4fced6a722f624c18d658dee42612b83ea24c1caf7c0eb3a8/jsonschema_path-0.3.4.tar.gz", hash = "sha256:8365356039f16cc65fddffafda5f58766e34bebab7d6d105616ab52bc4297001", size = 11159, upload-time = "2025-01-24T14:33:16.547Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cb/58/3485da8cb93d2f393bce453adeef16896751f14ba3e2024bc21dc9597646/jsonschema_path-0.3.4-py3-none-any.whl", hash = "sha256:f502191fdc2b22050f9a81c9237be9d27145b9001c55842bece5e94e382e52f8", size = 14810, upload-time = "2025-01-24T14:33:14.652Z" },
+]
+
+[[package]]
+name = "jsonschema-specifications"
+version = "2025.4.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "referencing" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/bf/ce/46fbd9c8119cfc3581ee5643ea49464d168028cfb5caff5fc0596d0cf914/jsonschema_specifications-2025.4.1.tar.gz", hash = "sha256:630159c9f4dbea161a6a2205c3011cc4f18ff381b189fff48bb39b9bf26ae608", size = 15513, upload-time = "2025-04-23T12:34:07.418Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/01/0e/b27cdbaccf30b890c40ed1da9fd4a3593a5cf94dae54fb34f8a4b74fcd3f/jsonschema_specifications-2025.4.1-py3-none-any.whl", hash = "sha256:4653bffbd6584f7de83a67e0d620ef16900b390ddc7939d56684d6c81e33f1af", size = 18437, upload-time = "2025-04-23T12:34:05.422Z" },
+]
+
+[[package]]
+name = "lazy-object-proxy"
+version = "1.11.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/57/f9/1f56571ed82fb324f293661690635cf42c41deb8a70a6c9e6edc3e9bb3c8/lazy_object_proxy-1.11.0.tar.gz", hash = "sha256:18874411864c9fbbbaa47f9fc1dd7aea754c86cfde21278ef427639d1dd78e9c", size = 44736, upload-time = "2025-04-16T16:53:48.482Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/4d/24/dae4759469e9cd318fef145f7cfac7318261b47b23a4701aa477b0c3b42c/lazy_object_proxy-1.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9a9f39098e93a63618a79eef2889ae3cf0605f676cd4797fdfd49fcd7ddc318b", size = 28142, upload-time = "2025-04-16T16:53:37.663Z" },
+ { url = "https://files.pythonhosted.org/packages/de/0c/645a881f5f27952a02f24584d96f9f326748be06ded2cee25f8f8d1cd196/lazy_object_proxy-1.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:ee13f67f4fcd044ef27bfccb1c93d39c100046fec1fad6e9a1fcdfd17492aeb3", size = 28380, upload-time = "2025-04-16T16:53:39.07Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/0f/6e004f928f7ff5abae2b8e1f68835a3870252f886e006267702e1efc5c7b/lazy_object_proxy-1.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fd4c84eafd8dd15ea16f7d580758bc5c2ce1f752faec877bb2b1f9f827c329cd", size = 28149, upload-time = "2025-04-16T16:53:40.135Z" },
+ { url = "https://files.pythonhosted.org/packages/63/cb/b8363110e32cc1fd82dc91296315f775d37a39df1c1cfa976ec1803dac89/lazy_object_proxy-1.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:d2503427bda552d3aefcac92f81d9e7ca631e680a2268cbe62cd6a58de6409b7", size = 28389, upload-time = "2025-04-16T16:53:43.612Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/89/68c50fcfd81e11480cd8ee7f654c9bd790a9053b9a0efe9983d46106f6a9/lazy_object_proxy-1.11.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0613116156801ab3fccb9e2b05ed83b08ea08c2517fdc6c6bc0d4697a1a376e3", size = 28777, upload-time = "2025-04-16T16:53:41.371Z" },
+ { url = "https://files.pythonhosted.org/packages/39/d0/7e967689e24de8ea6368ec33295f9abc94b9f3f0cd4571bfe148dc432190/lazy_object_proxy-1.11.0-cp313-cp313t-win_amd64.whl", hash = "sha256:bb03c507d96b65f617a6337dedd604399d35face2cdf01526b913fb50c4cb6e8", size = 29598, upload-time = "2025-04-16T16:53:42.513Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/1e/fb441c07b6662ec1fc92b249225ba6e6e5221b05623cb0131d082f782edc/lazy_object_proxy-1.11.0-py3-none-any.whl", hash = "sha256:a56a5093d433341ff7da0e89f9b486031ccd222ec8e52ec84d0ec1cdc819674b", size = 16635, upload-time = "2025-04-16T16:53:47.198Z" },
+]
+
+[[package]]
+name = "markdown-it-py"
+version = "3.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "mdurl" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" },
+]
+
+[[package]]
+name = "markupsafe"
+version = "3.0.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274, upload-time = "2024-10-18T15:21:13.777Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348, upload-time = "2024-10-18T15:21:14.822Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149, upload-time = "2024-10-18T15:21:15.642Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118, upload-time = "2024-10-18T15:21:17.133Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993, upload-time = "2024-10-18T15:21:18.064Z" },
+ { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178, upload-time = "2024-10-18T15:21:18.859Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319, upload-time = "2024-10-18T15:21:19.671Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352, upload-time = "2024-10-18T15:21:20.971Z" },
+ { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097, upload-time = "2024-10-18T15:21:22.646Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601, upload-time = "2024-10-18T15:21:23.499Z" },
+ { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" },
+ { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" },
+ { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" },
+ { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" },
+ { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" },
+ { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" },
+]
+
+[[package]]
+name = "mcp"
+version = "1.12.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "httpx" },
+ { name = "httpx-sse" },
+ { name = "jsonschema" },
+ { name = "pydantic" },
+ { name = "pydantic-settings" },
+ { name = "python-multipart" },
+ { name = "pywin32", marker = "sys_platform == 'win32'" },
+ { name = "sse-starlette" },
+ { name = "starlette" },
+ { name = "uvicorn", marker = "sys_platform != 'emscripten'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/4d/19/9955e2df5384ff5dd25d38f8e88aaf89d2d3d9d39f27e7383eaf0b293836/mcp-1.12.3.tar.gz", hash = "sha256:ab2e05f5e5c13e1dc90a4a9ef23ac500a6121362a564447855ef0ab643a99fed", size = 427203, upload-time = "2025-07-31T18:36:36.795Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8f/8b/0be74e3308a486f1d127f3f6767de5f9f76454c9b4183210c61cc50999b6/mcp-1.12.3-py3-none-any.whl", hash = "sha256:5483345bf39033b858920a5b6348a303acacf45b23936972160ff152107b850e", size = 158810, upload-time = "2025-07-31T18:36:34.915Z" },
+]
+
+[package.optional-dependencies]
+cli = [
+ { name = "python-dotenv" },
+ { name = "typer" },
+]
+
+[[package]]
+name = "mdurl"
+version = "0.1.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
+]
+
+[[package]]
+name = "more-itertools"
+version = "10.7.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ce/a0/834b0cebabbfc7e311f30b46c8188790a37f89fc8d756660346fe5abfd09/more_itertools-10.7.0.tar.gz", hash = "sha256:9fddd5403be01a94b204faadcff459ec3568cf110265d3c54323e1e866ad29d3", size = 127671, upload-time = "2025-04-22T14:17:41.838Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2b/9f/7ba6f94fc1e9ac3d2b853fdff3035fb2fa5afbed898c4a72b8a020610594/more_itertools-10.7.0-py3-none-any.whl", hash = "sha256:d43980384673cb07d2f7d2d918c616b30c659c089ee23953f601d6609c67510e", size = 65278, upload-time = "2025-04-22T14:17:40.49Z" },
+]
+
+[[package]]
+name = "openapi-core"
+version = "0.19.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "isodate" },
+ { name = "jsonschema" },
+ { name = "jsonschema-path" },
+ { name = "more-itertools" },
+ { name = "openapi-schema-validator" },
+ { name = "openapi-spec-validator" },
+ { name = "parse" },
+ { name = "typing-extensions" },
+ { name = "werkzeug" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b1/35/1acaa5f2fcc6e54eded34a2ec74b479439c4e469fc4e8d0e803fda0234db/openapi_core-0.19.5.tar.gz", hash = "sha256:421e753da56c391704454e66afe4803a290108590ac8fa6f4a4487f4ec11f2d3", size = 103264, upload-time = "2025-03-20T20:17:28.193Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/27/6f/83ead0e2e30a90445ee4fc0135f43741aebc30cca5b43f20968b603e30b6/openapi_core-0.19.5-py3-none-any.whl", hash = "sha256:ef7210e83a59394f46ce282639d8d26ad6fc8094aa904c9c16eb1bac8908911f", size = 106595, upload-time = "2025-03-20T20:17:26.77Z" },
+]
+
+[[package]]
+name = "openapi-pydantic"
+version = "0.5.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pydantic" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/02/2e/58d83848dd1a79cb92ed8e63f6ba901ca282c5f09d04af9423ec26c56fd7/openapi_pydantic-0.5.1.tar.gz", hash = "sha256:ff6835af6bde7a459fb93eb93bb92b8749b754fc6e51b2f1590a19dc3005ee0d", size = 60892, upload-time = "2025-01-08T19:29:27.083Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/12/cf/03675d8bd8ecbf4445504d8071adab19f5f993676795708e36402ab38263/openapi_pydantic-0.5.1-py3-none-any.whl", hash = "sha256:a3a09ef4586f5bd760a8df7f43028b60cafb6d9f61de2acba9574766255ab146", size = 96381, upload-time = "2025-01-08T19:29:25.275Z" },
+]
+
+[[package]]
+name = "openapi-schema-validator"
+version = "0.6.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "jsonschema" },
+ { name = "jsonschema-specifications" },
+ { name = "rfc3339-validator" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/8b/f3/5507ad3325169347cd8ced61c232ff3df70e2b250c49f0fe140edb4973c6/openapi_schema_validator-0.6.3.tar.gz", hash = "sha256:f37bace4fc2a5d96692f4f8b31dc0f8d7400fd04f3a937798eaf880d425de6ee", size = 11550, upload-time = "2025-01-10T18:08:22.268Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/21/c6/ad0fba32775ae749016829dace42ed80f4407b171da41313d1a3a5f102e4/openapi_schema_validator-0.6.3-py3-none-any.whl", hash = "sha256:f3b9870f4e556b5a62a1c39da72a6b4b16f3ad9c73dc80084b1b11e74ba148a3", size = 8755, upload-time = "2025-01-10T18:08:19.758Z" },
+]
+
+[[package]]
+name = "openapi-spec-validator"
+version = "0.7.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "jsonschema" },
+ { name = "jsonschema-path" },
+ { name = "lazy-object-proxy" },
+ { name = "openapi-schema-validator" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/82/af/fe2d7618d6eae6fb3a82766a44ed87cd8d6d82b4564ed1c7cfb0f6378e91/openapi_spec_validator-0.7.2.tar.gz", hash = "sha256:cc029309b5c5dbc7859df0372d55e9d1ff43e96d678b9ba087f7c56fc586f734", size = 36855, upload-time = "2025-06-07T14:48:56.299Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/27/dd/b3fd642260cb17532f66cc1e8250f3507d1e580483e209dc1e9d13bd980d/openapi_spec_validator-0.7.2-py3-none-any.whl", hash = "sha256:4bbdc0894ec85f1d1bea1d6d9c8b2c3c8d7ccaa13577ef40da9c006c9fd0eb60", size = 39713, upload-time = "2025-06-07T14:48:54.077Z" },
+]
+
+[[package]]
+name = "parse"
+version = "1.20.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/4f/78/d9b09ba24bb36ef8b83b71be547e118d46214735b6dfb39e4bfde0e9b9dd/parse-1.20.2.tar.gz", hash = "sha256:b41d604d16503c79d81af5165155c0b20f6c8d6c559efa66b4b695c3e5a0a0ce", size = 29391, upload-time = "2024-06-11T04:41:57.34Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d0/31/ba45bf0b2aa7898d81cbbfac0e88c267befb59ad91a19e36e1bc5578ddb1/parse-1.20.2-py2.py3-none-any.whl", hash = "sha256:967095588cb802add9177d0c0b6133b5ba33b1ea9007ca800e526f42a85af558", size = 20126, upload-time = "2024-06-11T04:41:55.057Z" },
+]
+
+[[package]]
+name = "pathable"
+version = "0.4.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/67/93/8f2c2075b180c12c1e9f6a09d1a985bc2036906b13dff1d8917e395f2048/pathable-0.4.4.tar.gz", hash = "sha256:6905a3cd17804edfac7875b5f6c9142a218c7caef78693c2dbbbfbac186d88b2", size = 8124, upload-time = "2025-01-10T18:43:13.247Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7d/eb/b6260b31b1a96386c0a880edebe26f89669098acea8e0318bff6adb378fd/pathable-0.4.4-py3-none-any.whl", hash = "sha256:5ae9e94793b6ef5a4cbe0a7ce9dbbefc1eec38df253763fd0aeeacf2762dbbc2", size = 9592, upload-time = "2025-01-10T18:43:11.88Z" },
+]
+
+[[package]]
+name = "psycopg2-binary"
+version = "2.9.11"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ac/6c/8767aaa597ba424643dc87348c6f1754dd9f48e80fdc1b9f7ca5c3a7c213/psycopg2-binary-2.9.11.tar.gz", hash = "sha256:b6aed9e096bf63f9e75edf2581aa9a7e7186d97ab5c177aa6c87797cd591236c", size = 379620, upload-time = "2025-10-10T11:14:48.041Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d8/91/f870a02f51be4a65987b45a7de4c2e1897dd0d01051e2b559a38fa634e3e/psycopg2_binary-2.9.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:be9b840ac0525a283a96b556616f5b4820e0526addb8dcf6525a0fa162730be4", size = 3756603, upload-time = "2025-10-10T11:11:52.213Z" },
+ { url = "https://files.pythonhosted.org/packages/27/fa/cae40e06849b6c9a95eb5c04d419942f00d9eaac8d81626107461e268821/psycopg2_binary-2.9.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f090b7ddd13ca842ebfe301cd587a76a4cf0913b1e429eb92c1be5dbeb1a19bc", size = 3864509, upload-time = "2025-10-10T11:11:56.452Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/75/364847b879eb630b3ac8293798e380e441a957c53657995053c5ec39a316/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ab8905b5dcb05bf3fb22e0cf90e10f469563486ffb6a96569e51f897c750a76a", size = 4411159, upload-time = "2025-10-10T11:12:00.49Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/a0/567f7ea38b6e1c62aafd58375665a547c00c608a471620c0edc364733e13/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf940cd7e7fec19181fdbc29d76911741153d51cab52e5c21165f3262125685e", size = 4468234, upload-time = "2025-10-10T11:12:04.892Z" },
+ { url = "https://files.pythonhosted.org/packages/30/da/4e42788fb811bbbfd7b7f045570c062f49e350e1d1f3df056c3fb5763353/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa0f693d3c68ae925966f0b14b8edda71696608039f4ed61b1fe9ffa468d16db", size = 4166236, upload-time = "2025-10-10T11:12:11.674Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/94/c1777c355bc560992af848d98216148be5f1be001af06e06fc49cbded578/psycopg2_binary-2.9.11-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a1cf393f1cdaf6a9b57c0a719a1068ba1069f022a59b8b1fe44b006745b59757", size = 3983083, upload-time = "2025-10-30T02:55:15.73Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/42/c9a21edf0e3daa7825ed04a4a8588686c6c14904344344a039556d78aa58/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7a6beb4beaa62f88592ccc65df20328029d721db309cb3250b0aae0fa146c3", size = 3652281, upload-time = "2025-10-10T11:12:17.713Z" },
+ { url = "https://files.pythonhosted.org/packages/12/22/dedfbcfa97917982301496b6b5e5e6c5531d1f35dd2b488b08d1ebc52482/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:31b32c457a6025e74d233957cc9736742ac5a6cb196c6b68499f6bb51390bd6a", size = 3298010, upload-time = "2025-10-10T11:12:22.671Z" },
+ { url = "https://files.pythonhosted.org/packages/66/ea/d3390e6696276078bd01b2ece417deac954dfdd552d2edc3d03204416c0c/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:edcb3aeb11cb4bf13a2af3c53a15b3d612edeb6409047ea0b5d6a21a9d744b34", size = 3044641, upload-time = "2025-10-30T02:55:19.929Z" },
+ { url = "https://files.pythonhosted.org/packages/12/9a/0402ded6cbd321da0c0ba7d34dc12b29b14f5764c2fc10750daa38e825fc/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b6d93d7c0b61a1dd6197d208ab613eb7dcfdcca0a49c42ceb082257991de9d", size = 3347940, upload-time = "2025-10-10T11:12:26.529Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/d2/99b55e85832ccde77b211738ff3925a5d73ad183c0b37bcbbe5a8ff04978/psycopg2_binary-2.9.11-cp312-cp312-win_amd64.whl", hash = "sha256:b33fabeb1fde21180479b2d4667e994de7bbf0eec22832ba5d9b5e4cf65b6c6d", size = 2714147, upload-time = "2025-10-10T11:12:29.535Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/a8/a2709681b3ac11b0b1786def10006b8995125ba268c9a54bea6f5ae8bd3e/psycopg2_binary-2.9.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b8fb3db325435d34235b044b199e56cdf9ff41223a4b9752e8576465170bb38c", size = 3756572, upload-time = "2025-10-10T11:12:32.873Z" },
+ { url = "https://files.pythonhosted.org/packages/62/e1/c2b38d256d0dafd32713e9f31982a5b028f4a3651f446be70785f484f472/psycopg2_binary-2.9.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:366df99e710a2acd90efed3764bb1e28df6c675d33a7fb40df9b7281694432ee", size = 3864529, upload-time = "2025-10-10T11:12:36.791Z" },
+ { url = "https://files.pythonhosted.org/packages/11/32/b2ffe8f3853c181e88f0a157c5fb4e383102238d73c52ac6d93a5c8bffe6/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c55b385daa2f92cb64b12ec4536c66954ac53654c7f15a203578da4e78105c0", size = 4411242, upload-time = "2025-10-10T11:12:42.388Z" },
+ { url = "https://files.pythonhosted.org/packages/10/04/6ca7477e6160ae258dc96f67c371157776564679aefd247b66f4661501a2/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c0377174bf1dd416993d16edc15357f6eb17ac998244cca19bc67cdc0e2e5766", size = 4468258, upload-time = "2025-10-10T11:12:48.654Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/7e/6a1a38f86412df101435809f225d57c1a021307dd0689f7a5e7fe83588b1/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c6ff3335ce08c75afaed19e08699e8aacf95d4a260b495a4a8545244fe2ceb3", size = 4166295, upload-time = "2025-10-10T11:12:52.525Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/7d/c07374c501b45f3579a9eb761cbf2604ddef3d96ad48679112c2c5aa9c25/psycopg2_binary-2.9.11-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:84011ba3109e06ac412f95399b704d3d6950e386b7994475b231cf61eec2fc1f", size = 3983133, upload-time = "2025-10-30T02:55:24.329Z" },
+ { url = "https://files.pythonhosted.org/packages/82/56/993b7104cb8345ad7d4516538ccf8f0d0ac640b1ebd8c754a7b024e76878/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ba34475ceb08cccbdd98f6b46916917ae6eeb92b5ae111df10b544c3a4621dc4", size = 3652383, upload-time = "2025-10-10T11:12:56.387Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/ac/eaeb6029362fd8d454a27374d84c6866c82c33bfc24587b4face5a8e43ef/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b31e90fdd0f968c2de3b26ab014314fe814225b6c324f770952f7d38abf17e3c", size = 3298168, upload-time = "2025-10-10T11:13:00.403Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/39/50c3facc66bded9ada5cbc0de867499a703dc6bca6be03070b4e3b65da6c/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d526864e0f67f74937a8fce859bd56c979f5e2ec57ca7c627f5f1071ef7fee60", size = 3044712, upload-time = "2025-10-30T02:55:27.975Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/8e/b7de019a1f562f72ada81081a12823d3c1590bedc48d7d2559410a2763fe/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04195548662fa544626c8ea0f06561eb6203f1984ba5b4562764fbeb4c3d14b1", size = 3347549, upload-time = "2025-10-10T11:13:03.971Z" },
+ { url = "https://files.pythonhosted.org/packages/80/2d/1bb683f64737bbb1f86c82b7359db1eb2be4e2c0c13b947f80efefa7d3e5/psycopg2_binary-2.9.11-cp313-cp313-win_amd64.whl", hash = "sha256:efff12b432179443f54e230fdf60de1f6cc726b6c832db8701227d089310e8aa", size = 2714215, upload-time = "2025-10-10T11:13:07.14Z" },
+ { url = "https://files.pythonhosted.org/packages/64/12/93ef0098590cf51d9732b4f139533732565704f45bdc1ffa741b7c95fb54/psycopg2_binary-2.9.11-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:92e3b669236327083a2e33ccfa0d320dd01b9803b3e14dd986a4fc54aa00f4e1", size = 3756567, upload-time = "2025-10-10T11:13:11.885Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/a9/9d55c614a891288f15ca4b5209b09f0f01e3124056924e17b81b9fa054cc/psycopg2_binary-2.9.11-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e0deeb03da539fa3577fcb0b3f2554a97f7e5477c246098dbb18091a4a01c16f", size = 3864755, upload-time = "2025-10-10T11:13:17.727Z" },
+ { url = "https://files.pythonhosted.org/packages/13/1e/98874ce72fd29cbde93209977b196a2edae03f8490d1bd8158e7f1daf3a0/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9b52a3f9bb540a3e4ec0f6ba6d31339727b2950c9772850d6545b7eae0b9d7c5", size = 4411646, upload-time = "2025-10-10T11:13:24.432Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/bd/a335ce6645334fb8d758cc358810defca14a1d19ffbc8a10bd38a2328565/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:db4fd476874ccfdbb630a54426964959e58da4c61c9feba73e6094d51303d7d8", size = 4468701, upload-time = "2025-10-10T11:13:29.266Z" },
+ { url = "https://files.pythonhosted.org/packages/44/d6/c8b4f53f34e295e45709b7568bf9b9407a612ea30387d35eb9fa84f269b4/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47f212c1d3be608a12937cc131bd85502954398aaa1320cb4c14421a0ffccf4c", size = 4166293, upload-time = "2025-10-10T11:13:33.336Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/e0/f8cc36eadd1b716ab36bb290618a3292e009867e5c97ce4aba908cb99644/psycopg2_binary-2.9.11-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e35b7abae2b0adab776add56111df1735ccc71406e56203515e228a8dc07089f", size = 3983184, upload-time = "2025-10-30T02:55:32.483Z" },
+ { url = "https://files.pythonhosted.org/packages/53/3e/2a8fe18a4e61cfb3417da67b6318e12691772c0696d79434184a511906dc/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fcf21be3ce5f5659daefd2b3b3b6e4727b028221ddc94e6c1523425579664747", size = 3652650, upload-time = "2025-10-10T11:13:38.181Z" },
+ { url = "https://files.pythonhosted.org/packages/76/36/03801461b31b29fe58d228c24388f999fe814dfc302856e0d17f97d7c54d/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9bd81e64e8de111237737b29d68039b9c813bdf520156af36d26819c9a979e5f", size = 3298663, upload-time = "2025-10-10T11:13:44.878Z" },
+ { url = "https://files.pythonhosted.org/packages/97/77/21b0ea2e1a73aa5fa9222b2a6b8ba325c43c3a8d54272839c991f2345656/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:32770a4d666fbdafab017086655bcddab791d7cb260a16679cc5a7338b64343b", size = 3044737, upload-time = "2025-10-30T02:55:35.69Z" },
+ { url = "https://files.pythonhosted.org/packages/67/69/f36abe5f118c1dca6d3726ceae164b9356985805480731ac6712a63f24f0/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c3cb3a676873d7506825221045bd70e0427c905b9c8ee8d6acd70cfcbd6e576d", size = 3347643, upload-time = "2025-10-10T11:13:53.499Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/36/9c0c326fe3a4227953dfb29f5d0c8ae3b8eb8c1cd2967aa569f50cb3c61f/psycopg2_binary-2.9.11-cp314-cp314-win_amd64.whl", hash = "sha256:4012c9c954dfaccd28f94e84ab9f94e12df76b4afb22331b1f0d3154893a6316", size = 2803913, upload-time = "2025-10-10T11:13:57.058Z" },
+]
+
+[[package]]
+name = "pycparser"
+version = "2.22"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload-time = "2024-03-30T13:22:22.564Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" },
+]
+
+[[package]]
+name = "pydantic"
+version = "2.11.7"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "annotated-types" },
+ { name = "pydantic-core" },
+ { name = "typing-extensions" },
+ { name = "typing-inspection" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350, upload-time = "2025-06-14T08:33:17.137Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782, upload-time = "2025-06-14T08:33:14.905Z" },
+]
+
+[package.optional-dependencies]
+email = [
+ { name = "email-validator" },
+]
+
+[[package]]
+name = "pydantic-core"
+version = "2.33.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload-time = "2025-04-23T18:31:25.863Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload-time = "2025-04-23T18:31:27.341Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload-time = "2025-04-23T18:31:28.956Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload-time = "2025-04-23T18:31:31.025Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload-time = "2025-04-23T18:31:32.514Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload-time = "2025-04-23T18:31:33.958Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload-time = "2025-04-23T18:31:39.095Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload-time = "2025-04-23T18:31:41.034Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload-time = "2025-04-23T18:31:42.757Z" },
+ { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload-time = "2025-04-23T18:31:44.304Z" },
+ { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload-time = "2025-04-23T18:31:45.891Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" },
+ { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" },
+ { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" },
+ { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" },
+ { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" },
+ { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" },
+ { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" },
+ { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" },
+]
+
+[[package]]
+name = "pydantic-settings"
+version = "2.10.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pydantic" },
+ { name = "python-dotenv" },
+ { name = "typing-inspection" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/68/85/1ea668bbab3c50071ca613c6ab30047fb36ab0da1b92fa8f17bbc38fd36c/pydantic_settings-2.10.1.tar.gz", hash = "sha256:06f0062169818d0f5524420a360d632d5857b83cffd4d42fe29597807a1614ee", size = 172583, upload-time = "2025-06-24T13:26:46.841Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/58/f0/427018098906416f580e3cf1366d3b1abfb408a0652e9f31600c24a1903c/pydantic_settings-2.10.1-py3-none-any.whl", hash = "sha256:a60952460b99cf661dc25c29c0ef171721f98bfcb52ef8d9ea4c943d7c8cc796", size = 45235, upload-time = "2025-06-24T13:26:45.485Z" },
+]
+
+[[package]]
+name = "pygments"
+version = "2.19.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
+]
+
+[[package]]
+name = "pyperclip"
+version = "1.9.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/30/23/2f0a3efc4d6a32f3b63cdff36cd398d9701d26cda58e3ab97ac79fb5e60d/pyperclip-1.9.0.tar.gz", hash = "sha256:b7de0142ddc81bfc5c7507eea19da920b92252b548b96186caf94a5e2527d310", size = 20961, upload-time = "2024-06-18T20:38:48.401Z" }
+
+[[package]]
+name = "python-dotenv"
+version = "1.1.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload-time = "2025-06-24T04:21:07.341Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" },
+]
+
+[[package]]
+name = "python-multipart"
+version = "0.0.20"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" },
+]
+
+[[package]]
+name = "pywin32"
+version = "311"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" },
+ { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" },
+ { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" },
+]
+
+[[package]]
+name = "pyyaml"
+version = "6.0.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" },
+ { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" },
+ { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" },
+ { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" },
+ { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" },
+ { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" },
+]
+
+[[package]]
+name = "referencing"
+version = "0.36.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "attrs" },
+ { name = "rpds-py" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744, upload-time = "2025-01-25T08:48:16.138Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775, upload-time = "2025-01-25T08:48:14.241Z" },
+]
+
+[[package]]
+name = "requests"
+version = "2.32.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "charset-normalizer" },
+ { name = "idna" },
+ { name = "urllib3" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" },
+]
+
+[[package]]
+name = "rfc3339-validator"
+version = "0.1.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "six" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/28/ea/a9387748e2d111c3c2b275ba970b735e04e15cdb1eb30693b6b5708c4dbd/rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b", size = 5513, upload-time = "2021-05-12T16:37:54.178Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa", size = 3490, upload-time = "2021-05-12T16:37:52.536Z" },
+]
+
+[[package]]
+name = "rich"
+version = "14.1.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markdown-it-py" },
+ { name = "pygments" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/fe/75/af448d8e52bf1d8fa6a9d089ca6c07ff4453d86c65c145d0a300bb073b9b/rich-14.1.0.tar.gz", hash = "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8", size = 224441, upload-time = "2025-07-25T07:32:58.125Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e3/30/3c4d035596d3cf444529e0b2953ad0466f6049528a879d27534700580395/rich-14.1.0-py3-none-any.whl", hash = "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f", size = 243368, upload-time = "2025-07-25T07:32:56.73Z" },
+]
+
+[[package]]
+name = "rich-rst"
+version = "1.3.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "docutils" },
+ { name = "rich" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b0/69/5514c3a87b5f10f09a34bb011bc0927bc12c596c8dae5915604e71abc386/rich_rst-1.3.1.tar.gz", hash = "sha256:fad46e3ba42785ea8c1785e2ceaa56e0ffa32dbe5410dec432f37e4107c4f383", size = 13839, upload-time = "2024-04-30T04:40:38.125Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fd/bc/cc4e3dbc5e7992398dcb7a8eda0cbcf4fb792a0cdb93f857b478bf3cf884/rich_rst-1.3.1-py3-none-any.whl", hash = "sha256:498a74e3896507ab04492d326e794c3ef76e7cda078703aa592d1853d91098c1", size = 11621, upload-time = "2024-04-30T04:40:32.619Z" },
+]
+
+[[package]]
+name = "rpds-py"
+version = "0.26.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a5/aa/4456d84bbb54adc6a916fb10c9b374f78ac840337644e4a5eda229c81275/rpds_py-0.26.0.tar.gz", hash = "sha256:20dae58a859b0906f0685642e591056f1e787f3a8b39c8e8749a45dc7d26bdb0", size = 27385, upload-time = "2025-07-01T15:57:13.958Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ea/86/90eb87c6f87085868bd077c7a9938006eb1ce19ed4d06944a90d3560fce2/rpds_py-0.26.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:894514d47e012e794f1350f076c427d2347ebf82f9b958d554d12819849a369d", size = 363933, upload-time = "2025-07-01T15:54:15.734Z" },
+ { url = "https://files.pythonhosted.org/packages/63/78/4469f24d34636242c924626082b9586f064ada0b5dbb1e9d096ee7a8e0c6/rpds_py-0.26.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc921b96fa95a097add244da36a1d9e4f3039160d1d30f1b35837bf108c21136", size = 350447, upload-time = "2025-07-01T15:54:16.922Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/91/c448ed45efdfdade82348d5e7995e15612754826ea640afc20915119734f/rpds_py-0.26.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e1157659470aa42a75448b6e943c895be8c70531c43cb78b9ba990778955582", size = 384711, upload-time = "2025-07-01T15:54:18.101Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/43/e5c86fef4be7f49828bdd4ecc8931f0287b1152c0bb0163049b3218740e7/rpds_py-0.26.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:521ccf56f45bb3a791182dc6b88ae5f8fa079dd705ee42138c76deb1238e554e", size = 400865, upload-time = "2025-07-01T15:54:19.295Z" },
+ { url = "https://files.pythonhosted.org/packages/55/34/e00f726a4d44f22d5c5fe2e5ddd3ac3d7fd3f74a175607781fbdd06fe375/rpds_py-0.26.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9def736773fd56b305c0eef698be5192c77bfa30d55a0e5885f80126c4831a15", size = 517763, upload-time = "2025-07-01T15:54:20.858Z" },
+ { url = "https://files.pythonhosted.org/packages/52/1c/52dc20c31b147af724b16104500fba13e60123ea0334beba7b40e33354b4/rpds_py-0.26.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cdad4ea3b4513b475e027be79e5a0ceac8ee1c113a1a11e5edc3c30c29f964d8", size = 406651, upload-time = "2025-07-01T15:54:22.508Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/77/87d7bfabfc4e821caa35481a2ff6ae0b73e6a391bb6b343db2c91c2b9844/rpds_py-0.26.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82b165b07f416bdccf5c84546a484cc8f15137ca38325403864bfdf2b5b72f6a", size = 386079, upload-time = "2025-07-01T15:54:23.987Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/d4/7f2200c2d3ee145b65b3cddc4310d51f7da6a26634f3ac87125fd789152a/rpds_py-0.26.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d04cab0a54b9dba4d278fe955a1390da3cf71f57feb78ddc7cb67cbe0bd30323", size = 421379, upload-time = "2025-07-01T15:54:25.073Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/13/9fdd428b9c820869924ab62236b8688b122baa22d23efdd1c566938a39ba/rpds_py-0.26.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:79061ba1a11b6a12743a2b0f72a46aa2758613d454aa6ba4f5a265cc48850158", size = 562033, upload-time = "2025-07-01T15:54:26.225Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/e1/b69686c3bcbe775abac3a4c1c30a164a2076d28df7926041f6c0eb5e8d28/rpds_py-0.26.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f405c93675d8d4c5ac87364bb38d06c988e11028a64b52a47158a355079661f3", size = 591639, upload-time = "2025-07-01T15:54:27.424Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/c9/1e3d8c8863c84a90197ac577bbc3d796a92502124c27092413426f670990/rpds_py-0.26.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dafd4c44b74aa4bed4b250f1aed165b8ef5de743bcca3b88fc9619b6087093d2", size = 557105, upload-time = "2025-07-01T15:54:29.93Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/c5/90c569649057622959f6dcc40f7b516539608a414dfd54b8d77e3b201ac0/rpds_py-0.26.0-cp312-cp312-win32.whl", hash = "sha256:3da5852aad63fa0c6f836f3359647870e21ea96cf433eb393ffa45263a170d44", size = 223272, upload-time = "2025-07-01T15:54:31.128Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/16/19f5d9f2a556cfed454eebe4d354c38d51c20f3db69e7b4ce6cff904905d/rpds_py-0.26.0-cp312-cp312-win_amd64.whl", hash = "sha256:cf47cfdabc2194a669dcf7a8dbba62e37a04c5041d2125fae0233b720da6f05c", size = 234995, upload-time = "2025-07-01T15:54:32.195Z" },
+ { url = "https://files.pythonhosted.org/packages/83/f0/7935e40b529c0e752dfaa7880224771b51175fce08b41ab4a92eb2fbdc7f/rpds_py-0.26.0-cp312-cp312-win_arm64.whl", hash = "sha256:20ab1ae4fa534f73647aad289003f1104092890849e0266271351922ed5574f8", size = 223198, upload-time = "2025-07-01T15:54:33.271Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/67/bb62d0109493b12b1c6ab00de7a5566aa84c0e44217c2d94bee1bd370da9/rpds_py-0.26.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:696764a5be111b036256c0b18cd29783fab22154690fc698062fc1b0084b511d", size = 363917, upload-time = "2025-07-01T15:54:34.755Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/f3/34e6ae1925a5706c0f002a8d2d7f172373b855768149796af87bd65dcdb9/rpds_py-0.26.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1e6c15d2080a63aaed876e228efe4f814bc7889c63b1e112ad46fdc8b368b9e1", size = 350073, upload-time = "2025-07-01T15:54:36.292Z" },
+ { url = "https://files.pythonhosted.org/packages/75/83/1953a9d4f4e4de7fd0533733e041c28135f3c21485faaef56a8aadbd96b5/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:390e3170babf42462739a93321e657444f0862c6d722a291accc46f9d21ed04e", size = 384214, upload-time = "2025-07-01T15:54:37.469Z" },
+ { url = "https://files.pythonhosted.org/packages/48/0e/983ed1b792b3322ea1d065e67f4b230f3b96025f5ce3878cc40af09b7533/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7da84c2c74c0f5bc97d853d9e17bb83e2dcafcff0dc48286916001cc114379a1", size = 400113, upload-time = "2025-07-01T15:54:38.954Z" },
+ { url = "https://files.pythonhosted.org/packages/69/7f/36c0925fff6f660a80be259c5b4f5e53a16851f946eb080351d057698528/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c5fe114a6dd480a510b6d3661d09d67d1622c4bf20660a474507aaee7eeeee9", size = 515189, upload-time = "2025-07-01T15:54:40.57Z" },
+ { url = "https://files.pythonhosted.org/packages/13/45/cbf07fc03ba7a9b54662c9badb58294ecfb24f828b9732970bd1a431ed5c/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3100b3090269f3a7ea727b06a6080d4eb7439dca4c0e91a07c5d133bb1727ea7", size = 406998, upload-time = "2025-07-01T15:54:43.025Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/b0/8fa5e36e58657997873fd6a1cf621285ca822ca75b4b3434ead047daa307/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c03c9b0c64afd0320ae57de4c982801271c0c211aa2d37f3003ff5feb75bb04", size = 385903, upload-time = "2025-07-01T15:54:44.752Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/f7/b25437772f9f57d7a9fbd73ed86d0dcd76b4c7c6998348c070d90f23e315/rpds_py-0.26.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5963b72ccd199ade6ee493723d18a3f21ba7d5b957017607f815788cef50eaf1", size = 419785, upload-time = "2025-07-01T15:54:46.043Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/6b/63ffa55743dfcb4baf2e9e77a0b11f7f97ed96a54558fcb5717a4b2cd732/rpds_py-0.26.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9da4e873860ad5bab3291438525cae80169daecbfafe5657f7f5fb4d6b3f96b9", size = 561329, upload-time = "2025-07-01T15:54:47.64Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/07/1f4f5e2886c480a2346b1e6759c00278b8a69e697ae952d82ae2e6ee5db0/rpds_py-0.26.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5afaddaa8e8c7f1f7b4c5c725c0070b6eed0228f705b90a1732a48e84350f4e9", size = 590875, upload-time = "2025-07-01T15:54:48.9Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/bc/e6639f1b91c3a55f8c41b47d73e6307051b6e246254a827ede730624c0f8/rpds_py-0.26.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4916dc96489616a6f9667e7526af8fa693c0fdb4f3acb0e5d9f4400eb06a47ba", size = 556636, upload-time = "2025-07-01T15:54:50.619Z" },
+ { url = "https://files.pythonhosted.org/packages/05/4c/b3917c45566f9f9a209d38d9b54a1833f2bb1032a3e04c66f75726f28876/rpds_py-0.26.0-cp313-cp313-win32.whl", hash = "sha256:2a343f91b17097c546b93f7999976fd6c9d5900617aa848c81d794e062ab302b", size = 222663, upload-time = "2025-07-01T15:54:52.023Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/0b/0851bdd6025775aaa2365bb8de0697ee2558184c800bfef8d7aef5ccde58/rpds_py-0.26.0-cp313-cp313-win_amd64.whl", hash = "sha256:0a0b60701f2300c81b2ac88a5fb893ccfa408e1c4a555a77f908a2596eb875a5", size = 234428, upload-time = "2025-07-01T15:54:53.692Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/e8/a47c64ed53149c75fb581e14a237b7b7cd18217e969c30d474d335105622/rpds_py-0.26.0-cp313-cp313-win_arm64.whl", hash = "sha256:257d011919f133a4746958257f2c75238e3ff54255acd5e3e11f3ff41fd14256", size = 222571, upload-time = "2025-07-01T15:54:54.822Z" },
+ { url = "https://files.pythonhosted.org/packages/89/bf/3d970ba2e2bcd17d2912cb42874107390f72873e38e79267224110de5e61/rpds_py-0.26.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:529c8156d7506fba5740e05da8795688f87119cce330c244519cf706a4a3d618", size = 360475, upload-time = "2025-07-01T15:54:56.228Z" },
+ { url = "https://files.pythonhosted.org/packages/82/9f/283e7e2979fc4ec2d8ecee506d5a3675fce5ed9b4b7cb387ea5d37c2f18d/rpds_py-0.26.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f53ec51f9d24e9638a40cabb95078ade8c99251945dad8d57bf4aabe86ecee35", size = 346692, upload-time = "2025-07-01T15:54:58.561Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/03/7e50423c04d78daf391da3cc4330bdb97042fc192a58b186f2d5deb7befd/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab504c4d654e4a29558eaa5bb8cea5fdc1703ea60a8099ffd9c758472cf913f", size = 379415, upload-time = "2025-07-01T15:54:59.751Z" },
+ { url = "https://files.pythonhosted.org/packages/57/00/d11ee60d4d3b16808432417951c63df803afb0e0fc672b5e8d07e9edaaae/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd0641abca296bc1a00183fe44f7fced8807ed49d501f188faa642d0e4975b83", size = 391783, upload-time = "2025-07-01T15:55:00.898Z" },
+ { url = "https://files.pythonhosted.org/packages/08/b3/1069c394d9c0d6d23c5b522e1f6546b65793a22950f6e0210adcc6f97c3e/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69b312fecc1d017b5327afa81d4da1480f51c68810963a7336d92203dbb3d4f1", size = 512844, upload-time = "2025-07-01T15:55:02.201Z" },
+ { url = "https://files.pythonhosted.org/packages/08/3b/c4fbf0926800ed70b2c245ceca99c49f066456755f5d6eb8863c2c51e6d0/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c741107203954f6fc34d3066d213d0a0c40f7bb5aafd698fb39888af277c70d8", size = 402105, upload-time = "2025-07-01T15:55:03.698Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/b0/db69b52ca07413e568dae9dc674627a22297abb144c4d6022c6d78f1e5cc/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc3e55a7db08dc9a6ed5fb7103019d2c1a38a349ac41901f9f66d7f95750942f", size = 383440, upload-time = "2025-07-01T15:55:05.398Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/e1/c65255ad5b63903e56b3bb3ff9dcc3f4f5c3badde5d08c741ee03903e951/rpds_py-0.26.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9e851920caab2dbcae311fd28f4313c6953993893eb5c1bb367ec69d9a39e7ed", size = 412759, upload-time = "2025-07-01T15:55:08.316Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/22/bb731077872377a93c6e93b8a9487d0406c70208985831034ccdeed39c8e/rpds_py-0.26.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dfbf280da5f876d0b00c81f26bedce274e72a678c28845453885a9b3c22ae632", size = 556032, upload-time = "2025-07-01T15:55:09.52Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/8b/393322ce7bac5c4530fb96fc79cc9ea2f83e968ff5f6e873f905c493e1c4/rpds_py-0.26.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:1cc81d14ddfa53d7f3906694d35d54d9d3f850ef8e4e99ee68bc0d1e5fed9a9c", size = 585416, upload-time = "2025-07-01T15:55:11.216Z" },
+ { url = "https://files.pythonhosted.org/packages/49/ae/769dc372211835bf759319a7aae70525c6eb523e3371842c65b7ef41c9c6/rpds_py-0.26.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dca83c498b4650a91efcf7b88d669b170256bf8017a5db6f3e06c2bf031f57e0", size = 554049, upload-time = "2025-07-01T15:55:13.004Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/f9/4c43f9cc203d6ba44ce3146246cdc38619d92c7bd7bad4946a3491bd5b70/rpds_py-0.26.0-cp313-cp313t-win32.whl", hash = "sha256:4d11382bcaf12f80b51d790dee295c56a159633a8e81e6323b16e55d81ae37e9", size = 218428, upload-time = "2025-07-01T15:55:14.486Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/8b/9286b7e822036a4a977f2f1e851c7345c20528dbd56b687bb67ed68a8ede/rpds_py-0.26.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff110acded3c22c033e637dd8896e411c7d3a11289b2edf041f86663dbc791e9", size = 231524, upload-time = "2025-07-01T15:55:15.745Z" },
+ { url = "https://files.pythonhosted.org/packages/55/07/029b7c45db910c74e182de626dfdae0ad489a949d84a468465cd0ca36355/rpds_py-0.26.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:da619979df60a940cd434084355c514c25cf8eb4cf9a508510682f6c851a4f7a", size = 364292, upload-time = "2025-07-01T15:55:17.001Z" },
+ { url = "https://files.pythonhosted.org/packages/13/d1/9b3d3f986216b4d1f584878dca15ce4797aaf5d372d738974ba737bf68d6/rpds_py-0.26.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ea89a2458a1a75f87caabefe789c87539ea4e43b40f18cff526052e35bbb4fdf", size = 350334, upload-time = "2025-07-01T15:55:18.922Z" },
+ { url = "https://files.pythonhosted.org/packages/18/98/16d5e7bc9ec715fa9668731d0cf97f6b032724e61696e2db3d47aeb89214/rpds_py-0.26.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feac1045b3327a45944e7dcbeb57530339f6b17baff154df51ef8b0da34c8c12", size = 384875, upload-time = "2025-07-01T15:55:20.399Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/13/aa5e2b1ec5ab0e86a5c464d53514c0467bec6ba2507027d35fc81818358e/rpds_py-0.26.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b818a592bd69bfe437ee8368603d4a2d928c34cffcdf77c2e761a759ffd17d20", size = 399993, upload-time = "2025-07-01T15:55:21.729Z" },
+ { url = "https://files.pythonhosted.org/packages/17/03/8021810b0e97923abdbab6474c8b77c69bcb4b2c58330777df9ff69dc559/rpds_py-0.26.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a8b0dd8648709b62d9372fc00a57466f5fdeefed666afe3fea5a6c9539a0331", size = 516683, upload-time = "2025-07-01T15:55:22.918Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/b1/da8e61c87c2f3d836954239fdbbfb477bb7b54d74974d8f6fcb34342d166/rpds_py-0.26.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6d3498ad0df07d81112aa6ec6c95a7e7b1ae00929fb73e7ebee0f3faaeabad2f", size = 408825, upload-time = "2025-07-01T15:55:24.207Z" },
+ { url = "https://files.pythonhosted.org/packages/38/bc/1fc173edaaa0e52c94b02a655db20697cb5fa954ad5a8e15a2c784c5cbdd/rpds_py-0.26.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24a4146ccb15be237fdef10f331c568e1b0e505f8c8c9ed5d67759dac58ac246", size = 387292, upload-time = "2025-07-01T15:55:25.554Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/eb/3a9bb4bd90867d21916f253caf4f0d0be7098671b6715ad1cead9fe7bab9/rpds_py-0.26.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a9a63785467b2d73635957d32a4f6e73d5e4df497a16a6392fa066b753e87387", size = 420435, upload-time = "2025-07-01T15:55:27.798Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/16/e066dcdb56f5632713445271a3f8d3d0b426d51ae9c0cca387799df58b02/rpds_py-0.26.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:de4ed93a8c91debfd5a047be327b7cc8b0cc6afe32a716bbbc4aedca9e2a83af", size = 562410, upload-time = "2025-07-01T15:55:29.057Z" },
+ { url = "https://files.pythonhosted.org/packages/60/22/ddbdec7eb82a0dc2e455be44c97c71c232983e21349836ce9f272e8a3c29/rpds_py-0.26.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:caf51943715b12af827696ec395bfa68f090a4c1a1d2509eb4e2cb69abbbdb33", size = 590724, upload-time = "2025-07-01T15:55:30.719Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/b4/95744085e65b7187d83f2fcb0bef70716a1ea0a9e5d8f7f39a86e5d83424/rpds_py-0.26.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4a59e5bc386de021f56337f757301b337d7ab58baa40174fb150accd480bc953", size = 558285, upload-time = "2025-07-01T15:55:31.981Z" },
+ { url = "https://files.pythonhosted.org/packages/37/37/6309a75e464d1da2559446f9c811aa4d16343cebe3dbb73701e63f760caa/rpds_py-0.26.0-cp314-cp314-win32.whl", hash = "sha256:92c8db839367ef16a662478f0a2fe13e15f2227da3c1430a782ad0f6ee009ec9", size = 223459, upload-time = "2025-07-01T15:55:33.312Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/6f/8e9c11214c46098b1d1391b7e02b70bb689ab963db3b19540cba17315291/rpds_py-0.26.0-cp314-cp314-win_amd64.whl", hash = "sha256:b0afb8cdd034150d4d9f53926226ed27ad15b7f465e93d7468caaf5eafae0d37", size = 236083, upload-time = "2025-07-01T15:55:34.933Z" },
+ { url = "https://files.pythonhosted.org/packages/47/af/9c4638994dd623d51c39892edd9d08e8be8220a4b7e874fa02c2d6e91955/rpds_py-0.26.0-cp314-cp314-win_arm64.whl", hash = "sha256:ca3f059f4ba485d90c8dc75cb5ca897e15325e4e609812ce57f896607c1c0867", size = 223291, upload-time = "2025-07-01T15:55:36.202Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/db/669a241144460474aab03e254326b32c42def83eb23458a10d163cb9b5ce/rpds_py-0.26.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:5afea17ab3a126006dc2f293b14ffc7ef3c85336cf451564a0515ed7648033da", size = 361445, upload-time = "2025-07-01T15:55:37.483Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/2d/133f61cc5807c6c2fd086a46df0eb8f63a23f5df8306ff9f6d0fd168fecc/rpds_py-0.26.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:69f0c0a3df7fd3a7eec50a00396104bb9a843ea6d45fcc31c2d5243446ffd7a7", size = 347206, upload-time = "2025-07-01T15:55:38.828Z" },
+ { url = "https://files.pythonhosted.org/packages/05/bf/0e8fb4c05f70273469eecf82f6ccf37248558526a45321644826555db31b/rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:801a71f70f9813e82d2513c9a96532551fce1e278ec0c64610992c49c04c2dad", size = 380330, upload-time = "2025-07-01T15:55:40.175Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/a8/060d24185d8b24d3923322f8d0ede16df4ade226a74e747b8c7c978e3dd3/rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:df52098cde6d5e02fa75c1f6244f07971773adb4a26625edd5c18fee906fa84d", size = 392254, upload-time = "2025-07-01T15:55:42.015Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/7b/7c2e8a9ee3e6bc0bae26bf29f5219955ca2fbb761dca996a83f5d2f773fe/rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9bc596b30f86dc6f0929499c9e574601679d0341a0108c25b9b358a042f51bca", size = 516094, upload-time = "2025-07-01T15:55:43.603Z" },
+ { url = "https://files.pythonhosted.org/packages/75/d6/f61cafbed8ba1499b9af9f1777a2a199cd888f74a96133d8833ce5eaa9c5/rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9dfbe56b299cf5875b68eb6f0ebaadc9cac520a1989cac0db0765abfb3709c19", size = 402889, upload-time = "2025-07-01T15:55:45.275Z" },
+ { url = "https://files.pythonhosted.org/packages/92/19/c8ac0a8a8df2dd30cdec27f69298a5c13e9029500d6d76718130f5e5be10/rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac64f4b2bdb4ea622175c9ab7cf09444e412e22c0e02e906978b3b488af5fde8", size = 384301, upload-time = "2025-07-01T15:55:47.098Z" },
+ { url = "https://files.pythonhosted.org/packages/41/e1/6b1859898bc292a9ce5776016c7312b672da00e25cec74d7beced1027286/rpds_py-0.26.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:181ef9b6bbf9845a264f9aa45c31836e9f3c1f13be565d0d010e964c661d1e2b", size = 412891, upload-time = "2025-07-01T15:55:48.412Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/b9/ceb39af29913c07966a61367b3c08b4f71fad841e32c6b59a129d5974698/rpds_py-0.26.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:49028aa684c144ea502a8e847d23aed5e4c2ef7cadfa7d5eaafcb40864844b7a", size = 557044, upload-time = "2025-07-01T15:55:49.816Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/27/35637b98380731a521f8ec4f3fd94e477964f04f6b2f8f7af8a2d889a4af/rpds_py-0.26.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e5d524d68a474a9688336045bbf76cb0def88549c1b2ad9dbfec1fb7cfbe9170", size = 585774, upload-time = "2025-07-01T15:55:51.192Z" },
+ { url = "https://files.pythonhosted.org/packages/52/d9/3f0f105420fecd18551b678c9a6ce60bd23986098b252a56d35781b3e7e9/rpds_py-0.26.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c1851f429b822831bd2edcbe0cfd12ee9ea77868f8d3daf267b189371671c80e", size = 554886, upload-time = "2025-07-01T15:55:52.541Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/c5/347c056a90dc8dd9bc240a08c527315008e1b5042e7a4cf4ac027be9d38a/rpds_py-0.26.0-cp314-cp314t-win32.whl", hash = "sha256:7bdb17009696214c3b66bb3590c6d62e14ac5935e53e929bcdbc5a495987a84f", size = 219027, upload-time = "2025-07-01T15:55:53.874Z" },
+ { url = "https://files.pythonhosted.org/packages/75/04/5302cea1aa26d886d34cadbf2dc77d90d7737e576c0065f357b96dc7a1a6/rpds_py-0.26.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f14440b9573a6f76b4ee4770c13f0b5921f71dde3b6fcb8dabbefd13b7fe05d7", size = 232821, upload-time = "2025-07-01T15:55:55.167Z" },
+]
+
+[[package]]
+name = "shellingham"
+version = "1.5.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
+]
+
+[[package]]
+name = "six"
+version = "1.17.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
+]
+
+[[package]]
+name = "sniffio"
+version = "1.3.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
+]
+
+[[package]]
+name = "soupsieve"
+version = "2.7"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/3f/f4/4a80cd6ef364b2e8b65b15816a843c0980f7a5a2b4dc701fc574952aa19f/soupsieve-2.7.tar.gz", hash = "sha256:ad282f9b6926286d2ead4750552c8a6142bc4c783fd66b0293547c8fe6ae126a", size = 103418, upload-time = "2025-04-20T18:50:08.518Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e7/9c/0e6afc12c269578be5c0c1c9f4b49a8d32770a080260c333ac04cc1c832d/soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4", size = 36677, upload-time = "2025-04-20T18:50:07.196Z" },
+]
+
+[[package]]
+name = "sse-starlette"
+version = "3.0.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/42/6f/22ed6e33f8a9e76ca0a412405f31abb844b779d52c5f96660766edcd737c/sse_starlette-3.0.2.tar.gz", hash = "sha256:ccd60b5765ebb3584d0de2d7a6e4f745672581de4f5005ab31c3a25d10b52b3a", size = 20985, upload-time = "2025-07-27T09:07:44.565Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ef/10/c78f463b4ef22eef8491f218f692be838282cd65480f6e423d7730dfd1fb/sse_starlette-3.0.2-py3-none-any.whl", hash = "sha256:16b7cbfddbcd4eaca11f7b586f3b8a080f1afe952c15813455b162edea619e5a", size = 11297, upload-time = "2025-07-27T09:07:43.268Z" },
+]
+
+[[package]]
+name = "starlette"
+version = "0.47.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/04/57/d062573f391d062710d4088fa1369428c38d51460ab6fedff920efef932e/starlette-0.47.2.tar.gz", hash = "sha256:6ae9aa5db235e4846decc1e7b79c4f346adf41e9777aebeb49dfd09bbd7023d8", size = 2583948, upload-time = "2025-07-20T17:31:58.522Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f7/1f/b876b1f83aef204198a42dc101613fefccb32258e5428b5f9259677864b4/starlette-0.47.2-py3-none-any.whl", hash = "sha256:c5847e96134e5c5371ee9fac6fdf1a67336d5815e09eb2a01fdb57a351ef915b", size = 72984, upload-time = "2025-07-20T17:31:56.738Z" },
+]
+
+[[package]]
+name = "typer"
+version = "0.16.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+ { name = "rich" },
+ { name = "shellingham" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c5/8c/7d682431efca5fd290017663ea4588bf6f2c6aad085c7f108c5dbc316e70/typer-0.16.0.tar.gz", hash = "sha256:af377ffaee1dbe37ae9440cb4e8f11686ea5ce4e9bae01b84ae7c63b87f1dd3b", size = 102625, upload-time = "2025-05-26T14:30:31.824Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/76/42/3efaf858001d2c2913de7f354563e3a3a2f0decae3efe98427125a8f441e/typer-0.16.0-py3-none-any.whl", hash = "sha256:1f79bed11d4d02d4310e3c1b7ba594183bcedb0ac73b27a9e5f28f6fb5b98855", size = 46317, upload-time = "2025-05-26T14:30:30.523Z" },
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.14.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" },
+]
+
+[[package]]
+name = "typing-inspection"
+version = "0.4.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" },
+]
+
+[[package]]
+name = "urllib3"
+version = "2.5.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" },
+]
+
+[[package]]
+name = "uvicorn"
+version = "0.35.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+ { name = "h11" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/5e/42/e0e305207bb88c6b8d3061399c6a961ffe5fbb7e2aa63c9234df7259e9cd/uvicorn-0.35.0.tar.gz", hash = "sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01", size = 78473, upload-time = "2025-06-28T16:15:46.058Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d2/e2/dc81b1bd1dcfe91735810265e9d26bc8ec5da45b4c0f6237e286819194c3/uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a", size = 66406, upload-time = "2025-06-28T16:15:44.816Z" },
+]
+
+[[package]]
+name = "werkzeug"
+version = "3.1.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markupsafe" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/32/af/d4502dc713b4ccea7175d764718d5183caf8d0867a4f0190d5d4a45cea49/werkzeug-3.1.1.tar.gz", hash = "sha256:8cd39dfbdfc1e051965f156163e2974e52c210f130810e9ad36858f0fd3edad4", size = 806453, upload-time = "2024-11-01T16:40:45.462Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ee/ea/c67e1dee1ba208ed22c06d1d547ae5e293374bfc43e0eb0ef5e262b68561/werkzeug-3.1.1-py3-none-any.whl", hash = "sha256:a71124d1ef06008baafa3d266c02f56e1836a5984afd6dd6c9230669d60d9fb5", size = 224371, upload-time = "2024-11-01T16:40:43.994Z" },
+]
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/.github/workflows/publish.yml b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/.github/workflows/publish.yml
new file mode 100644
index 00000000..acacdc5c
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/.github/workflows/publish.yml
@@ -0,0 +1,34 @@
+name: Publish to PyPI
+
+on:
+ release:
+ types: [published]
+
+jobs:
+ build-n-publish:
+ name: Build and publish to PyPI
+ runs-on: ubuntu-latest
+ environment:
+ name: release
+ url: https://pypi.org/project/excel-mcp-server
+ permissions:
+ id-token: write
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Python
+ uses: actions/setup-python@v4
+ with:
+ python-version: "3.x"
+
+ - name: Install hatch dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install hatch
+
+ - name: Build package
+ run: hatch build
+
+ - name: Publish to PyPI
+ uses: pypa/gh-action-pypi-publish@release/v1
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/.gitignore b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/.gitignore
new file mode 100644
index 00000000..67bec94c
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/.gitignore
@@ -0,0 +1,6 @@
+.venv/
+dist/
+excel_files/
+__pycache__/
+.notes/
+*.log
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/.mcpbignore b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/.mcpbignore
new file mode 100644
index 00000000..3d5b7bb6
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/.mcpbignore
@@ -0,0 +1,57 @@
+# Exclude everything except manifest and icon (uvx pattern)
+# The uvx command downloads the package from PyPI at runtime
+
+# Python source files
+*.py
+*.pyc
+*.pyo
+__pycache__/
+
+# Package files
+*.toml
+*.lock
+*.txt
+*.cfg
+*.ini
+
+# Source directories
+src/
+tests/
+docs/
+
+# Assets (icon is copied to root)
+assets/
+
+# Documentation
+*.md
+!README.md
+
+# Build artifacts
+*.egg-info/
+dist/
+build/
+.eggs/
+
+# Environment
+.env*
+*.local
+.venv/
+venv/
+
+# IDE/Editor
+.vscode/
+.idea/
+*.swp
+*.swo
+
+# Git
+.git/
+.gitignore
+.gitattributes
+
+# CI/CD
+.github/
+
+# Misc
+.DS_Store
+Thumbs.db
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/.python-version b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/.python-version
new file mode 100644
index 00000000..7c7a975f
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/.python-version
@@ -0,0 +1 @@
+3.10
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/LICENSE b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/LICENSE
new file mode 100644
index 00000000..1f121f23
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2025 Haris
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/README.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/README.md
new file mode 100644
index 00000000..eede6e4d
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/README.md
@@ -0,0 +1,114 @@
+
+
+
+
+[](https://pypi.org/project/excel-mcp-server/)
+[](https://pepy.tech/project/excel-mcp-server)
+[](https://opensource.org/licenses/MIT)
+[](https://smithery.ai/server/@haris-musa/excel-mcp-server)
+[](https://cursor.com/install-mcp?name=excel-mcp-server&config=eyJjb21tYW5kIjoidXZ4IGV4Y2VsLW1jcC1zZXJ2ZXIgc3RkaW8ifQ%3D%3D)
+
+A Model Context Protocol (MCP) server that lets you manipulate Excel files without needing Microsoft Excel installed. Create, read, and modify Excel workbooks with your AI agent.
+
+## Features
+
+- 📊 **Excel Operations**: Create, read, update workbooks and worksheets
+- 📈 **Data Manipulation**: Formulas, formatting, charts, pivot tables, and Excel tables
+- 🔍 **Data Validation**: Built-in validation for ranges, formulas, and data integrity
+- 🎨 **Formatting**: Font styling, colors, borders, alignment, and conditional formatting
+- 📋 **Table Operations**: Create and manage Excel tables with custom styling
+- 📊 **Chart Creation**: Generate various chart types (line, bar, pie, scatter, etc.)
+- 🔄 **Pivot Tables**: Create dynamic pivot tables for data analysis
+- 🔧 **Sheet Management**: Copy, rename, delete worksheets with ease
+- 🔌 **Triple transport support**: stdio, SSE (deprecated), and streamable HTTP
+- 🌐 **Remote & Local**: Works both locally and as a remote service
+
+## Usage
+
+The server supports three transport methods:
+
+### 1. Stdio Transport (for local use)
+
+```bash
+uvx excel-mcp-server stdio
+```
+
+```json
+{
+ "mcpServers": {
+ "excel": {
+ "command": "uvx",
+ "args": ["excel-mcp-server", "stdio"]
+ }
+ }
+}
+```
+
+### 2. SSE Transport (Server-Sent Events - Deprecated)
+
+```bash
+uvx excel-mcp-server sse
+```
+
+**SSE transport connection**:
+```json
+{
+ "mcpServers": {
+ "excel": {
+ "url": "http://localhost:8000/sse",
+ }
+ }
+}
+```
+
+### 3. Streamable HTTP Transport (Recommended for remote connections)
+
+```bash
+uvx excel-mcp-server streamable-http
+```
+
+**Streamable HTTP transport connection**:
+```json
+{
+ "mcpServers": {
+ "excel": {
+ "url": "http://localhost:8000/mcp",
+ }
+ }
+}
+```
+
+## Environment Variables & File Path Handling
+
+### SSE and Streamable HTTP Transports
+
+When running the server with the **SSE or Streamable HTTP protocols**, you **must set the `EXCEL_FILES_PATH` environment variable on the server side**. This variable tells the server where to read and write Excel files.
+- If not set, it defaults to `./excel_files`.
+
+You can also set the `FASTMCP_PORT` environment variable to control the port the server listens on (default is `8017` if not set).
+- Example (Windows PowerShell):
+ ```powershell
+ $env:EXCEL_FILES_PATH="E:\MyExcelFiles"
+ $env:FASTMCP_PORT="8007"
+ uvx excel-mcp-server streamable-http
+ ```
+- Example (Linux/macOS):
+ ```bash
+ EXCEL_FILES_PATH=/path/to/excel_files FASTMCP_PORT=8007 uvx excel-mcp-server streamable-http
+ ```
+
+### Stdio Transport
+
+When using the **stdio protocol**, the file path is provided with each tool call, so you do **not** need to set `EXCEL_FILES_PATH` on the server. The server will use the path sent by the client for each operation.
+
+## Available Tools
+
+The server provides a comprehensive set of Excel manipulation tools. See [TOOLS.md](TOOLS.md) for complete documentation of all available tools.
+
+## Star History
+
+[](https://www.star-history.com/#haris-musa/excel-mcp-server&Date)
+
+## License
+
+MIT License - see [LICENSE](LICENSE) for details.
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/TOOLS.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/TOOLS.md
new file mode 100644
index 00000000..2d8f71d1
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/TOOLS.md
@@ -0,0 +1,473 @@
+# Excel MCP Server Tools
+
+This document provides detailed information about all available tools in the Excel MCP server.
+
+## Workbook Operations
+
+### create_workbook
+
+Creates a new Excel workbook.
+
+```python
+create_workbook(filepath: str) -> str
+```
+
+- `filepath`: Path where to create workbook
+- Returns: Success message with created file path
+
+### create_worksheet
+
+Creates a new worksheet in an existing workbook.
+
+```python
+create_worksheet(filepath: str, sheet_name: str) -> str
+```
+
+- `filepath`: Path to Excel file
+- `sheet_name`: Name for the new worksheet
+- Returns: Success message
+
+### get_workbook_metadata
+
+Get metadata about workbook including sheets and ranges.
+
+```python
+get_workbook_metadata(filepath: str, include_ranges: bool = False) -> str
+```
+
+- `filepath`: Path to Excel file
+- `include_ranges`: Whether to include range information
+- Returns: String representation of workbook metadata
+
+## Data Operations
+
+### write_data_to_excel
+
+Write data to Excel worksheet.
+
+```python
+write_data_to_excel(
+ filepath: str,
+ sheet_name: str,
+ data: List[Dict],
+ start_cell: str = "A1"
+) -> str
+```
+
+- `filepath`: Path to Excel file
+- `sheet_name`: Target worksheet name
+- `data`: List of dictionaries containing data to write
+- `start_cell`: Starting cell (default: "A1")
+- Returns: Success message
+
+### read_data_from_excel
+
+Read data from Excel worksheet.
+
+```python
+read_data_from_excel(
+ filepath: str,
+ sheet_name: str,
+ start_cell: str = "A1",
+ end_cell: str = None,
+ preview_only: bool = False
+) -> str
+```
+
+- `filepath`: Path to Excel file
+- `sheet_name`: Source worksheet name
+- `start_cell`: Starting cell (default: "A1")
+- `end_cell`: Optional ending cell
+- `preview_only`: Whether to return only a preview
+- Returns: String representation of data
+
+## Formatting Operations
+
+### format_range
+
+Apply formatting to a range of cells.
+
+```python
+format_range(
+ filepath: str,
+ sheet_name: str,
+ start_cell: str,
+ end_cell: str = None,
+ bold: bool = False,
+ italic: bool = False,
+ underline: bool = False,
+ font_size: int = None,
+ font_color: str = None,
+ bg_color: str = None,
+ border_style: str = None,
+ border_color: str = None,
+ number_format: str = None,
+ alignment: str = None,
+ wrap_text: bool = False,
+ merge_cells: bool = False,
+ protection: Dict[str, Any] = None,
+ conditional_format: Dict[str, Any] = None
+) -> str
+```
+
+- `filepath`: Path to Excel file
+- `sheet_name`: Target worksheet name
+- `start_cell`: Starting cell of range
+- `end_cell`: Optional ending cell of range
+- Various formatting options (see parameters)
+- Returns: Success message
+
+### merge_cells
+
+Merge a range of cells.
+
+```python
+merge_cells(filepath: str, sheet_name: str, start_cell: str, end_cell: str) -> str
+```
+
+- `filepath`: Path to Excel file
+- `sheet_name`: Target worksheet name
+- `start_cell`: Starting cell of range
+- `end_cell`: Ending cell of range
+- Returns: Success message
+
+### unmerge_cells
+
+Unmerge a previously merged range of cells.
+
+```python
+unmerge_cells(filepath: str, sheet_name: str, start_cell: str, end_cell: str) -> str
+```
+
+- `filepath`: Path to Excel file
+- `sheet_name`: Target worksheet name
+- `start_cell`: Starting cell of range
+- `end_cell`: Ending cell of range
+- Returns: Success message
+
+### get_merged_cells
+
+Get merged cells in a worksheet.
+
+```python
+get_merged_cells(filepath: str, sheet_name: str) -> str
+```
+
+- `filepath`: Path to Excel file
+- `sheet_name`: Target worksheet name
+- Returns: String representation of merged cells
+
+
+## Formula Operations
+
+### apply_formula
+
+Apply Excel formula to cell.
+
+```python
+apply_formula(filepath: str, sheet_name: str, cell: str, formula: str) -> str
+```
+
+- `filepath`: Path to Excel file
+- `sheet_name`: Target worksheet name
+- `cell`: Target cell reference
+- `formula`: Excel formula to apply
+- Returns: Success message
+
+### validate_formula_syntax
+
+Validate Excel formula syntax without applying it.
+
+```python
+validate_formula_syntax(filepath: str, sheet_name: str, cell: str, formula: str) -> str
+```
+
+- `filepath`: Path to Excel file
+- `sheet_name`: Target worksheet name
+- `cell`: Target cell reference
+- `formula`: Excel formula to validate
+- Returns: Validation result message
+
+## Chart Operations
+
+### create_chart
+
+Create chart in worksheet.
+
+```python
+create_chart(
+ filepath: str,
+ sheet_name: str,
+ data_range: str,
+ chart_type: str,
+ target_cell: str,
+ title: str = "",
+ x_axis: str = "",
+ y_axis: str = ""
+) -> str
+```
+
+- `filepath`: Path to Excel file
+- `sheet_name`: Target worksheet name
+- `data_range`: Range containing chart data
+- `chart_type`: Type of chart (line, bar, pie, scatter, area)
+- `target_cell`: Cell where to place chart
+- `title`: Optional chart title
+- `x_axis`: Optional X-axis label
+- `y_axis`: Optional Y-axis label
+- Returns: Success message
+
+## Pivot Table Operations
+
+### create_pivot_table
+
+Create pivot table in worksheet.
+
+```python
+create_pivot_table(
+ filepath: str,
+ sheet_name: str,
+ data_range: str,
+ target_cell: str,
+ rows: List[str],
+ values: List[str],
+ columns: List[str] = None,
+ agg_func: str = "mean"
+) -> str
+```
+
+- `filepath`: Path to Excel file
+- `sheet_name`: Target worksheet name
+- `data_range`: Range containing source data
+- `target_cell`: Cell where to place pivot table
+- `rows`: Fields for row labels
+- `values`: Fields for values
+- `columns`: Optional fields for column labels
+- `agg_func`: Aggregation function (sum, count, average, max, min)
+- Returns: Success message
+
+## Table Operations
+
+### create_table
+
+Creates a native Excel table from a specified range of data.
+
+```python
+create_table(
+ filepath: str,
+ sheet_name: str,
+ data_range: str,
+ table_name: str = None,
+ table_style: str = "TableStyleMedium9"
+) -> str
+```
+
+- `filepath`: Path to the Excel file.
+- `sheet_name`: Name of the worksheet.
+- `data_range`: The cell range for the table (e.g., "A1:D5").
+- `table_name`: Optional unique name for the table.
+- `table_style`: Optional visual style for the table.
+- Returns: Success message.
+
+## Worksheet Operations
+
+### copy_worksheet
+
+Copy worksheet within workbook.
+
+```python
+copy_worksheet(filepath: str, source_sheet: str, target_sheet: str) -> str
+```
+
+- `filepath`: Path to Excel file
+- `source_sheet`: Name of sheet to copy
+- `target_sheet`: Name for new sheet
+- Returns: Success message
+
+### delete_worksheet
+
+Delete worksheet from workbook.
+
+```python
+delete_worksheet(filepath: str, sheet_name: str) -> str
+```
+
+- `filepath`: Path to Excel file
+- `sheet_name`: Name of sheet to delete
+- Returns: Success message
+
+### rename_worksheet
+
+Rename worksheet in workbook.
+
+```python
+rename_worksheet(filepath: str, old_name: str, new_name: str) -> str
+```
+
+- `filepath`: Path to Excel file
+- `old_name`: Current sheet name
+- `new_name`: New sheet name
+- Returns: Success message
+
+## Range Operations
+
+### copy_range
+
+Copy a range of cells to another location.
+
+```python
+copy_range(
+ filepath: str,
+ sheet_name: str,
+ source_start: str,
+ source_end: str,
+ target_start: str,
+ target_sheet: str = None
+) -> str
+```
+
+- `filepath`: Path to Excel file
+- `sheet_name`: Source worksheet name
+- `source_start`: Starting cell of source range
+- `source_end`: Ending cell of source range
+- `target_start`: Starting cell for paste
+- `target_sheet`: Optional target worksheet name
+- Returns: Success message
+
+### delete_range
+
+Delete a range of cells and shift remaining cells.
+
+```python
+delete_range(
+ filepath: str,
+ sheet_name: str,
+ start_cell: str,
+ end_cell: str,
+ shift_direction: str = "up"
+) -> str
+```
+
+- `filepath`: Path to Excel file
+- `sheet_name`: Target worksheet name
+- `start_cell`: Starting cell of range
+- `end_cell`: Ending cell of range
+- `shift_direction`: Direction to shift cells ("up" or "left")
+- Returns: Success message
+
+### validate_excel_range
+
+Validate if a range exists and is properly formatted.
+
+```python
+validate_excel_range(
+ filepath: str,
+ sheet_name: str,
+ start_cell: str,
+ end_cell: str = None
+) -> str
+```
+
+- `filepath`: Path to Excel file
+- `sheet_name`: Target worksheet name
+- `start_cell`: Starting cell of range
+- `end_cell`: Optional ending cell of range
+- Returns: Validation result message
+
+### get_data_validation_info
+
+Get data validation rules and metadata for a worksheet.
+
+```python
+get_data_validation_info(filepath: str, sheet_name: str) -> str
+```
+
+- `filepath`: Path to Excel file
+- `sheet_name`: Target worksheet name
+- Returns: JSON string containing all data validation rules with metadata including:
+ - Validation type (list, whole, decimal, date, time, textLength)
+ - Operator (between, notBetween, equal, greaterThan, lessThan, etc.)
+ - Allowed values for list validations (resolved from ranges)
+ - Formula constraints for numeric/date validations
+ - Cell ranges where validation applies
+ - Prompt and error messages
+
+**Note**: The `read_data_from_excel` tool automatically includes validation metadata for individual cells when available.
+
+## Row and Column Operations
+
+### insert_rows
+
+Insert one or more rows starting at the specified row.
+
+```python
+insert_rows(
+ filepath: str,
+ sheet_name: str,
+ start_row: int,
+ count: int = 1
+) -> str
+```
+
+- `filepath`: Path to Excel file
+- `sheet_name`: Target worksheet name
+- `start_row`: Row number where to start inserting (1-based)
+- `count`: Number of rows to insert (default: 1)
+- Returns: Success message
+
+### insert_columns
+
+Insert one or more columns starting at the specified column.
+
+```python
+insert_columns(
+ filepath: str,
+ sheet_name: str,
+ start_col: int,
+ count: int = 1
+) -> str
+```
+
+- `filepath`: Path to Excel file
+- `sheet_name`: Target worksheet name
+- `start_col`: Column number where to start inserting (1-based)
+- `count`: Number of columns to insert (default: 1)
+- Returns: Success message
+
+### delete_sheet_rows
+
+Delete one or more rows starting at the specified row.
+
+```python
+delete_sheet_rows(
+ filepath: str,
+ sheet_name: str,
+ start_row: int,
+ count: int = 1
+) -> str
+```
+
+- `filepath`: Path to Excel file
+- `sheet_name`: Target worksheet name
+- `start_row`: Row number where to start deleting (1-based)
+- `count`: Number of rows to delete (default: 1)
+- Returns: Success message
+
+### delete_sheet_columns
+
+Delete one or more columns starting at the specified column.
+
+```python
+delete_sheet_columns(
+ filepath: str,
+ sheet_name: str,
+ start_col: int,
+ count: int = 1
+) -> str
+```
+
+- `filepath`: Path to Excel file
+- `sheet_name`: Target worksheet name
+- `start_col`: Column number where to start deleting (1-based)
+- `count`: Number of columns to delete (default: 1)
+- Returns: Success message
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/assets/logo.png b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/assets/logo.png
new file mode 100644
index 00000000..dc8a3934
Binary files /dev/null and b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/assets/logo.png differ
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/assets/logo.svg b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/assets/logo.svg
new file mode 100644
index 00000000..1e85b4f6
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/assets/logo.svg
@@ -0,0 +1,8 @@
+
+
+
+ Excel
+ MCP
+ Server
+
+
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/docs/CNAME b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/docs/CNAME
new file mode 100644
index 00000000..9c8ae693
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/docs/CNAME
@@ -0,0 +1 @@
+excelmcpserver.com
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/docs/index.html b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/docs/index.html
new file mode 100644
index 00000000..38cc571a
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/docs/index.html
@@ -0,0 +1,182 @@
+
+
+
+
+
+ Excel MCP Server
+
+
+
+
+
+
+
+
+ Excel MCP Server
+ A Model Context Protocol (MCP) server that lets you manipulate Excel files without needing Microsoft Excel installed. Create, read, and modify Excel workbooks with your AI agent.
+
+
+
+ Features
+
+ 📊 Create and modify Excel workbooks
+ 📝 Read and write data
+ 🎨 Apply formatting and styles
+ 📈 Create charts and visualizations
+ 📊 Generate pivot tables
+ 🔄 Manage worksheets and ranges
+ 🔌 Triple transport support: stdio, streamable HTTP, and SSE
+
+
+
+
+ Available Tools
+ A comprehensive set of tools to interact with your Excel files.
+
+ Workbook Operations: Create workbooks and worksheets, and get metadata.
+ Data Operations: Read and write data to worksheets.
+ Formatting Operations: Apply rich formatting to cell ranges.
+ Formula Operations: Apply and validate Excel formulas.
+ Chart Operations: Create various types of charts.
+ Pivot Table Operations: Generate pivot tables from your data.
+ Table Operations: Create native Excel tables.
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/excel-mcp-server-0.1.7.mcpb b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/excel-mcp-server-0.1.7.mcpb
new file mode 100644
index 00000000..1f70d346
Binary files /dev/null and b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/excel-mcp-server-0.1.7.mcpb differ
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/icon.png b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/icon.png
new file mode 100644
index 00000000..dc8a3934
Binary files /dev/null and b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/icon.png differ
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/manifest.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/manifest.json
new file mode 100644
index 00000000..815d5f66
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/manifest.json
@@ -0,0 +1,53 @@
+{
+ "manifest_version": "0.3",
+ "name": "excel-mcp-server",
+ "version": "0.1.7",
+ "description": "A Model Context Protocol server for Excel file manipulation",
+ "author": {
+ "name": "haris",
+ "url": "https://github.com/haris-musa"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/haris-musa/excel-mcp-server"
+ },
+ "homepage": "https://github.com/haris-musa/excel-mcp-server",
+ "documentation": "https://github.com/haris-musa/excel-mcp-server#readme",
+ "support": "https://github.com/haris-musa/excel-mcp-server/issues",
+ "icon": "icon.png",
+ "server": {
+ "type": "python",
+ "entry_point": "src/excel_mcp/__main__.py",
+ "mcp_config": {
+ "command": "uvx",
+ "args": ["excel-mcp-server", "stdio"]
+ }
+ },
+ "tools": [
+ {"name": "create_workbook", "description": "Create a new Excel workbook"},
+ {"name": "create_worksheet", "description": "Create a new worksheet in a workbook"},
+ {"name": "get_workbook_metadata", "description": "Get workbook metadata and structure"},
+ {"name": "write_data_to_excel", "description": "Write data to Excel cells"},
+ {"name": "read_data_from_excel", "description": "Read data from Excel range"},
+ {"name": "format_range", "description": "Apply formatting to cell range"},
+ {"name": "merge_cells", "description": "Merge cells in a range"},
+ {"name": "unmerge_cells", "description": "Unmerge previously merged cells"},
+ {"name": "get_merged_cells", "description": "Get list of merged cell ranges"},
+ {"name": "apply_formula", "description": "Apply Excel formula to cell"},
+ {"name": "validate_formula_syntax", "description": "Validate Excel formula syntax"},
+ {"name": "create_chart", "description": "Create chart (line, bar, pie, scatter, etc.)"},
+ {"name": "create_pivot_table", "description": "Create pivot table from data"},
+ {"name": "create_table", "description": "Create Excel table with styling"},
+ {"name": "copy_worksheet", "description": "Copy worksheet within workbook"},
+ {"name": "delete_worksheet", "description": "Delete worksheet from workbook"},
+ {"name": "rename_worksheet", "description": "Rename a worksheet"},
+ {"name": "copy_range", "description": "Copy cell range to another location"},
+ {"name": "delete_range", "description": "Delete cell range contents"},
+ {"name": "validate_excel_range", "description": "Validate Excel range format"},
+ {"name": "get_data_validation_info", "description": "Get data validation rules for cell"},
+ {"name": "insert_rows", "description": "Insert rows into worksheet"},
+ {"name": "insert_columns", "description": "Insert columns into worksheet"},
+ {"name": "delete_sheet_rows", "description": "Delete rows from worksheet"},
+ {"name": "delete_sheet_columns", "description": "Delete columns from worksheet"}
+ ]
+}
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/pyproject.toml b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/pyproject.toml
new file mode 100644
index 00000000..6e1e3c77
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/pyproject.toml
@@ -0,0 +1,28 @@
+[project]
+name = "excel-mcp-server"
+version = "0.1.7"
+description = "Excel MCP Server for manipulating Excel files"
+readme = "README.md"
+requires-python = ">=3.10"
+dependencies = [
+ "mcp[cli]>=1.10.1",
+ "fastmcp>=2.0.0,<3.0.0",
+ "openpyxl>=3.1.5",
+ "typer>=0.16.0"
+]
+[[project.authors]]
+name = "haris"
+email = "haris.musa@outlook.com"
+
+[project.scripts]
+excel-mcp-server = "excel_mcp.__main__:app"
+
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[tool.hatch.build.targets.wheel]
+packages = ["src/excel_mcp"]
+
+[tool.hatch.build]
+packages = ["src/excel_mcp"]
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/__main__.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/__main__.py
new file mode 100644
index 00000000..950187fe
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/__main__.py
@@ -0,0 +1,50 @@
+import typer
+
+from .server import run_sse, run_stdio, run_streamable_http
+
+app = typer.Typer(help="Excel MCP Server")
+
+@app.command()
+def sse():
+ """Start Excel MCP Server in SSE mode"""
+ try:
+ run_sse()
+ except KeyboardInterrupt:
+ print("\nShutting down server...")
+ except Exception as e:
+ print(f"\nError: {e}")
+ import traceback
+ traceback.print_exc()
+ finally:
+ print("Service stopped.")
+
+@app.command()
+def streamable_http():
+ """Start Excel MCP Server in streamable HTTP mode"""
+ try:
+ run_streamable_http()
+ except KeyboardInterrupt:
+ print("\nShutting down server...")
+ except Exception as e:
+ print(f"\nError: {e}")
+ import traceback
+ traceback.print_exc()
+ finally:
+ print("Service stopped.")
+
+@app.command()
+def stdio():
+ """Start Excel MCP Server in stdio mode"""
+ try:
+ run_stdio()
+ except KeyboardInterrupt:
+ print("\nShutting down server...")
+ except Exception as e:
+ print(f"\nError: {e}")
+ import traceback
+ traceback.print_exc()
+ finally:
+ print("Service stopped.")
+
+if __name__ == "__main__":
+ app()
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/calculations.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/calculations.py
new file mode 100644
index 00000000..a46e12b0
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/calculations.py
@@ -0,0 +1,60 @@
+from typing import Any
+import logging
+
+from .workbook import get_or_create_workbook
+from .cell_utils import validate_cell_reference
+from .exceptions import ValidationError, CalculationError
+from .validation import validate_formula
+
+logger = logging.getLogger(__name__)
+
+def apply_formula(
+ filepath: str,
+ sheet_name: str,
+ cell: str,
+ formula: str
+) -> dict[str, Any]:
+ """Apply any Excel formula to a cell."""
+ try:
+ if not validate_cell_reference(cell):
+ raise ValidationError(f"Invalid cell reference: {cell}")
+
+ wb = get_or_create_workbook(filepath)
+ if sheet_name not in wb.sheetnames:
+ raise ValidationError(f"Sheet '{sheet_name}' not found")
+
+ sheet = wb[sheet_name]
+
+ # Ensure formula starts with =
+ if not formula.startswith('='):
+ formula = f'={formula}'
+
+ # Validate formula syntax
+ is_valid, message = validate_formula(formula)
+ if not is_valid:
+ raise CalculationError(f"Invalid formula syntax: {message}")
+
+ try:
+ # Apply formula to the cell
+ cell_obj = sheet[cell]
+ cell_obj.value = formula
+ except Exception as e:
+ raise CalculationError(f"Failed to apply formula to cell: {str(e)}")
+
+ try:
+ wb.save(filepath)
+ except Exception as e:
+ raise CalculationError(f"Failed to save workbook after applying formula: {str(e)}")
+
+ return {
+ "message": f"Applied formula '{formula}' to cell {cell}",
+ "cell": cell,
+ "formula": formula
+ }
+
+ except (ValidationError, CalculationError) as e:
+ logger.error(str(e))
+ raise
+ except Exception as e:
+ logger.error(f"Failed to apply formula: {e}")
+ raise CalculationError(str(e))
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/cell_utils.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/cell_utils.py
new file mode 100644
index 00000000..32fe8226
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/cell_utils.py
@@ -0,0 +1,54 @@
+import re
+
+from openpyxl.utils import column_index_from_string
+
+def parse_cell_range(
+ cell_ref: str,
+ end_ref: str | None = None
+) -> tuple[int, int, int | None, int | None]:
+ """Parse Excel cell reference into row and column indices."""
+ if end_ref:
+ start_cell = cell_ref
+ end_cell = end_ref
+ else:
+ start_cell = cell_ref
+ end_cell = None
+
+ match = re.match(r"([A-Z]+)([0-9]+)", start_cell.upper())
+ if not match:
+ raise ValueError(f"Invalid cell reference: {start_cell}")
+ col_str, row_str = match.groups()
+ start_row = int(row_str)
+ start_col = column_index_from_string(col_str)
+
+ if end_cell:
+ match = re.match(r"([A-Z]+)([0-9]+)", end_cell.upper())
+ if not match:
+ raise ValueError(f"Invalid cell reference: {end_cell}")
+ col_str, row_str = match.groups()
+ end_row = int(row_str)
+ end_col = column_index_from_string(col_str)
+ else:
+ end_row = None
+ end_col = None
+
+ return start_row, start_col, end_row, end_col
+
+def validate_cell_reference(cell_ref: str) -> bool:
+ """Validate Excel cell reference format (e.g., 'A1', 'BC123')"""
+ if not cell_ref:
+ return False
+
+ # Split into column and row parts
+ col = row = ""
+ for c in cell_ref:
+ if c.isalpha():
+ if row: # Letters after numbers not allowed
+ return False
+ col += c
+ elif c.isdigit():
+ row += c
+ else:
+ return False
+
+ return bool(col and row)
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/cell_validation.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/cell_validation.py
new file mode 100644
index 00000000..ef14f145
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/cell_validation.py
@@ -0,0 +1,179 @@
+import logging
+from typing import Any, Dict, List, Optional
+
+from openpyxl.worksheet.worksheet import Worksheet
+from openpyxl.utils.cell import coordinate_from_string, column_index_from_string
+
+logger = logging.getLogger(__name__)
+
+def get_data_validation_for_cell(worksheet: Worksheet, cell_address: str) -> Optional[Dict[str, Any]]:
+ """Get data validation metadata for a specific cell.
+
+ Args:
+ worksheet: The openpyxl worksheet object
+ cell_address: Cell address like 'A1', 'B2', etc.
+
+ Returns:
+ Dictionary with validation metadata or None if no validation exists
+ """
+ try:
+ # Convert cell address to row/col coordinates
+ col_letter, row = coordinate_from_string(cell_address)
+ col_idx = column_index_from_string(col_letter)
+
+ # Check each data validation rule in the worksheet
+ for dv in worksheet.data_validations.dataValidation:
+ # Check if this cell is covered by the validation rule
+ if _cell_in_validation_range(row, col_idx, dv):
+ return _extract_validation_metadata(dv, cell_address, worksheet)
+
+ return None
+
+ except Exception as e:
+ logger.warning(f"Failed to get validation for cell {cell_address}: {e}")
+ return None
+
+def _cell_in_validation_range(row: int, col: int, data_validation) -> bool:
+ """Check if a cell is within a data validation range."""
+ try:
+ # data_validation.sqref contains the cell ranges this validation applies to
+ for cell_range in data_validation.sqref.ranges:
+ if (cell_range.min_row <= row <= cell_range.max_row and
+ cell_range.min_col <= col <= cell_range.max_col):
+ return True
+ return False
+ except Exception as e:
+ logger.warning(f"Error checking if cell ({row}, {col}) is in validation range for DV sqref '{getattr(data_validation, 'sqref', 'N/A')}': {e}")
+ return False
+
+def _extract_validation_metadata(data_validation, cell_address: str, worksheet: Optional[Worksheet] = None) -> Dict[str, Any]:
+ """Extract metadata from a DataValidation object."""
+ try:
+ validation_info = {
+ "cell": cell_address,
+ "has_validation": True,
+ "validation_type": data_validation.type,
+ "allow_blank": data_validation.allowBlank,
+ }
+
+ # Add operator for validation types that use it
+ if data_validation.operator:
+ validation_info["operator"] = data_validation.operator
+
+ # Add optional fields if they exist
+ if data_validation.prompt:
+ validation_info["prompt"] = data_validation.prompt
+ if data_validation.promptTitle:
+ validation_info["prompt_title"] = data_validation.promptTitle
+ if data_validation.error:
+ validation_info["error_message"] = data_validation.error
+ if data_validation.errorTitle:
+ validation_info["error_title"] = data_validation.errorTitle
+
+ # For list type validations (dropdown lists), extract allowed values
+ if data_validation.type == "list" and data_validation.formula1:
+ allowed_values = _extract_list_values(data_validation.formula1, worksheet)
+ validation_info["allowed_values"] = allowed_values
+
+ # For other validation types, include the formulas
+ elif data_validation.formula1:
+ validation_info["formula1"] = data_validation.formula1
+ if data_validation.formula2:
+ validation_info["formula2"] = data_validation.formula2
+
+ return validation_info
+
+ except Exception as e:
+ logger.warning(f"Failed to extract validation metadata: {e}")
+ return {
+ "cell": cell_address,
+ "has_validation": True,
+ "validation_type": "unknown",
+ "error": f"Failed to parse validation: {e}"
+ }
+
+def _extract_list_values(formula: str, worksheet: Optional[Worksheet] = None) -> List[str]:
+ """Extract allowed values from a list validation formula."""
+ try:
+ # Remove quotes if present
+ formula = formula.strip('"')
+
+ # Handle comma-separated list
+ if ',' in formula:
+ # Split by comma and clean up each value
+ values = [val.strip().strip('"') for val in formula.split(',')]
+ return [val for val in values if val] # Remove empty values
+
+ # Handle range reference (e.g., "$A$1:$A$5" or "Sheet1!$A$1:$A$5")
+ elif (':' in formula or formula.startswith('$')) and worksheet:
+ try:
+ # Remove potential leading '=' if it's a formula like '=Sheet1!$A$1:$A$5'
+ range_ref = formula
+ if formula.startswith('='):
+ range_ref = formula[1:]
+
+ actual_values = []
+ # worksheet[range_ref] can resolve ranges like "A1:A5" or "SheetName!A1:A5"
+ # It returns a tuple of tuples of cells for ranges, or a single cell
+ range_cells = worksheet[range_ref]
+
+ # Handle single cell or range
+ if hasattr(range_cells, 'value'): # Single cell
+ if range_cells.value is not None:
+ actual_values.append(str(range_cells.value))
+ else: # Range of cells
+ for row_of_cells in range_cells:
+ # Handle case where row_of_cells might be a single cell
+ if hasattr(row_of_cells, 'value'):
+ if row_of_cells.value is not None:
+ actual_values.append(str(row_of_cells.value))
+ else:
+ for cell in row_of_cells:
+ if cell.value is not None:
+ actual_values.append(str(cell.value))
+
+ if actual_values:
+ return actual_values
+ return [f"Range: {formula} (empty or unresolvable)"]
+
+ except Exception as e:
+ logger.warning(f"Could not resolve range '{formula}' for list validation: {e}")
+ return [f"Range: {formula} (resolution error)"]
+
+ # Handle range reference when worksheet not available
+ elif ':' in formula or formula.startswith('$'):
+ return [f"Range: {formula}"]
+
+ # Single value
+ else:
+ return [formula.strip('"')]
+
+ except Exception as e:
+ logger.warning(f"Failed to parse list formula '{formula}': {e}")
+ return [formula] # Return original formula if parsing fails
+
+def get_all_validation_ranges(worksheet: Worksheet) -> List[Dict[str, Any]]:
+ """Get all data validation ranges in a worksheet.
+
+ Returns:
+ List of dictionaries containing validation range information
+ """
+ validations = []
+
+ try:
+ for dv in worksheet.data_validations.dataValidation:
+ validation_info = {
+ "ranges": str(dv.sqref),
+ "validation_type": dv.type,
+ "allow_blank": dv.allowBlank,
+ }
+
+ if dv.type == "list" and dv.formula1:
+ validation_info["allowed_values"] = _extract_list_values(dv.formula1, worksheet)
+
+ validations.append(validation_info)
+
+ except Exception as e:
+ logger.warning(f"Failed to get validation ranges: {e}")
+
+ return validations
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/chart.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/chart.py
new file mode 100644
index 00000000..e92fd6f3
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/chart.py
@@ -0,0 +1,257 @@
+from typing import Any, Optional, Dict
+import logging
+from enum import Enum
+
+from openpyxl import load_workbook
+from openpyxl.chart import (
+ BarChart, LineChart, PieChart, ScatterChart,
+ AreaChart, Reference, Series
+)
+from openpyxl.chart.label import DataLabelList
+from openpyxl.chart.legend import Legend
+from openpyxl.chart.axis import ChartLines
+from openpyxl.drawing.spreadsheet_drawing import (
+ AnchorMarker, OneCellAnchor, SpreadsheetDrawing
+)
+from openpyxl.utils import column_index_from_string
+
+from .cell_utils import parse_cell_range
+from .exceptions import ValidationError, ChartError
+
+logger = logging.getLogger(__name__)
+
+class ChartType(str, Enum):
+ """Supported chart types"""
+ LINE = "line"
+ BAR = "bar"
+ PIE = "pie"
+ SCATTER = "scatter"
+ AREA = "area"
+ BUBBLE = "bubble"
+ STOCK = "stock"
+ SURFACE = "surface"
+ RADAR = "radar"
+
+class ChartStyle:
+ """Chart style configuration"""
+ def __init__(
+ self,
+ title_size: int = 14,
+ title_bold: bool = True,
+ axis_label_size: int = 12,
+ show_legend: bool = True,
+ legend_position: str = "r",
+ show_data_labels: bool = True,
+ grid_lines: bool = False,
+ style_id: int = 2
+ ):
+ self.title_size = title_size
+ self.title_bold = title_bold
+ self.axis_label_size = axis_label_size
+ self.show_legend = show_legend
+ self.legend_position = legend_position
+ self.show_data_labels = show_data_labels
+ self.grid_lines = grid_lines
+ self.style_id = style_id
+
+def create_chart_in_sheet(
+ filepath: str,
+ sheet_name: str,
+ data_range: str,
+ chart_type: str,
+ target_cell: str,
+ title: str = "",
+ x_axis: str = "",
+ y_axis: str = "",
+ style: Optional[Dict] = None
+) -> dict[str, Any]:
+ """Create chart in sheet with enhanced styling options"""
+ # Ensure style dict exists and defaults to showing data labels
+ if style is None:
+ style = {"show_data_labels": True}
+ else:
+ # If caller omitted the flag, default to True
+ style.setdefault("show_data_labels", True)
+ try:
+ wb = load_workbook(filepath)
+ if sheet_name not in wb.sheetnames:
+ logger.error(f"Sheet '{sheet_name}' not found")
+ raise ValidationError(f"Sheet '{sheet_name}' not found")
+
+ worksheet = wb[sheet_name]
+
+ # Initialize collections if they don't exist
+ if not hasattr(worksheet, '_drawings'):
+ worksheet._drawings = []
+ if not hasattr(worksheet, '_charts'):
+ worksheet._charts = []
+
+ # Parse the data range
+ if "!" in data_range:
+ range_sheet_name, cell_range = data_range.split("!")
+ if range_sheet_name not in wb.sheetnames:
+ logger.error(f"Sheet '{range_sheet_name}' referenced in data range not found")
+ raise ValidationError(f"Sheet '{range_sheet_name}' referenced in data range not found")
+ else:
+ cell_range = data_range
+
+ try:
+ start_cell, end_cell = cell_range.split(":")
+ start_row, start_col, end_row, end_col = parse_cell_range(start_cell, end_cell)
+ except ValueError as e:
+ logger.error(f"Invalid data range format: {e}")
+ raise ValidationError(f"Invalid data range format: {str(e)}")
+
+ # Validate chart type
+ chart_classes = {
+ "line": LineChart,
+ "bar": BarChart,
+ "pie": PieChart,
+ "scatter": ScatterChart,
+ "area": AreaChart
+ }
+
+ chart_type_lower = chart_type.lower()
+ ChartClass = chart_classes.get(chart_type_lower)
+ if not ChartClass:
+ logger.error(f"Unsupported chart type: {chart_type}")
+ raise ValidationError(
+ f"Unsupported chart type: {chart_type}. "
+ f"Supported types: {', '.join(chart_classes.keys())}"
+ )
+
+ chart = ChartClass()
+
+ # Basic chart settings
+ chart.title = title
+ if hasattr(chart, "x_axis"):
+ chart.x_axis.title = x_axis
+ if hasattr(chart, "y_axis"):
+ chart.y_axis.title = y_axis
+
+ try:
+ # Create data references
+ if chart_type_lower == "scatter":
+ # For scatter charts, create series for each pair of columns
+ for col in range(start_col + 1, end_col + 1):
+ x_values = Reference(
+ worksheet,
+ min_row=start_row + 1,
+ max_row=end_row,
+ min_col=start_col
+ )
+ y_values = Reference(
+ worksheet,
+ min_row=start_row + 1,
+ max_row=end_row,
+ min_col=col
+ )
+ series = Series(y_values, x_values, title_from_data=True)
+ chart.series.append(series)
+ else:
+ # For other chart types
+ data = Reference(
+ worksheet,
+ min_row=start_row,
+ max_row=end_row,
+ min_col=start_col + 1,
+ max_col=end_col
+ )
+ cats = Reference(
+ worksheet,
+ min_row=start_row + 1,
+ max_row=end_row,
+ min_col=start_col
+ )
+ chart.add_data(data, titles_from_data=True)
+ chart.set_categories(cats)
+ except Exception as e:
+ logger.error(f"Failed to create chart data references: {e}")
+ raise ChartError(f"Failed to create chart data references: {str(e)}")
+
+ # Apply style if provided
+ try:
+ if style.get("show_legend", True):
+ chart.legend = Legend()
+ chart.legend.position = style.get("legend_position", "r")
+ else:
+ chart.legend = None
+
+ if style.get("show_data_labels", False):
+ data_labels = DataLabelList()
+ # Gather optional overrides
+ dlo = style.get("data_label_options", {}) if isinstance(style.get("data_label_options", {}), dict) else {}
+
+ # Helper to read bool with fallback
+ def _opt(name: str, default: bool) -> bool:
+ return bool(dlo.get(name, default))
+
+ # Apply options – Excel will concatenate any that are set to True
+ data_labels.showVal = _opt("show_val", True)
+ data_labels.showCatName = _opt("show_cat_name", False)
+ data_labels.showSerName = _opt("show_ser_name", False)
+ data_labels.showLegendKey = _opt("show_legend_key", False)
+ data_labels.showPercent = _opt("show_percent", False)
+ data_labels.showBubbleSize = _opt("show_bubble_size", False)
+
+ chart.dataLabels = data_labels
+
+ if style.get("grid_lines", False):
+ if hasattr(chart, "x_axis"):
+ chart.x_axis.majorGridlines = ChartLines()
+ if hasattr(chart, "y_axis"):
+ chart.y_axis.majorGridlines = ChartLines()
+ except Exception as e:
+ logger.error(f"Failed to apply chart style: {e}")
+ raise ChartError(f"Failed to apply chart style: {str(e)}")
+
+ # Set chart size
+ chart.width = 15
+ chart.height = 7.5
+
+ # Create drawing and anchor
+ try:
+ drawing = SpreadsheetDrawing()
+ drawing.chart = chart
+
+ # Validate target cell format
+ if not target_cell or not any(c.isalpha() for c in target_cell) or not any(c.isdigit() for c in target_cell):
+ raise ValidationError(f"Invalid target cell format: {target_cell}")
+
+ # Create anchor
+ col = column_index_from_string(target_cell[0]) - 1
+ row = int(target_cell[1:]) - 1
+ anchor = OneCellAnchor()
+ anchor._from = AnchorMarker(col=col, row=row)
+ drawing.anchor = anchor
+
+ # Add to worksheet
+ worksheet._drawings.append(drawing)
+ worksheet._charts.append(chart)
+ except ValueError as e:
+ logger.error(f"Invalid target cell: {e}")
+ raise ValidationError(f"Invalid target cell: {str(e)}")
+ except Exception as e:
+ logger.error(f"Failed to create chart drawing: {e}")
+ raise ChartError(f"Failed to create chart drawing: {str(e)}")
+
+ try:
+ wb.save(filepath)
+ except Exception as e:
+ logger.error(f"Failed to save workbook: {e}")
+ raise ChartError(f"Failed to save workbook with chart: {str(e)}")
+
+ return {
+ "message": f"{chart_type.capitalize()} chart created successfully",
+ "details": {
+ "type": chart_type,
+ "location": target_cell,
+ "data_range": data_range
+ }
+ }
+
+ except (ValidationError, ChartError):
+ raise
+ except Exception as e:
+ logger.error(f"Unexpected error creating chart: {e}")
+ raise ChartError(f"Unexpected error creating chart: {str(e)}")
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/data.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/data.py
new file mode 100644
index 00000000..b0b9b86e
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/data.py
@@ -0,0 +1,280 @@
+from pathlib import Path
+from typing import Any, Dict, List, Optional
+import logging
+
+from openpyxl import load_workbook
+from openpyxl.worksheet.worksheet import Worksheet
+from openpyxl.utils import get_column_letter
+
+from .exceptions import DataError
+from .cell_utils import parse_cell_range
+from .cell_validation import get_data_validation_for_cell
+
+logger = logging.getLogger(__name__)
+
+def read_excel_range(
+ filepath: Path | str,
+ sheet_name: str,
+ start_cell: str = "A1",
+ end_cell: Optional[str] = None,
+ preview_only: bool = False
+) -> List[Dict[str, Any]]:
+ """Read data from Excel range with optional preview mode"""
+ try:
+ wb = load_workbook(filepath, read_only=False)
+
+ if sheet_name not in wb.sheetnames:
+ raise DataError(f"Sheet '{sheet_name}' not found")
+
+ ws = wb[sheet_name]
+
+ # Parse start cell
+ if ':' in start_cell:
+ start_cell, end_cell = start_cell.split(':')
+
+ # Get start coordinates
+ try:
+ start_coords = parse_cell_range(f"{start_cell}:{start_cell}")
+ if not start_coords or not all(coord is not None for coord in start_coords[:2]):
+ raise DataError(f"Invalid start cell reference: {start_cell}")
+ start_row, start_col = start_coords[0], start_coords[1]
+ except ValueError as e:
+ raise DataError(f"Invalid start cell format: {str(e)}")
+
+ # Determine end coordinates
+ if end_cell:
+ try:
+ end_coords = parse_cell_range(f"{end_cell}:{end_cell}")
+ if not end_coords or not all(coord is not None for coord in end_coords[:2]):
+ raise DataError(f"Invalid end cell reference: {end_cell}")
+ end_row, end_col = end_coords[0], end_coords[1]
+ except ValueError as e:
+ raise DataError(f"Invalid end cell format: {str(e)}")
+ else:
+ # If no end_cell, use the full data range of the sheet
+ if ws.max_row == 1 and ws.max_column == 1 and ws.cell(1, 1).value is None:
+ # Handle empty sheet
+ end_row, end_col = start_row, start_col
+ else:
+ # Use the sheet's own boundaries
+ start_row, start_col = ws.min_row, ws.min_column
+ end_row, end_col = ws.max_row, ws.max_column
+
+ # Validate range bounds
+ if start_row > ws.max_row or start_col > ws.max_column:
+ # This case can happen if start_cell is outside the used area on a sheet with data
+ # or on a completely empty sheet.
+ logger.warning(
+ f"Start cell {start_cell} is outside the sheet's data boundary "
+ f"({get_column_letter(ws.min_column)}{ws.min_row}:{get_column_letter(ws.max_column)}{ws.max_row}). "
+ f"No data will be read."
+ )
+ return []
+
+ data = []
+ for row in range(start_row, end_row + 1):
+ row_data = []
+ for col in range(start_col, end_col + 1):
+ cell = ws.cell(row=row, column=col)
+ row_data.append(cell.value)
+ if any(v is not None for v in row_data):
+ data.append(row_data)
+
+ wb.close()
+ return data
+ except DataError as e:
+ logger.error(str(e))
+ raise
+ except Exception as e:
+ logger.error(f"Failed to read Excel range: {e}")
+ raise DataError(str(e))
+
+def write_data(
+ filepath: str,
+ sheet_name: Optional[str],
+ data: Optional[List[List]],
+ start_cell: str = "A1",
+) -> Dict[str, str]:
+ """Write data to Excel sheet with workbook handling
+
+ Headers are handled intelligently based on context.
+ """
+ try:
+ if not data:
+ raise DataError("No data provided to write")
+
+ wb = load_workbook(filepath)
+
+ # If no sheet specified, use active sheet
+ if not sheet_name:
+ active_sheet = wb.active
+ if active_sheet is None:
+ raise DataError("No active sheet found in workbook")
+ sheet_name = active_sheet.title
+ elif sheet_name not in wb.sheetnames:
+ wb.create_sheet(sheet_name)
+
+ ws = wb[sheet_name]
+
+ # Validate start cell
+ try:
+ start_coords = parse_cell_range(start_cell)
+ if not start_coords or not all(coord is not None for coord in start_coords[:2]):
+ raise DataError(f"Invalid start cell reference: {start_cell}")
+ except ValueError as e:
+ raise DataError(f"Invalid start cell format: {str(e)}")
+
+ if len(data) > 0:
+ _write_data_to_worksheet(ws, data, start_cell)
+
+ wb.save(filepath)
+ wb.close()
+
+ return {"message": f"Data written to {sheet_name}", "active_sheet": sheet_name}
+ except DataError as e:
+ logger.error(str(e))
+ raise
+ except Exception as e:
+ logger.error(f"Failed to write data: {e}")
+ raise DataError(str(e))
+
+def _write_data_to_worksheet(
+ worksheet: Worksheet,
+ data: List[List],
+ start_cell: str = "A1",
+) -> None:
+ """Write data to worksheet with intelligent header handling"""
+ try:
+ if not data:
+ raise DataError("No data provided to write")
+
+ try:
+ start_coords = parse_cell_range(start_cell)
+ if not start_coords or not all(x is not None for x in start_coords[:2]):
+ raise DataError(f"Invalid start cell reference: {start_cell}")
+ start_row, start_col = start_coords[0], start_coords[1]
+ except ValueError as e:
+ raise DataError(f"Invalid start cell format: {str(e)}")
+
+ # Write data
+ for i, row in enumerate(data):
+ for j, val in enumerate(row):
+ worksheet.cell(row=start_row + i, column=start_col + j, value=val)
+ except DataError as e:
+ logger.error(str(e))
+ raise
+ except Exception as e:
+ logger.error(f"Failed to write worksheet data: {e}")
+ raise DataError(str(e))
+
+def read_excel_range_with_metadata(
+ filepath: Path | str,
+ sheet_name: str,
+ start_cell: str = "A1",
+ end_cell: Optional[str] = None,
+ include_validation: bool = True
+) -> Dict[str, Any]:
+ """Read data from Excel range with cell metadata including validation rules.
+
+ Args:
+ filepath: Path to Excel file
+ sheet_name: Name of worksheet
+ start_cell: Starting cell address
+ end_cell: Ending cell address (optional)
+ include_validation: Whether to include validation metadata
+
+ Returns:
+ Dictionary containing structured cell data with metadata
+ """
+ try:
+ wb = load_workbook(filepath, read_only=False)
+
+ if sheet_name not in wb.sheetnames:
+ raise DataError(f"Sheet '{sheet_name}' not found")
+
+ ws = wb[sheet_name]
+
+ # Parse start cell
+ if ':' in start_cell:
+ start_cell, end_cell = start_cell.split(':')
+
+ # Get start coordinates
+ try:
+ start_coords = parse_cell_range(f"{start_cell}:{start_cell}")
+ if not start_coords or not all(coord is not None for coord in start_coords[:2]):
+ raise DataError(f"Invalid start cell reference: {start_cell}")
+ start_row, start_col = start_coords[0], start_coords[1]
+ except ValueError as e:
+ raise DataError(f"Invalid start cell format: {str(e)}")
+
+ # Determine end coordinates
+ if end_cell:
+ try:
+ end_coords = parse_cell_range(f"{end_cell}:{end_cell}")
+ if not end_coords or not all(coord is not None for coord in end_coords[:2]):
+ raise DataError(f"Invalid end cell reference: {end_cell}")
+ end_row, end_col = end_coords[0], end_coords[1]
+ except ValueError as e:
+ raise DataError(f"Invalid end cell format: {str(e)}")
+ else:
+ # If no end_cell, use the full data range of the sheet
+ if ws.max_row == 1 and ws.max_column == 1 and ws.cell(1, 1).value is None:
+ # Handle empty sheet
+ end_row, end_col = start_row, start_col
+ else:
+ # Use the sheet's own boundaries, but respect the provided start_cell
+ end_row, end_col = ws.max_row, ws.max_column
+ # If start_cell is 'A1' (default), we should find the true start
+ if start_cell == 'A1':
+ start_row, start_col = ws.min_row, ws.min_column
+
+ # Validate range bounds
+ if start_row > ws.max_row or start_col > ws.max_column:
+ # This case can happen if start_cell is outside the used area on a sheet with data
+ # or on a completely empty sheet.
+ logger.warning(
+ f"Start cell {start_cell} is outside the sheet's data boundary "
+ f"({get_column_letter(ws.min_column)}{ws.min_row}:{get_column_letter(ws.max_column)}{ws.max_row}). "
+ f"No data will be read."
+ )
+ return {"range": f"{start_cell}:", "sheet_name": sheet_name, "cells": []}
+
+ # Build structured cell data
+ range_str = f"{get_column_letter(start_col)}{start_row}:{get_column_letter(end_col)}{end_row}"
+ range_data = {
+ "range": range_str,
+ "sheet_name": sheet_name,
+ "cells": []
+ }
+
+ for row in range(start_row, end_row + 1):
+ for col in range(start_col, end_col + 1):
+ cell = ws.cell(row=row, column=col)
+ cell_address = f"{get_column_letter(col)}{row}"
+
+ cell_data = {
+ "address": cell_address,
+ "value": cell.value,
+ "row": row,
+ "column": col
+ }
+
+ # Add validation metadata if requested
+ if include_validation:
+ validation_info = get_data_validation_for_cell(ws, cell_address)
+ if validation_info:
+ cell_data["validation"] = validation_info
+ else:
+ cell_data["validation"] = {"has_validation": False}
+
+ range_data["cells"].append(cell_data)
+
+ wb.close()
+ return range_data
+
+ except DataError as e:
+ logger.error(str(e))
+ raise
+ except Exception as e:
+ logger.error(f"Failed to read Excel range with metadata: {e}")
+ raise DataError(str(e))
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/exceptions.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/exceptions.py
new file mode 100644
index 00000000..80d1afd6
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/exceptions.py
@@ -0,0 +1,35 @@
+class ExcelMCPError(Exception):
+ """Base exception for Excel MCP errors."""
+ pass
+
+class WorkbookError(ExcelMCPError):
+ """Raised when workbook operations fail."""
+ pass
+
+class SheetError(ExcelMCPError):
+ """Raised when sheet operations fail."""
+ pass
+
+class DataError(ExcelMCPError):
+ """Raised when data operations fail."""
+ pass
+
+class ValidationError(ExcelMCPError):
+ """Raised when validation fails."""
+ pass
+
+class FormattingError(ExcelMCPError):
+ """Raised when formatting operations fail."""
+ pass
+
+class CalculationError(ExcelMCPError):
+ """Raised when formula calculations fail."""
+ pass
+
+class PivotError(ExcelMCPError):
+ """Raised when pivot table operations fail."""
+ pass
+
+class ChartError(ExcelMCPError):
+ """Raised when chart operations fail."""
+ pass
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/formatting.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/formatting.py
new file mode 100644
index 00000000..34dd8398
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/formatting.py
@@ -0,0 +1,249 @@
+import logging
+from typing import Any, Dict, Optional
+
+from openpyxl.styles import (
+ PatternFill, Border, Side, Alignment, Protection, Font,
+ Color
+)
+from openpyxl.formatting.rule import (
+ ColorScaleRule, DataBarRule, IconSetRule,
+ FormulaRule, CellIsRule
+)
+
+from .workbook import get_or_create_workbook
+from .cell_utils import parse_cell_range, validate_cell_reference
+from .exceptions import ValidationError, FormattingError
+
+logger = logging.getLogger(__name__)
+
+def format_range(
+ filepath: str,
+ sheet_name: str,
+ start_cell: str,
+ end_cell: Optional[str] = None,
+ bold: bool = False,
+ italic: bool = False,
+ underline: bool = False,
+ font_size: Optional[int] = None,
+ font_color: Optional[str] = None,
+ bg_color: Optional[str] = None,
+ border_style: Optional[str] = None,
+ border_color: Optional[str] = None,
+ number_format: Optional[str] = None,
+ alignment: Optional[str] = None,
+ wrap_text: bool = False,
+ merge_cells: bool = False,
+ protection: Optional[Dict[str, Any]] = None,
+ conditional_format: Optional[Dict[str, Any]] = None
+) -> Dict[str, Any]:
+ """Apply formatting to a range of cells.
+
+ This function handles all Excel formatting operations including:
+ - Font properties (bold, italic, size, color, etc.)
+ - Cell fill/background color
+ - Borders (style and color)
+ - Number formatting
+ - Alignment and text wrapping
+ - Cell merging
+ - Protection
+ - Conditional formatting
+
+ Args:
+ filepath: Path to Excel file
+ sheet_name: Name of worksheet
+ start_cell: Starting cell reference
+ end_cell: Optional ending cell reference
+ bold: Whether to make text bold
+ italic: Whether to make text italic
+ underline: Whether to underline text
+ font_size: Font size in points
+ font_color: Font color (hex code)
+ bg_color: Background color (hex code)
+ border_style: Border style (thin, medium, thick, double)
+ border_color: Border color (hex code)
+ number_format: Excel number format string
+ alignment: Text alignment (left, center, right, justify)
+ wrap_text: Whether to wrap text
+ merge_cells: Whether to merge the range
+ protection: Cell protection settings
+ conditional_format: Conditional formatting rules
+
+ Returns:
+ Dictionary with operation status
+ """
+ try:
+ # Validate cell references
+ if not validate_cell_reference(start_cell):
+ raise ValidationError(f"Invalid start cell reference: {start_cell}")
+
+ if end_cell and not validate_cell_reference(end_cell):
+ raise ValidationError(f"Invalid end cell reference: {end_cell}")
+
+ wb = get_or_create_workbook(filepath)
+ if sheet_name not in wb.sheetnames:
+ raise ValidationError(f"Sheet '{sheet_name}' not found")
+
+ sheet = wb[sheet_name]
+
+ # Get cell range coordinates
+ try:
+ start_row, start_col, end_row, end_col = parse_cell_range(start_cell, end_cell)
+ except ValueError as e:
+ raise ValidationError(f"Invalid cell range: {str(e)}")
+
+ # If no end cell specified, use start cell coordinates
+ if end_row is None:
+ end_row = start_row
+ if end_col is None:
+ end_col = start_col
+
+ # Apply font formatting
+ font_args = {
+ "bold": bold,
+ "italic": italic,
+ "underline": 'single' if underline else None,
+ }
+ if font_size is not None:
+ font_args["size"] = font_size
+ if font_color is not None:
+ try:
+ # Ensure color has FF prefix for full opacity
+ font_color = font_color if font_color.startswith('FF') else f'FF{font_color}'
+ font_args["color"] = Color(rgb=font_color)
+ except ValueError as e:
+ raise FormattingError(f"Invalid font color: {str(e)}")
+ font = Font(**font_args)
+
+ # Apply fill
+ fill = None
+ if bg_color is not None:
+ try:
+ # Ensure color has FF prefix for full opacity
+ bg_color = bg_color if bg_color.startswith('FF') else f'FF{bg_color}'
+ fill = PatternFill(
+ start_color=Color(rgb=bg_color),
+ end_color=Color(rgb=bg_color),
+ fill_type='solid'
+ )
+ except ValueError as e:
+ raise FormattingError(f"Invalid background color: {str(e)}")
+
+ # Apply borders
+ border = None
+ if border_style is not None:
+ try:
+ border_color = border_color if border_color else "000000"
+ border_color = border_color if border_color.startswith('FF') else f'FF{border_color}'
+ side = Side(
+ style=border_style,
+ color=Color(rgb=border_color)
+ )
+ border = Border(
+ left=side,
+ right=side,
+ top=side,
+ bottom=side
+ )
+ except ValueError as e:
+ raise FormattingError(f"Invalid border settings: {str(e)}")
+
+ # Apply alignment
+ align = None
+ if alignment is not None or wrap_text:
+ try:
+ align = Alignment(
+ horizontal=alignment,
+ vertical='center',
+ wrap_text=wrap_text
+ )
+ except ValueError as e:
+ raise FormattingError(f"Invalid alignment settings: {str(e)}")
+
+ # Apply protection
+ protect = None
+ if protection is not None:
+ try:
+ protect = Protection(**protection)
+ except ValueError as e:
+ raise FormattingError(f"Invalid protection settings: {str(e)}")
+
+ # Apply formatting to range
+ for row in range(start_row, end_row + 1):
+ for col in range(start_col, end_col + 1):
+ cell = sheet.cell(row=row, column=col)
+ cell.font = font
+ if fill is not None:
+ cell.fill = fill
+ if border is not None:
+ cell.border = border
+ if align is not None:
+ cell.alignment = align
+ if protect is not None:
+ cell.protection = protect
+ if number_format is not None:
+ cell.number_format = number_format
+
+ # Merge cells if requested
+ if merge_cells and end_cell:
+ try:
+ range_str = f"{start_cell}:{end_cell}"
+ sheet.merge_cells(range_str)
+ except ValueError as e:
+ raise FormattingError(f"Failed to merge cells: {str(e)}")
+
+ # Apply conditional formatting
+ if conditional_format is not None:
+ range_str = f"{start_cell}:{end_cell}" if end_cell else start_cell
+ rule_type = conditional_format.get('type')
+ if not rule_type:
+ raise FormattingError("Conditional format type not specified")
+
+ params = conditional_format.get('params', {})
+
+ # Handle fill parameter for cell_is rule
+ if rule_type == 'cell_is' and 'fill' in params:
+ fill_params = params['fill']
+ if isinstance(fill_params, dict):
+ try:
+ fill_color = fill_params.get('fgColor', 'FFC7CE') # Default to light red
+ fill_color = fill_color if fill_color.startswith('FF') else f'FF{fill_color}'
+ params['fill'] = PatternFill(
+ start_color=fill_color,
+ end_color=fill_color,
+ fill_type='solid'
+ )
+ except ValueError as e:
+ raise FormattingError(f"Invalid conditional format fill color: {str(e)}")
+
+ try:
+ if rule_type == 'color_scale':
+ rule = ColorScaleRule(**params)
+ elif rule_type == 'data_bar':
+ rule = DataBarRule(**params)
+ elif rule_type == 'icon_set':
+ rule = IconSetRule(**params)
+ elif rule_type == 'formula':
+ rule = FormulaRule(**params)
+ elif rule_type == 'cell_is':
+ rule = CellIsRule(**params)
+ else:
+ raise FormattingError(f"Invalid conditional format type: {rule_type}")
+
+ sheet.conditional_formatting.add(range_str, rule)
+ except Exception as e:
+ raise FormattingError(f"Failed to apply conditional formatting: {str(e)}")
+
+ wb.save(filepath)
+
+ range_str = f"{start_cell}:{end_cell}" if end_cell else start_cell
+ return {
+ "message": f"Applied formatting to range {range_str}",
+ "range": range_str
+ }
+
+ except (ValidationError, FormattingError) as e:
+ logger.error(str(e))
+ raise
+ except Exception as e:
+ logger.error(f"Failed to apply formatting: {e}")
+ raise FormattingError(str(e))
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/pivot.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/pivot.py
new file mode 100644
index 00000000..9e141d0b
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/pivot.py
@@ -0,0 +1,270 @@
+from typing import Any
+import uuid
+import logging
+
+from openpyxl import load_workbook
+from openpyxl.utils import get_column_letter
+from openpyxl.worksheet.table import Table, TableStyleInfo
+from openpyxl.styles import Font
+
+from .data import read_excel_range
+from .cell_utils import parse_cell_range
+from .exceptions import ValidationError, PivotError
+
+logger = logging.getLogger(__name__)
+
+def create_pivot_table(
+ filepath: str,
+ sheet_name: str,
+ data_range: str,
+ rows: list[str],
+ values: list[str],
+ columns: list[str] | None = None,
+ agg_func: str = "sum"
+) -> dict[str, Any]:
+ """Create pivot table in sheet using Excel table functionality
+
+ Args:
+ filepath: Path to Excel file
+ sheet_name: Name of worksheet containing source data
+ data_range: Source data range reference
+ target_cell: Cell reference for pivot table position
+ rows: Fields for row labels
+ values: Fields for values
+ columns: Optional fields for column labels
+ agg_func: Aggregation function (sum, count, average, max, min)
+
+ Returns:
+ Dictionary with status message and pivot table dimensions
+ """
+ try:
+ wb = load_workbook(filepath)
+ if sheet_name not in wb.sheetnames:
+ raise ValidationError(f"Sheet '{sheet_name}' not found")
+
+ # Parse ranges
+ if ':' not in data_range:
+ raise ValidationError("Data range must be in format 'A1:B2'")
+
+ try:
+ start_cell, end_cell = data_range.split(':')
+ start_row, start_col, end_row, end_col = parse_cell_range(start_cell, end_cell)
+ except ValueError as e:
+ raise ValidationError(f"Invalid data range format: {str(e)}")
+
+ if end_row is None or end_col is None:
+ raise ValidationError("Invalid data range format: missing end coordinates")
+
+ # Create range string
+ data_range_str = f"{get_column_letter(start_col)}{start_row}:{get_column_letter(end_col)}{end_row}"
+
+ # Clean up field names by removing aggregation suffixes
+ def clean_field_name(field: str) -> str:
+ field = str(field).strip()
+ for suffix in [" (sum)", " (average)", " (count)", " (min)", " (max)"]:
+ if field.lower().endswith(suffix):
+ return field[:-len(suffix)]
+ return field
+
+ # Read source data and convert to list of dicts
+ try:
+ data_as_list = read_excel_range(filepath, sheet_name, start_cell, end_cell)
+ if not data_as_list or len(data_as_list) < 2:
+ raise PivotError("Source data must have a header row and at least one data row.")
+
+ headers = [str(h) for h in data_as_list[0]]
+ data = [dict(zip(headers, row)) for row in data_as_list[1:]]
+
+ if not data:
+ raise PivotError("No data rows found after header.")
+
+ except Exception as e:
+ raise PivotError(f"Failed to read or process source data: {str(e)}")
+
+ # Validate aggregation function
+ valid_agg_funcs = ["sum", "average", "count", "min", "max"]
+ if agg_func.lower() not in valid_agg_funcs:
+ raise ValidationError(
+ f"Invalid aggregation function. Must be one of: {', '.join(valid_agg_funcs)}"
+ )
+
+ # Validate field names exist in data
+ if data:
+ available_fields_raw = data[0].keys()
+ available_fields = {clean_field_name(str(header)).lower() for header in available_fields_raw}
+
+ for field_list, field_type in [(rows, "row"), (values, "value")]:
+ for field in field_list:
+ if clean_field_name(str(field)).lower() not in available_fields:
+ raise ValidationError(
+ f"Invalid {field_type} field '{field}'. "
+ f"Available fields: {', '.join(sorted(available_fields_raw))}"
+ )
+
+ if columns:
+ for field in columns:
+ if clean_field_name(str(field)).lower() not in available_fields:
+ raise ValidationError(
+ f"Invalid column field '{field}'. "
+ f"Available fields: {', '.join(sorted(available_fields_raw))}"
+ )
+
+ # Clean up row and value field names
+ cleaned_rows = [clean_field_name(field) for field in rows]
+ cleaned_values = [clean_field_name(field) for field in values]
+
+ # Create pivot sheet
+ pivot_sheet_name = f"{sheet_name}_pivot"
+ if pivot_sheet_name in wb.sheetnames:
+ wb.remove(wb[pivot_sheet_name])
+ pivot_ws = wb.create_sheet(pivot_sheet_name)
+
+ # Write headers
+ current_row = 1
+ current_col = 1
+
+ # Write row field headers
+ for field in cleaned_rows:
+ cell = pivot_ws.cell(row=current_row, column=current_col, value=field)
+ cell.font = Font(bold=True)
+ current_col += 1
+
+ # Write value field headers
+ for field in cleaned_values:
+ cell = pivot_ws.cell(row=current_row, column=current_col, value=f"{field} ({agg_func})")
+ cell.font = Font(bold=True)
+ current_col += 1
+
+ # Get unique values for each row field
+ field_values = {}
+ for field in cleaned_rows:
+ all_values = []
+ for record in data:
+ value = str(record.get(field, ''))
+ all_values.append(value)
+ field_values[field] = sorted(set(all_values))
+
+ # Generate all combinations of row field values
+ row_combinations = _get_combinations(field_values)
+
+ # Calculate table dimensions for formatting
+ total_rows = len(row_combinations) + 1 # +1 for header
+ total_cols = len(cleaned_rows) + len(cleaned_values)
+
+ # Write data rows
+ current_row = 2
+ for combo in row_combinations:
+ # Write row field values
+ col = 1
+ for field in cleaned_rows:
+ pivot_ws.cell(row=current_row, column=col, value=combo[field])
+ col += 1
+
+ # Filter data for current combination
+ filtered_data = _filter_data(data, combo, {})
+
+ # Calculate and write aggregated values
+ for value_field in cleaned_values:
+ try:
+ value = _aggregate_values(filtered_data, value_field, agg_func)
+ pivot_ws.cell(row=current_row, column=col, value=value)
+ except Exception as e:
+ raise PivotError(f"Failed to aggregate values for field '{value_field}': {str(e)}")
+ col += 1
+
+ current_row += 1
+
+ # Create a table for the pivot data
+ try:
+ pivot_range = f"A1:{get_column_letter(total_cols)}{total_rows}"
+ pivot_table = Table(
+ displayName=f"PivotTable_{uuid.uuid4().hex[:8]}",
+ ref=pivot_range
+ )
+ style = TableStyleInfo(
+ name="TableStyleMedium9",
+ showFirstColumn=False,
+ showLastColumn=False,
+ showRowStripes=True,
+ showColumnStripes=True
+ )
+ pivot_table.tableStyleInfo = style
+ pivot_ws.add_table(pivot_table)
+ except Exception as e:
+ raise PivotError(f"Failed to create pivot table formatting: {str(e)}")
+
+ try:
+ wb.save(filepath)
+ except Exception as e:
+ raise PivotError(f"Failed to save workbook: {str(e)}")
+
+ return {
+ "message": "Summary table created successfully",
+ "details": {
+ "source_range": data_range_str,
+ "pivot_sheet": pivot_sheet_name,
+ "rows": cleaned_rows,
+ "columns": columns or [],
+ "values": cleaned_values,
+ "aggregation": agg_func
+ }
+ }
+
+ except (ValidationError, PivotError) as e:
+ logger.error(str(e))
+ raise
+ except Exception as e:
+ logger.error(f"Failed to create pivot table: {e}")
+ raise PivotError(str(e))
+
+
+def _get_combinations(field_values: dict[str, set]) -> list[dict]:
+ """Get all combinations of field values."""
+ result = [{}]
+ for field, values in list(field_values.items()): # Convert to list to avoid runtime changes
+ new_result = []
+ for combo in result:
+ for value in sorted(values): # Sort for consistent ordering
+ new_combo = combo.copy()
+ new_combo[field] = value
+ new_result.append(new_combo)
+ result = new_result
+ return result
+
+
+def _filter_data(data: list[dict], row_filters: dict, col_filters: dict) -> list[dict]:
+ """Filter data based on row and column filters."""
+ result = []
+ for record in data:
+ matches = True
+ for field, value in row_filters.items():
+ if record.get(field) != value:
+ matches = False
+ break
+ for field, value in col_filters.items():
+ if record.get(field) != value:
+ matches = False
+ break
+ if matches:
+ result.append(record)
+ return result
+
+
+def _aggregate_values(data: list[dict], field: str, agg_func: str) -> float:
+ """Aggregate values using the specified function."""
+ values = [record[field] for record in data if field in record and isinstance(record[field], (int, float))]
+ if not values:
+ return 0
+
+ if agg_func == "sum":
+ return sum(values)
+ elif agg_func == "average":
+ return sum(values) / len(values)
+ elif agg_func == "count":
+ return len(values)
+ elif agg_func == "min":
+ return min(values)
+ elif agg_func == "max":
+ return max(values)
+ else:
+ return sum(values) # Default to sum
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/server.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/server.py
new file mode 100644
index 00000000..dcfccf9b
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/server.py
@@ -0,0 +1,848 @@
+import logging
+import os
+from typing import Any, List, Dict, Optional
+
+from mcp.server.fastmcp import FastMCP
+from mcp.types import ToolAnnotations
+
+# Import exceptions
+from excel_mcp.exceptions import (
+ ValidationError,
+ WorkbookError,
+ SheetError,
+ DataError,
+ FormattingError,
+ CalculationError,
+ PivotError,
+ ChartError
+)
+
+# Import from excel_mcp package with consistent _impl suffixes
+from excel_mcp.validation import (
+ validate_formula_in_cell_operation as validate_formula_impl,
+ validate_range_in_sheet_operation as validate_range_impl
+)
+from excel_mcp.chart import create_chart_in_sheet as create_chart_impl
+from excel_mcp.workbook import get_workbook_info
+from excel_mcp.data import write_data
+from excel_mcp.pivot import create_pivot_table as create_pivot_table_impl
+from excel_mcp.tables import create_excel_table as create_table_impl
+from excel_mcp.sheet import (
+ copy_sheet,
+ delete_sheet,
+ rename_sheet,
+ merge_range,
+ unmerge_range,
+ get_merged_ranges,
+ insert_row,
+ insert_cols,
+ delete_rows,
+ delete_cols,
+)
+
+# Get project root directory path for log file path.
+# When using the stdio transmission method,
+# relative paths may cause log files to fail to create
+# due to the client's running location and permission issues,
+# resulting in the program not being able to run.
+# Thus using os.path.join(ROOT_DIR, "excel-mcp.log") instead.
+
+ROOT_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+LOG_FILE = os.path.join(ROOT_DIR, "excel-mcp.log")
+
+# Initialize EXCEL_FILES_PATH variable without assigning a value
+EXCEL_FILES_PATH = None
+
+# Configure logging
+logging.basicConfig(
+ level=logging.INFO,
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
+ handlers=[
+ # Referring to https://github.com/modelcontextprotocol/python-sdk/issues/409#issuecomment-2816831318
+ # The stdio mode server MUST NOT write anything to its stdout that is not a valid MCP message.
+ logging.FileHandler(LOG_FILE)
+ ],
+)
+logger = logging.getLogger("excel-mcp")
+# Initialize FastMCP server
+mcp = FastMCP(
+ "excel-mcp",
+ host=os.environ.get("FASTMCP_HOST", "0.0.0.0"),
+ port=int(os.environ.get("FASTMCP_PORT", "8017")),
+ instructions="Excel MCP Server for manipulating Excel files"
+)
+
+def get_excel_path(filename: str) -> str:
+ """Get full path to Excel file.
+
+ Args:
+ filename: Name of Excel file
+
+ Returns:
+ Full path to Excel file
+ """
+ # If filename is already an absolute path, return it
+ if os.path.isabs(filename):
+ return filename
+
+ # Check if in SSE mode (EXCEL_FILES_PATH is not None)
+ if EXCEL_FILES_PATH is None:
+ # Must use absolute path
+ raise ValueError(f"Invalid filename: {filename}, must be an absolute path when not in SSE mode")
+
+ # In SSE mode, if it's a relative path, resolve it based on EXCEL_FILES_PATH
+ return os.path.join(EXCEL_FILES_PATH, filename)
+
+@mcp.tool(
+ annotations=ToolAnnotations(
+ title="Apply Formula",
+ destructiveHint=True,
+ ),
+)
+def apply_formula(
+ filepath: str,
+ sheet_name: str,
+ cell: str,
+ formula: str,
+) -> str:
+ """
+ Apply Excel formula to cell.
+ Excel formula will write to cell with verification.
+ """
+ try:
+ full_path = get_excel_path(filepath)
+ # First validate the formula
+ validation = validate_formula_impl(full_path, sheet_name, cell, formula)
+ if isinstance(validation, dict) and "error" in validation:
+ return f"Error: {validation['error']}"
+
+ # If valid, apply the formula
+ from excel_mcp.calculations import apply_formula as apply_formula_impl
+ result = apply_formula_impl(full_path, sheet_name, cell, formula)
+ return result["message"]
+ except (ValidationError, CalculationError) as e:
+ return f"Error: {str(e)}"
+ except Exception as e:
+ logger.error(f"Error applying formula: {e}")
+ raise
+
+@mcp.tool(
+ annotations=ToolAnnotations(
+ title="Validate Formula Syntax",
+ readOnlyHint=True,
+ ),
+)
+def validate_formula_syntax(
+ filepath: str,
+ sheet_name: str,
+ cell: str,
+ formula: str,
+) -> str:
+ """Validate Excel formula syntax without applying it."""
+ try:
+ full_path = get_excel_path(filepath)
+ result = validate_formula_impl(full_path, sheet_name, cell, formula)
+ return result["message"]
+ except (ValidationError, CalculationError) as e:
+ return f"Error: {str(e)}"
+ except Exception as e:
+ logger.error(f"Error validating formula: {e}")
+ raise
+
+@mcp.tool(
+ annotations=ToolAnnotations(
+ title="Format Range",
+ destructiveHint=True,
+ ),
+)
+def format_range(
+ filepath: str,
+ sheet_name: str,
+ start_cell: str,
+ end_cell: Optional[str] = None,
+ bold: bool = False,
+ italic: bool = False,
+ underline: bool = False,
+ font_size: Optional[int] = None,
+ font_color: Optional[str] = None,
+ bg_color: Optional[str] = None,
+ border_style: Optional[str] = None,
+ border_color: Optional[str] = None,
+ number_format: Optional[str] = None,
+ alignment: Optional[str] = None,
+ wrap_text: bool = False,
+ merge_cells: bool = False,
+ protection: Optional[Dict[str, Any]] = None,
+ conditional_format: Optional[Dict[str, Any]] = None
+) -> str:
+ """Apply formatting to a range of cells."""
+ try:
+ full_path = get_excel_path(filepath)
+ from excel_mcp.formatting import format_range as format_range_func
+
+ # Convert None values to appropriate defaults for the underlying function
+ format_range_func(
+ filepath=full_path,
+ sheet_name=sheet_name,
+ start_cell=start_cell,
+ end_cell=end_cell, # This can be None
+ bold=bold,
+ italic=italic,
+ underline=underline,
+ font_size=font_size, # This can be None
+ font_color=font_color, # This can be None
+ bg_color=bg_color, # This can be None
+ border_style=border_style, # This can be None
+ border_color=border_color, # This can be None
+ number_format=number_format, # This can be None
+ alignment=alignment, # This can be None
+ wrap_text=wrap_text,
+ merge_cells=merge_cells,
+ protection=protection, # This can be None
+ conditional_format=conditional_format # This can be None
+ )
+ return "Range formatted successfully"
+ except (ValidationError, FormattingError) as e:
+ return f"Error: {str(e)}"
+ except Exception as e:
+ logger.error(f"Error formatting range: {e}")
+ raise
+
+@mcp.tool(
+ annotations=ToolAnnotations(
+ title="Read Data from Excel",
+ readOnlyHint=True,
+ ),
+)
+def read_data_from_excel(
+ filepath: str,
+ sheet_name: str,
+ start_cell: str = "A1",
+ end_cell: Optional[str] = None,
+ preview_only: bool = False
+) -> str:
+ """
+ Read data from Excel worksheet with cell metadata including validation rules.
+
+ Args:
+ filepath: Path to Excel file
+ sheet_name: Name of worksheet
+ start_cell: Starting cell (default A1)
+ end_cell: Ending cell (optional, auto-expands if not provided)
+ preview_only: Whether to return preview only
+
+ Returns:
+ JSON string containing structured cell data with validation metadata.
+ Each cell includes: address, value, row, column, and validation info (if any).
+ """
+ try:
+ full_path = get_excel_path(filepath)
+ from excel_mcp.data import read_excel_range_with_metadata
+ result = read_excel_range_with_metadata(
+ full_path,
+ sheet_name,
+ start_cell,
+ end_cell
+ )
+ if not result or not result.get("cells"):
+ return "No data found in specified range"
+
+ # Return as formatted JSON string
+ import json
+ return json.dumps(result, indent=2, default=str)
+
+ except Exception as e:
+ logger.error(f"Error reading data: {e}")
+ raise
+
+@mcp.tool(
+ annotations=ToolAnnotations(
+ title="Write Data to Excel",
+ destructiveHint=True,
+ ),
+)
+def write_data_to_excel(
+ filepath: str,
+ sheet_name: str,
+ data: List[List],
+ start_cell: str = "A1",
+) -> str:
+ """
+ Write data to Excel worksheet.
+ Excel formula will write to cell without any verification.
+
+ PARAMETERS:
+ filepath: Path to Excel file
+ sheet_name: Name of worksheet to write to
+ data: List of lists containing data to write to the worksheet, sublists are assumed to be rows
+ start_cell: Cell to start writing to, default is "A1"
+
+ """
+ try:
+ full_path = get_excel_path(filepath)
+ result = write_data(full_path, sheet_name, data, start_cell)
+ return result["message"]
+ except (ValidationError, DataError) as e:
+ return f"Error: {str(e)}"
+ except Exception as e:
+ logger.error(f"Error writing data: {e}")
+ raise
+
+@mcp.tool(
+ annotations=ToolAnnotations(
+ title="Create Workbook",
+ destructiveHint=True,
+ ),
+)
+def create_workbook(filepath: str) -> str:
+ """Create new Excel workbook."""
+ try:
+ full_path = get_excel_path(filepath)
+ from excel_mcp.workbook import create_workbook as create_workbook_impl
+ create_workbook_impl(full_path)
+ return f"Created workbook at {full_path}"
+ except WorkbookError as e:
+ return f"Error: {str(e)}"
+ except Exception as e:
+ logger.error(f"Error creating workbook: {e}")
+ raise
+
+@mcp.tool(
+ annotations=ToolAnnotations(
+ title="Create Worksheet",
+ destructiveHint=True,
+ ),
+)
+def create_worksheet(filepath: str, sheet_name: str) -> str:
+ """Create new worksheet in workbook."""
+ try:
+ full_path = get_excel_path(filepath)
+ from excel_mcp.workbook import create_sheet as create_worksheet_impl
+ result = create_worksheet_impl(full_path, sheet_name)
+ return result["message"]
+ except (ValidationError, WorkbookError) as e:
+ return f"Error: {str(e)}"
+ except Exception as e:
+ logger.error(f"Error creating worksheet: {e}")
+ raise
+
+@mcp.tool(
+ annotations=ToolAnnotations(
+ title="Create Chart",
+ destructiveHint=True,
+ ),
+)
+def create_chart(
+ filepath: str,
+ sheet_name: str,
+ data_range: str,
+ chart_type: str,
+ target_cell: str,
+ title: str = "",
+ x_axis: str = "",
+ y_axis: str = ""
+) -> str:
+ """Create chart in worksheet."""
+ try:
+ full_path = get_excel_path(filepath)
+ result = create_chart_impl(
+ filepath=full_path,
+ sheet_name=sheet_name,
+ data_range=data_range,
+ chart_type=chart_type,
+ target_cell=target_cell,
+ title=title,
+ x_axis=x_axis,
+ y_axis=y_axis
+ )
+ return result["message"]
+ except (ValidationError, ChartError) as e:
+ return f"Error: {str(e)}"
+ except Exception as e:
+ logger.error(f"Error creating chart: {e}")
+ raise
+
+@mcp.tool(
+ annotations=ToolAnnotations(
+ title="Create Pivot Table",
+ destructiveHint=True,
+ ),
+)
+def create_pivot_table(
+ filepath: str,
+ sheet_name: str,
+ data_range: str,
+ rows: List[str],
+ values: List[str],
+ columns: Optional[List[str]] = None,
+ agg_func: str = "mean"
+) -> str:
+ """Create pivot table in worksheet."""
+ try:
+ full_path = get_excel_path(filepath)
+ result = create_pivot_table_impl(
+ filepath=full_path,
+ sheet_name=sheet_name,
+ data_range=data_range,
+ rows=rows,
+ values=values,
+ columns=columns or [],
+ agg_func=agg_func
+ )
+ return result["message"]
+ except (ValidationError, PivotError) as e:
+ return f"Error: {str(e)}"
+ except Exception as e:
+ logger.error(f"Error creating pivot table: {e}")
+ raise
+
+@mcp.tool(
+ annotations=ToolAnnotations(
+ title="Create Table",
+ destructiveHint=True,
+ ),
+)
+def create_table(
+ filepath: str,
+ sheet_name: str,
+ data_range: str,
+ table_name: Optional[str] = None,
+ table_style: str = "TableStyleMedium9"
+) -> str:
+ """Creates a native Excel table from a specified range of data."""
+ try:
+ full_path = get_excel_path(filepath)
+ result = create_table_impl(
+ filepath=full_path,
+ sheet_name=sheet_name,
+ data_range=data_range,
+ table_name=table_name,
+ table_style=table_style
+ )
+ return result["message"]
+ except DataError as e:
+ return f"Error: {str(e)}"
+ except Exception as e:
+ logger.error(f"Error creating table: {e}")
+ raise
+
+@mcp.tool(
+ annotations=ToolAnnotations(
+ title="Copy Worksheet",
+ destructiveHint=True,
+ ),
+)
+def copy_worksheet(
+ filepath: str,
+ source_sheet: str,
+ target_sheet: str
+) -> str:
+ """Copy worksheet within workbook."""
+ try:
+ full_path = get_excel_path(filepath)
+ result = copy_sheet(full_path, source_sheet, target_sheet)
+ return result["message"]
+ except (ValidationError, SheetError) as e:
+ return f"Error: {str(e)}"
+ except Exception as e:
+ logger.error(f"Error copying worksheet: {e}")
+ raise
+
+@mcp.tool(
+ annotations=ToolAnnotations(
+ title="Delete Worksheet",
+ destructiveHint=True,
+ ),
+)
+def delete_worksheet(
+ filepath: str,
+ sheet_name: str
+) -> str:
+ """Delete worksheet from workbook."""
+ try:
+ full_path = get_excel_path(filepath)
+ result = delete_sheet(full_path, sheet_name)
+ return result["message"]
+ except (ValidationError, SheetError) as e:
+ return f"Error: {str(e)}"
+ except Exception as e:
+ logger.error(f"Error deleting worksheet: {e}")
+ raise
+
+@mcp.tool(
+ annotations=ToolAnnotations(
+ title="Rename Worksheet",
+ destructiveHint=True,
+ ),
+)
+def rename_worksheet(
+ filepath: str,
+ old_name: str,
+ new_name: str
+) -> str:
+ """Rename worksheet in workbook."""
+ try:
+ full_path = get_excel_path(filepath)
+ result = rename_sheet(full_path, old_name, new_name)
+ return result["message"]
+ except (ValidationError, SheetError) as e:
+ return f"Error: {str(e)}"
+ except Exception as e:
+ logger.error(f"Error renaming worksheet: {e}")
+ raise
+
+@mcp.tool(
+ annotations=ToolAnnotations(
+ title="Get Workbook Metadata",
+ readOnlyHint=True,
+ ),
+)
+def get_workbook_metadata(
+ filepath: str,
+ include_ranges: bool = False
+) -> str:
+ """Get metadata about workbook including sheets, ranges, etc."""
+ try:
+ full_path = get_excel_path(filepath)
+ result = get_workbook_info(full_path, include_ranges=include_ranges)
+ return str(result)
+ except WorkbookError as e:
+ return f"Error: {str(e)}"
+ except Exception as e:
+ logger.error(f"Error getting workbook metadata: {e}")
+ raise
+
+@mcp.tool(
+ annotations=ToolAnnotations(
+ title="Merge Cells",
+ destructiveHint=True,
+ ),
+)
+def merge_cells(filepath: str, sheet_name: str, start_cell: str, end_cell: str) -> str:
+ """Merge a range of cells."""
+ try:
+ full_path = get_excel_path(filepath)
+ result = merge_range(full_path, sheet_name, start_cell, end_cell)
+ return result["message"]
+ except (ValidationError, SheetError) as e:
+ return f"Error: {str(e)}"
+ except Exception as e:
+ logger.error(f"Error merging cells: {e}")
+ raise
+
+@mcp.tool(
+ annotations=ToolAnnotations(
+ title="Unmerge Cells",
+ destructiveHint=True,
+ ),
+)
+def unmerge_cells(filepath: str, sheet_name: str, start_cell: str, end_cell: str) -> str:
+ """Unmerge a range of cells."""
+ try:
+ full_path = get_excel_path(filepath)
+ result = unmerge_range(full_path, sheet_name, start_cell, end_cell)
+ return result["message"]
+ except (ValidationError, SheetError) as e:
+ return f"Error: {str(e)}"
+ except Exception as e:
+ logger.error(f"Error unmerging cells: {e}")
+ raise
+
+@mcp.tool(
+ annotations=ToolAnnotations(
+ title="Get Merged Cells",
+ readOnlyHint=True,
+ ),
+)
+def get_merged_cells(filepath: str, sheet_name: str) -> str:
+ """Get merged cells in a worksheet."""
+ try:
+ full_path = get_excel_path(filepath)
+ return str(get_merged_ranges(full_path, sheet_name))
+ except (ValidationError, SheetError) as e:
+ return f"Error: {str(e)}"
+ except Exception as e:
+ logger.error(f"Error getting merged cells: {e}")
+ raise
+
+@mcp.tool(
+ annotations=ToolAnnotations(
+ title="Copy Range",
+ destructiveHint=True,
+ ),
+)
+def copy_range(
+ filepath: str,
+ sheet_name: str,
+ source_start: str,
+ source_end: str,
+ target_start: str,
+ target_sheet: Optional[str] = None
+) -> str:
+ """Copy a range of cells to another location."""
+ try:
+ full_path = get_excel_path(filepath)
+ from excel_mcp.sheet import copy_range_operation
+ result = copy_range_operation(
+ full_path,
+ sheet_name,
+ source_start,
+ source_end,
+ target_start,
+ target_sheet or sheet_name # Use source sheet if target_sheet is None
+ )
+ return result["message"]
+ except (ValidationError, SheetError) as e:
+ return f"Error: {str(e)}"
+ except Exception as e:
+ logger.error(f"Error copying range: {e}")
+ raise
+
+@mcp.tool(
+ annotations=ToolAnnotations(
+ title="Delete Range",
+ destructiveHint=True,
+ ),
+)
+def delete_range(
+ filepath: str,
+ sheet_name: str,
+ start_cell: str,
+ end_cell: str,
+ shift_direction: str = "up"
+) -> str:
+ """Delete a range of cells and shift remaining cells."""
+ try:
+ full_path = get_excel_path(filepath)
+ from excel_mcp.sheet import delete_range_operation
+ result = delete_range_operation(
+ full_path,
+ sheet_name,
+ start_cell,
+ end_cell,
+ shift_direction
+ )
+ return result["message"]
+ except (ValidationError, SheetError) as e:
+ return f"Error: {str(e)}"
+ except Exception as e:
+ logger.error(f"Error deleting range: {e}")
+ raise
+
+@mcp.tool(
+ annotations=ToolAnnotations(
+ title="Validate Excel Range",
+ readOnlyHint=True,
+ ),
+)
+def validate_excel_range(
+ filepath: str,
+ sheet_name: str,
+ start_cell: str,
+ end_cell: Optional[str] = None
+) -> str:
+ """Validate if a range exists and is properly formatted."""
+ try:
+ full_path = get_excel_path(filepath)
+ range_str = start_cell if not end_cell else f"{start_cell}:{end_cell}"
+ result = validate_range_impl(full_path, sheet_name, range_str)
+ return result["message"]
+ except ValidationError as e:
+ return f"Error: {str(e)}"
+ except Exception as e:
+ logger.error(f"Error validating range: {e}")
+ raise
+
+@mcp.tool(
+ annotations=ToolAnnotations(
+ title="Get Data Validation Info",
+ readOnlyHint=True,
+ ),
+)
+def get_data_validation_info(
+ filepath: str,
+ sheet_name: str
+) -> str:
+ """
+ Get all data validation rules in a worksheet.
+
+ This tool helps identify which cell ranges have validation rules
+ and what types of validation are applied.
+
+ Args:
+ filepath: Path to Excel file
+ sheet_name: Name of worksheet
+
+ Returns:
+ JSON string containing all validation rules in the worksheet
+ """
+ try:
+ full_path = get_excel_path(filepath)
+ from openpyxl import load_workbook
+ from excel_mcp.cell_validation import get_all_validation_ranges
+
+ wb = load_workbook(full_path, read_only=False)
+ if sheet_name not in wb.sheetnames:
+ return f"Error: Sheet '{sheet_name}' not found"
+
+ ws = wb[sheet_name]
+ validations = get_all_validation_ranges(ws)
+ wb.close()
+
+ if not validations:
+ return "No data validation rules found in this worksheet"
+
+ import json
+ return json.dumps({
+ "sheet_name": sheet_name,
+ "validation_rules": validations
+ }, indent=2, default=str)
+
+ except Exception as e:
+ logger.error(f"Error getting validation info: {e}")
+ raise
+
+@mcp.tool(
+ annotations=ToolAnnotations(
+ title="Insert Rows",
+ destructiveHint=True,
+ ),
+)
+def insert_rows(
+ filepath: str,
+ sheet_name: str,
+ start_row: int,
+ count: int = 1
+) -> str:
+ """Insert one or more rows starting at the specified row."""
+ try:
+ full_path = get_excel_path(filepath)
+ result = insert_row(full_path, sheet_name, start_row, count)
+ return result["message"]
+ except (ValidationError, SheetError) as e:
+ return f"Error: {str(e)}"
+ except Exception as e:
+ logger.error(f"Error inserting rows: {e}")
+ raise
+
+@mcp.tool(
+ annotations=ToolAnnotations(
+ title="Insert Columns",
+ destructiveHint=True,
+ ),
+)
+def insert_columns(
+ filepath: str,
+ sheet_name: str,
+ start_col: int,
+ count: int = 1
+) -> str:
+ """Insert one or more columns starting at the specified column."""
+ try:
+ full_path = get_excel_path(filepath)
+ result = insert_cols(full_path, sheet_name, start_col, count)
+ return result["message"]
+ except (ValidationError, SheetError) as e:
+ return f"Error: {str(e)}"
+ except Exception as e:
+ logger.error(f"Error inserting columns: {e}")
+ raise
+
+@mcp.tool(
+ annotations=ToolAnnotations(
+ title="Delete Rows",
+ destructiveHint=True,
+ ),
+)
+def delete_sheet_rows(
+ filepath: str,
+ sheet_name: str,
+ start_row: int,
+ count: int = 1
+) -> str:
+ """Delete one or more rows starting at the specified row."""
+ try:
+ full_path = get_excel_path(filepath)
+ result = delete_rows(full_path, sheet_name, start_row, count)
+ return result["message"]
+ except (ValidationError, SheetError) as e:
+ return f"Error: {str(e)}"
+ except Exception as e:
+ logger.error(f"Error deleting rows: {e}")
+ raise
+
+@mcp.tool(
+ annotations=ToolAnnotations(
+ title="Delete Columns",
+ destructiveHint=True,
+ ),
+)
+def delete_sheet_columns(
+ filepath: str,
+ sheet_name: str,
+ start_col: int,
+ count: int = 1
+) -> str:
+ """Delete one or more columns starting at the specified column."""
+ try:
+ full_path = get_excel_path(filepath)
+ result = delete_cols(full_path, sheet_name, start_col, count)
+ return result["message"]
+ except (ValidationError, SheetError) as e:
+ return f"Error: {str(e)}"
+ except Exception as e:
+ logger.error(f"Error deleting columns: {e}")
+ raise
+
+def run_sse():
+ """Run Excel MCP server in SSE mode."""
+ # Assign value to EXCEL_FILES_PATH in SSE mode
+ global EXCEL_FILES_PATH
+ EXCEL_FILES_PATH = os.environ.get("EXCEL_FILES_PATH", "./excel_files")
+ # Create directory if it doesn't exist
+ os.makedirs(EXCEL_FILES_PATH, exist_ok=True)
+
+ try:
+ logger.info(f"Starting Excel MCP server with SSE transport (files directory: {EXCEL_FILES_PATH})")
+ mcp.run(transport="sse")
+ except KeyboardInterrupt:
+ logger.info("Server stopped by user")
+ except Exception as e:
+ logger.error(f"Server failed: {e}")
+ raise
+ finally:
+ logger.info("Server shutdown complete")
+
+def run_streamable_http():
+ """Run Excel MCP server in streamable HTTP mode."""
+ # Assign value to EXCEL_FILES_PATH in streamable HTTP mode
+ global EXCEL_FILES_PATH
+ EXCEL_FILES_PATH = os.environ.get("EXCEL_FILES_PATH", "./excel_files")
+ # Create directory if it doesn't exist
+ os.makedirs(EXCEL_FILES_PATH, exist_ok=True)
+
+ try:
+ logger.info(f"Starting Excel MCP server with streamable HTTP transport (files directory: {EXCEL_FILES_PATH})")
+ mcp.run(transport="streamable-http")
+ except KeyboardInterrupt:
+ logger.info("Server stopped by user")
+ except Exception as e:
+ logger.error(f"Server failed: {e}")
+ raise
+ finally:
+ logger.info("Server shutdown complete")
+
+def run_stdio():
+ """Run Excel MCP server in stdio mode."""
+ # No need to assign EXCEL_FILES_PATH in stdio mode
+
+ try:
+ logger.info("Starting Excel MCP server with stdio transport")
+ mcp.run(transport="stdio")
+ except KeyboardInterrupt:
+ logger.info("Server stopped by user")
+ except Exception as e:
+ logger.error(f"Server failed: {e}")
+ raise
+ finally:
+ logger.info("Server shutdown complete")
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/sheet.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/sheet.py
new file mode 100644
index 00000000..357a32dd
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/sheet.py
@@ -0,0 +1,475 @@
+import logging
+from typing import Any, Dict, Optional
+from copy import copy
+
+from openpyxl import load_workbook
+from openpyxl.worksheet.worksheet import Worksheet
+from openpyxl.utils import get_column_letter, column_index_from_string
+from openpyxl.styles import Font, Border, PatternFill, Side
+
+from .cell_utils import parse_cell_range
+from .exceptions import SheetError, ValidationError
+
+logger = logging.getLogger(__name__)
+
+def copy_sheet(filepath: str, source_sheet: str, target_sheet: str) -> Dict[str, Any]:
+ """Copy a worksheet within the same workbook."""
+ try:
+ wb = load_workbook(filepath)
+ if source_sheet not in wb.sheetnames:
+ raise SheetError(f"Source sheet '{source_sheet}' not found")
+
+ if target_sheet in wb.sheetnames:
+ raise SheetError(f"Target sheet '{target_sheet}' already exists")
+
+ source = wb[source_sheet]
+ target = wb.copy_worksheet(source)
+ target.title = target_sheet
+
+ wb.save(filepath)
+ return {"message": f"Sheet '{source_sheet}' copied to '{target_sheet}'"}
+ except SheetError as e:
+ logger.error(str(e))
+ raise
+ except Exception as e:
+ logger.error(f"Failed to copy sheet: {e}")
+ raise SheetError(str(e))
+
+def delete_sheet(filepath: str, sheet_name: str) -> Dict[str, Any]:
+ """Delete a worksheet from the workbook."""
+ try:
+ wb = load_workbook(filepath)
+ if sheet_name not in wb.sheetnames:
+ raise SheetError(f"Sheet '{sheet_name}' not found")
+
+ if len(wb.sheetnames) == 1:
+ raise SheetError("Cannot delete the only sheet in workbook")
+
+ del wb[sheet_name]
+ wb.save(filepath)
+ return {"message": f"Sheet '{sheet_name}' deleted"}
+ except SheetError as e:
+ logger.error(str(e))
+ raise
+ except Exception as e:
+ logger.error(f"Failed to delete sheet: {e}")
+ raise SheetError(str(e))
+
+def rename_sheet(filepath: str, old_name: str, new_name: str) -> Dict[str, Any]:
+ """Rename a worksheet."""
+ try:
+ wb = load_workbook(filepath)
+ if old_name not in wb.sheetnames:
+ raise SheetError(f"Sheet '{old_name}' not found")
+
+ if new_name in wb.sheetnames:
+ raise SheetError(f"Sheet '{new_name}' already exists")
+
+ sheet = wb[old_name]
+ sheet.title = new_name
+ wb.save(filepath)
+ return {"message": f"Sheet renamed from '{old_name}' to '{new_name}'"}
+ except SheetError as e:
+ logger.error(str(e))
+ raise
+ except Exception as e:
+ logger.error(f"Failed to rename sheet: {e}")
+ raise SheetError(str(e))
+
+def format_range_string(start_row: int, start_col: int, end_row: int, end_col: int) -> str:
+ """Format range string from row and column indices."""
+ return f"{get_column_letter(start_col)}{start_row}:{get_column_letter(end_col)}{end_row}"
+
+def copy_range(
+ source_ws: Worksheet,
+ target_ws: Worksheet,
+ source_range: str,
+ target_start: Optional[str] = None,
+) -> None:
+ """Copy range from source worksheet to target worksheet."""
+ # Parse source range
+ if ':' in source_range:
+ source_start, source_end = source_range.split(':')
+ else:
+ source_start = source_range
+ source_end = None
+
+ src_start_row, src_start_col, src_end_row, src_end_col = parse_cell_range(
+ source_start, source_end
+ )
+
+ if src_end_row is None:
+ src_end_row = src_start_row
+ src_end_col = src_start_col
+
+ if target_start is None:
+ target_start = source_start
+
+ tgt_start_row, tgt_start_col, _, _ = parse_cell_range(target_start)
+
+ for i, row in enumerate(range(src_start_row, src_end_row + 1)):
+ for j, col in enumerate(range(src_start_col, src_end_col + 1)):
+ source_cell = source_ws.cell(row=row, column=col)
+ target_cell = target_ws.cell(row=tgt_start_row + i, column=tgt_start_col + j)
+
+ target_cell.value = source_cell.value
+
+ try:
+ # Copy font
+ font_kwargs = {}
+ if hasattr(source_cell.font, 'name'):
+ font_kwargs['name'] = source_cell.font.name
+ if hasattr(source_cell.font, 'size'):
+ font_kwargs['size'] = source_cell.font.size
+ if hasattr(source_cell.font, 'bold'):
+ font_kwargs['bold'] = source_cell.font.bold
+ if hasattr(source_cell.font, 'italic'):
+ font_kwargs['italic'] = source_cell.font.italic
+ if hasattr(source_cell.font, 'color'):
+ font_color = None
+ if source_cell.font.color:
+ font_color = source_cell.font.color.rgb
+ font_kwargs['color'] = font_color
+ target_cell.font = Font(**font_kwargs)
+
+ # Copy border
+ new_border = Border()
+ for side in ['left', 'right', 'top', 'bottom']:
+ source_side = getattr(source_cell.border, side)
+ if source_side and source_side.style:
+ side_color = source_side.color.rgb if source_side.color else None
+ setattr(new_border, side, Side(
+ style=source_side.style,
+ color=side_color
+ ))
+ target_cell.border = new_border
+
+ # Copy fill
+ if hasattr(source_cell, 'fill'):
+ fill_kwargs = {'patternType': source_cell.fill.patternType}
+ if hasattr(source_cell.fill, 'fgColor') and source_cell.fill.fgColor:
+ fg_color = None
+ if hasattr(source_cell.fill.fgColor, 'rgb'):
+ fg_color = source_cell.fill.fgColor.rgb
+ fill_kwargs['fgColor'] = fg_color
+ if hasattr(source_cell.fill, 'bgColor') and source_cell.fill.bgColor:
+ bg_color = None
+ if hasattr(source_cell.fill.bgColor, 'rgb'):
+ bg_color = source_cell.fill.bgColor.rgb
+ fill_kwargs['bgColor'] = bg_color
+ target_cell.fill = PatternFill(**fill_kwargs)
+
+ # Copy number format and alignment
+ if source_cell.number_format:
+ target_cell.number_format = source_cell.number_format
+ if source_cell.alignment:
+ target_cell.alignment = source_cell.alignment
+
+ except Exception:
+ continue
+
+def delete_range(worksheet: Worksheet, start_cell: str, end_cell: Optional[str] = None) -> None:
+ """Delete contents and formatting of a range."""
+ start_row, start_col, end_row, end_col = parse_cell_range(start_cell, end_cell)
+
+ if end_row is None:
+ end_row = start_row
+ end_col = start_col
+
+ for row in range(start_row, end_row + 1):
+ for col in range(start_col, end_col + 1):
+ cell = worksheet.cell(row=row, column=col)
+ cell.value = None
+ cell.font = Font()
+ cell.border = Border()
+ cell.fill = PatternFill()
+ cell.number_format = "General"
+ cell.alignment = None
+
+def merge_range(filepath: str, sheet_name: str, start_cell: str, end_cell: str) -> Dict[str, Any]:
+ """Merge a range of cells."""
+ try:
+ wb = load_workbook(filepath)
+ if sheet_name not in wb.sheetnames:
+ raise SheetError(f"Sheet '{sheet_name}' not found")
+
+ start_row, start_col, end_row, end_col = parse_cell_range(start_cell, end_cell)
+
+ if end_row is None or end_col is None:
+ raise SheetError("Both start and end cells must be specified for merging")
+
+ range_string = format_range_string(start_row, start_col, end_row, end_col)
+ worksheet = wb[sheet_name]
+ worksheet.merge_cells(range_string)
+ wb.save(filepath)
+ return {"message": f"Range '{range_string}' merged in sheet '{sheet_name}'"}
+ except SheetError as e:
+ logger.error(str(e))
+ raise
+ except Exception as e:
+ logger.error(f"Failed to merge range: {e}")
+ raise SheetError(str(e))
+
+def unmerge_range(filepath: str, sheet_name: str, start_cell: str, end_cell: str) -> Dict[str, Any]:
+ """Unmerge a range of cells."""
+ try:
+ wb = load_workbook(filepath)
+ if sheet_name not in wb.sheetnames:
+ raise SheetError(f"Sheet '{sheet_name}' not found")
+
+ worksheet = wb[sheet_name]
+
+ start_row, start_col, end_row, end_col = parse_cell_range(start_cell, end_cell)
+
+ if end_row is None or end_col is None:
+ raise SheetError("Both start and end cells must be specified for unmerging")
+
+ range_string = format_range_string(start_row, start_col, end_row, end_col)
+
+ # Check if range is actually merged
+ merged_ranges = worksheet.merged_cells.ranges
+ target_range = range_string.upper()
+
+ if not any(str(merged_range).upper() == target_range for merged_range in merged_ranges):
+ raise SheetError(f"Range '{range_string}' is not merged")
+
+ worksheet.unmerge_cells(range_string)
+ wb.save(filepath)
+ return {"message": f"Range '{range_string}' unmerged successfully"}
+ except SheetError as e:
+ logger.error(str(e))
+ raise
+ except Exception as e:
+ logger.error(f"Failed to unmerge range: {e}")
+ raise SheetError(str(e))
+
+def get_merged_ranges(filepath: str, sheet_name: str) -> list[str]:
+ """Get merged cells in a worksheet."""
+ try:
+ wb = load_workbook(filepath)
+ if sheet_name not in wb.sheetnames:
+ raise SheetError(f"Sheet '{sheet_name}' not found")
+ worksheet = wb[sheet_name]
+ return [str(merged_range) for merged_range in worksheet.merged_cells.ranges]
+ except SheetError as e:
+ logger.error(str(e))
+ raise
+ except Exception as e:
+ logger.error(f"Failed to get merged cells: {e}")
+ raise SheetError(str(e))
+
+def copy_range_operation(
+ filepath: str,
+ sheet_name: str,
+ source_start: str,
+ source_end: str,
+ target_start: str,
+ target_sheet: Optional[str] = None
+) -> Dict:
+ """Copy a range of cells to another location."""
+ try:
+ wb = load_workbook(filepath)
+ if sheet_name not in wb.sheetnames:
+ logger.error(f"Sheet '{sheet_name}' not found")
+ raise ValidationError(f"Sheet '{sheet_name}' not found")
+
+ source_ws = wb[sheet_name]
+ target_ws = wb[target_sheet] if target_sheet else source_ws
+
+ # Parse source range
+ try:
+ start_row, start_col, end_row, end_col = parse_cell_range(source_start, source_end)
+ except ValueError as e:
+ logger.error(f"Invalid source range: {e}")
+ raise ValidationError(f"Invalid source range: {str(e)}")
+
+ # Parse target starting point
+ try:
+ target_row = int(''.join(filter(str.isdigit, target_start)))
+ target_col = column_index_from_string(''.join(filter(str.isalpha, target_start)))
+ except ValueError as e:
+ logger.error(f"Invalid target cell: {e}")
+ raise ValidationError(f"Invalid target cell: {str(e)}")
+
+ # Copy the range
+ row_offset = target_row - start_row
+ col_offset = target_col - start_col
+
+ for i in range(start_row, end_row + 1):
+ for j in range(start_col, end_col + 1):
+ source_cell = source_ws.cell(row=i, column=j)
+ target_cell = target_ws.cell(row=i + row_offset, column=j + col_offset)
+ target_cell.value = source_cell.value
+ if source_cell.has_style:
+ target_cell._style = copy(source_cell._style)
+
+ wb.save(filepath)
+ return {"message": f"Range copied successfully"}
+
+ except (ValidationError, SheetError):
+ raise
+ except Exception as e:
+ logger.error(f"Failed to copy range: {e}")
+ raise SheetError(f"Failed to copy range: {str(e)}")
+
+def delete_range_operation(
+ filepath: str,
+ sheet_name: str,
+ start_cell: str,
+ end_cell: Optional[str] = None,
+ shift_direction: str = "up"
+) -> Dict[str, Any]:
+ """Delete a range of cells and shift remaining cells."""
+ try:
+ wb = load_workbook(filepath)
+ if sheet_name not in wb.sheetnames:
+ raise SheetError(f"Sheet '{sheet_name}' not found")
+
+ worksheet = wb[sheet_name]
+
+ # Validate range
+ try:
+ start_row, start_col, end_row, end_col = parse_cell_range(start_cell, end_cell)
+ if end_row and end_row > worksheet.max_row:
+ raise SheetError(f"End row {end_row} out of bounds (1-{worksheet.max_row})")
+ if end_col and end_col > worksheet.max_column:
+ raise SheetError(f"End column {end_col} out of bounds (1-{worksheet.max_column})")
+ except ValueError as e:
+ raise SheetError(f"Invalid range: {str(e)}")
+
+ # Validate shift direction
+ if shift_direction not in ["up", "left"]:
+ raise ValidationError(f"Invalid shift direction: {shift_direction}. Must be 'up' or 'left'")
+
+ range_string = format_range_string(
+ start_row, start_col,
+ end_row or start_row,
+ end_col or start_col
+ )
+
+ # Delete range contents
+ delete_range(worksheet, start_cell, end_cell)
+
+ # Shift cells if needed
+ if shift_direction == "up":
+ worksheet.delete_rows(start_row, (end_row or start_row) - start_row + 1)
+ elif shift_direction == "left":
+ worksheet.delete_cols(start_col, (end_col or start_col) - start_col + 1)
+
+ wb.save(filepath)
+
+ return {"message": f"Range {range_string} deleted successfully"}
+ except (ValidationError, SheetError) as e:
+ logger.error(str(e))
+ raise
+ except Exception as e:
+ logger.error(f"Failed to delete range: {e}")
+ raise SheetError(str(e))
+
+def insert_row(filepath: str, sheet_name: str, start_row: int, count: int = 1) -> Dict[str, Any]:
+ """Insert one or more rows starting at the specified row."""
+ try:
+ wb = load_workbook(filepath)
+ if sheet_name not in wb.sheetnames:
+ raise SheetError(f"Sheet '{sheet_name}' not found")
+
+ worksheet = wb[sheet_name]
+
+ # Validate parameters
+ if start_row < 1:
+ raise ValidationError("Start row must be 1 or greater")
+ if count < 1:
+ raise ValidationError("Count must be 1 or greater")
+
+ worksheet.insert_rows(start_row, count)
+ wb.save(filepath)
+
+ return {"message": f"Inserted {count} row(s) starting at row {start_row} in sheet '{sheet_name}'"}
+ except (ValidationError, SheetError) as e:
+ logger.error(str(e))
+ raise
+ except Exception as e:
+ logger.error(f"Failed to insert rows: {e}")
+ raise SheetError(str(e))
+
+def insert_cols(filepath: str, sheet_name: str, start_col: int, count: int = 1) -> Dict[str, Any]:
+ """Insert one or more columns starting at the specified column."""
+ try:
+ wb = load_workbook(filepath)
+ if sheet_name not in wb.sheetnames:
+ raise SheetError(f"Sheet '{sheet_name}' not found")
+
+ worksheet = wb[sheet_name]
+
+ # Validate parameters
+ if start_col < 1:
+ raise ValidationError("Start column must be 1 or greater")
+ if count < 1:
+ raise ValidationError("Count must be 1 or greater")
+
+ worksheet.insert_cols(start_col, count)
+ wb.save(filepath)
+
+ return {"message": f"Inserted {count} column(s) starting at column {start_col} in sheet '{sheet_name}'"}
+ except (ValidationError, SheetError) as e:
+ logger.error(str(e))
+ raise
+ except Exception as e:
+ logger.error(f"Failed to insert columns: {e}")
+ raise SheetError(str(e))
+
+def delete_rows(filepath: str, sheet_name: str, start_row: int, count: int = 1) -> Dict[str, Any]:
+ """Delete one or more rows starting at the specified row."""
+ try:
+ wb = load_workbook(filepath)
+ if sheet_name not in wb.sheetnames:
+ raise SheetError(f"Sheet '{sheet_name}' not found")
+
+ worksheet = wb[sheet_name]
+
+ # Validate parameters
+ if start_row < 1:
+ raise ValidationError("Start row must be 1 or greater")
+ if count < 1:
+ raise ValidationError("Count must be 1 or greater")
+ if start_row > worksheet.max_row:
+ raise ValidationError(f"Start row {start_row} exceeds worksheet bounds (max row: {worksheet.max_row})")
+
+ worksheet.delete_rows(start_row, count)
+ wb.save(filepath)
+
+ return {"message": f"Deleted {count} row(s) starting at row {start_row} in sheet '{sheet_name}'"}
+ except (ValidationError, SheetError) as e:
+ logger.error(str(e))
+ raise
+ except Exception as e:
+ logger.error(f"Failed to delete rows: {e}")
+ raise SheetError(str(e))
+
+def delete_cols(filepath: str, sheet_name: str, start_col: int, count: int = 1) -> Dict[str, Any]:
+ """Delete one or more columns starting at the specified column."""
+ try:
+ wb = load_workbook(filepath)
+ if sheet_name not in wb.sheetnames:
+ raise SheetError(f"Sheet '{sheet_name}' not found")
+
+ worksheet = wb[sheet_name]
+
+ # Validate parameters
+ if start_col < 1:
+ raise ValidationError("Start column must be 1 or greater")
+ if count < 1:
+ raise ValidationError("Count must be 1 or greater")
+ if start_col > worksheet.max_column:
+ raise ValidationError(f"Start column {start_col} exceeds worksheet bounds (max column: {worksheet.max_column})")
+
+ worksheet.delete_cols(start_col, count)
+ wb.save(filepath)
+
+ return {"message": f"Deleted {count} column(s) starting at column {start_col} in sheet '{sheet_name}'"}
+ except (ValidationError, SheetError) as e:
+ logger.error(str(e))
+ raise
+ except Exception as e:
+ logger.error(f"Failed to delete columns: {e}")
+ raise SheetError(str(e))
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/tables.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/tables.py
new file mode 100644
index 00000000..ed5168d4
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/tables.py
@@ -0,0 +1,69 @@
+import uuid
+import logging
+
+from openpyxl import load_workbook
+from openpyxl.worksheet.table import Table, TableStyleInfo
+from .exceptions import DataError
+
+logger = logging.getLogger(__name__)
+
+def create_excel_table(
+ filepath: str,
+ sheet_name: str,
+ data_range: str,
+ table_name: str | None = None,
+ table_style: str = "TableStyleMedium9"
+) -> dict:
+ """Creates a native Excel table for the given data range.
+
+ Args:
+ filepath: Path to the Excel file.
+ sheet_name: Name of the worksheet.
+ data_range: The cell range for the table (e.g., "A1:D5").
+ table_name: A unique name for the table. If not provided, a unique name is generated.
+ table_style: The visual style to apply to the table.
+
+ Returns:
+ A dictionary with a success message and table details.
+ """
+ try:
+ wb = load_workbook(filepath)
+ if sheet_name not in wb.sheetnames:
+ raise DataError(f"Sheet '{sheet_name}' not found.")
+
+ ws = wb[sheet_name]
+
+ # If no table name is provided, generate a unique one
+ if not table_name:
+ table_name = f"Table_{uuid.uuid4().hex[:8]}"
+
+ # Check if table name already exists
+ if table_name in ws.parent.defined_names:
+ raise DataError(f"Table name '{table_name}' already exists.")
+
+ # Create the table
+ table = Table(displayName=table_name, ref=data_range)
+
+ # Apply style
+ style = TableStyleInfo(
+ name=table_style,
+ showFirstColumn=False,
+ showLastColumn=False,
+ showRowStripes=True,
+ showColumnStripes=False
+ )
+ table.tableStyleInfo = style
+
+ ws.add_table(table)
+
+ wb.save(filepath)
+
+ return {
+ "message": f"Successfully created table '{table_name}' in sheet '{sheet_name}'.",
+ "table_name": table_name,
+ "range": data_range
+ }
+
+ except Exception as e:
+ logger.error(f"Failed to create table: {e}")
+ raise DataError(str(e))
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/validation.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/validation.py
new file mode 100644
index 00000000..faaa838c
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/validation.py
@@ -0,0 +1,235 @@
+import logging
+import re
+from typing import Any
+
+from openpyxl import load_workbook
+from openpyxl.utils import get_column_letter
+from openpyxl.worksheet.worksheet import Worksheet
+
+from .cell_utils import parse_cell_range, validate_cell_reference
+from .exceptions import ValidationError
+
+logger = logging.getLogger(__name__)
+
+def validate_formula_in_cell_operation(
+ filepath: str,
+ sheet_name: str,
+ cell: str,
+ formula: str
+) -> dict[str, Any]:
+ """Validate Excel formula before writing"""
+ try:
+ wb = load_workbook(filepath)
+ if sheet_name not in wb.sheetnames:
+ raise ValidationError(f"Sheet '{sheet_name}' not found")
+
+ if not validate_cell_reference(cell):
+ raise ValidationError(f"Invalid cell reference: {cell}")
+
+ # First validate the provided formula's syntax
+ is_valid, message = validate_formula(formula)
+ if not is_valid:
+ raise ValidationError(f"Invalid formula syntax: {message}")
+
+ # Additional validation for cell references in formula
+ cell_refs = re.findall(r'[A-Z]+[0-9]+(?::[A-Z]+[0-9]+)?', formula)
+ for ref in cell_refs:
+ if ':' in ref: # Range reference
+ start, end = ref.split(':')
+ if not (validate_cell_reference(start) and validate_cell_reference(end)):
+ raise ValidationError(f"Invalid cell range reference in formula: {ref}")
+ else: # Single cell reference
+ if not validate_cell_reference(ref):
+ raise ValidationError(f"Invalid cell reference in formula: {ref}")
+
+ # Now check if there's a formula in the cell and compare
+ sheet = wb[sheet_name]
+ cell_obj = sheet[cell]
+ current_formula = cell_obj.value
+
+ # If cell has a formula (starts with =)
+ if isinstance(current_formula, str) and current_formula.startswith('='):
+ if formula.startswith('='):
+ if current_formula != formula:
+ return {
+ "message": "Formula is valid but doesn't match cell content",
+ "valid": True,
+ "matches": False,
+ "cell": cell,
+ "provided_formula": formula,
+ "current_formula": current_formula
+ }
+ else:
+ if current_formula != f"={formula}":
+ return {
+ "message": "Formula is valid but doesn't match cell content",
+ "valid": True,
+ "matches": False,
+ "cell": cell,
+ "provided_formula": formula,
+ "current_formula": current_formula
+ }
+ else:
+ return {
+ "message": "Formula is valid and matches cell content",
+ "valid": True,
+ "matches": True,
+ "cell": cell,
+ "formula": formula
+ }
+ else:
+ return {
+ "message": "Formula is valid but cell contains no formula",
+ "valid": True,
+ "matches": False,
+ "cell": cell,
+ "provided_formula": formula,
+ "current_content": str(current_formula) if current_formula else ""
+ }
+
+ except ValidationError as e:
+ logger.error(str(e))
+ raise
+ except Exception as e:
+ logger.error(f"Failed to validate formula: {e}")
+ raise ValidationError(str(e))
+
+def validate_range_in_sheet_operation(
+ filepath: str,
+ sheet_name: str,
+ start_cell: str,
+ end_cell: str | None = None,
+) -> dict[str, Any]:
+ """Validate if a range exists in a worksheet and return data range info."""
+ try:
+ wb = load_workbook(filepath)
+ if sheet_name not in wb.sheetnames:
+ raise ValidationError(f"Sheet '{sheet_name}' not found")
+
+ worksheet = wb[sheet_name]
+
+ # Get actual data dimensions
+ data_max_row = worksheet.max_row
+ data_max_col = worksheet.max_column
+
+ # Validate range
+ try:
+ start_row, start_col, end_row, end_col = parse_cell_range(start_cell, end_cell)
+ except ValueError as e:
+ raise ValidationError(f"Invalid range: {str(e)}")
+
+ # If end not specified, use start
+ if end_row is None:
+ end_row = start_row
+ if end_col is None:
+ end_col = start_col
+
+ # Validate bounds against maximum possible Excel limits
+ is_valid, message = validate_range_bounds(
+ worksheet, start_row, start_col, end_row, end_col
+ )
+ if not is_valid:
+ raise ValidationError(message)
+
+ range_str = f"{start_cell}" if end_cell is None else f"{start_cell}:{end_cell}"
+ data_range_str = f"A1:{get_column_letter(data_max_col)}{data_max_row}"
+
+ # Check if range is within data or extends beyond
+ extends_beyond_data = (
+ end_row > data_max_row or
+ end_col > data_max_col
+ )
+
+ return {
+ "message": (
+ f"Range '{range_str}' is valid. "
+ f"Sheet contains data in range '{data_range_str}'"
+ ),
+ "valid": True,
+ "range": range_str,
+ "data_range": data_range_str,
+ "extends_beyond_data": extends_beyond_data,
+ "data_dimensions": {
+ "max_row": data_max_row,
+ "max_col": data_max_col,
+ "max_col_letter": get_column_letter(data_max_col)
+ }
+ }
+ except ValidationError as e:
+ logger.error(str(e))
+ raise
+ except Exception as e:
+ logger.error(f"Failed to validate range: {e}")
+ raise ValidationError(str(e))
+
+def validate_formula(formula: str) -> tuple[bool, str]:
+ """Validate Excel formula syntax and safety"""
+ if not formula.startswith("="):
+ return False, "Formula must start with '='"
+
+ # Remove the '=' prefix for validation
+ formula = formula[1:]
+
+ # Check for balanced parentheses
+ parens = 0
+ for c in formula:
+ if c == "(":
+ parens += 1
+ elif c == ")":
+ parens -= 1
+ if parens < 0:
+ return False, "Unmatched closing parenthesis"
+
+ if parens > 0:
+ return False, "Unclosed parenthesis"
+
+ # Basic function name validation
+ func_pattern = r"([A-Z]+)\("
+ funcs = re.findall(func_pattern, formula)
+ unsafe_funcs = {"INDIRECT", "HYPERLINK", "WEBSERVICE", "DGET", "RTD"}
+
+ for func in funcs:
+ if func in unsafe_funcs:
+ return False, f"Unsafe function: {func}"
+
+ return True, "Formula is valid"
+
+
+def validate_range_bounds(
+ worksheet: Worksheet,
+ start_row: int,
+ start_col: int,
+ end_row: int | None = None,
+ end_col: int | None = None,
+) -> tuple[bool, str]:
+ """Validate that cell range is within worksheet bounds"""
+ max_row = worksheet.max_row
+ max_col = worksheet.max_column
+
+ try:
+ # Check start cell bounds
+ if start_row < 1 or start_row > max_row:
+ return False, f"Start row {start_row} out of bounds (1-{max_row})"
+ if start_col < 1 or start_col > max_col:
+ return False, (
+ f"Start column {get_column_letter(start_col)} "
+ f"out of bounds (A-{get_column_letter(max_col)})"
+ )
+
+ # If end cell specified, check its bounds
+ if end_row is not None and end_col is not None:
+ if end_row < start_row:
+ return False, "End row cannot be before start row"
+ if end_col < start_col:
+ return False, "End column cannot be before start column"
+ if end_row > max_row:
+ return False, f"End row {end_row} out of bounds (1-{max_row})"
+ if end_col > max_col:
+ return False, (
+ f"End column {get_column_letter(end_col)} "
+ f"out of bounds (A-{get_column_letter(max_col)})"
+ )
+
+ return True, "Range is valid"
+ except Exception as e:
+ return False, f"Invalid range: {e!s}"
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/workbook.py b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/workbook.py
new file mode 100644
index 00000000..9d6f193f
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/src/excel_mcp/workbook.py
@@ -0,0 +1,96 @@
+import logging
+from pathlib import Path
+from typing import Any
+
+from openpyxl import Workbook, load_workbook
+from openpyxl.utils import get_column_letter
+
+from .exceptions import WorkbookError
+
+logger = logging.getLogger(__name__)
+
+def create_workbook(filepath: str, sheet_name: str = "Sheet1") -> dict[str, Any]:
+ """Create a new Excel workbook with optional custom sheet name"""
+ try:
+ wb = Workbook()
+ # Rename default sheet
+ if "Sheet" in wb.sheetnames:
+ sheet = wb["Sheet"]
+ sheet.title = sheet_name
+ else:
+ wb.create_sheet(sheet_name)
+
+ path = Path(filepath)
+ path.parent.mkdir(parents=True, exist_ok=True)
+ wb.save(str(path))
+ return {
+ "message": f"Created workbook: {filepath}",
+ "active_sheet": sheet_name,
+ "workbook": wb
+ }
+ except Exception as e:
+ logger.error(f"Failed to create workbook: {e}")
+ raise WorkbookError(f"Failed to create workbook: {e!s}")
+
+def get_or_create_workbook(filepath: str) -> Workbook:
+ """Get existing workbook or create new one if it doesn't exist"""
+ try:
+ return load_workbook(filepath)
+ except FileNotFoundError:
+ return create_workbook(filepath)["workbook"]
+
+def create_sheet(filepath: str, sheet_name: str) -> dict:
+ """Create a new worksheet in the workbook if it doesn't exist."""
+ try:
+ wb = load_workbook(filepath)
+
+ # Check if sheet already exists
+ if sheet_name in wb.sheetnames:
+ raise WorkbookError(f"Sheet {sheet_name} already exists")
+
+ # Create new sheet
+ wb.create_sheet(sheet_name)
+ wb.save(filepath)
+ wb.close()
+ return {"message": f"Sheet {sheet_name} created successfully"}
+ except WorkbookError as e:
+ logger.error(str(e))
+ raise
+ except Exception as e:
+ logger.error(f"Failed to create sheet: {e}")
+ raise WorkbookError(str(e))
+
+def get_workbook_info(filepath: str, include_ranges: bool = False) -> dict[str, Any]:
+ """Get metadata about workbook including sheets, ranges, etc."""
+ try:
+ path = Path(filepath)
+ if not path.exists():
+ raise WorkbookError(f"File not found: {filepath}")
+
+ wb = load_workbook(filepath, read_only=False)
+
+ info = {
+ "filename": path.name,
+ "sheets": wb.sheetnames,
+ "size": path.stat().st_size,
+ "modified": path.stat().st_mtime
+ }
+
+ if include_ranges:
+ # Add used ranges for each sheet
+ ranges = {}
+ for sheet_name in wb.sheetnames:
+ ws = wb[sheet_name]
+ if ws.max_row > 0 and ws.max_column > 0:
+ ranges[sheet_name] = f"A1:{get_column_letter(ws.max_column)}{ws.max_row}"
+ info["used_ranges"] = ranges
+
+ wb.close()
+ return info
+
+ except WorkbookError as e:
+ logger.error(str(e))
+ raise
+ except Exception as e:
+ logger.error(f"Failed to get workbook info: {e}")
+ raise WorkbookError(str(e))
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/uv.lock b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/uv.lock
new file mode 100644
index 00000000..691e4b43
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/excel-mcp-server/uv.lock
@@ -0,0 +1,1211 @@
+version = 1
+requires-python = ">=3.10"
+
+[[package]]
+name = "annotated-types"
+version = "0.7.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 },
+]
+
+[[package]]
+name = "anyio"
+version = "4.8.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
+ { name = "idna" },
+ { name = "sniffio" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a3/73/199a98fc2dae33535d6b8e8e6ec01f8c1d76c9adb096c6b7d64823038cde/anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a", size = 181126 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/46/eb/e7f063ad1fec6b3178a3cd82d1a3c4de82cccf283fc42746168188e1cdd5/anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a", size = 96041 },
+]
+
+[[package]]
+name = "attrs"
+version = "25.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815 },
+]
+
+[[package]]
+name = "authlib"
+version = "1.6.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cryptography" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/8e/a1/d8d1c6f8bc922c0b87ae0d933a8ed57be1bef6970894ed79c2852a153cd3/authlib-1.6.1.tar.gz", hash = "sha256:4dffdbb1460ba6ec8c17981a4c67af7d8af131231b5a36a88a1e8c80c111cdfd", size = 159988 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f9/58/cc6a08053f822f98f334d38a27687b69c6655fb05cd74a7a5e70a2aeed95/authlib-1.6.1-py2.py3-none-any.whl", hash = "sha256:e9d2031c34c6309373ab845afc24168fe9e93dc52d252631f52642f21f5ed06e", size = 239299 },
+]
+
+[[package]]
+name = "certifi"
+version = "2025.1.31"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 },
+]
+
+[[package]]
+name = "cffi"
+version = "1.17.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pycparser" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/90/07/f44ca684db4e4f08a3fdc6eeb9a0d15dc6883efc7b8c90357fdbf74e186c/cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", size = 182191 },
+ { url = "https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", size = 178592 },
+ { url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024 },
+ { url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188 },
+ { url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571 },
+ { url = "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687 },
+ { url = "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211 },
+ { url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325 },
+ { url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784 },
+ { url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564 },
+ { url = "https://files.pythonhosted.org/packages/f8/fe/4d41c2f200c4a457933dbd98d3cf4e911870877bd94d9656cc0fcb390681/cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", size = 171804 },
+ { url = "https://files.pythonhosted.org/packages/d1/b6/0b0f5ab93b0df4acc49cae758c81fe4e5ef26c3ae2e10cc69249dfd8b3ab/cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", size = 181299 },
+ { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264 },
+ { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651 },
+ { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259 },
+ { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200 },
+ { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235 },
+ { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721 },
+ { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242 },
+ { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999 },
+ { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242 },
+ { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604 },
+ { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727 },
+ { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400 },
+ { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178 },
+ { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840 },
+ { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803 },
+ { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850 },
+ { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729 },
+ { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256 },
+ { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424 },
+ { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568 },
+ { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736 },
+ { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448 },
+ { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976 },
+ { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989 },
+ { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802 },
+ { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792 },
+ { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893 },
+ { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810 },
+ { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200 },
+ { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447 },
+ { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358 },
+ { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 },
+ { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 },
+ { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 },
+]
+
+[[package]]
+name = "charset-normalizer"
+version = "3.4.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/95/28/9901804da60055b406e1a1c5ba7aac1276fb77f1dde635aabfc7fd84b8ab/charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941", size = 201818 },
+ { url = "https://files.pythonhosted.org/packages/d9/9b/892a8c8af9110935e5adcbb06d9c6fe741b6bb02608c6513983048ba1a18/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd", size = 144649 },
+ { url = "https://files.pythonhosted.org/packages/7b/a5/4179abd063ff6414223575e008593861d62abfc22455b5d1a44995b7c101/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6", size = 155045 },
+ { url = "https://files.pythonhosted.org/packages/3b/95/bc08c7dfeddd26b4be8c8287b9bb055716f31077c8b0ea1cd09553794665/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d", size = 147356 },
+ { url = "https://files.pythonhosted.org/packages/a8/2d/7a5b635aa65284bf3eab7653e8b4151ab420ecbae918d3e359d1947b4d61/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86", size = 149471 },
+ { url = "https://files.pythonhosted.org/packages/ae/38/51fc6ac74251fd331a8cfdb7ec57beba8c23fd5493f1050f71c87ef77ed0/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c", size = 151317 },
+ { url = "https://files.pythonhosted.org/packages/b7/17/edee1e32215ee6e9e46c3e482645b46575a44a2d72c7dfd49e49f60ce6bf/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0", size = 146368 },
+ { url = "https://files.pythonhosted.org/packages/26/2c/ea3e66f2b5f21fd00b2825c94cafb8c326ea6240cd80a91eb09e4a285830/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef", size = 154491 },
+ { url = "https://files.pythonhosted.org/packages/52/47/7be7fa972422ad062e909fd62460d45c3ef4c141805b7078dbab15904ff7/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6", size = 157695 },
+ { url = "https://files.pythonhosted.org/packages/2f/42/9f02c194da282b2b340f28e5fb60762de1151387a36842a92b533685c61e/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366", size = 154849 },
+ { url = "https://files.pythonhosted.org/packages/67/44/89cacd6628f31fb0b63201a618049be4be2a7435a31b55b5eb1c3674547a/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db", size = 150091 },
+ { url = "https://files.pythonhosted.org/packages/1f/79/4b8da9f712bc079c0f16b6d67b099b0b8d808c2292c937f267d816ec5ecc/charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a", size = 98445 },
+ { url = "https://files.pythonhosted.org/packages/7d/d7/96970afb4fb66497a40761cdf7bd4f6fca0fc7bafde3a84f836c1f57a926/charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509", size = 105782 },
+ { url = "https://files.pythonhosted.org/packages/05/85/4c40d00dcc6284a1c1ad5de5e0996b06f39d8232f1031cd23c2f5c07ee86/charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", size = 198794 },
+ { url = "https://files.pythonhosted.org/packages/41/d9/7a6c0b9db952598e97e93cbdfcb91bacd89b9b88c7c983250a77c008703c/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", size = 142846 },
+ { url = "https://files.pythonhosted.org/packages/66/82/a37989cda2ace7e37f36c1a8ed16c58cf48965a79c2142713244bf945c89/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", size = 153350 },
+ { url = "https://files.pythonhosted.org/packages/df/68/a576b31b694d07b53807269d05ec3f6f1093e9545e8607121995ba7a8313/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", size = 145657 },
+ { url = "https://files.pythonhosted.org/packages/92/9b/ad67f03d74554bed3aefd56fe836e1623a50780f7c998d00ca128924a499/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f", size = 147260 },
+ { url = "https://files.pythonhosted.org/packages/a6/e6/8aebae25e328160b20e31a7e9929b1578bbdc7f42e66f46595a432f8539e/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", size = 149164 },
+ { url = "https://files.pythonhosted.org/packages/8b/f2/b3c2f07dbcc248805f10e67a0262c93308cfa149a4cd3d1fe01f593e5fd2/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", size = 144571 },
+ { url = "https://files.pythonhosted.org/packages/60/5b/c3f3a94bc345bc211622ea59b4bed9ae63c00920e2e8f11824aa5708e8b7/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", size = 151952 },
+ { url = "https://files.pythonhosted.org/packages/e2/4d/ff460c8b474122334c2fa394a3f99a04cf11c646da895f81402ae54f5c42/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", size = 155959 },
+ { url = "https://files.pythonhosted.org/packages/a2/2b/b964c6a2fda88611a1fe3d4c400d39c66a42d6c169c924818c848f922415/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", size = 153030 },
+ { url = "https://files.pythonhosted.org/packages/59/2e/d3b9811db26a5ebf444bc0fa4f4be5aa6d76fc6e1c0fd537b16c14e849b6/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", size = 148015 },
+ { url = "https://files.pythonhosted.org/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", size = 98106 },
+ { url = "https://files.pythonhosted.org/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", size = 105402 },
+ { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936 },
+ { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790 },
+ { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924 },
+ { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626 },
+ { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567 },
+ { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957 },
+ { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408 },
+ { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399 },
+ { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815 },
+ { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537 },
+ { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565 },
+ { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357 },
+ { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776 },
+ { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622 },
+ { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435 },
+ { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653 },
+ { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231 },
+ { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243 },
+ { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442 },
+ { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147 },
+ { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057 },
+ { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454 },
+ { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174 },
+ { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166 },
+ { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064 },
+ { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641 },
+ { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626 },
+]
+
+[[package]]
+name = "click"
+version = "8.1.8"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 },
+]
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
+]
+
+[[package]]
+name = "cryptography"
+version = "45.0.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/95/1e/49527ac611af559665f71cbb8f92b332b5ec9c6fbc4e88b0f8e92f5e85df/cryptography-45.0.5.tar.gz", hash = "sha256:72e76caa004ab63accdf26023fccd1d087f6d90ec6048ff33ad0445abf7f605a", size = 744903 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f0/fb/09e28bc0c46d2c547085e60897fea96310574c70fb21cd58a730a45f3403/cryptography-45.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:101ee65078f6dd3e5a028d4f19c07ffa4dd22cce6a20eaa160f8b5219911e7d8", size = 7043092 },
+ { url = "https://files.pythonhosted.org/packages/b1/05/2194432935e29b91fb649f6149c1a4f9e6d3d9fc880919f4ad1bcc22641e/cryptography-45.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3a264aae5f7fbb089dbc01e0242d3b67dffe3e6292e1f5182122bdf58e65215d", size = 4205926 },
+ { url = "https://files.pythonhosted.org/packages/07/8b/9ef5da82350175e32de245646b1884fc01124f53eb31164c77f95a08d682/cryptography-45.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e74d30ec9c7cb2f404af331d5b4099a9b322a8a6b25c4632755c8757345baac5", size = 4429235 },
+ { url = "https://files.pythonhosted.org/packages/7c/e1/c809f398adde1994ee53438912192d92a1d0fc0f2d7582659d9ef4c28b0c/cryptography-45.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3af26738f2db354aafe492fb3869e955b12b2ef2e16908c8b9cb928128d42c57", size = 4209785 },
+ { url = "https://files.pythonhosted.org/packages/d0/8b/07eb6bd5acff58406c5e806eff34a124936f41a4fb52909ffa4d00815f8c/cryptography-45.0.5-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e6c00130ed423201c5bc5544c23359141660b07999ad82e34e7bb8f882bb78e0", size = 3893050 },
+ { url = "https://files.pythonhosted.org/packages/ec/ef/3333295ed58d900a13c92806b67e62f27876845a9a908c939f040887cca9/cryptography-45.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:dd420e577921c8c2d31289536c386aaa30140b473835e97f83bc71ea9d2baf2d", size = 4457379 },
+ { url = "https://files.pythonhosted.org/packages/d9/9d/44080674dee514dbb82b21d6fa5d1055368f208304e2ab1828d85c9de8f4/cryptography-45.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:d05a38884db2ba215218745f0781775806bde4f32e07b135348355fe8e4991d9", size = 4209355 },
+ { url = "https://files.pythonhosted.org/packages/c9/d8/0749f7d39f53f8258e5c18a93131919ac465ee1f9dccaf1b3f420235e0b5/cryptography-45.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:ad0caded895a00261a5b4aa9af828baede54638754b51955a0ac75576b831b27", size = 4456087 },
+ { url = "https://files.pythonhosted.org/packages/09/d7/92acac187387bf08902b0bf0699816f08553927bdd6ba3654da0010289b4/cryptography-45.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9024beb59aca9d31d36fcdc1604dd9bbeed0a55bface9f1908df19178e2f116e", size = 4332873 },
+ { url = "https://files.pythonhosted.org/packages/03/c2/840e0710da5106a7c3d4153c7215b2736151bba60bf4491bdb421df5056d/cryptography-45.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:91098f02ca81579c85f66df8a588c78f331ca19089763d733e34ad359f474174", size = 4564651 },
+ { url = "https://files.pythonhosted.org/packages/2e/92/cc723dd6d71e9747a887b94eb3827825c6c24b9e6ce2bb33b847d31d5eaa/cryptography-45.0.5-cp311-abi3-win32.whl", hash = "sha256:926c3ea71a6043921050eaa639137e13dbe7b4ab25800932a8498364fc1abec9", size = 2929050 },
+ { url = "https://files.pythonhosted.org/packages/1f/10/197da38a5911a48dd5389c043de4aec4b3c94cb836299b01253940788d78/cryptography-45.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:b85980d1e345fe769cfc57c57db2b59cff5464ee0c045d52c0df087e926fbe63", size = 3403224 },
+ { url = "https://files.pythonhosted.org/packages/fe/2b/160ce8c2765e7a481ce57d55eba1546148583e7b6f85514472b1d151711d/cryptography-45.0.5-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:f3562c2f23c612f2e4a6964a61d942f891d29ee320edb62ff48ffb99f3de9ae8", size = 7017143 },
+ { url = "https://files.pythonhosted.org/packages/c2/e7/2187be2f871c0221a81f55ee3105d3cf3e273c0a0853651d7011eada0d7e/cryptography-45.0.5-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3fcfbefc4a7f332dece7272a88e410f611e79458fab97b5efe14e54fe476f4fd", size = 4197780 },
+ { url = "https://files.pythonhosted.org/packages/b9/cf/84210c447c06104e6be9122661159ad4ce7a8190011669afceeaea150524/cryptography-45.0.5-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:460f8c39ba66af7db0545a8c6f2eabcbc5a5528fc1cf6c3fa9a1e44cec33385e", size = 4420091 },
+ { url = "https://files.pythonhosted.org/packages/3e/6a/cb8b5c8bb82fafffa23aeff8d3a39822593cee6e2f16c5ca5c2ecca344f7/cryptography-45.0.5-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9b4cf6318915dccfe218e69bbec417fdd7c7185aa7aab139a2c0beb7468c89f0", size = 4198711 },
+ { url = "https://files.pythonhosted.org/packages/04/f7/36d2d69df69c94cbb2473871926daf0f01ad8e00fe3986ac3c1e8c4ca4b3/cryptography-45.0.5-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2089cc8f70a6e454601525e5bf2779e665d7865af002a5dec8d14e561002e135", size = 3883299 },
+ { url = "https://files.pythonhosted.org/packages/82/c7/f0ea40f016de72f81288e9fe8d1f6748036cb5ba6118774317a3ffc6022d/cryptography-45.0.5-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0027d566d65a38497bc37e0dd7c2f8ceda73597d2ac9ba93810204f56f52ebc7", size = 4450558 },
+ { url = "https://files.pythonhosted.org/packages/06/ae/94b504dc1a3cdf642d710407c62e86296f7da9e66f27ab12a1ee6fdf005b/cryptography-45.0.5-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:be97d3a19c16a9be00edf79dca949c8fa7eff621763666a145f9f9535a5d7f42", size = 4198020 },
+ { url = "https://files.pythonhosted.org/packages/05/2b/aaf0adb845d5dabb43480f18f7ca72e94f92c280aa983ddbd0bcd6ecd037/cryptography-45.0.5-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:7760c1c2e1a7084153a0f68fab76e754083b126a47d0117c9ed15e69e2103492", size = 4449759 },
+ { url = "https://files.pythonhosted.org/packages/91/e4/f17e02066de63e0100a3a01b56f8f1016973a1d67551beaf585157a86b3f/cryptography-45.0.5-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6ff8728d8d890b3dda5765276d1bc6fb099252915a2cd3aff960c4c195745dd0", size = 4319991 },
+ { url = "https://files.pythonhosted.org/packages/f2/2e/e2dbd629481b499b14516eed933f3276eb3239f7cee2dcfa4ee6b44d4711/cryptography-45.0.5-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7259038202a47fdecee7e62e0fd0b0738b6daa335354396c6ddebdbe1206af2a", size = 4554189 },
+ { url = "https://files.pythonhosted.org/packages/f8/ea/a78a0c38f4c8736287b71c2ea3799d173d5ce778c7d6e3c163a95a05ad2a/cryptography-45.0.5-cp37-abi3-win32.whl", hash = "sha256:1e1da5accc0c750056c556a93c3e9cb828970206c68867712ca5805e46dc806f", size = 2911769 },
+ { url = "https://files.pythonhosted.org/packages/79/b3/28ac139109d9005ad3f6b6f8976ffede6706a6478e21c889ce36c840918e/cryptography-45.0.5-cp37-abi3-win_amd64.whl", hash = "sha256:90cb0a7bb35959f37e23303b7eed0a32280510030daba3f7fdfbb65defde6a97", size = 3390016 },
+ { url = "https://files.pythonhosted.org/packages/f8/8b/34394337abe4566848a2bd49b26bcd4b07fd466afd3e8cce4cb79a390869/cryptography-45.0.5-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:206210d03c1193f4e1ff681d22885181d47efa1ab3018766a7b32a7b3d6e6afd", size = 3575762 },
+ { url = "https://files.pythonhosted.org/packages/8b/5d/a19441c1e89afb0f173ac13178606ca6fab0d3bd3ebc29e9ed1318b507fc/cryptography-45.0.5-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c648025b6840fe62e57107e0a25f604db740e728bd67da4f6f060f03017d5097", size = 4140906 },
+ { url = "https://files.pythonhosted.org/packages/4b/db/daceb259982a3c2da4e619f45b5bfdec0e922a23de213b2636e78ef0919b/cryptography-45.0.5-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b8fa8b0a35a9982a3c60ec79905ba5bb090fc0b9addcfd3dc2dd04267e45f25e", size = 4374411 },
+ { url = "https://files.pythonhosted.org/packages/6a/35/5d06ad06402fc522c8bf7eab73422d05e789b4e38fe3206a85e3d6966c11/cryptography-45.0.5-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:14d96584701a887763384f3c47f0ca7c1cce322aa1c31172680eb596b890ec30", size = 4140942 },
+ { url = "https://files.pythonhosted.org/packages/65/79/020a5413347e44c382ef1f7f7e7a66817cd6273e3e6b5a72d18177b08b2f/cryptography-45.0.5-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:57c816dfbd1659a367831baca4b775b2a5b43c003daf52e9d57e1d30bc2e1b0e", size = 4374079 },
+ { url = "https://files.pythonhosted.org/packages/9b/c5/c0e07d84a9a2a8a0ed4f865e58f37c71af3eab7d5e094ff1b21f3f3af3bc/cryptography-45.0.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b9e38e0a83cd51e07f5a48ff9691cae95a79bea28fe4ded168a8e5c6c77e819d", size = 3321362 },
+ { url = "https://files.pythonhosted.org/packages/c0/71/9bdbcfd58d6ff5084687fe722c58ac718ebedbc98b9f8f93781354e6d286/cryptography-45.0.5-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:8c4a6ff8a30e9e3d38ac0539e9a9e02540ab3f827a3394f8852432f6b0ea152e", size = 3587878 },
+ { url = "https://files.pythonhosted.org/packages/f0/63/83516cfb87f4a8756eaa4203f93b283fda23d210fc14e1e594bd5f20edb6/cryptography-45.0.5-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bd4c45986472694e5121084c6ebbd112aa919a25e783b87eb95953c9573906d6", size = 4152447 },
+ { url = "https://files.pythonhosted.org/packages/22/11/d2823d2a5a0bd5802b3565437add16f5c8ce1f0778bf3822f89ad2740a38/cryptography-45.0.5-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:982518cd64c54fcada9d7e5cf28eabd3ee76bd03ab18e08a48cad7e8b6f31b18", size = 4386778 },
+ { url = "https://files.pythonhosted.org/packages/5f/38/6bf177ca6bce4fe14704ab3e93627c5b0ca05242261a2e43ef3168472540/cryptography-45.0.5-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:12e55281d993a793b0e883066f590c1ae1e802e3acb67f8b442e721e475e6463", size = 4151627 },
+ { url = "https://files.pythonhosted.org/packages/38/6a/69fc67e5266bff68a91bcb81dff8fb0aba4d79a78521a08812048913e16f/cryptography-45.0.5-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:5aa1e32983d4443e310f726ee4b071ab7569f58eedfdd65e9675484a4eb67bd1", size = 4385593 },
+ { url = "https://files.pythonhosted.org/packages/f6/34/31a1604c9a9ade0fdab61eb48570e09a796f4d9836121266447b0eaf7feb/cryptography-45.0.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:e357286c1b76403dd384d938f93c46b2b058ed4dfcdce64a770f0537ed3feb6f", size = 3331106 },
+]
+
+[[package]]
+name = "cyclopts"
+version = "3.22.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "attrs" },
+ { name = "docstring-parser", marker = "python_full_version < '4.0'" },
+ { name = "rich" },
+ { name = "rich-rst" },
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a3/d5/24c6c894f3833bc93d4944c2064309dfd633c0becf93e16fc79d76edd388/cyclopts-3.22.5.tar.gz", hash = "sha256:fa2450b9840abc41c6aa37af5eaeafc7a1264e08054e3a2fe39d49aa154f592a", size = 74890 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/df/e5/a7b6db64f08cfe065e531ec6b508fa7dac704fab70d05adb5bc0c2c1d1b6/cyclopts-3.22.5-py3-none-any.whl", hash = "sha256:92efb4a094d9812718d7efe0bffa319a19cb661f230dbf24406c18cd8809fb82", size = 84994 },
+]
+
+[[package]]
+name = "dnspython"
+version = "2.7.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b5/4a/263763cb2ba3816dd94b08ad3a33d5fdae34ecb856678773cc40a3605829/dnspython-2.7.0.tar.gz", hash = "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1", size = 345197 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/68/1b/e0a87d256e40e8c888847551b20a017a6b98139178505dc7ffb96f04e954/dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86", size = 313632 },
+]
+
+[[package]]
+name = "docstring-parser"
+version = "0.17.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896 },
+]
+
+[[package]]
+name = "docutils"
+version = "0.22"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e9/86/5b41c32ecedcfdb4c77b28b6cb14234f252075f8cdb254531727a35547dd/docutils-0.22.tar.gz", hash = "sha256:ba9d57750e92331ebe7c08a1bbf7a7f8143b86c476acd51528b042216a6aad0f", size = 2277984 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/44/57/8db39bc5f98f042e0153b1de9fb88e1a409a33cda4dd7f723c2ed71e01f6/docutils-0.22-py3-none-any.whl", hash = "sha256:4ed966a0e96a0477d852f7af31bdcb3adc049fbb35ccba358c2ea8a03287615e", size = 630709 },
+]
+
+[[package]]
+name = "email-validator"
+version = "2.2.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "dnspython" },
+ { name = "idna" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/48/ce/13508a1ec3f8bb981ae4ca79ea40384becc868bfae97fd1c942bb3a001b1/email_validator-2.2.0.tar.gz", hash = "sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7", size = 48967 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d7/ee/bf0adb559ad3c786f12bcbc9296b3f5675f529199bef03e2df281fa1fadb/email_validator-2.2.0-py3-none-any.whl", hash = "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631", size = 33521 },
+]
+
+[[package]]
+name = "et-xmlfile"
+version = "2.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d3/38/af70d7ab1ae9d4da450eeec1fa3918940a5fafb9055e934af8d6eb0c2313/et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54", size = 17234 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059 },
+]
+
+[[package]]
+name = "excel-mcp-server"
+version = "0.1.7"
+source = { editable = "." }
+dependencies = [
+ { name = "fastmcp" },
+ { name = "mcp", extra = ["cli"] },
+ { name = "openpyxl" },
+ { name = "typer" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "fastmcp", specifier = ">=2.0.0,<3.0.0" },
+ { name = "mcp", extras = ["cli"], specifier = ">=1.10.1" },
+ { name = "openpyxl", specifier = ">=3.1.5" },
+ { name = "typer", specifier = ">=0.16.0" },
+]
+
+[[package]]
+name = "exceptiongroup"
+version = "1.2.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 },
+]
+
+[[package]]
+name = "fastmcp"
+version = "2.11.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "authlib" },
+ { name = "cyclopts" },
+ { name = "exceptiongroup" },
+ { name = "httpx" },
+ { name = "mcp" },
+ { name = "openapi-core" },
+ { name = "openapi-pydantic" },
+ { name = "pydantic", extra = ["email"] },
+ { name = "pyperclip" },
+ { name = "python-dotenv" },
+ { name = "rich" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c2/02/0701624e938fe4d1f13464de9bdc27be9aba2e4c4d41edab3ea496d31751/fastmcp-2.11.0.tar.gz", hash = "sha256:af0c52988607d8e9197df300e91880169e8fe24fd6ca177dca6a9eb6b245ce3c", size = 2663877 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0c/9a/51108b68e77650a7289b5f1ceff8dc0929ab48a26d1d2015f22121a9d183/fastmcp-2.11.0-py3-none-any.whl", hash = "sha256:8709a04522e66fda407b469fbe4d3290651aa7b06097b91c097e9a973c9b9bb3", size = 256193 },
+]
+
+[[package]]
+name = "h11"
+version = "0.14.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 },
+]
+
+[[package]]
+name = "httpcore"
+version = "1.0.7"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "h11" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 },
+]
+
+[[package]]
+name = "httpx"
+version = "0.28.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "certifi" },
+ { name = "httpcore" },
+ { name = "idna" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 },
+]
+
+[[package]]
+name = "httpx-sse"
+version = "0.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819 },
+]
+
+[[package]]
+name = "idna"
+version = "3.10"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
+]
+
+[[package]]
+name = "isodate"
+version = "0.7.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/54/4d/e940025e2ce31a8ce1202635910747e5a87cc3a6a6bb2d00973375014749/isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6", size = 29705 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320 },
+]
+
+[[package]]
+name = "jsonschema"
+version = "4.24.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "attrs" },
+ { name = "jsonschema-specifications" },
+ { name = "referencing" },
+ { name = "rpds-py" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/bf/d3/1cf5326b923a53515d8f3a2cd442e6d7e94fcc444716e879ea70a0ce3177/jsonschema-4.24.0.tar.gz", hash = "sha256:0b4e8069eb12aedfa881333004bccaec24ecef5a8a6a4b6df142b2cc9599d196", size = 353480 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a2/3d/023389198f69c722d039351050738d6755376c8fd343e91dc493ea485905/jsonschema-4.24.0-py3-none-any.whl", hash = "sha256:a462455f19f5faf404a7902952b6f0e3ce868f3ee09a359b05eca6673bd8412d", size = 88709 },
+]
+
+[[package]]
+name = "jsonschema-path"
+version = "0.3.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pathable" },
+ { name = "pyyaml" },
+ { name = "referencing" },
+ { name = "requests" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/6e/45/41ebc679c2a4fced6a722f624c18d658dee42612b83ea24c1caf7c0eb3a8/jsonschema_path-0.3.4.tar.gz", hash = "sha256:8365356039f16cc65fddffafda5f58766e34bebab7d6d105616ab52bc4297001", size = 11159 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cb/58/3485da8cb93d2f393bce453adeef16896751f14ba3e2024bc21dc9597646/jsonschema_path-0.3.4-py3-none-any.whl", hash = "sha256:f502191fdc2b22050f9a81c9237be9d27145b9001c55842bece5e94e382e52f8", size = 14810 },
+]
+
+[[package]]
+name = "jsonschema-specifications"
+version = "2025.4.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "referencing" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/bf/ce/46fbd9c8119cfc3581ee5643ea49464d168028cfb5caff5fc0596d0cf914/jsonschema_specifications-2025.4.1.tar.gz", hash = "sha256:630159c9f4dbea161a6a2205c3011cc4f18ff381b189fff48bb39b9bf26ae608", size = 15513 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/01/0e/b27cdbaccf30b890c40ed1da9fd4a3593a5cf94dae54fb34f8a4b74fcd3f/jsonschema_specifications-2025.4.1-py3-none-any.whl", hash = "sha256:4653bffbd6584f7de83a67e0d620ef16900b390ddc7939d56684d6c81e33f1af", size = 18437 },
+]
+
+[[package]]
+name = "lazy-object-proxy"
+version = "1.11.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/57/f9/1f56571ed82fb324f293661690635cf42c41deb8a70a6c9e6edc3e9bb3c8/lazy_object_proxy-1.11.0.tar.gz", hash = "sha256:18874411864c9fbbbaa47f9fc1dd7aea754c86cfde21278ef427639d1dd78e9c", size = 44736 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/21/c8/457f1555f066f5bacc44337141294153dc993b5e9132272ab54a64ee98a2/lazy_object_proxy-1.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:132bc8a34f2f2d662a851acfd1b93df769992ed1b81e2b1fda7db3e73b0d5a18", size = 28045 },
+ { url = "https://files.pythonhosted.org/packages/18/33/3260b4f8de6f0942008479fee6950b2b40af11fc37dba23aa3672b0ce8a6/lazy_object_proxy-1.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:01261a3afd8621a1accb5682df2593dc7ec7d21d38f411011a5712dcd418fbed", size = 28441 },
+ { url = "https://files.pythonhosted.org/packages/51/f6/eb645ca1ff7408bb69e9b1fe692cce1d74394efdbb40d6207096c0cd8381/lazy_object_proxy-1.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:090935756cc041e191f22f4f9c7fd4fe9a454717067adf5b1bbd2ce3046b556e", size = 28047 },
+ { url = "https://files.pythonhosted.org/packages/13/9c/aabbe1e8b99b8b0edb846b49a517edd636355ac97364419d9ba05b8fa19f/lazy_object_proxy-1.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:76ec715017f06410f57df442c1a8d66e6b5f7035077785b129817f5ae58810a4", size = 28440 },
+ { url = "https://files.pythonhosted.org/packages/4d/24/dae4759469e9cd318fef145f7cfac7318261b47b23a4701aa477b0c3b42c/lazy_object_proxy-1.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9a9f39098e93a63618a79eef2889ae3cf0605f676cd4797fdfd49fcd7ddc318b", size = 28142 },
+ { url = "https://files.pythonhosted.org/packages/de/0c/645a881f5f27952a02f24584d96f9f326748be06ded2cee25f8f8d1cd196/lazy_object_proxy-1.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:ee13f67f4fcd044ef27bfccb1c93d39c100046fec1fad6e9a1fcdfd17492aeb3", size = 28380 },
+ { url = "https://files.pythonhosted.org/packages/a8/0f/6e004f928f7ff5abae2b8e1f68835a3870252f886e006267702e1efc5c7b/lazy_object_proxy-1.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fd4c84eafd8dd15ea16f7d580758bc5c2ce1f752faec877bb2b1f9f827c329cd", size = 28149 },
+ { url = "https://files.pythonhosted.org/packages/63/cb/b8363110e32cc1fd82dc91296315f775d37a39df1c1cfa976ec1803dac89/lazy_object_proxy-1.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:d2503427bda552d3aefcac92f81d9e7ca631e680a2268cbe62cd6a58de6409b7", size = 28389 },
+ { url = "https://files.pythonhosted.org/packages/7b/89/68c50fcfd81e11480cd8ee7f654c9bd790a9053b9a0efe9983d46106f6a9/lazy_object_proxy-1.11.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0613116156801ab3fccb9e2b05ed83b08ea08c2517fdc6c6bc0d4697a1a376e3", size = 28777 },
+ { url = "https://files.pythonhosted.org/packages/39/d0/7e967689e24de8ea6368ec33295f9abc94b9f3f0cd4571bfe148dc432190/lazy_object_proxy-1.11.0-cp313-cp313t-win_amd64.whl", hash = "sha256:bb03c507d96b65f617a6337dedd604399d35face2cdf01526b913fb50c4cb6e8", size = 29598 },
+ { url = "https://files.pythonhosted.org/packages/e7/1e/fb441c07b6662ec1fc92b249225ba6e6e5221b05623cb0131d082f782edc/lazy_object_proxy-1.11.0-py3-none-any.whl", hash = "sha256:a56a5093d433341ff7da0e89f9b486031ccd222ec8e52ec84d0ec1cdc819674b", size = 16635 },
+]
+
+[[package]]
+name = "markdown-it-py"
+version = "3.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "mdurl" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 },
+]
+
+[[package]]
+name = "markupsafe"
+version = "3.0.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357 },
+ { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393 },
+ { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732 },
+ { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866 },
+ { url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964 },
+ { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977 },
+ { url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366 },
+ { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091 },
+ { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065 },
+ { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514 },
+ { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353 },
+ { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392 },
+ { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984 },
+ { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120 },
+ { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032 },
+ { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057 },
+ { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359 },
+ { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306 },
+ { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094 },
+ { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521 },
+ { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274 },
+ { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348 },
+ { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149 },
+ { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118 },
+ { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993 },
+ { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178 },
+ { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319 },
+ { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352 },
+ { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097 },
+ { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601 },
+ { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 },
+ { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 },
+ { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 },
+ { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 },
+ { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 },
+ { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 },
+ { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 },
+ { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 },
+ { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 },
+ { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 },
+ { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 },
+ { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 },
+ { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 },
+ { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 },
+ { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 },
+ { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 },
+ { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 },
+ { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 },
+ { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 },
+ { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 },
+]
+
+[[package]]
+name = "mcp"
+version = "1.10.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "httpx" },
+ { name = "httpx-sse" },
+ { name = "jsonschema" },
+ { name = "pydantic" },
+ { name = "pydantic-settings" },
+ { name = "python-multipart" },
+ { name = "sse-starlette" },
+ { name = "starlette" },
+ { name = "uvicorn", marker = "sys_platform != 'emscripten'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/7c/68/63045305f29ff680a9cd5be360c755270109e6b76f696ea6824547ddbc30/mcp-1.10.1.tar.gz", hash = "sha256:aaa0957d8307feeff180da2d9d359f2b801f35c0c67f1882136239055ef034c2", size = 392969 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d7/3f/435a5b3d10ae242a9d6c2b33175551173c3c61fe637dc893be05c4ed0aaf/mcp-1.10.1-py3-none-any.whl", hash = "sha256:4d08301aefe906dce0fa482289db55ce1db831e3e67212e65b5e23ad8454b3c5", size = 150878 },
+]
+
+[package.optional-dependencies]
+cli = [
+ { name = "python-dotenv" },
+ { name = "typer" },
+]
+
+[[package]]
+name = "mdurl"
+version = "0.1.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 },
+]
+
+[[package]]
+name = "more-itertools"
+version = "10.7.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ce/a0/834b0cebabbfc7e311f30b46c8188790a37f89fc8d756660346fe5abfd09/more_itertools-10.7.0.tar.gz", hash = "sha256:9fddd5403be01a94b204faadcff459ec3568cf110265d3c54323e1e866ad29d3", size = 127671 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2b/9f/7ba6f94fc1e9ac3d2b853fdff3035fb2fa5afbed898c4a72b8a020610594/more_itertools-10.7.0-py3-none-any.whl", hash = "sha256:d43980384673cb07d2f7d2d918c616b30c659c089ee23953f601d6609c67510e", size = 65278 },
+]
+
+[[package]]
+name = "openapi-core"
+version = "0.19.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "isodate" },
+ { name = "jsonschema" },
+ { name = "jsonschema-path" },
+ { name = "more-itertools" },
+ { name = "openapi-schema-validator" },
+ { name = "openapi-spec-validator" },
+ { name = "parse" },
+ { name = "typing-extensions" },
+ { name = "werkzeug" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b1/35/1acaa5f2fcc6e54eded34a2ec74b479439c4e469fc4e8d0e803fda0234db/openapi_core-0.19.5.tar.gz", hash = "sha256:421e753da56c391704454e66afe4803a290108590ac8fa6f4a4487f4ec11f2d3", size = 103264 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/27/6f/83ead0e2e30a90445ee4fc0135f43741aebc30cca5b43f20968b603e30b6/openapi_core-0.19.5-py3-none-any.whl", hash = "sha256:ef7210e83a59394f46ce282639d8d26ad6fc8094aa904c9c16eb1bac8908911f", size = 106595 },
+]
+
+[[package]]
+name = "openapi-pydantic"
+version = "0.5.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pydantic" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/02/2e/58d83848dd1a79cb92ed8e63f6ba901ca282c5f09d04af9423ec26c56fd7/openapi_pydantic-0.5.1.tar.gz", hash = "sha256:ff6835af6bde7a459fb93eb93bb92b8749b754fc6e51b2f1590a19dc3005ee0d", size = 60892 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/12/cf/03675d8bd8ecbf4445504d8071adab19f5f993676795708e36402ab38263/openapi_pydantic-0.5.1-py3-none-any.whl", hash = "sha256:a3a09ef4586f5bd760a8df7f43028b60cafb6d9f61de2acba9574766255ab146", size = 96381 },
+]
+
+[[package]]
+name = "openapi-schema-validator"
+version = "0.6.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "jsonschema" },
+ { name = "jsonschema-specifications" },
+ { name = "rfc3339-validator" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/8b/f3/5507ad3325169347cd8ced61c232ff3df70e2b250c49f0fe140edb4973c6/openapi_schema_validator-0.6.3.tar.gz", hash = "sha256:f37bace4fc2a5d96692f4f8b31dc0f8d7400fd04f3a937798eaf880d425de6ee", size = 11550 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/21/c6/ad0fba32775ae749016829dace42ed80f4407b171da41313d1a3a5f102e4/openapi_schema_validator-0.6.3-py3-none-any.whl", hash = "sha256:f3b9870f4e556b5a62a1c39da72a6b4b16f3ad9c73dc80084b1b11e74ba148a3", size = 8755 },
+]
+
+[[package]]
+name = "openapi-spec-validator"
+version = "0.7.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "jsonschema" },
+ { name = "jsonschema-path" },
+ { name = "lazy-object-proxy" },
+ { name = "openapi-schema-validator" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/82/af/fe2d7618d6eae6fb3a82766a44ed87cd8d6d82b4564ed1c7cfb0f6378e91/openapi_spec_validator-0.7.2.tar.gz", hash = "sha256:cc029309b5c5dbc7859df0372d55e9d1ff43e96d678b9ba087f7c56fc586f734", size = 36855 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/27/dd/b3fd642260cb17532f66cc1e8250f3507d1e580483e209dc1e9d13bd980d/openapi_spec_validator-0.7.2-py3-none-any.whl", hash = "sha256:4bbdc0894ec85f1d1bea1d6d9c8b2c3c8d7ccaa13577ef40da9c006c9fd0eb60", size = 39713 },
+]
+
+[[package]]
+name = "openpyxl"
+version = "3.1.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "et-xmlfile" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/3d/f9/88d94a75de065ea32619465d2f77b29a0469500e99012523b91cc4141cd1/openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050", size = 186464 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910 },
+]
+
+[[package]]
+name = "parse"
+version = "1.20.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/4f/78/d9b09ba24bb36ef8b83b71be547e118d46214735b6dfb39e4bfde0e9b9dd/parse-1.20.2.tar.gz", hash = "sha256:b41d604d16503c79d81af5165155c0b20f6c8d6c559efa66b4b695c3e5a0a0ce", size = 29391 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d0/31/ba45bf0b2aa7898d81cbbfac0e88c267befb59ad91a19e36e1bc5578ddb1/parse-1.20.2-py2.py3-none-any.whl", hash = "sha256:967095588cb802add9177d0c0b6133b5ba33b1ea9007ca800e526f42a85af558", size = 20126 },
+]
+
+[[package]]
+name = "pathable"
+version = "0.4.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/67/93/8f2c2075b180c12c1e9f6a09d1a985bc2036906b13dff1d8917e395f2048/pathable-0.4.4.tar.gz", hash = "sha256:6905a3cd17804edfac7875b5f6c9142a218c7caef78693c2dbbbfbac186d88b2", size = 8124 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7d/eb/b6260b31b1a96386c0a880edebe26f89669098acea8e0318bff6adb378fd/pathable-0.4.4-py3-none-any.whl", hash = "sha256:5ae9e94793b6ef5a4cbe0a7ce9dbbefc1eec38df253763fd0aeeacf2762dbbc2", size = 9592 },
+]
+
+[[package]]
+name = "pycparser"
+version = "2.22"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 },
+]
+
+[[package]]
+name = "pydantic"
+version = "2.11.7"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "annotated-types" },
+ { name = "pydantic-core" },
+ { name = "typing-extensions" },
+ { name = "typing-inspection" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782 },
+]
+
+[package.optional-dependencies]
+email = [
+ { name = "email-validator" },
+]
+
+[[package]]
+name = "pydantic-core"
+version = "2.33.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e5/92/b31726561b5dae176c2d2c2dc43a9c5bfba5d32f96f8b4c0a600dd492447/pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8", size = 2028817 },
+ { url = "https://files.pythonhosted.org/packages/a3/44/3f0b95fafdaca04a483c4e685fe437c6891001bf3ce8b2fded82b9ea3aa1/pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d", size = 1861357 },
+ { url = "https://files.pythonhosted.org/packages/30/97/e8f13b55766234caae05372826e8e4b3b96e7b248be3157f53237682e43c/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d", size = 1898011 },
+ { url = "https://files.pythonhosted.org/packages/9b/a3/99c48cf7bafc991cc3ee66fd544c0aae8dc907b752f1dad2d79b1b5a471f/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572", size = 1982730 },
+ { url = "https://files.pythonhosted.org/packages/de/8e/a5b882ec4307010a840fb8b58bd9bf65d1840c92eae7534c7441709bf54b/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02", size = 2136178 },
+ { url = "https://files.pythonhosted.org/packages/e4/bb/71e35fc3ed05af6834e890edb75968e2802fe98778971ab5cba20a162315/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b", size = 2736462 },
+ { url = "https://files.pythonhosted.org/packages/31/0d/c8f7593e6bc7066289bbc366f2235701dcbebcd1ff0ef8e64f6f239fb47d/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2", size = 2005652 },
+ { url = "https://files.pythonhosted.org/packages/d2/7a/996d8bd75f3eda405e3dd219ff5ff0a283cd8e34add39d8ef9157e722867/pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a", size = 2113306 },
+ { url = "https://files.pythonhosted.org/packages/ff/84/daf2a6fb2db40ffda6578a7e8c5a6e9c8affb251a05c233ae37098118788/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac", size = 2073720 },
+ { url = "https://files.pythonhosted.org/packages/77/fb/2258da019f4825128445ae79456a5499c032b55849dbd5bed78c95ccf163/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a", size = 2244915 },
+ { url = "https://files.pythonhosted.org/packages/d8/7a/925ff73756031289468326e355b6fa8316960d0d65f8b5d6b3a3e7866de7/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b", size = 2241884 },
+ { url = "https://files.pythonhosted.org/packages/0b/b0/249ee6d2646f1cdadcb813805fe76265745c4010cf20a8eba7b0e639d9b2/pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22", size = 1910496 },
+ { url = "https://files.pythonhosted.org/packages/66/ff/172ba8f12a42d4b552917aa65d1f2328990d3ccfc01d5b7c943ec084299f/pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640", size = 1955019 },
+ { url = "https://files.pythonhosted.org/packages/3f/8d/71db63483d518cbbf290261a1fc2839d17ff89fce7089e08cad07ccfce67/pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7", size = 2028584 },
+ { url = "https://files.pythonhosted.org/packages/24/2f/3cfa7244ae292dd850989f328722d2aef313f74ffc471184dc509e1e4e5a/pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246", size = 1855071 },
+ { url = "https://files.pythonhosted.org/packages/b3/d3/4ae42d33f5e3f50dd467761304be2fa0a9417fbf09735bc2cce003480f2a/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f", size = 1897823 },
+ { url = "https://files.pythonhosted.org/packages/f4/f3/aa5976e8352b7695ff808599794b1fba2a9ae2ee954a3426855935799488/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc", size = 1983792 },
+ { url = "https://files.pythonhosted.org/packages/d5/7a/cda9b5a23c552037717f2b2a5257e9b2bfe45e687386df9591eff7b46d28/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de", size = 2136338 },
+ { url = "https://files.pythonhosted.org/packages/2b/9f/b8f9ec8dd1417eb9da784e91e1667d58a2a4a7b7b34cf4af765ef663a7e5/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a", size = 2730998 },
+ { url = "https://files.pythonhosted.org/packages/47/bc/cd720e078576bdb8255d5032c5d63ee5c0bf4b7173dd955185a1d658c456/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef", size = 2003200 },
+ { url = "https://files.pythonhosted.org/packages/ca/22/3602b895ee2cd29d11a2b349372446ae9727c32e78a94b3d588a40fdf187/pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e", size = 2113890 },
+ { url = "https://files.pythonhosted.org/packages/ff/e6/e3c5908c03cf00d629eb38393a98fccc38ee0ce8ecce32f69fc7d7b558a7/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d", size = 2073359 },
+ { url = "https://files.pythonhosted.org/packages/12/e7/6a36a07c59ebefc8777d1ffdaf5ae71b06b21952582e4b07eba88a421c79/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30", size = 2245883 },
+ { url = "https://files.pythonhosted.org/packages/16/3f/59b3187aaa6cc0c1e6616e8045b284de2b6a87b027cce2ffcea073adf1d2/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf", size = 2241074 },
+ { url = "https://files.pythonhosted.org/packages/e0/ed/55532bb88f674d5d8f67ab121a2a13c385df382de2a1677f30ad385f7438/pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51", size = 1910538 },
+ { url = "https://files.pythonhosted.org/packages/fe/1b/25b7cccd4519c0b23c2dd636ad39d381abf113085ce4f7bec2b0dc755eb1/pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab", size = 1952909 },
+ { url = "https://files.pythonhosted.org/packages/49/a9/d809358e49126438055884c4366a1f6227f0f84f635a9014e2deb9b9de54/pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65", size = 1897786 },
+ { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000 },
+ { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996 },
+ { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957 },
+ { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199 },
+ { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296 },
+ { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109 },
+ { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028 },
+ { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044 },
+ { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881 },
+ { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034 },
+ { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187 },
+ { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628 },
+ { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866 },
+ { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894 },
+ { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688 },
+ { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808 },
+ { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580 },
+ { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859 },
+ { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810 },
+ { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498 },
+ { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611 },
+ { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924 },
+ { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196 },
+ { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389 },
+ { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223 },
+ { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473 },
+ { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269 },
+ { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921 },
+ { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162 },
+ { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560 },
+ { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777 },
+ { url = "https://files.pythonhosted.org/packages/30/68/373d55e58b7e83ce371691f6eaa7175e3a24b956c44628eb25d7da007917/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa", size = 2023982 },
+ { url = "https://files.pythonhosted.org/packages/a4/16/145f54ac08c96a63d8ed6442f9dec17b2773d19920b627b18d4f10a061ea/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29", size = 1858412 },
+ { url = "https://files.pythonhosted.org/packages/41/b1/c6dc6c3e2de4516c0bb2c46f6a373b91b5660312342a0cf5826e38ad82fa/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d", size = 1892749 },
+ { url = "https://files.pythonhosted.org/packages/12/73/8cd57e20afba760b21b742106f9dbdfa6697f1570b189c7457a1af4cd8a0/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e", size = 2067527 },
+ { url = "https://files.pythonhosted.org/packages/e3/d5/0bb5d988cc019b3cba4a78f2d4b3854427fc47ee8ec8e9eaabf787da239c/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c", size = 2108225 },
+ { url = "https://files.pythonhosted.org/packages/f1/c5/00c02d1571913d496aabf146106ad8239dc132485ee22efe08085084ff7c/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec", size = 2069490 },
+ { url = "https://files.pythonhosted.org/packages/22/a8/dccc38768274d3ed3a59b5d06f59ccb845778687652daa71df0cab4040d7/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052", size = 2237525 },
+ { url = "https://files.pythonhosted.org/packages/d4/e7/4f98c0b125dda7cf7ccd14ba936218397b44f50a56dd8c16a3091df116c3/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c", size = 2238446 },
+ { url = "https://files.pythonhosted.org/packages/ce/91/2ec36480fdb0b783cd9ef6795753c1dea13882f2e68e73bce76ae8c21e6a/pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808", size = 2066678 },
+ { url = "https://files.pythonhosted.org/packages/7b/27/d4ae6487d73948d6f20dddcd94be4ea43e74349b56eba82e9bdee2d7494c/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8", size = 2025200 },
+ { url = "https://files.pythonhosted.org/packages/f1/b8/b3cb95375f05d33801024079b9392a5ab45267a63400bf1866e7ce0f0de4/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593", size = 1859123 },
+ { url = "https://files.pythonhosted.org/packages/05/bc/0d0b5adeda59a261cd30a1235a445bf55c7e46ae44aea28f7bd6ed46e091/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612", size = 1892852 },
+ { url = "https://files.pythonhosted.org/packages/3e/11/d37bdebbda2e449cb3f519f6ce950927b56d62f0b84fd9cb9e372a26a3d5/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7", size = 2067484 },
+ { url = "https://files.pythonhosted.org/packages/8c/55/1f95f0a05ce72ecb02a8a8a1c3be0579bbc29b1d5ab68f1378b7bebc5057/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e", size = 2108896 },
+ { url = "https://files.pythonhosted.org/packages/53/89/2b2de6c81fa131f423246a9109d7b2a375e83968ad0800d6e57d0574629b/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8", size = 2069475 },
+ { url = "https://files.pythonhosted.org/packages/b8/e9/1f7efbe20d0b2b10f6718944b5d8ece9152390904f29a78e68d4e7961159/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf", size = 2239013 },
+ { url = "https://files.pythonhosted.org/packages/3c/b2/5309c905a93811524a49b4e031e9851a6b00ff0fb668794472ea7746b448/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb", size = 2238715 },
+ { url = "https://files.pythonhosted.org/packages/32/56/8a7ca5d2cd2cda1d245d34b1c9a942920a718082ae8e54e5f3e5a58b7add/pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1", size = 2066757 },
+]
+
+[[package]]
+name = "pydantic-settings"
+version = "2.7.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pydantic" },
+ { name = "python-dotenv" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/73/7b/c58a586cd7d9ac66d2ee4ba60ca2d241fa837c02bca9bea80a9a8c3d22a9/pydantic_settings-2.7.1.tar.gz", hash = "sha256:10c9caad35e64bfb3c2fbf70a078c0e25cc92499782e5200747f942a065dec93", size = 79920 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b4/46/93416fdae86d40879714f72956ac14df9c7b76f7d41a4d68aa9f71a0028b/pydantic_settings-2.7.1-py3-none-any.whl", hash = "sha256:590be9e6e24d06db33a4262829edef682500ef008565a969c73d39d5f8bfb3fd", size = 29718 },
+]
+
+[[package]]
+name = "pygments"
+version = "2.19.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 },
+]
+
+[[package]]
+name = "pyperclip"
+version = "1.9.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/30/23/2f0a3efc4d6a32f3b63cdff36cd398d9701d26cda58e3ab97ac79fb5e60d/pyperclip-1.9.0.tar.gz", hash = "sha256:b7de0142ddc81bfc5c7507eea19da920b92252b548b96186caf94a5e2527d310", size = 20961 }
+
+[[package]]
+name = "python-dotenv"
+version = "1.1.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556 },
+]
+
+[[package]]
+name = "python-multipart"
+version = "0.0.20"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546 },
+]
+
+[[package]]
+name = "pyyaml"
+version = "6.0.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199 },
+ { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758 },
+ { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463 },
+ { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280 },
+ { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239 },
+ { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802 },
+ { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527 },
+ { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052 },
+ { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774 },
+ { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 },
+ { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 },
+ { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 },
+ { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 },
+ { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 },
+ { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 },
+ { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 },
+ { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 },
+ { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 },
+ { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 },
+ { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 },
+ { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 },
+ { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 },
+ { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 },
+ { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 },
+ { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 },
+ { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 },
+ { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 },
+ { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 },
+ { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 },
+ { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 },
+ { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 },
+ { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 },
+ { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 },
+ { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 },
+ { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 },
+ { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 },
+]
+
+[[package]]
+name = "referencing"
+version = "0.36.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "attrs" },
+ { name = "rpds-py" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775 },
+]
+
+[[package]]
+name = "requests"
+version = "2.32.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "charset-normalizer" },
+ { name = "idna" },
+ { name = "urllib3" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847 },
+]
+
+[[package]]
+name = "rfc3339-validator"
+version = "0.1.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "six" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/28/ea/a9387748e2d111c3c2b275ba970b735e04e15cdb1eb30693b6b5708c4dbd/rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b", size = 5513 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa", size = 3490 },
+]
+
+[[package]]
+name = "rich"
+version = "13.9.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markdown-it-py" },
+ { name = "pygments" },
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 },
+]
+
+[[package]]
+name = "rich-rst"
+version = "1.3.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "docutils" },
+ { name = "rich" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b0/69/5514c3a87b5f10f09a34bb011bc0927bc12c596c8dae5915604e71abc386/rich_rst-1.3.1.tar.gz", hash = "sha256:fad46e3ba42785ea8c1785e2ceaa56e0ffa32dbe5410dec432f37e4107c4f383", size = 13839 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fd/bc/cc4e3dbc5e7992398dcb7a8eda0cbcf4fb792a0cdb93f857b478bf3cf884/rich_rst-1.3.1-py3-none-any.whl", hash = "sha256:498a74e3896507ab04492d326e794c3ef76e7cda078703aa592d1853d91098c1", size = 11621 },
+]
+
+[[package]]
+name = "rpds-py"
+version = "0.26.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a5/aa/4456d84bbb54adc6a916fb10c9b374f78ac840337644e4a5eda229c81275/rpds_py-0.26.0.tar.gz", hash = "sha256:20dae58a859b0906f0685642e591056f1e787f3a8b39c8e8749a45dc7d26bdb0", size = 27385 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b9/31/1459645f036c3dfeacef89e8e5825e430c77dde8489f3b99eaafcd4a60f5/rpds_py-0.26.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:4c70c70f9169692b36307a95f3d8c0a9fcd79f7b4a383aad5eaa0e9718b79b37", size = 372466 },
+ { url = "https://files.pythonhosted.org/packages/dd/ff/3d0727f35836cc8773d3eeb9a46c40cc405854e36a8d2e951f3a8391c976/rpds_py-0.26.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:777c62479d12395bfb932944e61e915741e364c843afc3196b694db3d669fcd0", size = 357825 },
+ { url = "https://files.pythonhosted.org/packages/bf/ce/badc5e06120a54099ae287fa96d82cbb650a5f85cf247ffe19c7b157fd1f/rpds_py-0.26.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec671691e72dff75817386aa02d81e708b5a7ec0dec6669ec05213ff6b77e1bd", size = 381530 },
+ { url = "https://files.pythonhosted.org/packages/1e/a5/fa5d96a66c95d06c62d7a30707b6a4cfec696ab8ae280ee7be14e961e118/rpds_py-0.26.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6a1cb5d6ce81379401bbb7f6dbe3d56de537fb8235979843f0d53bc2e9815a79", size = 396933 },
+ { url = "https://files.pythonhosted.org/packages/00/a7/7049d66750f18605c591a9db47d4a059e112a0c9ff8de8daf8fa0f446bba/rpds_py-0.26.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4f789e32fa1fb6a7bf890e0124e7b42d1e60d28ebff57fe806719abb75f0e9a3", size = 513973 },
+ { url = "https://files.pythonhosted.org/packages/0e/f1/528d02c7d6b29d29fac8fd784b354d3571cc2153f33f842599ef0cf20dd2/rpds_py-0.26.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c55b0a669976cf258afd718de3d9ad1b7d1fe0a91cd1ab36f38b03d4d4aeaaf", size = 402293 },
+ { url = "https://files.pythonhosted.org/packages/15/93/fde36cd6e4685df2cd08508f6c45a841e82f5bb98c8d5ecf05649522acb5/rpds_py-0.26.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c70d9ec912802ecfd6cd390dadb34a9578b04f9bcb8e863d0a7598ba5e9e7ccc", size = 383787 },
+ { url = "https://files.pythonhosted.org/packages/69/f2/5007553aaba1dcae5d663143683c3dfd03d9395289f495f0aebc93e90f24/rpds_py-0.26.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3021933c2cb7def39d927b9862292e0f4c75a13d7de70eb0ab06efed4c508c19", size = 416312 },
+ { url = "https://files.pythonhosted.org/packages/8f/a7/ce52c75c1e624a79e48a69e611f1c08844564e44c85db2b6f711d76d10ce/rpds_py-0.26.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8a7898b6ca3b7d6659e55cdac825a2e58c638cbf335cde41f4619e290dd0ad11", size = 558403 },
+ { url = "https://files.pythonhosted.org/packages/79/d5/e119db99341cc75b538bf4cb80504129fa22ce216672fb2c28e4a101f4d9/rpds_py-0.26.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:12bff2ad9447188377f1b2794772f91fe68bb4bbfa5a39d7941fbebdbf8c500f", size = 588323 },
+ { url = "https://files.pythonhosted.org/packages/93/94/d28272a0b02f5fe24c78c20e13bbcb95f03dc1451b68e7830ca040c60bd6/rpds_py-0.26.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:191aa858f7d4902e975d4cf2f2d9243816c91e9605070aeb09c0a800d187e323", size = 554541 },
+ { url = "https://files.pythonhosted.org/packages/93/e0/8c41166602f1b791da892d976057eba30685486d2e2c061ce234679c922b/rpds_py-0.26.0-cp310-cp310-win32.whl", hash = "sha256:b37a04d9f52cb76b6b78f35109b513f6519efb481d8ca4c321f6a3b9580b3f45", size = 220442 },
+ { url = "https://files.pythonhosted.org/packages/87/f0/509736bb752a7ab50fb0270c2a4134d671a7b3038030837e5536c3de0e0b/rpds_py-0.26.0-cp310-cp310-win_amd64.whl", hash = "sha256:38721d4c9edd3eb6670437d8d5e2070063f305bfa2d5aa4278c51cedcd508a84", size = 231314 },
+ { url = "https://files.pythonhosted.org/packages/09/4c/4ee8f7e512030ff79fda1df3243c88d70fc874634e2dbe5df13ba4210078/rpds_py-0.26.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:9e8cb77286025bdb21be2941d64ac6ca016130bfdcd228739e8ab137eb4406ed", size = 372610 },
+ { url = "https://files.pythonhosted.org/packages/fa/9d/3dc16be00f14fc1f03c71b1d67c8df98263ab2710a2fbd65a6193214a527/rpds_py-0.26.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5e09330b21d98adc8ccb2dbb9fc6cb434e8908d4c119aeaa772cb1caab5440a0", size = 358032 },
+ { url = "https://files.pythonhosted.org/packages/e7/5a/7f1bf8f045da2866324a08ae80af63e64e7bfaf83bd31f865a7b91a58601/rpds_py-0.26.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c9c1b92b774b2e68d11193dc39620d62fd8ab33f0a3c77ecdabe19c179cdbc1", size = 381525 },
+ { url = "https://files.pythonhosted.org/packages/45/8a/04479398c755a066ace10e3d158866beb600867cacae194c50ffa783abd0/rpds_py-0.26.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:824e6d3503ab990d7090768e4dfd9e840837bae057f212ff9f4f05ec6d1975e7", size = 397089 },
+ { url = "https://files.pythonhosted.org/packages/72/88/9203f47268db488a1b6d469d69c12201ede776bb728b9d9f29dbfd7df406/rpds_py-0.26.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8ad7fd2258228bf288f2331f0a6148ad0186b2e3643055ed0db30990e59817a6", size = 514255 },
+ { url = "https://files.pythonhosted.org/packages/f5/b4/01ce5d1e853ddf81fbbd4311ab1eff0b3cf162d559288d10fd127e2588b5/rpds_py-0.26.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0dc23bbb3e06ec1ea72d515fb572c1fea59695aefbffb106501138762e1e915e", size = 402283 },
+ { url = "https://files.pythonhosted.org/packages/34/a2/004c99936997bfc644d590a9defd9e9c93f8286568f9c16cdaf3e14429a7/rpds_py-0.26.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d80bf832ac7b1920ee29a426cdca335f96a2b5caa839811803e999b41ba9030d", size = 383881 },
+ { url = "https://files.pythonhosted.org/packages/05/1b/ef5fba4a8f81ce04c427bfd96223f92f05e6cd72291ce9d7523db3b03a6c/rpds_py-0.26.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0919f38f5542c0a87e7b4afcafab6fd2c15386632d249e9a087498571250abe3", size = 415822 },
+ { url = "https://files.pythonhosted.org/packages/16/80/5c54195aec456b292f7bd8aa61741c8232964063fd8a75fdde9c1e982328/rpds_py-0.26.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d422b945683e409000c888e384546dbab9009bb92f7c0b456e217988cf316107", size = 558347 },
+ { url = "https://files.pythonhosted.org/packages/f2/1c/1845c1b1fd6d827187c43afe1841d91678d7241cbdb5420a4c6de180a538/rpds_py-0.26.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:77a7711fa562ba2da1aa757e11024ad6d93bad6ad7ede5afb9af144623e5f76a", size = 587956 },
+ { url = "https://files.pythonhosted.org/packages/2e/ff/9e979329dd131aa73a438c077252ddabd7df6d1a7ad7b9aacf6261f10faa/rpds_py-0.26.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:238e8c8610cb7c29460e37184f6799547f7e09e6a9bdbdab4e8edb90986a2318", size = 554363 },
+ { url = "https://files.pythonhosted.org/packages/00/8b/d78cfe034b71ffbe72873a136e71acc7a831a03e37771cfe59f33f6de8a2/rpds_py-0.26.0-cp311-cp311-win32.whl", hash = "sha256:893b022bfbdf26d7bedb083efeea624e8550ca6eb98bf7fea30211ce95b9201a", size = 220123 },
+ { url = "https://files.pythonhosted.org/packages/94/c1/3c8c94c7dd3905dbfde768381ce98778500a80db9924731d87ddcdb117e9/rpds_py-0.26.0-cp311-cp311-win_amd64.whl", hash = "sha256:87a5531de9f71aceb8af041d72fc4cab4943648d91875ed56d2e629bef6d4c03", size = 231732 },
+ { url = "https://files.pythonhosted.org/packages/67/93/e936fbed1b734eabf36ccb5d93c6a2e9246fbb13c1da011624b7286fae3e/rpds_py-0.26.0-cp311-cp311-win_arm64.whl", hash = "sha256:de2713f48c1ad57f89ac25b3cb7daed2156d8e822cf0eca9b96a6f990718cc41", size = 221917 },
+ { url = "https://files.pythonhosted.org/packages/ea/86/90eb87c6f87085868bd077c7a9938006eb1ce19ed4d06944a90d3560fce2/rpds_py-0.26.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:894514d47e012e794f1350f076c427d2347ebf82f9b958d554d12819849a369d", size = 363933 },
+ { url = "https://files.pythonhosted.org/packages/63/78/4469f24d34636242c924626082b9586f064ada0b5dbb1e9d096ee7a8e0c6/rpds_py-0.26.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc921b96fa95a097add244da36a1d9e4f3039160d1d30f1b35837bf108c21136", size = 350447 },
+ { url = "https://files.pythonhosted.org/packages/ad/91/c448ed45efdfdade82348d5e7995e15612754826ea640afc20915119734f/rpds_py-0.26.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e1157659470aa42a75448b6e943c895be8c70531c43cb78b9ba990778955582", size = 384711 },
+ { url = "https://files.pythonhosted.org/packages/ec/43/e5c86fef4be7f49828bdd4ecc8931f0287b1152c0bb0163049b3218740e7/rpds_py-0.26.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:521ccf56f45bb3a791182dc6b88ae5f8fa079dd705ee42138c76deb1238e554e", size = 400865 },
+ { url = "https://files.pythonhosted.org/packages/55/34/e00f726a4d44f22d5c5fe2e5ddd3ac3d7fd3f74a175607781fbdd06fe375/rpds_py-0.26.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9def736773fd56b305c0eef698be5192c77bfa30d55a0e5885f80126c4831a15", size = 517763 },
+ { url = "https://files.pythonhosted.org/packages/52/1c/52dc20c31b147af724b16104500fba13e60123ea0334beba7b40e33354b4/rpds_py-0.26.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cdad4ea3b4513b475e027be79e5a0ceac8ee1c113a1a11e5edc3c30c29f964d8", size = 406651 },
+ { url = "https://files.pythonhosted.org/packages/2e/77/87d7bfabfc4e821caa35481a2ff6ae0b73e6a391bb6b343db2c91c2b9844/rpds_py-0.26.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82b165b07f416bdccf5c84546a484cc8f15137ca38325403864bfdf2b5b72f6a", size = 386079 },
+ { url = "https://files.pythonhosted.org/packages/e3/d4/7f2200c2d3ee145b65b3cddc4310d51f7da6a26634f3ac87125fd789152a/rpds_py-0.26.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d04cab0a54b9dba4d278fe955a1390da3cf71f57feb78ddc7cb67cbe0bd30323", size = 421379 },
+ { url = "https://files.pythonhosted.org/packages/ae/13/9fdd428b9c820869924ab62236b8688b122baa22d23efdd1c566938a39ba/rpds_py-0.26.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:79061ba1a11b6a12743a2b0f72a46aa2758613d454aa6ba4f5a265cc48850158", size = 562033 },
+ { url = "https://files.pythonhosted.org/packages/f3/e1/b69686c3bcbe775abac3a4c1c30a164a2076d28df7926041f6c0eb5e8d28/rpds_py-0.26.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f405c93675d8d4c5ac87364bb38d06c988e11028a64b52a47158a355079661f3", size = 591639 },
+ { url = "https://files.pythonhosted.org/packages/5c/c9/1e3d8c8863c84a90197ac577bbc3d796a92502124c27092413426f670990/rpds_py-0.26.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dafd4c44b74aa4bed4b250f1aed165b8ef5de743bcca3b88fc9619b6087093d2", size = 557105 },
+ { url = "https://files.pythonhosted.org/packages/9f/c5/90c569649057622959f6dcc40f7b516539608a414dfd54b8d77e3b201ac0/rpds_py-0.26.0-cp312-cp312-win32.whl", hash = "sha256:3da5852aad63fa0c6f836f3359647870e21ea96cf433eb393ffa45263a170d44", size = 223272 },
+ { url = "https://files.pythonhosted.org/packages/7d/16/19f5d9f2a556cfed454eebe4d354c38d51c20f3db69e7b4ce6cff904905d/rpds_py-0.26.0-cp312-cp312-win_amd64.whl", hash = "sha256:cf47cfdabc2194a669dcf7a8dbba62e37a04c5041d2125fae0233b720da6f05c", size = 234995 },
+ { url = "https://files.pythonhosted.org/packages/83/f0/7935e40b529c0e752dfaa7880224771b51175fce08b41ab4a92eb2fbdc7f/rpds_py-0.26.0-cp312-cp312-win_arm64.whl", hash = "sha256:20ab1ae4fa534f73647aad289003f1104092890849e0266271351922ed5574f8", size = 223198 },
+ { url = "https://files.pythonhosted.org/packages/6a/67/bb62d0109493b12b1c6ab00de7a5566aa84c0e44217c2d94bee1bd370da9/rpds_py-0.26.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:696764a5be111b036256c0b18cd29783fab22154690fc698062fc1b0084b511d", size = 363917 },
+ { url = "https://files.pythonhosted.org/packages/4b/f3/34e6ae1925a5706c0f002a8d2d7f172373b855768149796af87bd65dcdb9/rpds_py-0.26.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1e6c15d2080a63aaed876e228efe4f814bc7889c63b1e112ad46fdc8b368b9e1", size = 350073 },
+ { url = "https://files.pythonhosted.org/packages/75/83/1953a9d4f4e4de7fd0533733e041c28135f3c21485faaef56a8aadbd96b5/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:390e3170babf42462739a93321e657444f0862c6d722a291accc46f9d21ed04e", size = 384214 },
+ { url = "https://files.pythonhosted.org/packages/48/0e/983ed1b792b3322ea1d065e67f4b230f3b96025f5ce3878cc40af09b7533/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7da84c2c74c0f5bc97d853d9e17bb83e2dcafcff0dc48286916001cc114379a1", size = 400113 },
+ { url = "https://files.pythonhosted.org/packages/69/7f/36c0925fff6f660a80be259c5b4f5e53a16851f946eb080351d057698528/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c5fe114a6dd480a510b6d3661d09d67d1622c4bf20660a474507aaee7eeeee9", size = 515189 },
+ { url = "https://files.pythonhosted.org/packages/13/45/cbf07fc03ba7a9b54662c9badb58294ecfb24f828b9732970bd1a431ed5c/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3100b3090269f3a7ea727b06a6080d4eb7439dca4c0e91a07c5d133bb1727ea7", size = 406998 },
+ { url = "https://files.pythonhosted.org/packages/6c/b0/8fa5e36e58657997873fd6a1cf621285ca822ca75b4b3434ead047daa307/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c03c9b0c64afd0320ae57de4c982801271c0c211aa2d37f3003ff5feb75bb04", size = 385903 },
+ { url = "https://files.pythonhosted.org/packages/4b/f7/b25437772f9f57d7a9fbd73ed86d0dcd76b4c7c6998348c070d90f23e315/rpds_py-0.26.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5963b72ccd199ade6ee493723d18a3f21ba7d5b957017607f815788cef50eaf1", size = 419785 },
+ { url = "https://files.pythonhosted.org/packages/a7/6b/63ffa55743dfcb4baf2e9e77a0b11f7f97ed96a54558fcb5717a4b2cd732/rpds_py-0.26.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9da4e873860ad5bab3291438525cae80169daecbfafe5657f7f5fb4d6b3f96b9", size = 561329 },
+ { url = "https://files.pythonhosted.org/packages/2f/07/1f4f5e2886c480a2346b1e6759c00278b8a69e697ae952d82ae2e6ee5db0/rpds_py-0.26.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5afaddaa8e8c7f1f7b4c5c725c0070b6eed0228f705b90a1732a48e84350f4e9", size = 590875 },
+ { url = "https://files.pythonhosted.org/packages/cc/bc/e6639f1b91c3a55f8c41b47d73e6307051b6e246254a827ede730624c0f8/rpds_py-0.26.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4916dc96489616a6f9667e7526af8fa693c0fdb4f3acb0e5d9f4400eb06a47ba", size = 556636 },
+ { url = "https://files.pythonhosted.org/packages/05/4c/b3917c45566f9f9a209d38d9b54a1833f2bb1032a3e04c66f75726f28876/rpds_py-0.26.0-cp313-cp313-win32.whl", hash = "sha256:2a343f91b17097c546b93f7999976fd6c9d5900617aa848c81d794e062ab302b", size = 222663 },
+ { url = "https://files.pythonhosted.org/packages/e0/0b/0851bdd6025775aaa2365bb8de0697ee2558184c800bfef8d7aef5ccde58/rpds_py-0.26.0-cp313-cp313-win_amd64.whl", hash = "sha256:0a0b60701f2300c81b2ac88a5fb893ccfa408e1c4a555a77f908a2596eb875a5", size = 234428 },
+ { url = "https://files.pythonhosted.org/packages/ed/e8/a47c64ed53149c75fb581e14a237b7b7cd18217e969c30d474d335105622/rpds_py-0.26.0-cp313-cp313-win_arm64.whl", hash = "sha256:257d011919f133a4746958257f2c75238e3ff54255acd5e3e11f3ff41fd14256", size = 222571 },
+ { url = "https://files.pythonhosted.org/packages/89/bf/3d970ba2e2bcd17d2912cb42874107390f72873e38e79267224110de5e61/rpds_py-0.26.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:529c8156d7506fba5740e05da8795688f87119cce330c244519cf706a4a3d618", size = 360475 },
+ { url = "https://files.pythonhosted.org/packages/82/9f/283e7e2979fc4ec2d8ecee506d5a3675fce5ed9b4b7cb387ea5d37c2f18d/rpds_py-0.26.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f53ec51f9d24e9638a40cabb95078ade8c99251945dad8d57bf4aabe86ecee35", size = 346692 },
+ { url = "https://files.pythonhosted.org/packages/e3/03/7e50423c04d78daf391da3cc4330bdb97042fc192a58b186f2d5deb7befd/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab504c4d654e4a29558eaa5bb8cea5fdc1703ea60a8099ffd9c758472cf913f", size = 379415 },
+ { url = "https://files.pythonhosted.org/packages/57/00/d11ee60d4d3b16808432417951c63df803afb0e0fc672b5e8d07e9edaaae/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd0641abca296bc1a00183fe44f7fced8807ed49d501f188faa642d0e4975b83", size = 391783 },
+ { url = "https://files.pythonhosted.org/packages/08/b3/1069c394d9c0d6d23c5b522e1f6546b65793a22950f6e0210adcc6f97c3e/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69b312fecc1d017b5327afa81d4da1480f51c68810963a7336d92203dbb3d4f1", size = 512844 },
+ { url = "https://files.pythonhosted.org/packages/08/3b/c4fbf0926800ed70b2c245ceca99c49f066456755f5d6eb8863c2c51e6d0/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c741107203954f6fc34d3066d213d0a0c40f7bb5aafd698fb39888af277c70d8", size = 402105 },
+ { url = "https://files.pythonhosted.org/packages/1c/b0/db69b52ca07413e568dae9dc674627a22297abb144c4d6022c6d78f1e5cc/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc3e55a7db08dc9a6ed5fb7103019d2c1a38a349ac41901f9f66d7f95750942f", size = 383440 },
+ { url = "https://files.pythonhosted.org/packages/4c/e1/c65255ad5b63903e56b3bb3ff9dcc3f4f5c3badde5d08c741ee03903e951/rpds_py-0.26.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9e851920caab2dbcae311fd28f4313c6953993893eb5c1bb367ec69d9a39e7ed", size = 412759 },
+ { url = "https://files.pythonhosted.org/packages/e4/22/bb731077872377a93c6e93b8a9487d0406c70208985831034ccdeed39c8e/rpds_py-0.26.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dfbf280da5f876d0b00c81f26bedce274e72a678c28845453885a9b3c22ae632", size = 556032 },
+ { url = "https://files.pythonhosted.org/packages/e0/8b/393322ce7bac5c4530fb96fc79cc9ea2f83e968ff5f6e873f905c493e1c4/rpds_py-0.26.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:1cc81d14ddfa53d7f3906694d35d54d9d3f850ef8e4e99ee68bc0d1e5fed9a9c", size = 585416 },
+ { url = "https://files.pythonhosted.org/packages/49/ae/769dc372211835bf759319a7aae70525c6eb523e3371842c65b7ef41c9c6/rpds_py-0.26.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dca83c498b4650a91efcf7b88d669b170256bf8017a5db6f3e06c2bf031f57e0", size = 554049 },
+ { url = "https://files.pythonhosted.org/packages/6b/f9/4c43f9cc203d6ba44ce3146246cdc38619d92c7bd7bad4946a3491bd5b70/rpds_py-0.26.0-cp313-cp313t-win32.whl", hash = "sha256:4d11382bcaf12f80b51d790dee295c56a159633a8e81e6323b16e55d81ae37e9", size = 218428 },
+ { url = "https://files.pythonhosted.org/packages/7e/8b/9286b7e822036a4a977f2f1e851c7345c20528dbd56b687bb67ed68a8ede/rpds_py-0.26.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff110acded3c22c033e637dd8896e411c7d3a11289b2edf041f86663dbc791e9", size = 231524 },
+ { url = "https://files.pythonhosted.org/packages/55/07/029b7c45db910c74e182de626dfdae0ad489a949d84a468465cd0ca36355/rpds_py-0.26.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:da619979df60a940cd434084355c514c25cf8eb4cf9a508510682f6c851a4f7a", size = 364292 },
+ { url = "https://files.pythonhosted.org/packages/13/d1/9b3d3f986216b4d1f584878dca15ce4797aaf5d372d738974ba737bf68d6/rpds_py-0.26.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ea89a2458a1a75f87caabefe789c87539ea4e43b40f18cff526052e35bbb4fdf", size = 350334 },
+ { url = "https://files.pythonhosted.org/packages/18/98/16d5e7bc9ec715fa9668731d0cf97f6b032724e61696e2db3d47aeb89214/rpds_py-0.26.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feac1045b3327a45944e7dcbeb57530339f6b17baff154df51ef8b0da34c8c12", size = 384875 },
+ { url = "https://files.pythonhosted.org/packages/f9/13/aa5e2b1ec5ab0e86a5c464d53514c0467bec6ba2507027d35fc81818358e/rpds_py-0.26.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b818a592bd69bfe437ee8368603d4a2d928c34cffcdf77c2e761a759ffd17d20", size = 399993 },
+ { url = "https://files.pythonhosted.org/packages/17/03/8021810b0e97923abdbab6474c8b77c69bcb4b2c58330777df9ff69dc559/rpds_py-0.26.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a8b0dd8648709b62d9372fc00a57466f5fdeefed666afe3fea5a6c9539a0331", size = 516683 },
+ { url = "https://files.pythonhosted.org/packages/dc/b1/da8e61c87c2f3d836954239fdbbfb477bb7b54d74974d8f6fcb34342d166/rpds_py-0.26.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6d3498ad0df07d81112aa6ec6c95a7e7b1ae00929fb73e7ebee0f3faaeabad2f", size = 408825 },
+ { url = "https://files.pythonhosted.org/packages/38/bc/1fc173edaaa0e52c94b02a655db20697cb5fa954ad5a8e15a2c784c5cbdd/rpds_py-0.26.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24a4146ccb15be237fdef10f331c568e1b0e505f8c8c9ed5d67759dac58ac246", size = 387292 },
+ { url = "https://files.pythonhosted.org/packages/7c/eb/3a9bb4bd90867d21916f253caf4f0d0be7098671b6715ad1cead9fe7bab9/rpds_py-0.26.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a9a63785467b2d73635957d32a4f6e73d5e4df497a16a6392fa066b753e87387", size = 420435 },
+ { url = "https://files.pythonhosted.org/packages/cd/16/e066dcdb56f5632713445271a3f8d3d0b426d51ae9c0cca387799df58b02/rpds_py-0.26.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:de4ed93a8c91debfd5a047be327b7cc8b0cc6afe32a716bbbc4aedca9e2a83af", size = 562410 },
+ { url = "https://files.pythonhosted.org/packages/60/22/ddbdec7eb82a0dc2e455be44c97c71c232983e21349836ce9f272e8a3c29/rpds_py-0.26.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:caf51943715b12af827696ec395bfa68f090a4c1a1d2509eb4e2cb69abbbdb33", size = 590724 },
+ { url = "https://files.pythonhosted.org/packages/2c/b4/95744085e65b7187d83f2fcb0bef70716a1ea0a9e5d8f7f39a86e5d83424/rpds_py-0.26.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4a59e5bc386de021f56337f757301b337d7ab58baa40174fb150accd480bc953", size = 558285 },
+ { url = "https://files.pythonhosted.org/packages/37/37/6309a75e464d1da2559446f9c811aa4d16343cebe3dbb73701e63f760caa/rpds_py-0.26.0-cp314-cp314-win32.whl", hash = "sha256:92c8db839367ef16a662478f0a2fe13e15f2227da3c1430a782ad0f6ee009ec9", size = 223459 },
+ { url = "https://files.pythonhosted.org/packages/d9/6f/8e9c11214c46098b1d1391b7e02b70bb689ab963db3b19540cba17315291/rpds_py-0.26.0-cp314-cp314-win_amd64.whl", hash = "sha256:b0afb8cdd034150d4d9f53926226ed27ad15b7f465e93d7468caaf5eafae0d37", size = 236083 },
+ { url = "https://files.pythonhosted.org/packages/47/af/9c4638994dd623d51c39892edd9d08e8be8220a4b7e874fa02c2d6e91955/rpds_py-0.26.0-cp314-cp314-win_arm64.whl", hash = "sha256:ca3f059f4ba485d90c8dc75cb5ca897e15325e4e609812ce57f896607c1c0867", size = 223291 },
+ { url = "https://files.pythonhosted.org/packages/4d/db/669a241144460474aab03e254326b32c42def83eb23458a10d163cb9b5ce/rpds_py-0.26.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:5afea17ab3a126006dc2f293b14ffc7ef3c85336cf451564a0515ed7648033da", size = 361445 },
+ { url = "https://files.pythonhosted.org/packages/3b/2d/133f61cc5807c6c2fd086a46df0eb8f63a23f5df8306ff9f6d0fd168fecc/rpds_py-0.26.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:69f0c0a3df7fd3a7eec50a00396104bb9a843ea6d45fcc31c2d5243446ffd7a7", size = 347206 },
+ { url = "https://files.pythonhosted.org/packages/05/bf/0e8fb4c05f70273469eecf82f6ccf37248558526a45321644826555db31b/rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:801a71f70f9813e82d2513c9a96532551fce1e278ec0c64610992c49c04c2dad", size = 380330 },
+ { url = "https://files.pythonhosted.org/packages/d4/a8/060d24185d8b24d3923322f8d0ede16df4ade226a74e747b8c7c978e3dd3/rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:df52098cde6d5e02fa75c1f6244f07971773adb4a26625edd5c18fee906fa84d", size = 392254 },
+ { url = "https://files.pythonhosted.org/packages/b9/7b/7c2e8a9ee3e6bc0bae26bf29f5219955ca2fbb761dca996a83f5d2f773fe/rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9bc596b30f86dc6f0929499c9e574601679d0341a0108c25b9b358a042f51bca", size = 516094 },
+ { url = "https://files.pythonhosted.org/packages/75/d6/f61cafbed8ba1499b9af9f1777a2a199cd888f74a96133d8833ce5eaa9c5/rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9dfbe56b299cf5875b68eb6f0ebaadc9cac520a1989cac0db0765abfb3709c19", size = 402889 },
+ { url = "https://files.pythonhosted.org/packages/92/19/c8ac0a8a8df2dd30cdec27f69298a5c13e9029500d6d76718130f5e5be10/rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac64f4b2bdb4ea622175c9ab7cf09444e412e22c0e02e906978b3b488af5fde8", size = 384301 },
+ { url = "https://files.pythonhosted.org/packages/41/e1/6b1859898bc292a9ce5776016c7312b672da00e25cec74d7beced1027286/rpds_py-0.26.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:181ef9b6bbf9845a264f9aa45c31836e9f3c1f13be565d0d010e964c661d1e2b", size = 412891 },
+ { url = "https://files.pythonhosted.org/packages/ef/b9/ceb39af29913c07966a61367b3c08b4f71fad841e32c6b59a129d5974698/rpds_py-0.26.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:49028aa684c144ea502a8e847d23aed5e4c2ef7cadfa7d5eaafcb40864844b7a", size = 557044 },
+ { url = "https://files.pythonhosted.org/packages/2f/27/35637b98380731a521f8ec4f3fd94e477964f04f6b2f8f7af8a2d889a4af/rpds_py-0.26.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e5d524d68a474a9688336045bbf76cb0def88549c1b2ad9dbfec1fb7cfbe9170", size = 585774 },
+ { url = "https://files.pythonhosted.org/packages/52/d9/3f0f105420fecd18551b678c9a6ce60bd23986098b252a56d35781b3e7e9/rpds_py-0.26.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c1851f429b822831bd2edcbe0cfd12ee9ea77868f8d3daf267b189371671c80e", size = 554886 },
+ { url = "https://files.pythonhosted.org/packages/6b/c5/347c056a90dc8dd9bc240a08c527315008e1b5042e7a4cf4ac027be9d38a/rpds_py-0.26.0-cp314-cp314t-win32.whl", hash = "sha256:7bdb17009696214c3b66bb3590c6d62e14ac5935e53e929bcdbc5a495987a84f", size = 219027 },
+ { url = "https://files.pythonhosted.org/packages/75/04/5302cea1aa26d886d34cadbf2dc77d90d7737e576c0065f357b96dc7a1a6/rpds_py-0.26.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f14440b9573a6f76b4ee4770c13f0b5921f71dde3b6fcb8dabbefd13b7fe05d7", size = 232821 },
+ { url = "https://files.pythonhosted.org/packages/ef/9a/1f033b0b31253d03d785b0cd905bc127e555ab496ea6b4c7c2e1f951f2fd/rpds_py-0.26.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3c0909c5234543ada2515c05dc08595b08d621ba919629e94427e8e03539c958", size = 373226 },
+ { url = "https://files.pythonhosted.org/packages/58/29/5f88023fd6aaaa8ca3c4a6357ebb23f6f07da6079093ccf27c99efce87db/rpds_py-0.26.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:c1fb0cda2abcc0ac62f64e2ea4b4e64c57dfd6b885e693095460c61bde7bb18e", size = 359230 },
+ { url = "https://files.pythonhosted.org/packages/6c/6c/13eaebd28b439da6964dde22712b52e53fe2824af0223b8e403249d10405/rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84d142d2d6cf9b31c12aa4878d82ed3b2324226270b89b676ac62ccd7df52d08", size = 382363 },
+ { url = "https://files.pythonhosted.org/packages/55/fc/3bb9c486b06da19448646f96147796de23c5811ef77cbfc26f17307b6a9d/rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a547e21c5610b7e9093d870be50682a6a6cf180d6da0f42c47c306073bfdbbf6", size = 397146 },
+ { url = "https://files.pythonhosted.org/packages/15/18/9d1b79eb4d18e64ba8bba9e7dec6f9d6920b639f22f07ee9368ca35d4673/rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:35e9a70a0f335371275cdcd08bc5b8051ac494dd58bff3bbfb421038220dc871", size = 514804 },
+ { url = "https://files.pythonhosted.org/packages/4f/5a/175ad7191bdbcd28785204621b225ad70e85cdfd1e09cc414cb554633b21/rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0dfa6115c6def37905344d56fb54c03afc49104e2ca473d5dedec0f6606913b4", size = 402820 },
+ { url = "https://files.pythonhosted.org/packages/11/45/6a67ecf6d61c4d4aff4bc056e864eec4b2447787e11d1c2c9a0242c6e92a/rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:313cfcd6af1a55a286a3c9a25f64af6d0e46cf60bc5798f1db152d97a216ff6f", size = 384567 },
+ { url = "https://files.pythonhosted.org/packages/a1/ba/16589da828732b46454c61858950a78fe4c931ea4bf95f17432ffe64b241/rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f7bf2496fa563c046d05e4d232d7b7fd61346e2402052064b773e5c378bf6f73", size = 416520 },
+ { url = "https://files.pythonhosted.org/packages/81/4b/00092999fc7c0c266045e984d56b7314734cc400a6c6dc4d61a35f135a9d/rpds_py-0.26.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:aa81873e2c8c5aa616ab8e017a481a96742fdf9313c40f14338ca7dbf50cb55f", size = 559362 },
+ { url = "https://files.pythonhosted.org/packages/96/0c/43737053cde1f93ac4945157f7be1428724ab943e2132a0d235a7e161d4e/rpds_py-0.26.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:68ffcf982715f5b5b7686bdd349ff75d422e8f22551000c24b30eaa1b7f7ae84", size = 588113 },
+ { url = "https://files.pythonhosted.org/packages/46/46/8e38f6161466e60a997ed7e9951ae5de131dedc3cf778ad35994b4af823d/rpds_py-0.26.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:6188de70e190847bb6db3dc3981cbadff87d27d6fe9b4f0e18726d55795cee9b", size = 555429 },
+ { url = "https://files.pythonhosted.org/packages/2c/ac/65da605e9f1dd643ebe615d5bbd11b6efa1d69644fc4bf623ea5ae385a82/rpds_py-0.26.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:1c962145c7473723df9722ba4c058de12eb5ebedcb4e27e7d902920aa3831ee8", size = 231950 },
+ { url = "https://files.pythonhosted.org/packages/51/f2/b5c85b758a00c513bb0389f8fc8e61eb5423050c91c958cdd21843faa3e6/rpds_py-0.26.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f61a9326f80ca59214d1cceb0a09bb2ece5b2563d4e0cd37bfd5515c28510674", size = 373505 },
+ { url = "https://files.pythonhosted.org/packages/23/e0/25db45e391251118e915e541995bb5f5ac5691a3b98fb233020ba53afc9b/rpds_py-0.26.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:183f857a53bcf4b1b42ef0f57ca553ab56bdd170e49d8091e96c51c3d69ca696", size = 359468 },
+ { url = "https://files.pythonhosted.org/packages/0b/73/dd5ee6075bb6491be3a646b301dfd814f9486d924137a5098e61f0487e16/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:941c1cfdf4799d623cf3aa1d326a6b4fdb7a5799ee2687f3516738216d2262fb", size = 382680 },
+ { url = "https://files.pythonhosted.org/packages/2f/10/84b522ff58763a5c443f5bcedc1820240e454ce4e620e88520f04589e2ea/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72a8d9564a717ee291f554eeb4bfeafe2309d5ec0aa6c475170bdab0f9ee8e88", size = 397035 },
+ { url = "https://files.pythonhosted.org/packages/06/ea/8667604229a10a520fcbf78b30ccc278977dcc0627beb7ea2c96b3becef0/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:511d15193cbe013619dd05414c35a7dedf2088fcee93c6bbb7c77859765bd4e8", size = 514922 },
+ { url = "https://files.pythonhosted.org/packages/24/e6/9ed5b625c0661c4882fc8cdf302bf8e96c73c40de99c31e0b95ed37d508c/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aea1f9741b603a8d8fedb0ed5502c2bc0accbc51f43e2ad1337fe7259c2b77a5", size = 402822 },
+ { url = "https://files.pythonhosted.org/packages/8a/58/212c7b6fd51946047fb45d3733da27e2fa8f7384a13457c874186af691b1/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4019a9d473c708cf2f16415688ef0b4639e07abaa569d72f74745bbeffafa2c7", size = 384336 },
+ { url = "https://files.pythonhosted.org/packages/aa/f5/a40ba78748ae8ebf4934d4b88e77b98497378bc2c24ba55ebe87a4e87057/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:093d63b4b0f52d98ebae33b8c50900d3d67e0666094b1be7a12fffd7f65de74b", size = 416871 },
+ { url = "https://files.pythonhosted.org/packages/d5/a6/33b1fc0c9f7dcfcfc4a4353daa6308b3ece22496ceece348b3e7a7559a09/rpds_py-0.26.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2abe21d8ba64cded53a2a677e149ceb76dcf44284202d737178afe7ba540c1eb", size = 559439 },
+ { url = "https://files.pythonhosted.org/packages/71/2d/ceb3f9c12f8cfa56d34995097f6cd99da1325642c60d1b6680dd9df03ed8/rpds_py-0.26.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:4feb7511c29f8442cbbc28149a92093d32e815a28aa2c50d333826ad2a20fdf0", size = 588380 },
+ { url = "https://files.pythonhosted.org/packages/c8/ed/9de62c2150ca8e2e5858acf3f4f4d0d180a38feef9fdab4078bea63d8dba/rpds_py-0.26.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:e99685fc95d386da368013e7fb4269dd39c30d99f812a8372d62f244f662709c", size = 555334 },
+]
+
+[[package]]
+name = "shellingham"
+version = "1.5.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 },
+]
+
+[[package]]
+name = "six"
+version = "1.17.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 },
+]
+
+[[package]]
+name = "sniffio"
+version = "1.3.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 },
+]
+
+[[package]]
+name = "sse-starlette"
+version = "2.2.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "starlette" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/71/a4/80d2a11af59fe75b48230846989e93979c892d3a20016b42bb44edb9e398/sse_starlette-2.2.1.tar.gz", hash = "sha256:54470d5f19274aeed6b2d473430b08b4b379ea851d953b11d7f1c4a2c118b419", size = 17376 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d9/e0/5b8bd393f27f4a62461c5cf2479c75a2cc2ffa330976f9f00f5f6e4f50eb/sse_starlette-2.2.1-py3-none-any.whl", hash = "sha256:6410a3d3ba0c89e7675d4c273a301d64649c03a5ef1ca101f10b47f895fd0e99", size = 10120 },
+]
+
+[[package]]
+name = "starlette"
+version = "0.45.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ff/fb/2984a686808b89a6781526129a4b51266f678b2d2b97ab2d325e56116df8/starlette-0.45.3.tar.gz", hash = "sha256:2cbcba2a75806f8a41c722141486f37c28e30a0921c5f6fe4346cb0dcee1302f", size = 2574076 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d9/61/f2b52e107b1fc8944b33ef56bf6ac4ebbe16d91b94d2b87ce013bf63fb84/starlette-0.45.3-py3-none-any.whl", hash = "sha256:dfb6d332576f136ec740296c7e8bb8c8a7125044e7c6da30744718880cdd059d", size = 71507 },
+]
+
+[[package]]
+name = "typer"
+version = "0.16.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+ { name = "rich" },
+ { name = "shellingham" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c5/8c/7d682431efca5fd290017663ea4588bf6f2c6aad085c7f108c5dbc316e70/typer-0.16.0.tar.gz", hash = "sha256:af377ffaee1dbe37ae9440cb4e8f11686ea5ce4e9bae01b84ae7c63b87f1dd3b", size = 102625 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/76/42/3efaf858001d2c2913de7f354563e3a3a2f0decae3efe98427125a8f441e/typer-0.16.0-py3-none-any.whl", hash = "sha256:1f79bed11d4d02d4310e3c1b7ba594183bcedb0ac73b27a9e5f28f6fb5b98855", size = 46317 },
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.12.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 },
+]
+
+[[package]]
+name = "typing-inspection"
+version = "0.4.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552 },
+]
+
+[[package]]
+name = "urllib3"
+version = "2.5.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795 },
+]
+
+[[package]]
+name = "uvicorn"
+version = "0.34.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+ { name = "h11" },
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/4b/4d/938bd85e5bf2edeec766267a5015ad969730bb91e31b44021dfe8b22df6c/uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9", size = 76568 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315 },
+]
+
+[[package]]
+name = "werkzeug"
+version = "3.1.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markupsafe" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/32/af/d4502dc713b4ccea7175d764718d5183caf8d0867a4f0190d5d4a45cea49/werkzeug-3.1.1.tar.gz", hash = "sha256:8cd39dfbdfc1e051965f156163e2974e52c210f130810e9ad36858f0fd3edad4", size = 806453 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ee/ea/c67e1dee1ba208ed22c06d1d547ae5e293374bfc43e0eb0ef5e262b68561/werkzeug-3.1.1-py3-none-any.whl", hash = "sha256:a71124d1ef06008baafa3d266c02f56e1836a5984afd6dd6c9230669d60d9fb5", size = 224371 },
+]
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/Dockerfile b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/Dockerfile
new file mode 100644
index 00000000..418b1400
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/Dockerfile
@@ -0,0 +1,25 @@
+FROM node:22.12-alpine AS builder
+
+WORKDIR /app
+
+COPY src/filesystem /app
+COPY tsconfig.json /tsconfig.json
+
+RUN --mount=type=cache,target=/root/.npm npm install
+
+RUN --mount=type=cache,target=/root/.npm-production npm ci --ignore-scripts --omit-dev
+
+
+FROM node:22-alpine AS release
+
+WORKDIR /app
+
+COPY --from=builder /app/dist /app/dist
+COPY --from=builder /app/package.json /app/package.json
+COPY --from=builder /app/package-lock.json /app/package-lock.json
+
+ENV NODE_ENV=production
+
+RUN npm ci --ignore-scripts --omit-dev
+
+ENTRYPOINT ["node", "/app/dist/index.js"]
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/README.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/README.md
new file mode 100644
index 00000000..bf087a2b
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/README.md
@@ -0,0 +1,321 @@
+# Filesystem MCP Server
+
+Node.js server implementing Model Context Protocol (MCP) for filesystem operations.
+
+## Features
+
+- Read/write files
+- Create/list/delete directories
+- Move files/directories
+- Search files
+- Get file metadata
+- Dynamic directory access control via [Roots](https://modelcontextprotocol.io/docs/learn/client-concepts#roots)
+
+## Directory Access Control
+
+The server uses a flexible directory access control system. Directories can be specified via command-line arguments or dynamically via [Roots](https://modelcontextprotocol.io/docs/learn/client-concepts#roots).
+
+### Method 1: Command-line Arguments
+Specify Allowed directories when starting the server:
+```bash
+mcp-server-filesystem /path/to/dir1 /path/to/dir2
+```
+
+### Method 2: MCP Roots (Recommended)
+MCP clients that support [Roots](https://modelcontextprotocol.io/docs/learn/client-concepts#roots) can dynamically update the Allowed directories.
+
+Roots notified by Client to Server, completely replace any server-side Allowed directories when provided.
+
+**Important**: If server starts without command-line arguments AND client doesn't support roots protocol (or provides empty roots), the server will throw an error during initialization.
+
+This is the recommended method, as this enables runtime directory updates via `roots/list_changed` notifications without server restart, providing a more flexible and modern integration experience.
+
+### How It Works
+
+The server's directory access control follows this flow:
+
+1. **Server Startup**
+ - Server starts with directories from command-line arguments (if provided)
+ - If no arguments provided, server starts with empty allowed directories
+
+2. **Client Connection & Initialization**
+ - Client connects and sends `initialize` request with capabilities
+ - Server checks if client supports roots protocol (`capabilities.roots`)
+
+3. **Roots Protocol Handling** (if client supports roots)
+ - **On initialization**: Server requests roots from client via `roots/list`
+ - Client responds with its configured roots
+ - Server replaces ALL allowed directories with client's roots
+ - **On runtime updates**: Client can send `notifications/roots/list_changed`
+ - Server requests updated roots and replaces allowed directories again
+
+4. **Fallback Behavior** (if client doesn't support roots)
+ - Server continues using command-line directories only
+ - No dynamic updates possible
+
+5. **Access Control**
+ - All filesystem operations are restricted to allowed directories
+ - Use `list_allowed_directories` tool to see current directories
+ - Server requires at least ONE allowed directory to operate
+
+**Note**: The server will only allow operations within directories specified either via `args` or via Roots.
+
+
+
+## API
+
+### Tools
+
+- **read_text_file**
+ - Read complete contents of a file as text
+ - Inputs:
+ - `path` (string)
+ - `head` (number, optional): First N lines
+ - `tail` (number, optional): Last N lines
+ - Always treats the file as UTF-8 text regardless of extension
+ - Cannot specify both `head` and `tail` simultaneously
+
+- **read_media_file**
+ - Read an image or audio file
+ - Inputs:
+ - `path` (string)
+ - Streams the file and returns base64 data with the corresponding MIME type
+
+- **read_multiple_files**
+ - Read multiple files simultaneously
+ - Input: `paths` (string[])
+ - Failed reads won't stop the entire operation
+
+- **write_file**
+ - Create new file or overwrite existing (exercise caution with this)
+ - Inputs:
+ - `path` (string): File location
+ - `content` (string): File content
+
+- **edit_file**
+ - Make selective edits using advanced pattern matching and formatting
+ - Features:
+ - Line-based and multi-line content matching
+ - Whitespace normalization with indentation preservation
+ - Multiple simultaneous edits with correct positioning
+ - Indentation style detection and preservation
+ - Git-style diff output with context
+ - Preview changes with dry run mode
+ - Inputs:
+ - `path` (string): File to edit
+ - `edits` (array): List of edit operations
+ - `oldText` (string): Text to search for (can be substring)
+ - `newText` (string): Text to replace with
+ - `dryRun` (boolean): Preview changes without applying (default: false)
+ - Returns detailed diff and match information for dry runs, otherwise applies changes
+ - Best Practice: Always use dryRun first to preview changes before applying them
+
+- **create_directory**
+ - Create new directory or ensure it exists
+ - Input: `path` (string)
+ - Creates parent directories if needed
+ - Succeeds silently if directory exists
+
+- **list_directory**
+ - List directory contents with [FILE] or [DIR] prefixes
+ - Input: `path` (string)
+
+- **list_directory_with_sizes**
+ - List directory contents with [FILE] or [DIR] prefixes, including file sizes
+ - Inputs:
+ - `path` (string): Directory path to list
+ - `sortBy` (string, optional): Sort entries by "name" or "size" (default: "name")
+ - Returns detailed listing with file sizes and summary statistics
+ - Shows total files, directories, and combined size
+
+- **move_file**
+ - Move or rename files and directories
+ - Inputs:
+ - `source` (string)
+ - `destination` (string)
+ - Fails if destination exists
+
+- **search_files**
+ - Recursively search for files/directories that match or do not match patterns
+ - Inputs:
+ - `path` (string): Starting directory
+ - `pattern` (string): Search pattern
+ - `excludePatterns` (string[]): Exclude any patterns.
+ - Glob-style pattern matching
+ - Returns full paths to matches
+
+- **directory_tree**
+ - Get recursive JSON tree structure of directory contents
+ - Inputs:
+ - `path` (string): Starting directory
+ - `excludePatterns` (string[]): Exclude any patterns. Glob formats are supported.
+ - Returns:
+ - JSON array where each entry contains:
+ - `name` (string): File/directory name
+ - `type` ('file'|'directory'): Entry type
+ - `children` (array): Present only for directories
+ - Empty array for empty directories
+ - Omitted for files
+ - Output is formatted with 2-space indentation for readability
+
+- **get_file_info**
+ - Get detailed file/directory metadata
+ - Input: `path` (string)
+ - Returns:
+ - Size
+ - Creation time
+ - Modified time
+ - Access time
+ - Type (file/directory)
+ - Permissions
+
+- **list_allowed_directories**
+ - List all directories the server is allowed to access
+ - No input required
+ - Returns:
+ - Directories that this server can read/write from
+
+### Tool annotations (MCP hints)
+
+This server sets [MCP ToolAnnotations](https://modelcontextprotocol.io/specification/2025-03-26/server/tools#toolannotations)
+on each tool so clients can:
+
+- Distinguish **read‑only** tools from write‑capable tools.
+- Understand which write operations are **idempotent** (safe to retry with the same arguments).
+- Highlight operations that may be **destructive** (overwriting or heavily mutating data).
+
+The mapping for filesystem tools is:
+
+| Tool | readOnlyHint | idempotentHint | destructiveHint | Notes |
+|-----------------------------|--------------|----------------|-----------------|--------------------------------------------------|
+| `read_text_file` | `true` | – | – | Pure read |
+| `read_media_file` | `true` | – | – | Pure read |
+| `read_multiple_files` | `true` | – | – | Pure read |
+| `list_directory` | `true` | – | – | Pure read |
+| `list_directory_with_sizes` | `true` | – | – | Pure read |
+| `directory_tree` | `true` | – | – | Pure read |
+| `search_files` | `true` | – | – | Pure read |
+| `get_file_info` | `true` | – | – | Pure read |
+| `list_allowed_directories` | `true` | – | – | Pure read |
+| `create_directory` | `false` | `true` | `false` | Re‑creating the same dir is a no‑op |
+| `write_file` | `false` | `true` | `true` | Overwrites existing files |
+| `edit_file` | `false` | `false` | `true` | Re‑applying edits can fail or double‑apply |
+| `move_file` | `false` | `false` | `true` | Deletes source file |
+
+> Note: `idempotentHint` and `destructiveHint` are meaningful only when `readOnlyHint` is `false`, as defined by the MCP spec.
+
+## Usage with Claude Desktop
+Add this to your `claude_desktop_config.json`:
+
+Note: you can provide sandboxed directories to the server by mounting them to `/projects`. Adding the `ro` flag will make the directory readonly by the server.
+
+### Docker
+Note: all directories must be mounted to `/projects` by default.
+
+```json
+{
+ "mcpServers": {
+ "filesystem": {
+ "command": "docker",
+ "args": [
+ "run",
+ "-i",
+ "--rm",
+ "--mount", "type=bind,src=/Users/username/Desktop,dst=/projects/Desktop",
+ "--mount", "type=bind,src=/path/to/other/allowed/dir,dst=/projects/other/allowed/dir,ro",
+ "--mount", "type=bind,src=/path/to/file.txt,dst=/projects/path/to/file.txt",
+ "mcp/filesystem",
+ "/projects"
+ ]
+ }
+ }
+}
+```
+
+### NPX
+
+```json
+{
+ "mcpServers": {
+ "filesystem": {
+ "command": "npx",
+ "args": [
+ "-y",
+ "@modelcontextprotocol/server-filesystem",
+ "/Users/username/Desktop",
+ "/path/to/other/allowed/dir"
+ ]
+ }
+ }
+}
+```
+
+## Usage with VS Code
+
+For quick installation, click the installation buttons below...
+
+[](https://insiders.vscode.dev/redirect/mcp/install?name=filesystem&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40modelcontextprotocol%2Fserver-filesystem%22%2C%22%24%7BworkspaceFolder%7D%22%5D%7D) [](https://insiders.vscode.dev/redirect/mcp/install?name=filesystem&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40modelcontextprotocol%2Fserver-filesystem%22%2C%22%24%7BworkspaceFolder%7D%22%5D%7D&quality=insiders)
+
+[](https://insiders.vscode.dev/redirect/mcp/install?name=filesystem&config=%7B%22command%22%3A%22docker%22%2C%22args%22%3A%5B%22run%22%2C%22-i%22%2C%22--rm%22%2C%22--mount%22%2C%22type%3Dbind%2Csrc%3D%24%7BworkspaceFolder%7D%2Cdst%3D%2Fprojects%2Fworkspace%22%2C%22mcp%2Ffilesystem%22%2C%22%2Fprojects%22%5D%7D) [](https://insiders.vscode.dev/redirect/mcp/install?name=filesystem&config=%7B%22command%22%3A%22docker%22%2C%22args%22%3A%5B%22run%22%2C%22-i%22%2C%22--rm%22%2C%22--mount%22%2C%22type%3Dbind%2Csrc%3D%24%7BworkspaceFolder%7D%2Cdst%3D%2Fprojects%2Fworkspace%22%2C%22mcp%2Ffilesystem%22%2C%22%2Fprojects%22%5D%7D&quality=insiders)
+
+For manual installation, you can configure the MCP server using one of these methods:
+
+**Method 1: User Configuration (Recommended)**
+Add the configuration to your user-level MCP configuration file. Open the Command Palette (`Ctrl + Shift + P`) and run `MCP: Open User Configuration`. This will open your user `mcp.json` file where you can add the server configuration.
+
+**Method 2: Workspace Configuration**
+Alternatively, you can add the configuration to a file called `.vscode/mcp.json` in your workspace. This will allow you to share the configuration with others.
+
+> For more details about MCP configuration in VS Code, see the [official VS Code MCP documentation](https://code.visualstudio.com/docs/copilot/customization/mcp-servers).
+
+You can provide sandboxed directories to the server by mounting them to `/projects`. Adding the `ro` flag will make the directory readonly by the server.
+
+### Docker
+Note: all directories must be mounted to `/projects` by default.
+
+```json
+{
+ "servers": {
+ "filesystem": {
+ "command": "docker",
+ "args": [
+ "run",
+ "-i",
+ "--rm",
+ "--mount", "type=bind,src=${workspaceFolder},dst=/projects/workspace",
+ "mcp/filesystem",
+ "/projects"
+ ]
+ }
+ }
+}
+```
+
+### NPX
+
+```json
+{
+ "servers": {
+ "filesystem": {
+ "command": "npx",
+ "args": [
+ "-y",
+ "@modelcontextprotocol/server-filesystem",
+ "${workspaceFolder}"
+ ]
+ }
+ }
+}
+```
+
+## Build
+
+Docker build:
+
+```bash
+docker build -t mcp/filesystem -f src/filesystem/Dockerfile .
+```
+
+## License
+
+This MCP server is licensed under the MIT License. This means you are free to use, modify, and distribute the software, subject to the terms and conditions of the MIT License. For more details, please see the LICENSE file in the project repository.
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/__tests__/directory-tree.test.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/__tests__/directory-tree.test.ts
new file mode 100644
index 00000000..04c8278c
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/__tests__/directory-tree.test.ts
@@ -0,0 +1,147 @@
+import { describe, it, expect, beforeEach, afterEach } from 'vitest';
+import * as fs from 'fs/promises';
+import * as path from 'path';
+import * as os from 'os';
+
+// We need to test the buildTree function, but it's defined inside the request handler
+// So we'll extract the core logic into a testable function
+import { minimatch } from 'minimatch';
+
+interface TreeEntry {
+ name: string;
+ type: 'file' | 'directory';
+ children?: TreeEntry[];
+}
+
+async function buildTreeForTesting(currentPath: string, rootPath: string, excludePatterns: string[] = []): Promise {
+ const entries = await fs.readdir(currentPath, {withFileTypes: true});
+ const result: TreeEntry[] = [];
+
+ for (const entry of entries) {
+ const relativePath = path.relative(rootPath, path.join(currentPath, entry.name));
+ const shouldExclude = excludePatterns.some(pattern => {
+ if (pattern.includes('*')) {
+ return minimatch(relativePath, pattern, {dot: true});
+ }
+ // For files: match exact name or as part of path
+ // For directories: match as directory path
+ return minimatch(relativePath, pattern, {dot: true}) ||
+ minimatch(relativePath, `**/${pattern}`, {dot: true}) ||
+ minimatch(relativePath, `**/${pattern}/**`, {dot: true});
+ });
+ if (shouldExclude)
+ continue;
+
+ const entryData: TreeEntry = {
+ name: entry.name,
+ type: entry.isDirectory() ? 'directory' : 'file'
+ };
+
+ if (entry.isDirectory()) {
+ const subPath = path.join(currentPath, entry.name);
+ entryData.children = await buildTreeForTesting(subPath, rootPath, excludePatterns);
+ }
+
+ result.push(entryData);
+ }
+
+ return result;
+}
+
+describe('buildTree exclude patterns', () => {
+ let testDir: string;
+
+ beforeEach(async () => {
+ testDir = await fs.mkdtemp(path.join(os.tmpdir(), 'filesystem-test-'));
+
+ // Create test directory structure
+ await fs.mkdir(path.join(testDir, 'src'));
+ await fs.mkdir(path.join(testDir, 'node_modules'));
+ await fs.mkdir(path.join(testDir, '.git'));
+ await fs.mkdir(path.join(testDir, 'nested', 'node_modules'), { recursive: true });
+
+ // Create test files
+ await fs.writeFile(path.join(testDir, '.env'), 'SECRET=value');
+ await fs.writeFile(path.join(testDir, '.env.local'), 'LOCAL_SECRET=value');
+ await fs.writeFile(path.join(testDir, 'src', 'index.js'), 'console.log("hello");');
+ await fs.writeFile(path.join(testDir, 'package.json'), '{}');
+ await fs.writeFile(path.join(testDir, 'node_modules', 'module.js'), 'module.exports = {};');
+ await fs.writeFile(path.join(testDir, 'nested', 'node_modules', 'deep.js'), 'module.exports = {};');
+ });
+
+ afterEach(async () => {
+ await fs.rm(testDir, { recursive: true, force: true });
+ });
+
+ it('should exclude files matching simple patterns', async () => {
+ // Test the current implementation - this will fail until the bug is fixed
+ const tree = await buildTreeForTesting(testDir, testDir, ['.env']);
+ const fileNames = tree.map(entry => entry.name);
+
+ expect(fileNames).not.toContain('.env');
+ expect(fileNames).toContain('.env.local'); // Should not exclude this
+ expect(fileNames).toContain('src');
+ expect(fileNames).toContain('package.json');
+ });
+
+ it('should exclude directories matching simple patterns', async () => {
+ const tree = await buildTreeForTesting(testDir, testDir, ['node_modules']);
+ const dirNames = tree.map(entry => entry.name);
+
+ expect(dirNames).not.toContain('node_modules');
+ expect(dirNames).toContain('src');
+ expect(dirNames).toContain('.git');
+ });
+
+ it('should exclude nested directories with same pattern', async () => {
+ const tree = await buildTreeForTesting(testDir, testDir, ['node_modules']);
+
+ // Find the nested directory
+ const nestedDir = tree.find(entry => entry.name === 'nested');
+ expect(nestedDir).toBeDefined();
+ expect(nestedDir!.children).toBeDefined();
+
+ // The nested/node_modules should also be excluded
+ const nestedChildren = nestedDir!.children!.map(child => child.name);
+ expect(nestedChildren).not.toContain('node_modules');
+ });
+
+ it('should handle glob patterns correctly', async () => {
+ const tree = await buildTreeForTesting(testDir, testDir, ['*.env']);
+ const fileNames = tree.map(entry => entry.name);
+
+ expect(fileNames).not.toContain('.env');
+ expect(fileNames).toContain('.env.local'); // *.env should not match .env.local
+ expect(fileNames).toContain('src');
+ });
+
+ it('should handle dot files correctly', async () => {
+ const tree = await buildTreeForTesting(testDir, testDir, ['.git']);
+ const dirNames = tree.map(entry => entry.name);
+
+ expect(dirNames).not.toContain('.git');
+ expect(dirNames).toContain('.env'); // Should not exclude this
+ });
+
+ it('should work with multiple exclude patterns', async () => {
+ const tree = await buildTreeForTesting(testDir, testDir, ['node_modules', '.env', '.git']);
+ const entryNames = tree.map(entry => entry.name);
+
+ expect(entryNames).not.toContain('node_modules');
+ expect(entryNames).not.toContain('.env');
+ expect(entryNames).not.toContain('.git');
+ expect(entryNames).toContain('src');
+ expect(entryNames).toContain('package.json');
+ });
+
+ it('should handle empty exclude patterns', async () => {
+ const tree = await buildTreeForTesting(testDir, testDir, []);
+ const entryNames = tree.map(entry => entry.name);
+
+ // All entries should be included
+ expect(entryNames).toContain('node_modules');
+ expect(entryNames).toContain('.env');
+ expect(entryNames).toContain('.git');
+ expect(entryNames).toContain('src');
+ });
+});
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/__tests__/lib.test.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/__tests__/lib.test.ts
new file mode 100644
index 00000000..f7e585af
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/__tests__/lib.test.ts
@@ -0,0 +1,725 @@
+import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
+import fs from 'fs/promises';
+import path from 'path';
+import os from 'os';
+import {
+ // Pure utility functions
+ formatSize,
+ normalizeLineEndings,
+ createUnifiedDiff,
+ // Security & validation functions
+ validatePath,
+ setAllowedDirectories,
+ // File operations
+ getFileStats,
+ readFileContent,
+ writeFileContent,
+ // Search & filtering functions
+ searchFilesWithValidation,
+ // File editing functions
+ applyFileEdits,
+ tailFile,
+ headFile
+} from '../lib.js';
+
+// Mock fs module
+vi.mock('fs/promises');
+const mockFs = fs as any;
+
+describe('Lib Functions', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ // Set up allowed directories for tests
+ const allowedDirs = process.platform === 'win32' ? ['C:\\Users\\test', 'C:\\temp', 'C:\\allowed'] : ['/home/user', '/tmp', '/allowed'];
+ setAllowedDirectories(allowedDirs);
+ });
+
+ afterEach(() => {
+ vi.restoreAllMocks();
+ // Clear allowed directories after tests
+ setAllowedDirectories([]);
+ });
+
+ describe('Pure Utility Functions', () => {
+ describe('formatSize', () => {
+ it('formats bytes correctly', () => {
+ expect(formatSize(0)).toBe('0 B');
+ expect(formatSize(512)).toBe('512 B');
+ expect(formatSize(1024)).toBe('1.00 KB');
+ expect(formatSize(1536)).toBe('1.50 KB');
+ expect(formatSize(1048576)).toBe('1.00 MB');
+ expect(formatSize(1073741824)).toBe('1.00 GB');
+ expect(formatSize(1099511627776)).toBe('1.00 TB');
+ });
+
+ it('handles edge cases', () => {
+ expect(formatSize(1023)).toBe('1023 B');
+ expect(formatSize(1025)).toBe('1.00 KB');
+ expect(formatSize(1048575)).toBe('1024.00 KB');
+ });
+
+ it('handles very large numbers beyond TB', () => {
+ // The function only supports up to TB, so very large numbers will show as TB
+ expect(formatSize(1024 * 1024 * 1024 * 1024 * 1024)).toBe('1024.00 TB');
+ expect(formatSize(Number.MAX_SAFE_INTEGER)).toContain('TB');
+ });
+
+ it('handles negative numbers', () => {
+ // Negative numbers will result in NaN for the log calculation
+ expect(formatSize(-1024)).toContain('NaN');
+ expect(formatSize(-0)).toBe('0 B');
+ });
+
+ it('handles decimal numbers', () => {
+ expect(formatSize(1536.5)).toBe('1.50 KB');
+ expect(formatSize(1023.9)).toBe('1023.9 B');
+ });
+
+ it('handles very small positive numbers', () => {
+ expect(formatSize(1)).toBe('1 B');
+ expect(formatSize(0.5)).toBe('0.5 B');
+ expect(formatSize(0.1)).toBe('0.1 B');
+ });
+ });
+
+ describe('normalizeLineEndings', () => {
+ it('converts CRLF to LF', () => {
+ expect(normalizeLineEndings('line1\r\nline2\r\nline3')).toBe('line1\nline2\nline3');
+ });
+
+ it('leaves LF unchanged', () => {
+ expect(normalizeLineEndings('line1\nline2\nline3')).toBe('line1\nline2\nline3');
+ });
+
+ it('handles mixed line endings', () => {
+ expect(normalizeLineEndings('line1\r\nline2\nline3\r\n')).toBe('line1\nline2\nline3\n');
+ });
+
+ it('handles empty string', () => {
+ expect(normalizeLineEndings('')).toBe('');
+ });
+ });
+
+ describe('createUnifiedDiff', () => {
+ it('creates diff for simple changes', () => {
+ const original = 'line1\nline2\nline3';
+ const modified = 'line1\nmodified line2\nline3';
+ const diff = createUnifiedDiff(original, modified, 'test.txt');
+
+ expect(diff).toContain('--- test.txt');
+ expect(diff).toContain('+++ test.txt');
+ expect(diff).toContain('-line2');
+ expect(diff).toContain('+modified line2');
+ });
+
+ it('handles CRLF normalization', () => {
+ const original = 'line1\r\nline2\r\n';
+ const modified = 'line1\nmodified line2\n';
+ const diff = createUnifiedDiff(original, modified);
+
+ expect(diff).toContain('-line2');
+ expect(diff).toContain('+modified line2');
+ });
+
+ it('handles identical content', () => {
+ const content = 'line1\nline2\nline3';
+ const diff = createUnifiedDiff(content, content);
+
+ // Should not contain any +/- lines for identical content (excluding header lines)
+ expect(diff.split('\n').filter((line: string) => line.startsWith('+++') || line.startsWith('---'))).toHaveLength(2);
+ expect(diff.split('\n').filter((line: string) => line.startsWith('+') && !line.startsWith('+++'))).toHaveLength(0);
+ expect(diff.split('\n').filter((line: string) => line.startsWith('-') && !line.startsWith('---'))).toHaveLength(0);
+ });
+
+ it('handles empty content', () => {
+ const diff = createUnifiedDiff('', '');
+ expect(diff).toContain('--- file');
+ expect(diff).toContain('+++ file');
+ });
+
+ it('handles default filename parameter', () => {
+ const diff = createUnifiedDiff('old', 'new');
+ expect(diff).toContain('--- file');
+ expect(diff).toContain('+++ file');
+ });
+
+ it('handles custom filename', () => {
+ const diff = createUnifiedDiff('old', 'new', 'custom.txt');
+ expect(diff).toContain('--- custom.txt');
+ expect(diff).toContain('+++ custom.txt');
+ });
+ });
+ });
+
+ describe('Security & Validation Functions', () => {
+ describe('validatePath', () => {
+ // Use Windows-compatible paths for testing
+ const allowedDirs = process.platform === 'win32' ? ['C:\\Users\\test', 'C:\\temp'] : ['/home/user', '/tmp'];
+
+ beforeEach(() => {
+ mockFs.realpath.mockImplementation(async (path: any) => path.toString());
+ });
+
+ it('validates allowed paths', async () => {
+ const testPath = process.platform === 'win32' ? 'C:\\Users\\test\\file.txt' : '/home/user/file.txt';
+ const result = await validatePath(testPath);
+ expect(result).toBe(testPath);
+ });
+
+ it('rejects disallowed paths', async () => {
+ const testPath = process.platform === 'win32' ? 'C:\\Windows\\System32\\file.txt' : '/etc/passwd';
+ await expect(validatePath(testPath))
+ .rejects.toThrow('Access denied - path outside allowed directories');
+ });
+
+ it('handles non-existent files by checking parent directory', async () => {
+ const newFilePath = process.platform === 'win32' ? 'C:\\Users\\test\\newfile.txt' : '/home/user/newfile.txt';
+ const parentPath = process.platform === 'win32' ? 'C:\\Users\\test' : '/home/user';
+
+ // Create an error with the ENOENT code that the implementation checks for
+ const enoentError = new Error('ENOENT') as NodeJS.ErrnoException;
+ enoentError.code = 'ENOENT';
+
+ mockFs.realpath
+ .mockRejectedValueOnce(enoentError)
+ .mockResolvedValueOnce(parentPath);
+
+ const result = await validatePath(newFilePath);
+ expect(result).toBe(path.resolve(newFilePath));
+ });
+
+ it('rejects when parent directory does not exist', async () => {
+ const newFilePath = process.platform === 'win32' ? 'C:\\Users\\test\\nonexistent\\newfile.txt' : '/home/user/nonexistent/newfile.txt';
+
+ // Create errors with the ENOENT code
+ const enoentError1 = new Error('ENOENT') as NodeJS.ErrnoException;
+ enoentError1.code = 'ENOENT';
+ const enoentError2 = new Error('ENOENT') as NodeJS.ErrnoException;
+ enoentError2.code = 'ENOENT';
+
+ mockFs.realpath
+ .mockRejectedValueOnce(enoentError1)
+ .mockRejectedValueOnce(enoentError2);
+
+ await expect(validatePath(newFilePath))
+ .rejects.toThrow('Parent directory does not exist');
+ });
+
+ it('resolves relative paths against allowed directories instead of process.cwd()', async () => {
+ const relativePath = 'test-file.txt';
+ const originalCwd = process.cwd;
+
+ // Mock process.cwd to return a directory outside allowed directories
+ const disallowedCwd = process.platform === 'win32' ? 'C:\\Windows\\System32' : '/root';
+ (process as any).cwd = vi.fn(() => disallowedCwd);
+
+ try {
+ const result = await validatePath(relativePath);
+
+ // Result should be resolved against first allowed directory, not process.cwd()
+ const expectedPath = process.platform === 'win32'
+ ? path.resolve('C:\\Users\\test', relativePath)
+ : path.resolve('/home/user', relativePath);
+
+ expect(result).toBe(expectedPath);
+ expect(result).not.toContain(disallowedCwd);
+ } finally {
+ // Restore original process.cwd
+ process.cwd = originalCwd;
+ }
+ });
+ });
+ });
+
+ describe('File Operations', () => {
+ describe('getFileStats', () => {
+ it('returns file statistics', async () => {
+ const mockStats = {
+ size: 1024,
+ birthtime: new Date('2023-01-01'),
+ mtime: new Date('2023-01-02'),
+ atime: new Date('2023-01-03'),
+ isDirectory: () => false,
+ isFile: () => true,
+ mode: 0o644
+ };
+
+ mockFs.stat.mockResolvedValueOnce(mockStats as any);
+
+ const result = await getFileStats('/test/file.txt');
+
+ expect(result).toEqual({
+ size: 1024,
+ created: new Date('2023-01-01'),
+ modified: new Date('2023-01-02'),
+ accessed: new Date('2023-01-03'),
+ isDirectory: false,
+ isFile: true,
+ permissions: '644'
+ });
+ });
+
+ it('handles directory statistics', async () => {
+ const mockStats = {
+ size: 4096,
+ birthtime: new Date('2023-01-01'),
+ mtime: new Date('2023-01-02'),
+ atime: new Date('2023-01-03'),
+ isDirectory: () => true,
+ isFile: () => false,
+ mode: 0o755
+ };
+
+ mockFs.stat.mockResolvedValueOnce(mockStats as any);
+
+ const result = await getFileStats('/test/dir');
+
+ expect(result.isDirectory).toBe(true);
+ expect(result.isFile).toBe(false);
+ expect(result.permissions).toBe('755');
+ });
+ });
+
+ describe('readFileContent', () => {
+ it('reads file with default encoding', async () => {
+ mockFs.readFile.mockResolvedValueOnce('file content');
+
+ const result = await readFileContent('/test/file.txt');
+
+ expect(result).toBe('file content');
+ expect(mockFs.readFile).toHaveBeenCalledWith('/test/file.txt', 'utf-8');
+ });
+
+ it('reads file with custom encoding', async () => {
+ mockFs.readFile.mockResolvedValueOnce('file content');
+
+ const result = await readFileContent('/test/file.txt', 'ascii');
+
+ expect(result).toBe('file content');
+ expect(mockFs.readFile).toHaveBeenCalledWith('/test/file.txt', 'ascii');
+ });
+ });
+
+ describe('writeFileContent', () => {
+ it('writes file content', async () => {
+ mockFs.writeFile.mockResolvedValueOnce(undefined);
+
+ await writeFileContent('/test/file.txt', 'new content');
+
+ expect(mockFs.writeFile).toHaveBeenCalledWith('/test/file.txt', 'new content', { encoding: "utf-8", flag: 'wx' });
+ });
+ });
+
+ });
+
+ describe('Search & Filtering Functions', () => {
+ describe('searchFilesWithValidation', () => {
+ beforeEach(() => {
+ mockFs.realpath.mockImplementation(async (path: any) => path.toString());
+ });
+
+
+ it('excludes files matching exclude patterns', async () => {
+ const mockEntries = [
+ { name: 'test.txt', isDirectory: () => false },
+ { name: 'test.log', isDirectory: () => false },
+ { name: 'node_modules', isDirectory: () => true }
+ ];
+
+ mockFs.readdir.mockResolvedValueOnce(mockEntries as any);
+
+ const testDir = process.platform === 'win32' ? 'C:\\allowed\\dir' : '/allowed/dir';
+ const allowedDirs = process.platform === 'win32' ? ['C:\\allowed'] : ['/allowed'];
+
+ // Mock realpath to return the same path for validation to pass
+ mockFs.realpath.mockImplementation(async (inputPath: any) => {
+ const pathStr = inputPath.toString();
+ // Return the path as-is for validation
+ return pathStr;
+ });
+
+ const result = await searchFilesWithValidation(
+ testDir,
+ '*test*',
+ allowedDirs,
+ { excludePatterns: ['*.log', 'node_modules'] }
+ );
+
+ const expectedResult = process.platform === 'win32' ? 'C:\\allowed\\dir\\test.txt' : '/allowed/dir/test.txt';
+ expect(result).toEqual([expectedResult]);
+ });
+
+ it('handles validation errors during search', async () => {
+ const mockEntries = [
+ { name: 'test.txt', isDirectory: () => false },
+ { name: 'invalid_file.txt', isDirectory: () => false }
+ ];
+
+ mockFs.readdir.mockResolvedValueOnce(mockEntries as any);
+
+ // Mock validatePath to throw error for invalid_file.txt
+ mockFs.realpath.mockImplementation(async (path: any) => {
+ if (path.toString().includes('invalid_file.txt')) {
+ throw new Error('Access denied');
+ }
+ return path.toString();
+ });
+
+ const testDir = process.platform === 'win32' ? 'C:\\allowed\\dir' : '/allowed/dir';
+ const allowedDirs = process.platform === 'win32' ? ['C:\\allowed'] : ['/allowed'];
+
+ const result = await searchFilesWithValidation(
+ testDir,
+ '*test*',
+ allowedDirs,
+ {}
+ );
+
+ // Should only return the valid file, skipping the invalid one
+ const expectedResult = process.platform === 'win32' ? 'C:\\allowed\\dir\\test.txt' : '/allowed/dir/test.txt';
+ expect(result).toEqual([expectedResult]);
+ });
+
+ it('handles complex exclude patterns with wildcards', async () => {
+ const mockEntries = [
+ { name: 'test.txt', isDirectory: () => false },
+ { name: 'test.backup', isDirectory: () => false },
+ { name: 'important_test.js', isDirectory: () => false }
+ ];
+
+ mockFs.readdir.mockResolvedValueOnce(mockEntries as any);
+
+ const testDir = process.platform === 'win32' ? 'C:\\allowed\\dir' : '/allowed/dir';
+ const allowedDirs = process.platform === 'win32' ? ['C:\\allowed'] : ['/allowed'];
+
+ const result = await searchFilesWithValidation(
+ testDir,
+ '*test*',
+ allowedDirs,
+ { excludePatterns: ['*.backup'] }
+ );
+
+ const expectedResults = process.platform === 'win32' ? [
+ 'C:\\allowed\\dir\\test.txt',
+ 'C:\\allowed\\dir\\important_test.js'
+ ] : [
+ '/allowed/dir/test.txt',
+ '/allowed/dir/important_test.js'
+ ];
+ expect(result).toEqual(expectedResults);
+ });
+ });
+ });
+
+ describe('File Editing Functions', () => {
+ describe('applyFileEdits', () => {
+ beforeEach(() => {
+ mockFs.readFile.mockResolvedValue('line1\nline2\nline3\n');
+ mockFs.writeFile.mockResolvedValue(undefined);
+ });
+
+ it('applies simple text replacement', async () => {
+ const edits = [
+ { oldText: 'line2', newText: 'modified line2' }
+ ];
+
+ mockFs.rename.mockResolvedValueOnce(undefined);
+
+ const result = await applyFileEdits('/test/file.txt', edits, false);
+
+ expect(result).toContain('modified line2');
+ // Should write to temporary file then rename
+ expect(mockFs.writeFile).toHaveBeenCalledWith(
+ expect.stringMatching(/\/test\/file\.txt\.[a-f0-9]+\.tmp$/),
+ 'line1\nmodified line2\nline3\n',
+ 'utf-8'
+ );
+ expect(mockFs.rename).toHaveBeenCalledWith(
+ expect.stringMatching(/\/test\/file\.txt\.[a-f0-9]+\.tmp$/),
+ '/test/file.txt'
+ );
+ });
+
+ it('handles dry run mode', async () => {
+ const edits = [
+ { oldText: 'line2', newText: 'modified line2' }
+ ];
+
+ const result = await applyFileEdits('/test/file.txt', edits, true);
+
+ expect(result).toContain('modified line2');
+ expect(mockFs.writeFile).not.toHaveBeenCalled();
+ });
+
+ it('applies multiple edits sequentially', async () => {
+ const edits = [
+ { oldText: 'line1', newText: 'first line' },
+ { oldText: 'line3', newText: 'third line' }
+ ];
+
+ mockFs.rename.mockResolvedValueOnce(undefined);
+
+ await applyFileEdits('/test/file.txt', edits, false);
+
+ expect(mockFs.writeFile).toHaveBeenCalledWith(
+ expect.stringMatching(/\/test\/file\.txt\.[a-f0-9]+\.tmp$/),
+ 'first line\nline2\nthird line\n',
+ 'utf-8'
+ );
+ expect(mockFs.rename).toHaveBeenCalledWith(
+ expect.stringMatching(/\/test\/file\.txt\.[a-f0-9]+\.tmp$/),
+ '/test/file.txt'
+ );
+ });
+
+ it('handles whitespace-flexible matching', async () => {
+ mockFs.readFile.mockResolvedValue(' line1\n line2\n line3\n');
+
+ const edits = [
+ { oldText: 'line2', newText: 'modified line2' }
+ ];
+
+ mockFs.rename.mockResolvedValueOnce(undefined);
+
+ await applyFileEdits('/test/file.txt', edits, false);
+
+ expect(mockFs.writeFile).toHaveBeenCalledWith(
+ expect.stringMatching(/\/test\/file\.txt\.[a-f0-9]+\.tmp$/),
+ ' line1\n modified line2\n line3\n',
+ 'utf-8'
+ );
+ expect(mockFs.rename).toHaveBeenCalledWith(
+ expect.stringMatching(/\/test\/file\.txt\.[a-f0-9]+\.tmp$/),
+ '/test/file.txt'
+ );
+ });
+
+ it('throws error for non-matching edits', async () => {
+ const edits = [
+ { oldText: 'nonexistent line', newText: 'replacement' }
+ ];
+
+ await expect(applyFileEdits('/test/file.txt', edits, false))
+ .rejects.toThrow('Could not find exact match for edit');
+ });
+
+ it('handles complex multi-line edits with indentation', async () => {
+ mockFs.readFile.mockResolvedValue('function test() {\n console.log("hello");\n return true;\n}');
+
+ const edits = [
+ {
+ oldText: ' console.log("hello");\n return true;',
+ newText: ' console.log("world");\n console.log("test");\n return false;'
+ }
+ ];
+
+ mockFs.rename.mockResolvedValueOnce(undefined);
+
+ await applyFileEdits('/test/file.js', edits, false);
+
+ expect(mockFs.writeFile).toHaveBeenCalledWith(
+ expect.stringMatching(/\/test\/file\.js\.[a-f0-9]+\.tmp$/),
+ 'function test() {\n console.log("world");\n console.log("test");\n return false;\n}',
+ 'utf-8'
+ );
+ expect(mockFs.rename).toHaveBeenCalledWith(
+ expect.stringMatching(/\/test\/file\.js\.[a-f0-9]+\.tmp$/),
+ '/test/file.js'
+ );
+ });
+
+ it('handles edits with different indentation patterns', async () => {
+ mockFs.readFile.mockResolvedValue(' if (condition) {\n doSomething();\n }');
+
+ const edits = [
+ {
+ oldText: 'doSomething();',
+ newText: 'doSomethingElse();\n doAnotherThing();'
+ }
+ ];
+
+ mockFs.rename.mockResolvedValueOnce(undefined);
+
+ await applyFileEdits('/test/file.js', edits, false);
+
+ expect(mockFs.writeFile).toHaveBeenCalledWith(
+ expect.stringMatching(/\/test\/file\.js\.[a-f0-9]+\.tmp$/),
+ ' if (condition) {\n doSomethingElse();\n doAnotherThing();\n }',
+ 'utf-8'
+ );
+ expect(mockFs.rename).toHaveBeenCalledWith(
+ expect.stringMatching(/\/test\/file\.js\.[a-f0-9]+\.tmp$/),
+ '/test/file.js'
+ );
+ });
+
+ it('handles CRLF line endings in file content', async () => {
+ mockFs.readFile.mockResolvedValue('line1\r\nline2\r\nline3\r\n');
+
+ const edits = [
+ { oldText: 'line2', newText: 'modified line2' }
+ ];
+
+ mockFs.rename.mockResolvedValueOnce(undefined);
+
+ await applyFileEdits('/test/file.txt', edits, false);
+
+ expect(mockFs.writeFile).toHaveBeenCalledWith(
+ expect.stringMatching(/\/test\/file\.txt\.[a-f0-9]+\.tmp$/),
+ 'line1\nmodified line2\nline3\n',
+ 'utf-8'
+ );
+ expect(mockFs.rename).toHaveBeenCalledWith(
+ expect.stringMatching(/\/test\/file\.txt\.[a-f0-9]+\.tmp$/),
+ '/test/file.txt'
+ );
+ });
+ });
+
+ describe('tailFile', () => {
+ it('handles empty files', async () => {
+ mockFs.stat.mockResolvedValue({ size: 0 } as any);
+
+ const result = await tailFile('/test/empty.txt', 5);
+
+ expect(result).toBe('');
+ expect(mockFs.open).not.toHaveBeenCalled();
+ });
+
+ it('calls stat to check file size', async () => {
+ mockFs.stat.mockResolvedValue({ size: 100 } as any);
+
+ // Mock file handle with proper typing
+ const mockFileHandle = {
+ read: vi.fn(),
+ close: vi.fn()
+ } as any;
+
+ mockFileHandle.read.mockResolvedValue({ bytesRead: 0 });
+ mockFileHandle.close.mockResolvedValue(undefined);
+
+ mockFs.open.mockResolvedValue(mockFileHandle);
+
+ await tailFile('/test/file.txt', 2);
+
+ expect(mockFs.stat).toHaveBeenCalledWith('/test/file.txt');
+ expect(mockFs.open).toHaveBeenCalledWith('/test/file.txt', 'r');
+ });
+
+ it('handles files with content and returns last lines', async () => {
+ mockFs.stat.mockResolvedValue({ size: 50 } as any);
+
+ const mockFileHandle = {
+ read: vi.fn(),
+ close: vi.fn()
+ } as any;
+
+ // Simulate reading file content in chunks
+ mockFileHandle.read
+ .mockResolvedValueOnce({ bytesRead: 20, buffer: Buffer.from('line3\nline4\nline5\n') })
+ .mockResolvedValueOnce({ bytesRead: 0 });
+ mockFileHandle.close.mockResolvedValue(undefined);
+
+ mockFs.open.mockResolvedValue(mockFileHandle);
+
+ const result = await tailFile('/test/file.txt', 2);
+
+ expect(mockFileHandle.close).toHaveBeenCalled();
+ });
+
+ it('handles read errors gracefully', async () => {
+ mockFs.stat.mockResolvedValue({ size: 100 } as any);
+
+ const mockFileHandle = {
+ read: vi.fn(),
+ close: vi.fn()
+ } as any;
+
+ mockFileHandle.read.mockResolvedValue({ bytesRead: 0 });
+ mockFileHandle.close.mockResolvedValue(undefined);
+
+ mockFs.open.mockResolvedValue(mockFileHandle);
+
+ await tailFile('/test/file.txt', 5);
+
+ expect(mockFileHandle.close).toHaveBeenCalled();
+ });
+ });
+
+ describe('headFile', () => {
+ it('opens file for reading', async () => {
+ // Mock file handle with proper typing
+ const mockFileHandle = {
+ read: vi.fn(),
+ close: vi.fn()
+ } as any;
+
+ mockFileHandle.read.mockResolvedValue({ bytesRead: 0 });
+ mockFileHandle.close.mockResolvedValue(undefined);
+
+ mockFs.open.mockResolvedValue(mockFileHandle);
+
+ await headFile('/test/file.txt', 2);
+
+ expect(mockFs.open).toHaveBeenCalledWith('/test/file.txt', 'r');
+ });
+
+ it('handles files with content and returns first lines', async () => {
+ const mockFileHandle = {
+ read: vi.fn(),
+ close: vi.fn()
+ } as any;
+
+ // Simulate reading file content with newlines
+ mockFileHandle.read
+ .mockResolvedValueOnce({ bytesRead: 20, buffer: Buffer.from('line1\nline2\nline3\n') })
+ .mockResolvedValueOnce({ bytesRead: 0 });
+ mockFileHandle.close.mockResolvedValue(undefined);
+
+ mockFs.open.mockResolvedValue(mockFileHandle);
+
+ const result = await headFile('/test/file.txt', 2);
+
+ expect(mockFileHandle.close).toHaveBeenCalled();
+ });
+
+ it('handles files with leftover content', async () => {
+ const mockFileHandle = {
+ read: vi.fn(),
+ close: vi.fn()
+ } as any;
+
+ // Simulate reading file content without final newline
+ mockFileHandle.read
+ .mockResolvedValueOnce({ bytesRead: 15, buffer: Buffer.from('line1\nline2\nend') })
+ .mockResolvedValueOnce({ bytesRead: 0 });
+ mockFileHandle.close.mockResolvedValue(undefined);
+
+ mockFs.open.mockResolvedValue(mockFileHandle);
+
+ const result = await headFile('/test/file.txt', 5);
+
+ expect(mockFileHandle.close).toHaveBeenCalled();
+ });
+
+ it('handles reaching requested line count', async () => {
+ const mockFileHandle = {
+ read: vi.fn(),
+ close: vi.fn()
+ } as any;
+
+ // Simulate reading exactly the requested number of lines
+ mockFileHandle.read
+ .mockResolvedValueOnce({ bytesRead: 12, buffer: Buffer.from('line1\nline2\n') })
+ .mockResolvedValueOnce({ bytesRead: 0 });
+ mockFileHandle.close.mockResolvedValue(undefined);
+
+ mockFs.open.mockResolvedValue(mockFileHandle);
+
+ const result = await headFile('/test/file.txt', 2);
+
+ expect(mockFileHandle.close).toHaveBeenCalled();
+ });
+ });
+ });
+});
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/__tests__/path-utils.test.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/__tests__/path-utils.test.ts
new file mode 100644
index 00000000..5530cba1
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/__tests__/path-utils.test.ts
@@ -0,0 +1,371 @@
+import { describe, it, expect, afterEach } from 'vitest';
+import { normalizePath, expandHome, convertToWindowsPath } from '../path-utils.js';
+
+describe('Path Utilities', () => {
+ describe('convertToWindowsPath', () => {
+ it('leaves Unix paths unchanged', () => {
+ expect(convertToWindowsPath('/usr/local/bin'))
+ .toBe('/usr/local/bin');
+ expect(convertToWindowsPath('/home/user/some path'))
+ .toBe('/home/user/some path');
+ });
+
+ it('never converts WSL paths (they work correctly in WSL with Node.js fs)', () => {
+ // WSL paths should NEVER be converted, regardless of platform
+ // They are valid Linux paths that work with Node.js fs operations inside WSL
+ expect(convertToWindowsPath('/mnt/c/NS/MyKindleContent'))
+ .toBe('/mnt/c/NS/MyKindleContent');
+ expect(convertToWindowsPath('/mnt/d/Documents'))
+ .toBe('/mnt/d/Documents');
+ });
+
+ it('converts Unix-style Windows paths only on Windows platform', () => {
+ // On Windows, /c/ style paths should be converted
+ if (process.platform === 'win32') {
+ expect(convertToWindowsPath('/c/NS/MyKindleContent'))
+ .toBe('C:\\NS\\MyKindleContent');
+ } else {
+ // On Linux, leave them unchanged
+ expect(convertToWindowsPath('/c/NS/MyKindleContent'))
+ .toBe('/c/NS/MyKindleContent');
+ }
+ });
+
+ it('leaves Windows paths unchanged but ensures backslashes', () => {
+ expect(convertToWindowsPath('C:\\NS\\MyKindleContent'))
+ .toBe('C:\\NS\\MyKindleContent');
+ expect(convertToWindowsPath('C:/NS/MyKindleContent'))
+ .toBe('C:\\NS\\MyKindleContent');
+ });
+
+ it('handles Windows paths with spaces', () => {
+ expect(convertToWindowsPath('C:\\Program Files\\Some App'))
+ .toBe('C:\\Program Files\\Some App');
+ expect(convertToWindowsPath('C:/Program Files/Some App'))
+ .toBe('C:\\Program Files\\Some App');
+ });
+
+ it('handles drive letter paths based on platform', () => {
+ // WSL paths should never be converted
+ expect(convertToWindowsPath('/mnt/d/some/path'))
+ .toBe('/mnt/d/some/path');
+
+ if (process.platform === 'win32') {
+ // On Windows, Unix-style paths like /d/ should be converted
+ expect(convertToWindowsPath('/d/some/path'))
+ .toBe('D:\\some\\path');
+ } else {
+ // On Linux, /d/ is just a regular Unix path
+ expect(convertToWindowsPath('/d/some/path'))
+ .toBe('/d/some/path');
+ }
+ });
+ });
+
+ describe('normalizePath', () => {
+ it('preserves Unix paths', () => {
+ expect(normalizePath('/usr/local/bin'))
+ .toBe('/usr/local/bin');
+ expect(normalizePath('/home/user/some path'))
+ .toBe('/home/user/some path');
+ expect(normalizePath('"/usr/local/some app/"'))
+ .toBe('/usr/local/some app');
+ expect(normalizePath('/usr/local//bin/app///'))
+ .toBe('/usr/local/bin/app');
+ expect(normalizePath('/'))
+ .toBe('/');
+ expect(normalizePath('///'))
+ .toBe('/');
+ });
+
+ it('removes surrounding quotes', () => {
+ expect(normalizePath('"C:\\NS\\My Kindle Content"'))
+ .toBe('C:\\NS\\My Kindle Content');
+ });
+
+ it('normalizes backslashes', () => {
+ expect(normalizePath('C:\\\\NS\\\\MyKindleContent'))
+ .toBe('C:\\NS\\MyKindleContent');
+ });
+
+ it('converts forward slashes to backslashes on Windows', () => {
+ expect(normalizePath('C:/NS/MyKindleContent'))
+ .toBe('C:\\NS\\MyKindleContent');
+ });
+
+ it('always preserves WSL paths (they work correctly in WSL)', () => {
+ // WSL paths should ALWAYS be preserved, regardless of platform
+ // This is the fix for issue #2795
+ expect(normalizePath('/mnt/c/NS/MyKindleContent'))
+ .toBe('/mnt/c/NS/MyKindleContent');
+ expect(normalizePath('/mnt/d/Documents'))
+ .toBe('/mnt/d/Documents');
+ });
+
+ it('handles Unix-style Windows paths', () => {
+ // On Windows, /c/ paths should be converted
+ if (process.platform === 'win32') {
+ expect(normalizePath('/c/NS/MyKindleContent'))
+ .toBe('C:\\NS\\MyKindleContent');
+ } else if (process.platform === 'linux') {
+ // On Linux, /c/ is just a regular Unix path
+ expect(normalizePath('/c/NS/MyKindleContent'))
+ .toBe('/c/NS/MyKindleContent');
+ }
+ });
+
+ it('handles paths with spaces and mixed slashes', () => {
+ expect(normalizePath('C:/NS/My Kindle Content'))
+ .toBe('C:\\NS\\My Kindle Content');
+ // WSL paths should always be preserved
+ expect(normalizePath('/mnt/c/NS/My Kindle Content'))
+ .toBe('/mnt/c/NS/My Kindle Content');
+ expect(normalizePath('C:\\Program Files (x86)\\App Name'))
+ .toBe('C:\\Program Files (x86)\\App Name');
+ expect(normalizePath('"C:\\Program Files\\App Name"'))
+ .toBe('C:\\Program Files\\App Name');
+ expect(normalizePath(' C:\\Program Files\\App Name '))
+ .toBe('C:\\Program Files\\App Name');
+ });
+
+ it('preserves spaces in all path formats', () => {
+ // WSL paths should always be preserved
+ expect(normalizePath('/mnt/c/Program Files/App Name'))
+ .toBe('/mnt/c/Program Files/App Name');
+
+ if (process.platform === 'win32') {
+ // On Windows, Unix-style paths like /c/ should be converted
+ expect(normalizePath('/c/Program Files/App Name'))
+ .toBe('C:\\Program Files\\App Name');
+ } else {
+ // On Linux, /c/ is just a regular Unix path
+ expect(normalizePath('/c/Program Files/App Name'))
+ .toBe('/c/Program Files/App Name');
+ }
+ expect(normalizePath('C:/Program Files/App Name'))
+ .toBe('C:\\Program Files\\App Name');
+ });
+
+ it('handles special characters in paths', () => {
+ // Test ampersand in path
+ expect(normalizePath('C:\\NS\\Sub&Folder'))
+ .toBe('C:\\NS\\Sub&Folder');
+ expect(normalizePath('C:/NS/Sub&Folder'))
+ .toBe('C:\\NS\\Sub&Folder');
+ // WSL paths should always be preserved
+ expect(normalizePath('/mnt/c/NS/Sub&Folder'))
+ .toBe('/mnt/c/NS/Sub&Folder');
+
+ // Test tilde in path (short names in Windows)
+ expect(normalizePath('C:\\NS\\MYKIND~1'))
+ .toBe('C:\\NS\\MYKIND~1');
+ expect(normalizePath('/Users/NEMANS~1/FOLDER~2/SUBFO~1/Public/P12PST~1'))
+ .toBe('/Users/NEMANS~1/FOLDER~2/SUBFO~1/Public/P12PST~1');
+
+ // Test other special characters
+ expect(normalizePath('C:\\Path with #hash'))
+ .toBe('C:\\Path with #hash');
+ expect(normalizePath('C:\\Path with (parentheses)'))
+ .toBe('C:\\Path with (parentheses)');
+ expect(normalizePath('C:\\Path with [brackets]'))
+ .toBe('C:\\Path with [brackets]');
+ expect(normalizePath('C:\\Path with @at+plus$dollar%percent'))
+ .toBe('C:\\Path with @at+plus$dollar%percent');
+ });
+
+ it('capitalizes lowercase drive letters for Windows paths', () => {
+ expect(normalizePath('c:/windows/system32'))
+ .toBe('C:\\windows\\system32');
+ // WSL paths should always be preserved
+ expect(normalizePath('/mnt/d/my/folder'))
+ .toBe('/mnt/d/my/folder');
+
+ if (process.platform === 'win32') {
+ // On Windows, Unix-style paths should be converted and capitalized
+ expect(normalizePath('/e/another/folder'))
+ .toBe('E:\\another\\folder');
+ } else {
+ // On Linux, /e/ is just a regular Unix path
+ expect(normalizePath('/e/another/folder'))
+ .toBe('/e/another/folder');
+ }
+ });
+
+ it('handles UNC paths correctly', () => {
+ // UNC paths should preserve the leading double backslash
+ const uncPath = '\\\\SERVER\\share\\folder';
+ expect(normalizePath(uncPath)).toBe('\\\\SERVER\\share\\folder');
+
+ // Test UNC path with double backslashes that need normalization
+ const uncPathWithDoubles = '\\\\\\\\SERVER\\\\share\\\\folder';
+ expect(normalizePath(uncPathWithDoubles)).toBe('\\\\SERVER\\share\\folder');
+ });
+
+ it('returns normalized non-Windows/WSL/Unix-style Windows paths as is after basic normalization', () => {
+ // A path that looks somewhat absolute but isn't a drive or recognized Unix root for Windows conversion
+ // These paths should be preserved as-is (not converted to Windows C:\ format or WSL format)
+ const otherAbsolutePath = '\\someserver\\share\\file';
+ expect(normalizePath(otherAbsolutePath)).toBe(otherAbsolutePath);
+ });
+ });
+
+ describe('expandHome', () => {
+ it('expands ~ to home directory', () => {
+ const result = expandHome('~/test');
+ expect(result).toContain('test');
+ expect(result).not.toContain('~');
+ });
+
+ it('expands bare ~ to home directory', () => {
+ const result = expandHome('~');
+ expect(result).not.toContain('~');
+ expect(result.length).toBeGreaterThan(0);
+ });
+
+ it('leaves other paths unchanged', () => {
+ expect(expandHome('C:/test')).toBe('C:/test');
+ });
+ });
+
+ describe('WSL path handling (issue #2795 fix)', () => {
+ // Save original platform
+ const originalPlatform = process.platform;
+
+ afterEach(() => {
+ // Restore platform after each test
+ Object.defineProperty(process, 'platform', {
+ value: originalPlatform,
+ writable: true,
+ configurable: true
+ });
+ });
+
+ it('should NEVER convert WSL paths - they work correctly in WSL with Node.js fs', () => {
+ // The key insight: When running `wsl npx ...`, Node.js runs INSIDE WSL (process.platform === 'linux')
+ // and /mnt/c/ paths work correctly with Node.js fs operations in that environment.
+ // Converting them to C:\ format breaks fs operations because Windows paths don't work inside WSL.
+
+ // Mock Linux platform (inside WSL)
+ Object.defineProperty(process, 'platform', {
+ value: 'linux',
+ writable: true,
+ configurable: true
+ });
+
+ // WSL paths should NOT be converted, even inside WSL
+ expect(normalizePath('/mnt/c/Users/username/folder'))
+ .toBe('/mnt/c/Users/username/folder');
+
+ expect(normalizePath('/mnt/d/Documents/project'))
+ .toBe('/mnt/d/Documents/project');
+ });
+
+ it('should also preserve WSL paths when running on Windows', () => {
+ // Mock Windows platform
+ Object.defineProperty(process, 'platform', {
+ value: 'win32',
+ writable: true,
+ configurable: true
+ });
+
+ // WSL paths should still be preserved (though they wouldn't be accessible from Windows Node.js)
+ expect(normalizePath('/mnt/c/Users/username/folder'))
+ .toBe('/mnt/c/Users/username/folder');
+
+ expect(normalizePath('/mnt/d/Documents/project'))
+ .toBe('/mnt/d/Documents/project');
+ });
+
+ it('should convert Unix-style Windows paths (/c/) only when running on Windows (win32)', () => {
+ // Mock process.platform to be 'win32' (Windows)
+ Object.defineProperty(process, 'platform', {
+ value: 'win32',
+ writable: true,
+ configurable: true
+ });
+
+ // Unix-style Windows paths like /c/ should be converted on Windows
+ expect(normalizePath('/c/Users/username/folder'))
+ .toBe('C:\\Users\\username\\folder');
+
+ expect(normalizePath('/d/Documents/project'))
+ .toBe('D:\\Documents\\project');
+ });
+
+ it('should NOT convert Unix-style paths (/c/) when running inside WSL (linux)', () => {
+ // Mock process.platform to be 'linux' (WSL/Linux)
+ Object.defineProperty(process, 'platform', {
+ value: 'linux',
+ writable: true,
+ configurable: true
+ });
+
+ // When on Linux, /c/ is just a regular Unix directory, not a drive letter
+ expect(normalizePath('/c/some/path'))
+ .toBe('/c/some/path');
+
+ expect(normalizePath('/d/another/path'))
+ .toBe('/d/another/path');
+ });
+
+ it('should preserve regular Unix paths on all platforms', () => {
+ // Test on Linux
+ Object.defineProperty(process, 'platform', {
+ value: 'linux',
+ writable: true,
+ configurable: true
+ });
+
+ expect(normalizePath('/home/user/documents'))
+ .toBe('/home/user/documents');
+
+ expect(normalizePath('/var/log/app'))
+ .toBe('/var/log/app');
+
+ // Test on Windows (though these paths wouldn't work on Windows)
+ Object.defineProperty(process, 'platform', {
+ value: 'win32',
+ writable: true,
+ configurable: true
+ });
+
+ expect(normalizePath('/home/user/documents'))
+ .toBe('/home/user/documents');
+
+ expect(normalizePath('/var/log/app'))
+ .toBe('/var/log/app');
+ });
+
+ it('reproduces exact scenario from issue #2795', () => {
+ // Simulate running inside WSL: wsl npx @modelcontextprotocol/server-filesystem /mnt/c/Users/username/folder
+ Object.defineProperty(process, 'platform', {
+ value: 'linux',
+ writable: true,
+ configurable: true
+ });
+
+ // This is the exact path from the issue
+ const inputPath = '/mnt/c/Users/username/folder';
+ const result = normalizePath(inputPath);
+
+ // Should NOT convert to C:\Users\username\folder
+ expect(result).toBe('/mnt/c/Users/username/folder');
+ expect(result).not.toContain('C:');
+ expect(result).not.toContain('\\');
+ });
+
+ it('should handle relative path slash conversion based on platform', () => {
+ // This test verifies platform-specific behavior naturally without mocking
+ // On Windows: forward slashes converted to backslashes
+ // On Linux/Unix: forward slashes preserved
+ const relativePath = 'some/relative/path';
+ const result = normalizePath(relativePath);
+
+ if (originalPlatform === 'win32') {
+ expect(result).toBe('some\\relative\\path');
+ } else {
+ expect(result).toBe('some/relative/path');
+ }
+ });
+ });
+});
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/__tests__/path-validation.test.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/__tests__/path-validation.test.ts
new file mode 100644
index 00000000..81ad247e
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/__tests__/path-validation.test.ts
@@ -0,0 +1,1000 @@
+import { describe, it, expect, beforeEach, afterEach } from 'vitest';
+import * as path from 'path';
+import * as fs from 'fs/promises';
+import * as os from 'os';
+import { isPathWithinAllowedDirectories } from '../path-validation.js';
+
+/**
+ * Check if the current environment supports symlink creation
+ */
+async function checkSymlinkSupport(): Promise {
+ const testDir = await fs.mkdtemp(path.join(os.tmpdir(), 'symlink-test-'));
+ try {
+ const targetFile = path.join(testDir, 'target.txt');
+ const linkFile = path.join(testDir, 'link.txt');
+
+ await fs.writeFile(targetFile, 'test');
+ await fs.symlink(targetFile, linkFile);
+
+ // If we get here, symlinks are supported
+ return true;
+ } catch (error) {
+ // EPERM indicates no symlink permissions
+ if ((error as NodeJS.ErrnoException).code === 'EPERM') {
+ return false;
+ }
+ // Other errors might indicate a real problem
+ throw error;
+ } finally {
+ await fs.rm(testDir, { recursive: true, force: true });
+ }
+}
+
+// Global variable to store symlink support status
+let symlinkSupported: boolean | null = null;
+
+/**
+ * Get cached symlink support status, checking once per test run
+ */
+async function getSymlinkSupport(): Promise {
+ if (symlinkSupported === null) {
+ symlinkSupported = await checkSymlinkSupport();
+ if (!symlinkSupported) {
+ console.log('\n⚠️ Symlink tests will be skipped - symlink creation not supported in this environment');
+ console.log(' On Windows, enable Developer Mode or run as Administrator to enable symlink tests');
+ }
+ }
+ return symlinkSupported;
+}
+
+describe('Path Validation', () => {
+ it('allows exact directory match', () => {
+ const allowed = ['/home/user/project'];
+ expect(isPathWithinAllowedDirectories('/home/user/project', allowed)).toBe(true);
+ });
+
+ it('allows subdirectories', () => {
+ const allowed = ['/home/user/project'];
+ expect(isPathWithinAllowedDirectories('/home/user/project/src', allowed)).toBe(true);
+ expect(isPathWithinAllowedDirectories('/home/user/project/src/index.js', allowed)).toBe(true);
+ expect(isPathWithinAllowedDirectories('/home/user/project/deeply/nested/file.txt', allowed)).toBe(true);
+ });
+
+ it('blocks similar directory names (prefix vulnerability)', () => {
+ const allowed = ['/home/user/project'];
+ expect(isPathWithinAllowedDirectories('/home/user/project2', allowed)).toBe(false);
+ expect(isPathWithinAllowedDirectories('/home/user/project_backup', allowed)).toBe(false);
+ expect(isPathWithinAllowedDirectories('/home/user/project-old', allowed)).toBe(false);
+ expect(isPathWithinAllowedDirectories('/home/user/projectile', allowed)).toBe(false);
+ expect(isPathWithinAllowedDirectories('/home/user/project.bak', allowed)).toBe(false);
+ });
+
+ it('blocks paths outside allowed directories', () => {
+ const allowed = ['/home/user/project'];
+ expect(isPathWithinAllowedDirectories('/home/user/other', allowed)).toBe(false);
+ expect(isPathWithinAllowedDirectories('/etc/passwd', allowed)).toBe(false);
+ expect(isPathWithinAllowedDirectories('/home/user', allowed)).toBe(false);
+ expect(isPathWithinAllowedDirectories('/', allowed)).toBe(false);
+ });
+
+ it('handles multiple allowed directories', () => {
+ const allowed = ['/home/user/project1', '/home/user/project2'];
+ expect(isPathWithinAllowedDirectories('/home/user/project1/src', allowed)).toBe(true);
+ expect(isPathWithinAllowedDirectories('/home/user/project2/src', allowed)).toBe(true);
+ expect(isPathWithinAllowedDirectories('/home/user/project3', allowed)).toBe(false);
+ expect(isPathWithinAllowedDirectories('/home/user/project1_backup', allowed)).toBe(false);
+ expect(isPathWithinAllowedDirectories('/home/user/project2-old', allowed)).toBe(false);
+ });
+
+ it('blocks parent and sibling directories', () => {
+ const allowed = ['/test/allowed'];
+
+ // Parent directory
+ expect(isPathWithinAllowedDirectories('/test', allowed)).toBe(false);
+ expect(isPathWithinAllowedDirectories('/', allowed)).toBe(false);
+
+ // Sibling with common prefix
+ expect(isPathWithinAllowedDirectories('/test/allowed_sibling', allowed)).toBe(false);
+ expect(isPathWithinAllowedDirectories('/test/allowed2', allowed)).toBe(false);
+ });
+
+ it('handles paths with special characters', () => {
+ const allowed = ['/home/user/my-project (v2)'];
+
+ expect(isPathWithinAllowedDirectories('/home/user/my-project (v2)', allowed)).toBe(true);
+ expect(isPathWithinAllowedDirectories('/home/user/my-project (v2)/src', allowed)).toBe(true);
+ expect(isPathWithinAllowedDirectories('/home/user/my-project (v2)_backup', allowed)).toBe(false);
+ expect(isPathWithinAllowedDirectories('/home/user/my-project', allowed)).toBe(false);
+ });
+
+ describe('Input validation', () => {
+ it('rejects empty inputs', () => {
+ const allowed = ['/home/user/project'];
+
+ expect(isPathWithinAllowedDirectories('', allowed)).toBe(false);
+ expect(isPathWithinAllowedDirectories('/home/user/project', [])).toBe(false);
+ });
+
+ it('handles trailing separators correctly', () => {
+ const allowed = ['/home/user/project'];
+
+ // Path with trailing separator should still match
+ expect(isPathWithinAllowedDirectories('/home/user/project/', allowed)).toBe(true);
+
+ // Allowed directory with trailing separator
+ const allowedWithSep = ['/home/user/project/'];
+ expect(isPathWithinAllowedDirectories('/home/user/project', allowedWithSep)).toBe(true);
+ expect(isPathWithinAllowedDirectories('/home/user/project/', allowedWithSep)).toBe(true);
+
+ // Should still block similar names with or without trailing separators
+ expect(isPathWithinAllowedDirectories('/home/user/project2', allowedWithSep)).toBe(false);
+ expect(isPathWithinAllowedDirectories('/home/user/project2', allowed)).toBe(false);
+ expect(isPathWithinAllowedDirectories('/home/user/project2/', allowed)).toBe(false);
+ });
+
+ it('skips empty directory entries in allowed list', () => {
+ const allowed = ['', '/home/user/project', ''];
+ expect(isPathWithinAllowedDirectories('/home/user/project', allowed)).toBe(true);
+ expect(isPathWithinAllowedDirectories('/home/user/project/src', allowed)).toBe(true);
+
+ // Should still validate properly with empty entries
+ expect(isPathWithinAllowedDirectories('/home/user/other', allowed)).toBe(false);
+ });
+
+ it('handles Windows paths with trailing separators', () => {
+ if (path.sep === '\\') {
+ const allowed = ['C:\\Users\\project'];
+
+ // Path with trailing separator
+ expect(isPathWithinAllowedDirectories('C:\\Users\\project\\', allowed)).toBe(true);
+
+ // Allowed with trailing separator
+ const allowedWithSep = ['C:\\Users\\project\\'];
+ expect(isPathWithinAllowedDirectories('C:\\Users\\project', allowedWithSep)).toBe(true);
+ expect(isPathWithinAllowedDirectories('C:\\Users\\project\\', allowedWithSep)).toBe(true);
+
+ // Should still block similar names
+ expect(isPathWithinAllowedDirectories('C:\\Users\\project2\\', allowed)).toBe(false);
+ }
+ });
+ });
+
+ describe('Error handling', () => {
+ it('normalizes relative paths to absolute', () => {
+ const allowed = [process.cwd()];
+
+ // Relative paths get normalized to absolute paths based on cwd
+ expect(isPathWithinAllowedDirectories('relative/path', allowed)).toBe(true);
+ expect(isPathWithinAllowedDirectories('./file', allowed)).toBe(true);
+
+ // Parent directory references that escape allowed directory
+ const parentAllowed = ['/home/user/project'];
+ expect(isPathWithinAllowedDirectories('../parent', parentAllowed)).toBe(false);
+ });
+
+ it('returns false for relative paths in allowed directories', () => {
+ const badAllowed = ['relative/path', '/some/other/absolute/path'];
+
+ // Relative paths in allowed dirs are normalized to absolute based on cwd
+ // The normalized 'relative/path' won't match our test path
+ expect(isPathWithinAllowedDirectories('/some/other/absolute/path/file', badAllowed)).toBe(true);
+ expect(isPathWithinAllowedDirectories('/absolute/path/file', badAllowed)).toBe(false);
+ });
+
+ it('handles null and undefined inputs gracefully', () => {
+ const allowed = ['/home/user/project'];
+
+ // Should return false, not crash
+ expect(isPathWithinAllowedDirectories(null as any, allowed)).toBe(false);
+ expect(isPathWithinAllowedDirectories(undefined as any, allowed)).toBe(false);
+ expect(isPathWithinAllowedDirectories('/path', null as any)).toBe(false);
+ expect(isPathWithinAllowedDirectories('/path', undefined as any)).toBe(false);
+ });
+ });
+
+ describe('Unicode and special characters', () => {
+ it('handles unicode characters in paths', () => {
+ const allowed = ['/home/user/café'];
+
+ expect(isPathWithinAllowedDirectories('/home/user/café', allowed)).toBe(true);
+ expect(isPathWithinAllowedDirectories('/home/user/café/file', allowed)).toBe(true);
+
+ // Different unicode representation won't match (not normalized)
+ const decomposed = '/home/user/cafe\u0301'; // e + combining accent
+ expect(isPathWithinAllowedDirectories(decomposed, allowed)).toBe(false);
+ });
+
+ it('handles paths with spaces correctly', () => {
+ const allowed = ['/home/user/my project'];
+
+ expect(isPathWithinAllowedDirectories('/home/user/my project', allowed)).toBe(true);
+ expect(isPathWithinAllowedDirectories('/home/user/my project/file', allowed)).toBe(true);
+
+ // Partial matches should fail
+ expect(isPathWithinAllowedDirectories('/home/user/my', allowed)).toBe(false);
+ expect(isPathWithinAllowedDirectories('/home/user/my proj', allowed)).toBe(false);
+ });
+ });
+
+ describe('Overlapping allowed directories', () => {
+ it('handles nested allowed directories correctly', () => {
+ const allowed = ['/home', '/home/user', '/home/user/project'];
+
+ // All paths under /home are allowed
+ expect(isPathWithinAllowedDirectories('/home/anything', allowed)).toBe(true);
+ expect(isPathWithinAllowedDirectories('/home/user/anything', allowed)).toBe(true);
+ expect(isPathWithinAllowedDirectories('/home/user/project/anything', allowed)).toBe(true);
+
+ // First match wins (most permissive)
+ expect(isPathWithinAllowedDirectories('/home/other/deep/path', allowed)).toBe(true);
+ });
+
+ it('handles root directory as allowed', () => {
+ const allowed = ['/'];
+
+ // Everything is allowed under root (dangerous configuration)
+ expect(isPathWithinAllowedDirectories('/', allowed)).toBe(true);
+ expect(isPathWithinAllowedDirectories('/any/path', allowed)).toBe(true);
+ expect(isPathWithinAllowedDirectories('/etc/passwd', allowed)).toBe(true);
+ expect(isPathWithinAllowedDirectories('/home/user/secret', allowed)).toBe(true);
+
+ // But only on the same filesystem root
+ if (path.sep === '\\') {
+ expect(isPathWithinAllowedDirectories('D:\\other', ['/'])).toBe(false);
+ }
+ });
+ });
+
+ describe('Cross-platform behavior', () => {
+ it('handles Windows-style paths on Windows', () => {
+ if (path.sep === '\\') {
+ const allowed = ['C:\\Users\\project'];
+ expect(isPathWithinAllowedDirectories('C:\\Users\\project', allowed)).toBe(true);
+ expect(isPathWithinAllowedDirectories('C:\\Users\\project\\src', allowed)).toBe(true);
+ expect(isPathWithinAllowedDirectories('C:\\Users\\project2', allowed)).toBe(false);
+ expect(isPathWithinAllowedDirectories('C:\\Users\\project_backup', allowed)).toBe(false);
+ }
+ });
+
+ it('handles Unix-style paths on Unix', () => {
+ if (path.sep === '/') {
+ const allowed = ['/home/user/project'];
+ expect(isPathWithinAllowedDirectories('/home/user/project', allowed)).toBe(true);
+ expect(isPathWithinAllowedDirectories('/home/user/project/src', allowed)).toBe(true);
+ expect(isPathWithinAllowedDirectories('/home/user/project2', allowed)).toBe(false);
+ }
+ });
+ });
+
+ describe('Validation Tests - Path Traversal', () => {
+ it('blocks path traversal attempts', () => {
+ const allowed = ['/home/user/project'];
+
+ // Basic traversal attempts
+ expect(isPathWithinAllowedDirectories('/home/user/project/../../../etc/passwd', allowed)).toBe(false);
+ expect(isPathWithinAllowedDirectories('/home/user/project/../../other', allowed)).toBe(false);
+ expect(isPathWithinAllowedDirectories('/home/user/project/../project2', allowed)).toBe(false);
+
+ // Mixed traversal with valid segments
+ expect(isPathWithinAllowedDirectories('/home/user/project/src/../../project2', allowed)).toBe(false);
+ expect(isPathWithinAllowedDirectories('/home/user/project/./../../other', allowed)).toBe(false);
+
+ // Multiple traversal sequences
+ expect(isPathWithinAllowedDirectories('/home/user/project/../project/../../../etc', allowed)).toBe(false);
+ });
+
+ it('blocks traversal in allowed directories', () => {
+ const allowed = ['/home/user/project/../safe'];
+
+ // The allowed directory itself should be normalized and safe
+ expect(isPathWithinAllowedDirectories('/home/user/safe/file', allowed)).toBe(true);
+ expect(isPathWithinAllowedDirectories('/home/user/project/file', allowed)).toBe(false);
+ });
+
+ it('handles complex traversal patterns', () => {
+ const allowed = ['/home/user/project'];
+
+ // Double dots in filenames (not traversal) - these normalize to paths within allowed dir
+ expect(isPathWithinAllowedDirectories('/home/user/project/..test', allowed)).toBe(true); // Not traversal
+ expect(isPathWithinAllowedDirectories('/home/user/project/test..', allowed)).toBe(true); // Not traversal
+ expect(isPathWithinAllowedDirectories('/home/user/project/te..st', allowed)).toBe(true); // Not traversal
+
+ // Actual traversal
+ expect(isPathWithinAllowedDirectories('/home/user/project/../test', allowed)).toBe(false); // Is traversal - goes to /home/user/test
+
+ // Edge case: /home/user/project/.. normalizes to /home/user (parent dir)
+ expect(isPathWithinAllowedDirectories('/home/user/project/..', allowed)).toBe(false); // Goes to parent
+ });
+ });
+
+ describe('Validation Tests - Null Bytes', () => {
+ it('rejects paths with null bytes', () => {
+ const allowed = ['/home/user/project'];
+
+ expect(isPathWithinAllowedDirectories('/home/user/project\x00/etc/passwd', allowed)).toBe(false);
+ expect(isPathWithinAllowedDirectories('/home/user/project/test\x00.txt', allowed)).toBe(false);
+ expect(isPathWithinAllowedDirectories('\x00/home/user/project', allowed)).toBe(false);
+ expect(isPathWithinAllowedDirectories('/home/user/project/\x00', allowed)).toBe(false);
+ });
+
+ it('rejects allowed directories with null bytes', () => {
+ const allowed = ['/home/user/project\x00'];
+
+ expect(isPathWithinAllowedDirectories('/home/user/project', allowed)).toBe(false);
+ expect(isPathWithinAllowedDirectories('/home/user/project/file', allowed)).toBe(false);
+ });
+ });
+
+ describe('Validation Tests - Special Characters', () => {
+ it('allows percent signs in filenames', () => {
+ const allowed = ['/home/user/project'];
+
+ // Percent is a valid filename character
+ expect(isPathWithinAllowedDirectories('/home/user/project/report_50%.pdf', allowed)).toBe(true);
+ expect(isPathWithinAllowedDirectories('/home/user/project/Q1_25%_growth', allowed)).toBe(true);
+ expect(isPathWithinAllowedDirectories('/home/user/project/%41', allowed)).toBe(true); // File named %41
+
+ // URL encoding is NOT decoded by path.normalize, so these are just odd filenames
+ expect(isPathWithinAllowedDirectories('/home/user/project/%2e%2e', allowed)).toBe(true); // File named "%2e%2e"
+ expect(isPathWithinAllowedDirectories('/home/user/project/file%20name', allowed)).toBe(true); // File with %20 in name
+ });
+
+ it('handles percent signs in allowed directories', () => {
+ const allowed = ['/home/user/project%20files'];
+
+ // This is a directory literally named "project%20files"
+ expect(isPathWithinAllowedDirectories('/home/user/project%20files/test', allowed)).toBe(true);
+ expect(isPathWithinAllowedDirectories('/home/user/project files/test', allowed)).toBe(false); // Different dir
+ });
+ });
+
+ describe('Path Normalization', () => {
+ it('normalizes paths before comparison', () => {
+ const allowed = ['/home/user/project'];
+
+ // Trailing slashes
+ expect(isPathWithinAllowedDirectories('/home/user/project/', allowed)).toBe(true);
+ expect(isPathWithinAllowedDirectories('/home/user/project//', allowed)).toBe(true);
+ expect(isPathWithinAllowedDirectories('/home/user/project///', allowed)).toBe(true);
+
+ // Current directory references
+ expect(isPathWithinAllowedDirectories('/home/user/project/./src', allowed)).toBe(true);
+ expect(isPathWithinAllowedDirectories('/home/user/./project/src', allowed)).toBe(true);
+
+ // Multiple slashes
+ expect(isPathWithinAllowedDirectories('/home/user/project//src//file', allowed)).toBe(true);
+ expect(isPathWithinAllowedDirectories('/home//user//project//src', allowed)).toBe(true);
+
+ // Should still block outside paths
+ expect(isPathWithinAllowedDirectories('/home/user//project2', allowed)).toBe(false);
+ });
+
+ it('handles mixed separators correctly', () => {
+ if (path.sep === '\\') {
+ const allowed = ['C:\\Users\\project'];
+
+ // Mixed separators should be normalized
+ expect(isPathWithinAllowedDirectories('C:/Users/project', allowed)).toBe(true);
+ expect(isPathWithinAllowedDirectories('C:\\Users/project\\src', allowed)).toBe(true);
+ expect(isPathWithinAllowedDirectories('C:/Users\\project/src', allowed)).toBe(true);
+ }
+ });
+ });
+
+ describe('Edge Cases', () => {
+ it('rejects non-string inputs safely', () => {
+ const allowed = ['/home/user/project'];
+
+ expect(isPathWithinAllowedDirectories(123 as any, allowed)).toBe(false);
+ expect(isPathWithinAllowedDirectories({} as any, allowed)).toBe(false);
+ expect(isPathWithinAllowedDirectories([] as any, allowed)).toBe(false);
+ expect(isPathWithinAllowedDirectories(null as any, allowed)).toBe(false);
+ expect(isPathWithinAllowedDirectories(undefined as any, allowed)).toBe(false);
+
+ // Non-string in allowed directories
+ expect(isPathWithinAllowedDirectories('/home/user/project', [123 as any])).toBe(false);
+ expect(isPathWithinAllowedDirectories('/home/user/project', [{} as any])).toBe(false);
+ });
+
+ it('handles very long paths', () => {
+ const allowed = ['/home/user/project'];
+
+ // Create a very long path that's still valid
+ const longSubPath = 'a/'.repeat(1000) + 'file.txt';
+ expect(isPathWithinAllowedDirectories(`/home/user/project/${longSubPath}`, allowed)).toBe(true);
+
+ // Very long path that escapes
+ const escapePath = 'a/'.repeat(1000) + '../'.repeat(1001) + 'etc/passwd';
+ expect(isPathWithinAllowedDirectories(`/home/user/project/${escapePath}`, allowed)).toBe(false);
+ });
+ });
+
+ describe('Additional Coverage', () => {
+ it('handles allowed directories with traversal that normalizes safely', () => {
+ // These allowed dirs contain traversal but normalize to valid paths
+ const allowed = ['/home/user/../user/project'];
+
+ // Should normalize to /home/user/project and work correctly
+ expect(isPathWithinAllowedDirectories('/home/user/project/file', allowed)).toBe(true);
+ expect(isPathWithinAllowedDirectories('/home/user/other', allowed)).toBe(false);
+ });
+
+ it('handles symbolic dots in filenames', () => {
+ const allowed = ['/home/user/project'];
+
+ // Single and double dots as actual filenames (not traversal)
+ expect(isPathWithinAllowedDirectories('/home/user/project/.', allowed)).toBe(true);
+ expect(isPathWithinAllowedDirectories('/home/user/project/..', allowed)).toBe(false); // This normalizes to parent
+ expect(isPathWithinAllowedDirectories('/home/user/project/...', allowed)).toBe(true); // Three dots is a valid filename
+ expect(isPathWithinAllowedDirectories('/home/user/project/....', allowed)).toBe(true); // Four dots is a valid filename
+ });
+
+ it('handles UNC paths on Windows', () => {
+ if (path.sep === '\\') {
+ const allowed = ['\\\\server\\share\\project'];
+
+ expect(isPathWithinAllowedDirectories('\\\\server\\share\\project', allowed)).toBe(true);
+ expect(isPathWithinAllowedDirectories('\\\\server\\share\\project\\file', allowed)).toBe(true);
+ expect(isPathWithinAllowedDirectories('\\\\server\\share\\other', allowed)).toBe(false);
+ expect(isPathWithinAllowedDirectories('\\\\other\\share\\project', allowed)).toBe(false);
+ }
+ });
+ });
+
+ describe('Symlink Tests', () => {
+ let testDir: string;
+ let allowedDir: string;
+ let forbiddenDir: string;
+
+ beforeEach(async () => {
+ testDir = await fs.mkdtemp(path.join(os.tmpdir(), 'fs-error-test-'));
+ allowedDir = path.join(testDir, 'allowed');
+ forbiddenDir = path.join(testDir, 'forbidden');
+
+ await fs.mkdir(allowedDir, { recursive: true });
+ await fs.mkdir(forbiddenDir, { recursive: true });
+ });
+
+ afterEach(async () => {
+ await fs.rm(testDir, { recursive: true, force: true });
+ });
+
+ it('validates symlink handling', async () => {
+ // Test with symlinks
+ try {
+ const linkPath = path.join(allowedDir, 'bad-link');
+ const targetPath = path.join(forbiddenDir, 'target.txt');
+
+ await fs.writeFile(targetPath, 'content');
+ await fs.symlink(targetPath, linkPath);
+
+ // In real implementation, this would throw with the resolved path
+ const realPath = await fs.realpath(linkPath);
+ const allowed = [allowedDir];
+
+ // Symlink target should be outside allowed directory
+ expect(isPathWithinAllowedDirectories(realPath, allowed)).toBe(false);
+ } catch (error) {
+ // Skip if no symlink permissions
+ }
+ });
+
+ it('handles non-existent paths correctly', async () => {
+ const newFilePath = path.join(allowedDir, 'subdir', 'newfile.txt');
+
+ // Parent directory doesn't exist
+ try {
+ await fs.access(newFilePath);
+ } catch (error) {
+ expect((error as NodeJS.ErrnoException).code).toBe('ENOENT');
+ }
+
+ // After creating parent, validation should work
+ await fs.mkdir(path.dirname(newFilePath), { recursive: true });
+ const allowed = [allowedDir];
+ expect(isPathWithinAllowedDirectories(newFilePath, allowed)).toBe(true);
+ });
+
+ // Test path resolution consistency for symlinked files
+ it('validates symlinked files consistently between path and resolved forms', async () => {
+ try {
+ // Setup: Create target file in forbidden area
+ const targetFile = path.join(forbiddenDir, 'target.txt');
+ await fs.writeFile(targetFile, 'TARGET_CONTENT');
+
+ // Create symlink inside allowed directory pointing to forbidden file
+ const symlinkPath = path.join(allowedDir, 'link-to-target.txt');
+ await fs.symlink(targetFile, symlinkPath);
+
+ // The symlink path itself passes validation (looks like it's in allowed dir)
+ expect(isPathWithinAllowedDirectories(symlinkPath, [allowedDir])).toBe(true);
+
+ // But the resolved path should fail validation
+ const resolvedPath = await fs.realpath(symlinkPath);
+ expect(isPathWithinAllowedDirectories(resolvedPath, [allowedDir])).toBe(false);
+
+ // Verify the resolved path goes to the forbidden location (normalize both paths for macOS temp dirs)
+ expect(await fs.realpath(resolvedPath)).toBe(await fs.realpath(targetFile));
+ } catch (error) {
+ // Skip if no symlink permissions on the system
+ if ((error as NodeJS.ErrnoException).code !== 'EPERM') {
+ throw error;
+ }
+ }
+ });
+
+ // Test allowed directory resolution behavior
+ it('validates paths correctly when allowed directory is resolved from symlink', async () => {
+ try {
+ // Setup: Create the actual target directory with content
+ const actualTargetDir = path.join(testDir, 'actual-target');
+ await fs.mkdir(actualTargetDir, { recursive: true });
+ const targetFile = path.join(actualTargetDir, 'file.txt');
+ await fs.writeFile(targetFile, 'FILE_CONTENT');
+
+ // Setup: Create symlink directory that points to target
+ const symlinkDir = path.join(testDir, 'symlink-dir');
+ await fs.symlink(actualTargetDir, symlinkDir);
+
+ // Simulate resolved allowed directory (what the server startup should do)
+ const resolvedAllowedDir = await fs.realpath(symlinkDir);
+ const resolvedTargetDir = await fs.realpath(actualTargetDir);
+ expect(resolvedAllowedDir).toBe(resolvedTargetDir);
+
+ // Test 1: File access through original symlink path should pass validation with resolved allowed dir
+ const fileViaSymlink = path.join(symlinkDir, 'file.txt');
+ const resolvedFile = await fs.realpath(fileViaSymlink);
+ expect(isPathWithinAllowedDirectories(resolvedFile, [resolvedAllowedDir])).toBe(true);
+
+ // Test 2: File access through resolved path should also pass validation
+ const fileViaResolved = path.join(resolvedTargetDir, 'file.txt');
+ expect(isPathWithinAllowedDirectories(fileViaResolved, [resolvedAllowedDir])).toBe(true);
+
+ // Test 3: Demonstrate inconsistent behavior with unresolved allowed directories
+ // If allowed dirs were not resolved (storing symlink paths instead):
+ const unresolvedAllowedDirs = [symlinkDir];
+ // This validation would incorrectly fail for the same content:
+ expect(isPathWithinAllowedDirectories(resolvedFile, unresolvedAllowedDirs)).toBe(false);
+
+ } catch (error) {
+ // Skip if no symlink permissions on the system
+ if ((error as NodeJS.ErrnoException).code !== 'EPERM') {
+ throw error;
+ }
+ }
+ });
+
+ // Test for macOS /tmp -> /private/tmp symlink issue (GitHub issue #3253)
+ // When allowed directories include BOTH original and resolved paths,
+ // paths through either form should be accepted
+ it('allows paths through both original and resolved symlink directories', async () => {
+ try {
+ // Setup: Create the actual target directory with content
+ const actualTargetDir = path.join(testDir, 'actual-target');
+ await fs.mkdir(actualTargetDir, { recursive: true });
+ const targetFile = path.join(actualTargetDir, 'file.txt');
+ await fs.writeFile(targetFile, 'FILE_CONTENT');
+
+ // Setup: Create symlink directory that points to target (simulates /tmp -> /private/tmp)
+ const symlinkDir = path.join(testDir, 'symlink-dir');
+ await fs.symlink(actualTargetDir, symlinkDir);
+
+ // Get the resolved path
+ const resolvedDir = await fs.realpath(symlinkDir);
+
+ // THE FIX: Store BOTH original symlink path AND resolved path in allowed directories
+ // This is what the server should do during startup to fix issue #3253
+ const allowedDirsWithBoth = [symlinkDir, resolvedDir];
+
+ // Test 1: Path through original symlink should pass validation
+ // (e.g., user requests /tmp/file.txt when /tmp is in allowed dirs)
+ const fileViaSymlink = path.join(symlinkDir, 'file.txt');
+ expect(isPathWithinAllowedDirectories(fileViaSymlink, allowedDirsWithBoth)).toBe(true);
+
+ // Test 2: Path through resolved directory should also pass validation
+ // (e.g., user requests /private/tmp/file.txt)
+ const fileViaResolved = path.join(resolvedDir, 'file.txt');
+ expect(isPathWithinAllowedDirectories(fileViaResolved, allowedDirsWithBoth)).toBe(true);
+
+ // Test 3: The resolved path of the symlink file should also pass
+ const resolvedFile = await fs.realpath(fileViaSymlink);
+ expect(isPathWithinAllowedDirectories(resolvedFile, allowedDirsWithBoth)).toBe(true);
+
+ // Verify both paths point to the same actual file
+ expect(resolvedFile).toBe(await fs.realpath(fileViaResolved));
+
+ } catch (error) {
+ // Skip if no symlink permissions on the system
+ if ((error as NodeJS.ErrnoException).code !== 'EPERM') {
+ throw error;
+ }
+ }
+ });
+
+ it('resolves nested symlink chains completely', async () => {
+ try {
+ // Setup: Create target file in forbidden area
+ const actualTarget = path.join(forbiddenDir, 'target-file.txt');
+ await fs.writeFile(actualTarget, 'FINAL_CONTENT');
+
+ // Create chain of symlinks: allowedFile -> link2 -> link1 -> actualTarget
+ const link1 = path.join(testDir, 'intermediate-link1');
+ const link2 = path.join(testDir, 'intermediate-link2');
+ const allowedFile = path.join(allowedDir, 'seemingly-safe-file');
+
+ await fs.symlink(actualTarget, link1);
+ await fs.symlink(link1, link2);
+ await fs.symlink(link2, allowedFile);
+
+ // The allowed file path passes basic validation
+ expect(isPathWithinAllowedDirectories(allowedFile, [allowedDir])).toBe(true);
+
+ // But complete resolution reveals the forbidden target
+ const fullyResolvedPath = await fs.realpath(allowedFile);
+ expect(isPathWithinAllowedDirectories(fullyResolvedPath, [allowedDir])).toBe(false);
+ expect(await fs.realpath(fullyResolvedPath)).toBe(await fs.realpath(actualTarget));
+
+ } catch (error) {
+ // Skip if no symlink permissions on the system
+ if ((error as NodeJS.ErrnoException).code !== 'EPERM') {
+ throw error;
+ }
+ }
+ });
+ });
+
+ describe('Path Validation Race Condition Tests', () => {
+ let testDir: string;
+ let allowedDir: string;
+ let forbiddenDir: string;
+ let targetFile: string;
+ let testPath: string;
+
+ beforeEach(async () => {
+ testDir = await fs.mkdtemp(path.join(os.tmpdir(), 'race-test-'));
+ allowedDir = path.join(testDir, 'allowed');
+ forbiddenDir = path.join(testDir, 'outside');
+ targetFile = path.join(forbiddenDir, 'target.txt');
+ testPath = path.join(allowedDir, 'test.txt');
+
+ await fs.mkdir(allowedDir, { recursive: true });
+ await fs.mkdir(forbiddenDir, { recursive: true });
+ await fs.writeFile(targetFile, 'ORIGINAL CONTENT', 'utf-8');
+ });
+
+ afterEach(async () => {
+ await fs.rm(testDir, { recursive: true, force: true });
+ });
+
+ it('validates non-existent file paths based on parent directory', async () => {
+ const allowed = [allowedDir];
+
+ expect(isPathWithinAllowedDirectories(testPath, allowed)).toBe(true);
+ await expect(fs.access(testPath)).rejects.toThrow();
+
+ const parentDir = path.dirname(testPath);
+ expect(isPathWithinAllowedDirectories(parentDir, allowed)).toBe(true);
+ });
+
+ it('demonstrates symlink race condition allows writing outside allowed directories', async () => {
+ const symlinkSupported = await getSymlinkSupport();
+ if (!symlinkSupported) {
+ console.log(' ⏭️ Skipping symlink race condition test - symlinks not supported');
+ return;
+ }
+
+ const allowed = [allowedDir];
+
+ await expect(fs.access(testPath)).rejects.toThrow();
+ expect(isPathWithinAllowedDirectories(testPath, allowed)).toBe(true);
+
+ await fs.symlink(targetFile, testPath);
+ await fs.writeFile(testPath, 'MODIFIED CONTENT', 'utf-8');
+
+ const targetContent = await fs.readFile(targetFile, 'utf-8');
+ expect(targetContent).toBe('MODIFIED CONTENT');
+
+ const resolvedPath = await fs.realpath(testPath);
+ expect(isPathWithinAllowedDirectories(resolvedPath, allowed)).toBe(false);
+ });
+
+ it('shows timing differences between validation approaches', async () => {
+ const symlinkSupported = await getSymlinkSupport();
+ if (!symlinkSupported) {
+ console.log(' ⏭️ Skipping timing validation test - symlinks not supported');
+ return;
+ }
+
+ const allowed = [allowedDir];
+
+ const validation1 = isPathWithinAllowedDirectories(testPath, allowed);
+ expect(validation1).toBe(true);
+
+ await fs.symlink(targetFile, testPath);
+
+ const resolvedPath = await fs.realpath(testPath);
+ const validation2 = isPathWithinAllowedDirectories(resolvedPath, allowed);
+ expect(validation2).toBe(false);
+
+ expect(validation1).not.toBe(validation2);
+ });
+
+ it('validates directory creation timing', async () => {
+ const symlinkSupported = await getSymlinkSupport();
+ if (!symlinkSupported) {
+ console.log(' ⏭️ Skipping directory creation timing test - symlinks not supported');
+ return;
+ }
+
+ const allowed = [allowedDir];
+ const testDir = path.join(allowedDir, 'newdir');
+
+ expect(isPathWithinAllowedDirectories(testDir, allowed)).toBe(true);
+
+ await fs.symlink(forbiddenDir, testDir);
+
+ expect(isPathWithinAllowedDirectories(testDir, allowed)).toBe(true);
+
+ const resolved = await fs.realpath(testDir);
+ expect(isPathWithinAllowedDirectories(resolved, allowed)).toBe(false);
+ });
+
+ it('demonstrates exclusive file creation behavior', async () => {
+ const symlinkSupported = await getSymlinkSupport();
+ if (!symlinkSupported) {
+ console.log(' ⏭️ Skipping exclusive file creation test - symlinks not supported');
+ return;
+ }
+
+ const allowed = [allowedDir];
+
+ await fs.symlink(targetFile, testPath);
+
+ await expect(fs.open(testPath, 'wx')).rejects.toThrow(/EEXIST/);
+
+ await fs.writeFile(testPath, 'NEW CONTENT', 'utf-8');
+ const targetContent = await fs.readFile(targetFile, 'utf-8');
+ expect(targetContent).toBe('NEW CONTENT');
+ });
+
+ it('should use resolved parent paths for non-existent files', async () => {
+ const symlinkSupported = await getSymlinkSupport();
+ if (!symlinkSupported) {
+ console.log(' ⏭️ Skipping resolved parent paths test - symlinks not supported');
+ return;
+ }
+
+ const allowed = [allowedDir];
+
+ const symlinkDir = path.join(allowedDir, 'link');
+ await fs.symlink(forbiddenDir, symlinkDir);
+
+ const fileThroughSymlink = path.join(symlinkDir, 'newfile.txt');
+
+ expect(fileThroughSymlink.startsWith(allowedDir)).toBe(true);
+
+ const parentDir = path.dirname(fileThroughSymlink);
+ const resolvedParent = await fs.realpath(parentDir);
+ expect(isPathWithinAllowedDirectories(resolvedParent, allowed)).toBe(false);
+
+ const expectedSafePath = path.join(resolvedParent, path.basename(fileThroughSymlink));
+ expect(isPathWithinAllowedDirectories(expectedSafePath, allowed)).toBe(false);
+ });
+
+ it('demonstrates parent directory symlink traversal', async () => {
+ const symlinkSupported = await getSymlinkSupport();
+ if (!symlinkSupported) {
+ console.log(' ⏭️ Skipping parent directory symlink traversal test - symlinks not supported');
+ return;
+ }
+
+ const allowed = [allowedDir];
+ const deepPath = path.join(allowedDir, 'sub1', 'sub2', 'file.txt');
+
+ expect(isPathWithinAllowedDirectories(deepPath, allowed)).toBe(true);
+
+ const sub1Path = path.join(allowedDir, 'sub1');
+ await fs.symlink(forbiddenDir, sub1Path);
+
+ await fs.mkdir(path.join(sub1Path, 'sub2'), { recursive: true });
+ await fs.writeFile(deepPath, 'CONTENT', 'utf-8');
+
+ const realPath = await fs.realpath(deepPath);
+ const realAllowedDir = await fs.realpath(allowedDir);
+ const realForbiddenDir = await fs.realpath(forbiddenDir);
+
+ expect(realPath.startsWith(realAllowedDir)).toBe(false);
+ expect(realPath.startsWith(realForbiddenDir)).toBe(true);
+ });
+
+ it('should prevent race condition between validatePath and file operation', async () => {
+ const symlinkSupported = await getSymlinkSupport();
+ if (!symlinkSupported) {
+ console.log(' ⏭️ Skipping race condition prevention test - symlinks not supported');
+ return;
+ }
+
+ const allowed = [allowedDir];
+ const racePath = path.join(allowedDir, 'race-file.txt');
+ const targetFile = path.join(forbiddenDir, 'target.txt');
+
+ await fs.writeFile(targetFile, 'ORIGINAL CONTENT', 'utf-8');
+
+ // Path validation would pass (file doesn't exist, parent is in allowed dir)
+ expect(await fs.access(racePath).then(() => false).catch(() => true)).toBe(true);
+ expect(isPathWithinAllowedDirectories(racePath, allowed)).toBe(true);
+
+ // Race condition: symlink created after validation but before write
+ await fs.symlink(targetFile, racePath);
+
+ // With exclusive write flag, write should fail on symlink
+ await expect(
+ fs.writeFile(racePath, 'NEW CONTENT', { encoding: 'utf-8', flag: 'wx' })
+ ).rejects.toThrow(/EEXIST/);
+
+ // Verify content unchanged
+ const targetContent = await fs.readFile(targetFile, 'utf-8');
+ expect(targetContent).toBe('ORIGINAL CONTENT');
+
+ // The symlink exists but write was blocked
+ const actualWritePath = await fs.realpath(racePath);
+ expect(actualWritePath).toBe(await fs.realpath(targetFile));
+ expect(isPathWithinAllowedDirectories(actualWritePath, allowed)).toBe(false);
+ });
+
+ it('should allow overwrites to legitimate files within allowed directories', async () => {
+ const allowed = [allowedDir];
+ const legitFile = path.join(allowedDir, 'legit-file.txt');
+
+ // Create a legitimate file
+ await fs.writeFile(legitFile, 'ORIGINAL', 'utf-8');
+
+ // Opening with w should work for legitimate files
+ const fd = await fs.open(legitFile, 'w');
+ try {
+ await fd.write('UPDATED', 0, 'utf-8');
+ } finally {
+ await fd.close();
+ }
+
+ const content = await fs.readFile(legitFile, 'utf-8');
+ expect(content).toBe('UPDATED');
+ });
+
+ it('should handle symlinks that point within allowed directories', async () => {
+ const symlinkSupported = await getSymlinkSupport();
+ if (!symlinkSupported) {
+ console.log(' ⏭️ Skipping symlinks within allowed directories test - symlinks not supported');
+ return;
+ }
+
+ const allowed = [allowedDir];
+ const targetFile = path.join(allowedDir, 'target.txt');
+ const symlinkPath = path.join(allowedDir, 'symlink.txt');
+
+ // Create target file within allowed directory
+ await fs.writeFile(targetFile, 'TARGET CONTENT', 'utf-8');
+
+ // Create symlink pointing to allowed file
+ await fs.symlink(targetFile, symlinkPath);
+
+ // Opening symlink with w follows it to the target
+ const fd = await fs.open(symlinkPath, 'w');
+ try {
+ await fd.write('UPDATED VIA SYMLINK', 0, 'utf-8');
+ } finally {
+ await fd.close();
+ }
+
+ // Both symlink and target should show updated content
+ const symlinkContent = await fs.readFile(symlinkPath, 'utf-8');
+ const targetContent = await fs.readFile(targetFile, 'utf-8');
+ expect(symlinkContent).toBe('UPDATED VIA SYMLINK');
+ expect(targetContent).toBe('UPDATED VIA SYMLINK');
+ });
+
+ it('should prevent overwriting files through symlinks pointing outside allowed directories', async () => {
+ const symlinkSupported = await getSymlinkSupport();
+ if (!symlinkSupported) {
+ console.log(' ⏭️ Skipping symlink overwrite prevention test - symlinks not supported');
+ return;
+ }
+
+ const allowed = [allowedDir];
+ const legitFile = path.join(allowedDir, 'existing.txt');
+ const targetFile = path.join(forbiddenDir, 'target.txt');
+
+ // Create a legitimate file first
+ await fs.writeFile(legitFile, 'LEGIT CONTENT', 'utf-8');
+
+ // Create target file in forbidden directory
+ await fs.writeFile(targetFile, 'FORBIDDEN CONTENT', 'utf-8');
+
+ // Now replace the legitimate file with a symlink to forbidden location
+ await fs.unlink(legitFile);
+ await fs.symlink(targetFile, legitFile);
+
+ // Simulate the server's validation logic
+ const stats = await fs.lstat(legitFile);
+ expect(stats.isSymbolicLink()).toBe(true);
+
+ const realPath = await fs.realpath(legitFile);
+ expect(isPathWithinAllowedDirectories(realPath, allowed)).toBe(false);
+
+ // With atomic rename, symlinks are replaced not followed
+ // So this test now demonstrates the protection
+
+ // Verify content remains unchanged
+ const targetContent = await fs.readFile(targetFile, 'utf-8');
+ expect(targetContent).toBe('FORBIDDEN CONTENT');
+ });
+
+ it('demonstrates race condition in read operations', async () => {
+ const symlinkSupported = await getSymlinkSupport();
+ if (!symlinkSupported) {
+ console.log(' ⏭️ Skipping race condition in read operations test - symlinks not supported');
+ return;
+ }
+
+ const allowed = [allowedDir];
+ const legitFile = path.join(allowedDir, 'readable.txt');
+ const secretFile = path.join(forbiddenDir, 'secret.txt');
+
+ // Create legitimate file
+ await fs.writeFile(legitFile, 'PUBLIC CONTENT', 'utf-8');
+
+ // Create secret file in forbidden directory
+ await fs.writeFile(secretFile, 'SECRET CONTENT', 'utf-8');
+
+ // Step 1: validatePath would pass for legitimate file
+ expect(isPathWithinAllowedDirectories(legitFile, allowed)).toBe(true);
+
+ // Step 2: Race condition - replace file with symlink after validation
+ await fs.unlink(legitFile);
+ await fs.symlink(secretFile, legitFile);
+
+ // Step 3: Read operation follows symlink to forbidden location
+ const content = await fs.readFile(legitFile, 'utf-8');
+
+ // This shows the vulnerability - we read forbidden content
+ expect(content).toBe('SECRET CONTENT');
+ expect(isPathWithinAllowedDirectories(await fs.realpath(legitFile), allowed)).toBe(false);
+ });
+
+ it('verifies rename does not follow symlinks', async () => {
+ const symlinkSupported = await getSymlinkSupport();
+ if (!symlinkSupported) {
+ console.log(' ⏭️ Skipping rename symlink test - symlinks not supported');
+ return;
+ }
+
+ const allowed = [allowedDir];
+ const tempFile = path.join(allowedDir, 'temp.txt');
+ const targetSymlink = path.join(allowedDir, 'target-symlink.txt');
+ const forbiddenTarget = path.join(forbiddenDir, 'forbidden-target.txt');
+
+ // Create forbidden target
+ await fs.writeFile(forbiddenTarget, 'ORIGINAL CONTENT', 'utf-8');
+
+ // Create symlink pointing to forbidden location
+ await fs.symlink(forbiddenTarget, targetSymlink);
+
+ // Write temp file
+ await fs.writeFile(tempFile, 'NEW CONTENT', 'utf-8');
+
+ // Rename temp file to symlink path
+ await fs.rename(tempFile, targetSymlink);
+
+ // Check what happened
+ const symlinkExists = await fs.lstat(targetSymlink).then(() => true).catch(() => false);
+ const isSymlink = symlinkExists && (await fs.lstat(targetSymlink)).isSymbolicLink();
+ const targetContent = await fs.readFile(targetSymlink, 'utf-8');
+ const forbiddenContent = await fs.readFile(forbiddenTarget, 'utf-8');
+
+ // Rename should replace the symlink with a regular file
+ expect(isSymlink).toBe(false);
+ expect(targetContent).toBe('NEW CONTENT');
+ expect(forbiddenContent).toBe('ORIGINAL CONTENT'); // Unchanged
+ });
+ });
+});
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/__tests__/roots-utils.test.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/__tests__/roots-utils.test.ts
new file mode 100644
index 00000000..1a394839
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/__tests__/roots-utils.test.ts
@@ -0,0 +1,84 @@
+import { describe, it, expect, beforeEach, afterEach } from 'vitest';
+import { getValidRootDirectories } from '../roots-utils.js';
+import { mkdtempSync, rmSync, mkdirSync, writeFileSync, realpathSync } from 'fs';
+import { tmpdir } from 'os';
+import { join } from 'path';
+import type { Root } from '@modelcontextprotocol/sdk/types.js';
+
+describe('getValidRootDirectories', () => {
+ let testDir1: string;
+ let testDir2: string;
+ let testDir3: string;
+ let testFile: string;
+
+ beforeEach(() => {
+ // Create test directories
+ testDir1 = realpathSync(mkdtempSync(join(tmpdir(), 'mcp-roots-test1-')));
+ testDir2 = realpathSync(mkdtempSync(join(tmpdir(), 'mcp-roots-test2-')));
+ testDir3 = realpathSync(mkdtempSync(join(tmpdir(), 'mcp-roots-test3-')));
+
+ // Create a test file (not a directory)
+ testFile = join(testDir1, 'test-file.txt');
+ writeFileSync(testFile, 'test content');
+ });
+
+ afterEach(() => {
+ // Cleanup
+ rmSync(testDir1, { recursive: true, force: true });
+ rmSync(testDir2, { recursive: true, force: true });
+ rmSync(testDir3, { recursive: true, force: true });
+ });
+
+ describe('valid directory processing', () => {
+ it('should process all URI formats and edge cases', async () => {
+ const roots = [
+ { uri: `file://${testDir1}`, name: 'File URI' },
+ { uri: testDir2, name: 'Plain path' },
+ { uri: testDir3 } // Plain path without name property
+ ];
+
+ const result = await getValidRootDirectories(roots);
+
+ expect(result).toContain(testDir1);
+ expect(result).toContain(testDir2);
+ expect(result).toContain(testDir3);
+ expect(result).toHaveLength(3);
+ });
+
+ it('should normalize complex paths', async () => {
+ const subDir = join(testDir1, 'subdir');
+ mkdirSync(subDir);
+
+ const roots = [
+ { uri: `file://${testDir1}/./subdir/../subdir`, name: 'Complex Path' }
+ ];
+
+ const result = await getValidRootDirectories(roots);
+
+ expect(result).toHaveLength(1);
+ expect(result[0]).toBe(subDir);
+ });
+ });
+
+ describe('error handling', () => {
+
+ it('should handle various error types', async () => {
+ const nonExistentDir = join(tmpdir(), 'non-existent-directory-12345');
+ const invalidPath = '\0invalid\0path'; // Null bytes cause different error types
+ const roots = [
+ { uri: `file://${testDir1}`, name: 'Valid Dir' },
+ { uri: `file://${nonExistentDir}`, name: 'Non-existent Dir' },
+ { uri: `file://${testFile}`, name: 'File Not Dir' },
+ { uri: `file://${invalidPath}`, name: 'Invalid Path' }
+ ];
+
+ const result = await getValidRootDirectories(roots);
+
+ expect(result).toContain(testDir1);
+ expect(result).not.toContain(nonExistentDir);
+ expect(result).not.toContain(testFile);
+ expect(result).not.toContain(invalidPath);
+ expect(result).toHaveLength(1);
+ });
+ });
+});
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/__tests__/startup-validation.test.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/__tests__/startup-validation.test.ts
new file mode 100644
index 00000000..3be283df
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/__tests__/startup-validation.test.ts
@@ -0,0 +1,100 @@
+import { describe, it, expect, beforeEach, afterEach } from 'vitest';
+import { spawn } from 'child_process';
+import * as path from 'path';
+import * as fs from 'fs/promises';
+import * as os from 'os';
+
+const SERVER_PATH = path.join(__dirname, '..', 'dist', 'index.js');
+
+/**
+ * Spawns the filesystem server with given arguments and returns exit info
+ */
+async function spawnServer(args: string[], timeoutMs = 2000): Promise<{ exitCode: number | null; stderr: string }> {
+ return new Promise((resolve) => {
+ const proc = spawn('node', [SERVER_PATH, ...args], {
+ stdio: ['pipe', 'pipe', 'pipe'],
+ });
+
+ let stderr = '';
+ proc.stderr?.on('data', (data) => {
+ stderr += data.toString();
+ });
+
+ const timeout = setTimeout(() => {
+ proc.kill('SIGTERM');
+ }, timeoutMs);
+
+ proc.on('close', (code) => {
+ clearTimeout(timeout);
+ resolve({ exitCode: code, stderr });
+ });
+
+ proc.on('error', (err) => {
+ clearTimeout(timeout);
+ resolve({ exitCode: 1, stderr: err.message });
+ });
+ });
+}
+
+describe('Startup Directory Validation', () => {
+ let testDir: string;
+ let accessibleDir: string;
+ let accessibleDir2: string;
+
+ beforeEach(async () => {
+ testDir = await fs.mkdtemp(path.join(os.tmpdir(), 'fs-startup-test-'));
+ accessibleDir = path.join(testDir, 'accessible');
+ accessibleDir2 = path.join(testDir, 'accessible2');
+ await fs.mkdir(accessibleDir, { recursive: true });
+ await fs.mkdir(accessibleDir2, { recursive: true });
+ });
+
+ afterEach(async () => {
+ await fs.rm(testDir, { recursive: true, force: true });
+ });
+
+ it('should start successfully with all accessible directories', async () => {
+ const result = await spawnServer([accessibleDir, accessibleDir2]);
+ // Server starts and runs (we kill it after timeout, so exit code is null or from SIGTERM)
+ expect(result.stderr).toContain('Secure MCP Filesystem Server running on stdio');
+ expect(result.stderr).not.toContain('Error:');
+ });
+
+ it('should skip inaccessible directory and continue with accessible one', async () => {
+ const nonExistentDir = path.join(testDir, 'non-existent-dir-12345');
+
+ const result = await spawnServer([nonExistentDir, accessibleDir]);
+
+ // Should warn about inaccessible directory
+ expect(result.stderr).toContain('Warning: Cannot access directory');
+ expect(result.stderr).toContain(nonExistentDir);
+
+ // Should still start successfully
+ expect(result.stderr).toContain('Secure MCP Filesystem Server running on stdio');
+ });
+
+ it('should exit with error when ALL directories are inaccessible', async () => {
+ const nonExistent1 = path.join(testDir, 'non-existent-1');
+ const nonExistent2 = path.join(testDir, 'non-existent-2');
+
+ const result = await spawnServer([nonExistent1, nonExistent2]);
+
+ // Should exit with error
+ expect(result.exitCode).toBe(1);
+ expect(result.stderr).toContain('Error: None of the specified directories are accessible');
+ });
+
+ it('should warn when path is not a directory', async () => {
+ const filePath = path.join(testDir, 'not-a-directory.txt');
+ await fs.writeFile(filePath, 'content');
+
+ const result = await spawnServer([filePath, accessibleDir]);
+
+ // Should warn about non-directory
+ expect(result.stderr).toContain('Warning:');
+ expect(result.stderr).toContain('not a directory');
+
+ // Should still start with the valid directory
+ expect(result.stderr).toContain('Secure MCP Filesystem Server running on stdio');
+ });
+});
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/__tests__/structured-content.test.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/__tests__/structured-content.test.ts
new file mode 100644
index 00000000..4b8f92b0
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/__tests__/structured-content.test.ts
@@ -0,0 +1,158 @@
+import { describe, it, expect, beforeEach, afterEach } from 'vitest';
+import * as fs from 'fs/promises';
+import * as path from 'path';
+import * as os from 'os';
+import { Client } from '@modelcontextprotocol/sdk/client/index.js';
+import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
+import { spawn } from 'child_process';
+
+/**
+ * Integration tests to verify that tool handlers return structuredContent
+ * that matches the declared outputSchema.
+ *
+ * These tests address issues #3110, #3106, #3093 where tools were returning
+ * structuredContent: { content: [contentBlock] } (array) instead of
+ * structuredContent: { content: string } as declared in outputSchema.
+ */
+describe('structuredContent schema compliance', () => {
+ let client: Client;
+ let transport: StdioClientTransport;
+ let testDir: string;
+
+ beforeEach(async () => {
+ // Create a temp directory for testing
+ testDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mcp-fs-test-'));
+
+ // Create test files
+ await fs.writeFile(path.join(testDir, 'test.txt'), 'test content');
+ await fs.mkdir(path.join(testDir, 'subdir'));
+ await fs.writeFile(path.join(testDir, 'subdir', 'nested.txt'), 'nested content');
+
+ // Start the MCP server
+ const serverPath = path.resolve(__dirname, '../dist/index.js');
+ transport = new StdioClientTransport({
+ command: 'node',
+ args: [serverPath, testDir],
+ });
+
+ client = new Client({
+ name: 'test-client',
+ version: '1.0.0',
+ }, {
+ capabilities: {}
+ });
+
+ await client.connect(transport);
+ });
+
+ afterEach(async () => {
+ await client?.close();
+ await fs.rm(testDir, { recursive: true, force: true });
+ });
+
+ describe('directory_tree', () => {
+ it('should return structuredContent.content as a string, not an array', async () => {
+ const result = await client.callTool({
+ name: 'directory_tree',
+ arguments: { path: testDir }
+ });
+
+ // The result should have structuredContent
+ expect(result.structuredContent).toBeDefined();
+
+ // structuredContent.content should be a string (matching outputSchema: { content: z.string() })
+ const structuredContent = result.structuredContent as { content: unknown };
+ expect(typeof structuredContent.content).toBe('string');
+
+ // It should NOT be an array
+ expect(Array.isArray(structuredContent.content)).toBe(false);
+
+ // The content should be valid JSON representing the tree
+ const treeData = JSON.parse(structuredContent.content as string);
+ expect(Array.isArray(treeData)).toBe(true);
+ });
+ });
+
+ describe('list_directory_with_sizes', () => {
+ it('should return structuredContent.content as a string, not an array', async () => {
+ const result = await client.callTool({
+ name: 'list_directory_with_sizes',
+ arguments: { path: testDir }
+ });
+
+ // The result should have structuredContent
+ expect(result.structuredContent).toBeDefined();
+
+ // structuredContent.content should be a string (matching outputSchema: { content: z.string() })
+ const structuredContent = result.structuredContent as { content: unknown };
+ expect(typeof structuredContent.content).toBe('string');
+
+ // It should NOT be an array
+ expect(Array.isArray(structuredContent.content)).toBe(false);
+
+ // The content should contain directory listing info
+ expect(structuredContent.content).toContain('[FILE]');
+ });
+ });
+
+ describe('move_file', () => {
+ it('should return structuredContent.content as a string, not an array', async () => {
+ const sourcePath = path.join(testDir, 'test.txt');
+ const destPath = path.join(testDir, 'moved.txt');
+
+ const result = await client.callTool({
+ name: 'move_file',
+ arguments: {
+ source: sourcePath,
+ destination: destPath
+ }
+ });
+
+ // The result should have structuredContent
+ expect(result.structuredContent).toBeDefined();
+
+ // structuredContent.content should be a string (matching outputSchema: { content: z.string() })
+ const structuredContent = result.structuredContent as { content: unknown };
+ expect(typeof structuredContent.content).toBe('string');
+
+ // It should NOT be an array
+ expect(Array.isArray(structuredContent.content)).toBe(false);
+
+ // The content should contain success message
+ expect(structuredContent.content).toContain('Successfully moved');
+ });
+ });
+
+ describe('list_directory (control - already working)', () => {
+ it('should return structuredContent.content as a string', async () => {
+ const result = await client.callTool({
+ name: 'list_directory',
+ arguments: { path: testDir }
+ });
+
+ expect(result.structuredContent).toBeDefined();
+
+ const structuredContent = result.structuredContent as { content: unknown };
+ expect(typeof structuredContent.content).toBe('string');
+ expect(Array.isArray(structuredContent.content)).toBe(false);
+ });
+ });
+
+ describe('search_files (control - already working)', () => {
+ it('should return structuredContent.content as a string', async () => {
+ const result = await client.callTool({
+ name: 'search_files',
+ arguments: {
+ path: testDir,
+ pattern: '*.txt'
+ }
+ });
+
+ expect(result.structuredContent).toBeDefined();
+
+ const structuredContent = result.structuredContent as { content: unknown };
+ expect(typeof structuredContent.content).toBe('string');
+ expect(Array.isArray(structuredContent.content)).toBe(false);
+ });
+ });
+});
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/index.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/index.ts
new file mode 100644
index 00000000..7b67e63e
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/index.ts
@@ -0,0 +1,767 @@
+#!/usr/bin/env node
+
+import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
+import {
+ CallToolResult,
+ RootsListChangedNotificationSchema,
+ type Root,
+} from "@modelcontextprotocol/sdk/types.js";
+import fs from "fs/promises";
+import { createReadStream } from "fs";
+import path from "path";
+import { z } from "zod";
+import { minimatch } from "minimatch";
+import { normalizePath, expandHome } from './path-utils.js';
+import { getValidRootDirectories } from './roots-utils.js';
+import {
+ // Function imports
+ formatSize,
+ validatePath,
+ getFileStats,
+ readFileContent,
+ writeFileContent,
+ searchFilesWithValidation,
+ applyFileEdits,
+ tailFile,
+ headFile,
+ setAllowedDirectories,
+} from './lib.js';
+
+// Command line argument parsing
+const args = process.argv.slice(2);
+if (args.length === 0) {
+ console.error("Usage: mcp-server-filesystem [allowed-directory] [additional-directories...]");
+ console.error("Note: Allowed directories can be provided via:");
+ console.error(" 1. Command-line arguments (shown above)");
+ console.error(" 2. MCP roots protocol (if client supports it)");
+ console.error("At least one directory must be provided by EITHER method for the server to operate.");
+}
+
+// Store allowed directories in normalized and resolved form
+// We store BOTH the original path AND the resolved path to handle symlinks correctly
+// This fixes the macOS /tmp -> /private/tmp symlink issue where users specify /tmp
+// but the resolved path is /private/tmp
+let allowedDirectories = (await Promise.all(
+ args.map(async (dir) => {
+ const expanded = expandHome(dir);
+ const absolute = path.resolve(expanded);
+ const normalizedOriginal = normalizePath(absolute);
+ try {
+ // Security: Resolve symlinks in allowed directories during startup
+ // This ensures we know the real paths and can validate against them later
+ const resolved = await fs.realpath(absolute);
+ const normalizedResolved = normalizePath(resolved);
+ // Return both original and resolved paths if they differ
+ // This allows matching against either /tmp or /private/tmp on macOS
+ if (normalizedOriginal !== normalizedResolved) {
+ return [normalizedOriginal, normalizedResolved];
+ }
+ return [normalizedResolved];
+ } catch (error) {
+ // If we can't resolve (doesn't exist), use the normalized absolute path
+ // This allows configuring allowed dirs that will be created later
+ return [normalizedOriginal];
+ }
+ })
+)).flat();
+
+// Filter to only accessible directories, warn about inaccessible ones
+const accessibleDirectories: string[] = [];
+for (const dir of allowedDirectories) {
+ try {
+ const stats = await fs.stat(dir);
+ if (stats.isDirectory()) {
+ accessibleDirectories.push(dir);
+ } else {
+ console.error(`Warning: ${dir} is not a directory, skipping`);
+ }
+ } catch (error) {
+ console.error(`Warning: Cannot access directory ${dir}, skipping`);
+ }
+}
+
+// Exit only if ALL paths are inaccessible (and some were specified)
+if (accessibleDirectories.length === 0 && allowedDirectories.length > 0) {
+ console.error("Error: None of the specified directories are accessible");
+ process.exit(1);
+}
+
+allowedDirectories = accessibleDirectories;
+
+// Initialize the global allowedDirectories in lib.ts
+setAllowedDirectories(allowedDirectories);
+
+// Schema definitions
+const ReadTextFileArgsSchema = z.object({
+ path: z.string(),
+ tail: z.number().optional().describe('If provided, returns only the last N lines of the file'),
+ head: z.number().optional().describe('If provided, returns only the first N lines of the file')
+});
+
+const ReadMediaFileArgsSchema = z.object({
+ path: z.string()
+});
+
+const ReadMultipleFilesArgsSchema = z.object({
+ paths: z
+ .array(z.string())
+ .min(1, "At least one file path must be provided")
+ .describe("Array of file paths to read. Each path must be a string pointing to a valid file within allowed directories."),
+});
+
+const WriteFileArgsSchema = z.object({
+ path: z.string(),
+ content: z.string(),
+});
+
+const EditOperation = z.object({
+ oldText: z.string().describe('Text to search for - must match exactly'),
+ newText: z.string().describe('Text to replace with')
+});
+
+const EditFileArgsSchema = z.object({
+ path: z.string(),
+ edits: z.array(EditOperation),
+ dryRun: z.boolean().default(false).describe('Preview changes using git-style diff format')
+});
+
+const CreateDirectoryArgsSchema = z.object({
+ path: z.string(),
+});
+
+const ListDirectoryArgsSchema = z.object({
+ path: z.string(),
+});
+
+const ListDirectoryWithSizesArgsSchema = z.object({
+ path: z.string(),
+ sortBy: z.enum(['name', 'size']).optional().default('name').describe('Sort entries by name or size'),
+});
+
+const DirectoryTreeArgsSchema = z.object({
+ path: z.string(),
+ excludePatterns: z.array(z.string()).optional().default([])
+});
+
+const MoveFileArgsSchema = z.object({
+ source: z.string(),
+ destination: z.string(),
+});
+
+const SearchFilesArgsSchema = z.object({
+ path: z.string(),
+ pattern: z.string(),
+ excludePatterns: z.array(z.string()).optional().default([])
+});
+
+const GetFileInfoArgsSchema = z.object({
+ path: z.string(),
+});
+
+// Server setup
+const server = new McpServer(
+ {
+ name: "secure-filesystem-server",
+ version: "0.2.0",
+ }
+);
+
+// Reads a file as a stream of buffers, concatenates them, and then encodes
+// the result to a Base64 string. This is a memory-efficient way to handle
+// binary data from a stream before the final encoding.
+async function readFileAsBase64Stream(filePath: string): Promise {
+ return new Promise((resolve, reject) => {
+ const stream = createReadStream(filePath);
+ const chunks: Buffer[] = [];
+ stream.on('data', (chunk) => {
+ chunks.push(chunk as Buffer);
+ });
+ stream.on('end', () => {
+ const finalBuffer = Buffer.concat(chunks);
+ resolve(finalBuffer.toString('base64'));
+ });
+ stream.on('error', (err) => reject(err));
+ });
+}
+
+// Tool registrations
+
+// read_file (deprecated) and read_text_file
+const readTextFileHandler = async (args: z.infer) => {
+ const validPath = await validatePath(args.path);
+
+ if (args.head && args.tail) {
+ throw new Error("Cannot specify both head and tail parameters simultaneously");
+ }
+
+ let content: string;
+ if (args.tail) {
+ content = await tailFile(validPath, args.tail);
+ } else if (args.head) {
+ content = await headFile(validPath, args.head);
+ } else {
+ content = await readFileContent(validPath);
+ }
+
+ return {
+ content: [{ type: "text" as const, text: content }],
+ structuredContent: { content }
+ };
+};
+
+server.registerTool(
+ "read_file",
+ {
+ title: "Read File (Deprecated)",
+ description: "Read the complete contents of a file as text. DEPRECATED: Use read_text_file instead.",
+ inputSchema: ReadTextFileArgsSchema.shape,
+ outputSchema: { content: z.string() },
+ annotations: { readOnlyHint: true }
+ },
+ readTextFileHandler
+);
+
+server.registerTool(
+ "read_text_file",
+ {
+ title: "Read Text File",
+ description:
+ "Read the complete contents of a file from the file system as text. " +
+ "Handles various text encodings and provides detailed error messages " +
+ "if the file cannot be read. Use this tool when you need to examine " +
+ "the contents of a single file. Use the 'head' parameter to read only " +
+ "the first N lines of a file, or the 'tail' parameter to read only " +
+ "the last N lines of a file. Operates on the file as text regardless of extension. " +
+ "Only works within allowed directories.",
+ inputSchema: {
+ path: z.string(),
+ tail: z.number().optional().describe("If provided, returns only the last N lines of the file"),
+ head: z.number().optional().describe("If provided, returns only the first N lines of the file")
+ },
+ outputSchema: { content: z.string() },
+ annotations: { readOnlyHint: true }
+ },
+ readTextFileHandler
+);
+
+server.registerTool(
+ "read_media_file",
+ {
+ title: "Read Media File",
+ description:
+ "Read an image or audio file. Returns the base64 encoded data and MIME type. " +
+ "Only works within allowed directories.",
+ inputSchema: {
+ path: z.string()
+ },
+ outputSchema: {
+ content: z.array(z.object({
+ type: z.enum(["image", "audio", "blob"]),
+ data: z.string(),
+ mimeType: z.string()
+ }))
+ },
+ annotations: { readOnlyHint: true }
+ },
+ async (args: z.infer) => {
+ const validPath = await validatePath(args.path);
+ const extension = path.extname(validPath).toLowerCase();
+ const mimeTypes: Record = {
+ ".png": "image/png",
+ ".jpg": "image/jpeg",
+ ".jpeg": "image/jpeg",
+ ".gif": "image/gif",
+ ".webp": "image/webp",
+ ".bmp": "image/bmp",
+ ".svg": "image/svg+xml",
+ ".mp3": "audio/mpeg",
+ ".wav": "audio/wav",
+ ".ogg": "audio/ogg",
+ ".flac": "audio/flac",
+ };
+ const mimeType = mimeTypes[extension] || "application/octet-stream";
+ const data = await readFileAsBase64Stream(validPath);
+
+ const type = mimeType.startsWith("image/")
+ ? "image"
+ : mimeType.startsWith("audio/")
+ ? "audio"
+ // Fallback for other binary types, not officially supported by the spec but has been used for some time
+ : "blob";
+ const contentItem = { type: type as 'image' | 'audio' | 'blob', data, mimeType };
+ return {
+ content: [contentItem],
+ structuredContent: { content: [contentItem] }
+ } as unknown as CallToolResult;
+ }
+);
+
+server.registerTool(
+ "read_multiple_files",
+ {
+ title: "Read Multiple Files",
+ description:
+ "Read the contents of multiple files simultaneously. This is more " +
+ "efficient than reading files one by one when you need to analyze " +
+ "or compare multiple files. Each file's content is returned with its " +
+ "path as a reference. Failed reads for individual files won't stop " +
+ "the entire operation. Only works within allowed directories.",
+ inputSchema: {
+ paths: z.array(z.string())
+ .min(1)
+ .describe("Array of file paths to read. Each path must be a string pointing to a valid file within allowed directories.")
+ },
+ outputSchema: { content: z.string() },
+ annotations: { readOnlyHint: true }
+ },
+ async (args: z.infer) => {
+ const results = await Promise.all(
+ args.paths.map(async (filePath: string) => {
+ try {
+ const validPath = await validatePath(filePath);
+ const content = await readFileContent(validPath);
+ return `${filePath}:\n${content}\n`;
+ } catch (error) {
+ const errorMessage = error instanceof Error ? error.message : String(error);
+ return `${filePath}: Error - ${errorMessage}`;
+ }
+ }),
+ );
+ const text = results.join("\n---\n");
+ return {
+ content: [{ type: "text" as const, text }],
+ structuredContent: { content: text }
+ };
+ }
+);
+
+server.registerTool(
+ "write_file",
+ {
+ title: "Write File",
+ description:
+ "Create a new file or completely overwrite an existing file with new content. " +
+ "Use with caution as it will overwrite existing files without warning. " +
+ "Handles text content with proper encoding. Only works within allowed directories.",
+ inputSchema: {
+ path: z.string(),
+ content: z.string()
+ },
+ outputSchema: { content: z.string() },
+ annotations: { readOnlyHint: false, idempotentHint: true, destructiveHint: true }
+ },
+ async (args: z.infer) => {
+ const validPath = await validatePath(args.path);
+ await writeFileContent(validPath, args.content);
+ const text = `Successfully wrote to ${args.path}`;
+ return {
+ content: [{ type: "text" as const, text }],
+ structuredContent: { content: text }
+ };
+ }
+);
+
+server.registerTool(
+ "edit_file",
+ {
+ title: "Edit File",
+ description:
+ "Make line-based edits to a text file. Each edit replaces exact line sequences " +
+ "with new content. Returns a git-style diff showing the changes made. " +
+ "Only works within allowed directories.",
+ inputSchema: {
+ path: z.string(),
+ edits: z.array(z.object({
+ oldText: z.string().describe("Text to search for - must match exactly"),
+ newText: z.string().describe("Text to replace with")
+ })),
+ dryRun: z.boolean().default(false).describe("Preview changes using git-style diff format")
+ },
+ outputSchema: { content: z.string() },
+ annotations: { readOnlyHint: false, idempotentHint: false, destructiveHint: true }
+ },
+ async (args: z.infer) => {
+ const validPath = await validatePath(args.path);
+ const result = await applyFileEdits(validPath, args.edits, args.dryRun);
+ return {
+ content: [{ type: "text" as const, text: result }],
+ structuredContent: { content: result }
+ };
+ }
+);
+
+server.registerTool(
+ "create_directory",
+ {
+ title: "Create Directory",
+ description:
+ "Create a new directory or ensure a directory exists. Can create multiple " +
+ "nested directories in one operation. If the directory already exists, " +
+ "this operation will succeed silently. Perfect for setting up directory " +
+ "structures for projects or ensuring required paths exist. Only works within allowed directories.",
+ inputSchema: {
+ path: z.string()
+ },
+ outputSchema: { content: z.string() },
+ annotations: { readOnlyHint: false, idempotentHint: true, destructiveHint: false }
+ },
+ async (args: z.infer) => {
+ const validPath = await validatePath(args.path);
+ await fs.mkdir(validPath, { recursive: true });
+ const text = `Successfully created directory ${args.path}`;
+ return {
+ content: [{ type: "text" as const, text }],
+ structuredContent: { content: text }
+ };
+ }
+);
+
+server.registerTool(
+ "list_directory",
+ {
+ title: "List Directory",
+ description:
+ "Get a detailed listing of all files and directories in a specified path. " +
+ "Results clearly distinguish between files and directories with [FILE] and [DIR] " +
+ "prefixes. This tool is essential for understanding directory structure and " +
+ "finding specific files within a directory. Only works within allowed directories.",
+ inputSchema: {
+ path: z.string()
+ },
+ outputSchema: { content: z.string() },
+ annotations: { readOnlyHint: true }
+ },
+ async (args: z.infer) => {
+ const validPath = await validatePath(args.path);
+ const entries = await fs.readdir(validPath, { withFileTypes: true });
+ const formatted = entries
+ .map((entry) => `${entry.isDirectory() ? "[DIR]" : "[FILE]"} ${entry.name}`)
+ .join("\n");
+ return {
+ content: [{ type: "text" as const, text: formatted }],
+ structuredContent: { content: formatted }
+ };
+ }
+);
+
+server.registerTool(
+ "list_directory_with_sizes",
+ {
+ title: "List Directory with Sizes",
+ description:
+ "Get a detailed listing of all files and directories in a specified path, including sizes. " +
+ "Results clearly distinguish between files and directories with [FILE] and [DIR] " +
+ "prefixes. This tool is useful for understanding directory structure and " +
+ "finding specific files within a directory. Only works within allowed directories.",
+ inputSchema: {
+ path: z.string(),
+ sortBy: z.enum(["name", "size"]).optional().default("name").describe("Sort entries by name or size")
+ },
+ outputSchema: { content: z.string() },
+ annotations: { readOnlyHint: true }
+ },
+ async (args: z.infer) => {
+ const validPath = await validatePath(args.path);
+ const entries = await fs.readdir(validPath, { withFileTypes: true });
+
+ // Get detailed information for each entry
+ const detailedEntries = await Promise.all(
+ entries.map(async (entry) => {
+ const entryPath = path.join(validPath, entry.name);
+ try {
+ const stats = await fs.stat(entryPath);
+ return {
+ name: entry.name,
+ isDirectory: entry.isDirectory(),
+ size: stats.size,
+ mtime: stats.mtime
+ };
+ } catch (error) {
+ return {
+ name: entry.name,
+ isDirectory: entry.isDirectory(),
+ size: 0,
+ mtime: new Date(0)
+ };
+ }
+ })
+ );
+
+ // Sort entries based on sortBy parameter
+ const sortedEntries = [...detailedEntries].sort((a, b) => {
+ if (args.sortBy === 'size') {
+ return b.size - a.size; // Descending by size
+ }
+ // Default sort by name
+ return a.name.localeCompare(b.name);
+ });
+
+ // Format the output
+ const formattedEntries = sortedEntries.map(entry =>
+ `${entry.isDirectory ? "[DIR]" : "[FILE]"} ${entry.name.padEnd(30)} ${
+ entry.isDirectory ? "" : formatSize(entry.size).padStart(10)
+ }`
+ );
+
+ // Add summary
+ const totalFiles = detailedEntries.filter(e => !e.isDirectory).length;
+ const totalDirs = detailedEntries.filter(e => e.isDirectory).length;
+ const totalSize = detailedEntries.reduce((sum, entry) => sum + (entry.isDirectory ? 0 : entry.size), 0);
+
+ const summary = [
+ "",
+ `Total: ${totalFiles} files, ${totalDirs} directories`,
+ `Combined size: ${formatSize(totalSize)}`
+ ];
+
+ const text = [...formattedEntries, ...summary].join("\n");
+ const contentBlock = { type: "text" as const, text };
+ return {
+ content: [contentBlock],
+ structuredContent: { content: text }
+ };
+ }
+);
+
+server.registerTool(
+ "directory_tree",
+ {
+ title: "Directory Tree",
+ description:
+ "Get a recursive tree view of files and directories as a JSON structure. " +
+ "Each entry includes 'name', 'type' (file/directory), and 'children' for directories. " +
+ "Files have no children array, while directories always have a children array (which may be empty). " +
+ "The output is formatted with 2-space indentation for readability. Only works within allowed directories.",
+ inputSchema: {
+ path: z.string(),
+ excludePatterns: z.array(z.string()).optional().default([])
+ },
+ outputSchema: { content: z.string() },
+ annotations: { readOnlyHint: true }
+ },
+ async (args: z.infer) => {
+ interface TreeEntry {
+ name: string;
+ type: 'file' | 'directory';
+ children?: TreeEntry[];
+ }
+ const rootPath = args.path;
+
+ async function buildTree(currentPath: string, excludePatterns: string[] = []): Promise {
+ const validPath = await validatePath(currentPath);
+ const entries = await fs.readdir(validPath, { withFileTypes: true });
+ const result: TreeEntry[] = [];
+
+ for (const entry of entries) {
+ const relativePath = path.relative(rootPath, path.join(currentPath, entry.name));
+ const shouldExclude = excludePatterns.some(pattern => {
+ if (pattern.includes('*')) {
+ return minimatch(relativePath, pattern, { dot: true });
+ }
+ // For files: match exact name or as part of path
+ // For directories: match as directory path
+ return minimatch(relativePath, pattern, { dot: true }) ||
+ minimatch(relativePath, `**/${pattern}`, { dot: true }) ||
+ minimatch(relativePath, `**/${pattern}/**`, { dot: true });
+ });
+ if (shouldExclude)
+ continue;
+
+ const entryData: TreeEntry = {
+ name: entry.name,
+ type: entry.isDirectory() ? 'directory' : 'file'
+ };
+
+ if (entry.isDirectory()) {
+ const subPath = path.join(currentPath, entry.name);
+ entryData.children = await buildTree(subPath, excludePatterns);
+ }
+
+ result.push(entryData);
+ }
+
+ return result;
+ }
+
+ const treeData = await buildTree(rootPath, args.excludePatterns);
+ const text = JSON.stringify(treeData, null, 2);
+ const contentBlock = { type: "text" as const, text };
+ return {
+ content: [contentBlock],
+ structuredContent: { content: text }
+ };
+ }
+);
+
+server.registerTool(
+ "move_file",
+ {
+ title: "Move File",
+ description:
+ "Move or rename files and directories. Can move files between directories " +
+ "and rename them in a single operation. If the destination exists, the " +
+ "operation will fail. Works across different directories and can be used " +
+ "for simple renaming within the same directory. Both source and destination must be within allowed directories.",
+ inputSchema: {
+ source: z.string(),
+ destination: z.string()
+ },
+ outputSchema: { content: z.string() },
+ annotations: { readOnlyHint: false, idempotentHint: false, destructiveHint: true }
+ },
+ async (args: z.infer) => {
+ const validSourcePath = await validatePath(args.source);
+ const validDestPath = await validatePath(args.destination);
+ await fs.rename(validSourcePath, validDestPath);
+ const text = `Successfully moved ${args.source} to ${args.destination}`;
+ const contentBlock = { type: "text" as const, text };
+ return {
+ content: [contentBlock],
+ structuredContent: { content: text }
+ };
+ }
+);
+
+server.registerTool(
+ "search_files",
+ {
+ title: "Search Files",
+ description:
+ "Recursively search for files and directories matching a pattern. " +
+ "The patterns should be glob-style patterns that match paths relative to the working directory. " +
+ "Use pattern like '*.ext' to match files in current directory, and '**/*.ext' to match files in all subdirectories. " +
+ "Returns full paths to all matching items. Great for finding files when you don't know their exact location. " +
+ "Only searches within allowed directories.",
+ inputSchema: {
+ path: z.string(),
+ pattern: z.string(),
+ excludePatterns: z.array(z.string()).optional().default([])
+ },
+ outputSchema: { content: z.string() },
+ annotations: { readOnlyHint: true }
+ },
+ async (args: z.infer) => {
+ const validPath = await validatePath(args.path);
+ const results = await searchFilesWithValidation(validPath, args.pattern, allowedDirectories, { excludePatterns: args.excludePatterns });
+ const text = results.length > 0 ? results.join("\n") : "No matches found";
+ return {
+ content: [{ type: "text" as const, text }],
+ structuredContent: { content: text }
+ };
+ }
+);
+
+server.registerTool(
+ "get_file_info",
+ {
+ title: "Get File Info",
+ description:
+ "Retrieve detailed metadata about a file or directory. Returns comprehensive " +
+ "information including size, creation time, last modified time, permissions, " +
+ "and type. This tool is perfect for understanding file characteristics " +
+ "without reading the actual content. Only works within allowed directories.",
+ inputSchema: {
+ path: z.string()
+ },
+ outputSchema: { content: z.string() },
+ annotations: { readOnlyHint: true }
+ },
+ async (args: z.infer) => {
+ const validPath = await validatePath(args.path);
+ const info = await getFileStats(validPath);
+ const text = Object.entries(info)
+ .map(([key, value]) => `${key}: ${value}`)
+ .join("\n");
+ return {
+ content: [{ type: "text" as const, text }],
+ structuredContent: { content: text }
+ };
+ }
+);
+
+server.registerTool(
+ "list_allowed_directories",
+ {
+ title: "List Allowed Directories",
+ description:
+ "Returns the list of directories that this server is allowed to access. " +
+ "Subdirectories within these allowed directories are also accessible. " +
+ "Use this to understand which directories and their nested paths are available " +
+ "before trying to access files.",
+ inputSchema: {},
+ outputSchema: { content: z.string() },
+ annotations: { readOnlyHint: true }
+ },
+ async () => {
+ const text = `Allowed directories:\n${allowedDirectories.join('\n')}`;
+ return {
+ content: [{ type: "text" as const, text }],
+ structuredContent: { content: text }
+ };
+ }
+);
+
+// Updates allowed directories based on MCP client roots
+async function updateAllowedDirectoriesFromRoots(requestedRoots: Root[]) {
+ const validatedRootDirs = await getValidRootDirectories(requestedRoots);
+ if (validatedRootDirs.length > 0) {
+ allowedDirectories = [...validatedRootDirs];
+ setAllowedDirectories(allowedDirectories); // Update the global state in lib.ts
+ console.error(`Updated allowed directories from MCP roots: ${validatedRootDirs.length} valid directories`);
+ } else {
+ console.error("No valid root directories provided by client");
+ }
+}
+
+// Handles dynamic roots updates during runtime, when client sends "roots/list_changed" notification, server fetches the updated roots and replaces all allowed directories with the new roots.
+server.server.setNotificationHandler(RootsListChangedNotificationSchema, async () => {
+ try {
+ // Request the updated roots list from the client
+ const response = await server.server.listRoots();
+ if (response && 'roots' in response) {
+ await updateAllowedDirectoriesFromRoots(response.roots);
+ }
+ } catch (error) {
+ console.error("Failed to request roots from client:", error instanceof Error ? error.message : String(error));
+ }
+});
+
+// Handles post-initialization setup, specifically checking for and fetching MCP roots.
+server.server.oninitialized = async () => {
+ const clientCapabilities = server.server.getClientCapabilities();
+
+ if (clientCapabilities?.roots) {
+ try {
+ const response = await server.server.listRoots();
+ if (response && 'roots' in response) {
+ await updateAllowedDirectoriesFromRoots(response.roots);
+ } else {
+ console.error("Client returned no roots set, keeping current settings");
+ }
+ } catch (error) {
+ console.error("Failed to request initial roots from client:", error instanceof Error ? error.message : String(error));
+ }
+ } else {
+ if (allowedDirectories.length > 0) {
+ console.error("Client does not support MCP Roots, using allowed directories set from server args:", allowedDirectories);
+ }else{
+ throw new Error(`Server cannot operate: No allowed directories available. Server was started without command-line directories and client either does not support MCP roots protocol or provided empty roots. Please either: 1) Start server with directory arguments, or 2) Use a client that supports MCP roots protocol and provides valid root directories.`);
+ }
+ }
+};
+
+// Start server
+async function runServer() {
+ const transport = new StdioServerTransport();
+ await server.connect(transport);
+ console.error("Secure MCP Filesystem Server running on stdio");
+ if (allowedDirectories.length === 0) {
+ console.error("Started without allowed directories - waiting for client to provide roots via MCP protocol");
+ }
+}
+
+runServer().catch((error) => {
+ console.error("Fatal error running server:", error);
+ process.exit(1);
+});
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/lib.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/lib.ts
new file mode 100644
index 00000000..17e4654c
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/lib.ts
@@ -0,0 +1,415 @@
+import fs from "fs/promises";
+import path from "path";
+import os from 'os';
+import { randomBytes } from 'crypto';
+import { diffLines, createTwoFilesPatch } from 'diff';
+import { minimatch } from 'minimatch';
+import { normalizePath, expandHome } from './path-utils.js';
+import { isPathWithinAllowedDirectories } from './path-validation.js';
+
+// Global allowed directories - set by the main module
+let allowedDirectories: string[] = [];
+
+// Function to set allowed directories from the main module
+export function setAllowedDirectories(directories: string[]): void {
+ allowedDirectories = [...directories];
+}
+
+// Function to get current allowed directories
+export function getAllowedDirectories(): string[] {
+ return [...allowedDirectories];
+}
+
+// Type definitions
+interface FileInfo {
+ size: number;
+ created: Date;
+ modified: Date;
+ accessed: Date;
+ isDirectory: boolean;
+ isFile: boolean;
+ permissions: string;
+}
+
+export interface SearchOptions {
+ excludePatterns?: string[];
+}
+
+export interface SearchResult {
+ path: string;
+ isDirectory: boolean;
+}
+
+// Pure Utility Functions
+export function formatSize(bytes: number): string {
+ const units = ['B', 'KB', 'MB', 'GB', 'TB'];
+ if (bytes === 0) return '0 B';
+
+ const i = Math.floor(Math.log(bytes) / Math.log(1024));
+
+ if (i < 0 || i === 0) return `${bytes} ${units[0]}`;
+
+ const unitIndex = Math.min(i, units.length - 1);
+ return `${(bytes / Math.pow(1024, unitIndex)).toFixed(2)} ${units[unitIndex]}`;
+}
+
+export function normalizeLineEndings(text: string): string {
+ return text.replace(/\r\n/g, '\n');
+}
+
+export function createUnifiedDiff(originalContent: string, newContent: string, filepath: string = 'file'): string {
+ // Ensure consistent line endings for diff
+ const normalizedOriginal = normalizeLineEndings(originalContent);
+ const normalizedNew = normalizeLineEndings(newContent);
+
+ return createTwoFilesPatch(
+ filepath,
+ filepath,
+ normalizedOriginal,
+ normalizedNew,
+ 'original',
+ 'modified'
+ );
+}
+
+// Helper function to resolve relative paths against allowed directories
+function resolveRelativePathAgainstAllowedDirectories(relativePath: string): string {
+ if (allowedDirectories.length === 0) {
+ // Fallback to process.cwd() if no allowed directories are set
+ return path.resolve(process.cwd(), relativePath);
+ }
+
+ // Try to resolve relative path against each allowed directory
+ for (const allowedDir of allowedDirectories) {
+ const candidate = path.resolve(allowedDir, relativePath);
+ const normalizedCandidate = normalizePath(candidate);
+
+ // Check if the resulting path lies within any allowed directory
+ if (isPathWithinAllowedDirectories(normalizedCandidate, allowedDirectories)) {
+ return candidate;
+ }
+ }
+
+ // If no valid resolution found, use the first allowed directory as base
+ // This provides a consistent fallback behavior
+ return path.resolve(allowedDirectories[0], relativePath);
+}
+
+// Security & Validation Functions
+export async function validatePath(requestedPath: string): Promise {
+ const expandedPath = expandHome(requestedPath);
+ const absolute = path.isAbsolute(expandedPath)
+ ? path.resolve(expandedPath)
+ : resolveRelativePathAgainstAllowedDirectories(expandedPath);
+
+ const normalizedRequested = normalizePath(absolute);
+
+ // Security: Check if path is within allowed directories before any file operations
+ const isAllowed = isPathWithinAllowedDirectories(normalizedRequested, allowedDirectories);
+ if (!isAllowed) {
+ throw new Error(`Access denied - path outside allowed directories: ${absolute} not in ${allowedDirectories.join(', ')}`);
+ }
+
+ // Security: Handle symlinks by checking their real path to prevent symlink attacks
+ // This prevents attackers from creating symlinks that point outside allowed directories
+ try {
+ const realPath = await fs.realpath(absolute);
+ const normalizedReal = normalizePath(realPath);
+ if (!isPathWithinAllowedDirectories(normalizedReal, allowedDirectories)) {
+ throw new Error(`Access denied - symlink target outside allowed directories: ${realPath} not in ${allowedDirectories.join(', ')}`);
+ }
+ return realPath;
+ } catch (error) {
+ // Security: For new files that don't exist yet, verify parent directory
+ // This ensures we can't create files in unauthorized locations
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
+ const parentDir = path.dirname(absolute);
+ try {
+ const realParentPath = await fs.realpath(parentDir);
+ const normalizedParent = normalizePath(realParentPath);
+ if (!isPathWithinAllowedDirectories(normalizedParent, allowedDirectories)) {
+ throw new Error(`Access denied - parent directory outside allowed directories: ${realParentPath} not in ${allowedDirectories.join(', ')}`);
+ }
+ return absolute;
+ } catch {
+ throw new Error(`Parent directory does not exist: ${parentDir}`);
+ }
+ }
+ throw error;
+ }
+}
+
+
+// File Operations
+export async function getFileStats(filePath: string): Promise {
+ const stats = await fs.stat(filePath);
+ return {
+ size: stats.size,
+ created: stats.birthtime,
+ modified: stats.mtime,
+ accessed: stats.atime,
+ isDirectory: stats.isDirectory(),
+ isFile: stats.isFile(),
+ permissions: stats.mode.toString(8).slice(-3),
+ };
+}
+
+export async function readFileContent(filePath: string, encoding: string = 'utf-8'): Promise {
+ return await fs.readFile(filePath, encoding as BufferEncoding);
+}
+
+export async function writeFileContent(filePath: string, content: string): Promise {
+ try {
+ // Security: 'wx' flag ensures exclusive creation - fails if file/symlink exists,
+ // preventing writes through pre-existing symlinks
+ await fs.writeFile(filePath, content, { encoding: "utf-8", flag: 'wx' });
+ } catch (error) {
+ if ((error as NodeJS.ErrnoException).code === 'EEXIST') {
+ // Security: Use atomic rename to prevent race conditions where symlinks
+ // could be created between validation and write. Rename operations
+ // replace the target file atomically and don't follow symlinks.
+ const tempPath = `${filePath}.${randomBytes(16).toString('hex')}.tmp`;
+ try {
+ await fs.writeFile(tempPath, content, 'utf-8');
+ await fs.rename(tempPath, filePath);
+ } catch (renameError) {
+ try {
+ await fs.unlink(tempPath);
+ } catch {}
+ throw renameError;
+ }
+ } else {
+ throw error;
+ }
+ }
+}
+
+
+// File Editing Functions
+interface FileEdit {
+ oldText: string;
+ newText: string;
+}
+
+export async function applyFileEdits(
+ filePath: string,
+ edits: FileEdit[],
+ dryRun: boolean = false
+): Promise {
+ // Read file content and normalize line endings
+ const content = normalizeLineEndings(await fs.readFile(filePath, 'utf-8'));
+
+ // Apply edits sequentially
+ let modifiedContent = content;
+ for (const edit of edits) {
+ const normalizedOld = normalizeLineEndings(edit.oldText);
+ const normalizedNew = normalizeLineEndings(edit.newText);
+
+ // If exact match exists, use it
+ if (modifiedContent.includes(normalizedOld)) {
+ modifiedContent = modifiedContent.replace(normalizedOld, normalizedNew);
+ continue;
+ }
+
+ // Otherwise, try line-by-line matching with flexibility for whitespace
+ const oldLines = normalizedOld.split('\n');
+ const contentLines = modifiedContent.split('\n');
+ let matchFound = false;
+
+ for (let i = 0; i <= contentLines.length - oldLines.length; i++) {
+ const potentialMatch = contentLines.slice(i, i + oldLines.length);
+
+ // Compare lines with normalized whitespace
+ const isMatch = oldLines.every((oldLine, j) => {
+ const contentLine = potentialMatch[j];
+ return oldLine.trim() === contentLine.trim();
+ });
+
+ if (isMatch) {
+ // Preserve original indentation of first line
+ const originalIndent = contentLines[i].match(/^\s*/)?.[0] || '';
+ const newLines = normalizedNew.split('\n').map((line, j) => {
+ if (j === 0) return originalIndent + line.trimStart();
+ // For subsequent lines, try to preserve relative indentation
+ const oldIndent = oldLines[j]?.match(/^\s*/)?.[0] || '';
+ const newIndent = line.match(/^\s*/)?.[0] || '';
+ if (oldIndent && newIndent) {
+ const relativeIndent = newIndent.length - oldIndent.length;
+ return originalIndent + ' '.repeat(Math.max(0, relativeIndent)) + line.trimStart();
+ }
+ return line;
+ });
+
+ contentLines.splice(i, oldLines.length, ...newLines);
+ modifiedContent = contentLines.join('\n');
+ matchFound = true;
+ break;
+ }
+ }
+
+ if (!matchFound) {
+ throw new Error(`Could not find exact match for edit:\n${edit.oldText}`);
+ }
+ }
+
+ // Create unified diff
+ const diff = createUnifiedDiff(content, modifiedContent, filePath);
+
+ // Format diff with appropriate number of backticks
+ let numBackticks = 3;
+ while (diff.includes('`'.repeat(numBackticks))) {
+ numBackticks++;
+ }
+ const formattedDiff = `${'`'.repeat(numBackticks)}diff\n${diff}${'`'.repeat(numBackticks)}\n\n`;
+
+ if (!dryRun) {
+ // Security: Use atomic rename to prevent race conditions where symlinks
+ // could be created between validation and write. Rename operations
+ // replace the target file atomically and don't follow symlinks.
+ const tempPath = `${filePath}.${randomBytes(16).toString('hex')}.tmp`;
+ try {
+ await fs.writeFile(tempPath, modifiedContent, 'utf-8');
+ await fs.rename(tempPath, filePath);
+ } catch (error) {
+ try {
+ await fs.unlink(tempPath);
+ } catch {}
+ throw error;
+ }
+ }
+
+ return formattedDiff;
+}
+
+// Memory-efficient implementation to get the last N lines of a file
+export async function tailFile(filePath: string, numLines: number): Promise {
+ const CHUNK_SIZE = 1024; // Read 1KB at a time
+ const stats = await fs.stat(filePath);
+ const fileSize = stats.size;
+
+ if (fileSize === 0) return '';
+
+ // Open file for reading
+ const fileHandle = await fs.open(filePath, 'r');
+ try {
+ const lines: string[] = [];
+ let position = fileSize;
+ let chunk = Buffer.alloc(CHUNK_SIZE);
+ let linesFound = 0;
+ let remainingText = '';
+
+ // Read chunks from the end of the file until we have enough lines
+ while (position > 0 && linesFound < numLines) {
+ const size = Math.min(CHUNK_SIZE, position);
+ position -= size;
+
+ const { bytesRead } = await fileHandle.read(chunk, 0, size, position);
+ if (!bytesRead) break;
+
+ // Get the chunk as a string and prepend any remaining text from previous iteration
+ const readData = chunk.slice(0, bytesRead).toString('utf-8');
+ const chunkText = readData + remainingText;
+
+ // Split by newlines and count
+ const chunkLines = normalizeLineEndings(chunkText).split('\n');
+
+ // If this isn't the end of the file, the first line is likely incomplete
+ // Save it to prepend to the next chunk
+ if (position > 0) {
+ remainingText = chunkLines[0];
+ chunkLines.shift(); // Remove the first (incomplete) line
+ }
+
+ // Add lines to our result (up to the number we need)
+ for (let i = chunkLines.length - 1; i >= 0 && linesFound < numLines; i--) {
+ lines.unshift(chunkLines[i]);
+ linesFound++;
+ }
+ }
+
+ return lines.join('\n');
+ } finally {
+ await fileHandle.close();
+ }
+}
+
+// New function to get the first N lines of a file
+export async function headFile(filePath: string, numLines: number): Promise {
+ const fileHandle = await fs.open(filePath, 'r');
+ try {
+ const lines: string[] = [];
+ let buffer = '';
+ let bytesRead = 0;
+ const chunk = Buffer.alloc(1024); // 1KB buffer
+
+ // Read chunks and count lines until we have enough or reach EOF
+ while (lines.length < numLines) {
+ const result = await fileHandle.read(chunk, 0, chunk.length, bytesRead);
+ if (result.bytesRead === 0) break; // End of file
+ bytesRead += result.bytesRead;
+ buffer += chunk.slice(0, result.bytesRead).toString('utf-8');
+
+ const newLineIndex = buffer.lastIndexOf('\n');
+ if (newLineIndex !== -1) {
+ const completeLines = buffer.slice(0, newLineIndex).split('\n');
+ buffer = buffer.slice(newLineIndex + 1);
+ for (const line of completeLines) {
+ lines.push(line);
+ if (lines.length >= numLines) break;
+ }
+ }
+ }
+
+ // If there is leftover content and we still need lines, add it
+ if (buffer.length > 0 && lines.length < numLines) {
+ lines.push(buffer);
+ }
+
+ return lines.join('\n');
+ } finally {
+ await fileHandle.close();
+ }
+}
+
+export async function searchFilesWithValidation(
+ rootPath: string,
+ pattern: string,
+ allowedDirectories: string[],
+ options: SearchOptions = {}
+): Promise {
+ const { excludePatterns = [] } = options;
+ const results: string[] = [];
+
+ async function search(currentPath: string) {
+ const entries = await fs.readdir(currentPath, { withFileTypes: true });
+
+ for (const entry of entries) {
+ const fullPath = path.join(currentPath, entry.name);
+
+ try {
+ await validatePath(fullPath);
+
+ const relativePath = path.relative(rootPath, fullPath);
+ const shouldExclude = excludePatterns.some(excludePattern =>
+ minimatch(relativePath, excludePattern, { dot: true })
+ );
+
+ if (shouldExclude) continue;
+
+ // Use glob matching for the search pattern
+ if (minimatch(relativePath, pattern, { dot: true })) {
+ results.push(fullPath);
+ }
+
+ if (entry.isDirectory()) {
+ await search(fullPath);
+ }
+ } catch {
+ continue;
+ }
+ }
+ }
+
+ await search(rootPath);
+ return results;
+}
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/.package-lock.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/.package-lock.json
new file mode 100644
index 00000000..da1e0b9e
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/.package-lock.json
@@ -0,0 +1,2798 @@
+{
+ "name": "@modelcontextprotocol/server-filesystem",
+ "version": "0.6.3",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "node_modules/@ampproject/remapping": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
+ "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz",
+ "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.0"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
+ "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@bcoe/v8-coverage": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz",
+ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
+ "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@hono/node-server": {
+ "version": "1.19.11",
+ "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz",
+ "integrity": "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.14.1"
+ },
+ "peerDependencies": {
+ "hono": "^4"
+ }
+ },
+ "node_modules/@isaacs/cliui": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
+ "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^5.1.2",
+ "string-width-cjs": "npm:string-width@^4.2.0",
+ "strip-ansi": "^7.0.1",
+ "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+ "wrap-ansi": "^8.1.0",
+ "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@istanbuljs/schema": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
+ "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@modelcontextprotocol/sdk": {
+ "version": "1.27.1",
+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.27.1.tgz",
+ "integrity": "sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA==",
+ "license": "MIT",
+ "dependencies": {
+ "@hono/node-server": "^1.19.9",
+ "ajv": "^8.17.1",
+ "ajv-formats": "^3.0.1",
+ "content-type": "^1.0.5",
+ "cors": "^2.8.5",
+ "cross-spawn": "^7.0.5",
+ "eventsource": "^3.0.2",
+ "eventsource-parser": "^3.0.0",
+ "express": "^5.2.1",
+ "express-rate-limit": "^8.2.1",
+ "hono": "^4.11.4",
+ "jose": "^6.1.3",
+ "json-schema-typed": "^8.0.2",
+ "pkce-challenge": "^5.0.0",
+ "raw-body": "^3.0.0",
+ "zod": "^3.25 || ^4.0",
+ "zod-to-json-schema": "^3.25.1"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@cfworker/json-schema": "^4.1.1",
+ "zod": "^3.25 || ^4.0"
+ },
+ "peerDependenciesMeta": {
+ "@cfworker/json-schema": {
+ "optional": true
+ },
+ "zod": {
+ "optional": false
+ }
+ }
+ },
+ "node_modules/@pkgjs/parseargs": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
+ "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz",
+ "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz",
+ "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@types/diff": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/@types/diff/-/diff-5.2.3.tgz",
+ "integrity": "sha512-K0Oqlrq3kQMaO2RhfrNQX5trmt+XLyom88zS0u84nnIcLvFnRUMRRHmrGny5GSM+kNO9IZLARsdQHDzkhAgmrQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/minimatch": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz",
+ "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "22.19.15",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz",
+ "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
+ "node_modules/@vitest/coverage-v8": {
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.9.tgz",
+ "integrity": "sha512-Z2cOr0ksM00MpEfyVE8KXIYPEcBFxdbLSs56L8PO0QQMxt/6bDj45uQfxoc96v05KW3clk7vvgP0qfDit9DmfQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@ampproject/remapping": "^2.3.0",
+ "@bcoe/v8-coverage": "^0.2.3",
+ "debug": "^4.3.7",
+ "istanbul-lib-coverage": "^3.2.2",
+ "istanbul-lib-report": "^3.0.1",
+ "istanbul-lib-source-maps": "^5.0.6",
+ "istanbul-reports": "^3.1.7",
+ "magic-string": "^0.30.12",
+ "magicast": "^0.3.5",
+ "std-env": "^3.8.0",
+ "test-exclude": "^7.0.1",
+ "tinyrainbow": "^1.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@vitest/browser": "2.1.9",
+ "vitest": "2.1.9"
+ },
+ "peerDependenciesMeta": {
+ "@vitest/browser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/expect": {
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz",
+ "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/spy": "2.1.9",
+ "@vitest/utils": "2.1.9",
+ "chai": "^5.1.2",
+ "tinyrainbow": "^1.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/mocker": {
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz",
+ "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/spy": "2.1.9",
+ "estree-walker": "^3.0.3",
+ "magic-string": "^0.30.12"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "msw": "^2.4.9",
+ "vite": "^5.0.0"
+ },
+ "peerDependenciesMeta": {
+ "msw": {
+ "optional": true
+ },
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/pretty-format": {
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz",
+ "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyrainbow": "^1.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/runner": {
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz",
+ "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/utils": "2.1.9",
+ "pathe": "^1.1.2"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/snapshot": {
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz",
+ "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "2.1.9",
+ "magic-string": "^0.30.12",
+ "pathe": "^1.1.2"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/spy": {
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz",
+ "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyspy": "^3.0.2"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/utils": {
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz",
+ "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "2.1.9",
+ "loupe": "^3.1.2",
+ "tinyrainbow": "^1.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/accepts": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
+ "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "^3.0.0",
+ "negotiator": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "8.18.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
+ "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ajv-formats": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz",
+ "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^8.0.0"
+ },
+ "peerDependencies": {
+ "ajv": "^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "ajv": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
+ "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "6.2.3",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
+ "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/assertion-error": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
+ "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "license": "MIT",
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/body-parser": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz",
+ "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "^3.1.2",
+ "content-type": "^1.0.5",
+ "debug": "^4.4.3",
+ "http-errors": "^2.0.0",
+ "iconv-lite": "^0.7.0",
+ "on-finished": "^2.4.1",
+ "qs": "^6.14.1",
+ "raw-body": "^3.0.1",
+ "type-is": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz",
+ "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==",
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/cac": {
+ "version": "6.7.14",
+ "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
+ "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/chai": {
+ "version": "5.3.3",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz",
+ "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "assertion-error": "^2.0.1",
+ "check-error": "^2.1.1",
+ "deep-eql": "^5.0.1",
+ "loupe": "^3.1.0",
+ "pathval": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/check-error": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz",
+ "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 16"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "license": "MIT"
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/content-disposition": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
+ "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
+ "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.6.0"
+ }
+ },
+ "node_modules/cors": {
+ "version": "2.8.6",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
+ "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
+ "license": "MIT",
+ "dependencies": {
+ "object-assign": "^4",
+ "vary": "^1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/deep-eql": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
+ "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/diff": {
+ "version": "8.0.3",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.3.tgz",
+ "integrity": "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.3.1"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/eastasianwidth": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
+ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
+ "license": "MIT"
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+ "license": "MIT"
+ },
+ "node_modules/emoji-regex": {
+ "version": "9.2.2",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+ "license": "MIT"
+ },
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-module-lexer": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
+ "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
+ "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.21.5",
+ "@esbuild/android-arm": "0.21.5",
+ "@esbuild/android-arm64": "0.21.5",
+ "@esbuild/android-x64": "0.21.5",
+ "@esbuild/darwin-arm64": "0.21.5",
+ "@esbuild/darwin-x64": "0.21.5",
+ "@esbuild/freebsd-arm64": "0.21.5",
+ "@esbuild/freebsd-x64": "0.21.5",
+ "@esbuild/linux-arm": "0.21.5",
+ "@esbuild/linux-arm64": "0.21.5",
+ "@esbuild/linux-ia32": "0.21.5",
+ "@esbuild/linux-loong64": "0.21.5",
+ "@esbuild/linux-mips64el": "0.21.5",
+ "@esbuild/linux-ppc64": "0.21.5",
+ "@esbuild/linux-riscv64": "0.21.5",
+ "@esbuild/linux-s390x": "0.21.5",
+ "@esbuild/linux-x64": "0.21.5",
+ "@esbuild/netbsd-x64": "0.21.5",
+ "@esbuild/openbsd-x64": "0.21.5",
+ "@esbuild/sunos-x64": "0.21.5",
+ "@esbuild/win32-arm64": "0.21.5",
+ "@esbuild/win32-ia32": "0.21.5",
+ "@esbuild/win32-x64": "0.21.5"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "license": "MIT"
+ },
+ "node_modules/estree-walker": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0"
+ }
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/eventsource": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz",
+ "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==",
+ "license": "MIT",
+ "dependencies": {
+ "eventsource-parser": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/eventsource-parser": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz",
+ "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/expect-type": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
+ "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/express": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
+ "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "^2.0.0",
+ "body-parser": "^2.2.1",
+ "content-disposition": "^1.0.0",
+ "content-type": "^1.0.5",
+ "cookie": "^0.7.1",
+ "cookie-signature": "^1.2.1",
+ "debug": "^4.4.0",
+ "depd": "^2.0.0",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "etag": "^1.8.1",
+ "finalhandler": "^2.1.0",
+ "fresh": "^2.0.0",
+ "http-errors": "^2.0.0",
+ "merge-descriptors": "^2.0.0",
+ "mime-types": "^3.0.0",
+ "on-finished": "^2.4.1",
+ "once": "^1.4.0",
+ "parseurl": "^1.3.3",
+ "proxy-addr": "^2.0.7",
+ "qs": "^6.14.0",
+ "range-parser": "^1.2.1",
+ "router": "^2.2.0",
+ "send": "^1.1.0",
+ "serve-static": "^2.2.0",
+ "statuses": "^2.0.1",
+ "type-is": "^2.0.1",
+ "vary": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/express-rate-limit": {
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.0.tgz",
+ "integrity": "sha512-KJzBawY6fB9FiZGdE/0aftepZ91YlaGIrV8vgblRM3J8X+dHx/aiowJWwkx6LIGyuqGiANsjSwwrbb8mifOJ4Q==",
+ "license": "MIT",
+ "dependencies": {
+ "ip-address": "10.1.0"
+ },
+ "engines": {
+ "node": ">= 16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/express-rate-limit"
+ },
+ "peerDependencies": {
+ "express": ">= 4.11"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "license": "MIT"
+ },
+ "node_modules/fast-uri": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
+ "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/finalhandler": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
+ "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.0",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "on-finished": "^2.4.1",
+ "parseurl": "^1.3.3",
+ "statuses": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/foreground-child": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
+ "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
+ "license": "ISC",
+ "dependencies": {
+ "cross-spawn": "^7.0.6",
+ "signal-exit": "^4.0.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
+ "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/glob": {
+ "version": "10.5.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
+ "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
+ "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
+ "license": "ISC",
+ "dependencies": {
+ "foreground-child": "^3.1.0",
+ "jackspeak": "^3.1.2",
+ "minimatch": "^9.0.4",
+ "minipass": "^7.1.2",
+ "package-json-from-dist": "^1.0.0",
+ "path-scurry": "^1.11.1"
+ },
+ "bin": {
+ "glob": "dist/esm/bin.mjs"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/glob/node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "license": "MIT"
+ },
+ "node_modules/glob/node_modules/brace-expansion": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/glob/node_modules/minimatch": {
+ "version": "9.0.9",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
+ "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/hono": {
+ "version": "4.12.5",
+ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.5.tgz",
+ "integrity": "sha512-3qq+FUBtlTHhtYxbxheZgY8NIFnkkC/MR8u5TTsr7YZ3wixryQ3cCwn3iZbg8p8B88iDBBAYSfZDS75t8MN7Vg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=16.9.0"
+ }
+ },
+ "node_modules/html-escaper": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
+ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "~2.0.0",
+ "inherits": "~2.0.4",
+ "setprototypeof": "~1.2.0",
+ "statuses": "~2.0.2",
+ "toidentifier": "~1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
+ "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+ "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
+ "node_modules/interpret": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz",
+ "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/ip-address": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
+ "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.16.1",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
+ "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-promise": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
+ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
+ "license": "MIT"
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "license": "ISC"
+ },
+ "node_modules/istanbul-lib-coverage": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
+ "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/istanbul-lib-report": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
+ "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "istanbul-lib-coverage": "^3.0.0",
+ "make-dir": "^4.0.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/istanbul-lib-source-maps": {
+ "version": "5.0.6",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz",
+ "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.23",
+ "debug": "^4.1.1",
+ "istanbul-lib-coverage": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/istanbul-reports": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz",
+ "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "html-escaper": "^2.0.0",
+ "istanbul-lib-report": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/jackspeak": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
+ "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "@isaacs/cliui": "^8.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ },
+ "optionalDependencies": {
+ "@pkgjs/parseargs": "^0.11.0"
+ }
+ },
+ "node_modules/jose": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.0.tgz",
+ "integrity": "sha512-xsfE1TcSCbUdo6U07tR0mvhg0flGxU8tPLbF03mirl2ukGQENhUg4ubGYQnhVH0b5stLlPM+WOqDkEl1R1y5sQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/panva"
+ }
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "license": "MIT"
+ },
+ "node_modules/json-schema-typed": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz",
+ "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==",
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/loupe": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz",
+ "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "license": "ISC"
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/magicast": {
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz",
+ "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.25.4",
+ "@babel/types": "^7.25.4",
+ "source-map-js": "^1.2.0"
+ }
+ },
+ "node_modules/make-dir": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
+ "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
+ "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
+ "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.54.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
+ "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "^1.54.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "10.2.4",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz",
+ "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "brace-expansion": "^5.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/minimist": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/minipass": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
+ "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/negotiator": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
+ "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "license": "ISC",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/package-json-from-dist": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
+ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
+ "license": "BlueOak-1.0.0"
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/path-scurry": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
+ "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "lru-cache": "^10.2.0",
+ "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
+ "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/pathe": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
+ "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/pathval": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz",
+ "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14.16"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/pkce-challenge": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz",
+ "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.8",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
+ "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "license": "MIT",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/qs": {
+ "version": "6.15.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz",
+ "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
+ "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.7.0",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/rechoir": {
+ "version": "0.6.2",
+ "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz",
+ "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==",
+ "dev": true,
+ "dependencies": {
+ "resolve": "^1.1.6"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/require-from-string": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/resolve": {
+ "version": "1.22.11",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
+ "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-core-module": "^2.16.1",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
+ "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.59.0",
+ "@rollup/rollup-android-arm64": "4.59.0",
+ "@rollup/rollup-darwin-arm64": "4.59.0",
+ "@rollup/rollup-darwin-x64": "4.59.0",
+ "@rollup/rollup-freebsd-arm64": "4.59.0",
+ "@rollup/rollup-freebsd-x64": "4.59.0",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.59.0",
+ "@rollup/rollup-linux-arm-musleabihf": "4.59.0",
+ "@rollup/rollup-linux-arm64-gnu": "4.59.0",
+ "@rollup/rollup-linux-arm64-musl": "4.59.0",
+ "@rollup/rollup-linux-loong64-gnu": "4.59.0",
+ "@rollup/rollup-linux-loong64-musl": "4.59.0",
+ "@rollup/rollup-linux-ppc64-gnu": "4.59.0",
+ "@rollup/rollup-linux-ppc64-musl": "4.59.0",
+ "@rollup/rollup-linux-riscv64-gnu": "4.59.0",
+ "@rollup/rollup-linux-riscv64-musl": "4.59.0",
+ "@rollup/rollup-linux-s390x-gnu": "4.59.0",
+ "@rollup/rollup-linux-x64-gnu": "4.59.0",
+ "@rollup/rollup-linux-x64-musl": "4.59.0",
+ "@rollup/rollup-openbsd-x64": "4.59.0",
+ "@rollup/rollup-openharmony-arm64": "4.59.0",
+ "@rollup/rollup-win32-arm64-msvc": "4.59.0",
+ "@rollup/rollup-win32-ia32-msvc": "4.59.0",
+ "@rollup/rollup-win32-x64-gnu": "4.59.0",
+ "@rollup/rollup-win32-x64-msvc": "4.59.0",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/router": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
+ "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.0",
+ "depd": "^2.0.0",
+ "is-promise": "^4.0.0",
+ "parseurl": "^1.3.3",
+ "path-to-regexp": "^8.0.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/send": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
+ "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.3",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "etag": "^1.8.1",
+ "fresh": "^2.0.0",
+ "http-errors": "^2.0.1",
+ "mime-types": "^3.0.2",
+ "ms": "^2.1.3",
+ "on-finished": "^2.4.1",
+ "range-parser": "^1.2.1",
+ "statuses": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/serve-static": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
+ "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "parseurl": "^1.3.3",
+ "send": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "license": "ISC"
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shelljs": {
+ "version": "0.8.5",
+ "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz",
+ "integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "glob": "^7.0.0",
+ "interpret": "^1.0.0",
+ "rechoir": "^0.6.2"
+ },
+ "bin": {
+ "shjs": "bin/shjs"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/shelljs/node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/shelljs/node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/shelljs/node_modules/glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/shelljs/node_modules/minimatch": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/shx": {
+ "version": "0.3.4",
+ "resolved": "https://registry.npmjs.org/shx/-/shx-0.3.4.tgz",
+ "integrity": "sha512-N6A9MLVqjxZYcVn8hLmtneQWIJtp8IKzMP4eMnx+nqkvXoqinUPCbUFLp2UcWTEIUONhlk0ewxr/jaVGlc+J+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "minimist": "^1.2.3",
+ "shelljs": "^0.8.5"
+ },
+ "bin": {
+ "shx": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/siginfo": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
+ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/signal-exit": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/stackback": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
+ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/statuses": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/std-env": {
+ "version": "3.10.0",
+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
+ "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/string-width": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+ "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+ "license": "MIT",
+ "dependencies": {
+ "eastasianwidth": "^0.2.0",
+ "emoji-regex": "^9.2.2",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/string-width-cjs": {
+ "name": "string-width",
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width-cjs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "license": "MIT"
+ },
+ "node_modules/string-width-cjs/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
+ "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.2.2"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/strip-ansi-cjs": {
+ "name": "strip-ansi",
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/test-exclude": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz",
+ "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "@istanbuljs/schema": "^0.1.2",
+ "glob": "^10.4.1",
+ "minimatch": "^10.2.2"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tinybench": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
+ "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinyexec": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
+ "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinypool": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz",
+ "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ }
+ },
+ "node_modules/tinyrainbow": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz",
+ "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tinyspy": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz",
+ "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/type-is": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
+ "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
+ "license": "MIT",
+ "dependencies": {
+ "content-type": "^1.0.5",
+ "media-typer": "^1.1.0",
+ "mime-types": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/vite": {
+ "version": "5.4.21",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
+ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.21.3",
+ "postcss": "^8.4.43",
+ "rollup": "^4.20.0"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || >=20.0.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite-node": {
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz",
+ "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cac": "^6.7.14",
+ "debug": "^4.3.7",
+ "es-module-lexer": "^1.5.4",
+ "pathe": "^1.1.2",
+ "vite": "^5.0.0"
+ },
+ "bin": {
+ "vite-node": "vite-node.mjs"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/vitest": {
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz",
+ "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/expect": "2.1.9",
+ "@vitest/mocker": "2.1.9",
+ "@vitest/pretty-format": "^2.1.9",
+ "@vitest/runner": "2.1.9",
+ "@vitest/snapshot": "2.1.9",
+ "@vitest/spy": "2.1.9",
+ "@vitest/utils": "2.1.9",
+ "chai": "^5.1.2",
+ "debug": "^4.3.7",
+ "expect-type": "^1.1.0",
+ "magic-string": "^0.30.12",
+ "pathe": "^1.1.2",
+ "std-env": "^3.8.0",
+ "tinybench": "^2.9.0",
+ "tinyexec": "^0.3.1",
+ "tinypool": "^1.0.1",
+ "tinyrainbow": "^1.2.0",
+ "vite": "^5.0.0",
+ "vite-node": "2.1.9",
+ "why-is-node-running": "^2.3.0"
+ },
+ "bin": {
+ "vitest": "vitest.mjs"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@edge-runtime/vm": "*",
+ "@types/node": "^18.0.0 || >=20.0.0",
+ "@vitest/browser": "2.1.9",
+ "@vitest/ui": "2.1.9",
+ "happy-dom": "*",
+ "jsdom": "*"
+ },
+ "peerDependenciesMeta": {
+ "@edge-runtime/vm": {
+ "optional": true
+ },
+ "@types/node": {
+ "optional": true
+ },
+ "@vitest/browser": {
+ "optional": true
+ },
+ "@vitest/ui": {
+ "optional": true
+ },
+ "happy-dom": {
+ "optional": true
+ },
+ "jsdom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/why-is-node-running": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
+ "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "siginfo": "^2.0.0",
+ "stackback": "0.0.2"
+ },
+ "bin": {
+ "why-is-node-running": "cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
+ "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.1.0",
+ "string-width": "^5.0.1",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs": {
+ "name": "wrap-ansi",
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "license": "MIT"
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "license": "ISC"
+ },
+ "node_modules/zod": {
+ "version": "4.3.6",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
+ "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
+ "node_modules/zod-to-json-schema": {
+ "version": "3.25.1",
+ "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz",
+ "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==",
+ "license": "ISC",
+ "peerDependencies": {
+ "zod": "^3.25 || ^4"
+ }
+ }
+ }
+}
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@ampproject/remapping/LICENSE b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@ampproject/remapping/LICENSE
new file mode 100644
index 00000000..d6456956
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@ampproject/remapping/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@ampproject/remapping/README.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@ampproject/remapping/README.md
new file mode 100644
index 00000000..1463c9f6
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@ampproject/remapping/README.md
@@ -0,0 +1,218 @@
+# @ampproject/remapping
+
+> Remap sequential sourcemaps through transformations to point at the original source code
+
+Remapping allows you to take the sourcemaps generated through transforming your code and "remap"
+them to the original source locations. Think "my minified code, transformed with babel and bundled
+with webpack", all pointing to the correct location in your original source code.
+
+With remapping, none of your source code transformations need to be aware of the input's sourcemap,
+they only need to generate an output sourcemap. This greatly simplifies building custom
+transformations (think a find-and-replace).
+
+## Installation
+
+```sh
+npm install @ampproject/remapping
+```
+
+## Usage
+
+```typescript
+function remapping(
+ map: SourceMap | SourceMap[],
+ loader: (file: string, ctx: LoaderContext) => (SourceMap | null | undefined),
+ options?: { excludeContent: boolean, decodedMappings: boolean }
+): SourceMap;
+
+// LoaderContext gives the loader the importing sourcemap, tree depth, the ability to override the
+// "source" location (where child sources are resolved relative to, or the location of original
+// source), and the ability to override the "content" of an original source for inclusion in the
+// output sourcemap.
+type LoaderContext = {
+ readonly importer: string;
+ readonly depth: number;
+ source: string;
+ content: string | null | undefined;
+}
+```
+
+`remapping` takes the final output sourcemap, and a `loader` function. For every source file pointer
+in the sourcemap, the `loader` will be called with the resolved path. If the path itself represents
+a transformed file (it has a sourcmap associated with it), then the `loader` should return that
+sourcemap. If not, the path will be treated as an original, untransformed source code.
+
+```js
+// Babel transformed "helloworld.js" into "transformed.js"
+const transformedMap = JSON.stringify({
+ file: 'transformed.js',
+ // 1st column of 2nd line of output file translates into the 1st source
+ // file, line 3, column 2
+ mappings: ';CAEE',
+ sources: ['helloworld.js'],
+ version: 3,
+});
+
+// Uglify minified "transformed.js" into "transformed.min.js"
+const minifiedTransformedMap = JSON.stringify({
+ file: 'transformed.min.js',
+ // 0th column of 1st line of output file translates into the 1st source
+ // file, line 2, column 1.
+ mappings: 'AACC',
+ names: [],
+ sources: ['transformed.js'],
+ version: 3,
+});
+
+const remapped = remapping(
+ minifiedTransformedMap,
+ (file, ctx) => {
+
+ // The "transformed.js" file is an transformed file.
+ if (file === 'transformed.js') {
+ // The root importer is empty.
+ console.assert(ctx.importer === '');
+ // The depth in the sourcemap tree we're currently loading.
+ // The root `minifiedTransformedMap` is depth 0, and its source children are depth 1, etc.
+ console.assert(ctx.depth === 1);
+
+ return transformedMap;
+ }
+
+ // Loader will be called to load transformedMap's source file pointers as well.
+ console.assert(file === 'helloworld.js');
+ // `transformed.js`'s sourcemap points into `helloworld.js`.
+ console.assert(ctx.importer === 'transformed.js');
+ // This is a source child of `transformed`, which is a source child of `minifiedTransformedMap`.
+ console.assert(ctx.depth === 2);
+ return null;
+ }
+);
+
+console.log(remapped);
+// {
+// file: 'transpiled.min.js',
+// mappings: 'AAEE',
+// sources: ['helloworld.js'],
+// version: 3,
+// };
+```
+
+In this example, `loader` will be called twice:
+
+1. `"transformed.js"`, the first source file pointer in the `minifiedTransformedMap`. We return the
+ associated sourcemap for it (its a transformed file, after all) so that sourcemap locations can
+ be traced through it into the source files it represents.
+2. `"helloworld.js"`, our original, unmodified source code. This file does not have a sourcemap, so
+ we return `null`.
+
+The `remapped` sourcemap now points from `transformed.min.js` into locations in `helloworld.js`. If
+you were to read the `mappings`, it says "0th column of the first line output line points to the 1st
+column of the 2nd line of the file `helloworld.js`".
+
+### Multiple transformations of a file
+
+As a convenience, if you have multiple single-source transformations of a file, you may pass an
+array of sourcemap files in the order of most-recent transformation sourcemap first. Note that this
+changes the `importer` and `depth` of each call to our loader. So our above example could have been
+written as:
+
+```js
+const remapped = remapping(
+ [minifiedTransformedMap, transformedMap],
+ () => null
+);
+
+console.log(remapped);
+// {
+// file: 'transpiled.min.js',
+// mappings: 'AAEE',
+// sources: ['helloworld.js'],
+// version: 3,
+// };
+```
+
+### Advanced control of the loading graph
+
+#### `source`
+
+The `source` property can overridden to any value to change the location of the current load. Eg,
+for an original source file, it allows us to change the location to the original source regardless
+of what the sourcemap source entry says. And for transformed files, it allows us to change the
+relative resolving location for child sources of the loaded sourcemap.
+
+```js
+const remapped = remapping(
+ minifiedTransformedMap,
+ (file, ctx) => {
+
+ if (file === 'transformed.js') {
+ // We pretend the transformed.js file actually exists in the 'src/' directory. When the nested
+ // source files are loaded, they will now be relative to `src/`.
+ ctx.source = 'src/transformed.js';
+ return transformedMap;
+ }
+
+ console.assert(file === 'src/helloworld.js');
+ // We could futher change the source of this original file, eg, to be inside a nested directory
+ // itself. This will be reflected in the remapped sourcemap.
+ ctx.source = 'src/nested/transformed.js';
+ return null;
+ }
+);
+
+console.log(remapped);
+// {
+// …,
+// sources: ['src/nested/helloworld.js'],
+// };
+```
+
+
+#### `content`
+
+The `content` property can be overridden when we encounter an original source file. Eg, this allows
+you to manually provide the source content of the original file regardless of whether the
+`sourcesContent` field is present in the parent sourcemap. It can also be set to `null` to remove
+the source content.
+
+```js
+const remapped = remapping(
+ minifiedTransformedMap,
+ (file, ctx) => {
+
+ if (file === 'transformed.js') {
+ // transformedMap does not include a `sourcesContent` field, so usually the remapped sourcemap
+ // would not include any `sourcesContent` values.
+ return transformedMap;
+ }
+
+ console.assert(file === 'helloworld.js');
+ // We can read the file to provide the source content.
+ ctx.content = fs.readFileSync(file, 'utf8');
+ return null;
+ }
+);
+
+console.log(remapped);
+// {
+// …,
+// sourcesContent: [
+// 'console.log("Hello world!")',
+// ],
+// };
+```
+
+### Options
+
+#### excludeContent
+
+By default, `excludeContent` is `false`. Passing `{ excludeContent: true }` will exclude the
+`sourcesContent` field from the returned sourcemap. This is mainly useful when you want to reduce
+the size out the sourcemap.
+
+#### decodedMappings
+
+By default, `decodedMappings` is `false`. Passing `{ decodedMappings: true }` will leave the
+`mappings` field in a [decoded state](https://github.com/rich-harris/sourcemap-codec) instead of
+encoding into a VLQ string.
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@ampproject/remapping/package.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@ampproject/remapping/package.json
new file mode 100644
index 00000000..091224c6
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@ampproject/remapping/package.json
@@ -0,0 +1,75 @@
+{
+ "name": "@ampproject/remapping",
+ "version": "2.3.0",
+ "description": "Remap sequential sourcemaps through transformations to point at the original source code",
+ "keywords": [
+ "source",
+ "map",
+ "remap"
+ ],
+ "main": "dist/remapping.umd.js",
+ "module": "dist/remapping.mjs",
+ "types": "dist/types/remapping.d.ts",
+ "exports": {
+ ".": [
+ {
+ "types": "./dist/types/remapping.d.ts",
+ "browser": "./dist/remapping.umd.js",
+ "require": "./dist/remapping.umd.js",
+ "import": "./dist/remapping.mjs"
+ },
+ "./dist/remapping.umd.js"
+ ],
+ "./package.json": "./package.json"
+ },
+ "files": [
+ "dist"
+ ],
+ "author": "Justin Ridgewell ",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/ampproject/remapping.git"
+ },
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=6.0.0"
+ },
+ "scripts": {
+ "build": "run-s -n build:*",
+ "build:rollup": "rollup -c rollup.config.js",
+ "build:ts": "tsc --project tsconfig.build.json",
+ "lint": "run-s -n lint:*",
+ "lint:prettier": "npm run test:lint:prettier -- --write",
+ "lint:ts": "npm run test:lint:ts -- --fix",
+ "prebuild": "rm -rf dist",
+ "prepublishOnly": "npm run preversion",
+ "preversion": "run-s test build",
+ "test": "run-s -n test:lint test:only",
+ "test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand",
+ "test:lint": "run-s -n test:lint:*",
+ "test:lint:prettier": "prettier --check '{src,test}/**/*.ts'",
+ "test:lint:ts": "eslint '{src,test}/**/*.ts'",
+ "test:only": "jest --coverage",
+ "test:watch": "jest --coverage --watch"
+ },
+ "devDependencies": {
+ "@rollup/plugin-typescript": "8.3.2",
+ "@types/jest": "27.4.1",
+ "@typescript-eslint/eslint-plugin": "5.20.0",
+ "@typescript-eslint/parser": "5.20.0",
+ "eslint": "8.14.0",
+ "eslint-config-prettier": "8.5.0",
+ "jest": "27.5.1",
+ "jest-config": "27.5.1",
+ "npm-run-all": "4.1.5",
+ "prettier": "2.6.2",
+ "rollup": "2.70.2",
+ "ts-jest": "27.1.4",
+ "tslib": "2.4.0",
+ "typescript": "4.6.3"
+ },
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+}
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/helper-string-parser/LICENSE b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/helper-string-parser/LICENSE
new file mode 100644
index 00000000..f31575ec
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/helper-string-parser/LICENSE
@@ -0,0 +1,22 @@
+MIT License
+
+Copyright (c) 2014-present Sebastian McKenzie and other contributors
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/helper-string-parser/README.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/helper-string-parser/README.md
new file mode 100644
index 00000000..771b4700
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/helper-string-parser/README.md
@@ -0,0 +1,19 @@
+# @babel/helper-string-parser
+
+> A utility package to parse strings
+
+See our website [@babel/helper-string-parser](https://babeljs.io/docs/babel-helper-string-parser) for more information.
+
+## Install
+
+Using npm:
+
+```sh
+npm install --save @babel/helper-string-parser
+```
+
+or using yarn:
+
+```sh
+yarn add @babel/helper-string-parser
+```
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/helper-string-parser/package.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/helper-string-parser/package.json
new file mode 100644
index 00000000..c4c86e4f
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/helper-string-parser/package.json
@@ -0,0 +1,31 @@
+{
+ "name": "@babel/helper-string-parser",
+ "version": "7.27.1",
+ "description": "A utility package to parse strings",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/babel/babel.git",
+ "directory": "packages/babel-helper-string-parser"
+ },
+ "homepage": "https://babel.dev/docs/en/next/babel-helper-string-parser",
+ "license": "MIT",
+ "publishConfig": {
+ "access": "public"
+ },
+ "main": "./lib/index.js",
+ "devDependencies": {
+ "charcodes": "^0.2.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "author": "The Babel Team (https://babel.dev/team)",
+ "exports": {
+ ".": {
+ "types": "./lib/index.d.ts",
+ "default": "./lib/index.js"
+ },
+ "./package.json": "./package.json"
+ },
+ "type": "commonjs"
+}
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/helper-validator-identifier/LICENSE b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/helper-validator-identifier/LICENSE
new file mode 100644
index 00000000..f31575ec
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/helper-validator-identifier/LICENSE
@@ -0,0 +1,22 @@
+MIT License
+
+Copyright (c) 2014-present Sebastian McKenzie and other contributors
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/helper-validator-identifier/README.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/helper-validator-identifier/README.md
new file mode 100644
index 00000000..05c19e64
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/helper-validator-identifier/README.md
@@ -0,0 +1,19 @@
+# @babel/helper-validator-identifier
+
+> Validate identifier/keywords name
+
+See our website [@babel/helper-validator-identifier](https://babeljs.io/docs/babel-helper-validator-identifier) for more information.
+
+## Install
+
+Using npm:
+
+```sh
+npm install --save @babel/helper-validator-identifier
+```
+
+or using yarn:
+
+```sh
+yarn add @babel/helper-validator-identifier
+```
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/helper-validator-identifier/package.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/helper-validator-identifier/package.json
new file mode 100644
index 00000000..1aea38db
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/helper-validator-identifier/package.json
@@ -0,0 +1,31 @@
+{
+ "name": "@babel/helper-validator-identifier",
+ "version": "7.28.5",
+ "description": "Validate identifier/keywords name",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/babel/babel.git",
+ "directory": "packages/babel-helper-validator-identifier"
+ },
+ "license": "MIT",
+ "publishConfig": {
+ "access": "public"
+ },
+ "main": "./lib/index.js",
+ "exports": {
+ ".": {
+ "types": "./lib/index.d.ts",
+ "default": "./lib/index.js"
+ },
+ "./package.json": "./package.json"
+ },
+ "devDependencies": {
+ "@unicode/unicode-17.0.0": "^1.6.10",
+ "charcodes": "^0.2.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "author": "The Babel Team (https://babel.dev/team)",
+ "type": "commonjs"
+}
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/parser/CHANGELOG.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/parser/CHANGELOG.md
new file mode 100644
index 00000000..b3840ac8
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/parser/CHANGELOG.md
@@ -0,0 +1,1073 @@
+# Changelog
+
+> **Tags:**
+> - :boom: [Breaking Change]
+> - :eyeglasses: [Spec Compliance]
+> - :rocket: [New Feature]
+> - :bug: [Bug Fix]
+> - :memo: [Documentation]
+> - :house: [Internal]
+> - :nail_care: [Polish]
+
+> Semver Policy: https://github.com/babel/babel/tree/main/packages/babel-parser#semver
+
+_Note: Gaps between patch versions are faulty, broken or test releases._
+
+See the [Babel Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md) for the pre-6.8.0 version Changelog.
+
+## 6.17.1 (2017-05-10)
+
+### :bug: Bug Fix
+ * Fix typo in flow spread operator error (Brian Ng)
+ * Fixed invalid number literal parsing ([#473](https://github.com/babel/babylon/pull/473)) (Alex Kuzmenko)
+ * Fix number parser ([#433](https://github.com/babel/babylon/pull/433)) (Alex Kuzmenko)
+ * Ensure non pattern shorthand props are checked for reserved words ([#479](https://github.com/babel/babylon/pull/479)) (Brian Ng)
+ * Remove jsx context when parsing arrow functions ([#475](https://github.com/babel/babylon/pull/475)) (Brian Ng)
+ * Allow super in class properties ([#499](https://github.com/babel/babylon/pull/499)) (Brian Ng)
+ * Allow flow class field to be named constructor ([#510](https://github.com/babel/babylon/pull/510)) (Brian Ng)
+
+## 6.17.0 (2017-04-20)
+
+### :bug: Bug Fix
+ * Cherry-pick #418 to 6.x ([#476](https://github.com/babel/babylon/pull/476)) (Sebastian McKenzie)
+ * Add support for invalid escapes in tagged templates ([#274](https://github.com/babel/babylon/pull/274)) (Kevin Gibbons)
+ * Throw error if new.target is used outside of a function ([#402](https://github.com/babel/babylon/pull/402)) (Brian Ng)
+ * Fix parsing of class properties ([#351](https://github.com/babel/babylon/pull/351)) (Kevin Gibbons)
+ * Fix parsing yield with dynamicImport ([#383](https://github.com/babel/babylon/pull/383)) (Brian Ng)
+ * Ensure consistent start args for parseParenItem ([#386](https://github.com/babel/babylon/pull/386)) (Brian Ng)
+
+## 7.0.0-beta.8 (2017-04-04)
+
+### New Feature
+* Add support for flow type spread (#418) (Conrad Buck)
+* Allow statics in flow interfaces (#427) (Brian Ng)
+
+### Bug Fix
+* Fix predicate attachment to match flow parser (#428) (Brian Ng)
+* Add extra.raw back to JSXText and JSXAttribute (#344) (Alex Rattray)
+* Fix rest parameters with array and objects (#424) (Brian Ng)
+* Fix number parser (#433) (Alex Kuzmenko)
+
+### Docs
+* Fix CONTRIBUTING.md [skip ci] (#432) (Alex Kuzmenko)
+
+### Internal
+* Use babel-register script when running babel smoke tests (#442) (Brian Ng)
+
+## 7.0.0-beta.7 (2017-03-22)
+
+### Spec Compliance
+* Remove babylon plugin for template revision since it's stage-4 (#426) (Henry Zhu)
+
+### Bug Fix
+
+* Fix push-pop logic in flow (#405) (Daniel Tschinder)
+
+## 7.0.0-beta.6 (2017-03-21)
+
+### New Feature
+* Add support for invalid escapes in tagged templates (#274) (Kevin Gibbons)
+
+### Polish
+* Improves error message when super is called outside of constructor (#408) (Arshabh Kumar Agarwal)
+
+### Docs
+
+* [7.0] Moved value field in spec from ObjectMember to ObjectProperty as ObjectMethod's don't have it (#415) [skip ci] (James Browning)
+
+## 7.0.0-beta.5 (2017-03-21)
+
+### Bug Fix
+* Throw error if new.target is used outside of a function (#402) (Brian Ng)
+* Fix parsing of class properties (#351) (Kevin Gibbons)
+
+### Other
+ * Test runner: Detect extra property in 'actual' but not in 'expected'. (#407) (Andy)
+ * Optimize travis builds (#419) (Daniel Tschinder)
+ * Update codecov to 2.0 (#412) (Daniel Tschinder)
+ * Fix spec for ClassMethod: It doesn't have a function, it *is* a function. (#406) [skip ci] (Andy)
+ * Changed Non-existent RestPattern to RestElement which is what is actually parsed (#409) [skip ci] (James Browning)
+ * Upgrade flow to 0.41 (Daniel Tschinder)
+ * Fix watch command (#403) (Brian Ng)
+ * Update yarn lock (Daniel Tschinder)
+ * Fix watch command (#403) (Brian Ng)
+ * chore(package): update flow-bin to version 0.41.0 (#395) (greenkeeper[bot])
+ * Add estree test for correct order of directives (Daniel Tschinder)
+ * Add DoExpression to spec (#364) (Alex Kuzmenko)
+ * Mention cloning of repository in CONTRIBUTING.md (#391) [skip ci] (Sumedh Nimkarde)
+ * Explain how to run only one test (#389) [skip ci] (Aaron Ang)
+
+ ## 7.0.0-beta.4 (2017-03-01)
+
+* Don't consume async when checking for async func decl (#377) (Brian Ng)
+* add `ranges` option [skip ci] (Henry Zhu)
+* Don't parse class properties without initializers when classProperties is disabled and Flow is enabled (#300) (Andrew Levine)
+
+## 7.0.0-beta.3 (2017-02-28)
+
+- [7.0] Change RestProperty/SpreadProperty to RestElement/SpreadElement (#384)
+- Merge changes from 6.x
+
+## 7.0.0-beta.2 (2017-02-20)
+
+- estree: correctly change literals in all cases (#368) (Daniel Tschinder)
+
+## 7.0.0-beta.1 (2017-02-20)
+
+- Fix negative number literal typeannotations (#366) (Daniel Tschinder)
+- Update contributing with more test info [skip ci] (#355) (Brian Ng)
+
+## 7.0.0-beta.0 (2017-02-15)
+
+- Reintroduce Variance node (#333) (Daniel Tschinder)
+- Rename NumericLiteralTypeAnnotation to NumberLiteralTypeAnnotation (#332) (Charles Pick)
+- [7.0] Remove ForAwaitStatement, add await flag to ForOfStatement (#349) (Brandon Dail)
+- chore(package): update ava to version 0.18.0 (#345) (greenkeeper[bot])
+- chore(package): update babel-plugin-istanbul to version 4.0.0 (#350) (greenkeeper[bot])
+- Change location of ObjectTypeIndexer to match flow (#228) (Daniel Tschinder)
+- Rename flow AST Type ExistentialTypeParam to ExistsTypeAnnotation (#322) (Toru Kobayashi)
+- Revert "Temporary rollback for erroring on trailing comma with spread (#154)" (#290) (Daniel Tschinder)
+- Remove classConstructorCall plugin (#291) (Brian Ng)
+- Update yarn.lock (Daniel Tschinder)
+- Update cross-env to 3.x (Daniel Tschinder)
+- [7.0] Remove node 0.10, 0.12 and 5 from Travis (#284) (Sergey Rubanov)
+- Remove `String.fromCodePoint` shim (#279) (Mathias Bynens)
+
+## 6.16.1 (2017-02-23)
+
+### :bug: Regression
+
+- Revert "Fix export default async function to be FunctionDeclaration" ([#375](https://github.com/babel/babylon/pull/375))
+
+Need to modify Babel for this AST node change, so moving to 7.0.
+
+- Revert "Don't parse class properties without initializers when classProperties plugin is disabled, and Flow is enabled" ([#376](https://github.com/babel/babylon/pull/376))
+
+[react-native](https://github.com/facebook/react-native/issues/12542) broke with this so we reverted.
+
+## 6.16.0 (2017-02-23)
+
+### :rocket: New Feature
+
+***ESTree*** compatibility as plugin ([#277](https://github.com/babel/babylon/pull/277)) (Daniel Tschinder)
+
+We finally introduce a new compatibility layer for ESTree. To put babylon into ESTree-compatible mode the new plugin `estree` can be enabled. In this mode the parser will output an AST that is compliant to the specs of [ESTree](https://github.com/estree/estree/)
+
+We highly recommend everyone who uses babylon outside of babel to use this plugin. This will make it much easier for users to switch between different ESTree-compatible parsers. We so far tested several projects with different parsers and exchanged their parser to babylon and in nearly all cases it worked out of the box. Some other estree-compatible parsers include `acorn`, `esprima`, `espree`, `flow-parser`, etc.
+
+To enable `estree` mode simply add the plugin in the config:
+```json
+{
+ "plugins": [ "estree" ]
+}
+```
+
+If you want to migrate your project from non-ESTree mode to ESTree, have a look at our [Readme](https://github.com/babel/babylon/#output), where all deviations are mentioned.
+
+Add a parseExpression public method ([#213](https://github.com/babel/babylon/pull/213)) (jeromew)
+
+Babylon exports a new function to parse a single expression
+
+```js
+import { parseExpression } from 'babylon';
+
+const ast = parseExpression('x || y && z', options);
+```
+
+The returned AST will only consist of the expression. The options are the same as for `parse()`
+
+Add startLine option ([#346](https://github.com/babel/babylon/pull/346)) (Raphael Mu)
+
+A new option was added to babylon allowing to change the initial linenumber for the first line which is usually `1`.
+Changing this for example to `100` will make line `1` of the input source to be marked as line `100`, line `2` as `101`, line `3` as `102`, ...
+
+Function predicate declaration ([#103](https://github.com/babel/babylon/pull/103)) (Panagiotis Vekris)
+
+Added support for function predicates which flow introduced in version 0.33.0
+
+```js
+declare function is_number(x: mixed): boolean %checks(typeof x === "number");
+```
+
+Allow imports in declare module ([#315](https://github.com/babel/babylon/pull/315)) (Daniel Tschinder)
+
+Added support for imports within module declarations which flow introduced in version 0.37.0
+
+```js
+declare module "C" {
+ import type { DT } from "D";
+ declare export type CT = { D: DT };
+}
+```
+
+### :eyeglasses: Spec Compliance
+
+Forbid semicolons after decorators in classes ([#352](https://github.com/babel/babylon/pull/352)) (Kevin Gibbons)
+
+This example now correctly throws an error when there is a semicolon after the decorator:
+
+```js
+class A {
+@a;
+foo(){}
+}
+```
+
+Keywords are not allowed as local specifier ([#307](https://github.com/babel/babylon/pull/307)) (Daniel Tschinder)
+
+Using keywords in imports is not allowed anymore:
+
+```js
+import { default } from "foo";
+import { a as debugger } from "foo";
+```
+
+Do not allow overwritting of primitive types ([#314](https://github.com/babel/babylon/pull/314)) (Daniel Tschinder)
+
+In flow it is now forbidden to overwrite the primitive types `"any"`, `"mixed"`, `"empty"`, `"bool"`, `"boolean"`, `"number"`, `"string"`, `"void"` and `"null"` with your own type declaration.
+
+Disallow import type { type a } from … ([#305](https://github.com/babel/babylon/pull/305)) (Daniel Tschinder)
+
+The following code now correctly throws an error
+
+```js
+import type { type a } from "foo";
+```
+
+Don't parse class properties without initializers when classProperties is disabled and Flow is enabled ([#300](https://github.com/babel/babylon/pull/300)) (Andrew Levine)
+
+Ensure that you enable the `classProperties` plugin in order to enable correct parsing of class properties. Prior to this version it was possible to parse them by enabling the `flow` plugin but this was not intended the behaviour.
+
+If you enable the flow plugin you can only define the type of the class properties, but not initialize them.
+
+Fix export default async function to be FunctionDeclaration ([#324](https://github.com/babel/babylon/pull/324)) (Daniel Tschinder)
+
+Parsing the following code now returns a `FunctionDeclaration` AST node instead of `FunctionExpression`.
+
+```js
+export default async function bar() {};
+```
+
+### :nail_care: Polish
+
+Improve error message on attempt to destructure named import ([#288](https://github.com/babel/babylon/pull/288)) (Brian Ng)
+
+### :bug: Bug Fix
+
+Fix negative number literal typeannotations ([#366](https://github.com/babel/babylon/pull/366)) (Daniel Tschinder)
+
+Ensure takeDecorators is called on exported class ([#358](https://github.com/babel/babylon/pull/358)) (Brian Ng)
+
+ESTree: correctly change literals in all cases ([#368](https://github.com/babel/babylon/pull/368)) (Daniel Tschinder)
+
+Correctly convert RestProperty to Assignable ([#339](https://github.com/babel/babylon/pull/339)) (Daniel Tschinder)
+
+Fix #321 by allowing question marks in type params ([#338](https://github.com/babel/babylon/pull/338)) (Daniel Tschinder)
+
+Fix #336 by correctly setting arrow-param ([#337](https://github.com/babel/babylon/pull/337)) (Daniel Tschinder)
+
+Fix parse error when destructuring `set` with default value ([#317](https://github.com/babel/babylon/pull/317)) (Brian Ng)
+
+Fix ObjectTypeCallProperty static ([#298](https://github.com/babel/babylon/pull/298)) (Dan Harper)
+
+
+### :house: Internal
+
+Fix generator-method-with-computed-name spec ([#360](https://github.com/babel/babylon/pull/360)) (Alex Rattray)
+
+Fix flow type-parameter-declaration test with unintended semantic ([#361](https://github.com/babel/babylon/pull/361)) (Alex Rattray)
+
+Cleanup and splitup parser functions ([#295](https://github.com/babel/babylon/pull/295)) (Daniel Tschinder)
+
+chore(package): update flow-bin to version 0.38.0 ([#313](https://github.com/babel/babylon/pull/313)) (greenkeeper[bot])
+
+Call inner function instead of 1:1 copy to plugin ([#294](https://github.com/babel/babylon/pull/294)) (Daniel Tschinder)
+
+Update eslint-config-babel to the latest version 🚀 ([#299](https://github.com/babel/babylon/pull/299)) (greenkeeper[bot])
+
+Update eslint-config-babel to the latest version 🚀 ([#293](https://github.com/babel/babylon/pull/293)) (greenkeeper[bot])
+
+devDeps: remove eslint-plugin-babel ([#292](https://github.com/babel/babylon/pull/292)) (Kai Cataldo)
+
+Correct indent eslint rule config ([#276](https://github.com/babel/babylon/pull/276)) (Daniel Tschinder)
+
+Fail tests that have expected.json and throws-option ([#285](https://github.com/babel/babylon/pull/285)) (Daniel Tschinder)
+
+### :memo: Documentation
+
+Update contributing with more test info [skip ci] ([#355](https://github.com/babel/babylon/pull/355)) (Brian Ng)
+
+Update API documentation ([#330](https://github.com/babel/babylon/pull/330)) (Timothy Gu)
+
+Added keywords to package.json ([#323](https://github.com/babel/babylon/pull/323)) (Dmytro)
+
+AST spec: fix casing of `RegExpLiteral` ([#318](https://github.com/babel/babylon/pull/318)) (Mathias Bynens)
+
+## 6.15.0 (2017-01-10)
+
+### :eyeglasses: Spec Compliance
+
+Add support for Flow shorthand import type ([#267](https://github.com/babel/babylon/pull/267)) (Jeff Morrison)
+
+This change implements flows new shorthand import syntax
+and where previously you had to write this code:
+
+```js
+import {someValue} from "blah";
+import type {someType} from "blah";
+import typeof {someOtherValue} from "blah";
+```
+
+you can now write it like this:
+
+```js
+import {
+ someValue,
+ type someType,
+ typeof someOtherValue,
+} from "blah";
+```
+
+For more information look at [this](https://github.com/facebook/flow/pull/2890) pull request.
+
+flow: allow leading pipes in all positions ([#256](https://github.com/babel/babylon/pull/256)) (Vladimir Kurchatkin)
+
+This change now allows a leading pipe everywhere types can be used:
+```js
+var f = (x): | 1 | 2 => 1;
+```
+
+Throw error when exporting non-declaration ([#241](https://github.com/babel/babylon/pull/241)) (Kai Cataldo)
+
+Previously babylon parsed the following exports, although they are not valid:
+```js
+export typeof foo;
+export new Foo();
+export function() {};
+export for (;;);
+export while(foo);
+```
+
+### :bug: Bug Fix
+
+Don't set inType flag when parsing property names ([#266](https://github.com/babel/babylon/pull/266)) (Vladimir Kurchatkin)
+
+This fixes parsing of this case:
+
+```js
+const map = {
+ [age <= 17] : 'Too young'
+};
+```
+
+Fix source location for JSXEmptyExpression nodes (fixes #248) ([#249](https://github.com/babel/babylon/pull/249)) (James Long)
+
+The following case produced an invalid AST
+```js
+{/* foo */}
+```
+
+Use fromCodePoint to convert high value unicode entities ([#243](https://github.com/babel/babylon/pull/243)) (Ryan Duffy)
+
+When high value unicode entities (e.g. 💩) were used in the input source code they are now correctly encoded in the resulting AST.
+
+Rename folder to avoid Windows-illegal characters ([#281](https://github.com/babel/babylon/pull/281)) (Ryan Plant)
+
+Allow this.state.clone() when parsing decorators ([#262](https://github.com/babel/babylon/pull/262)) (Alex Rattray)
+
+### :house: Internal
+
+User external-helpers ([#254](https://github.com/babel/babylon/pull/254)) (Daniel Tschinder)
+
+Add watch script for dev ([#234](https://github.com/babel/babylon/pull/234)) (Kai Cataldo)
+
+Freeze current plugins list for "*" option, and remove from README.md ([#245](https://github.com/babel/babylon/pull/245)) (Andrew Levine)
+
+Prepare tests for multiple fixture runners. ([#240](https://github.com/babel/babylon/pull/240)) (Daniel Tschinder)
+
+Add some test coverage for decorators stage-0 plugin ([#250](https://github.com/babel/babylon/pull/250)) (Andrew Levine)
+
+Refactor tokenizer types file ([#263](https://github.com/babel/babylon/pull/263)) (Sven SAULEAU)
+
+Update eslint-config-babel to the latest version 🚀 ([#273](https://github.com/babel/babylon/pull/273)) (greenkeeper[bot])
+
+chore(package): update rollup to version 0.41.0 ([#272](https://github.com/babel/babylon/pull/272)) (greenkeeper[bot])
+
+chore(package): update flow-bin to version 0.37.0 ([#255](https://github.com/babel/babylon/pull/255)) (greenkeeper[bot])
+
+## 6.14.1 (2016-11-17)
+
+### :bug: Bug Fix
+
+Allow `"plugins": ["*"]` ([#229](https://github.com/babel/babylon/pull/229)) (Daniel Tschinder)
+
+```js
+{
+ "plugins": ["*"]
+}
+```
+
+Will include all parser plugins instead of specifying each one individually. Useful for tools like babel-eslint, jscodeshift, and ast-explorer.
+
+## 6.14.0 (2016-11-16)
+
+### :eyeglasses: Spec Compliance
+
+Throw error for reserved words `enum` and `await` ([#195](https://github.com/babel/babylon/pull/195)) (Kai Cataldo)
+
+[11.6.2.2 Future Reserved Words](http://www.ecma-international.org/ecma-262/6.0/#sec-future-reserved-words)
+
+Babylon will throw for more reserved words such as `enum` or `await` (in strict mode).
+
+```
+class enum {} // throws
+class await {} // throws in strict mode (module)
+```
+
+Optional names for function types and object type indexers ([#197](https://github.com/babel/babylon/pull/197)) (Gabe Levi)
+
+So where you used to have to write
+
+```js
+type A = (x: string, y: boolean) => number;
+type B = (z: string) => number;
+type C = { [key: string]: number };
+```
+
+you can now write (with flow 0.34.0)
+
+```js
+type A = (string, boolean) => number;
+type B = string => number;
+type C = { [string]: number };
+```
+
+Parse flow nested array type annotations like `number[][]` ([#219](https://github.com/babel/babylon/pull/219)) (Bernhard Häussner)
+
+Supports these form now of specifying array types:
+
+```js
+var a: number[][][][];
+var b: string[][];
+```
+
+### :bug: Bug Fix
+
+Correctly eat semicolon at the end of `DelcareModuleExports` ([#223](https://github.com/babel/babylon/pull/223)) (Daniel Tschinder)
+
+```
+declare module "foo" { declare module.exports: number }
+declare module "foo" { declare module.exports: number; } // also allowed now
+```
+
+### :house: Internal
+
+ * Count Babel tests towards Babylon code coverage ([#182](https://github.com/babel/babylon/pull/182)) (Moti Zilberman)
+ * Fix strange line endings ([#214](https://github.com/babel/babylon/pull/214)) (Thomas Grainger)
+ * Add node 7 (Daniel Tschinder)
+ * chore(package): update flow-bin to version 0.34.0 ([#204](https://github.com/babel/babylon/pull/204)) (Greenkeeper)
+
+## v6.13.1 (2016-10-26)
+
+### :nail_care: Polish
+
+- Use rollup for bundling to speed up startup time ([#190](https://github.com/babel/babylon/pull/190)) ([@drewml](https://github.com/DrewML))
+
+```js
+const babylon = require('babylon');
+const ast = babylon.parse('var foo = "lol";');
+```
+
+With that test case, there was a ~95ms savings by removing the need for node to build/traverse the dependency graph.
+
+**Without bundling**
+
+
+**With bundling**
+
+
+- add clean command [skip ci] ([#201](https://github.com/babel/babylon/pull/201)) (Henry Zhu)
+- add ForAwaitStatement (async generator already added) [skip ci] ([#196](https://github.com/babel/babylon/pull/196)) (Henry Zhu)
+
+## v6.13.0 (2016-10-21)
+
+### :eyeglasses: Spec Compliance
+
+Property variance type annotations for Flow plugin ([#161](https://github.com/babel/babylon/pull/161)) (Sam Goldman)
+
+> See https://flowtype.org/docs/variance.html for more information
+
+```js
+type T = { +p: T };
+interface T { -p: T };
+declare class T { +[k:K]: V };
+class T { -[k:K]: V };
+class C2 { +p: T = e };
+```
+
+Raise error on duplicate definition of __proto__ ([#183](https://github.com/babel/babylon/pull/183)) (Moti Zilberman)
+
+```js
+({ __proto__: 1, __proto__: 2 }) // Throws an error now
+```
+
+### :bug: Bug Fix
+
+Flow: Allow class properties to be named `static` ([#184](https://github.com/babel/babylon/pull/184)) (Moti Zilberman)
+
+```js
+declare class A {
+ static: T;
+}
+```
+
+Allow "async" as identifier for object literal property shorthand ([#187](https://github.com/babel/babylon/pull/187)) (Andrew Levine)
+
+```js
+var foo = { async, bar };
+```
+
+### :nail_care: Polish
+
+Fix flowtype and add inType to state ([#189](https://github.com/babel/babylon/pull/189)) (Daniel Tschinder)
+
+> This improves the performance slightly (because of hidden classes)
+
+### :house: Internal
+
+Fix .gitattributes line ending setting ([#191](https://github.com/babel/babylon/pull/191)) (Moti Zilberman)
+
+Increase test coverage ([#175](https://github.com/babel/babylon/pull/175) (Moti Zilberman)
+
+Readd missin .eslinignore for IDEs (Daniel Tschinder)
+
+Error on missing expected.json fixture in CI ([#188](https://github.com/babel/babylon/pull/188)) (Moti Zilberman)
+
+Add .gitattributes and .editorconfig for LF line endings ([#179](https://github.com/babel/babylon/pull/179)) (Moti Zilberman)
+
+Fixes two tests that are failing after the merge of #172 ([#177](https://github.com/babel/babylon/pull/177)) (Moti Zilberman)
+
+## v6.12.0 (2016-10-14)
+
+### :eyeglasses: Spec Compliance
+
+Implement import() syntax ([#163](https://github.com/babel/babylon/pull/163)) (Jordan Gensler)
+
+#### Dynamic Import
+
+- Proposal Repo: https://github.com/domenic/proposal-dynamic-import
+- Championed by [@domenic](https://github.com/domenic)
+- stage-2
+- [sept-28 tc39 notes](https://github.com/rwaldron/tc39-notes/blob/master/es7/2016-09/sept-28.md#113a-import)
+
+> This repository contains a proposal for adding a "function-like" import() module loading syntactic form to JavaScript
+
+```js
+import(`./section-modules/${link.dataset.entryModule}.js`)
+.then(module => {
+ module.loadPageInto(main);
+})
+```
+
+Add EmptyTypeAnnotation ([#171](https://github.com/babel/babylon/pull/171)) (Sam Goldman)
+
+#### EmptyTypeAnnotation
+
+Just wasn't covered before.
+
+```js
+type T = empty;
+```
+
+### :bug: Bug Fix
+
+Fix crash when exporting with destructuring and sparse array ([#170](https://github.com/babel/babylon/pull/170)) (Jeroen Engels)
+
+```js
+// was failing due to sparse array
+export const { foo: [ ,, qux7 ] } = bar;
+```
+
+Allow keyword in Flow object declaration property names with type parameters ([#146](https://github.com/babel/babylon/pull/146)) (Dan Harper)
+
+```js
+declare class X {
+ foobar(): void;
+ static foobar(): void;
+}
+```
+
+Allow keyword in object/class property names with Flow type parameters ([#145](https://github.com/babel/babylon/pull/145)) (Dan Harper)
+
+```js
+class Foo {
+ delete(item: T): T {
+ return item;
+ }
+}
+```
+
+Allow typeAnnotations for yield expressions ([#174](https://github.com/babel/babylon/pull/174))) (Daniel Tschinder)
+
+```js
+function *foo() {
+ const x = (yield 5: any);
+}
+```
+
+### :nail_care: Polish
+
+Annotate more errors with expected token ([#172](https://github.com/babel/babylon/pull/172))) (Moti Zilberman)
+
+```js
+// Unexpected token, expected ; (1:6)
+{ set 1 }
+```
+
+### :house: Internal
+
+Remove kcheck ([#173](https://github.com/babel/babylon/pull/173))) (Daniel Tschinder)
+
+Also run flow, linting, babel tests on separate instances (add back node 0.10)
+
+## v6.11.6 (2016-10-12)
+
+### :bug: Bug Fix/Regression
+
+Fix crash when exporting with destructuring and sparse array ([#170](https://github.com/babel/babylon/pull/170)) (Jeroen Engels)
+
+```js
+// was failing with `Cannot read property 'type' of null` because of null identifiers
+export const { foo: [ ,, qux7 ] } = bar;
+```
+
+## v6.11.5 (2016-10-12)
+
+### :eyeglasses: Spec Compliance
+
+Fix: Check for duplicate named exports in exported destructuring assignments ([#144](https://github.com/babel/babylon/pull/144)) (Kai Cataldo)
+
+```js
+// `foo` has already been exported. Exported identifiers must be unique. (2:20)
+export function foo() {};
+export const { a: [{foo}] } = bar;
+```
+
+Fix: Check for duplicate named exports in exported rest elements/properties ([#164](https://github.com/babel/babylon/pull/164)) (Kai Cataldo)
+
+```js
+// `foo` has already been exported. Exported identifiers must be unique. (2:22)
+export const foo = 1;
+export const [bar, ...foo] = baz;
+```
+
+### :bug: Bug Fix
+
+Fix: Allow identifier `async` for default param in arrow expression ([#165](https://github.com/babel/babylon/pull/165)) (Kai Cataldo)
+
+```js
+// this is ok now
+const test = ({async = true}) => {};
+```
+
+### :nail_care: Polish
+
+Babylon will now print out the token it's expecting if there's a `SyntaxError` ([#150](https://github.com/babel/babylon/pull/150)) (Daniel Tschinder)
+
+```bash
+# So in the case of a missing ending curly (`}`)
+Module build failed: SyntaxError: Unexpected token, expected } (30:0)
+ 28 | }
+ 29 |
+> 30 |
+ | ^
+```
+
+## v6.11.4 (2016-10-03)
+
+Temporary rollback for erroring on trailing comma with spread (#154) (Henry Zhu)
+
+## v6.11.3 (2016-10-01)
+
+### :eyeglasses: Spec Compliance
+
+Add static errors for object rest (#149) ([@danez](https://github.com/danez))
+
+> https://github.com/sebmarkbage/ecmascript-rest-spread
+
+Object rest copies the *rest* of properties from the right hand side `obj` starting from the left to right.
+
+```js
+let { x, y, ...z } = { x: 1, y: 2, z: 3 };
+// x = 1
+// y = 2
+// z = { z: 3 }
+```
+
+#### New Syntax Errors:
+
+**SyntaxError**: The rest element has to be the last element when destructuring (1:10)
+```bash
+> 1 | let { ...x, y, z } = { x: 1, y: 2, z: 3};
+ | ^
+# Previous behavior:
+# x = { x: 1, y: 2, z: 3 }
+# y = 2
+# z = 3
+```
+
+Before, this was just a more verbose way of shallow copying `obj` since it doesn't actually do what you think.
+
+**SyntaxError**: Cannot have multiple rest elements when destructuring (1:13)
+
+```bash
+> 1 | let { x, ...y, ...z } = { x: 1, y: 2, z: 3};
+ | ^
+# Previous behavior:
+# x = 1
+# y = { y: 2, z: 3 }
+# z = { y: 2, z: 3 }
+```
+
+Before y and z would just be the same value anyway so there is no reason to need to have both.
+
+**SyntaxError**: A trailing comma is not permitted after the rest element (1:16)
+
+```js
+let { x, y, ...z, } = obj;
+```
+
+The rationale for this is that the use case for trailing comma is that you can add something at the end without affecting the line above. Since a RestProperty always has to be the last property it doesn't make sense.
+
+---
+
+get / set are valid property names in default assignment (#142) ([@jezell](https://github.com/jezell))
+
+```js
+// valid
+function something({ set = null, get = null }) {}
+```
+
+## v6.11.2 (2016-09-23)
+
+### Bug Fix
+
+- [#139](https://github.com/babel/babylon/issues/139) Don't do the duplicate check if not an identifier (#140) @hzoo
+
+```js
+// regression with duplicate export check
+SyntaxError: ./typography.js: `undefined` has already been exported. Exported identifiers must be unique. (22:13)
+ 20 |
+ 21 | export const { rhythm } = typography;
+> 22 | export const { TypographyStyle } = typography
+```
+
+Bail out for now, and make a change to account for destructuring in the next release.
+
+## 6.11.1 (2016-09-22)
+
+### Bug Fix
+- [#137](https://github.com/babel/babylon/pull/137) - Fix a regression with duplicate exports - it was erroring on all keys in `Object.prototype`. @danez
+
+```javascript
+export toString from './toString';
+```
+
+```bash
+`toString` has already been exported. Exported identifiers must be unique. (1:7)
+> 1 | export toString from './toString';
+ | ^
+ 2 |
+```
+
+## 6.11.0 (2016-09-22)
+
+### Spec Compliance (will break CI)
+
+- Disallow duplicate named exports ([#107](https://github.com/babel/babylon/pull/107)) @kaicataldo
+
+```js
+// Only one default export allowed per module. (2:9)
+export default function() {};
+export { foo as default };
+
+// Only one default export allowed per module. (2:0)
+export default {};
+export default function() {};
+
+// `Foo` has already been exported. Exported identifiers must be unique. (2:0)
+export { Foo };
+export class Foo {};
+```
+
+### New Feature (Syntax)
+
+- Add support for computed class property names ([#121](https://github.com/babel/babylon/pull/121)) @motiz88
+
+```js
+// AST
+interface ClassProperty <: Node {
+ type: "ClassProperty";
+ key: Identifier;
+ value: Expression;
+ computed: boolean; // added
+}
+```
+
+```js
+// with "plugins": ["classProperties"]
+class Foo {
+ [x]
+ ['y']
+}
+
+class Bar {
+ [p]
+ [m] () {}
+}
+ ```
+
+### Bug Fix
+
+- Fix `static` property falling through in the declare class Flow AST ([#135](https://github.com/babel/babylon/pull/135)) @danharper
+
+```js
+declare class X {
+ a: number;
+ static b: number; // static
+ c: number; // this was being marked as static in the AST as well
+}
+```
+
+### Polish
+
+- Rephrase "assigning/binding to rvalue" errors to include context ([#119](https://github.com/babel/babylon/pull/119)) @motiz88
+
+```js
+// Used to error with:
+// SyntaxError: Assigning to rvalue (1:0)
+
+// Now:
+// Invalid left-hand side in assignment expression (1:0)
+3 = 4
+
+// Invalid left-hand side in for-in statement (1:5)
+for (+i in {});
+```
+
+### Internal
+
+- Fix call to `this.parseMaybeAssign` with correct arguments ([#133](https://github.com/babel/babylon/pull/133)) @danez
+- Add semver note to changelog ([#131](https://github.com/babel/babylon/pull/131)) @hzoo
+
+## 6.10.0 (2016-09-19)
+
+> We plan to include some spec compliance bugs in patch versions. An example was the multiple default exports issue.
+
+### Spec Compliance
+
+* Implement ES2016 check for simple parameter list in strict mode ([#106](https://github.com/babel/babylon/pull/106)) (Timothy Gu)
+
+> It is a Syntax Error if ContainsUseStrict of FunctionBody is true and IsSimpleParameterList of FormalParameters is false. https://tc39.github.io/ecma262/2016/#sec-function-definitions-static-semantics-early-errors
+
+More Context: [tc39-notes](https://github.com/rwaldron/tc39-notes/blob/master/es7/2015-07/july-29.md#611-the-scope-of-use-strict-with-respect-to-destructuring-in-parameter-lists)
+
+For example:
+
+```js
+// this errors because it uses destructuring and default parameters
+// in a function with a "use strict" directive
+function a([ option1, option2 ] = []) {
+ "use strict";
+}
+ ```
+
+The solution would be to use a top level "use strict" or to remove the destructuring or default parameters when using a function + "use strict" or to.
+
+### New Feature
+
+* Exact object type annotations for Flow plugin ([#104](https://github.com/babel/babylon/pull/104)) (Basil Hosmer)
+
+Added to flow in https://github.com/facebook/flow/commit/c710c40aa2a115435098d6c0dfeaadb023cd39b8
+
+Looks like:
+
+```js
+var a : {| x: number, y: string |} = { x: 0, y: 'foo' };
+```
+
+### Bug Fixes
+
+* Include `typeParameter` location in `ArrowFunctionExpression` ([#126](https://github.com/babel/babylon/pull/126)) (Daniel Tschinder)
+* Error on invalid flow type annotation with default assignment ([#122](https://github.com/babel/babylon/pull/122)) (Dan Harper)
+* Fix Flow return types on arrow functions ([#124](https://github.com/babel/babylon/pull/124)) (Dan Harper)
+
+### Misc
+
+* Add tests for export extensions ([#127](https://github.com/babel/babylon/pull/127)) (Daniel Tschinder)
+* Fix Contributing guidelines [skip ci] (Daniel Tschinder)
+
+## 6.9.2 (2016-09-09)
+
+The only change is to remove the `babel-runtime` dependency by compiling with Babel's ES2015 loose mode. So using babylon standalone should be smaller.
+
+## 6.9.1 (2016-08-23)
+
+This release contains mainly small bugfixes but also updates babylons default mode to es2017. The features for `exponentiationOperator`, `asyncFunctions` and `trailingFunctionCommas` which previously needed to be activated via plugin are now enabled by default and the plugins are now no-ops.
+
+### Bug Fixes
+
+- Fix issues with default object params in async functions ([#96](https://github.com/babel/babylon/pull/96)) @danez
+- Fix issues with flow-types and async function ([#95](https://github.com/babel/babylon/pull/95)) @danez
+- Fix arrow functions with destructuring, types & default value ([#94](https://github.com/babel/babylon/pull/94)) @danharper
+- Fix declare class with qualified type identifier ([#97](https://github.com/babel/babylon/pull/97)) @danez
+- Remove exponentiationOperator, asyncFunctions, trailingFunctionCommas plugins and enable them by default ([#98](https://github.com/babel/babylon/pull/98)) @danez
+
+## 6.9.0 (2016-08-16)
+
+### New syntax support
+
+- Add JSX spread children ([#42](https://github.com/babel/babylon/pull/42)) @calebmer
+
+(Be aware that React is not going to support this syntax)
+
+```js
+
+ {...todos.map(todo => )}
+
+```
+
+- Add support for declare module.exports ([#72](https://github.com/babel/babylon/pull/72)) @danez
+
+```js
+declare module "foo" {
+ declare module.exports: {}
+}
+```
+
+### New Features
+
+- If supplied, attach filename property to comment node loc. ([#80](https://github.com/babel/babylon/pull/80)) @divmain
+- Add identifier name to node loc field ([#90](https://github.com/babel/babylon/pull/90)) @kittens
+
+### Bug Fixes
+
+- Fix exponential operator to behave according to spec ([#75](https://github.com/babel/babylon/pull/75)) @danez
+- Fix lookahead to not add comments to arrays which are not cloned ([#76](https://github.com/babel/babylon/pull/76)) @danez
+- Fix accidental fall-through in Flow type parsing. ([#82](https://github.com/babel/babylon/pull/82)) @xiemaisi
+- Only allow declares inside declare module ([#73](https://github.com/babel/babylon/pull/73)) @danez
+- Small fix for parsing type parameter declarations ([#83](https://github.com/babel/babylon/pull/83)) @gabelevi
+- Fix arrow param locations with flow types ([#57](https://github.com/babel/babylon/pull/57)) @danez
+- Fixes SyntaxError position with flow optional type ([#65](https://github.com/babel/babylon/pull/65)) @danez
+
+### Internal
+
+- Add codecoverage to tests @danez
+- Fix tests to not save expected output if we expect the test to fail @danez
+- Make a shallow clone of babel for testing @danez
+- chore(package): update cross-env to version 2.0.0 ([#77](https://github.com/babel/babylon/pull/77)) @greenkeeperio-bot
+- chore(package): update ava to version 0.16.0 ([#86](https://github.com/babel/babylon/pull/86)) @greenkeeperio-bot
+- chore(package): update babel-plugin-istanbul to version 2.0.0 ([#89](https://github.com/babel/babylon/pull/89)) @greenkeeperio-bot
+- chore(package): update nyc to version 8.0.0 ([#88](https://github.com/babel/babylon/pull/88)) @greenkeeperio-bot
+
+## 6.8.4 (2016-07-06)
+
+### Bug Fixes
+
+- Fix the location of params, when flow and default value used ([#68](https://github.com/babel/babylon/pull/68)) @danez
+
+## 6.8.3 (2016-07-02)
+
+### Bug Fixes
+
+- Fix performance regression introduced in 6.8.2 with conditionals ([#63](https://github.com/babel/babylon/pull/63)) @danez
+
+## 6.8.2 (2016-06-24)
+
+### Bug Fixes
+
+- Fix parse error with yielding jsx elements in generators `function* it() { yield ; }` ([#31](https://github.com/babel/babylon/pull/31)) @eldereal
+- When cloning nodes do not clone its comments ([#24](https://github.com/babel/babylon/pull/24)) @danez
+- Fix parse errors when using arrow functions with an spread element and return type `(...props): void => {}` ([#10](https://github.com/babel/babylon/pull/10)) @danez
+- Fix leading comments added from previous node ([#23](https://github.com/babel/babylon/pull/23)) @danez
+- Fix parse errors with flow's optional arguments `(arg?) => {}` ([#19](https://github.com/babel/babylon/pull/19)) @danez
+- Support negative numeric type literals @kittens
+- Remove line terminator restriction after await keyword @kittens
+- Remove grouped type arrow restriction as it seems flow no longer has it @kittens
+- Fix parse error with generic methods that have the name `get` or `set` `class foo { get() {} }` ([#55](https://github.com/babel/babylon/pull/55)) @vkurchatkin
+- Fix parse error with arrow functions that have flow type parameter declarations `(x: T): T => x;` ([#54](https://github.com/babel/babylon/pull/54)) @gabelevi
+
+### Documentation
+
+- Document AST differences from ESTree ([#41](https://github.com/babel/babylon/pull/41)) @nene
+- Move ast spec from babel/babel ([#46](https://github.com/babel/babylon/pull/46)) @hzoo
+
+### Internal
+
+- Enable skipped tests ([#16](https://github.com/babel/babylon/pull/16)) @danez
+- Add script to test latest version of babylon with babel ([#21](https://github.com/babel/babylon/pull/21)) @danez
+- Upgrade test runner ava @kittens
+- Add missing generate-identifier-regex script @kittens
+- Rename parser context types @kittens
+- Add node v6 to travis testing @hzoo
+- Update to Unicode v9 ([#45](https://github.com/babel/babylon/pull/45)) @mathiasbynens
+
+## 6.8.1 (2016-06-06)
+
+### New Feature
+
+- Parse type parameter declarations with defaults like `type Foo = T`
+
+### Bug Fixes
+- Type parameter declarations need 1 or more type parameters.
+- The existential type `*` is not a valid type parameter.
+- The existential type `*` is a primary type
+
+### Spec Compliance
+- The param list for type parameter declarations now consists of `TypeParameter` nodes
+- New `TypeParameter` AST Node (replaces using the `Identifier` node before)
+
+```
+interface TypeParameter <: Node {
+ bound: TypeAnnotation;
+ default: TypeAnnotation;
+ name: string;
+ variance: "plus" | "minus";
+}
+```
+
+## 6.8.0 (2016-05-02)
+
+#### New Feature
+
+##### Parse Method Parameter Decorators ([#12](https://github.com/babel/babylon/pull/12))
+
+> [Method Parameter Decorators](https://goo.gl/8MmCMG) is now a TC39 [stage 0 proposal](https://github.com/tc39/ecma262/blob/master/stage0.md).
+
+Examples:
+
+```js
+class Foo {
+ constructor(@foo() x, @bar({ a: 123 }) @baz() y) {}
+}
+
+export default function func(@foo() x, @bar({ a: 123 }) @baz() y) {}
+
+var obj = {
+ method(@foo() x, @bar({ a: 123 }) @baz() y) {}
+};
+```
+
+##### Parse for-await statements (w/ `asyncGenerators` plugin) ([#17](https://github.com/babel/babylon/pull/17))
+
+There is also a new node type, `ForAwaitStatement`.
+
+> [Async generators and for-await](https://github.com/tc39/proposal-async-iteration) are now a [stage 2 proposal](https://github.com/tc39/ecma262#current-proposals).
+
+Example:
+
+```js
+async function f() {
+ for await (let x of y);
+}
+```
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/parser/LICENSE b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/parser/LICENSE
new file mode 100644
index 00000000..d4c7fc58
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/parser/LICENSE
@@ -0,0 +1,19 @@
+Copyright (C) 2012-2014 by various contributors (see AUTHORS)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/parser/README.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/parser/README.md
new file mode 100644
index 00000000..a9463e81
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/parser/README.md
@@ -0,0 +1,19 @@
+# @babel/parser
+
+> A JavaScript parser
+
+See our website [@babel/parser](https://babeljs.io/docs/babel-parser) for more information or the [issues](https://github.com/babel/babel/issues?utf8=%E2%9C%93&q=is%3Aissue+label%3A%22pkg%3A%20parser%22+is%3Aopen) associated with this package.
+
+## Install
+
+Using npm:
+
+```sh
+npm install --save-dev @babel/parser
+```
+
+or using yarn:
+
+```sh
+yarn add @babel/parser --dev
+```
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/parser/bin/babel-parser.js b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/parser/bin/babel-parser.js
new file mode 100755
index 00000000..4808c5ee
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/parser/bin/babel-parser.js
@@ -0,0 +1,15 @@
+#!/usr/bin/env node
+/* eslint-disable no-var, unicorn/prefer-node-protocol */
+
+var parser = require("..");
+var fs = require("fs");
+
+var filename = process.argv[2];
+if (!filename) {
+ console.error("no filename specified");
+} else {
+ var file = fs.readFileSync(filename, "utf8");
+ var ast = parser.parse(file);
+
+ console.log(JSON.stringify(ast, null, " "));
+}
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/parser/package.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/parser/package.json
new file mode 100644
index 00000000..817f2e49
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/parser/package.json
@@ -0,0 +1,50 @@
+{
+ "name": "@babel/parser",
+ "version": "7.29.0",
+ "description": "A JavaScript parser",
+ "author": "The Babel Team (https://babel.dev/team)",
+ "homepage": "https://babel.dev/docs/en/next/babel-parser",
+ "bugs": "https://github.com/babel/babel/issues?utf8=%E2%9C%93&q=is%3Aissue+label%3A%22pkg%3A+parser+%28babylon%29%22+is%3Aopen",
+ "license": "MIT",
+ "publishConfig": {
+ "access": "public"
+ },
+ "keywords": [
+ "babel",
+ "javascript",
+ "parser",
+ "tc39",
+ "ecmascript",
+ "@babel/parser"
+ ],
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/babel/babel.git",
+ "directory": "packages/babel-parser"
+ },
+ "main": "./lib/index.js",
+ "types": "./typings/babel-parser.d.ts",
+ "files": [
+ "bin",
+ "lib",
+ "typings/babel-parser.d.ts",
+ "index.cjs"
+ ],
+ "engines": {
+ "node": ">=6.0.0"
+ },
+ "# dependencies": "This package doesn't actually have runtime dependencies. @babel/types is only needed for type definitions.",
+ "dependencies": {
+ "@babel/types": "^7.29.0"
+ },
+ "devDependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/helper-check-duplicate-nodes": "^7.28.6",
+ "@babel/helper-fixtures": "^7.28.6",
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "charcodes": "^0.2.0"
+ },
+ "bin": "./bin/babel-parser.js",
+ "type": "commonjs"
+}
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/parser/typings/babel-parser.d.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/parser/typings/babel-parser.d.ts
new file mode 100644
index 00000000..d083b0ab
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/parser/typings/babel-parser.d.ts
@@ -0,0 +1,262 @@
+// This file is auto-generated! Do not modify it directly.
+// Run `yarn gulp bundle-dts` to re-generate it.
+/* eslint-disable @typescript-eslint/consistent-type-imports, @typescript-eslint/no-redundant-type-constituents */
+import { File, Expression } from '@babel/types';
+
+declare class Position {
+ line: number;
+ column: number;
+ index: number;
+ constructor(line: number, col: number, index: number);
+}
+
+type SyntaxPlugin = "flow" | "typescript" | "jsx" | "pipelineOperator" | "placeholders";
+type ParseErrorCode = "BABEL_PARSER_SYNTAX_ERROR" | "BABEL_PARSER_SOURCETYPE_MODULE_REQUIRED";
+interface ParseErrorSpecification {
+ code: ParseErrorCode;
+ reasonCode: string;
+ syntaxPlugin?: SyntaxPlugin;
+ missingPlugin?: string | string[];
+ loc: Position;
+ details: ErrorDetails;
+ pos: number;
+}
+type ParseError$1 = SyntaxError & ParseErrorSpecification;
+
+type BABEL_8_BREAKING = false;
+type IF_BABEL_7 = false extends BABEL_8_BREAKING ? V : never;
+
+type Plugin$1 =
+ | "asyncDoExpressions"
+ | IF_BABEL_7<"asyncGenerators">
+ | IF_BABEL_7<"bigInt">
+ | IF_BABEL_7<"classPrivateMethods">
+ | IF_BABEL_7<"classPrivateProperties">
+ | IF_BABEL_7<"classProperties">
+ | IF_BABEL_7<"classStaticBlock">
+ | IF_BABEL_7<"decimal">
+ | "decorators-legacy"
+ | "deferredImportEvaluation"
+ | "decoratorAutoAccessors"
+ | "destructuringPrivate"
+ | IF_BABEL_7<"deprecatedImportAssert">
+ | "doExpressions"
+ | IF_BABEL_7<"dynamicImport">
+ | IF_BABEL_7<"explicitResourceManagement">
+ | "exportDefaultFrom"
+ | IF_BABEL_7<"exportNamespaceFrom">
+ | "flow"
+ | "flowComments"
+ | "functionBind"
+ | "functionSent"
+ | "importMeta"
+ | "jsx"
+ | IF_BABEL_7<"jsonStrings">
+ | IF_BABEL_7<"logicalAssignment">
+ | IF_BABEL_7<"importAssertions">
+ | IF_BABEL_7<"importReflection">
+ | "moduleBlocks"
+ | IF_BABEL_7<"moduleStringNames">
+ | IF_BABEL_7<"nullishCoalescingOperator">
+ | IF_BABEL_7<"numericSeparator">
+ | IF_BABEL_7<"objectRestSpread">
+ | IF_BABEL_7<"optionalCatchBinding">
+ | IF_BABEL_7<"optionalChaining">
+ | "partialApplication"
+ | "placeholders"
+ | IF_BABEL_7<"privateIn">
+ | IF_BABEL_7<"regexpUnicodeSets">
+ | "sourcePhaseImports"
+ | "throwExpressions"
+ | IF_BABEL_7<"topLevelAwait">
+ | "v8intrinsic"
+ | ParserPluginWithOptions[0];
+
+type ParserPluginWithOptions =
+ | ["decorators", DecoratorsPluginOptions]
+ | ["discardBinding", { syntaxType: "void" }]
+ | ["estree", { classFeatures?: boolean }]
+ | IF_BABEL_7<["importAttributes", { deprecatedAssertSyntax: boolean }]>
+ | IF_BABEL_7<["moduleAttributes", { version: "may-2020" }]>
+ | ["optionalChainingAssign", { version: "2023-07" }]
+ | ["pipelineOperator", PipelineOperatorPluginOptions]
+ | ["recordAndTuple", RecordAndTuplePluginOptions]
+ | ["flow", FlowPluginOptions]
+ | ["typescript", TypeScriptPluginOptions];
+
+type PluginConfig = Plugin$1 | ParserPluginWithOptions;
+
+interface DecoratorsPluginOptions {
+ decoratorsBeforeExport?: boolean;
+ allowCallParenthesized?: boolean;
+}
+
+interface PipelineOperatorPluginOptions {
+ proposal: BABEL_8_BREAKING extends false
+ ? "minimal" | "fsharp" | "hack" | "smart"
+ : "fsharp" | "hack";
+ topicToken?: "%" | "#" | "@@" | "^^" | "^";
+}
+
+interface RecordAndTuplePluginOptions {
+ syntaxType: "bar" | "hash";
+}
+
+type FlowPluginOptions = BABEL_8_BREAKING extends true
+ ? {
+ all?: boolean;
+ enums?: boolean;
+ }
+ : {
+ all?: boolean;
+ };
+
+interface TypeScriptPluginOptions {
+ dts?: boolean;
+ disallowAmbiguousJSXLike?: boolean;
+}
+
+type Plugin = PluginConfig;
+
+type SourceType = "script" | "commonjs" | "module" | "unambiguous";
+interface Options {
+ /**
+ * By default, import and export declarations can only appear at a program's top level.
+ * Setting this option to true allows them anywhere where a statement is allowed.
+ */
+ allowImportExportEverywhere?: boolean;
+ /**
+ * By default, await use is not allowed outside of an async function.
+ * Set this to true to accept such code.
+ */
+ allowAwaitOutsideFunction?: boolean;
+ /**
+ * By default, a return statement at the top level raises an error.
+ * Set this to true to accept such code.
+ */
+ allowReturnOutsideFunction?: boolean;
+ /**
+ * By default, new.target use is not allowed outside of a function or class.
+ * Set this to true to accept such code.
+ */
+ allowNewTargetOutsideFunction?: boolean;
+ /**
+ * By default, super calls are not allowed outside of a method.
+ * Set this to true to accept such code.
+ */
+ allowSuperOutsideMethod?: boolean;
+ /**
+ * By default, exported identifiers must refer to a declared variable.
+ * Set this to true to allow export statements to reference undeclared variables.
+ */
+ allowUndeclaredExports?: boolean;
+ /**
+ * By default, yield use is not allowed outside of a generator function.
+ * Set this to true to accept such code.
+ */
+ allowYieldOutsideFunction?: boolean;
+ /**
+ * By default, Babel parser JavaScript code according to Annex B syntax.
+ * Set this to `false` to disable such behavior.
+ */
+ annexB?: boolean;
+ /**
+ * By default, Babel attaches comments to adjacent AST nodes.
+ * When this option is set to false, comments are not attached.
+ * It can provide up to 30% performance improvement when the input code has many comments.
+ * @babel/eslint-parser will set it for you.
+ * It is not recommended to use attachComment: false with Babel transform,
+ * as doing so removes all the comments in output code, and renders annotations such as
+ * /* istanbul ignore next *\/ nonfunctional.
+ */
+ attachComment?: boolean;
+ /**
+ * By default, Babel always throws an error when it finds some invalid code.
+ * When this option is set to true, it will store the parsing error and
+ * try to continue parsing the invalid input file.
+ */
+ errorRecovery?: boolean;
+ /**
+ * Indicate the mode the code should be parsed in.
+ * Can be one of "script", "commonjs", "module", or "unambiguous". Defaults to "script".
+ * "unambiguous" will make @babel/parser attempt to guess, based on the presence
+ * of ES6 import or export statements.
+ * Files with ES6 imports and exports are considered "module" and are otherwise "script".
+ *
+ * Use "commonjs" to parse code that is intended to be run in a CommonJS environment such as Node.js.
+ */
+ sourceType?: SourceType;
+ /**
+ * Correlate output AST nodes with their source filename.
+ * Useful when generating code and source maps from the ASTs of multiple input files.
+ */
+ sourceFilename?: string;
+ /**
+ * By default, all source indexes start from 0.
+ * You can provide a start index to alternatively start with.
+ * Useful for integration with other source tools.
+ */
+ startIndex?: number;
+ /**
+ * By default, the first line of code parsed is treated as line 1.
+ * You can provide a line number to alternatively start with.
+ * Useful for integration with other source tools.
+ */
+ startLine?: number;
+ /**
+ * By default, the parsed code is treated as if it starts from line 1, column 0.
+ * You can provide a column number to alternatively start with.
+ * Useful for integration with other source tools.
+ */
+ startColumn?: number;
+ /**
+ * Array containing the plugins that you want to enable.
+ */
+ plugins?: Plugin[];
+ /**
+ * Should the parser work in strict mode.
+ * Defaults to true if sourceType === 'module'. Otherwise, false.
+ */
+ strictMode?: boolean;
+ /**
+ * Adds a ranges property to each node: [node.start, node.end]
+ */
+ ranges?: boolean;
+ /**
+ * Adds all parsed tokens to a tokens property on the File node.
+ */
+ tokens?: boolean;
+ /**
+ * By default, the parser adds information about parentheses by setting
+ * `extra.parenthesized` to `true` as needed.
+ * When this option is `true` the parser creates `ParenthesizedExpression`
+ * AST nodes instead of using the `extra` property.
+ */
+ createParenthesizedExpressions?: boolean;
+ /**
+ * The default is false in Babel 7 and true in Babel 8
+ * Set this to true to parse it as an `ImportExpression` node.
+ * Otherwise `import(foo)` is parsed as `CallExpression(Import, [Identifier(foo)])`.
+ */
+ createImportExpressions?: boolean;
+}
+
+type ParserOptions = Partial;
+type ParseError = ParseError$1;
+type ParseResult = Result & {
+ comments: File["comments"];
+ errors: null | ParseError[];
+ tokens?: File["tokens"];
+};
+/**
+ * Parse the provided code as an entire ECMAScript program.
+ */
+declare function parse(input: string, options?: ParserOptions): ParseResult;
+declare function parseExpression(input: string, options?: ParserOptions): ParseResult;
+
+declare const tokTypes: {
+ // todo(flow->ts) real token type
+ [name: string]: any;
+};
+
+export { DecoratorsPluginOptions, FlowPluginOptions, ParseError, ParseResult, ParserOptions, PluginConfig as ParserPlugin, ParserPluginWithOptions, PipelineOperatorPluginOptions, RecordAndTuplePluginOptions, TypeScriptPluginOptions, parse, parseExpression, tokTypes };
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/types/LICENSE b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/types/LICENSE
new file mode 100644
index 00000000..f31575ec
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/types/LICENSE
@@ -0,0 +1,22 @@
+MIT License
+
+Copyright (c) 2014-present Sebastian McKenzie and other contributors
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/types/README.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/types/README.md
new file mode 100644
index 00000000..54c9f819
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/types/README.md
@@ -0,0 +1,19 @@
+# @babel/types
+
+> Babel Types is a Lodash-esque utility library for AST nodes
+
+See our website [@babel/types](https://babeljs.io/docs/babel-types) for more information or the [issues](https://github.com/babel/babel/issues?utf8=%E2%9C%93&q=is%3Aissue+label%3A%22pkg%3A%20types%22+is%3Aopen) associated with this package.
+
+## Install
+
+Using npm:
+
+```sh
+npm install --save-dev @babel/types
+```
+
+or using yarn:
+
+```sh
+yarn add @babel/types --dev
+```
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/types/package.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/types/package.json
new file mode 100644
index 00000000..db753538
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@babel/types/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "@babel/types",
+ "version": "7.29.0",
+ "description": "Babel Types is a Lodash-esque utility library for AST nodes",
+ "author": "The Babel Team (https://babel.dev/team)",
+ "homepage": "https://babel.dev/docs/en/next/babel-types",
+ "bugs": "https://github.com/babel/babel/issues?utf8=%E2%9C%93&q=is%3Aissue+label%3A%22pkg%3A%20types%22+is%3Aopen",
+ "license": "MIT",
+ "publishConfig": {
+ "access": "public"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/babel/babel.git",
+ "directory": "packages/babel-types"
+ },
+ "main": "./lib/index.js",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "devDependencies": {
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-fixtures": "^7.28.6",
+ "@babel/parser": "^7.29.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "type": "commonjs",
+ "types": "./lib/index-legacy.d.ts",
+ "typesVersions": {
+ ">=4.1": {
+ "lib/index-legacy.d.ts": [
+ "lib/index.d.ts"
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@bcoe/v8-coverage/.editorconfig b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@bcoe/v8-coverage/.editorconfig
new file mode 100644
index 00000000..86a63dc0
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@bcoe/v8-coverage/.editorconfig
@@ -0,0 +1,9 @@
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+indent_style = space
+indent_size = 2
+insert_final_newline = true
+trim_trailing_whitespace = true
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@bcoe/v8-coverage/.gitattributes b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@bcoe/v8-coverage/.gitattributes
new file mode 100644
index 00000000..4b2c1a29
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@bcoe/v8-coverage/.gitattributes
@@ -0,0 +1,2 @@
+# Enforce `lf` for text files (even on Windows)
+text eol=lf
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@bcoe/v8-coverage/CHANGELOG.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@bcoe/v8-coverage/CHANGELOG.md
new file mode 100644
index 00000000..7300dec3
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@bcoe/v8-coverage/CHANGELOG.md
@@ -0,0 +1,250 @@
+## Next
+
+- **[Breaking change]** Replace `OutModules` enum by custom compiler option `mjsModule`.
+- **[Breaking change]** Drop support for Pug, Sass, Angular & Webpack.
+- **[Feature]** Expose custom registries for each target.
+- **[Feature]** Add `dist.tscOptions` for `lib` target to override options for
+ distribution builds.
+- **[Feature]** Native ESM tests with mocha.
+- **[Fix]** Disable deprecated TsLint rules from the default config
+- **[Fix]** Remove use of experimental `fs/promises` module.
+- **[Internal]** Fix continuous deployment script (stop confusing PRs to master
+ with push to master)
+- **[Internal]** Update dependencies
+- **[Internal]** Fix deprecated Mocha types.
+
+## 0.17.1 (2017-05-03)
+
+- **[Fix]** Update dependencies, remove `std/esm` warning.
+
+## 0.17.0 (2017-04-22)
+
+- **[Breaking change]** Update dependencies. Use `esm` instead of `@std/esm`, update Typescript to `2.8.3`.
+- **[Fix]** Fix Node processes spawn on Windows (Mocha, Nyc)
+
+## 0.16.2 (2017-02-07)
+
+- **[Fix]** Fix Typedoc generation: use `tsconfig.json` generated for the lib.
+- **[Fix]** Write source map for `.mjs` files
+- **[Fix]** Copy sources to `_src` when publishing a lib (#87).
+- **[Internal]** Restore continuous deployment of documentation.
+
+## 0.16.1 (2017-01-20)
+
+- **[Feature]** Support `mocha` tests on `.mjs` files (using `@std/esm`). Enabled by default
+ if `outModules` is configured to emit `.mjs`. **You currently need to add
+ `"@std/esm": {"esm": "cjs"}` to your `package.json`.**
+
+## 0.16.0 (2017-01-09)
+
+- **[Breaking change]** Enable `allowSyntheticDefaultImports` and `esModuleInterop` by default
+- **[Fix]** Allow deep module imports in default Tslint rules
+- **[Fix]** Drop dependency on deprecated `gulp-util`
+- **[Internal]** Replace most custom typings by types from `@types`
+
+## 0.15.8 (2017-12-05)
+
+- **[Fix]** Exit with non-zero code if command tested with coverage fails
+- **[Fix]** Solve duplicated error message when using the `run` mocha task.
+- **[Fix]** Exit with non-zero code when building scripts fails.
+
+## 0.15.7 (2017-11-29)
+
+- **[Feature]** Add `coverage` task to `mocha` target, use it for the default task
+
+## 0.15.6 (2017-11-29)
+
+- **[Fix]** Fix path to source in source maps.
+- **[Fix]** Disable `number-literal-format` in default Tslint rules. It enforced uppercase for hex.
+- **[Internal]** Enable integration with Greenkeeper.
+- **[Internal]** Enable integration with Codecov
+- **[Internal]** Enable code coverage
+
+## 0.15.5 (2017-11-10)
+
+- **[Feature]** Enable the following TsLint rules: `no-duplicate-switch-case`, `no-implicit-dependencies`,
+ `no-return-await`
+- **[Internal]** Update self-dependency `0.15.4`, this restores the README on _npm_
+- **[Internal]** Add homepage and author fields to package.json
+
+## 0.15.4 (2017-11-10)
+
+- **[Fix]** Add support for custom additional copy for distribution builds. [#49](https://github.com/demurgos/turbo-gulp/issues/49)
+- **[Internal]** Update self-dependency to `turbo-gulp`
+- **[Internal]** Add link to license in `README.md`
+
+## 0.15.3 (2017-11-09)
+
+**Rename to `turbo-gulp`**. This package was previously named `demurgos-web-build-tools`.
+This version is fully compatible: you can just change the name of your dependency.
+
+## 0.15.2 (2017-11-09)
+
+**The package is prepared to be renamed `turbo-gulp`.**
+This is the last version released as `demurgos-web-build-tools`.
+
+- **[Feature]** Add support for watch mode for library targets.
+- **[Fix]** Disable experimental support for `*.mjs` by default.
+- **[Fix]** Do not emit duplicate TS errors
+
+## 0.15.1 (2017-10-19)
+
+- **[Feature]** Add experimental support for `*.mjs` files
+- **[Fix]** Fix support of releases from Continuous Deployment using Travis.
+
+## 0.15.0 (2017-10-18)
+
+- **[Fix]** Add error handling for git deployment.
+- **[Internal]** Enable continuous deployment of the `master` branch.
+
+## 0.15.0-beta.11 (2017-08-29)
+
+- **[Feature]** Add `LibTarget.dist.copySrc` option to disable copy of source files to the dist directory.
+ This allows to prevent issues with missing custom typings.
+- **[Fix]** Mark `deploy` property of `LibTarget.typedoc` as optional.
+- **[Internal]** Update self-dependency to `v0.15.0-beta.10`.
+
+## 0.15.0-beta.10 (2017-08-28)
+
+- **[Breaking]** Update Tslint rules to use `tslint@5.7.0`.
+- **[Fix]** Set `allowJs` to false in default TSC options.
+- **[Fix]** Do not pipe output of git commands to stdout.
+- **[Internal]** Update self-dependency to `v0.15.0-beta.9`.
+
+## 0.15.0-beta.9 (2017-08-28)
+
+- **[Breaking]** Drop old-style `test` target.
+- **[Breaking]** Drop old-style `node` target.
+- **[Feature]** Add `mocha` target to run tests in `spec.ts` files.
+- **[Feature]** Add `node` target to build and run top-level Node applications.
+- **[Feature]** Provide `generateNodeTasks`, `generateLibTasks` and `generateMochaTasks` functions.
+ They create the tasks but do not register them.
+- **[Fix]** Run `clean` before `dist`, if defined.
+- **[Fix]** Run `dist` before `publish`.
+
+## 0.15.0-beta.8 (2017-08-26)
+
+- **[Fix]** Remove auth token and registry options for `:dist:publish`. It is better served
+ by configuring the environment appropriately.
+
+## 0.15.0-beta.7 (2017-08-26)
+
+- **[Feature]** Add `clean` task to `lib` targets.
+- **[Fix]** Ensure that `gitHead` is defined when publishing a package to npm.
+
+## 0.15.0-beta.6 (2017-08-22)
+
+- **[Feature]** Add support for Typedoc deployment to a remote git branch (such as `gh-pages`)
+- **[Feature]** Add support for `copy` tasks in new library target.
+- **[Fix]** Resolve absolute paths when compiling scripts with custom typings.
+
+## 0.15.0-beta.5 (2017-08-14)
+
+- **[Fix]** Fix package entry for the main module.
+
+## 0.15.0-beta.4 (2017-08-14)
+
+- **[Breaking]** Drop ES5 build exposed to browsers with the `browser` field in `package.json`.
+- **[Feature]** Introduce first new-style target (`LibTarget`). it supports typedoc generation, dev builds and
+ simple distribution.
+
+## 0.15.0-beta.3 (2017-08-11)
+
+- **[Breaking]** Update default lib target to use target-specific `srcDir`.
+- **[Feature]** Allow to complete `srcDir` in target.
+- **[Feature]** Add experimental library distribution supporting deep requires.
+
+## 0.15.0-beta.2 (2017-08-10)
+
+- **[Fix]** Default to CommonJS for project tsconfig.json
+- **[Fix]** Add Typescript configuration for default project.
+- **[Internal]** Update self-dependency to `0.15.0-beta.1`.
+
+## 0.15.0-beta.1 (2017-08-09)
+
+- **[Feature]** Support typed TSLint rules.
+- **[Internal]** Update gulpfile.ts to use build tools `0.15.0-beta.0`.
+- **[Fix]** Fix regressions caused by `0.15.0-beta.0` (missing type definition).
+
+## 0.15.0-beta.0 (2017-08-09)
+
+- **[Breaking]** Expose option interfaces directly in the main module instead of the `config` namespace.
+- **[Breaking]** Rename `DEFAULT_PROJECT_OPTIONS` to `DEFAULT_PROJECT`.
+- **[Feature]** Emit project-wide `tsconfig.json`.
+- **[Internal]** Convert gulpfile to Typescript, use `ts-node` to run it.
+- **[Internal]** Update dependencies
+
+## 0.14.3 (2017-07-16)
+
+- **[Feature]** Add `:lint:fix` project task to fix some lint errors.
+
+## 0.14.2 (2017-07-10)
+
+- **[Internal]** Update dependencies: add `package-lock.json` and update `tslint`.
+
+## 0.14.1 (2017-06-17)
+
+- **[Internal]** Update dependencies.
+- **[Internal]** Drop dependency on _Bluebird_.
+- **[Internal]** Drop dependency on _typings_.
+
+## 0.14.0 (2017-05-10)
+
+- **[Breaking]** Enforce trailing commas by default for multiline objects
+- **[Feature]** Allow bump from either `master` or a branch with the same name as the tag (exampel: `v1.2.3`)
+- **[Feature]** Support TSLint 8, allow to extend the default rules
+- **[Patch]** Allow mergeable namespaces
+
+# 0.13.1
+
+- **[Patch]** Allow namespaces in the default TS-Lint config
+
+# 0.13.0
+
+- **[Breaking]** Major overhaul of the angular target. The server build no longer depends on the client.
+- **[Breaking]** Update to `gulp@4` (from `gulp@3`)
+- **[Breaking]** Update to `tslint@7` (from `tslint@6`), add stricter default rules
+- **[Breaking]** Update signature of targetGenerators and project tasks: it only uses
+ `ProjectOptions` and `Target` now, the additional options are embedded in those two objects.
+- **[Breaking]** Remove `:install`, `:instal:npm` and `:install:typings`. Use the `prepare` script in
+ your `package.json` file instead.
+- Add `:tslint.json` project task to generate configuration for `tslint`
+- Add first class support for processing of `pug` and `sass` files, similar to `copy`
+- Implement end-to-end tests
+- Enable `emitDecoratorMetadata` in default typescript options.
+- Allow configuration of `:lint` with the `tslintOptions` property of the project configuration.
+- Add `:watch` tasks for incremental builds.
+
+# 0.12.3
+
+- Support `templateUrl` and `styleUrls` in angular modules.
+
+# 0.12.2
+
+- Add `:build:copy` task. It copies user-defined files.
+
+# 0.12.1
+
+- Fix `:watch` task.
+
+# 0.12.0
+
+- **[Breaking]**: Change naming convention for tasks. The names primary part is
+ the target, then the action (`lib:build` instead of `build:lib`) to group
+ the tasks per target.
+- **[Breaking]**: Use `typeRoots` instead of `definitions` in configuration to
+ specify Typescript definition files.
+- Generate `tsconfig.json` file (mainly for editors)
+- Implement the `test` target to run unit-tests with `mocha`.
+
+# 0.11.2
+
+- Target `angular`: Add `build::assets:sass` for `.scss` files (Sassy CSS)
+
+# 0.11.1
+
+- Rename project to `web-build-tools` (`demurgos-web-build-tools` on _npm_)
+- Target `angular`: Add `build::assets`, `build::pug` and `build::static`.
+- Update `gulp-typescript`: solve error message during compilation
+- Targets `node` and `angular`: `build::scripts` now include in-lined source maps
+- Target `node`: `watch:` to support incremental builds
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@bcoe/v8-coverage/LICENSE.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@bcoe/v8-coverage/LICENSE.md
new file mode 100644
index 00000000..d588b5c3
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@bcoe/v8-coverage/LICENSE.md
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright © 2015-2017 Charles Samborski
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@bcoe/v8-coverage/LICENSE.txt b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@bcoe/v8-coverage/LICENSE.txt
new file mode 100644
index 00000000..629264e9
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@bcoe/v8-coverage/LICENSE.txt
@@ -0,0 +1,14 @@
+Copyright (c) 2017, Contributors
+
+Permission to use, copy, modify, and/or distribute this software
+for any purpose with or without fee is hereby granted, provided
+that the above copyright notice and this permission notice
+appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES
+OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE
+LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES
+OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
+WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
+ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@bcoe/v8-coverage/README.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@bcoe/v8-coverage/README.md
new file mode 100644
index 00000000..eea761b9
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@bcoe/v8-coverage/README.md
@@ -0,0 +1,11 @@
+# V8 Coverage
+
+[](https://www.npmjs.com/package/@c88/v8-coverage)
+[](https://github.com/demurgos/v8-coverage)
+[](https://travis-ci.org/demurgos/v8-coverage)
+[](https://ci.appveyor.com/project/demurgos/v8-coverage)
+[](https://codecov.io/gh/demurgos/v8-coverage)
+
+## License
+
+[MIT License](./LICENSE.md)
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@bcoe/v8-coverage/gulpfile.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@bcoe/v8-coverage/gulpfile.ts
new file mode 100644
index 00000000..cdcfc818
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@bcoe/v8-coverage/gulpfile.ts
@@ -0,0 +1,95 @@
+import * as buildTools from "turbo-gulp";
+import { LibTarget, registerLibTasks } from "turbo-gulp/targets/lib";
+import { MochaTarget, registerMochaTasks } from "turbo-gulp/targets/mocha";
+
+import gulp from "gulp";
+import minimist from "minimist";
+
+interface Options {
+ devDist?: string;
+}
+
+const options: Options & minimist.ParsedArgs = minimist(process.argv.slice(2), {
+ string: ["devDist"],
+ default: {devDist: undefined},
+ alias: {devDist: "dev-dist"},
+});
+
+const project: buildTools.Project = {
+ root: __dirname,
+ packageJson: "package.json",
+ buildDir: "build",
+ distDir: "dist",
+ srcDir: "src",
+ typescript: {}
+};
+
+const lib: LibTarget = {
+ project,
+ name: "lib",
+ srcDir: "src/lib",
+ scripts: ["**/*.ts"],
+ mainModule: "index",
+ dist: {
+ packageJsonMap: (old: buildTools.PackageJson): buildTools.PackageJson => {
+ const version: string = options.devDist !== undefined ? `${old.version}-build.${options.devDist}` : old.version;
+ return {...old, version, scripts: undefined, private: false};
+ },
+ npmPublish: {
+ tag: options.devDist !== undefined ? "next" : "latest",
+ },
+ },
+ tscOptions: {
+ declaration: true,
+ skipLibCheck: true,
+ },
+ typedoc: {
+ dir: "typedoc",
+ name: "Helpers for V8 coverage files",
+ deploy: {
+ repository: "git@github.com:demurgos/v8-coverage.git",
+ branch: "gh-pages",
+ },
+ },
+ copy: [
+ {
+ files: ["**/*.json"],
+ },
+ ],
+ clean: {
+ dirs: ["build/lib", "dist/lib"],
+ },
+};
+
+const test: MochaTarget = {
+ project,
+ name: "test",
+ srcDir: "src",
+ scripts: ["test/**/*.ts", "lib/**/*.ts", "e2e/*/*.ts"],
+ customTypingsDir: "src/custom-typings",
+ tscOptions: {
+ allowSyntheticDefaultImports: true,
+ esModuleInterop: true,
+ skipLibCheck: true,
+ },
+ // generateTestMain: true,
+ copy: [
+ {
+ src: "e2e",
+ // /(project|test-resources)/
+ files: ["*/project/**/*", "*/test-resources/**/*"],
+ dest: "e2e",
+ },
+ ],
+ clean: {
+ dirs: ["build/test"],
+ },
+};
+
+const libTasks: any = registerLibTasks(gulp, lib);
+registerMochaTasks(gulp, test);
+buildTools.projectTasks.registerAll(gulp, project);
+
+gulp.task("all:tsconfig.json", gulp.parallel("lib:tsconfig.json", "test:tsconfig.json"));
+gulp.task("dist", libTasks.dist);
+gulp.task("default", libTasks.dist);
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@bcoe/v8-coverage/package.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@bcoe/v8-coverage/package.json
new file mode 100644
index 00000000..abc28b78
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@bcoe/v8-coverage/package.json
@@ -0,0 +1,48 @@
+{
+ "name": "@bcoe/v8-coverage",
+ "version": "0.2.3",
+ "description": "Helper functions for V8 coverage files.",
+ "author": "Charles Samborski (https://demurgos.net)",
+ "license": "MIT",
+ "main": "dist/lib/index",
+ "types": "dist/lib/index.d.ts",
+ "repository": {
+ "type": "git",
+ "url": "git://github.com/demurgos/v8-coverage.git"
+ },
+ "homepage": "https://demurgos.github.io/v8-coverage",
+ "scripts": {
+ "prepare": "gulp all:tsconfig.json && gulp dist",
+ "pretest": "gulp lib:build",
+ "test": "gulp test",
+ "lint": "gulp :lint:fix"
+ },
+ "devDependencies": {
+ "@types/chai": "^4.1.4",
+ "@types/gulp": "^4.0.5",
+ "@types/minimist": "^1.2.0",
+ "@types/mocha": "^5.2.2",
+ "@types/node": "^10.5.4",
+ "chai": "^4.1.2",
+ "codecov": "^3.0.2",
+ "gulp": "^4.0.0",
+ "gulp-cli": "^2.0.1",
+ "minimist": "^1.2.0",
+ "pre-commit": "^1.2.2",
+ "ts-node": "^8.3.0",
+ "turbo-gulp": "^0.20.1"
+ },
+ "nyc": {
+ "include": [
+ "build/test/lib/**/*.js",
+ "build/test/lib/**/*.mjs"
+ ],
+ "reporter": [
+ "text",
+ "html"
+ ],
+ "extension": [
+ ".mjs"
+ ]
+ }
+}
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@bcoe/v8-coverage/src/test/merge.spec.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@bcoe/v8-coverage/src/test/merge.spec.ts
new file mode 100644
index 00000000..9d5522a2
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@bcoe/v8-coverage/src/test/merge.spec.ts
@@ -0,0 +1,280 @@
+import chai from "chai";
+import fs from "fs";
+import path from "path";
+import { FunctionCov, mergeFunctionCovs, mergeProcessCovs, mergeScriptCovs, ProcessCov, ScriptCov } from "../lib";
+
+const REPO_ROOT: string = path.join(__dirname, "..", "..", "..", "..");
+const BENCHES_INPUT_DIR: string = path.join(REPO_ROOT, "benches");
+const BENCHES_DIR: string = path.join(REPO_ROOT, "test-data", "merge", "benches");
+const RANGES_DIR: string = path.join(REPO_ROOT, "test-data", "merge", "ranges");
+const BENCHES_TIMEOUT: number = 20000; // 20sec
+
+interface MergeRangeItem {
+ name: string;
+ status: "run" | "skip" | "only";
+ inputs: ProcessCov[];
+ expected: ProcessCov;
+}
+
+const FIXTURES_DIR: string = path.join(REPO_ROOT, "test-data", "bugs");
+function loadFixture(name: string) {
+ const content: string = fs.readFileSync(
+ path.resolve(FIXTURES_DIR, `${name}.json`),
+ {encoding: "UTF-8"},
+ );
+ return JSON.parse(content);
+}
+
+describe("merge", () => {
+ describe("Various", () => {
+ it("accepts empty arrays for `mergeProcessCovs`", () => {
+ const inputs: ProcessCov[] = [];
+ const expected: ProcessCov = {result: []};
+ const actual: ProcessCov = mergeProcessCovs(inputs);
+ chai.assert.deepEqual(actual, expected);
+ });
+
+ it("accepts empty arrays for `mergeScriptCovs`", () => {
+ const inputs: ScriptCov[] = [];
+ const expected: ScriptCov | undefined = undefined;
+ const actual: ScriptCov | undefined = mergeScriptCovs(inputs);
+ chai.assert.deepEqual(actual, expected);
+ });
+
+ it("accepts empty arrays for `mergeFunctionCovs`", () => {
+ const inputs: FunctionCov[] = [];
+ const expected: FunctionCov | undefined = undefined;
+ const actual: FunctionCov | undefined = mergeFunctionCovs(inputs);
+ chai.assert.deepEqual(actual, expected);
+ });
+
+ it("accepts arrays with a single item for `mergeProcessCovs`", () => {
+ const inputs: ProcessCov[] = [
+ {
+ result: [
+ {
+ scriptId: "123",
+ url: "/lib.js",
+ functions: [
+ {
+ functionName: "test",
+ isBlockCoverage: true,
+ ranges: [
+ {startOffset: 0, endOffset: 4, count: 2},
+ {startOffset: 1, endOffset: 2, count: 1},
+ {startOffset: 2, endOffset: 3, count: 1},
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ];
+ const expected: ProcessCov = {
+ result: [
+ {
+ scriptId: "0",
+ url: "/lib.js",
+ functions: [
+ {
+ functionName: "test",
+ isBlockCoverage: true,
+ ranges: [
+ {startOffset: 0, endOffset: 4, count: 2},
+ {startOffset: 1, endOffset: 3, count: 1},
+ ],
+ },
+ ],
+ },
+ ],
+ };
+ const actual: ProcessCov = mergeProcessCovs(inputs);
+ chai.assert.deepEqual(actual, expected);
+ });
+
+ describe("mergeProcessCovs", () => {
+ // see: https://github.com/demurgos/v8-coverage/issues/2
+ it("handles function coverage merged into block coverage", () => {
+ const blockCoverage: ProcessCov = loadFixture("issue-2-block-coverage");
+ const functionCoverage: ProcessCov = loadFixture("issue-2-func-coverage");
+ const inputs: ProcessCov[] = [
+ functionCoverage,
+ blockCoverage,
+ ];
+ const expected: ProcessCov = loadFixture("issue-2-expected");
+ const actual: ProcessCov = mergeProcessCovs(inputs);
+ chai.assert.deepEqual(actual, expected);
+ });
+
+ // see: https://github.com/demurgos/v8-coverage/issues/2
+ it("handles block coverage merged into function coverage", () => {
+ const blockCoverage: ProcessCov = loadFixture("issue-2-block-coverage");
+ const functionCoverage: ProcessCov = loadFixture("issue-2-func-coverage");
+ const inputs: ProcessCov[] = [
+ blockCoverage,
+ functionCoverage,
+ ];
+ const expected: ProcessCov = loadFixture("issue-2-expected");
+ const actual: ProcessCov = mergeProcessCovs(inputs);
+ chai.assert.deepEqual(actual, expected);
+ });
+ });
+
+ it("accepts arrays with a single item for `mergeScriptCovs`", () => {
+ const inputs: ScriptCov[] = [
+ {
+ scriptId: "123",
+ url: "/lib.js",
+ functions: [
+ {
+ functionName: "test",
+ isBlockCoverage: true,
+ ranges: [
+ {startOffset: 0, endOffset: 4, count: 2},
+ {startOffset: 1, endOffset: 2, count: 1},
+ {startOffset: 2, endOffset: 3, count: 1},
+ ],
+ },
+ ],
+ },
+ ];
+ const expected: ScriptCov | undefined = {
+ scriptId: "123",
+ url: "/lib.js",
+ functions: [
+ {
+ functionName: "test",
+ isBlockCoverage: true,
+ ranges: [
+ {startOffset: 0, endOffset: 4, count: 2},
+ {startOffset: 1, endOffset: 3, count: 1},
+ ],
+ },
+ ],
+ };
+ const actual: ScriptCov | undefined = mergeScriptCovs(inputs);
+ chai.assert.deepEqual(actual, expected);
+ });
+
+ it("accepts arrays with a single item for `mergeFunctionCovs`", () => {
+ const inputs: FunctionCov[] = [
+ {
+ functionName: "test",
+ isBlockCoverage: true,
+ ranges: [
+ {startOffset: 0, endOffset: 4, count: 2},
+ {startOffset: 1, endOffset: 2, count: 1},
+ {startOffset: 2, endOffset: 3, count: 1},
+ ],
+ },
+ ];
+ const expected: FunctionCov = {
+ functionName: "test",
+ isBlockCoverage: true,
+ ranges: [
+ {startOffset: 0, endOffset: 4, count: 2},
+ {startOffset: 1, endOffset: 3, count: 1},
+ ],
+ };
+ const actual: FunctionCov | undefined = mergeFunctionCovs(inputs);
+ chai.assert.deepEqual(actual, expected);
+ });
+ });
+
+ describe("ranges", () => {
+ for (const sourceFile of getSourceFiles()) {
+ const relPath: string = path.relative(RANGES_DIR, sourceFile);
+ describe(relPath, () => {
+ const content: string = fs.readFileSync(sourceFile, {encoding: "UTF-8"});
+ const items: MergeRangeItem[] = JSON.parse(content);
+ for (const item of items) {
+ const test: () => void = () => {
+ const actual: ProcessCov | undefined = mergeProcessCovs(item.inputs);
+ chai.assert.deepEqual(actual, item.expected);
+ };
+ switch (item.status) {
+ case "run":
+ it(item.name, test);
+ break;
+ case "only":
+ it.only(item.name, test);
+ break;
+ case "skip":
+ it.skip(item.name, test);
+ break;
+ default:
+ throw new Error(`Unexpected status: ${item.status}`);
+ }
+ }
+ });
+ }
+ });
+
+ describe("benches", () => {
+ for (const bench of getBenches()) {
+ const BENCHES_TO_SKIP: Set = new Set();
+ if (process.env.CI === "true") {
+ // Skip very large benchmarks when running continuous integration
+ BENCHES_TO_SKIP.add("node@10.11.0");
+ BENCHES_TO_SKIP.add("npm@6.4.1");
+ }
+
+ const name: string = path.basename(bench);
+
+ if (BENCHES_TO_SKIP.has(name)) {
+ it.skip(`${name} (skipped: too large for CI)`, testBench);
+ } else {
+ it(name, testBench);
+ }
+
+ async function testBench(this: Mocha.Context) {
+ this.timeout(BENCHES_TIMEOUT);
+
+ const inputFileNames: string[] = await fs.promises.readdir(bench);
+ const inputPromises: Promise[] = [];
+ for (const inputFileName of inputFileNames) {
+ const resolved: string = path.join(bench, inputFileName);
+ inputPromises.push(fs.promises.readFile(resolved).then(buffer => JSON.parse(buffer.toString("UTF-8"))));
+ }
+ const inputs: ProcessCov[] = await Promise.all(inputPromises);
+ const expectedPath: string = path.join(BENCHES_DIR, `${name}.json`);
+ const expectedContent: string = await fs.promises.readFile(expectedPath, {encoding: "UTF-8"}) as string;
+ const expected: ProcessCov = JSON.parse(expectedContent);
+ const startTime: number = Date.now();
+ const actual: ProcessCov | undefined = mergeProcessCovs(inputs);
+ const endTime: number = Date.now();
+ console.error(`Time (${name}): ${(endTime - startTime) / 1000}`);
+ chai.assert.deepEqual(actual, expected);
+ console.error(`OK: ${name}`);
+ }
+ }
+ });
+});
+
+function getSourceFiles() {
+ return getSourcesFrom(RANGES_DIR);
+
+ function* getSourcesFrom(dir: string): Iterable {
+ const names: string[] = fs.readdirSync(dir);
+ for (const name of names) {
+ const resolved: string = path.join(dir, name);
+ const stat: fs.Stats = fs.statSync(resolved);
+ if (stat.isDirectory()) {
+ yield* getSourcesFrom(dir);
+ } else {
+ yield resolved;
+ }
+ }
+ }
+}
+
+function* getBenches(): Iterable {
+ const names: string[] = fs.readdirSync(BENCHES_INPUT_DIR);
+ for (const name of names) {
+ const resolved: string = path.join(BENCHES_INPUT_DIR, name);
+ const stat: fs.Stats = fs.statSync(resolved);
+ if (stat.isDirectory()) {
+ yield resolved;
+ }
+ }
+}
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@bcoe/v8-coverage/tsconfig.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@bcoe/v8-coverage/tsconfig.json
new file mode 100644
index 00000000..73db48fe
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@bcoe/v8-coverage/tsconfig.json
@@ -0,0 +1,59 @@
+{
+ "compilerOptions": {
+ "allowJs": false,
+ "allowSyntheticDefaultImports": true,
+ "allowUnreachableCode": false,
+ "allowUnusedLabels": false,
+ "alwaysStrict": true,
+ "charset": "utf8",
+ "checkJs": false,
+ "declaration": false,
+ "disableSizeLimit": false,
+ "downlevelIteration": false,
+ "emitBOM": false,
+ "emitDecoratorMetadata": true,
+ "esModuleInterop": true,
+ "experimentalDecorators": true,
+ "forceConsistentCasingInFileNames": true,
+ "importHelpers": false,
+ "inlineSourceMap": false,
+ "inlineSources": false,
+ "isolatedModules": false,
+ "lib": [
+ "es2017",
+ "esnext.asynciterable"
+ ],
+ "locale": "en-us",
+ "module": "commonjs",
+ "moduleResolution": "node",
+ "newLine": "lf",
+ "noEmit": false,
+ "noEmitHelpers": false,
+ "noEmitOnError": true,
+ "noErrorTruncation": true,
+ "noFallthroughCasesInSwitch": true,
+ "noImplicitAny": true,
+ "noImplicitReturns": true,
+ "noImplicitThis": true,
+ "noStrictGenericChecks": false,
+ "noUnusedLocals": true,
+ "noUnusedParameters": false,
+ "noImplicitUseStrict": false,
+ "noLib": false,
+ "noResolve": false,
+ "preserveConstEnums": false,
+ "removeComments": false,
+ "skipLibCheck": true,
+ "sourceMap": true,
+ "strict": true,
+ "strictNullChecks": true,
+ "suppressExcessPropertyErrors": false,
+ "suppressImplicitAnyIndexErrors": false,
+ "target": "es2017",
+ "traceResolution": false,
+ "typeRoots": [
+ "src/lib/custom-typings",
+ "node_modules/@types"
+ ]
+ }
+}
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@esbuild/linux-x64/README.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@esbuild/linux-x64/README.md
new file mode 100644
index 00000000..b2f19300
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@esbuild/linux-x64/README.md
@@ -0,0 +1,3 @@
+# esbuild
+
+This is the Linux 64-bit binary for esbuild, a JavaScript bundler and minifier. See https://github.com/evanw/esbuild for details.
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@esbuild/linux-x64/bin/esbuild b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@esbuild/linux-x64/bin/esbuild
new file mode 100755
index 00000000..288f7689
Binary files /dev/null and b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@esbuild/linux-x64/bin/esbuild differ
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@esbuild/linux-x64/package.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@esbuild/linux-x64/package.json
new file mode 100644
index 00000000..b70b09ec
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@esbuild/linux-x64/package.json
@@ -0,0 +1,20 @@
+{
+ "name": "@esbuild/linux-x64",
+ "version": "0.21.5",
+ "description": "The Linux 64-bit binary for esbuild, a JavaScript bundler.",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/evanw/esbuild.git"
+ },
+ "license": "MIT",
+ "preferUnplugged": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "os": [
+ "linux"
+ ],
+ "cpu": [
+ "x64"
+ ]
+}
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@hono/node-server/LICENSE b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@hono/node-server/LICENSE
new file mode 100644
index 00000000..5f84afaf
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@hono/node-server/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2022 - present, Yusuke Wada and Hono contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@hono/node-server/README.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@hono/node-server/README.md
new file mode 100644
index 00000000..af0e9311
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@hono/node-server/README.md
@@ -0,0 +1,358 @@
+# Node.js Adapter for Hono
+
+This adapter `@hono/node-server` allows you to run your Hono application on Node.js.
+Initially, Hono wasn't designed for Node.js, but with this adapter, you can now use Hono on Node.js.
+It utilizes web standard APIs implemented in Node.js version 18 or higher.
+
+## Benchmarks
+
+Hono is 3.5 times faster than Express.
+
+Express:
+
+```txt
+$ bombardier -d 10s --fasthttp http://localhost:3000/
+
+Statistics Avg Stdev Max
+ Reqs/sec 16438.94 1603.39 19155.47
+ Latency 7.60ms 7.51ms 559.89ms
+ HTTP codes:
+ 1xx - 0, 2xx - 164494, 3xx - 0, 4xx - 0, 5xx - 0
+ others - 0
+ Throughput: 4.55MB/s
+```
+
+Hono + `@hono/node-server`:
+
+```txt
+$ bombardier -d 10s --fasthttp http://localhost:3000/
+
+Statistics Avg Stdev Max
+ Reqs/sec 58296.56 5512.74 74403.56
+ Latency 2.14ms 1.46ms 190.92ms
+ HTTP codes:
+ 1xx - 0, 2xx - 583059, 3xx - 0, 4xx - 0, 5xx - 0
+ others - 0
+ Throughput: 12.56MB/s
+```
+
+## Requirements
+
+It works on Node.js versions greater than 18.x. The specific required Node.js versions are as follows:
+
+- 18.x => 18.14.1+
+- 19.x => 19.7.0+
+- 20.x => 20.0.0+
+
+Essentially, you can simply use the latest version of each major release.
+
+## Installation
+
+You can install it from the npm registry with `npm` command:
+
+```sh
+npm install @hono/node-server
+```
+
+Or use `yarn`:
+
+```sh
+yarn add @hono/node-server
+```
+
+## Usage
+
+Just import `@hono/node-server` at the top and write the code as usual.
+The same code that runs on Cloudflare Workers, Deno, and Bun will work.
+
+```ts
+import { serve } from '@hono/node-server'
+import { Hono } from 'hono'
+
+const app = new Hono()
+app.get('/', (c) => c.text('Hono meets Node.js'))
+
+serve(app, (info) => {
+ console.log(`Listening on http://localhost:${info.port}`) // Listening on http://localhost:3000
+})
+```
+
+For example, run it using `ts-node`. Then an HTTP server will be launched. The default port is `3000`.
+
+```sh
+ts-node ./index.ts
+```
+
+Open `http://localhost:3000` with your browser.
+
+## Options
+
+### `port`
+
+```ts
+serve({
+ fetch: app.fetch,
+ port: 8787, // Port number, default is 3000
+})
+```
+
+### `createServer`
+
+```ts
+import { createServer } from 'node:https'
+import fs from 'node:fs'
+
+//...
+
+serve({
+ fetch: app.fetch,
+ createServer: createServer,
+ serverOptions: {
+ key: fs.readFileSync('test/fixtures/keys/agent1-key.pem'),
+ cert: fs.readFileSync('test/fixtures/keys/agent1-cert.pem'),
+ },
+})
+```
+
+### `overrideGlobalObjects`
+
+The default value is `true`. The Node.js Adapter rewrites the global Request/Response and uses a lightweight Request/Response to improve performance. If you don't want to do that, set `false`.
+
+```ts
+serve({
+ fetch: app.fetch,
+ overrideGlobalObjects: false,
+})
+```
+
+### `autoCleanupIncoming`
+
+The default value is `true`. The Node.js Adapter automatically cleans up (explicitly call `destroy()` method) if application is not finished to consume the incoming request. If you don't want to do that, set `false`.
+
+If the application accepts connections from arbitrary clients, this cleanup must be done otherwise incomplete requests from clients may cause the application to stop responding. If your application only accepts connections from trusted clients, such as in a reverse proxy environment and there is no process that returns a response without reading the body of the POST request all the way through, you can improve performance by setting it to `false`.
+
+```ts
+serve({
+ fetch: app.fetch,
+ autoCleanupIncoming: false,
+})
+```
+
+## Middleware
+
+Most built-in middleware also works with Node.js.
+Read [the documentation](https://hono.dev/middleware/builtin/basic-auth) and use the Middleware of your liking.
+
+```ts
+import { serve } from '@hono/node-server'
+import { Hono } from 'hono'
+import { prettyJSON } from 'hono/pretty-json'
+
+const app = new Hono()
+
+app.get('*', prettyJSON())
+app.get('/', (c) => c.json({ 'Hono meets': 'Node.js' }))
+
+serve(app)
+```
+
+## Serve Static Middleware
+
+Use Serve Static Middleware that has been created for Node.js.
+
+```ts
+import { serveStatic } from '@hono/node-server/serve-static'
+
+//...
+
+app.use('/static/*', serveStatic({ root: './' }))
+```
+
+If using a relative path, `root` will be relative to the current working directory from which the app was started.
+
+This can cause confusion when running your application locally.
+
+Imagine your project structure is:
+
+```
+my-hono-project/
+ src/
+ index.ts
+ static/
+ index.html
+```
+
+Typically, you would run your app from the project's root directory (`my-hono-project`),
+so you would need the following code to serve the `static` folder:
+
+```ts
+app.use('/static/*', serveStatic({ root: './static' }))
+```
+
+Notice that `root` here is not relative to `src/index.ts`, rather to `my-hono-project`.
+
+### Options
+
+#### `rewriteRequestPath`
+
+If you want to serve files in `./.foojs` with the request path `/__foo/*`, you can write like the following.
+
+```ts
+app.use(
+ '/__foo/*',
+ serveStatic({
+ root: './.foojs/',
+ rewriteRequestPath: (path: string) => path.replace(/^\/__foo/, ''),
+ })
+)
+```
+
+#### `onFound`
+
+You can specify handling when the requested file is found with `onFound`.
+
+```ts
+app.use(
+ '/static/*',
+ serveStatic({
+ // ...
+ onFound: (_path, c) => {
+ c.header('Cache-Control', `public, immutable, max-age=31536000`)
+ },
+ })
+)
+```
+
+#### `onNotFound`
+
+The `onNotFound` is useful for debugging. You can write a handle for when a file is not found.
+
+```ts
+app.use(
+ '/static/*',
+ serveStatic({
+ root: './non-existent-dir',
+ onNotFound: (path, c) => {
+ console.log(`${path} is not found, request to ${c.req.path}`)
+ },
+ })
+)
+```
+
+#### `precompressed`
+
+The `precompressed` option checks if files with extensions like `.br` or `.gz` are available and serves them based on the `Accept-Encoding` header. It prioritizes Brotli, then Zstd, and Gzip. If none are available, it serves the original file.
+
+```ts
+app.use(
+ '/static/*',
+ serveStatic({
+ precompressed: true,
+ })
+)
+```
+
+## ConnInfo Helper
+
+You can use the [ConnInfo Helper](https://hono.dev/docs/helpers/conninfo) by importing `getConnInfo` from `@hono/node-server/conninfo`.
+
+```ts
+import { getConnInfo } from '@hono/node-server/conninfo'
+
+app.get('/', (c) => {
+ const info = getConnInfo(c) // info is `ConnInfo`
+ return c.text(`Your remote address is ${info.remote.address}`)
+})
+```
+
+## Accessing Node.js API
+
+You can access the Node.js API from `c.env` in Node.js. For example, if you want to specify a type, you can write the following.
+
+```ts
+import { serve } from '@hono/node-server'
+import type { HttpBindings } from '@hono/node-server'
+import { Hono } from 'hono'
+
+const app = new Hono<{ Bindings: HttpBindings }>()
+
+app.get('/', (c) => {
+ return c.json({
+ remoteAddress: c.env.incoming.socket.remoteAddress,
+ })
+})
+
+serve(app)
+```
+
+The APIs that you can get from `c.env` are as follows.
+
+```ts
+type HttpBindings = {
+ incoming: IncomingMessage
+ outgoing: ServerResponse
+}
+
+type Http2Bindings = {
+ incoming: Http2ServerRequest
+ outgoing: Http2ServerResponse
+}
+```
+
+## Direct response from Node.js API
+
+You can directly respond to the client from the Node.js API.
+In that case, the response from Hono should be ignored, so return `RESPONSE_ALREADY_SENT`.
+
+> [!NOTE]
+> This feature can be used when migrating existing Node.js applications to Hono, but we recommend using Hono's API for new applications.
+
+```ts
+import { serve } from '@hono/node-server'
+import type { HttpBindings } from '@hono/node-server'
+import { RESPONSE_ALREADY_SENT } from '@hono/node-server/utils/response'
+import { Hono } from 'hono'
+
+const app = new Hono<{ Bindings: HttpBindings }>()
+
+app.get('/', (c) => {
+ const { outgoing } = c.env
+ outgoing.writeHead(200, { 'Content-Type': 'text/plain' })
+ outgoing.end('Hello World\n')
+
+ return RESPONSE_ALREADY_SENT
+})
+
+serve(app)
+```
+
+## Listen to a UNIX domain socket
+
+You can configure the HTTP server to listen to a UNIX domain socket instead of a TCP port.
+
+```ts
+import { createAdaptorServer } from '@hono/node-server'
+
+// ...
+
+const socketPath = '/tmp/example.sock'
+
+const server = createAdaptorServer(app)
+server.listen(socketPath, () => {
+ console.log(`Listening on ${socketPath}`)
+})
+```
+
+## Related projects
+
+- Hono -
+- Hono GitHub repository -
+
+## Authors
+
+- Yusuke Wada
+- Taku Amano
+
+## License
+
+MIT
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@hono/node-server/package.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@hono/node-server/package.json
new file mode 100644
index 00000000..92c45085
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@hono/node-server/package.json
@@ -0,0 +1,103 @@
+{
+ "name": "@hono/node-server",
+ "version": "1.19.11",
+ "description": "Node.js Adapter for Hono",
+ "main": "dist/index.js",
+ "types": "dist/index.d.ts",
+ "files": [
+ "dist"
+ ],
+ "exports": {
+ ".": {
+ "types": "./dist/index.d.ts",
+ "require": "./dist/index.js",
+ "import": "./dist/index.mjs"
+ },
+ "./serve-static": {
+ "types": "./dist/serve-static.d.ts",
+ "require": "./dist/serve-static.js",
+ "import": "./dist/serve-static.mjs"
+ },
+ "./vercel": {
+ "types": "./dist/vercel.d.ts",
+ "require": "./dist/vercel.js",
+ "import": "./dist/vercel.mjs"
+ },
+ "./utils/*": {
+ "types": "./dist/utils/*.d.ts",
+ "require": "./dist/utils/*.js",
+ "import": "./dist/utils/*.mjs"
+ },
+ "./conninfo": {
+ "types": "./dist/conninfo.d.ts",
+ "require": "./dist/conninfo.js",
+ "import": "./dist/conninfo.mjs"
+ }
+ },
+ "typesVersions": {
+ "*": {
+ ".": [
+ "./dist/index.d.ts"
+ ],
+ "serve-static": [
+ "./dist/serve-static.d.ts"
+ ],
+ "vercel": [
+ "./dist/vercel.d.ts"
+ ],
+ "utils/*": [
+ "./dist/utils/*.d.ts"
+ ],
+ "conninfo": [
+ "./dist/conninfo.d.ts"
+ ]
+ }
+ },
+ "scripts": {
+ "test": "node --expose-gc node_modules/jest/bin/jest.js",
+ "build": "tsup --external hono",
+ "watch": "tsup --watch",
+ "postbuild": "publint",
+ "prerelease": "bun run build && bun run test",
+ "release": "np",
+ "lint": "eslint src test",
+ "lint:fix": "eslint src test --fix",
+ "format": "prettier --check \"src/**/*.{js,ts}\" \"test/**/*.{js,ts}\"",
+ "format:fix": "prettier --write \"src/**/*.{js,ts}\" \"test/**/*.{js,ts}\""
+ },
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/honojs/node-server.git"
+ },
+ "homepage": "https://github.com/honojs/node-server",
+ "author": "Yusuke Wada (https://github.com/yusukebe)",
+ "publishConfig": {
+ "registry": "https://registry.npmjs.org",
+ "access": "public"
+ },
+ "engines": {
+ "node": ">=18.14.1"
+ },
+ "devDependencies": {
+ "@hono/eslint-config": "^1.0.1",
+ "@types/jest": "^29.5.3",
+ "@types/node": "^20.10.0",
+ "@types/supertest": "^2.0.12",
+ "@whatwg-node/fetch": "^0.9.14",
+ "eslint": "^9.10.0",
+ "hono": "^4.4.10",
+ "jest": "^29.6.1",
+ "np": "^7.7.0",
+ "prettier": "^3.2.4",
+ "publint": "^0.1.16",
+ "supertest": "^6.3.3",
+ "ts-jest": "^29.1.1",
+ "tsup": "^7.2.0",
+ "typescript": "^5.3.2"
+ },
+ "peerDependencies": {
+ "hono": "^4"
+ },
+ "packageManager": "bun@1.2.20"
+}
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@isaacs/cliui/LICENSE.txt b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@isaacs/cliui/LICENSE.txt
new file mode 100644
index 00000000..c7e27478
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@isaacs/cliui/LICENSE.txt
@@ -0,0 +1,14 @@
+Copyright (c) 2015, Contributors
+
+Permission to use, copy, modify, and/or distribute this software
+for any purpose with or without fee is hereby granted, provided
+that the above copyright notice and this permission notice
+appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES
+OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE
+LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES
+OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
+WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
+ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@isaacs/cliui/README.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@isaacs/cliui/README.md
new file mode 100644
index 00000000..48806426
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@isaacs/cliui/README.md
@@ -0,0 +1,143 @@
+# @isaacs/cliui
+
+Temporary fork of [cliui](http://npm.im/cliui).
+
+
+[](https://www.npmjs.com/package/cliui)
+[](https://conventionalcommits.org)
+
+
+easily create complex multi-column command-line-interfaces.
+
+## Example
+
+```js
+const ui = require('cliui')()
+
+ui.div('Usage: $0 [command] [options]')
+
+ui.div({
+ text: 'Options:',
+ padding: [2, 0, 1, 0]
+})
+
+ui.div(
+ {
+ text: "-f, --file",
+ width: 20,
+ padding: [0, 4, 0, 4]
+ },
+ {
+ text: "the file to load." +
+ chalk.green("(if this description is long it wraps).")
+ ,
+ width: 20
+ },
+ {
+ text: chalk.red("[required]"),
+ align: 'right'
+ }
+)
+
+console.log(ui.toString())
+```
+
+## Deno/ESM Support
+
+As of `v7` `cliui` supports [Deno](https://github.com/denoland/deno) and
+[ESM](https://nodejs.org/api/esm.html#esm_ecmascript_modules):
+
+```typescript
+import cliui from "https://deno.land/x/cliui/deno.ts";
+
+const ui = cliui({})
+
+ui.div('Usage: $0 [command] [options]')
+
+ui.div({
+ text: 'Options:',
+ padding: [2, 0, 1, 0]
+})
+
+ui.div({
+ text: "-f, --file",
+ width: 20,
+ padding: [0, 4, 0, 4]
+})
+
+console.log(ui.toString())
+```
+
+
+
+## Layout DSL
+
+cliui exposes a simple layout DSL:
+
+If you create a single `ui.div`, passing a string rather than an
+object:
+
+* `\n`: characters will be interpreted as new rows.
+* `\t`: characters will be interpreted as new columns.
+* `\s`: characters will be interpreted as padding.
+
+**as an example...**
+
+```js
+var ui = require('./')({
+ width: 60
+})
+
+ui.div(
+ 'Usage: node ./bin/foo.js\n' +
+ ' \t provide a regex\n' +
+ ' \t provide a glob\t [required]'
+)
+
+console.log(ui.toString())
+```
+
+**will output:**
+
+```shell
+Usage: node ./bin/foo.js
+ provide a regex
+ provide a glob [required]
+```
+
+## Methods
+
+```js
+cliui = require('cliui')
+```
+
+### cliui({width: integer})
+
+Specify the maximum width of the UI being generated.
+If no width is provided, cliui will try to get the current window's width and use it, and if that doesn't work, width will be set to `80`.
+
+### cliui({wrap: boolean})
+
+Enable or disable the wrapping of text in a column.
+
+### cliui.div(column, column, column)
+
+Create a row with any number of columns, a column
+can either be a string, or an object with the following
+options:
+
+* **text:** some text to place in the column.
+* **width:** the width of a column.
+* **align:** alignment, `right` or `center`.
+* **padding:** `[top, right, bottom, left]`.
+* **border:** should a border be placed around the div?
+
+### cliui.span(column, column, column)
+
+Similar to `div`, except the next row will be appended without
+a new line being created.
+
+### cliui.resetOutput()
+
+Resets the UI elements of the current cliui instance, maintaining the values
+set for `width` and `wrap`.
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@isaacs/cliui/index.mjs b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@isaacs/cliui/index.mjs
new file mode 100644
index 00000000..5177519a
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@isaacs/cliui/index.mjs
@@ -0,0 +1,14 @@
+// Bootstrap cliui with ESM dependencies:
+import { cliui } from './build/lib/index.js'
+
+import stringWidth from 'string-width'
+import stripAnsi from 'strip-ansi'
+import wrap from 'wrap-ansi'
+
+export default function ui (opts) {
+ return cliui(opts, {
+ stringWidth,
+ stripAnsi,
+ wrap
+ })
+}
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@isaacs/cliui/package.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@isaacs/cliui/package.json
new file mode 100644
index 00000000..7a952532
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@isaacs/cliui/package.json
@@ -0,0 +1,86 @@
+{
+ "name": "@isaacs/cliui",
+ "version": "8.0.2",
+ "description": "easily create complex multi-column command-line-interfaces",
+ "main": "build/index.cjs",
+ "exports": {
+ ".": [
+ {
+ "import": "./index.mjs",
+ "require": "./build/index.cjs"
+ },
+ "./build/index.cjs"
+ ]
+ },
+ "type": "module",
+ "module": "./index.mjs",
+ "scripts": {
+ "check": "standardx '**/*.ts' && standardx '**/*.js' && standardx '**/*.cjs'",
+ "fix": "standardx --fix '**/*.ts' && standardx --fix '**/*.js' && standardx --fix '**/*.cjs'",
+ "pretest": "rimraf build && tsc -p tsconfig.test.json && cross-env NODE_ENV=test npm run build:cjs",
+ "test": "c8 mocha ./test/*.cjs",
+ "test:esm": "c8 mocha ./test/**/*.mjs",
+ "postest": "check",
+ "coverage": "c8 report --check-coverage",
+ "precompile": "rimraf build",
+ "compile": "tsc",
+ "postcompile": "npm run build:cjs",
+ "build:cjs": "rollup -c",
+ "prepare": "npm run compile"
+ },
+ "repository": "yargs/cliui",
+ "standard": {
+ "ignore": [
+ "**/example/**"
+ ],
+ "globals": [
+ "it"
+ ]
+ },
+ "keywords": [
+ "cli",
+ "command-line",
+ "layout",
+ "design",
+ "console",
+ "wrap",
+ "table"
+ ],
+ "author": "Ben Coe ",
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^5.1.2",
+ "string-width-cjs": "npm:string-width@^4.2.0",
+ "strip-ansi": "^7.0.1",
+ "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+ "wrap-ansi": "^8.1.0",
+ "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+ },
+ "devDependencies": {
+ "@types/node": "^14.0.27",
+ "@typescript-eslint/eslint-plugin": "^4.0.0",
+ "@typescript-eslint/parser": "^4.0.0",
+ "c8": "^7.3.0",
+ "chai": "^4.2.0",
+ "chalk": "^4.1.0",
+ "cross-env": "^7.0.2",
+ "eslint": "^7.6.0",
+ "eslint-plugin-import": "^2.22.0",
+ "eslint-plugin-node": "^11.1.0",
+ "gts": "^3.0.0",
+ "mocha": "^10.0.0",
+ "rimraf": "^3.0.2",
+ "rollup": "^2.23.1",
+ "rollup-plugin-ts": "^3.0.2",
+ "standardx": "^7.0.0",
+ "typescript": "^4.0.0"
+ },
+ "files": [
+ "build",
+ "index.mjs",
+ "!*.d.ts"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+}
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@istanbuljs/schema/CHANGELOG.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@istanbuljs/schema/CHANGELOG.md
new file mode 100644
index 00000000..afdc8350
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@istanbuljs/schema/CHANGELOG.md
@@ -0,0 +1,44 @@
+# Changelog
+
+All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
+
+### [0.1.3](https://github.com/istanbuljs/schema/compare/v0.1.2...v0.1.3) (2021-02-13)
+
+
+### Features
+
+* Add `classPrivateMethods` and `topLevelAwait` default support ([#17](https://github.com/istanbuljs/schema/issues/17)) ([e732889](https://github.com/istanbuljs/schema/commit/e7328894ddeb61da256c1f13c2c2cc2e04f181df)), closes [#16](https://github.com/istanbuljs/schema/issues/16)
+* Add `numericSeparator` to default `parserPlugins` ([#12](https://github.com/istanbuljs/schema/issues/12)) ([fe32f00](https://github.com/istanbuljs/schema/commit/fe32f002f54c61467b1c1a487081f51c85ec8d10)), closes [#5](https://github.com/istanbuljs/schema/issues/5)
+* Add babel.config.mjs to default exclude ([#10](https://github.com/istanbuljs/schema/issues/10)) ([a4dbeaa](https://github.com/istanbuljs/schema/commit/a4dbeaa7045490a4d46754801ac71f5d99c9bd79))
+
+
+### Bug Fixes
+
+* Exclude tests with `tsx` or `jsx` extensions ([#13](https://github.com/istanbuljs/schema/issues/13)) ([c7747f7](https://github.com/istanbuljs/schema/commit/c7747f7a7df8a2b770036834af77dfd0ee445733)), closes [#11](https://github.com/istanbuljs/schema/issues/11)
+
+### [0.1.2](https://github.com/istanbuljs/schema/compare/v0.1.1...v0.1.2) (2019-12-05)
+
+
+### Features
+
+* Ignore *.d.ts ([#6](https://github.com/istanbuljs/schema/issues/6)) ([d867eaf](https://github.com/istanbuljs/schema/commit/d867eaff6ca4abcd4301990e2bdcdf53e438e9c4))
+* Update default exclude of dev tool configurations ([#7](https://github.com/istanbuljs/schema/issues/7)) ([c89f818](https://github.com/istanbuljs/schema/commit/c89f8185f30879bcdf8d2f1c3b7aba0ac7056fa9))
+
+## [0.1.1](https://github.com/istanbuljs/schema/compare/v0.1.0...v0.1.1) (2019-10-07)
+
+
+### Bug Fixes
+
+* Add missing `instrument` option ([#3](https://github.com/istanbuljs/schema/issues/3)) ([bf1217d](https://github.com/istanbuljs/schema/commit/bf1217d))
+
+
+### Features
+
+* Add `use-spawn-wrap` nyc option ([#4](https://github.com/istanbuljs/schema/issues/4)) ([b2ce2e8](https://github.com/istanbuljs/schema/commit/b2ce2e8))
+
+## 0.1.0 (2019-10-05)
+
+
+### Features
+
+* Initial implementation ([99bd3a5](https://github.com/istanbuljs/schema/commit/99bd3a5))
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@istanbuljs/schema/LICENSE b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@istanbuljs/schema/LICENSE
new file mode 100644
index 00000000..807a18bd
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@istanbuljs/schema/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2019 CFWare, LLC
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@istanbuljs/schema/README.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@istanbuljs/schema/README.md
new file mode 100644
index 00000000..9cac0288
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@istanbuljs/schema/README.md
@@ -0,0 +1,30 @@
+# @istanbuljs/schema
+
+[![Travis CI][travis-image]][travis-url]
+[![NPM Version][npm-image]][npm-url]
+[![NPM Downloads][downloads-image]][downloads-url]
+[![MIT][license-image]](LICENSE)
+
+Schemas describing various structures used by nyc and istanbuljs
+
+## Usage
+
+```js
+const {nyc} = require('@istanbuljs/schema').defaults;
+
+console.log(`Default exclude list:\n\t* ${nyc.exclude.join('\n\t* ')}`);
+```
+
+## `@istanbuljs/schema` for enterprise
+
+Available as part of the Tidelift Subscription.
+
+The maintainers of `@istanbuljs/schema` and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source dependencies you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the exact dependencies you use. [Learn more.](https://tidelift.com/subscription/pkg/npm-istanbuljs-schema?utm_source=npm-istanbuljs-schema&utm_medium=referral&utm_campaign=enterprise)
+
+[npm-image]: https://img.shields.io/npm/v/@istanbuljs/schema.svg
+[npm-url]: https://npmjs.org/package/@istanbuljs/schema
+[travis-image]: https://travis-ci.org/istanbuljs/schema.svg?branch=master
+[travis-url]: https://travis-ci.org/istanbuljs/schema
+[downloads-image]: https://img.shields.io/npm/dm/@istanbuljs/schema.svg
+[downloads-url]: https://npmjs.org/package/@istanbuljs/schema
+[license-image]: https://img.shields.io/npm/l/@istanbuljs/schema.svg
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@istanbuljs/schema/default-exclude.js b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@istanbuljs/schema/default-exclude.js
new file mode 100644
index 00000000..c6bb5264
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@istanbuljs/schema/default-exclude.js
@@ -0,0 +1,22 @@
+'use strict';
+
+const defaultExtension = require('./default-extension.js');
+const testFileExtensions = defaultExtension
+ .map(extension => extension.slice(1))
+ .join(',');
+
+module.exports = [
+ 'coverage/**',
+ 'packages/*/test{,s}/**',
+ '**/*.d.ts',
+ 'test{,s}/**',
+ `test{,-*}.{${testFileExtensions}}`,
+ `**/*{.,-}test.{${testFileExtensions}}`,
+ '**/__tests__/**',
+
+ /* Exclude common development tool configuration files */
+ '**/{ava,babel,nyc}.config.{js,cjs,mjs}',
+ '**/jest.config.{js,cjs,mjs,ts}',
+ '**/{karma,rollup,webpack}.config.js',
+ '**/.{eslint,mocha}rc.{js,cjs}'
+];
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@istanbuljs/schema/default-extension.js b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@istanbuljs/schema/default-extension.js
new file mode 100644
index 00000000..46ebadca
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@istanbuljs/schema/default-extension.js
@@ -0,0 +1,10 @@
+'use strict';
+
+module.exports = [
+ '.js',
+ '.cjs',
+ '.mjs',
+ '.ts',
+ '.tsx',
+ '.jsx'
+];
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@istanbuljs/schema/index.js b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@istanbuljs/schema/index.js
new file mode 100644
index 00000000..b35f6101
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@istanbuljs/schema/index.js
@@ -0,0 +1,466 @@
+'use strict';
+
+const defaultExclude = require('./default-exclude.js');
+const defaultExtension = require('./default-extension.js');
+
+const nycCommands = {
+ all: [null, 'check-coverage', 'instrument', 'merge', 'report'],
+ testExclude: [null, 'instrument', 'report', 'check-coverage'],
+ instrument: [null, 'instrument'],
+ checkCoverage: [null, 'report', 'check-coverage'],
+ report: [null, 'report'],
+ main: [null],
+ instrumentOnly: ['instrument']
+};
+
+const cwd = {
+ description: 'working directory used when resolving paths',
+ type: 'string',
+ get default() {
+ return process.cwd();
+ },
+ nycCommands: nycCommands.all
+};
+
+const nycrcPath = {
+ description: 'specify an explicit path to find nyc configuration',
+ nycCommands: nycCommands.all
+};
+
+const tempDir = {
+ description: 'directory to output raw coverage information to',
+ type: 'string',
+ default: './.nyc_output',
+ nycAlias: 't',
+ nycHiddenAlias: 'temp-directory',
+ nycCommands: [null, 'check-coverage', 'merge', 'report']
+};
+
+const testExclude = {
+ exclude: {
+ description: 'a list of specific files and directories that should be excluded from coverage, glob patterns are supported',
+ type: 'array',
+ items: {
+ type: 'string'
+ },
+ default: defaultExclude,
+ nycCommands: nycCommands.testExclude,
+ nycAlias: 'x'
+ },
+ excludeNodeModules: {
+ description: 'whether or not to exclude all node_module folders (i.e. **/node_modules/**) by default',
+ type: 'boolean',
+ default: true,
+ nycCommands: nycCommands.testExclude
+ },
+ include: {
+ description: 'a list of specific files that should be covered, glob patterns are supported',
+ type: 'array',
+ items: {
+ type: 'string'
+ },
+ default: [],
+ nycCommands: nycCommands.testExclude,
+ nycAlias: 'n'
+ },
+ extension: {
+ description: 'a list of extensions that nyc should handle in addition to .js',
+ type: 'array',
+ items: {
+ type: 'string'
+ },
+ default: defaultExtension,
+ nycCommands: nycCommands.testExclude,
+ nycAlias: 'e'
+ }
+};
+
+const instrumentVisitor = {
+ coverageVariable: {
+ description: 'variable to store coverage',
+ type: 'string',
+ default: '__coverage__',
+ nycCommands: nycCommands.instrument
+ },
+ coverageGlobalScope: {
+ description: 'scope to store the coverage variable',
+ type: 'string',
+ default: 'this',
+ nycCommands: nycCommands.instrument
+ },
+ coverageGlobalScopeFunc: {
+ description: 'avoid potentially replaced `Function` when finding global scope',
+ type: 'boolean',
+ default: true,
+ nycCommands: nycCommands.instrument
+ },
+ ignoreClassMethods: {
+ description: 'class method names to ignore for coverage',
+ type: 'array',
+ items: {
+ type: 'string'
+ },
+ default: [],
+ nycCommands: nycCommands.instrument
+ }
+};
+
+const instrumentParseGen = {
+ autoWrap: {
+ description: 'allow `return` statements outside of functions',
+ type: 'boolean',
+ default: true,
+ nycCommands: nycCommands.instrument
+ },
+ esModules: {
+ description: 'should files be treated as ES Modules',
+ type: 'boolean',
+ default: true,
+ nycCommands: nycCommands.instrument
+ },
+ parserPlugins: {
+ description: 'babel parser plugins to use when parsing the source',
+ type: 'array',
+ items: {
+ type: 'string'
+ },
+ /* Babel parser plugins are to be enabled when the feature is stage 3 and
+ * implemented in a released version of node.js. */
+ default: [
+ 'asyncGenerators',
+ 'bigInt',
+ 'classProperties',
+ 'classPrivateProperties',
+ 'classPrivateMethods',
+ 'dynamicImport',
+ 'importMeta',
+ 'numericSeparator',
+ 'objectRestSpread',
+ 'optionalCatchBinding',
+ 'topLevelAwait'
+ ],
+ nycCommands: nycCommands.instrument
+ },
+ compact: {
+ description: 'should the output be compacted?',
+ type: 'boolean',
+ default: true,
+ nycCommands: nycCommands.instrument
+ },
+ preserveComments: {
+ description: 'should comments be preserved in the output?',
+ type: 'boolean',
+ default: true,
+ nycCommands: nycCommands.instrument
+ },
+ produceSourceMap: {
+ description: 'should source maps be produced?',
+ type: 'boolean',
+ default: true,
+ nycCommands: nycCommands.instrument
+ }
+};
+
+const checkCoverage = {
+ excludeAfterRemap: {
+ description: 'should exclude logic be performed after the source-map remaps filenames?',
+ type: 'boolean',
+ default: true,
+ nycCommands: nycCommands.checkCoverage
+ },
+ branches: {
+ description: 'what % of branches must be covered?',
+ type: 'number',
+ default: 0,
+ minimum: 0,
+ maximum: 100,
+ nycCommands: nycCommands.checkCoverage
+ },
+ functions: {
+ description: 'what % of functions must be covered?',
+ type: 'number',
+ default: 0,
+ minimum: 0,
+ maximum: 100,
+ nycCommands: nycCommands.checkCoverage
+ },
+ lines: {
+ description: 'what % of lines must be covered?',
+ type: 'number',
+ default: 90,
+ minimum: 0,
+ maximum: 100,
+ nycCommands: nycCommands.checkCoverage
+ },
+ statements: {
+ description: 'what % of statements must be covered?',
+ type: 'number',
+ default: 0,
+ minimum: 0,
+ maximum: 100,
+ nycCommands: nycCommands.checkCoverage
+ },
+ perFile: {
+ description: 'check thresholds per file',
+ type: 'boolean',
+ default: false,
+ nycCommands: nycCommands.checkCoverage
+ }
+};
+
+const report = {
+ checkCoverage: {
+ description: 'check whether coverage is within thresholds provided',
+ type: 'boolean',
+ default: false,
+ nycCommands: nycCommands.report
+ },
+ reporter: {
+ description: 'coverage reporter(s) to use',
+ type: 'array',
+ items: {
+ type: 'string'
+ },
+ default: ['text'],
+ nycCommands: nycCommands.report,
+ nycAlias: 'r'
+ },
+ reportDir: {
+ description: 'directory to output coverage reports in',
+ type: 'string',
+ default: 'coverage',
+ nycCommands: nycCommands.report
+ },
+ showProcessTree: {
+ description: 'display the tree of spawned processes',
+ type: 'boolean',
+ default: false,
+ nycCommands: nycCommands.report
+ },
+ skipEmpty: {
+ description: 'don\'t show empty files (no lines of code) in report',
+ type: 'boolean',
+ default: false,
+ nycCommands: nycCommands.report
+ },
+ skipFull: {
+ description: 'don\'t show files with 100% statement, branch, and function coverage',
+ type: 'boolean',
+ default: false,
+ nycCommands: nycCommands.report
+ }
+};
+
+const nycMain = {
+ silent: {
+ description: 'don\'t output a report after tests finish running',
+ type: 'boolean',
+ default: false,
+ nycCommands: nycCommands.main,
+ nycAlias: 's'
+ },
+ all: {
+ description: 'whether or not to instrument all files of the project (not just the ones touched by your test suite)',
+ type: 'boolean',
+ default: false,
+ nycCommands: nycCommands.main,
+ nycAlias: 'a'
+ },
+ eager: {
+ description: 'instantiate the instrumenter at startup (see https://git.io/vMKZ9)',
+ type: 'boolean',
+ default: false,
+ nycCommands: nycCommands.main
+ },
+ cache: {
+ description: 'cache instrumentation results for improved performance',
+ type: 'boolean',
+ default: true,
+ nycCommands: nycCommands.main,
+ nycAlias: 'c'
+ },
+ cacheDir: {
+ description: 'explicitly set location for instrumentation cache',
+ type: 'string',
+ nycCommands: nycCommands.main
+ },
+ babelCache: {
+ description: 'cache babel transpilation results for improved performance',
+ type: 'boolean',
+ default: false,
+ nycCommands: nycCommands.main
+ },
+ useSpawnWrap: {
+ description: 'use spawn-wrap instead of setting process.env.NODE_OPTIONS',
+ type: 'boolean',
+ default: false,
+ nycCommands: nycCommands.main
+ },
+ hookRequire: {
+ description: 'should nyc wrap require?',
+ type: 'boolean',
+ default: true,
+ nycCommands: nycCommands.main
+ },
+ hookRunInContext: {
+ description: 'should nyc wrap vm.runInContext?',
+ type: 'boolean',
+ default: false,
+ nycCommands: nycCommands.main
+ },
+ hookRunInThisContext: {
+ description: 'should nyc wrap vm.runInThisContext?',
+ type: 'boolean',
+ default: false,
+ nycCommands: nycCommands.main
+ },
+ clean: {
+ description: 'should the .nyc_output folder be cleaned before executing tests',
+ type: 'boolean',
+ default: true,
+ nycCommands: nycCommands.main
+ }
+};
+
+const instrumentOnly = {
+ inPlace: {
+ description: 'should nyc run the instrumentation in place?',
+ type: 'boolean',
+ default: false,
+ nycCommands: nycCommands.instrumentOnly
+ },
+ exitOnError: {
+ description: 'should nyc exit when an instrumentation failure occurs?',
+ type: 'boolean',
+ default: false,
+ nycCommands: nycCommands.instrumentOnly
+ },
+ delete: {
+ description: 'should the output folder be deleted before instrumenting files?',
+ type: 'boolean',
+ default: false,
+ nycCommands: nycCommands.instrumentOnly
+ },
+ completeCopy: {
+ description: 'should nyc copy all files from input to output as well as instrumented files?',
+ type: 'boolean',
+ default: false,
+ nycCommands: nycCommands.instrumentOnly
+ }
+};
+
+const nyc = {
+ description: 'nyc configuration options',
+ type: 'object',
+ properties: {
+ cwd,
+ nycrcPath,
+ tempDir,
+
+ /* Test Exclude */
+ ...testExclude,
+
+ /* Instrumentation settings */
+ ...instrumentVisitor,
+
+ /* Instrumentation parser/generator settings */
+ ...instrumentParseGen,
+ sourceMap: {
+ description: 'should nyc detect and handle source maps?',
+ type: 'boolean',
+ default: true,
+ nycCommands: nycCommands.instrument
+ },
+ require: {
+ description: 'a list of additional modules that nyc should attempt to require in its subprocess, e.g., @babel/register, @babel/polyfill',
+ type: 'array',
+ items: {
+ type: 'string'
+ },
+ default: [],
+ nycCommands: nycCommands.instrument,
+ nycAlias: 'i'
+ },
+ instrument: {
+ description: 'should nyc handle instrumentation?',
+ type: 'boolean',
+ default: true,
+ nycCommands: nycCommands.instrument
+ },
+
+ /* Check coverage */
+ ...checkCoverage,
+
+ /* Report options */
+ ...report,
+
+ /* Main command options */
+ ...nycMain,
+
+ /* Instrument command options */
+ ...instrumentOnly
+ }
+};
+
+const configs = {
+ nyc,
+ testExclude: {
+ description: 'test-exclude options',
+ type: 'object',
+ properties: {
+ cwd,
+ ...testExclude
+ }
+ },
+ babelPluginIstanbul: {
+ description: 'babel-plugin-istanbul options',
+ type: 'object',
+ properties: {
+ cwd,
+ ...testExclude,
+ ...instrumentVisitor
+ }
+ },
+ instrumentVisitor: {
+ description: 'instrument visitor options',
+ type: 'object',
+ properties: instrumentVisitor
+ },
+ instrumenter: {
+ description: 'stand-alone instrumenter options',
+ type: 'object',
+ properties: {
+ ...instrumentVisitor,
+ ...instrumentParseGen
+ }
+ }
+};
+
+function defaultsReducer(defaults, [name, {default: value}]) {
+ /* Modifying arrays in defaults is safe, does not change schema. */
+ if (Array.isArray(value)) {
+ value = [...value];
+ }
+
+ return Object.assign(defaults, {[name]: value});
+}
+
+module.exports = {
+ ...configs,
+ defaults: Object.keys(configs).reduce(
+ (defaults, id) => {
+ Object.defineProperty(defaults, id, {
+ enumerable: true,
+ get() {
+ /* This defers `process.cwd()` until defaults are requested. */
+ return Object.entries(configs[id].properties)
+ .filter(([, info]) => 'default' in info)
+ .reduce(defaultsReducer, {});
+ }
+ });
+
+ return defaults;
+ },
+ {}
+ )
+};
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@istanbuljs/schema/package.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@istanbuljs/schema/package.json
new file mode 100644
index 00000000..1d22cde9
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@istanbuljs/schema/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "@istanbuljs/schema",
+ "version": "0.1.3",
+ "description": "Schemas describing various structures used by nyc and istanbuljs",
+ "main": "index.js",
+ "scripts": {
+ "release": "standard-version --sign",
+ "pretest": "xo",
+ "test": "tap",
+ "snap": "npm test -- --snapshot"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "author": "Corey Farrell",
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/istanbuljs/schema.git"
+ },
+ "bugs": {
+ "url": "https://github.com/istanbuljs/schema/issues"
+ },
+ "homepage": "https://github.com/istanbuljs/schema#readme",
+ "devDependencies": {
+ "standard-version": "^7.0.0",
+ "tap": "^14.6.7",
+ "xo": "^0.25.3"
+ }
+}
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/LICENSE b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/LICENSE
new file mode 100644
index 00000000..1f6ce94c
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/LICENSE
@@ -0,0 +1,19 @@
+Copyright 2024 Justin Ridgewell
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/README.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/README.md
new file mode 100644
index 00000000..93692b10
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/README.md
@@ -0,0 +1,227 @@
+# @jridgewell/gen-mapping
+
+> Generate source maps
+
+`gen-mapping` allows you to generate a source map during transpilation or minification.
+With a source map, you're able to trace the original location in the source file, either in Chrome's
+DevTools or using a library like [`@jridgewell/trace-mapping`][trace-mapping].
+
+You may already be familiar with the [`source-map`][source-map] package's `SourceMapGenerator`. This
+provides the same `addMapping` and `setSourceContent` API.
+
+## Installation
+
+```sh
+npm install @jridgewell/gen-mapping
+```
+
+## Usage
+
+```typescript
+import { GenMapping, addMapping, setSourceContent, toEncodedMap, toDecodedMap } from '@jridgewell/gen-mapping';
+
+const map = new GenMapping({
+ file: 'output.js',
+ sourceRoot: 'https://example.com/',
+});
+
+setSourceContent(map, 'input.js', `function foo() {}`);
+
+addMapping(map, {
+ // Lines start at line 1, columns at column 0.
+ generated: { line: 1, column: 0 },
+ source: 'input.js',
+ original: { line: 1, column: 0 },
+});
+
+addMapping(map, {
+ generated: { line: 1, column: 9 },
+ source: 'input.js',
+ original: { line: 1, column: 9 },
+ name: 'foo',
+});
+
+assert.deepEqual(toDecodedMap(map), {
+ version: 3,
+ file: 'output.js',
+ names: ['foo'],
+ sourceRoot: 'https://example.com/',
+ sources: ['input.js'],
+ sourcesContent: ['function foo() {}'],
+ mappings: [
+ [ [0, 0, 0, 0], [9, 0, 0, 9, 0] ]
+ ],
+});
+
+assert.deepEqual(toEncodedMap(map), {
+ version: 3,
+ file: 'output.js',
+ names: ['foo'],
+ sourceRoot: 'https://example.com/',
+ sources: ['input.js'],
+ sourcesContent: ['function foo() {}'],
+ mappings: 'AAAA,SAASA',
+});
+```
+
+### Smaller Sourcemaps
+
+Not everything needs to be added to a sourcemap, and needless markings can cause signficantly
+larger file sizes. `gen-mapping` exposes `maybeAddSegment`/`maybeAddMapping` APIs that will
+intelligently determine if this marking adds useful information. If not, the marking will be
+skipped.
+
+```typescript
+import { maybeAddMapping } from '@jridgewell/gen-mapping';
+
+const map = new GenMapping();
+
+// Adding a sourceless marking at the beginning of a line isn't useful.
+maybeAddMapping(map, {
+ generated: { line: 1, column: 0 },
+});
+
+// Adding a new source marking is useful.
+maybeAddMapping(map, {
+ generated: { line: 1, column: 0 },
+ source: 'input.js',
+ original: { line: 1, column: 0 },
+});
+
+// But adding another marking pointing to the exact same original location isn't, even if the
+// generated column changed.
+maybeAddMapping(map, {
+ generated: { line: 1, column: 9 },
+ source: 'input.js',
+ original: { line: 1, column: 0 },
+});
+
+assert.deepEqual(toEncodedMap(map), {
+ version: 3,
+ names: [],
+ sources: ['input.js'],
+ sourcesContent: [null],
+ mappings: 'AAAA',
+});
+```
+
+## Benchmarks
+
+```
+node v18.0.0
+
+amp.js.map
+Memory Usage:
+gen-mapping: addSegment 5852872 bytes
+gen-mapping: addMapping 7716042 bytes
+source-map-js 6143250 bytes
+source-map-0.6.1 6124102 bytes
+source-map-0.8.0 6121173 bytes
+Smallest memory usage is gen-mapping: addSegment
+
+Adding speed:
+gen-mapping: addSegment x 441 ops/sec ±2.07% (90 runs sampled)
+gen-mapping: addMapping x 350 ops/sec ±2.40% (86 runs sampled)
+source-map-js: addMapping x 169 ops/sec ±2.42% (80 runs sampled)
+source-map-0.6.1: addMapping x 167 ops/sec ±2.56% (80 runs sampled)
+source-map-0.8.0: addMapping x 168 ops/sec ±2.52% (80 runs sampled)
+Fastest is gen-mapping: addSegment
+
+Generate speed:
+gen-mapping: decoded output x 150,824,370 ops/sec ±0.07% (102 runs sampled)
+gen-mapping: encoded output x 663 ops/sec ±0.22% (98 runs sampled)
+source-map-js: encoded output x 197 ops/sec ±0.45% (84 runs sampled)
+source-map-0.6.1: encoded output x 198 ops/sec ±0.33% (85 runs sampled)
+source-map-0.8.0: encoded output x 197 ops/sec ±0.06% (93 runs sampled)
+Fastest is gen-mapping: decoded output
+
+
+***
+
+
+babel.min.js.map
+Memory Usage:
+gen-mapping: addSegment 37578063 bytes
+gen-mapping: addMapping 37212897 bytes
+source-map-js 47638527 bytes
+source-map-0.6.1 47690503 bytes
+source-map-0.8.0 47470188 bytes
+Smallest memory usage is gen-mapping: addMapping
+
+Adding speed:
+gen-mapping: addSegment x 31.05 ops/sec ±8.31% (43 runs sampled)
+gen-mapping: addMapping x 29.83 ops/sec ±7.36% (51 runs sampled)
+source-map-js: addMapping x 20.73 ops/sec ±6.22% (38 runs sampled)
+source-map-0.6.1: addMapping x 20.03 ops/sec ±10.51% (38 runs sampled)
+source-map-0.8.0: addMapping x 19.30 ops/sec ±8.27% (37 runs sampled)
+Fastest is gen-mapping: addSegment
+
+Generate speed:
+gen-mapping: decoded output x 381,379,234 ops/sec ±0.29% (96 runs sampled)
+gen-mapping: encoded output x 95.15 ops/sec ±2.98% (72 runs sampled)
+source-map-js: encoded output x 15.20 ops/sec ±7.41% (33 runs sampled)
+source-map-0.6.1: encoded output x 16.36 ops/sec ±10.46% (31 runs sampled)
+source-map-0.8.0: encoded output x 16.06 ops/sec ±6.45% (31 runs sampled)
+Fastest is gen-mapping: decoded output
+
+
+***
+
+
+preact.js.map
+Memory Usage:
+gen-mapping: addSegment 416247 bytes
+gen-mapping: addMapping 419824 bytes
+source-map-js 1024619 bytes
+source-map-0.6.1 1146004 bytes
+source-map-0.8.0 1113250 bytes
+Smallest memory usage is gen-mapping: addSegment
+
+Adding speed:
+gen-mapping: addSegment x 13,755 ops/sec ±0.15% (98 runs sampled)
+gen-mapping: addMapping x 13,013 ops/sec ±0.11% (101 runs sampled)
+source-map-js: addMapping x 4,564 ops/sec ±0.21% (98 runs sampled)
+source-map-0.6.1: addMapping x 4,562 ops/sec ±0.11% (99 runs sampled)
+source-map-0.8.0: addMapping x 4,593 ops/sec ±0.11% (100 runs sampled)
+Fastest is gen-mapping: addSegment
+
+Generate speed:
+gen-mapping: decoded output x 379,864,020 ops/sec ±0.23% (93 runs sampled)
+gen-mapping: encoded output x 14,368 ops/sec ±4.07% (82 runs sampled)
+source-map-js: encoded output x 5,261 ops/sec ±0.21% (99 runs sampled)
+source-map-0.6.1: encoded output x 5,124 ops/sec ±0.58% (99 runs sampled)
+source-map-0.8.0: encoded output x 5,434 ops/sec ±0.33% (96 runs sampled)
+Fastest is gen-mapping: decoded output
+
+
+***
+
+
+react.js.map
+Memory Usage:
+gen-mapping: addSegment 975096 bytes
+gen-mapping: addMapping 1102981 bytes
+source-map-js 2918836 bytes
+source-map-0.6.1 2885435 bytes
+source-map-0.8.0 2874336 bytes
+Smallest memory usage is gen-mapping: addSegment
+
+Adding speed:
+gen-mapping: addSegment x 4,772 ops/sec ±0.15% (100 runs sampled)
+gen-mapping: addMapping x 4,456 ops/sec ±0.13% (97 runs sampled)
+source-map-js: addMapping x 1,618 ops/sec ±0.24% (97 runs sampled)
+source-map-0.6.1: addMapping x 1,622 ops/sec ±0.12% (99 runs sampled)
+source-map-0.8.0: addMapping x 1,631 ops/sec ±0.12% (100 runs sampled)
+Fastest is gen-mapping: addSegment
+
+Generate speed:
+gen-mapping: decoded output x 379,107,695 ops/sec ±0.07% (99 runs sampled)
+gen-mapping: encoded output x 5,421 ops/sec ±1.60% (89 runs sampled)
+source-map-js: encoded output x 2,113 ops/sec ±1.81% (98 runs sampled)
+source-map-0.6.1: encoded output x 2,126 ops/sec ±0.10% (100 runs sampled)
+source-map-0.8.0: encoded output x 2,176 ops/sec ±0.39% (98 runs sampled)
+Fastest is gen-mapping: decoded output
+```
+
+[source-map]: https://www.npmjs.com/package/source-map
+[trace-mapping]: https://github.com/jridgewell/sourcemaps/tree/main/packages/trace-mapping
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/package.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/package.json
new file mode 100644
index 00000000..036f9b79
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/package.json
@@ -0,0 +1,67 @@
+{
+ "name": "@jridgewell/gen-mapping",
+ "version": "0.3.13",
+ "description": "Generate source maps",
+ "keywords": [
+ "source",
+ "map"
+ ],
+ "main": "dist/gen-mapping.umd.js",
+ "module": "dist/gen-mapping.mjs",
+ "types": "types/gen-mapping.d.cts",
+ "files": [
+ "dist",
+ "src",
+ "types"
+ ],
+ "exports": {
+ ".": [
+ {
+ "import": {
+ "types": "./types/gen-mapping.d.mts",
+ "default": "./dist/gen-mapping.mjs"
+ },
+ "default": {
+ "types": "./types/gen-mapping.d.cts",
+ "default": "./dist/gen-mapping.umd.js"
+ }
+ },
+ "./dist/gen-mapping.umd.js"
+ ],
+ "./package.json": "./package.json"
+ },
+ "scripts": {
+ "benchmark": "run-s build:code benchmark:*",
+ "benchmark:install": "cd benchmark && npm install",
+ "benchmark:only": "node --expose-gc benchmark/index.js",
+ "build": "run-s -n build:code build:types",
+ "build:code": "node ../../esbuild.mjs gen-mapping.ts",
+ "build:types": "run-s build:types:force build:types:emit build:types:mts",
+ "build:types:force": "rimraf tsconfig.build.tsbuildinfo",
+ "build:types:emit": "tsc --project tsconfig.build.json",
+ "build:types:mts": "node ../../mts-types.mjs",
+ "clean": "run-s -n clean:code clean:types",
+ "clean:code": "tsc --build --clean tsconfig.build.json",
+ "clean:types": "rimraf dist types",
+ "test": "run-s -n test:types test:only test:format",
+ "test:format": "prettier --check '{src,test}/**/*.ts'",
+ "test:only": "mocha",
+ "test:types": "eslint '{src,test}/**/*.ts'",
+ "lint": "run-s -n lint:types lint:format",
+ "lint:format": "npm run test:format -- --write",
+ "lint:types": "npm run test:types -- --fix",
+ "prepublishOnly": "npm run-s -n build test"
+ },
+ "homepage": "https://github.com/jridgewell/sourcemaps/tree/main/packages/gen-mapping",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/jridgewell/sourcemaps.git",
+ "directory": "packages/gen-mapping"
+ },
+ "author": "Justin Ridgewell ",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+}
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/src/gen-mapping.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/src/gen-mapping.ts
new file mode 100644
index 00000000..ecc878c5
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/src/gen-mapping.ts
@@ -0,0 +1,614 @@
+import { SetArray, put, remove } from './set-array';
+import {
+ encode,
+ // encodeGeneratedRanges,
+ // encodeOriginalScopes
+} from '@jridgewell/sourcemap-codec';
+import { TraceMap, decodedMappings } from '@jridgewell/trace-mapping';
+
+import {
+ COLUMN,
+ SOURCES_INDEX,
+ SOURCE_LINE,
+ SOURCE_COLUMN,
+ NAMES_INDEX,
+} from './sourcemap-segment';
+
+import type { SourceMapInput } from '@jridgewell/trace-mapping';
+// import type { OriginalScope, GeneratedRange } from '@jridgewell/sourcemap-codec';
+import type { SourceMapSegment } from './sourcemap-segment';
+import type {
+ DecodedSourceMap,
+ EncodedSourceMap,
+ Pos,
+ Mapping,
+ // BindingExpressionRange,
+ // OriginalPos,
+ // OriginalScopeInfo,
+ // GeneratedRangeInfo,
+} from './types';
+
+export type { DecodedSourceMap, EncodedSourceMap, Mapping };
+
+export type Options = {
+ file?: string | null;
+ sourceRoot?: string | null;
+};
+
+const NO_NAME = -1;
+
+/**
+ * Provides the state to generate a sourcemap.
+ */
+export class GenMapping {
+ declare private _names: SetArray;
+ declare private _sources: SetArray;
+ declare private _sourcesContent: (string | null)[];
+ declare private _mappings: SourceMapSegment[][];
+ // private declare _originalScopes: OriginalScope[][];
+ // private declare _generatedRanges: GeneratedRange[];
+ declare private _ignoreList: SetArray;
+ declare file: string | null | undefined;
+ declare sourceRoot: string | null | undefined;
+
+ constructor({ file, sourceRoot }: Options = {}) {
+ this._names = new SetArray();
+ this._sources = new SetArray();
+ this._sourcesContent = [];
+ this._mappings = [];
+ // this._originalScopes = [];
+ // this._generatedRanges = [];
+ this.file = file;
+ this.sourceRoot = sourceRoot;
+ this._ignoreList = new SetArray();
+ }
+}
+
+interface PublicMap {
+ _names: GenMapping['_names'];
+ _sources: GenMapping['_sources'];
+ _sourcesContent: GenMapping['_sourcesContent'];
+ _mappings: GenMapping['_mappings'];
+ // _originalScopes: GenMapping['_originalScopes'];
+ // _generatedRanges: GenMapping['_generatedRanges'];
+ _ignoreList: GenMapping['_ignoreList'];
+}
+
+/**
+ * Typescript doesn't allow friend access to private fields, so this just casts the map into a type
+ * with public access modifiers.
+ */
+function cast(map: unknown): PublicMap {
+ return map as any;
+}
+
+/**
+ * A low-level API to associate a generated position with an original source position. Line and
+ * column here are 0-based, unlike `addMapping`.
+ */
+export function addSegment(
+ map: GenMapping,
+ genLine: number,
+ genColumn: number,
+ source?: null,
+ sourceLine?: null,
+ sourceColumn?: null,
+ name?: null,
+ content?: null,
+): void;
+export function addSegment(
+ map: GenMapping,
+ genLine: number,
+ genColumn: number,
+ source: string,
+ sourceLine: number,
+ sourceColumn: number,
+ name?: null,
+ content?: string | null,
+): void;
+export function addSegment(
+ map: GenMapping,
+ genLine: number,
+ genColumn: number,
+ source: string,
+ sourceLine: number,
+ sourceColumn: number,
+ name: string,
+ content?: string | null,
+): void;
+export function addSegment(
+ map: GenMapping,
+ genLine: number,
+ genColumn: number,
+ source?: string | null,
+ sourceLine?: number | null,
+ sourceColumn?: number | null,
+ name?: string | null,
+ content?: string | null,
+): void {
+ return addSegmentInternal(
+ false,
+ map,
+ genLine,
+ genColumn,
+ source,
+ sourceLine,
+ sourceColumn,
+ name,
+ content,
+ );
+}
+
+/**
+ * A high-level API to associate a generated position with an original source position. Line is
+ * 1-based, but column is 0-based, due to legacy behavior in `source-map` library.
+ */
+export function addMapping(
+ map: GenMapping,
+ mapping: {
+ generated: Pos;
+ source?: null;
+ original?: null;
+ name?: null;
+ content?: null;
+ },
+): void;
+export function addMapping(
+ map: GenMapping,
+ mapping: {
+ generated: Pos;
+ source: string;
+ original: Pos;
+ name?: null;
+ content?: string | null;
+ },
+): void;
+export function addMapping(
+ map: GenMapping,
+ mapping: {
+ generated: Pos;
+ source: string;
+ original: Pos;
+ name: string;
+ content?: string | null;
+ },
+): void;
+export function addMapping(
+ map: GenMapping,
+ mapping: {
+ generated: Pos;
+ source?: string | null;
+ original?: Pos | null;
+ name?: string | null;
+ content?: string | null;
+ },
+): void {
+ return addMappingInternal(false, map, mapping as Parameters[2]);
+}
+
+/**
+ * Same as `addSegment`, but will only add the segment if it generates useful information in the
+ * resulting map. This only works correctly if segments are added **in order**, meaning you should
+ * not add a segment with a lower generated line/column than one that came before.
+ */
+export const maybeAddSegment: typeof addSegment = (
+ map,
+ genLine,
+ genColumn,
+ source,
+ sourceLine,
+ sourceColumn,
+ name,
+ content,
+) => {
+ return addSegmentInternal(
+ true,
+ map,
+ genLine,
+ genColumn,
+ source,
+ sourceLine,
+ sourceColumn,
+ name,
+ content,
+ );
+};
+
+/**
+ * Same as `addMapping`, but will only add the mapping if it generates useful information in the
+ * resulting map. This only works correctly if mappings are added **in order**, meaning you should
+ * not add a mapping with a lower generated line/column than one that came before.
+ */
+export const maybeAddMapping: typeof addMapping = (map, mapping) => {
+ return addMappingInternal(true, map, mapping as Parameters[2]);
+};
+
+/**
+ * Adds/removes the content of the source file to the source map.
+ */
+export function setSourceContent(map: GenMapping, source: string, content: string | null): void {
+ const {
+ _sources: sources,
+ _sourcesContent: sourcesContent,
+ // _originalScopes: originalScopes,
+ } = cast(map);
+ const index = put(sources, source);
+ sourcesContent[index] = content;
+ // if (index === originalScopes.length) originalScopes[index] = [];
+}
+
+export function setIgnore(map: GenMapping, source: string, ignore = true) {
+ const {
+ _sources: sources,
+ _sourcesContent: sourcesContent,
+ _ignoreList: ignoreList,
+ // _originalScopes: originalScopes,
+ } = cast(map);
+ const index = put(sources, source);
+ if (index === sourcesContent.length) sourcesContent[index] = null;
+ // if (index === originalScopes.length) originalScopes[index] = [];
+ if (ignore) put(ignoreList, index);
+ else remove(ignoreList, index);
+}
+
+/**
+ * Returns a sourcemap object (with decoded mappings) suitable for passing to a library that expects
+ * a sourcemap, or to JSON.stringify.
+ */
+export function toDecodedMap(map: GenMapping): DecodedSourceMap {
+ const {
+ _mappings: mappings,
+ _sources: sources,
+ _sourcesContent: sourcesContent,
+ _names: names,
+ _ignoreList: ignoreList,
+ // _originalScopes: originalScopes,
+ // _generatedRanges: generatedRanges,
+ } = cast(map);
+ removeEmptyFinalLines(mappings);
+
+ return {
+ version: 3,
+ file: map.file || undefined,
+ names: names.array,
+ sourceRoot: map.sourceRoot || undefined,
+ sources: sources.array,
+ sourcesContent,
+ mappings,
+ // originalScopes,
+ // generatedRanges,
+ ignoreList: ignoreList.array,
+ };
+}
+
+/**
+ * Returns a sourcemap object (with encoded mappings) suitable for passing to a library that expects
+ * a sourcemap, or to JSON.stringify.
+ */
+export function toEncodedMap(map: GenMapping): EncodedSourceMap {
+ const decoded = toDecodedMap(map);
+ return Object.assign({}, decoded, {
+ // originalScopes: decoded.originalScopes.map((os) => encodeOriginalScopes(os)),
+ // generatedRanges: encodeGeneratedRanges(decoded.generatedRanges as GeneratedRange[]),
+ mappings: encode(decoded.mappings as SourceMapSegment[][]),
+ });
+}
+
+/**
+ * Constructs a new GenMapping, using the already present mappings of the input.
+ */
+export function fromMap(input: SourceMapInput): GenMapping {
+ const map = new TraceMap(input);
+ const gen = new GenMapping({ file: map.file, sourceRoot: map.sourceRoot });
+
+ putAll(cast(gen)._names, map.names);
+ putAll(cast(gen)._sources, map.sources as string[]);
+ cast(gen)._sourcesContent = map.sourcesContent || map.sources.map(() => null);
+ cast(gen)._mappings = decodedMappings(map) as GenMapping['_mappings'];
+ // TODO: implement originalScopes/generatedRanges
+ if (map.ignoreList) putAll(cast(gen)._ignoreList, map.ignoreList);
+
+ return gen;
+}
+
+/**
+ * Returns an array of high-level mapping objects for every recorded segment, which could then be
+ * passed to the `source-map` library.
+ */
+export function allMappings(map: GenMapping): Mapping[] {
+ const out: Mapping[] = [];
+ const { _mappings: mappings, _sources: sources, _names: names } = cast(map);
+
+ for (let i = 0; i < mappings.length; i++) {
+ const line = mappings[i];
+ for (let j = 0; j < line.length; j++) {
+ const seg = line[j];
+
+ const generated = { line: i + 1, column: seg[COLUMN] };
+ let source: string | undefined = undefined;
+ let original: Pos | undefined = undefined;
+ let name: string | undefined = undefined;
+
+ if (seg.length !== 1) {
+ source = sources.array[seg[SOURCES_INDEX]];
+ original = { line: seg[SOURCE_LINE] + 1, column: seg[SOURCE_COLUMN] };
+
+ if (seg.length === 5) name = names.array[seg[NAMES_INDEX]];
+ }
+
+ out.push({ generated, source, original, name } as Mapping);
+ }
+ }
+
+ return out;
+}
+
+// This split declaration is only so that terser can elminiate the static initialization block.
+function addSegmentInternal(
+ skipable: boolean,
+ map: GenMapping,
+ genLine: number,
+ genColumn: number,
+ source: S,
+ sourceLine: S extends string ? number : null | undefined,
+ sourceColumn: S extends string ? number : null | undefined,
+ name: S extends string ? string | null | undefined : null | undefined,
+ content: S extends string ? string | null | undefined : null | undefined,
+): void {
+ const {
+ _mappings: mappings,
+ _sources: sources,
+ _sourcesContent: sourcesContent,
+ _names: names,
+ // _originalScopes: originalScopes,
+ } = cast(map);
+ const line = getIndex(mappings, genLine);
+ const index = getColumnIndex(line, genColumn);
+
+ if (!source) {
+ if (skipable && skipSourceless(line, index)) return;
+ return insert(line, index, [genColumn]);
+ }
+
+ // Sigh, TypeScript can't figure out sourceLine and sourceColumn aren't nullish if source
+ // isn't nullish.
+ assert(sourceLine);
+ assert(sourceColumn);
+
+ const sourcesIndex = put(sources, source);
+ const namesIndex = name ? put(names, name) : NO_NAME;
+ if (sourcesIndex === sourcesContent.length) sourcesContent[sourcesIndex] = content ?? null;
+ // if (sourcesIndex === originalScopes.length) originalScopes[sourcesIndex] = [];
+
+ if (skipable && skipSource(line, index, sourcesIndex, sourceLine, sourceColumn, namesIndex)) {
+ return;
+ }
+
+ return insert(
+ line,
+ index,
+ name
+ ? [genColumn, sourcesIndex, sourceLine, sourceColumn, namesIndex]
+ : [genColumn, sourcesIndex, sourceLine, sourceColumn],
+ );
+}
+
+function assert(_val: unknown): asserts _val is T {
+ // noop.
+}
+
+function getIndex(arr: T[][], index: number): T[] {
+ for (let i = arr.length; i <= index; i++) {
+ arr[i] = [];
+ }
+ return arr[index];
+}
+
+function getColumnIndex(line: SourceMapSegment[], genColumn: number): number {
+ let index = line.length;
+ for (let i = index - 1; i >= 0; index = i--) {
+ const current = line[i];
+ if (genColumn >= current[COLUMN]) break;
+ }
+ return index;
+}
+
+function insert(array: T[], index: number, value: T) {
+ for (let i = array.length; i > index; i--) {
+ array[i] = array[i - 1];
+ }
+ array[index] = value;
+}
+
+function removeEmptyFinalLines(mappings: SourceMapSegment[][]) {
+ const { length } = mappings;
+ let len = length;
+ for (let i = len - 1; i >= 0; len = i, i--) {
+ if (mappings[i].length > 0) break;
+ }
+ if (len < length) mappings.length = len;
+}
+
+function putAll(setarr: SetArray, array: T[]) {
+ for (let i = 0; i < array.length; i++) put(setarr, array[i]);
+}
+
+function skipSourceless(line: SourceMapSegment[], index: number): boolean {
+ // The start of a line is already sourceless, so adding a sourceless segment to the beginning
+ // doesn't generate any useful information.
+ if (index === 0) return true;
+
+ const prev = line[index - 1];
+ // If the previous segment is also sourceless, then adding another sourceless segment doesn't
+ // genrate any new information. Else, this segment will end the source/named segment and point to
+ // a sourceless position, which is useful.
+ return prev.length === 1;
+}
+
+function skipSource(
+ line: SourceMapSegment[],
+ index: number,
+ sourcesIndex: number,
+ sourceLine: number,
+ sourceColumn: number,
+ namesIndex: number,
+): boolean {
+ // A source/named segment at the start of a line gives position at that genColumn
+ if (index === 0) return false;
+
+ const prev = line[index - 1];
+
+ // If the previous segment is sourceless, then we're transitioning to a source.
+ if (prev.length === 1) return false;
+
+ // If the previous segment maps to the exact same source position, then this segment doesn't
+ // provide any new position information.
+ return (
+ sourcesIndex === prev[SOURCES_INDEX] &&
+ sourceLine === prev[SOURCE_LINE] &&
+ sourceColumn === prev[SOURCE_COLUMN] &&
+ namesIndex === (prev.length === 5 ? prev[NAMES_INDEX] : NO_NAME)
+ );
+}
+
+function addMappingInternal(
+ skipable: boolean,
+ map: GenMapping,
+ mapping: {
+ generated: Pos;
+ source: S;
+ original: S extends string ? Pos : null | undefined;
+ name: S extends string ? string | null | undefined : null | undefined;
+ content: S extends string ? string | null | undefined : null | undefined;
+ },
+) {
+ const { generated, source, original, name, content } = mapping;
+ if (!source) {
+ return addSegmentInternal(
+ skipable,
+ map,
+ generated.line - 1,
+ generated.column,
+ null,
+ null,
+ null,
+ null,
+ null,
+ );
+ }
+ assert(original);
+ return addSegmentInternal(
+ skipable,
+ map,
+ generated.line - 1,
+ generated.column,
+ source as string,
+ original.line - 1,
+ original.column,
+ name,
+ content,
+ );
+}
+
+/*
+export function addOriginalScope(
+ map: GenMapping,
+ data: {
+ start: Pos;
+ end: Pos;
+ source: string;
+ kind: string;
+ name?: string;
+ variables?: string[];
+ },
+): OriginalScopeInfo {
+ const { start, end, source, kind, name, variables } = data;
+ const {
+ _sources: sources,
+ _sourcesContent: sourcesContent,
+ _originalScopes: originalScopes,
+ _names: names,
+ } = cast(map);
+ const index = put(sources, source);
+ if (index === sourcesContent.length) sourcesContent[index] = null;
+ if (index === originalScopes.length) originalScopes[index] = [];
+
+ const kindIndex = put(names, kind);
+ const scope: OriginalScope = name
+ ? [start.line - 1, start.column, end.line - 1, end.column, kindIndex, put(names, name)]
+ : [start.line - 1, start.column, end.line - 1, end.column, kindIndex];
+ if (variables) {
+ scope.vars = variables.map((v) => put(names, v));
+ }
+ const len = originalScopes[index].push(scope);
+ return [index, len - 1, variables];
+}
+*/
+
+// Generated Ranges
+/*
+export function addGeneratedRange(
+ map: GenMapping,
+ data: {
+ start: Pos;
+ isScope: boolean;
+ originalScope?: OriginalScopeInfo;
+ callsite?: OriginalPos;
+ },
+): GeneratedRangeInfo {
+ const { start, isScope, originalScope, callsite } = data;
+ const {
+ _originalScopes: originalScopes,
+ _sources: sources,
+ _sourcesContent: sourcesContent,
+ _generatedRanges: generatedRanges,
+ } = cast(map);
+
+ const range: GeneratedRange = [
+ start.line - 1,
+ start.column,
+ 0,
+ 0,
+ originalScope ? originalScope[0] : -1,
+ originalScope ? originalScope[1] : -1,
+ ];
+ if (originalScope?.[2]) {
+ range.bindings = originalScope[2].map(() => [[-1]]);
+ }
+ if (callsite) {
+ const index = put(sources, callsite.source);
+ if (index === sourcesContent.length) sourcesContent[index] = null;
+ if (index === originalScopes.length) originalScopes[index] = [];
+ range.callsite = [index, callsite.line - 1, callsite.column];
+ }
+ if (isScope) range.isScope = true;
+ generatedRanges.push(range);
+
+ return [range, originalScope?.[2]];
+}
+
+export function setEndPosition(range: GeneratedRangeInfo, pos: Pos) {
+ range[0][2] = pos.line - 1;
+ range[0][3] = pos.column;
+}
+
+export function addBinding(
+ map: GenMapping,
+ range: GeneratedRangeInfo,
+ variable: string,
+ expression: string | BindingExpressionRange,
+) {
+ const { _names: names } = cast(map);
+ const bindings = (range[0].bindings ||= []);
+ const vars = range[1];
+
+ const index = vars!.indexOf(variable);
+ const binding = getIndex(bindings, index);
+
+ if (typeof expression === 'string') binding[0] = [put(names, expression)];
+ else {
+ const { start } = expression;
+ binding.push([put(names, expression.expression), start.line - 1, start.column]);
+ }
+}
+*/
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/src/set-array.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/src/set-array.ts
new file mode 100644
index 00000000..a2a73a52
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/src/set-array.ts
@@ -0,0 +1,82 @@
+type Key = string | number | symbol;
+
+/**
+ * SetArray acts like a `Set` (allowing only one occurrence of a string `key`), but provides the
+ * index of the `key` in the backing array.
+ *
+ * This is designed to allow synchronizing a second array with the contents of the backing array,
+ * like how in a sourcemap `sourcesContent[i]` is the source content associated with `source[i]`,
+ * and there are never duplicates.
+ */
+export class SetArray {
+ declare private _indexes: Record;
+ declare array: readonly T[];
+
+ constructor() {
+ this._indexes = { __proto__: null } as any;
+ this.array = [];
+ }
+}
+
+interface PublicSet {
+ array: T[];
+ _indexes: SetArray['_indexes'];
+}
+
+/**
+ * Typescript doesn't allow friend access to private fields, so this just casts the set into a type
+ * with public access modifiers.
+ */
+function cast(set: SetArray): PublicSet {
+ return set as any;
+}
+
+/**
+ * Gets the index associated with `key` in the backing array, if it is already present.
+ */
+export function get(setarr: SetArray, key: T): number | undefined {
+ return cast(setarr)._indexes[key];
+}
+
+/**
+ * Puts `key` into the backing array, if it is not already present. Returns
+ * the index of the `key` in the backing array.
+ */
+export function put(setarr: SetArray, key: T): number {
+ // The key may or may not be present. If it is present, it's a number.
+ const index = get(setarr, key);
+ if (index !== undefined) return index;
+
+ const { array, _indexes: indexes } = cast(setarr);
+
+ const length = array.push(key);
+ return (indexes[key] = length - 1);
+}
+
+/**
+ * Pops the last added item out of the SetArray.
+ */
+export function pop(setarr: SetArray): void {
+ const { array, _indexes: indexes } = cast(setarr);
+ if (array.length === 0) return;
+
+ const last = array.pop()!;
+ indexes[last] = undefined;
+}
+
+/**
+ * Removes the key, if it exists in the set.
+ */
+export function remove(setarr: SetArray, key: T): void {
+ const index = get(setarr, key);
+ if (index === undefined) return;
+
+ const { array, _indexes: indexes } = cast(setarr);
+ for (let i = index + 1; i < array.length; i++) {
+ const k = array[i];
+ array[i - 1] = k;
+ indexes[k]!--;
+ }
+ indexes[key] = undefined;
+ array.pop();
+}
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/src/sourcemap-segment.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/src/sourcemap-segment.ts
new file mode 100644
index 00000000..fb296dd3
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/src/sourcemap-segment.ts
@@ -0,0 +1,16 @@
+type GeneratedColumn = number;
+type SourcesIndex = number;
+type SourceLine = number;
+type SourceColumn = number;
+type NamesIndex = number;
+
+export type SourceMapSegment =
+ | [GeneratedColumn]
+ | [GeneratedColumn, SourcesIndex, SourceLine, SourceColumn]
+ | [GeneratedColumn, SourcesIndex, SourceLine, SourceColumn, NamesIndex];
+
+export const COLUMN = 0;
+export const SOURCES_INDEX = 1;
+export const SOURCE_LINE = 2;
+export const SOURCE_COLUMN = 3;
+export const NAMES_INDEX = 4;
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/src/types.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/src/types.ts
new file mode 100644
index 00000000..b087f706
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/src/types.ts
@@ -0,0 +1,61 @@
+// import type { GeneratedRange, OriginalScope } from '@jridgewell/sourcemap-codec';
+import type { SourceMapSegment } from './sourcemap-segment';
+
+export interface SourceMapV3 {
+ file?: string | null;
+ names: readonly string[];
+ sourceRoot?: string;
+ sources: readonly (string | null)[];
+ sourcesContent?: readonly (string | null)[];
+ version: 3;
+ ignoreList?: readonly number[];
+}
+
+export interface EncodedSourceMap extends SourceMapV3 {
+ mappings: string;
+ // originalScopes: string[];
+ // generatedRanges: string;
+}
+
+export interface DecodedSourceMap extends SourceMapV3 {
+ mappings: readonly SourceMapSegment[][];
+ // originalScopes: readonly OriginalScope[][];
+ // generatedRanges: readonly GeneratedRange[];
+}
+
+export interface Pos {
+ line: number; // 1-based
+ column: number; // 0-based
+}
+
+export interface OriginalPos extends Pos {
+ source: string;
+}
+
+export interface BindingExpressionRange {
+ start: Pos;
+ expression: string;
+}
+
+// export type OriginalScopeInfo = [number, number, string[] | undefined];
+// export type GeneratedRangeInfo = [GeneratedRange, string[] | undefined];
+
+export type Mapping =
+ | {
+ generated: Pos;
+ source: undefined;
+ original: undefined;
+ name: undefined;
+ }
+ | {
+ generated: Pos;
+ source: string;
+ original: Pos;
+ name: string;
+ }
+ | {
+ generated: Pos;
+ source: string;
+ original: Pos;
+ name: undefined;
+ };
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/gen-mapping.d.cts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/gen-mapping.d.cts
new file mode 100644
index 00000000..7618d857
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/gen-mapping.d.cts
@@ -0,0 +1,89 @@
+import type { SourceMapInput } from '@jridgewell/trace-mapping';
+import type { DecodedSourceMap, EncodedSourceMap, Pos, Mapping } from './types.cts';
+export type { DecodedSourceMap, EncodedSourceMap, Mapping };
+export type Options = {
+ file?: string | null;
+ sourceRoot?: string | null;
+};
+/**
+ * Provides the state to generate a sourcemap.
+ */
+export declare class GenMapping {
+ private _names;
+ private _sources;
+ private _sourcesContent;
+ private _mappings;
+ private _ignoreList;
+ file: string | null | undefined;
+ sourceRoot: string | null | undefined;
+ constructor({ file, sourceRoot }?: Options);
+}
+/**
+ * A low-level API to associate a generated position with an original source position. Line and
+ * column here are 0-based, unlike `addMapping`.
+ */
+export declare function addSegment(map: GenMapping, genLine: number, genColumn: number, source?: null, sourceLine?: null, sourceColumn?: null, name?: null, content?: null): void;
+export declare function addSegment(map: GenMapping, genLine: number, genColumn: number, source: string, sourceLine: number, sourceColumn: number, name?: null, content?: string | null): void;
+export declare function addSegment(map: GenMapping, genLine: number, genColumn: number, source: string, sourceLine: number, sourceColumn: number, name: string, content?: string | null): void;
+/**
+ * A high-level API to associate a generated position with an original source position. Line is
+ * 1-based, but column is 0-based, due to legacy behavior in `source-map` library.
+ */
+export declare function addMapping(map: GenMapping, mapping: {
+ generated: Pos;
+ source?: null;
+ original?: null;
+ name?: null;
+ content?: null;
+}): void;
+export declare function addMapping(map: GenMapping, mapping: {
+ generated: Pos;
+ source: string;
+ original: Pos;
+ name?: null;
+ content?: string | null;
+}): void;
+export declare function addMapping(map: GenMapping, mapping: {
+ generated: Pos;
+ source: string;
+ original: Pos;
+ name: string;
+ content?: string | null;
+}): void;
+/**
+ * Same as `addSegment`, but will only add the segment if it generates useful information in the
+ * resulting map. This only works correctly if segments are added **in order**, meaning you should
+ * not add a segment with a lower generated line/column than one that came before.
+ */
+export declare const maybeAddSegment: typeof addSegment;
+/**
+ * Same as `addMapping`, but will only add the mapping if it generates useful information in the
+ * resulting map. This only works correctly if mappings are added **in order**, meaning you should
+ * not add a mapping with a lower generated line/column than one that came before.
+ */
+export declare const maybeAddMapping: typeof addMapping;
+/**
+ * Adds/removes the content of the source file to the source map.
+ */
+export declare function setSourceContent(map: GenMapping, source: string, content: string | null): void;
+export declare function setIgnore(map: GenMapping, source: string, ignore?: boolean): void;
+/**
+ * Returns a sourcemap object (with decoded mappings) suitable for passing to a library that expects
+ * a sourcemap, or to JSON.stringify.
+ */
+export declare function toDecodedMap(map: GenMapping): DecodedSourceMap;
+/**
+ * Returns a sourcemap object (with encoded mappings) suitable for passing to a library that expects
+ * a sourcemap, or to JSON.stringify.
+ */
+export declare function toEncodedMap(map: GenMapping): EncodedSourceMap;
+/**
+ * Constructs a new GenMapping, using the already present mappings of the input.
+ */
+export declare function fromMap(input: SourceMapInput): GenMapping;
+/**
+ * Returns an array of high-level mapping objects for every recorded segment, which could then be
+ * passed to the `source-map` library.
+ */
+export declare function allMappings(map: GenMapping): Mapping[];
+//# sourceMappingURL=gen-mapping.d.ts.map
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/gen-mapping.d.cts.map b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/gen-mapping.d.cts.map
new file mode 100644
index 00000000..8a2b1835
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/gen-mapping.d.cts.map
@@ -0,0 +1 @@
+{"version":3,"file":"gen-mapping.d.ts","sourceRoot":"","sources":["../src/gen-mapping.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAGhE,OAAO,KAAK,EACV,gBAAgB,EAChB,gBAAgB,EAChB,GAAG,EACH,OAAO,EAKR,MAAM,SAAS,CAAC;AAEjB,YAAY,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,OAAO,EAAE,CAAC;AAE5D,MAAM,MAAM,OAAO,GAAG;IACpB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B,CAAC;AAIF;;GAEG;AACH,qBAAa,UAAU;IACrB,QAAgB,MAAM,CAAmB;IACzC,QAAgB,QAAQ,CAAmB;IAC3C,QAAgB,eAAe,CAAoB;IACnD,QAAgB,SAAS,CAAuB;IAGhD,QAAgB,WAAW,CAAmB;IACtC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAChC,UAAU,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;gBAElC,EAAE,IAAI,EAAE,UAAU,EAAE,GAAE,OAAY;CAW/C;AAoBD;;;GAGG;AACH,wBAAgB,UAAU,CACxB,GAAG,EAAE,UAAU,EACf,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE,IAAI,EACb,UAAU,CAAC,EAAE,IAAI,EACjB,YAAY,CAAC,EAAE,IAAI,EACnB,IAAI,CAAC,EAAE,IAAI,EACX,OAAO,CAAC,EAAE,IAAI,GACb,IAAI,CAAC;AACR,wBAAgB,UAAU,CACxB,GAAG,EAAE,UAAU,EACf,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM,EAClB,YAAY,EAAE,MAAM,EACpB,IAAI,CAAC,EAAE,IAAI,EACX,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,GACtB,IAAI,CAAC;AACR,wBAAgB,UAAU,CACxB,GAAG,EAAE,UAAU,EACf,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM,EAClB,YAAY,EAAE,MAAM,EACpB,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,GACtB,IAAI,CAAC;AAwBR;;;GAGG;AACH,wBAAgB,UAAU,CACxB,GAAG,EAAE,UAAU,EACf,OAAO,EAAE;IACP,SAAS,EAAE,GAAG,CAAC;IACf,MAAM,CAAC,EAAE,IAAI,CAAC;IACd,QAAQ,CAAC,EAAE,IAAI,CAAC;IAChB,IAAI,CAAC,EAAE,IAAI,CAAC;IACZ,OAAO,CAAC,EAAE,IAAI,CAAC;CAChB,GACA,IAAI,CAAC;AACR,wBAAgB,UAAU,CACxB,GAAG,EAAE,UAAU,EACf,OAAO,EAAE;IACP,SAAS,EAAE,GAAG,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,GAAG,CAAC;IACd,IAAI,CAAC,EAAE,IAAI,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB,GACA,IAAI,CAAC;AACR,wBAAgB,UAAU,CACxB,GAAG,EAAE,UAAU,EACf,OAAO,EAAE;IACP,SAAS,EAAE,GAAG,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,GAAG,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB,GACA,IAAI,CAAC;AAcR;;;;GAIG;AACH,eAAO,MAAM,eAAe,EAAE,OAAO,UAqBpC,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,eAAe,EAAE,OAAO,UAEpC,CAAC;AAEF;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAS9F;AAED,wBAAgB,SAAS,CAAC,GAAG,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,UAAO,QAYvE;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,UAAU,GAAG,gBAAgB,CAwB9D;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,UAAU,GAAG,gBAAgB,CAO9D;AAED;;GAEG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,cAAc,GAAG,UAAU,CAYzD;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO,EAAE,CA0BtD"}
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/gen-mapping.d.mts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/gen-mapping.d.mts
new file mode 100644
index 00000000..bbc0d89c
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/gen-mapping.d.mts
@@ -0,0 +1,89 @@
+import type { SourceMapInput } from '@jridgewell/trace-mapping';
+import type { DecodedSourceMap, EncodedSourceMap, Pos, Mapping } from './types.mts';
+export type { DecodedSourceMap, EncodedSourceMap, Mapping };
+export type Options = {
+ file?: string | null;
+ sourceRoot?: string | null;
+};
+/**
+ * Provides the state to generate a sourcemap.
+ */
+export declare class GenMapping {
+ private _names;
+ private _sources;
+ private _sourcesContent;
+ private _mappings;
+ private _ignoreList;
+ file: string | null | undefined;
+ sourceRoot: string | null | undefined;
+ constructor({ file, sourceRoot }?: Options);
+}
+/**
+ * A low-level API to associate a generated position with an original source position. Line and
+ * column here are 0-based, unlike `addMapping`.
+ */
+export declare function addSegment(map: GenMapping, genLine: number, genColumn: number, source?: null, sourceLine?: null, sourceColumn?: null, name?: null, content?: null): void;
+export declare function addSegment(map: GenMapping, genLine: number, genColumn: number, source: string, sourceLine: number, sourceColumn: number, name?: null, content?: string | null): void;
+export declare function addSegment(map: GenMapping, genLine: number, genColumn: number, source: string, sourceLine: number, sourceColumn: number, name: string, content?: string | null): void;
+/**
+ * A high-level API to associate a generated position with an original source position. Line is
+ * 1-based, but column is 0-based, due to legacy behavior in `source-map` library.
+ */
+export declare function addMapping(map: GenMapping, mapping: {
+ generated: Pos;
+ source?: null;
+ original?: null;
+ name?: null;
+ content?: null;
+}): void;
+export declare function addMapping(map: GenMapping, mapping: {
+ generated: Pos;
+ source: string;
+ original: Pos;
+ name?: null;
+ content?: string | null;
+}): void;
+export declare function addMapping(map: GenMapping, mapping: {
+ generated: Pos;
+ source: string;
+ original: Pos;
+ name: string;
+ content?: string | null;
+}): void;
+/**
+ * Same as `addSegment`, but will only add the segment if it generates useful information in the
+ * resulting map. This only works correctly if segments are added **in order**, meaning you should
+ * not add a segment with a lower generated line/column than one that came before.
+ */
+export declare const maybeAddSegment: typeof addSegment;
+/**
+ * Same as `addMapping`, but will only add the mapping if it generates useful information in the
+ * resulting map. This only works correctly if mappings are added **in order**, meaning you should
+ * not add a mapping with a lower generated line/column than one that came before.
+ */
+export declare const maybeAddMapping: typeof addMapping;
+/**
+ * Adds/removes the content of the source file to the source map.
+ */
+export declare function setSourceContent(map: GenMapping, source: string, content: string | null): void;
+export declare function setIgnore(map: GenMapping, source: string, ignore?: boolean): void;
+/**
+ * Returns a sourcemap object (with decoded mappings) suitable for passing to a library that expects
+ * a sourcemap, or to JSON.stringify.
+ */
+export declare function toDecodedMap(map: GenMapping): DecodedSourceMap;
+/**
+ * Returns a sourcemap object (with encoded mappings) suitable for passing to a library that expects
+ * a sourcemap, or to JSON.stringify.
+ */
+export declare function toEncodedMap(map: GenMapping): EncodedSourceMap;
+/**
+ * Constructs a new GenMapping, using the already present mappings of the input.
+ */
+export declare function fromMap(input: SourceMapInput): GenMapping;
+/**
+ * Returns an array of high-level mapping objects for every recorded segment, which could then be
+ * passed to the `source-map` library.
+ */
+export declare function allMappings(map: GenMapping): Mapping[];
+//# sourceMappingURL=gen-mapping.d.ts.map
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/gen-mapping.d.mts.map b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/gen-mapping.d.mts.map
new file mode 100644
index 00000000..8a2b1835
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/gen-mapping.d.mts.map
@@ -0,0 +1 @@
+{"version":3,"file":"gen-mapping.d.ts","sourceRoot":"","sources":["../src/gen-mapping.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAGhE,OAAO,KAAK,EACV,gBAAgB,EAChB,gBAAgB,EAChB,GAAG,EACH,OAAO,EAKR,MAAM,SAAS,CAAC;AAEjB,YAAY,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,OAAO,EAAE,CAAC;AAE5D,MAAM,MAAM,OAAO,GAAG;IACpB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B,CAAC;AAIF;;GAEG;AACH,qBAAa,UAAU;IACrB,QAAgB,MAAM,CAAmB;IACzC,QAAgB,QAAQ,CAAmB;IAC3C,QAAgB,eAAe,CAAoB;IACnD,QAAgB,SAAS,CAAuB;IAGhD,QAAgB,WAAW,CAAmB;IACtC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAChC,UAAU,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;gBAElC,EAAE,IAAI,EAAE,UAAU,EAAE,GAAE,OAAY;CAW/C;AAoBD;;;GAGG;AACH,wBAAgB,UAAU,CACxB,GAAG,EAAE,UAAU,EACf,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE,IAAI,EACb,UAAU,CAAC,EAAE,IAAI,EACjB,YAAY,CAAC,EAAE,IAAI,EACnB,IAAI,CAAC,EAAE,IAAI,EACX,OAAO,CAAC,EAAE,IAAI,GACb,IAAI,CAAC;AACR,wBAAgB,UAAU,CACxB,GAAG,EAAE,UAAU,EACf,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM,EAClB,YAAY,EAAE,MAAM,EACpB,IAAI,CAAC,EAAE,IAAI,EACX,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,GACtB,IAAI,CAAC;AACR,wBAAgB,UAAU,CACxB,GAAG,EAAE,UAAU,EACf,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM,EAClB,YAAY,EAAE,MAAM,EACpB,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,GACtB,IAAI,CAAC;AAwBR;;;GAGG;AACH,wBAAgB,UAAU,CACxB,GAAG,EAAE,UAAU,EACf,OAAO,EAAE;IACP,SAAS,EAAE,GAAG,CAAC;IACf,MAAM,CAAC,EAAE,IAAI,CAAC;IACd,QAAQ,CAAC,EAAE,IAAI,CAAC;IAChB,IAAI,CAAC,EAAE,IAAI,CAAC;IACZ,OAAO,CAAC,EAAE,IAAI,CAAC;CAChB,GACA,IAAI,CAAC;AACR,wBAAgB,UAAU,CACxB,GAAG,EAAE,UAAU,EACf,OAAO,EAAE;IACP,SAAS,EAAE,GAAG,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,GAAG,CAAC;IACd,IAAI,CAAC,EAAE,IAAI,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB,GACA,IAAI,CAAC;AACR,wBAAgB,UAAU,CACxB,GAAG,EAAE,UAAU,EACf,OAAO,EAAE;IACP,SAAS,EAAE,GAAG,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,GAAG,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB,GACA,IAAI,CAAC;AAcR;;;;GAIG;AACH,eAAO,MAAM,eAAe,EAAE,OAAO,UAqBpC,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,eAAe,EAAE,OAAO,UAEpC,CAAC;AAEF;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAS9F;AAED,wBAAgB,SAAS,CAAC,GAAG,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,UAAO,QAYvE;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,UAAU,GAAG,gBAAgB,CAwB9D;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,UAAU,GAAG,gBAAgB,CAO9D;AAED;;GAEG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,cAAc,GAAG,UAAU,CAYzD;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO,EAAE,CA0BtD"}
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/set-array.d.cts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/set-array.d.cts
new file mode 100644
index 00000000..5d8cda35
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/set-array.d.cts
@@ -0,0 +1,33 @@
+type Key = string | number | symbol;
+/**
+ * SetArray acts like a `Set` (allowing only one occurrence of a string `key`), but provides the
+ * index of the `key` in the backing array.
+ *
+ * This is designed to allow synchronizing a second array with the contents of the backing array,
+ * like how in a sourcemap `sourcesContent[i]` is the source content associated with `source[i]`,
+ * and there are never duplicates.
+ */
+export declare class SetArray {
+ private _indexes;
+ array: readonly T[];
+ constructor();
+}
+/**
+ * Gets the index associated with `key` in the backing array, if it is already present.
+ */
+export declare function get(setarr: SetArray, key: T): number | undefined;
+/**
+ * Puts `key` into the backing array, if it is not already present. Returns
+ * the index of the `key` in the backing array.
+ */
+export declare function put(setarr: SetArray, key: T): number;
+/**
+ * Pops the last added item out of the SetArray.
+ */
+export declare function pop(setarr: SetArray): void;
+/**
+ * Removes the key, if it exists in the set.
+ */
+export declare function remove(setarr: SetArray, key: T): void;
+export {};
+//# sourceMappingURL=set-array.d.ts.map
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/set-array.d.cts.map b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/set-array.d.cts.map
new file mode 100644
index 00000000..c52b8bce
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/set-array.d.cts.map
@@ -0,0 +1 @@
+{"version":3,"file":"set-array.d.ts","sourceRoot":"","sources":["../src/set-array.ts"],"names":[],"mappings":"AAAA,KAAK,GAAG,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAEpC;;;;;;;GAOG;AACH,qBAAa,QAAQ,CAAC,CAAC,SAAS,GAAG,GAAG,GAAG;IACvC,QAAgB,QAAQ,CAAgC;IAChD,KAAK,EAAE,SAAS,CAAC,EAAE,CAAC;;CAM7B;AAeD;;GAEG;AACH,wBAAgB,GAAG,CAAC,CAAC,SAAS,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,MAAM,GAAG,SAAS,CAElF;AAED;;;GAGG;AACH,wBAAgB,GAAG,CAAC,CAAC,SAAS,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,MAAM,CAStE;AAED;;GAEG;AACH,wBAAgB,GAAG,CAAC,CAAC,SAAS,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC,GAAG,IAAI,CAM5D;AAED;;GAEG;AACH,wBAAgB,MAAM,CAAC,CAAC,SAAS,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,IAAI,CAYvE"}
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/set-array.d.mts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/set-array.d.mts
new file mode 100644
index 00000000..5d8cda35
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/set-array.d.mts
@@ -0,0 +1,33 @@
+type Key = string | number | symbol;
+/**
+ * SetArray acts like a `Set` (allowing only one occurrence of a string `key`), but provides the
+ * index of the `key` in the backing array.
+ *
+ * This is designed to allow synchronizing a second array with the contents of the backing array,
+ * like how in a sourcemap `sourcesContent[i]` is the source content associated with `source[i]`,
+ * and there are never duplicates.
+ */
+export declare class SetArray {
+ private _indexes;
+ array: readonly T[];
+ constructor();
+}
+/**
+ * Gets the index associated with `key` in the backing array, if it is already present.
+ */
+export declare function get(setarr: SetArray, key: T): number | undefined;
+/**
+ * Puts `key` into the backing array, if it is not already present. Returns
+ * the index of the `key` in the backing array.
+ */
+export declare function put(setarr: SetArray, key: T): number;
+/**
+ * Pops the last added item out of the SetArray.
+ */
+export declare function pop(setarr: SetArray): void;
+/**
+ * Removes the key, if it exists in the set.
+ */
+export declare function remove(setarr: SetArray, key: T): void;
+export {};
+//# sourceMappingURL=set-array.d.ts.map
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/set-array.d.mts.map b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/set-array.d.mts.map
new file mode 100644
index 00000000..c52b8bce
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/set-array.d.mts.map
@@ -0,0 +1 @@
+{"version":3,"file":"set-array.d.ts","sourceRoot":"","sources":["../src/set-array.ts"],"names":[],"mappings":"AAAA,KAAK,GAAG,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAEpC;;;;;;;GAOG;AACH,qBAAa,QAAQ,CAAC,CAAC,SAAS,GAAG,GAAG,GAAG;IACvC,QAAgB,QAAQ,CAAgC;IAChD,KAAK,EAAE,SAAS,CAAC,EAAE,CAAC;;CAM7B;AAeD;;GAEG;AACH,wBAAgB,GAAG,CAAC,CAAC,SAAS,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,MAAM,GAAG,SAAS,CAElF;AAED;;;GAGG;AACH,wBAAgB,GAAG,CAAC,CAAC,SAAS,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,MAAM,CAStE;AAED;;GAEG;AACH,wBAAgB,GAAG,CAAC,CAAC,SAAS,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC,GAAG,IAAI,CAM5D;AAED;;GAEG;AACH,wBAAgB,MAAM,CAAC,CAAC,SAAS,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,IAAI,CAYvE"}
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/sourcemap-segment.d.cts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/sourcemap-segment.d.cts
new file mode 100644
index 00000000..68862952
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/sourcemap-segment.d.cts
@@ -0,0 +1,13 @@
+type GeneratedColumn = number;
+type SourcesIndex = number;
+type SourceLine = number;
+type SourceColumn = number;
+type NamesIndex = number;
+export type SourceMapSegment = [GeneratedColumn] | [GeneratedColumn, SourcesIndex, SourceLine, SourceColumn] | [GeneratedColumn, SourcesIndex, SourceLine, SourceColumn, NamesIndex];
+export declare const COLUMN = 0;
+export declare const SOURCES_INDEX = 1;
+export declare const SOURCE_LINE = 2;
+export declare const SOURCE_COLUMN = 3;
+export declare const NAMES_INDEX = 4;
+export {};
+//# sourceMappingURL=sourcemap-segment.d.ts.map
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/sourcemap-segment.d.cts.map b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/sourcemap-segment.d.cts.map
new file mode 100644
index 00000000..23cdc452
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/sourcemap-segment.d.cts.map
@@ -0,0 +1 @@
+{"version":3,"file":"sourcemap-segment.d.ts","sourceRoot":"","sources":["../src/sourcemap-segment.ts"],"names":[],"mappings":"AAAA,KAAK,eAAe,GAAG,MAAM,CAAC;AAC9B,KAAK,YAAY,GAAG,MAAM,CAAC;AAC3B,KAAK,UAAU,GAAG,MAAM,CAAC;AACzB,KAAK,YAAY,GAAG,MAAM,CAAC;AAC3B,KAAK,UAAU,GAAG,MAAM,CAAC;AAEzB,MAAM,MAAM,gBAAgB,GACxB,CAAC,eAAe,CAAC,GACjB,CAAC,eAAe,EAAE,YAAY,EAAE,UAAU,EAAE,YAAY,CAAC,GACzD,CAAC,eAAe,EAAE,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,UAAU,CAAC,CAAC;AAE1E,eAAO,MAAM,MAAM,IAAI,CAAC;AACxB,eAAO,MAAM,aAAa,IAAI,CAAC;AAC/B,eAAO,MAAM,WAAW,IAAI,CAAC;AAC7B,eAAO,MAAM,aAAa,IAAI,CAAC;AAC/B,eAAO,MAAM,WAAW,IAAI,CAAC"}
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/sourcemap-segment.d.mts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/sourcemap-segment.d.mts
new file mode 100644
index 00000000..68862952
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/sourcemap-segment.d.mts
@@ -0,0 +1,13 @@
+type GeneratedColumn = number;
+type SourcesIndex = number;
+type SourceLine = number;
+type SourceColumn = number;
+type NamesIndex = number;
+export type SourceMapSegment = [GeneratedColumn] | [GeneratedColumn, SourcesIndex, SourceLine, SourceColumn] | [GeneratedColumn, SourcesIndex, SourceLine, SourceColumn, NamesIndex];
+export declare const COLUMN = 0;
+export declare const SOURCES_INDEX = 1;
+export declare const SOURCE_LINE = 2;
+export declare const SOURCE_COLUMN = 3;
+export declare const NAMES_INDEX = 4;
+export {};
+//# sourceMappingURL=sourcemap-segment.d.ts.map
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/sourcemap-segment.d.mts.map b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/sourcemap-segment.d.mts.map
new file mode 100644
index 00000000..23cdc452
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/sourcemap-segment.d.mts.map
@@ -0,0 +1 @@
+{"version":3,"file":"sourcemap-segment.d.ts","sourceRoot":"","sources":["../src/sourcemap-segment.ts"],"names":[],"mappings":"AAAA,KAAK,eAAe,GAAG,MAAM,CAAC;AAC9B,KAAK,YAAY,GAAG,MAAM,CAAC;AAC3B,KAAK,UAAU,GAAG,MAAM,CAAC;AACzB,KAAK,YAAY,GAAG,MAAM,CAAC;AAC3B,KAAK,UAAU,GAAG,MAAM,CAAC;AAEzB,MAAM,MAAM,gBAAgB,GACxB,CAAC,eAAe,CAAC,GACjB,CAAC,eAAe,EAAE,YAAY,EAAE,UAAU,EAAE,YAAY,CAAC,GACzD,CAAC,eAAe,EAAE,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,UAAU,CAAC,CAAC;AAE1E,eAAO,MAAM,MAAM,IAAI,CAAC;AACxB,eAAO,MAAM,aAAa,IAAI,CAAC;AAC/B,eAAO,MAAM,WAAW,IAAI,CAAC;AAC7B,eAAO,MAAM,aAAa,IAAI,CAAC;AAC/B,eAAO,MAAM,WAAW,IAAI,CAAC"}
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/types.d.cts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/types.d.cts
new file mode 100644
index 00000000..58da00a9
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/types.d.cts
@@ -0,0 +1,44 @@
+import type { SourceMapSegment } from './sourcemap-segment.cts';
+export interface SourceMapV3 {
+ file?: string | null;
+ names: readonly string[];
+ sourceRoot?: string;
+ sources: readonly (string | null)[];
+ sourcesContent?: readonly (string | null)[];
+ version: 3;
+ ignoreList?: readonly number[];
+}
+export interface EncodedSourceMap extends SourceMapV3 {
+ mappings: string;
+}
+export interface DecodedSourceMap extends SourceMapV3 {
+ mappings: readonly SourceMapSegment[][];
+}
+export interface Pos {
+ line: number;
+ column: number;
+}
+export interface OriginalPos extends Pos {
+ source: string;
+}
+export interface BindingExpressionRange {
+ start: Pos;
+ expression: string;
+}
+export type Mapping = {
+ generated: Pos;
+ source: undefined;
+ original: undefined;
+ name: undefined;
+} | {
+ generated: Pos;
+ source: string;
+ original: Pos;
+ name: string;
+} | {
+ generated: Pos;
+ source: string;
+ original: Pos;
+ name: undefined;
+};
+//# sourceMappingURL=types.d.ts.map
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/types.d.cts.map b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/types.d.cts.map
new file mode 100644
index 00000000..159e734d
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/types.d.cts.map
@@ -0,0 +1 @@
+{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAE5D,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC;IACpC,cAAc,CAAC,EAAE,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC;IAC5C,OAAO,EAAE,CAAC,CAAC;IACX,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAChC;AAED,MAAM,WAAW,gBAAiB,SAAQ,WAAW;IACnD,QAAQ,EAAE,MAAM,CAAC;CAGlB;AAED,MAAM,WAAW,gBAAiB,SAAQ,WAAW;IACnD,QAAQ,EAAE,SAAS,gBAAgB,EAAE,EAAE,CAAC;CAGzC;AAED,MAAM,WAAW,GAAG;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,WAAY,SAAQ,GAAG;IACtC,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,GAAG,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;CACpB;AAKD,MAAM,MAAM,OAAO,GACf;IACE,SAAS,EAAE,GAAG,CAAC;IACf,MAAM,EAAE,SAAS,CAAC;IAClB,QAAQ,EAAE,SAAS,CAAC;IACpB,IAAI,EAAE,SAAS,CAAC;CACjB,GACD;IACE,SAAS,EAAE,GAAG,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,GAAG,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;CACd,GACD;IACE,SAAS,EAAE,GAAG,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,GAAG,CAAC;IACd,IAAI,EAAE,SAAS,CAAC;CACjB,CAAC"}
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/types.d.mts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/types.d.mts
new file mode 100644
index 00000000..e9837ebe
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/types.d.mts
@@ -0,0 +1,44 @@
+import type { SourceMapSegment } from './sourcemap-segment.mts';
+export interface SourceMapV3 {
+ file?: string | null;
+ names: readonly string[];
+ sourceRoot?: string;
+ sources: readonly (string | null)[];
+ sourcesContent?: readonly (string | null)[];
+ version: 3;
+ ignoreList?: readonly number[];
+}
+export interface EncodedSourceMap extends SourceMapV3 {
+ mappings: string;
+}
+export interface DecodedSourceMap extends SourceMapV3 {
+ mappings: readonly SourceMapSegment[][];
+}
+export interface Pos {
+ line: number;
+ column: number;
+}
+export interface OriginalPos extends Pos {
+ source: string;
+}
+export interface BindingExpressionRange {
+ start: Pos;
+ expression: string;
+}
+export type Mapping = {
+ generated: Pos;
+ source: undefined;
+ original: undefined;
+ name: undefined;
+} | {
+ generated: Pos;
+ source: string;
+ original: Pos;
+ name: string;
+} | {
+ generated: Pos;
+ source: string;
+ original: Pos;
+ name: undefined;
+};
+//# sourceMappingURL=types.d.ts.map
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/types.d.mts.map b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/types.d.mts.map
new file mode 100644
index 00000000..159e734d
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/gen-mapping/types/types.d.mts.map
@@ -0,0 +1 @@
+{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAE5D,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC;IACpC,cAAc,CAAC,EAAE,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC;IAC5C,OAAO,EAAE,CAAC,CAAC;IACX,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAChC;AAED,MAAM,WAAW,gBAAiB,SAAQ,WAAW;IACnD,QAAQ,EAAE,MAAM,CAAC;CAGlB;AAED,MAAM,WAAW,gBAAiB,SAAQ,WAAW;IACnD,QAAQ,EAAE,SAAS,gBAAgB,EAAE,EAAE,CAAC;CAGzC;AAED,MAAM,WAAW,GAAG;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,WAAY,SAAQ,GAAG;IACtC,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,GAAG,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;CACpB;AAKD,MAAM,MAAM,OAAO,GACf;IACE,SAAS,EAAE,GAAG,CAAC;IACf,MAAM,EAAE,SAAS,CAAC;IAClB,QAAQ,EAAE,SAAS,CAAC;IACpB,IAAI,EAAE,SAAS,CAAC;CACjB,GACD;IACE,SAAS,EAAE,GAAG,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,GAAG,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;CACd,GACD;IACE,SAAS,EAAE,GAAG,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,GAAG,CAAC;IACd,IAAI,EAAE,SAAS,CAAC;CACjB,CAAC"}
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/resolve-uri/LICENSE b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/resolve-uri/LICENSE
new file mode 100644
index 00000000..0a81b2ad
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/resolve-uri/LICENSE
@@ -0,0 +1,19 @@
+Copyright 2019 Justin Ridgewell
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/resolve-uri/README.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/resolve-uri/README.md
new file mode 100644
index 00000000..2fe70df7
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/resolve-uri/README.md
@@ -0,0 +1,40 @@
+# @jridgewell/resolve-uri
+
+> Resolve a URI relative to an optional base URI
+
+Resolve any combination of absolute URIs, protocol-realtive URIs, absolute paths, or relative paths.
+
+## Installation
+
+```sh
+npm install @jridgewell/resolve-uri
+```
+
+## Usage
+
+```typescript
+function resolve(input: string, base?: string): string;
+```
+
+```js
+import resolve from '@jridgewell/resolve-uri';
+
+resolve('foo', 'https://example.com'); // => 'https://example.com/foo'
+```
+
+| Input | Base | Resolution | Explanation |
+|-----------------------|-------------------------|--------------------------------|--------------------------------------------------------------|
+| `https://example.com` | _any_ | `https://example.com/` | Input is normalized only |
+| `//example.com` | `https://base.com/` | `https://example.com/` | Input inherits the base's protocol |
+| `//example.com` | _rest_ | `//example.com/` | Input is normalized only |
+| `/example` | `https://base.com/` | `https://base.com/example` | Input inherits the base's origin |
+| `/example` | `//base.com/` | `//base.com/example` | Input inherits the base's host and remains protocol relative |
+| `/example` | _rest_ | `/example` | Input is normalized only |
+| `example` | `https://base.com/dir/` | `https://base.com/dir/example` | Input is joined with the base |
+| `example` | `https://base.com/file` | `https://base.com/example` | Input is joined with the base without its file |
+| `example` | `//base.com/dir/` | `//base.com/dir/example` | Input is joined with the base's last directory |
+| `example` | `//base.com/file` | `//base.com/example` | Input is joined with the base without its file |
+| `example` | `/base/dir/` | `/base/dir/example` | Input is joined with the base's last directory |
+| `example` | `/base/file` | `/base/example` | Input is joined with the base without its file |
+| `example` | `base/dir/` | `base/dir/example` | Input is joined with the base's last directory |
+| `example` | `base/file` | `base/example` | Input is joined with the base without its file |
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/resolve-uri/package.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/resolve-uri/package.json
new file mode 100644
index 00000000..02a4c518
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/resolve-uri/package.json
@@ -0,0 +1,69 @@
+{
+ "name": "@jridgewell/resolve-uri",
+ "version": "3.1.2",
+ "description": "Resolve a URI relative to an optional base URI",
+ "keywords": [
+ "resolve",
+ "uri",
+ "url",
+ "path"
+ ],
+ "author": "Justin Ridgewell ",
+ "license": "MIT",
+ "repository": "https://github.com/jridgewell/resolve-uri",
+ "main": "dist/resolve-uri.umd.js",
+ "module": "dist/resolve-uri.mjs",
+ "types": "dist/types/resolve-uri.d.ts",
+ "exports": {
+ ".": [
+ {
+ "types": "./dist/types/resolve-uri.d.ts",
+ "browser": "./dist/resolve-uri.umd.js",
+ "require": "./dist/resolve-uri.umd.js",
+ "import": "./dist/resolve-uri.mjs"
+ },
+ "./dist/resolve-uri.umd.js"
+ ],
+ "./package.json": "./package.json"
+ },
+ "files": [
+ "dist"
+ ],
+ "engines": {
+ "node": ">=6.0.0"
+ },
+ "scripts": {
+ "prebuild": "rm -rf dist",
+ "build": "run-s -n build:*",
+ "build:rollup": "rollup -c rollup.config.js",
+ "build:ts": "tsc --project tsconfig.build.json",
+ "lint": "run-s -n lint:*",
+ "lint:prettier": "npm run test:lint:prettier -- --write",
+ "lint:ts": "npm run test:lint:ts -- --fix",
+ "pretest": "run-s build:rollup",
+ "test": "run-s -n test:lint test:only",
+ "test:debug": "mocha --inspect-brk",
+ "test:lint": "run-s -n test:lint:*",
+ "test:lint:prettier": "prettier --check '{src,test}/**/*.ts'",
+ "test:lint:ts": "eslint '{src,test}/**/*.ts'",
+ "test:only": "mocha",
+ "test:coverage": "c8 mocha",
+ "test:watch": "mocha --watch",
+ "prepublishOnly": "npm run preversion",
+ "preversion": "run-s test build"
+ },
+ "devDependencies": {
+ "@jridgewell/resolve-uri-latest": "npm:@jridgewell/resolve-uri@*",
+ "@rollup/plugin-typescript": "8.3.0",
+ "@typescript-eslint/eslint-plugin": "5.10.0",
+ "@typescript-eslint/parser": "5.10.0",
+ "c8": "7.11.0",
+ "eslint": "8.7.0",
+ "eslint-config-prettier": "8.3.0",
+ "mocha": "9.2.0",
+ "npm-run-all": "4.1.5",
+ "prettier": "2.5.1",
+ "rollup": "2.66.0",
+ "typescript": "4.5.5"
+ }
+}
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/LICENSE b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/LICENSE
new file mode 100644
index 00000000..1f6ce94c
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/LICENSE
@@ -0,0 +1,19 @@
+Copyright 2024 Justin Ridgewell
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/README.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/README.md
new file mode 100644
index 00000000..b3e0708b
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/README.md
@@ -0,0 +1,264 @@
+# @jridgewell/sourcemap-codec
+
+Encode/decode the `mappings` property of a [sourcemap](https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit).
+
+
+## Why?
+
+Sourcemaps are difficult to generate and manipulate, because the `mappings` property – the part that actually links the generated code back to the original source – is encoded using an obscure method called [Variable-length quantity](https://en.wikipedia.org/wiki/Variable-length_quantity). On top of that, each segment in the mapping contains offsets rather than absolute indices, which means that you can't look at a segment in isolation – you have to understand the whole sourcemap.
+
+This package makes the process slightly easier.
+
+
+## Installation
+
+```bash
+npm install @jridgewell/sourcemap-codec
+```
+
+
+## Usage
+
+```js
+import { encode, decode } from '@jridgewell/sourcemap-codec';
+
+var decoded = decode( ';EAEEA,EAAE,EAAC,CAAE;ECQY,UACC' );
+
+assert.deepEqual( decoded, [
+ // the first line (of the generated code) has no mappings,
+ // as shown by the starting semi-colon (which separates lines)
+ [],
+
+ // the second line contains four (comma-separated) segments
+ [
+ // segments are encoded as you'd expect:
+ // [ generatedCodeColumn, sourceIndex, sourceCodeLine, sourceCodeColumn, nameIndex ]
+
+ // i.e. the first segment begins at column 2, and maps back to the second column
+ // of the second line (both zero-based) of the 0th source, and uses the 0th
+ // name in the `map.names` array
+ [ 2, 0, 2, 2, 0 ],
+
+ // the remaining segments are 4-length rather than 5-length,
+ // because they don't map a name
+ [ 4, 0, 2, 4 ],
+ [ 6, 0, 2, 5 ],
+ [ 7, 0, 2, 7 ]
+ ],
+
+ // the final line contains two segments
+ [
+ [ 2, 1, 10, 19 ],
+ [ 12, 1, 11, 20 ]
+ ]
+]);
+
+var encoded = encode( decoded );
+assert.equal( encoded, ';EAEEA,EAAE,EAAC,CAAE;ECQY,UACC' );
+```
+
+## Benchmarks
+
+```
+node v20.10.0
+
+amp.js.map - 45120 segments
+
+Decode Memory Usage:
+local code 5815135 bytes
+@jridgewell/sourcemap-codec 1.4.15 5868160 bytes
+sourcemap-codec 5492584 bytes
+source-map-0.6.1 13569984 bytes
+source-map-0.8.0 6390584 bytes
+chrome dev tools 8011136 bytes
+Smallest memory usage is sourcemap-codec
+
+Decode speed:
+decode: local code x 492 ops/sec ±1.22% (90 runs sampled)
+decode: @jridgewell/sourcemap-codec 1.4.15 x 499 ops/sec ±1.16% (89 runs sampled)
+decode: sourcemap-codec x 376 ops/sec ±1.66% (89 runs sampled)
+decode: source-map-0.6.1 x 34.99 ops/sec ±0.94% (48 runs sampled)
+decode: source-map-0.8.0 x 351 ops/sec ±0.07% (95 runs sampled)
+chrome dev tools x 165 ops/sec ±0.91% (86 runs sampled)
+Fastest is decode: @jridgewell/sourcemap-codec 1.4.15
+
+Encode Memory Usage:
+local code 444248 bytes
+@jridgewell/sourcemap-codec 1.4.15 623024 bytes
+sourcemap-codec 8696280 bytes
+source-map-0.6.1 8745176 bytes
+source-map-0.8.0 8736624 bytes
+Smallest memory usage is local code
+
+Encode speed:
+encode: local code x 796 ops/sec ±0.11% (97 runs sampled)
+encode: @jridgewell/sourcemap-codec 1.4.15 x 795 ops/sec ±0.25% (98 runs sampled)
+encode: sourcemap-codec x 231 ops/sec ±0.83% (86 runs sampled)
+encode: source-map-0.6.1 x 166 ops/sec ±0.57% (86 runs sampled)
+encode: source-map-0.8.0 x 203 ops/sec ±0.45% (88 runs sampled)
+Fastest is encode: local code,encode: @jridgewell/sourcemap-codec 1.4.15
+
+
+***
+
+
+babel.min.js.map - 347793 segments
+
+Decode Memory Usage:
+local code 35424960 bytes
+@jridgewell/sourcemap-codec 1.4.15 35424696 bytes
+sourcemap-codec 36033464 bytes
+source-map-0.6.1 62253704 bytes
+source-map-0.8.0 43843920 bytes
+chrome dev tools 45111400 bytes
+Smallest memory usage is @jridgewell/sourcemap-codec 1.4.15
+
+Decode speed:
+decode: local code x 38.18 ops/sec ±5.44% (52 runs sampled)
+decode: @jridgewell/sourcemap-codec 1.4.15 x 38.36 ops/sec ±5.02% (52 runs sampled)
+decode: sourcemap-codec x 34.05 ops/sec ±4.45% (47 runs sampled)
+decode: source-map-0.6.1 x 4.31 ops/sec ±2.76% (15 runs sampled)
+decode: source-map-0.8.0 x 55.60 ops/sec ±0.13% (73 runs sampled)
+chrome dev tools x 16.94 ops/sec ±3.78% (46 runs sampled)
+Fastest is decode: source-map-0.8.0
+
+Encode Memory Usage:
+local code 2606016 bytes
+@jridgewell/sourcemap-codec 1.4.15 2626440 bytes
+sourcemap-codec 21152576 bytes
+source-map-0.6.1 25023928 bytes
+source-map-0.8.0 25256448 bytes
+Smallest memory usage is local code
+
+Encode speed:
+encode: local code x 127 ops/sec ±0.18% (83 runs sampled)
+encode: @jridgewell/sourcemap-codec 1.4.15 x 128 ops/sec ±0.26% (83 runs sampled)
+encode: sourcemap-codec x 29.31 ops/sec ±2.55% (53 runs sampled)
+encode: source-map-0.6.1 x 18.85 ops/sec ±3.19% (36 runs sampled)
+encode: source-map-0.8.0 x 19.34 ops/sec ±1.97% (36 runs sampled)
+Fastest is encode: @jridgewell/sourcemap-codec 1.4.15
+
+
+***
+
+
+preact.js.map - 1992 segments
+
+Decode Memory Usage:
+local code 261696 bytes
+@jridgewell/sourcemap-codec 1.4.15 244296 bytes
+sourcemap-codec 302816 bytes
+source-map-0.6.1 939176 bytes
+source-map-0.8.0 336 bytes
+chrome dev tools 587368 bytes
+Smallest memory usage is source-map-0.8.0
+
+Decode speed:
+decode: local code x 17,782 ops/sec ±0.32% (97 runs sampled)
+decode: @jridgewell/sourcemap-codec 1.4.15 x 17,863 ops/sec ±0.40% (100 runs sampled)
+decode: sourcemap-codec x 12,453 ops/sec ±0.27% (101 runs sampled)
+decode: source-map-0.6.1 x 1,288 ops/sec ±1.05% (96 runs sampled)
+decode: source-map-0.8.0 x 9,289 ops/sec ±0.27% (101 runs sampled)
+chrome dev tools x 4,769 ops/sec ±0.18% (100 runs sampled)
+Fastest is decode: @jridgewell/sourcemap-codec 1.4.15
+
+Encode Memory Usage:
+local code 262944 bytes
+@jridgewell/sourcemap-codec 1.4.15 25544 bytes
+sourcemap-codec 323048 bytes
+source-map-0.6.1 507808 bytes
+source-map-0.8.0 507480 bytes
+Smallest memory usage is @jridgewell/sourcemap-codec 1.4.15
+
+Encode speed:
+encode: local code x 24,207 ops/sec ±0.79% (95 runs sampled)
+encode: @jridgewell/sourcemap-codec 1.4.15 x 24,288 ops/sec ±0.48% (96 runs sampled)
+encode: sourcemap-codec x 6,761 ops/sec ±0.21% (100 runs sampled)
+encode: source-map-0.6.1 x 5,374 ops/sec ±0.17% (99 runs sampled)
+encode: source-map-0.8.0 x 5,633 ops/sec ±0.32% (99 runs sampled)
+Fastest is encode: @jridgewell/sourcemap-codec 1.4.15,encode: local code
+
+
+***
+
+
+react.js.map - 5726 segments
+
+Decode Memory Usage:
+local code 678816 bytes
+@jridgewell/sourcemap-codec 1.4.15 678816 bytes
+sourcemap-codec 816400 bytes
+source-map-0.6.1 2288864 bytes
+source-map-0.8.0 721360 bytes
+chrome dev tools 1012512 bytes
+Smallest memory usage is local code
+
+Decode speed:
+decode: local code x 6,178 ops/sec ±0.19% (98 runs sampled)
+decode: @jridgewell/sourcemap-codec 1.4.15 x 6,261 ops/sec ±0.22% (100 runs sampled)
+decode: sourcemap-codec x 4,472 ops/sec ±0.90% (99 runs sampled)
+decode: source-map-0.6.1 x 449 ops/sec ±0.31% (95 runs sampled)
+decode: source-map-0.8.0 x 3,219 ops/sec ±0.13% (100 runs sampled)
+chrome dev tools x 1,743 ops/sec ±0.20% (99 runs sampled)
+Fastest is decode: @jridgewell/sourcemap-codec 1.4.15
+
+Encode Memory Usage:
+local code 140960 bytes
+@jridgewell/sourcemap-codec 1.4.15 159808 bytes
+sourcemap-codec 969304 bytes
+source-map-0.6.1 930520 bytes
+source-map-0.8.0 930248 bytes
+Smallest memory usage is local code
+
+Encode speed:
+encode: local code x 8,013 ops/sec ±0.19% (100 runs sampled)
+encode: @jridgewell/sourcemap-codec 1.4.15 x 7,989 ops/sec ±0.20% (101 runs sampled)
+encode: sourcemap-codec x 2,472 ops/sec ±0.21% (99 runs sampled)
+encode: source-map-0.6.1 x 2,200 ops/sec ±0.17% (99 runs sampled)
+encode: source-map-0.8.0 x 2,220 ops/sec ±0.37% (99 runs sampled)
+Fastest is encode: local code
+
+
+***
+
+
+vscode.map - 2141001 segments
+
+Decode Memory Usage:
+local code 198955264 bytes
+@jridgewell/sourcemap-codec 1.4.15 199175352 bytes
+sourcemap-codec 199102688 bytes
+source-map-0.6.1 386323432 bytes
+source-map-0.8.0 244116432 bytes
+chrome dev tools 293734280 bytes
+Smallest memory usage is local code
+
+Decode speed:
+decode: local code x 3.90 ops/sec ±22.21% (15 runs sampled)
+decode: @jridgewell/sourcemap-codec 1.4.15 x 3.95 ops/sec ±23.53% (15 runs sampled)
+decode: sourcemap-codec x 3.82 ops/sec ±17.94% (14 runs sampled)
+decode: source-map-0.6.1 x 0.61 ops/sec ±7.81% (6 runs sampled)
+decode: source-map-0.8.0 x 9.54 ops/sec ±0.28% (28 runs sampled)
+chrome dev tools x 2.18 ops/sec ±10.58% (10 runs sampled)
+Fastest is decode: source-map-0.8.0
+
+Encode Memory Usage:
+local code 13509880 bytes
+@jridgewell/sourcemap-codec 1.4.15 13537648 bytes
+sourcemap-codec 32540104 bytes
+source-map-0.6.1 127531040 bytes
+source-map-0.8.0 127535312 bytes
+Smallest memory usage is local code
+
+Encode speed:
+encode: local code x 20.10 ops/sec ±0.19% (38 runs sampled)
+encode: @jridgewell/sourcemap-codec 1.4.15 x 20.26 ops/sec ±0.32% (38 runs sampled)
+encode: sourcemap-codec x 5.44 ops/sec ±1.64% (18 runs sampled)
+encode: source-map-0.6.1 x 2.30 ops/sec ±4.79% (10 runs sampled)
+encode: source-map-0.8.0 x 2.46 ops/sec ±6.53% (10 runs sampled)
+Fastest is encode: @jridgewell/sourcemap-codec 1.4.15
+```
+
+# License
+
+MIT
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/package.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/package.json
new file mode 100644
index 00000000..da551376
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/package.json
@@ -0,0 +1,63 @@
+{
+ "name": "@jridgewell/sourcemap-codec",
+ "version": "1.5.5",
+ "description": "Encode/decode sourcemap mappings",
+ "keywords": [
+ "sourcemap",
+ "vlq"
+ ],
+ "main": "dist/sourcemap-codec.umd.js",
+ "module": "dist/sourcemap-codec.mjs",
+ "types": "types/sourcemap-codec.d.cts",
+ "files": [
+ "dist",
+ "src",
+ "types"
+ ],
+ "exports": {
+ ".": [
+ {
+ "import": {
+ "types": "./types/sourcemap-codec.d.mts",
+ "default": "./dist/sourcemap-codec.mjs"
+ },
+ "default": {
+ "types": "./types/sourcemap-codec.d.cts",
+ "default": "./dist/sourcemap-codec.umd.js"
+ }
+ },
+ "./dist/sourcemap-codec.umd.js"
+ ],
+ "./package.json": "./package.json"
+ },
+ "scripts": {
+ "benchmark": "run-s build:code benchmark:*",
+ "benchmark:install": "cd benchmark && npm install",
+ "benchmark:only": "node --expose-gc benchmark/index.js",
+ "build": "run-s -n build:code build:types",
+ "build:code": "node ../../esbuild.mjs sourcemap-codec.ts",
+ "build:types": "run-s build:types:force build:types:emit build:types:mts",
+ "build:types:force": "rimraf tsconfig.build.tsbuildinfo",
+ "build:types:emit": "tsc --project tsconfig.build.json",
+ "build:types:mts": "node ../../mts-types.mjs",
+ "clean": "run-s -n clean:code clean:types",
+ "clean:code": "tsc --build --clean tsconfig.build.json",
+ "clean:types": "rimraf dist types",
+ "test": "run-s -n test:types test:only test:format",
+ "test:format": "prettier --check '{src,test}/**/*.ts'",
+ "test:only": "mocha",
+ "test:types": "eslint '{src,test}/**/*.ts'",
+ "lint": "run-s -n lint:types lint:format",
+ "lint:format": "npm run test:format -- --write",
+ "lint:types": "npm run test:types -- --fix",
+ "prepublishOnly": "npm run-s -n build test"
+ },
+ "homepage": "https://github.com/jridgewell/sourcemaps/tree/main/packages/sourcemap-codec",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/jridgewell/sourcemaps.git",
+ "directory": "packages/sourcemap-codec"
+ },
+ "author": "Justin Ridgewell ",
+ "license": "MIT"
+}
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/src/scopes.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/src/scopes.ts
new file mode 100644
index 00000000..d194c2f0
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/src/scopes.ts
@@ -0,0 +1,345 @@
+import { StringReader, StringWriter } from './strings';
+import { comma, decodeInteger, encodeInteger, hasMoreVlq, semicolon } from './vlq';
+
+const EMPTY: any[] = [];
+
+type Line = number;
+type Column = number;
+type Kind = number;
+type Name = number;
+type Var = number;
+type SourcesIndex = number;
+type ScopesIndex = number;
+
+type Mix = (A & O) | (B & O);
+
+export type OriginalScope = Mix<
+ [Line, Column, Line, Column, Kind],
+ [Line, Column, Line, Column, Kind, Name],
+ { vars: Var[] }
+>;
+
+export type GeneratedRange = Mix<
+ [Line, Column, Line, Column],
+ [Line, Column, Line, Column, SourcesIndex, ScopesIndex],
+ {
+ callsite: CallSite | null;
+ bindings: Binding[];
+ isScope: boolean;
+ }
+>;
+export type CallSite = [SourcesIndex, Line, Column];
+type Binding = BindingExpressionRange[];
+export type BindingExpressionRange = [Name] | [Name, Line, Column];
+
+export function decodeOriginalScopes(input: string): OriginalScope[] {
+ const { length } = input;
+ const reader = new StringReader(input);
+ const scopes: OriginalScope[] = [];
+ const stack: OriginalScope[] = [];
+ let line = 0;
+
+ for (; reader.pos < length; reader.pos++) {
+ line = decodeInteger(reader, line);
+ const column = decodeInteger(reader, 0);
+
+ if (!hasMoreVlq(reader, length)) {
+ const last = stack.pop()!;
+ last[2] = line;
+ last[3] = column;
+ continue;
+ }
+
+ const kind = decodeInteger(reader, 0);
+ const fields = decodeInteger(reader, 0);
+ const hasName = fields & 0b0001;
+
+ const scope: OriginalScope = (
+ hasName ? [line, column, 0, 0, kind, decodeInteger(reader, 0)] : [line, column, 0, 0, kind]
+ ) as OriginalScope;
+
+ let vars: Var[] = EMPTY;
+ if (hasMoreVlq(reader, length)) {
+ vars = [];
+ do {
+ const varsIndex = decodeInteger(reader, 0);
+ vars.push(varsIndex);
+ } while (hasMoreVlq(reader, length));
+ }
+ scope.vars = vars;
+
+ scopes.push(scope);
+ stack.push(scope);
+ }
+
+ return scopes;
+}
+
+export function encodeOriginalScopes(scopes: OriginalScope[]): string {
+ const writer = new StringWriter();
+
+ for (let i = 0; i < scopes.length; ) {
+ i = _encodeOriginalScopes(scopes, i, writer, [0]);
+ }
+
+ return writer.flush();
+}
+
+function _encodeOriginalScopes(
+ scopes: OriginalScope[],
+ index: number,
+ writer: StringWriter,
+ state: [
+ number, // GenColumn
+ ],
+): number {
+ const scope = scopes[index];
+ const { 0: startLine, 1: startColumn, 2: endLine, 3: endColumn, 4: kind, vars } = scope;
+
+ if (index > 0) writer.write(comma);
+
+ state[0] = encodeInteger(writer, startLine, state[0]);
+ encodeInteger(writer, startColumn, 0);
+ encodeInteger(writer, kind, 0);
+
+ const fields = scope.length === 6 ? 0b0001 : 0;
+ encodeInteger(writer, fields, 0);
+ if (scope.length === 6) encodeInteger(writer, scope[5], 0);
+
+ for (const v of vars) {
+ encodeInteger(writer, v, 0);
+ }
+
+ for (index++; index < scopes.length; ) {
+ const next = scopes[index];
+ const { 0: l, 1: c } = next;
+ if (l > endLine || (l === endLine && c >= endColumn)) {
+ break;
+ }
+ index = _encodeOriginalScopes(scopes, index, writer, state);
+ }
+
+ writer.write(comma);
+ state[0] = encodeInteger(writer, endLine, state[0]);
+ encodeInteger(writer, endColumn, 0);
+
+ return index;
+}
+
+export function decodeGeneratedRanges(input: string): GeneratedRange[] {
+ const { length } = input;
+ const reader = new StringReader(input);
+ const ranges: GeneratedRange[] = [];
+ const stack: GeneratedRange[] = [];
+
+ let genLine = 0;
+ let definitionSourcesIndex = 0;
+ let definitionScopeIndex = 0;
+ let callsiteSourcesIndex = 0;
+ let callsiteLine = 0;
+ let callsiteColumn = 0;
+ let bindingLine = 0;
+ let bindingColumn = 0;
+
+ do {
+ const semi = reader.indexOf(';');
+ let genColumn = 0;
+
+ for (; reader.pos < semi; reader.pos++) {
+ genColumn = decodeInteger(reader, genColumn);
+
+ if (!hasMoreVlq(reader, semi)) {
+ const last = stack.pop()!;
+ last[2] = genLine;
+ last[3] = genColumn;
+ continue;
+ }
+
+ const fields = decodeInteger(reader, 0);
+ const hasDefinition = fields & 0b0001;
+ const hasCallsite = fields & 0b0010;
+ const hasScope = fields & 0b0100;
+
+ let callsite: CallSite | null = null;
+ let bindings: Binding[] = EMPTY;
+ let range: GeneratedRange;
+ if (hasDefinition) {
+ const defSourcesIndex = decodeInteger(reader, definitionSourcesIndex);
+ definitionScopeIndex = decodeInteger(
+ reader,
+ definitionSourcesIndex === defSourcesIndex ? definitionScopeIndex : 0,
+ );
+
+ definitionSourcesIndex = defSourcesIndex;
+ range = [genLine, genColumn, 0, 0, defSourcesIndex, definitionScopeIndex] as GeneratedRange;
+ } else {
+ range = [genLine, genColumn, 0, 0] as GeneratedRange;
+ }
+
+ range.isScope = !!hasScope;
+
+ if (hasCallsite) {
+ const prevCsi = callsiteSourcesIndex;
+ const prevLine = callsiteLine;
+ callsiteSourcesIndex = decodeInteger(reader, callsiteSourcesIndex);
+ const sameSource = prevCsi === callsiteSourcesIndex;
+ callsiteLine = decodeInteger(reader, sameSource ? callsiteLine : 0);
+ callsiteColumn = decodeInteger(
+ reader,
+ sameSource && prevLine === callsiteLine ? callsiteColumn : 0,
+ );
+
+ callsite = [callsiteSourcesIndex, callsiteLine, callsiteColumn];
+ }
+ range.callsite = callsite;
+
+ if (hasMoreVlq(reader, semi)) {
+ bindings = [];
+ do {
+ bindingLine = genLine;
+ bindingColumn = genColumn;
+ const expressionsCount = decodeInteger(reader, 0);
+ let expressionRanges: BindingExpressionRange[];
+ if (expressionsCount < -1) {
+ expressionRanges = [[decodeInteger(reader, 0)]];
+ for (let i = -1; i > expressionsCount; i--) {
+ const prevBl = bindingLine;
+ bindingLine = decodeInteger(reader, bindingLine);
+ bindingColumn = decodeInteger(reader, bindingLine === prevBl ? bindingColumn : 0);
+ const expression = decodeInteger(reader, 0);
+ expressionRanges.push([expression, bindingLine, bindingColumn]);
+ }
+ } else {
+ expressionRanges = [[expressionsCount]];
+ }
+ bindings.push(expressionRanges);
+ } while (hasMoreVlq(reader, semi));
+ }
+ range.bindings = bindings;
+
+ ranges.push(range);
+ stack.push(range);
+ }
+
+ genLine++;
+ reader.pos = semi + 1;
+ } while (reader.pos < length);
+
+ return ranges;
+}
+
+export function encodeGeneratedRanges(ranges: GeneratedRange[]): string {
+ if (ranges.length === 0) return '';
+
+ const writer = new StringWriter();
+
+ for (let i = 0; i < ranges.length; ) {
+ i = _encodeGeneratedRanges(ranges, i, writer, [0, 0, 0, 0, 0, 0, 0]);
+ }
+
+ return writer.flush();
+}
+
+function _encodeGeneratedRanges(
+ ranges: GeneratedRange[],
+ index: number,
+ writer: StringWriter,
+ state: [
+ number, // GenLine
+ number, // GenColumn
+ number, // DefSourcesIndex
+ number, // DefScopesIndex
+ number, // CallSourcesIndex
+ number, // CallLine
+ number, // CallColumn
+ ],
+): number {
+ const range = ranges[index];
+ const {
+ 0: startLine,
+ 1: startColumn,
+ 2: endLine,
+ 3: endColumn,
+ isScope,
+ callsite,
+ bindings,
+ } = range;
+
+ if (state[0] < startLine) {
+ catchupLine(writer, state[0], startLine);
+ state[0] = startLine;
+ state[1] = 0;
+ } else if (index > 0) {
+ writer.write(comma);
+ }
+
+ state[1] = encodeInteger(writer, range[1], state[1]);
+
+ const fields =
+ (range.length === 6 ? 0b0001 : 0) | (callsite ? 0b0010 : 0) | (isScope ? 0b0100 : 0);
+ encodeInteger(writer, fields, 0);
+
+ if (range.length === 6) {
+ const { 4: sourcesIndex, 5: scopesIndex } = range;
+ if (sourcesIndex !== state[2]) {
+ state[3] = 0;
+ }
+ state[2] = encodeInteger(writer, sourcesIndex, state[2]);
+ state[3] = encodeInteger(writer, scopesIndex, state[3]);
+ }
+
+ if (callsite) {
+ const { 0: sourcesIndex, 1: callLine, 2: callColumn } = range.callsite!;
+ if (sourcesIndex !== state[4]) {
+ state[5] = 0;
+ state[6] = 0;
+ } else if (callLine !== state[5]) {
+ state[6] = 0;
+ }
+ state[4] = encodeInteger(writer, sourcesIndex, state[4]);
+ state[5] = encodeInteger(writer, callLine, state[5]);
+ state[6] = encodeInteger(writer, callColumn, state[6]);
+ }
+
+ if (bindings) {
+ for (const binding of bindings) {
+ if (binding.length > 1) encodeInteger(writer, -binding.length, 0);
+ const expression = binding[0][0];
+ encodeInteger(writer, expression, 0);
+ let bindingStartLine = startLine;
+ let bindingStartColumn = startColumn;
+ for (let i = 1; i < binding.length; i++) {
+ const expRange = binding[i];
+ bindingStartLine = encodeInteger(writer, expRange[1]!, bindingStartLine);
+ bindingStartColumn = encodeInteger(writer, expRange[2]!, bindingStartColumn);
+ encodeInteger(writer, expRange[0]!, 0);
+ }
+ }
+ }
+
+ for (index++; index < ranges.length; ) {
+ const next = ranges[index];
+ const { 0: l, 1: c } = next;
+ if (l > endLine || (l === endLine && c >= endColumn)) {
+ break;
+ }
+ index = _encodeGeneratedRanges(ranges, index, writer, state);
+ }
+
+ if (state[0] < endLine) {
+ catchupLine(writer, state[0], endLine);
+ state[0] = endLine;
+ state[1] = 0;
+ } else {
+ writer.write(comma);
+ }
+ state[1] = encodeInteger(writer, endColumn, state[1]);
+
+ return index;
+}
+
+function catchupLine(writer: StringWriter, lastLine: number, line: number) {
+ do {
+ writer.write(semicolon);
+ } while (++lastLine < line);
+}
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/src/sourcemap-codec.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/src/sourcemap-codec.ts
new file mode 100644
index 00000000..a81f894d
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/src/sourcemap-codec.ts
@@ -0,0 +1,111 @@
+import { comma, decodeInteger, encodeInteger, hasMoreVlq, semicolon } from './vlq';
+import { StringWriter, StringReader } from './strings';
+
+export {
+ decodeOriginalScopes,
+ encodeOriginalScopes,
+ decodeGeneratedRanges,
+ encodeGeneratedRanges,
+} from './scopes';
+export type { OriginalScope, GeneratedRange, CallSite, BindingExpressionRange } from './scopes';
+
+export type SourceMapSegment =
+ | [number]
+ | [number, number, number, number]
+ | [number, number, number, number, number];
+export type SourceMapLine = SourceMapSegment[];
+export type SourceMapMappings = SourceMapLine[];
+
+export function decode(mappings: string): SourceMapMappings {
+ const { length } = mappings;
+ const reader = new StringReader(mappings);
+ const decoded: SourceMapMappings = [];
+ let genColumn = 0;
+ let sourcesIndex = 0;
+ let sourceLine = 0;
+ let sourceColumn = 0;
+ let namesIndex = 0;
+
+ do {
+ const semi = reader.indexOf(';');
+ const line: SourceMapLine = [];
+ let sorted = true;
+ let lastCol = 0;
+ genColumn = 0;
+
+ while (reader.pos < semi) {
+ let seg: SourceMapSegment;
+
+ genColumn = decodeInteger(reader, genColumn);
+ if (genColumn < lastCol) sorted = false;
+ lastCol = genColumn;
+
+ if (hasMoreVlq(reader, semi)) {
+ sourcesIndex = decodeInteger(reader, sourcesIndex);
+ sourceLine = decodeInteger(reader, sourceLine);
+ sourceColumn = decodeInteger(reader, sourceColumn);
+
+ if (hasMoreVlq(reader, semi)) {
+ namesIndex = decodeInteger(reader, namesIndex);
+ seg = [genColumn, sourcesIndex, sourceLine, sourceColumn, namesIndex];
+ } else {
+ seg = [genColumn, sourcesIndex, sourceLine, sourceColumn];
+ }
+ } else {
+ seg = [genColumn];
+ }
+
+ line.push(seg);
+ reader.pos++;
+ }
+
+ if (!sorted) sort(line);
+ decoded.push(line);
+ reader.pos = semi + 1;
+ } while (reader.pos <= length);
+
+ return decoded;
+}
+
+function sort(line: SourceMapSegment[]) {
+ line.sort(sortComparator);
+}
+
+function sortComparator(a: SourceMapSegment, b: SourceMapSegment): number {
+ return a[0] - b[0];
+}
+
+export function encode(decoded: SourceMapMappings): string;
+export function encode(decoded: Readonly): string;
+export function encode(decoded: Readonly): string {
+ const writer = new StringWriter();
+ let sourcesIndex = 0;
+ let sourceLine = 0;
+ let sourceColumn = 0;
+ let namesIndex = 0;
+
+ for (let i = 0; i < decoded.length; i++) {
+ const line = decoded[i];
+ if (i > 0) writer.write(semicolon);
+ if (line.length === 0) continue;
+
+ let genColumn = 0;
+
+ for (let j = 0; j < line.length; j++) {
+ const segment = line[j];
+ if (j > 0) writer.write(comma);
+
+ genColumn = encodeInteger(writer, segment[0], genColumn);
+
+ if (segment.length === 1) continue;
+ sourcesIndex = encodeInteger(writer, segment[1], sourcesIndex);
+ sourceLine = encodeInteger(writer, segment[2], sourceLine);
+ sourceColumn = encodeInteger(writer, segment[3], sourceColumn);
+
+ if (segment.length === 4) continue;
+ namesIndex = encodeInteger(writer, segment[4], namesIndex);
+ }
+ }
+
+ return writer.flush();
+}
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/src/strings.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/src/strings.ts
new file mode 100644
index 00000000..d1619650
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/src/strings.ts
@@ -0,0 +1,65 @@
+const bufLength = 1024 * 16;
+
+// Provide a fallback for older environments.
+const td =
+ typeof TextDecoder !== 'undefined'
+ ? /* #__PURE__ */ new TextDecoder()
+ : typeof Buffer !== 'undefined'
+ ? {
+ decode(buf: Uint8Array): string {
+ const out = Buffer.from(buf.buffer, buf.byteOffset, buf.byteLength);
+ return out.toString();
+ },
+ }
+ : {
+ decode(buf: Uint8Array): string {
+ let out = '';
+ for (let i = 0; i < buf.length; i++) {
+ out += String.fromCharCode(buf[i]);
+ }
+ return out;
+ },
+ };
+
+export class StringWriter {
+ pos = 0;
+ private out = '';
+ private buffer = new Uint8Array(bufLength);
+
+ write(v: number): void {
+ const { buffer } = this;
+ buffer[this.pos++] = v;
+ if (this.pos === bufLength) {
+ this.out += td.decode(buffer);
+ this.pos = 0;
+ }
+ }
+
+ flush(): string {
+ const { buffer, out, pos } = this;
+ return pos > 0 ? out + td.decode(buffer.subarray(0, pos)) : out;
+ }
+}
+
+export class StringReader {
+ pos = 0;
+ declare private buffer: string;
+
+ constructor(buffer: string) {
+ this.buffer = buffer;
+ }
+
+ next(): number {
+ return this.buffer.charCodeAt(this.pos++);
+ }
+
+ peek(): number {
+ return this.buffer.charCodeAt(this.pos);
+ }
+
+ indexOf(char: string): number {
+ const { buffer, pos } = this;
+ const idx = buffer.indexOf(char, pos);
+ return idx === -1 ? buffer.length : idx;
+ }
+}
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/src/vlq.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/src/vlq.ts
new file mode 100644
index 00000000..a42c6815
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/src/vlq.ts
@@ -0,0 +1,55 @@
+import type { StringReader, StringWriter } from './strings';
+
+export const comma = ','.charCodeAt(0);
+export const semicolon = ';'.charCodeAt(0);
+
+const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
+const intToChar = new Uint8Array(64); // 64 possible chars.
+const charToInt = new Uint8Array(128); // z is 122 in ASCII
+
+for (let i = 0; i < chars.length; i++) {
+ const c = chars.charCodeAt(i);
+ intToChar[i] = c;
+ charToInt[c] = i;
+}
+
+export function decodeInteger(reader: StringReader, relative: number): number {
+ let value = 0;
+ let shift = 0;
+ let integer = 0;
+
+ do {
+ const c = reader.next();
+ integer = charToInt[c];
+ value |= (integer & 31) << shift;
+ shift += 5;
+ } while (integer & 32);
+
+ const shouldNegate = value & 1;
+ value >>>= 1;
+
+ if (shouldNegate) {
+ value = -0x80000000 | -value;
+ }
+
+ return relative + value;
+}
+
+export function encodeInteger(builder: StringWriter, num: number, relative: number): number {
+ let delta = num - relative;
+
+ delta = delta < 0 ? (-delta << 1) | 1 : delta << 1;
+ do {
+ let clamped = delta & 0b011111;
+ delta >>>= 5;
+ if (delta > 0) clamped |= 0b100000;
+ builder.write(intToChar[clamped]);
+ } while (delta > 0);
+
+ return num;
+}
+
+export function hasMoreVlq(reader: StringReader, max: number) {
+ if (reader.pos >= max) return false;
+ return reader.peek() !== comma;
+}
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/scopes.d.cts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/scopes.d.cts
new file mode 100644
index 00000000..c583c756
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/scopes.d.cts
@@ -0,0 +1,50 @@
+type Line = number;
+type Column = number;
+type Kind = number;
+type Name = number;
+type Var = number;
+type SourcesIndex = number;
+type ScopesIndex = number;
+type Mix = (A & O) | (B & O);
+export type OriginalScope = Mix<[
+ Line,
+ Column,
+ Line,
+ Column,
+ Kind
+], [
+ Line,
+ Column,
+ Line,
+ Column,
+ Kind,
+ Name
+], {
+ vars: Var[];
+}>;
+export type GeneratedRange = Mix<[
+ Line,
+ Column,
+ Line,
+ Column
+], [
+ Line,
+ Column,
+ Line,
+ Column,
+ SourcesIndex,
+ ScopesIndex
+], {
+ callsite: CallSite | null;
+ bindings: Binding[];
+ isScope: boolean;
+}>;
+export type CallSite = [SourcesIndex, Line, Column];
+type Binding = BindingExpressionRange[];
+export type BindingExpressionRange = [Name] | [Name, Line, Column];
+export declare function decodeOriginalScopes(input: string): OriginalScope[];
+export declare function encodeOriginalScopes(scopes: OriginalScope[]): string;
+export declare function decodeGeneratedRanges(input: string): GeneratedRange[];
+export declare function encodeGeneratedRanges(ranges: GeneratedRange[]): string;
+export {};
+//# sourceMappingURL=scopes.d.ts.map
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/scopes.d.cts.map b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/scopes.d.cts.map
new file mode 100644
index 00000000..630e6477
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/scopes.d.cts.map
@@ -0,0 +1 @@
+{"version":3,"file":"scopes.d.ts","sourceRoot":"","sources":["../src/scopes.ts"],"names":[],"mappings":"AAKA,KAAK,IAAI,GAAG,MAAM,CAAC;AACnB,KAAK,MAAM,GAAG,MAAM,CAAC;AACrB,KAAK,IAAI,GAAG,MAAM,CAAC;AACnB,KAAK,IAAI,GAAG,MAAM,CAAC;AACnB,KAAK,GAAG,GAAG,MAAM,CAAC;AAClB,KAAK,YAAY,GAAG,MAAM,CAAC;AAC3B,KAAK,WAAW,GAAG,MAAM,CAAC;AAE1B,KAAK,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;AAEtC,MAAM,MAAM,aAAa,GAAG,GAAG,CAC7B;IAAC,IAAI;IAAE,MAAM;IAAE,IAAI;IAAE,MAAM;IAAE,IAAI;CAAC,EAClC;IAAC,IAAI;IAAE,MAAM;IAAE,IAAI;IAAE,MAAM;IAAE,IAAI;IAAE,IAAI;CAAC,EACxC;IAAE,IAAI,EAAE,GAAG,EAAE,CAAA;CAAE,CAChB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG,GAAG,CAC9B;IAAC,IAAI;IAAE,MAAM;IAAE,IAAI;IAAE,MAAM;CAAC,EAC5B;IAAC,IAAI;IAAE,MAAM;IAAE,IAAI;IAAE,MAAM;IAAE,YAAY;IAAE,WAAW;CAAC,EACvD;IACE,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAAC;IAC1B,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,OAAO,EAAE,OAAO,CAAC;CAClB,CACF,CAAC;AACF,MAAM,MAAM,QAAQ,GAAG,CAAC,YAAY,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;AACpD,KAAK,OAAO,GAAG,sBAAsB,EAAE,CAAC;AACxC,MAAM,MAAM,sBAAsB,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;AAEnE,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,aAAa,EAAE,CAyCnE;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,aAAa,EAAE,GAAG,MAAM,CAQpE;AA2CD,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,cAAc,EAAE,CAoGrE;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,cAAc,EAAE,GAAG,MAAM,CAUtE"}
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/scopes.d.mts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/scopes.d.mts
new file mode 100644
index 00000000..c583c756
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/scopes.d.mts
@@ -0,0 +1,50 @@
+type Line = number;
+type Column = number;
+type Kind = number;
+type Name = number;
+type Var = number;
+type SourcesIndex = number;
+type ScopesIndex = number;
+type Mix = (A & O) | (B & O);
+export type OriginalScope = Mix<[
+ Line,
+ Column,
+ Line,
+ Column,
+ Kind
+], [
+ Line,
+ Column,
+ Line,
+ Column,
+ Kind,
+ Name
+], {
+ vars: Var[];
+}>;
+export type GeneratedRange = Mix<[
+ Line,
+ Column,
+ Line,
+ Column
+], [
+ Line,
+ Column,
+ Line,
+ Column,
+ SourcesIndex,
+ ScopesIndex
+], {
+ callsite: CallSite | null;
+ bindings: Binding[];
+ isScope: boolean;
+}>;
+export type CallSite = [SourcesIndex, Line, Column];
+type Binding = BindingExpressionRange[];
+export type BindingExpressionRange = [Name] | [Name, Line, Column];
+export declare function decodeOriginalScopes(input: string): OriginalScope[];
+export declare function encodeOriginalScopes(scopes: OriginalScope[]): string;
+export declare function decodeGeneratedRanges(input: string): GeneratedRange[];
+export declare function encodeGeneratedRanges(ranges: GeneratedRange[]): string;
+export {};
+//# sourceMappingURL=scopes.d.ts.map
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/scopes.d.mts.map b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/scopes.d.mts.map
new file mode 100644
index 00000000..630e6477
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/scopes.d.mts.map
@@ -0,0 +1 @@
+{"version":3,"file":"scopes.d.ts","sourceRoot":"","sources":["../src/scopes.ts"],"names":[],"mappings":"AAKA,KAAK,IAAI,GAAG,MAAM,CAAC;AACnB,KAAK,MAAM,GAAG,MAAM,CAAC;AACrB,KAAK,IAAI,GAAG,MAAM,CAAC;AACnB,KAAK,IAAI,GAAG,MAAM,CAAC;AACnB,KAAK,GAAG,GAAG,MAAM,CAAC;AAClB,KAAK,YAAY,GAAG,MAAM,CAAC;AAC3B,KAAK,WAAW,GAAG,MAAM,CAAC;AAE1B,KAAK,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;AAEtC,MAAM,MAAM,aAAa,GAAG,GAAG,CAC7B;IAAC,IAAI;IAAE,MAAM;IAAE,IAAI;IAAE,MAAM;IAAE,IAAI;CAAC,EAClC;IAAC,IAAI;IAAE,MAAM;IAAE,IAAI;IAAE,MAAM;IAAE,IAAI;IAAE,IAAI;CAAC,EACxC;IAAE,IAAI,EAAE,GAAG,EAAE,CAAA;CAAE,CAChB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG,GAAG,CAC9B;IAAC,IAAI;IAAE,MAAM;IAAE,IAAI;IAAE,MAAM;CAAC,EAC5B;IAAC,IAAI;IAAE,MAAM;IAAE,IAAI;IAAE,MAAM;IAAE,YAAY;IAAE,WAAW;CAAC,EACvD;IACE,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAAC;IAC1B,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,OAAO,EAAE,OAAO,CAAC;CAClB,CACF,CAAC;AACF,MAAM,MAAM,QAAQ,GAAG,CAAC,YAAY,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;AACpD,KAAK,OAAO,GAAG,sBAAsB,EAAE,CAAC;AACxC,MAAM,MAAM,sBAAsB,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;AAEnE,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,aAAa,EAAE,CAyCnE;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,aAAa,EAAE,GAAG,MAAM,CAQpE;AA2CD,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,cAAc,EAAE,CAoGrE;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,cAAc,EAAE,GAAG,MAAM,CAUtE"}
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/sourcemap-codec.d.cts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/sourcemap-codec.d.cts
new file mode 100644
index 00000000..5f35e22f
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/sourcemap-codec.d.cts
@@ -0,0 +1,9 @@
+export { decodeOriginalScopes, encodeOriginalScopes, decodeGeneratedRanges, encodeGeneratedRanges, } from './scopes.cts';
+export type { OriginalScope, GeneratedRange, CallSite, BindingExpressionRange } from './scopes.cts';
+export type SourceMapSegment = [number] | [number, number, number, number] | [number, number, number, number, number];
+export type SourceMapLine = SourceMapSegment[];
+export type SourceMapMappings = SourceMapLine[];
+export declare function decode(mappings: string): SourceMapMappings;
+export declare function encode(decoded: SourceMapMappings): string;
+export declare function encode(decoded: Readonly): string;
+//# sourceMappingURL=sourcemap-codec.d.ts.map
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/sourcemap-codec.d.cts.map b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/sourcemap-codec.d.cts.map
new file mode 100644
index 00000000..7123d520
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/sourcemap-codec.d.cts.map
@@ -0,0 +1 @@
+{"version":3,"file":"sourcemap-codec.d.ts","sourceRoot":"","sources":["../src/sourcemap-codec.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,UAAU,CAAC;AAClB,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,QAAQ,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAEhG,MAAM,MAAM,gBAAgB,GACxB,CAAC,MAAM,CAAC,GACR,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,GAChC,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;AAC7C,MAAM,MAAM,aAAa,GAAG,gBAAgB,EAAE,CAAC;AAC/C,MAAM,MAAM,iBAAiB,GAAG,aAAa,EAAE,CAAC;AAEhD,wBAAgB,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,iBAAiB,CAiD1D;AAUD,wBAAgB,MAAM,CAAC,OAAO,EAAE,iBAAiB,GAAG,MAAM,CAAC;AAC3D,wBAAgB,MAAM,CAAC,OAAO,EAAE,QAAQ,CAAC,iBAAiB,CAAC,GAAG,MAAM,CAAC"}
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/sourcemap-codec.d.mts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/sourcemap-codec.d.mts
new file mode 100644
index 00000000..199fb9f5
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/sourcemap-codec.d.mts
@@ -0,0 +1,9 @@
+export { decodeOriginalScopes, encodeOriginalScopes, decodeGeneratedRanges, encodeGeneratedRanges, } from './scopes.mts';
+export type { OriginalScope, GeneratedRange, CallSite, BindingExpressionRange } from './scopes.mts';
+export type SourceMapSegment = [number] | [number, number, number, number] | [number, number, number, number, number];
+export type SourceMapLine = SourceMapSegment[];
+export type SourceMapMappings = SourceMapLine[];
+export declare function decode(mappings: string): SourceMapMappings;
+export declare function encode(decoded: SourceMapMappings): string;
+export declare function encode(decoded: Readonly): string;
+//# sourceMappingURL=sourcemap-codec.d.ts.map
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/sourcemap-codec.d.mts.map b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/sourcemap-codec.d.mts.map
new file mode 100644
index 00000000..7123d520
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/sourcemap-codec.d.mts.map
@@ -0,0 +1 @@
+{"version":3,"file":"sourcemap-codec.d.ts","sourceRoot":"","sources":["../src/sourcemap-codec.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,UAAU,CAAC;AAClB,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,QAAQ,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAEhG,MAAM,MAAM,gBAAgB,GACxB,CAAC,MAAM,CAAC,GACR,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,GAChC,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;AAC7C,MAAM,MAAM,aAAa,GAAG,gBAAgB,EAAE,CAAC;AAC/C,MAAM,MAAM,iBAAiB,GAAG,aAAa,EAAE,CAAC;AAEhD,wBAAgB,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,iBAAiB,CAiD1D;AAUD,wBAAgB,MAAM,CAAC,OAAO,EAAE,iBAAiB,GAAG,MAAM,CAAC;AAC3D,wBAAgB,MAAM,CAAC,OAAO,EAAE,QAAQ,CAAC,iBAAiB,CAAC,GAAG,MAAM,CAAC"}
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/strings.d.cts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/strings.d.cts
new file mode 100644
index 00000000..62faceb3
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/strings.d.cts
@@ -0,0 +1,16 @@
+export declare class StringWriter {
+ pos: number;
+ private out;
+ private buffer;
+ write(v: number): void;
+ flush(): string;
+}
+export declare class StringReader {
+ pos: number;
+ private buffer;
+ constructor(buffer: string);
+ next(): number;
+ peek(): number;
+ indexOf(char: string): number;
+}
+//# sourceMappingURL=strings.d.ts.map
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/strings.d.cts.map b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/strings.d.cts.map
new file mode 100644
index 00000000..d3602da4
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/strings.d.cts.map
@@ -0,0 +1 @@
+{"version":3,"file":"strings.d.ts","sourceRoot":"","sources":["../src/strings.ts"],"names":[],"mappings":"AAuBA,qBAAa,YAAY;IACvB,GAAG,SAAK;IACR,OAAO,CAAC,GAAG,CAAM;IACjB,OAAO,CAAC,MAAM,CAA6B;IAE3C,KAAK,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI;IAStB,KAAK,IAAI,MAAM;CAIhB;AAED,qBAAa,YAAY;IACvB,GAAG,SAAK;IACR,QAAgB,MAAM,CAAS;gBAEnB,MAAM,EAAE,MAAM;IAI1B,IAAI,IAAI,MAAM;IAId,IAAI,IAAI,MAAM;IAId,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;CAK9B"}
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/strings.d.mts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/strings.d.mts
new file mode 100644
index 00000000..62faceb3
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/strings.d.mts
@@ -0,0 +1,16 @@
+export declare class StringWriter {
+ pos: number;
+ private out;
+ private buffer;
+ write(v: number): void;
+ flush(): string;
+}
+export declare class StringReader {
+ pos: number;
+ private buffer;
+ constructor(buffer: string);
+ next(): number;
+ peek(): number;
+ indexOf(char: string): number;
+}
+//# sourceMappingURL=strings.d.ts.map
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/strings.d.mts.map b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/strings.d.mts.map
new file mode 100644
index 00000000..d3602da4
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/strings.d.mts.map
@@ -0,0 +1 @@
+{"version":3,"file":"strings.d.ts","sourceRoot":"","sources":["../src/strings.ts"],"names":[],"mappings":"AAuBA,qBAAa,YAAY;IACvB,GAAG,SAAK;IACR,OAAO,CAAC,GAAG,CAAM;IACjB,OAAO,CAAC,MAAM,CAA6B;IAE3C,KAAK,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI;IAStB,KAAK,IAAI,MAAM;CAIhB;AAED,qBAAa,YAAY;IACvB,GAAG,SAAK;IACR,QAAgB,MAAM,CAAS;gBAEnB,MAAM,EAAE,MAAM;IAI1B,IAAI,IAAI,MAAM;IAId,IAAI,IAAI,MAAM;IAId,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;CAK9B"}
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/vlq.d.cts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/vlq.d.cts
new file mode 100644
index 00000000..dbd6602d
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/vlq.d.cts
@@ -0,0 +1,7 @@
+import type { StringReader, StringWriter } from './strings.cts';
+export declare const comma: number;
+export declare const semicolon: number;
+export declare function decodeInteger(reader: StringReader, relative: number): number;
+export declare function encodeInteger(builder: StringWriter, num: number, relative: number): number;
+export declare function hasMoreVlq(reader: StringReader, max: number): boolean;
+//# sourceMappingURL=vlq.d.ts.map
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/vlq.d.cts.map b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/vlq.d.cts.map
new file mode 100644
index 00000000..6fdc3569
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/vlq.d.cts.map
@@ -0,0 +1 @@
+{"version":3,"file":"vlq.d.ts","sourceRoot":"","sources":["../src/vlq.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAE5D,eAAO,MAAM,KAAK,QAAoB,CAAC;AACvC,eAAO,MAAM,SAAS,QAAoB,CAAC;AAY3C,wBAAgB,aAAa,CAAC,MAAM,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAoB5E;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAY1F;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,YAAY,EAAE,GAAG,EAAE,MAAM,WAG3D"}
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/vlq.d.mts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/vlq.d.mts
new file mode 100644
index 00000000..2c739bc9
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/vlq.d.mts
@@ -0,0 +1,7 @@
+import type { StringReader, StringWriter } from './strings.mts';
+export declare const comma: number;
+export declare const semicolon: number;
+export declare function decodeInteger(reader: StringReader, relative: number): number;
+export declare function encodeInteger(builder: StringWriter, num: number, relative: number): number;
+export declare function hasMoreVlq(reader: StringReader, max: number): boolean;
+//# sourceMappingURL=vlq.d.ts.map
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/vlq.d.mts.map b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/vlq.d.mts.map
new file mode 100644
index 00000000..6fdc3569
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/sourcemap-codec/types/vlq.d.mts.map
@@ -0,0 +1 @@
+{"version":3,"file":"vlq.d.ts","sourceRoot":"","sources":["../src/vlq.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAE5D,eAAO,MAAM,KAAK,QAAoB,CAAC;AACvC,eAAO,MAAM,SAAS,QAAoB,CAAC;AAY3C,wBAAgB,aAAa,CAAC,MAAM,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAoB5E;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAY1F;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,YAAY,EAAE,GAAG,EAAE,MAAM,WAG3D"}
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/LICENSE b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/LICENSE
new file mode 100644
index 00000000..1f6ce94c
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/LICENSE
@@ -0,0 +1,19 @@
+Copyright 2024 Justin Ridgewell
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/README.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/README.md
new file mode 100644
index 00000000..9fc0ed09
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/README.md
@@ -0,0 +1,348 @@
+# @jridgewell/trace-mapping
+
+> Trace the original position through a source map
+
+`trace-mapping` allows you to take the line and column of an output file and trace it to the
+original location in the source file through a source map.
+
+You may already be familiar with the [`source-map`][source-map] package's `SourceMapConsumer`. This
+provides the same `originalPositionFor` and `generatedPositionFor` API, without requiring WASM.
+
+## Installation
+
+```sh
+npm install @jridgewell/trace-mapping
+```
+
+## Usage
+
+```typescript
+import {
+ TraceMap,
+ originalPositionFor,
+ generatedPositionFor,
+ sourceContentFor,
+ isIgnored,
+} from '@jridgewell/trace-mapping';
+
+const tracer = new TraceMap({
+ version: 3,
+ sources: ['input.js'],
+ sourcesContent: ['content of input.js'],
+ names: ['foo'],
+ mappings: 'KAyCIA',
+ ignoreList: [],
+});
+
+// Lines start at line 1, columns at column 0.
+const traced = originalPositionFor(tracer, { line: 1, column: 5 });
+assert.deepEqual(traced, {
+ source: 'input.js',
+ line: 42,
+ column: 4,
+ name: 'foo',
+});
+
+const content = sourceContentFor(tracer, traced.source);
+assert.strictEqual(content, 'content for input.js');
+
+const generated = generatedPositionFor(tracer, {
+ source: 'input.js',
+ line: 42,
+ column: 4,
+});
+assert.deepEqual(generated, {
+ line: 1,
+ column: 5,
+});
+
+const ignored = isIgnored(tracer, 'input.js');
+assert.equal(ignored, false);
+```
+
+We also provide a lower level API to get the actual segment that matches our line and column. Unlike
+`originalPositionFor`, `traceSegment` uses a 0-base for `line`:
+
+```typescript
+import { traceSegment } from '@jridgewell/trace-mapping';
+
+// line is 0-base.
+const traced = traceSegment(tracer, /* line */ 0, /* column */ 5);
+
+// Segments are [outputColumn, sourcesIndex, sourceLine, sourceColumn, namesIndex]
+// Again, line is 0-base and so is sourceLine
+assert.deepEqual(traced, [5, 0, 41, 4, 0]);
+```
+
+### SectionedSourceMaps
+
+The sourcemap spec defines a special `sections` field that's designed to handle concatenation of
+output code with associated sourcemaps. This type of sourcemap is rarely used (no major build tool
+produces it), but if you are hand coding a concatenation you may need it. We provide an `AnyMap`
+helper that can receive either a regular sourcemap or a `SectionedSourceMap` and returns a
+`TraceMap` instance:
+
+```typescript
+import { AnyMap } from '@jridgewell/trace-mapping';
+const fooOutput = 'foo';
+const barOutput = 'bar';
+const output = [fooOutput, barOutput].join('\n');
+
+const sectioned = new AnyMap({
+ version: 3,
+ sections: [
+ {
+ // 0-base line and column
+ offset: { line: 0, column: 0 },
+ // fooOutput's sourcemap
+ map: {
+ version: 3,
+ sources: ['foo.js'],
+ names: ['foo'],
+ mappings: 'AAAAA',
+ },
+ },
+ {
+ // barOutput's sourcemap will not affect the first line, only the second
+ offset: { line: 1, column: 0 },
+ map: {
+ version: 3,
+ sources: ['bar.js'],
+ names: ['bar'],
+ mappings: 'AAAAA',
+ },
+ },
+ ],
+});
+
+const traced = originalPositionFor(sectioned, {
+ line: 2,
+ column: 0,
+});
+
+assert.deepEqual(traced, {
+ source: 'bar.js',
+ line: 1,
+ column: 0,
+ name: 'bar',
+});
+```
+
+## Benchmarks
+
+```
+node v20.10.0
+
+amp.js.map - 45120 segments
+
+Memory Usage:
+trace-mapping decoded 414164 bytes
+trace-mapping encoded 6274352 bytes
+source-map-js 10968904 bytes
+source-map-0.6.1 17587160 bytes
+source-map-0.8.0 8812155 bytes
+Chrome dev tools 8672912 bytes
+Smallest memory usage is trace-mapping decoded
+
+Init speed:
+trace-mapping: decoded JSON input x 205 ops/sec ±0.19% (88 runs sampled)
+trace-mapping: encoded JSON input x 405 ops/sec ±1.47% (88 runs sampled)
+trace-mapping: decoded Object input x 4,645 ops/sec ±0.15% (98 runs sampled)
+trace-mapping: encoded Object input x 458 ops/sec ±1.63% (91 runs sampled)
+source-map-js: encoded Object input x 75.48 ops/sec ±1.64% (67 runs sampled)
+source-map-0.6.1: encoded Object input x 39.37 ops/sec ±1.44% (53 runs sampled)
+Chrome dev tools: encoded Object input x 150 ops/sec ±1.76% (79 runs sampled)
+Fastest is trace-mapping: decoded Object input
+
+Trace speed (random):
+trace-mapping: decoded originalPositionFor x 44,946 ops/sec ±0.16% (99 runs sampled)
+trace-mapping: encoded originalPositionFor x 37,995 ops/sec ±1.81% (89 runs sampled)
+source-map-js: encoded originalPositionFor x 9,230 ops/sec ±1.36% (93 runs sampled)
+source-map-0.6.1: encoded originalPositionFor x 8,057 ops/sec ±0.84% (96 runs sampled)
+source-map-0.8.0: encoded originalPositionFor x 28,198 ops/sec ±1.12% (91 runs sampled)
+Chrome dev tools: encoded originalPositionFor x 46,276 ops/sec ±1.35% (95 runs sampled)
+Fastest is Chrome dev tools: encoded originalPositionFor
+
+Trace speed (ascending):
+trace-mapping: decoded originalPositionFor x 204,406 ops/sec ±0.19% (97 runs sampled)
+trace-mapping: encoded originalPositionFor x 196,695 ops/sec ±0.24% (99 runs sampled)
+source-map-js: encoded originalPositionFor x 11,948 ops/sec ±0.94% (99 runs sampled)
+source-map-0.6.1: encoded originalPositionFor x 10,730 ops/sec ±0.36% (100 runs sampled)
+source-map-0.8.0: encoded originalPositionFor x 51,427 ops/sec ±0.21% (98 runs sampled)
+Chrome dev tools: encoded originalPositionFor x 162,615 ops/sec ±0.18% (98 runs sampled)
+Fastest is trace-mapping: decoded originalPositionFor
+
+
+***
+
+
+babel.min.js.map - 347793 segments
+
+Memory Usage:
+trace-mapping decoded 18504 bytes
+trace-mapping encoded 35428008 bytes
+source-map-js 51676808 bytes
+source-map-0.6.1 63367136 bytes
+source-map-0.8.0 43158400 bytes
+Chrome dev tools 50721552 bytes
+Smallest memory usage is trace-mapping decoded
+
+Init speed:
+trace-mapping: decoded JSON input x 17.82 ops/sec ±6.35% (35 runs sampled)
+trace-mapping: encoded JSON input x 31.57 ops/sec ±7.50% (43 runs sampled)
+trace-mapping: decoded Object input x 867 ops/sec ±0.74% (94 runs sampled)
+trace-mapping: encoded Object input x 33.83 ops/sec ±7.66% (46 runs sampled)
+source-map-js: encoded Object input x 6.58 ops/sec ±3.31% (20 runs sampled)
+source-map-0.6.1: encoded Object input x 4.23 ops/sec ±3.43% (15 runs sampled)
+Chrome dev tools: encoded Object input x 22.14 ops/sec ±3.79% (41 runs sampled)
+Fastest is trace-mapping: decoded Object input
+
+Trace speed (random):
+trace-mapping: decoded originalPositionFor x 78,234 ops/sec ±1.48% (29 runs sampled)
+trace-mapping: encoded originalPositionFor x 60,761 ops/sec ±1.35% (21 runs sampled)
+source-map-js: encoded originalPositionFor x 51,448 ops/sec ±2.17% (89 runs sampled)
+source-map-0.6.1: encoded originalPositionFor x 47,221 ops/sec ±1.99% (15 runs sampled)
+source-map-0.8.0: encoded originalPositionFor x 84,002 ops/sec ±1.45% (27 runs sampled)
+Chrome dev tools: encoded originalPositionFor x 106,457 ops/sec ±1.38% (37 runs sampled)
+Fastest is Chrome dev tools: encoded originalPositionFor
+
+Trace speed (ascending):
+trace-mapping: decoded originalPositionFor x 930,943 ops/sec ±0.25% (99 runs sampled)
+trace-mapping: encoded originalPositionFor x 843,545 ops/sec ±0.34% (97 runs sampled)
+source-map-js: encoded originalPositionFor x 114,510 ops/sec ±1.37% (36 runs sampled)
+source-map-0.6.1: encoded originalPositionFor x 87,412 ops/sec ±0.72% (92 runs sampled)
+source-map-0.8.0: encoded originalPositionFor x 197,709 ops/sec ±0.89% (59 runs sampled)
+Chrome dev tools: encoded originalPositionFor x 688,983 ops/sec ±0.33% (98 runs sampled)
+Fastest is trace-mapping: decoded originalPositionFor
+
+
+***
+
+
+preact.js.map - 1992 segments
+
+Memory Usage:
+trace-mapping decoded 33136 bytes
+trace-mapping encoded 254240 bytes
+source-map-js 837488 bytes
+source-map-0.6.1 961928 bytes
+source-map-0.8.0 54384 bytes
+Chrome dev tools 709680 bytes
+Smallest memory usage is trace-mapping decoded
+
+Init speed:
+trace-mapping: decoded JSON input x 3,709 ops/sec ±0.13% (99 runs sampled)
+trace-mapping: encoded JSON input x 6,447 ops/sec ±0.22% (101 runs sampled)
+trace-mapping: decoded Object input x 83,062 ops/sec ±0.23% (100 runs sampled)
+trace-mapping: encoded Object input x 14,980 ops/sec ±0.28% (100 runs sampled)
+source-map-js: encoded Object input x 2,544 ops/sec ±0.16% (99 runs sampled)
+source-map-0.6.1: encoded Object input x 1,221 ops/sec ±0.37% (97 runs sampled)
+Chrome dev tools: encoded Object input x 4,241 ops/sec ±0.39% (93 runs sampled)
+Fastest is trace-mapping: decoded Object input
+
+Trace speed (random):
+trace-mapping: decoded originalPositionFor x 91,028 ops/sec ±0.14% (94 runs sampled)
+trace-mapping: encoded originalPositionFor x 84,348 ops/sec ±0.26% (98 runs sampled)
+source-map-js: encoded originalPositionFor x 26,998 ops/sec ±0.23% (98 runs sampled)
+source-map-0.6.1: encoded originalPositionFor x 18,049 ops/sec ±0.26% (100 runs sampled)
+source-map-0.8.0: encoded originalPositionFor x 41,916 ops/sec ±0.28% (98 runs sampled)
+Chrome dev tools: encoded originalPositionFor x 88,616 ops/sec ±0.14% (98 runs sampled)
+Fastest is trace-mapping: decoded originalPositionFor
+
+Trace speed (ascending):
+trace-mapping: decoded originalPositionFor x 319,960 ops/sec ±0.16% (100 runs sampled)
+trace-mapping: encoded originalPositionFor x 302,153 ops/sec ±0.18% (100 runs sampled)
+source-map-js: encoded originalPositionFor x 35,574 ops/sec ±0.19% (100 runs sampled)
+source-map-0.6.1: encoded originalPositionFor x 19,943 ops/sec ±0.12% (101 runs sampled)
+source-map-0.8.0: encoded originalPositionFor x 54,648 ops/sec ±0.20% (99 runs sampled)
+Chrome dev tools: encoded originalPositionFor x 278,319 ops/sec ±0.17% (102 runs sampled)
+Fastest is trace-mapping: decoded originalPositionFor
+
+
+***
+
+
+react.js.map - 5726 segments
+
+Memory Usage:
+trace-mapping decoded 10872 bytes
+trace-mapping encoded 681512 bytes
+source-map-js 2563944 bytes
+source-map-0.6.1 2150864 bytes
+source-map-0.8.0 88680 bytes
+Chrome dev tools 1149576 bytes
+Smallest memory usage is trace-mapping decoded
+
+Init speed:
+trace-mapping: decoded JSON input x 1,887 ops/sec ±0.28% (99 runs sampled)
+trace-mapping: encoded JSON input x 4,749 ops/sec ±0.48% (97 runs sampled)
+trace-mapping: decoded Object input x 74,236 ops/sec ±0.11% (99 runs sampled)
+trace-mapping: encoded Object input x 5,752 ops/sec ±0.38% (100 runs sampled)
+source-map-js: encoded Object input x 806 ops/sec ±0.19% (97 runs sampled)
+source-map-0.6.1: encoded Object input x 418 ops/sec ±0.33% (94 runs sampled)
+Chrome dev tools: encoded Object input x 1,524 ops/sec ±0.57% (92 runs sampled)
+Fastest is trace-mapping: decoded Object input
+
+Trace speed (random):
+trace-mapping: decoded originalPositionFor x 620,201 ops/sec ±0.33% (96 runs sampled)
+trace-mapping: encoded originalPositionFor x 579,548 ops/sec ±0.35% (97 runs sampled)
+source-map-js: encoded originalPositionFor x 230,983 ops/sec ±0.62% (54 runs sampled)
+source-map-0.6.1: encoded originalPositionFor x 158,145 ops/sec ±0.80% (46 runs sampled)
+source-map-0.8.0: encoded originalPositionFor x 343,801 ops/sec ±0.55% (96 runs sampled)
+Chrome dev tools: encoded originalPositionFor x 659,649 ops/sec ±0.49% (98 runs sampled)
+Fastest is Chrome dev tools: encoded originalPositionFor
+
+Trace speed (ascending):
+trace-mapping: decoded originalPositionFor x 2,368,079 ops/sec ±0.32% (98 runs sampled)
+trace-mapping: encoded originalPositionFor x 2,134,039 ops/sec ±2.72% (87 runs sampled)
+source-map-js: encoded originalPositionFor x 290,120 ops/sec ±2.49% (82 runs sampled)
+source-map-0.6.1: encoded originalPositionFor x 187,613 ops/sec ±0.86% (49 runs sampled)
+source-map-0.8.0: encoded originalPositionFor x 479,569 ops/sec ±0.65% (96 runs sampled)
+Chrome dev tools: encoded originalPositionFor x 2,048,414 ops/sec ±0.24% (98 runs sampled)
+Fastest is trace-mapping: decoded originalPositionFor
+
+
+***
+
+
+vscode.map - 2141001 segments
+
+Memory Usage:
+trace-mapping decoded 5206584 bytes
+trace-mapping encoded 208370336 bytes
+source-map-js 278493008 bytes
+source-map-0.6.1 391564048 bytes
+source-map-0.8.0 257508787 bytes
+Chrome dev tools 291053000 bytes
+Smallest memory usage is trace-mapping decoded
+
+Init speed:
+trace-mapping: decoded JSON input x 1.63 ops/sec ±33.88% (9 runs sampled)
+trace-mapping: encoded JSON input x 3.29 ops/sec ±36.13% (13 runs sampled)
+trace-mapping: decoded Object input x 103 ops/sec ±0.93% (77 runs sampled)
+trace-mapping: encoded Object input x 5.42 ops/sec ±28.54% (19 runs sampled)
+source-map-js: encoded Object input x 1.07 ops/sec ±13.84% (7 runs sampled)
+source-map-0.6.1: encoded Object input x 0.60 ops/sec ±2.43% (6 runs sampled)
+Chrome dev tools: encoded Object input x 2.61 ops/sec ±22.00% (11 runs sampled)
+Fastest is trace-mapping: decoded Object input
+
+Trace speed (random):
+trace-mapping: decoded originalPositionFor x 257,019 ops/sec ±0.97% (93 runs sampled)
+trace-mapping: encoded originalPositionFor x 179,163 ops/sec ±0.83% (92 runs sampled)
+source-map-js: encoded originalPositionFor x 73,337 ops/sec ±1.35% (87 runs sampled)
+source-map-0.6.1: encoded originalPositionFor x 38,797 ops/sec ±1.66% (88 runs sampled)
+source-map-0.8.0: encoded originalPositionFor x 107,758 ops/sec ±1.94% (45 runs sampled)
+Chrome dev tools: encoded originalPositionFor x 188,550 ops/sec ±1.85% (79 runs sampled)
+Fastest is trace-mapping: decoded originalPositionFor
+
+Trace speed (ascending):
+trace-mapping: decoded originalPositionFor x 447,621 ops/sec ±3.64% (94 runs sampled)
+trace-mapping: encoded originalPositionFor x 323,698 ops/sec ±5.20% (88 runs sampled)
+source-map-js: encoded originalPositionFor x 78,387 ops/sec ±1.69% (89 runs sampled)
+source-map-0.6.1: encoded originalPositionFor x 41,016 ops/sec ±3.01% (25 runs sampled)
+source-map-0.8.0: encoded originalPositionFor x 124,204 ops/sec ±0.90% (92 runs sampled)
+Chrome dev tools: encoded originalPositionFor x 230,087 ops/sec ±2.61% (93 runs sampled)
+Fastest is trace-mapping: decoded originalPositionFor
+```
+
+[source-map]: https://www.npmjs.com/package/source-map
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/package.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/package.json
new file mode 100644
index 00000000..9d3a1c08
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/package.json
@@ -0,0 +1,67 @@
+{
+ "name": "@jridgewell/trace-mapping",
+ "version": "0.3.31",
+ "description": "Trace the original position through a source map",
+ "keywords": [
+ "source",
+ "map"
+ ],
+ "main": "dist/trace-mapping.umd.js",
+ "module": "dist/trace-mapping.mjs",
+ "types": "types/trace-mapping.d.cts",
+ "files": [
+ "dist",
+ "src",
+ "types"
+ ],
+ "exports": {
+ ".": [
+ {
+ "import": {
+ "types": "./types/trace-mapping.d.mts",
+ "default": "./dist/trace-mapping.mjs"
+ },
+ "default": {
+ "types": "./types/trace-mapping.d.cts",
+ "default": "./dist/trace-mapping.umd.js"
+ }
+ },
+ "./dist/trace-mapping.umd.js"
+ ],
+ "./package.json": "./package.json"
+ },
+ "scripts": {
+ "benchmark": "run-s build:code benchmark:*",
+ "benchmark:install": "cd benchmark && npm install",
+ "benchmark:only": "node --expose-gc benchmark/index.mjs",
+ "build": "run-s -n build:code build:types",
+ "build:code": "node ../../esbuild.mjs trace-mapping.ts",
+ "build:types": "run-s build:types:force build:types:emit build:types:mts",
+ "build:types:force": "rimraf tsconfig.build.tsbuildinfo",
+ "build:types:emit": "tsc --project tsconfig.build.json",
+ "build:types:mts": "node ../../mts-types.mjs",
+ "clean": "run-s -n clean:code clean:types",
+ "clean:code": "tsc --build --clean tsconfig.build.json",
+ "clean:types": "rimraf dist types",
+ "test": "run-s -n test:types test:only test:format",
+ "test:format": "prettier --check '{src,test}/**/*.ts'",
+ "test:only": "mocha",
+ "test:types": "eslint '{src,test}/**/*.ts'",
+ "lint": "run-s -n lint:types lint:format",
+ "lint:format": "npm run test:format -- --write",
+ "lint:types": "npm run test:types -- --fix",
+ "prepublishOnly": "npm run-s -n build test"
+ },
+ "homepage": "https://github.com/jridgewell/sourcemaps/tree/main/packages/trace-mapping",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/jridgewell/sourcemaps.git",
+ "directory": "packages/trace-mapping"
+ },
+ "author": "Justin Ridgewell ",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+}
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/src/binary-search.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/src/binary-search.ts
new file mode 100644
index 00000000..c1144ad1
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/src/binary-search.ts
@@ -0,0 +1,115 @@
+import type { SourceMapSegment, ReverseSegment } from './sourcemap-segment';
+import { COLUMN } from './sourcemap-segment';
+
+export type MemoState = {
+ lastKey: number;
+ lastNeedle: number;
+ lastIndex: number;
+};
+
+export let found = false;
+
+/**
+ * A binary search implementation that returns the index if a match is found.
+ * If no match is found, then the left-index (the index associated with the item that comes just
+ * before the desired index) is returned. To maintain proper sort order, a splice would happen at
+ * the next index:
+ *
+ * ```js
+ * const array = [1, 3];
+ * const needle = 2;
+ * const index = binarySearch(array, needle, (item, needle) => item - needle);
+ *
+ * assert.equal(index, 0);
+ * array.splice(index + 1, 0, needle);
+ * assert.deepEqual(array, [1, 2, 3]);
+ * ```
+ */
+export function binarySearch(
+ haystack: SourceMapSegment[] | ReverseSegment[],
+ needle: number,
+ low: number,
+ high: number,
+): number {
+ while (low <= high) {
+ const mid = low + ((high - low) >> 1);
+ const cmp = haystack[mid][COLUMN] - needle;
+
+ if (cmp === 0) {
+ found = true;
+ return mid;
+ }
+
+ if (cmp < 0) {
+ low = mid + 1;
+ } else {
+ high = mid - 1;
+ }
+ }
+
+ found = false;
+ return low - 1;
+}
+
+export function upperBound(
+ haystack: SourceMapSegment[] | ReverseSegment[],
+ needle: number,
+ index: number,
+): number {
+ for (let i = index + 1; i < haystack.length; index = i++) {
+ if (haystack[i][COLUMN] !== needle) break;
+ }
+ return index;
+}
+
+export function lowerBound(
+ haystack: SourceMapSegment[] | ReverseSegment[],
+ needle: number,
+ index: number,
+): number {
+ for (let i = index - 1; i >= 0; index = i--) {
+ if (haystack[i][COLUMN] !== needle) break;
+ }
+ return index;
+}
+
+export function memoizedState(): MemoState {
+ return {
+ lastKey: -1,
+ lastNeedle: -1,
+ lastIndex: -1,
+ };
+}
+
+/**
+ * This overly complicated beast is just to record the last tested line/column and the resulting
+ * index, allowing us to skip a few tests if mappings are monotonically increasing.
+ */
+export function memoizedBinarySearch(
+ haystack: SourceMapSegment[] | ReverseSegment[],
+ needle: number,
+ state: MemoState,
+ key: number,
+): number {
+ const { lastKey, lastNeedle, lastIndex } = state;
+
+ let low = 0;
+ let high = haystack.length - 1;
+ if (key === lastKey) {
+ if (needle === lastNeedle) {
+ found = lastIndex !== -1 && haystack[lastIndex][COLUMN] === needle;
+ return lastIndex;
+ }
+
+ if (needle >= lastNeedle) {
+ // lastIndex may be -1 if the previous needle was not found.
+ low = lastIndex === -1 ? 0 : lastIndex;
+ } else {
+ high = lastIndex;
+ }
+ }
+ state.lastKey = key;
+ state.lastNeedle = needle;
+
+ return (state.lastIndex = binarySearch(haystack, needle, low, high));
+}
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/src/by-source.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/src/by-source.ts
new file mode 100644
index 00000000..1da6af05
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/src/by-source.ts
@@ -0,0 +1,41 @@
+import { COLUMN, SOURCES_INDEX, SOURCE_LINE, SOURCE_COLUMN } from './sourcemap-segment';
+import { sortComparator } from './sort';
+
+import type { ReverseSegment, SourceMapSegment } from './sourcemap-segment';
+
+export type Source = ReverseSegment[][];
+
+// Rebuilds the original source files, with mappings that are ordered by source line/column instead
+// of generated line/column.
+export default function buildBySources(
+ decoded: readonly SourceMapSegment[][],
+ memos: unknown[],
+): Source[] {
+ const sources: Source[] = memos.map(() => []);
+
+ for (let i = 0; i < decoded.length; i++) {
+ const line = decoded[i];
+ for (let j = 0; j < line.length; j++) {
+ const seg = line[j];
+ if (seg.length === 1) continue;
+
+ const sourceIndex = seg[SOURCES_INDEX];
+ const sourceLine = seg[SOURCE_LINE];
+ const sourceColumn = seg[SOURCE_COLUMN];
+
+ const source = sources[sourceIndex];
+ const segs = (source[sourceLine] ||= []);
+ segs.push([sourceColumn, i, seg[COLUMN]]);
+ }
+ }
+
+ for (let i = 0; i < sources.length; i++) {
+ const source = sources[i];
+ for (let j = 0; j < source.length; j++) {
+ const line = source[j];
+ if (line) line.sort(sortComparator);
+ }
+ }
+
+ return sources;
+}
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/src/flatten-map.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/src/flatten-map.ts
new file mode 100644
index 00000000..61ac40ca
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/src/flatten-map.ts
@@ -0,0 +1,192 @@
+import { TraceMap, presortedDecodedMap, decodedMappings } from './trace-mapping';
+import {
+ COLUMN,
+ SOURCES_INDEX,
+ SOURCE_LINE,
+ SOURCE_COLUMN,
+ NAMES_INDEX,
+} from './sourcemap-segment';
+import { parse } from './types';
+
+import type {
+ DecodedSourceMap,
+ DecodedSourceMapXInput,
+ EncodedSourceMapXInput,
+ SectionedSourceMapXInput,
+ SectionedSourceMapInput,
+ SectionXInput,
+ Ro,
+} from './types';
+import type { SourceMapSegment } from './sourcemap-segment';
+
+type FlattenMap = {
+ new (map: Ro, mapUrl?: string | null): TraceMap;
+ (map: Ro, mapUrl?: string | null): TraceMap;
+};
+
+export const FlattenMap: FlattenMap = function (map, mapUrl) {
+ const parsed = parse(map as SectionedSourceMapInput);
+
+ if (!('sections' in parsed)) {
+ return new TraceMap(parsed as DecodedSourceMapXInput | EncodedSourceMapXInput, mapUrl);
+ }
+
+ const mappings: SourceMapSegment[][] = [];
+ const sources: string[] = [];
+ const sourcesContent: (string | null)[] = [];
+ const names: string[] = [];
+ const ignoreList: number[] = [];
+
+ recurse(
+ parsed,
+ mapUrl,
+ mappings,
+ sources,
+ sourcesContent,
+ names,
+ ignoreList,
+ 0,
+ 0,
+ Infinity,
+ Infinity,
+ );
+
+ const joined: DecodedSourceMap = {
+ version: 3,
+ file: parsed.file,
+ names,
+ sources,
+ sourcesContent,
+ mappings,
+ ignoreList,
+ };
+
+ return presortedDecodedMap(joined);
+} as FlattenMap;
+
+function recurse(
+ input: SectionedSourceMapXInput,
+ mapUrl: string | null | undefined,
+ mappings: SourceMapSegment[][],
+ sources: string[],
+ sourcesContent: (string | null)[],
+ names: string[],
+ ignoreList: number[],
+ lineOffset: number,
+ columnOffset: number,
+ stopLine: number,
+ stopColumn: number,
+) {
+ const { sections } = input;
+ for (let i = 0; i < sections.length; i++) {
+ const { map, offset } = sections[i];
+
+ let sl = stopLine;
+ let sc = stopColumn;
+ if (i + 1 < sections.length) {
+ const nextOffset = sections[i + 1].offset;
+ sl = Math.min(stopLine, lineOffset + nextOffset.line);
+
+ if (sl === stopLine) {
+ sc = Math.min(stopColumn, columnOffset + nextOffset.column);
+ } else if (sl < stopLine) {
+ sc = columnOffset + nextOffset.column;
+ }
+ }
+
+ addSection(
+ map,
+ mapUrl,
+ mappings,
+ sources,
+ sourcesContent,
+ names,
+ ignoreList,
+ lineOffset + offset.line,
+ columnOffset + offset.column,
+ sl,
+ sc,
+ );
+ }
+}
+
+function addSection(
+ input: SectionXInput['map'],
+ mapUrl: string | null | undefined,
+ mappings: SourceMapSegment[][],
+ sources: string[],
+ sourcesContent: (string | null)[],
+ names: string[],
+ ignoreList: number[],
+ lineOffset: number,
+ columnOffset: number,
+ stopLine: number,
+ stopColumn: number,
+) {
+ const parsed = parse(input);
+ if ('sections' in parsed) return recurse(...(arguments as unknown as Parameters));
+
+ const map = new TraceMap(parsed, mapUrl);
+ const sourcesOffset = sources.length;
+ const namesOffset = names.length;
+ const decoded = decodedMappings(map);
+ const { resolvedSources, sourcesContent: contents, ignoreList: ignores } = map;
+
+ append(sources, resolvedSources);
+ append(names, map.names);
+
+ if (contents) append(sourcesContent, contents);
+ else for (let i = 0; i < resolvedSources.length; i++) sourcesContent.push(null);
+
+ if (ignores) for (let i = 0; i < ignores.length; i++) ignoreList.push(ignores[i] + sourcesOffset);
+
+ for (let i = 0; i < decoded.length; i++) {
+ const lineI = lineOffset + i;
+
+ // We can only add so many lines before we step into the range that the next section's map
+ // controls. When we get to the last line, then we'll start checking the segments to see if
+ // they've crossed into the column range. But it may not have any columns that overstep, so we
+ // still need to check that we don't overstep lines, too.
+ if (lineI > stopLine) return;
+
+ // The out line may already exist in mappings (if we're continuing the line started by a
+ // previous section). Or, we may have jumped ahead several lines to start this section.
+ const out = getLine(mappings, lineI);
+ // On the 0th loop, the section's column offset shifts us forward. On all other lines (since the
+ // map can be multiple lines), it doesn't.
+ const cOffset = i === 0 ? columnOffset : 0;
+
+ const line = decoded[i];
+ for (let j = 0; j < line.length; j++) {
+ const seg = line[j];
+ const column = cOffset + seg[COLUMN];
+
+ // If this segment steps into the column range that the next section's map controls, we need
+ // to stop early.
+ if (lineI === stopLine && column >= stopColumn) return;
+
+ if (seg.length === 1) {
+ out.push([column]);
+ continue;
+ }
+
+ const sourcesIndex = sourcesOffset + seg[SOURCES_INDEX];
+ const sourceLine = seg[SOURCE_LINE];
+ const sourceColumn = seg[SOURCE_COLUMN];
+ out.push(
+ seg.length === 4
+ ? [column, sourcesIndex, sourceLine, sourceColumn]
+ : [column, sourcesIndex, sourceLine, sourceColumn, namesOffset + seg[NAMES_INDEX]],
+ );
+ }
+ }
+}
+
+function append(arr: T[], other: T[]) {
+ for (let i = 0; i < other.length; i++) arr.push(other[i]);
+}
+
+function getLine(arr: T[][], index: number): T[] {
+ for (let i = arr.length; i <= index; i++) arr[i] = [];
+ return arr[index];
+}
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/src/resolve.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/src/resolve.ts
new file mode 100644
index 00000000..30bfa3b2
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/src/resolve.ts
@@ -0,0 +1,16 @@
+import resolveUri from '@jridgewell/resolve-uri';
+import stripFilename from './strip-filename';
+
+type Resolve = (source: string | null) => string;
+export default function resolver(
+ mapUrl: string | null | undefined,
+ sourceRoot: string | undefined,
+): Resolve {
+ const from = stripFilename(mapUrl);
+ // The sourceRoot is always treated as a directory, if it's not empty.
+ // https://github.com/mozilla/source-map/blob/8cb3ee57/lib/util.js#L327
+ // https://github.com/chromium/chromium/blob/da4adbb3/third_party/blink/renderer/devtools/front_end/sdk/SourceMap.js#L400-L401
+ const prefix = sourceRoot ? sourceRoot + '/' : '';
+
+ return (source) => resolveUri(prefix + (source || ''), from);
+}
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/src/sort.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/src/sort.ts
new file mode 100644
index 00000000..5d016cb7
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/src/sort.ts
@@ -0,0 +1,45 @@
+import { COLUMN } from './sourcemap-segment';
+
+import type { ReverseSegment, SourceMapSegment } from './sourcemap-segment';
+
+export default function maybeSort(
+ mappings: SourceMapSegment[][],
+ owned: boolean,
+): SourceMapSegment[][] {
+ const unsortedIndex = nextUnsortedSegmentLine(mappings, 0);
+ if (unsortedIndex === mappings.length) return mappings;
+
+ // If we own the array (meaning we parsed it from JSON), then we're free to directly mutate it. If
+ // not, we do not want to modify the consumer's input array.
+ if (!owned) mappings = mappings.slice();
+
+ for (let i = unsortedIndex; i < mappings.length; i = nextUnsortedSegmentLine(mappings, i + 1)) {
+ mappings[i] = sortSegments(mappings[i], owned);
+ }
+ return mappings;
+}
+
+function nextUnsortedSegmentLine(mappings: SourceMapSegment[][], start: number): number {
+ for (let i = start; i < mappings.length; i++) {
+ if (!isSorted(mappings[i])) return i;
+ }
+ return mappings.length;
+}
+
+function isSorted(line: SourceMapSegment[]): boolean {
+ for (let j = 1; j < line.length; j++) {
+ if (line[j][COLUMN] < line[j - 1][COLUMN]) {
+ return false;
+ }
+ }
+ return true;
+}
+
+function sortSegments(line: SourceMapSegment[], owned: boolean): SourceMapSegment[] {
+ if (!owned) line = line.slice();
+ return line.sort(sortComparator);
+}
+
+export function sortComparator(a: T, b: T): number {
+ return a[COLUMN] - b[COLUMN];
+}
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/src/sourcemap-segment.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/src/sourcemap-segment.ts
new file mode 100644
index 00000000..94f1b6ab
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/src/sourcemap-segment.ts
@@ -0,0 +1,23 @@
+type GeneratedColumn = number;
+type SourcesIndex = number;
+type SourceLine = number;
+type SourceColumn = number;
+type NamesIndex = number;
+
+type GeneratedLine = number;
+
+export type SourceMapSegment =
+ | [GeneratedColumn]
+ | [GeneratedColumn, SourcesIndex, SourceLine, SourceColumn]
+ | [GeneratedColumn, SourcesIndex, SourceLine, SourceColumn, NamesIndex];
+
+export type ReverseSegment = [SourceColumn, GeneratedLine, GeneratedColumn];
+
+export const COLUMN = 0;
+export const SOURCES_INDEX = 1;
+export const SOURCE_LINE = 2;
+export const SOURCE_COLUMN = 3;
+export const NAMES_INDEX = 4;
+
+export const REV_GENERATED_LINE = 1;
+export const REV_GENERATED_COLUMN = 2;
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/src/strip-filename.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/src/strip-filename.ts
new file mode 100644
index 00000000..2c889800
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/src/strip-filename.ts
@@ -0,0 +1,8 @@
+/**
+ * Removes everything after the last "/", but leaves the slash.
+ */
+export default function stripFilename(path: string | undefined | null): string {
+ if (!path) return '';
+ const index = path.lastIndexOf('/');
+ return path.slice(0, index + 1);
+}
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/src/trace-mapping.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/src/trace-mapping.ts
new file mode 100644
index 00000000..0b793d5b
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/src/trace-mapping.ts
@@ -0,0 +1,502 @@
+import { encode, decode } from '@jridgewell/sourcemap-codec';
+
+import resolver from './resolve';
+import maybeSort from './sort';
+import buildBySources from './by-source';
+import {
+ memoizedState,
+ memoizedBinarySearch,
+ upperBound,
+ lowerBound,
+ found as bsFound,
+} from './binary-search';
+import {
+ COLUMN,
+ SOURCES_INDEX,
+ SOURCE_LINE,
+ SOURCE_COLUMN,
+ NAMES_INDEX,
+ REV_GENERATED_LINE,
+ REV_GENERATED_COLUMN,
+} from './sourcemap-segment';
+import { parse } from './types';
+
+import type { SourceMapSegment, ReverseSegment } from './sourcemap-segment';
+import type {
+ SourceMapV3,
+ DecodedSourceMap,
+ EncodedSourceMap,
+ InvalidOriginalMapping,
+ OriginalMapping,
+ InvalidGeneratedMapping,
+ GeneratedMapping,
+ SourceMapInput,
+ Needle,
+ SourceNeedle,
+ SourceMap,
+ EachMapping,
+ Bias,
+ XInput,
+ SectionedSourceMap,
+ Ro,
+} from './types';
+import type { Source } from './by-source';
+import type { MemoState } from './binary-search';
+
+export type { SourceMapSegment } from './sourcemap-segment';
+export type {
+ SourceMap,
+ DecodedSourceMap,
+ EncodedSourceMap,
+ Section,
+ SectionedSourceMap,
+ SourceMapV3,
+ Bias,
+ EachMapping,
+ GeneratedMapping,
+ InvalidGeneratedMapping,
+ InvalidOriginalMapping,
+ Needle,
+ OriginalMapping,
+ OriginalMapping as Mapping,
+ SectionedSourceMapInput,
+ SourceMapInput,
+ SourceNeedle,
+ XInput,
+ EncodedSourceMapXInput,
+ DecodedSourceMapXInput,
+ SectionedSourceMapXInput,
+ SectionXInput,
+} from './types';
+
+interface PublicMap {
+ _encoded: TraceMap['_encoded'];
+ _decoded: TraceMap['_decoded'];
+ _decodedMemo: TraceMap['_decodedMemo'];
+ _bySources: TraceMap['_bySources'];
+ _bySourceMemos: TraceMap['_bySourceMemos'];
+}
+
+const LINE_GTR_ZERO = '`line` must be greater than 0 (lines start at line 1)';
+const COL_GTR_EQ_ZERO = '`column` must be greater than or equal to 0 (columns start at column 0)';
+
+export const LEAST_UPPER_BOUND = -1;
+export const GREATEST_LOWER_BOUND = 1;
+
+export { FlattenMap, FlattenMap as AnyMap } from './flatten-map';
+
+export class TraceMap implements SourceMap {
+ declare version: SourceMapV3['version'];
+ declare file: SourceMapV3['file'];
+ declare names: SourceMapV3['names'];
+ declare sourceRoot: SourceMapV3['sourceRoot'];
+ declare sources: SourceMapV3['sources'];
+ declare sourcesContent: SourceMapV3['sourcesContent'];
+ declare ignoreList: SourceMapV3['ignoreList'];
+
+ declare resolvedSources: string[];
+ declare private _encoded: string | undefined;
+
+ declare private _decoded: SourceMapSegment[][] | undefined;
+ declare private _decodedMemo: MemoState;
+
+ declare private _bySources: Source[] | undefined;
+ declare private _bySourceMemos: MemoState[] | undefined;
+
+ constructor(map: Ro, mapUrl?: string | null) {
+ const isString = typeof map === 'string';
+ if (!isString && (map as unknown as { _decodedMemo: any })._decodedMemo) return map as TraceMap;
+
+ const parsed = parse(map as Exclude);
+
+ const { version, file, names, sourceRoot, sources, sourcesContent } = parsed;
+ this.version = version;
+ this.file = file;
+ this.names = names || [];
+ this.sourceRoot = sourceRoot;
+ this.sources = sources;
+ this.sourcesContent = sourcesContent;
+ this.ignoreList = parsed.ignoreList || (parsed as XInput).x_google_ignoreList || undefined;
+
+ const resolve = resolver(mapUrl, sourceRoot);
+ this.resolvedSources = sources.map(resolve);
+
+ const { mappings } = parsed;
+ if (typeof mappings === 'string') {
+ this._encoded = mappings;
+ this._decoded = undefined;
+ } else if (Array.isArray(mappings)) {
+ this._encoded = undefined;
+ this._decoded = maybeSort(mappings, isString);
+ } else if ((parsed as unknown as SectionedSourceMap).sections) {
+ throw new Error(`TraceMap passed sectioned source map, please use FlattenMap export instead`);
+ } else {
+ throw new Error(`invalid source map: ${JSON.stringify(parsed)}`);
+ }
+
+ this._decodedMemo = memoizedState();
+ this._bySources = undefined;
+ this._bySourceMemos = undefined;
+ }
+}
+
+/**
+ * Typescript doesn't allow friend access to private fields, so this just casts the map into a type
+ * with public access modifiers.
+ */
+function cast(map: unknown): PublicMap {
+ return map as any;
+}
+
+/**
+ * Returns the encoded (VLQ string) form of the SourceMap's mappings field.
+ */
+export function encodedMappings(map: TraceMap): EncodedSourceMap['mappings'] {
+ return (cast(map)._encoded ??= encode(cast(map)._decoded!));
+}
+
+/**
+ * Returns the decoded (array of lines of segments) form of the SourceMap's mappings field.
+ */
+export function decodedMappings(map: TraceMap): Readonly {
+ return (cast(map)._decoded ||= decode(cast(map)._encoded!));
+}
+
+/**
+ * A low-level API to find the segment associated with a generated line/column (think, from a
+ * stack trace). Line and column here are 0-based, unlike `originalPositionFor`.
+ */
+export function traceSegment(
+ map: TraceMap,
+ line: number,
+ column: number,
+): Readonly | null {
+ const decoded = decodedMappings(map);
+
+ // It's common for parent source maps to have pointers to lines that have no
+ // mapping (like a "//# sourceMappingURL=") at the end of the child file.
+ if (line >= decoded.length) return null;
+
+ const segments = decoded[line];
+ const index = traceSegmentInternal(
+ segments,
+ cast(map)._decodedMemo,
+ line,
+ column,
+ GREATEST_LOWER_BOUND,
+ );
+
+ return index === -1 ? null : segments[index];
+}
+
+/**
+ * A higher-level API to find the source/line/column associated with a generated line/column
+ * (think, from a stack trace). Line is 1-based, but column is 0-based, due to legacy behavior in
+ * `source-map` library.
+ */
+export function originalPositionFor(
+ map: TraceMap,
+ needle: Needle,
+): OriginalMapping | InvalidOriginalMapping {
+ let { line, column, bias } = needle;
+ line--;
+ if (line < 0) throw new Error(LINE_GTR_ZERO);
+ if (column < 0) throw new Error(COL_GTR_EQ_ZERO);
+
+ const decoded = decodedMappings(map);
+
+ // It's common for parent source maps to have pointers to lines that have no
+ // mapping (like a "//# sourceMappingURL=") at the end of the child file.
+ if (line >= decoded.length) return OMapping(null, null, null, null);
+
+ const segments = decoded[line];
+ const index = traceSegmentInternal(
+ segments,
+ cast(map)._decodedMemo,
+ line,
+ column,
+ bias || GREATEST_LOWER_BOUND,
+ );
+
+ if (index === -1) return OMapping(null, null, null, null);
+
+ const segment = segments[index];
+ if (segment.length === 1) return OMapping(null, null, null, null);
+
+ const { names, resolvedSources } = map;
+ return OMapping(
+ resolvedSources[segment[SOURCES_INDEX]],
+ segment[SOURCE_LINE] + 1,
+ segment[SOURCE_COLUMN],
+ segment.length === 5 ? names[segment[NAMES_INDEX]] : null,
+ );
+}
+
+/**
+ * Finds the generated line/column position of the provided source/line/column source position.
+ */
+export function generatedPositionFor(
+ map: TraceMap,
+ needle: SourceNeedle,
+): GeneratedMapping | InvalidGeneratedMapping {
+ const { source, line, column, bias } = needle;
+ return generatedPosition(map, source, line, column, bias || GREATEST_LOWER_BOUND, false);
+}
+
+/**
+ * Finds all generated line/column positions of the provided source/line/column source position.
+ */
+export function allGeneratedPositionsFor(map: TraceMap, needle: SourceNeedle): GeneratedMapping[] {
+ const { source, line, column, bias } = needle;
+ // SourceMapConsumer uses LEAST_UPPER_BOUND for some reason, so we follow suit.
+ return generatedPosition(map, source, line, column, bias || LEAST_UPPER_BOUND, true);
+}
+
+/**
+ * Iterates each mapping in generated position order.
+ */
+export function eachMapping(map: TraceMap, cb: (mapping: EachMapping) => void): void {
+ const decoded = decodedMappings(map);
+ const { names, resolvedSources } = map;
+
+ for (let i = 0; i < decoded.length; i++) {
+ const line = decoded[i];
+ for (let j = 0; j < line.length; j++) {
+ const seg = line[j];
+
+ const generatedLine = i + 1;
+ const generatedColumn = seg[0];
+ let source = null;
+ let originalLine = null;
+ let originalColumn = null;
+ let name = null;
+ if (seg.length !== 1) {
+ source = resolvedSources[seg[1]];
+ originalLine = seg[2] + 1;
+ originalColumn = seg[3];
+ }
+ if (seg.length === 5) name = names[seg[4]];
+
+ cb({
+ generatedLine,
+ generatedColumn,
+ source,
+ originalLine,
+ originalColumn,
+ name,
+ } as EachMapping);
+ }
+ }
+}
+
+function sourceIndex(map: TraceMap, source: string): number {
+ const { sources, resolvedSources } = map;
+ let index = sources.indexOf(source);
+ if (index === -1) index = resolvedSources.indexOf(source);
+ return index;
+}
+
+/**
+ * Retrieves the source content for a particular source, if its found. Returns null if not.
+ */
+export function sourceContentFor(map: TraceMap, source: string): string | null {
+ const { sourcesContent } = map;
+ if (sourcesContent == null) return null;
+ const index = sourceIndex(map, source);
+ return index === -1 ? null : sourcesContent[index];
+}
+
+/**
+ * Determines if the source is marked to ignore by the source map.
+ */
+export function isIgnored(map: TraceMap, source: string): boolean {
+ const { ignoreList } = map;
+ if (ignoreList == null) return false;
+ const index = sourceIndex(map, source);
+ return index === -1 ? false : ignoreList.includes(index);
+}
+
+/**
+ * A helper that skips sorting of the input map's mappings array, which can be expensive for larger
+ * maps.
+ */
+export function presortedDecodedMap(map: DecodedSourceMap, mapUrl?: string): TraceMap {
+ const tracer = new TraceMap(clone(map, []), mapUrl);
+ cast(tracer)._decoded = map.mappings;
+ return tracer;
+}
+
+/**
+ * Returns a sourcemap object (with decoded mappings) suitable for passing to a library that expects
+ * a sourcemap, or to JSON.stringify.
+ */
+export function decodedMap(
+ map: TraceMap,
+): Omit & { mappings: readonly SourceMapSegment[][] } {
+ return clone(map, decodedMappings(map));
+}
+
+/**
+ * Returns a sourcemap object (with encoded mappings) suitable for passing to a library that expects
+ * a sourcemap, or to JSON.stringify.
+ */
+export function encodedMap(map: TraceMap): EncodedSourceMap {
+ return clone(map, encodedMappings(map));
+}
+
+function clone(
+ map: TraceMap | DecodedSourceMap,
+ mappings: T,
+): T extends string ? EncodedSourceMap : DecodedSourceMap {
+ return {
+ version: map.version,
+ file: map.file,
+ names: map.names,
+ sourceRoot: map.sourceRoot,
+ sources: map.sources,
+ sourcesContent: map.sourcesContent,
+ mappings,
+ ignoreList: map.ignoreList || (map as XInput).x_google_ignoreList,
+ } as any;
+}
+
+function OMapping(source: null, line: null, column: null, name: null): InvalidOriginalMapping;
+function OMapping(
+ source: string,
+ line: number,
+ column: number,
+ name: string | null,
+): OriginalMapping;
+function OMapping(
+ source: string | null,
+ line: number | null,
+ column: number | null,
+ name: string | null,
+): OriginalMapping | InvalidOriginalMapping {
+ return { source, line, column, name } as any;
+}
+
+function GMapping(line: null, column: null): InvalidGeneratedMapping;
+function GMapping(line: number, column: number): GeneratedMapping;
+function GMapping(
+ line: number | null,
+ column: number | null,
+): GeneratedMapping | InvalidGeneratedMapping {
+ return { line, column } as any;
+}
+
+function traceSegmentInternal(
+ segments: SourceMapSegment[],
+ memo: MemoState,
+ line: number,
+ column: number,
+ bias: Bias,
+): number;
+function traceSegmentInternal(
+ segments: ReverseSegment[],
+ memo: MemoState,
+ line: number,
+ column: number,
+ bias: Bias,
+): number;
+function traceSegmentInternal(
+ segments: SourceMapSegment[] | ReverseSegment[],
+ memo: MemoState,
+ line: number,
+ column: number,
+ bias: Bias,
+): number {
+ let index = memoizedBinarySearch(segments, column, memo, line);
+ if (bsFound) {
+ index = (bias === LEAST_UPPER_BOUND ? upperBound : lowerBound)(segments, column, index);
+ } else if (bias === LEAST_UPPER_BOUND) index++;
+
+ if (index === -1 || index === segments.length) return -1;
+ return index;
+}
+
+function sliceGeneratedPositions(
+ segments: ReverseSegment[],
+ memo: MemoState,
+ line: number,
+ column: number,
+ bias: Bias,
+): GeneratedMapping[] {
+ let min = traceSegmentInternal(segments, memo, line, column, GREATEST_LOWER_BOUND);
+
+ // We ignored the bias when tracing the segment so that we're guarnateed to find the first (in
+ // insertion order) segment that matched. Even if we did respect the bias when tracing, we would
+ // still need to call `lowerBound()` to find the first segment, which is slower than just looking
+ // for the GREATEST_LOWER_BOUND to begin with. The only difference that matters for us is when the
+ // binary search didn't match, in which case GREATEST_LOWER_BOUND just needs to increment to
+ // match LEAST_UPPER_BOUND.
+ if (!bsFound && bias === LEAST_UPPER_BOUND) min++;
+
+ if (min === -1 || min === segments.length) return [];
+
+ // We may have found the segment that started at an earlier column. If this is the case, then we
+ // need to slice all generated segments that match _that_ column, because all such segments span
+ // to our desired column.
+ const matchedColumn = bsFound ? column : segments[min][COLUMN];
+
+ // The binary search is not guaranteed to find the lower bound when a match wasn't found.
+ if (!bsFound) min = lowerBound(segments, matchedColumn, min);
+ const max = upperBound(segments, matchedColumn, min);
+
+ const result = [];
+ for (; min <= max; min++) {
+ const segment = segments[min];
+ result.push(GMapping(segment[REV_GENERATED_LINE] + 1, segment[REV_GENERATED_COLUMN]));
+ }
+ return result;
+}
+
+function generatedPosition(
+ map: TraceMap,
+ source: string,
+ line: number,
+ column: number,
+ bias: Bias,
+ all: false,
+): GeneratedMapping | InvalidGeneratedMapping;
+function generatedPosition(
+ map: TraceMap,
+ source: string,
+ line: number,
+ column: number,
+ bias: Bias,
+ all: true,
+): GeneratedMapping[];
+function generatedPosition(
+ map: TraceMap,
+ source: string,
+ line: number,
+ column: number,
+ bias: Bias,
+ all: boolean,
+): GeneratedMapping | InvalidGeneratedMapping | GeneratedMapping[] {
+ line--;
+ if (line < 0) throw new Error(LINE_GTR_ZERO);
+ if (column < 0) throw new Error(COL_GTR_EQ_ZERO);
+
+ const { sources, resolvedSources } = map;
+ let sourceIndex = sources.indexOf(source);
+ if (sourceIndex === -1) sourceIndex = resolvedSources.indexOf(source);
+ if (sourceIndex === -1) return all ? [] : GMapping(null, null);
+
+ const bySourceMemos = (cast(map)._bySourceMemos ||= sources.map(memoizedState));
+ const generated = (cast(map)._bySources ||= buildBySources(decodedMappings(map), bySourceMemos));
+
+ const segments = generated[sourceIndex][line];
+ if (segments == null) return all ? [] : GMapping(null, null);
+
+ const memo = bySourceMemos[sourceIndex];
+
+ if (all) return sliceGeneratedPositions(segments, memo, line, column, bias);
+
+ const index = traceSegmentInternal(segments, memo, line, column, bias);
+ if (index === -1) return GMapping(null, null);
+
+ const segment = segments[index];
+ return GMapping(segment[REV_GENERATED_LINE] + 1, segment[REV_GENERATED_COLUMN]);
+}
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/src/types.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/src/types.ts
new file mode 100644
index 00000000..730a61fb
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/src/types.ts
@@ -0,0 +1,114 @@
+import type { SourceMapSegment } from './sourcemap-segment';
+import type { GREATEST_LOWER_BOUND, LEAST_UPPER_BOUND, TraceMap } from './trace-mapping';
+
+export interface SourceMapV3 {
+ file?: string | null;
+ names: string[];
+ sourceRoot?: string;
+ sources: (string | null)[];
+ sourcesContent?: (string | null)[];
+ version: 3;
+ ignoreList?: number[];
+}
+
+export interface EncodedSourceMap extends SourceMapV3 {
+ mappings: string;
+}
+
+export interface DecodedSourceMap extends SourceMapV3 {
+ mappings: SourceMapSegment[][];
+}
+
+export interface Section {
+ offset: { line: number; column: number };
+ map: EncodedSourceMap | DecodedSourceMap | SectionedSourceMap;
+}
+
+export interface SectionedSourceMap {
+ file?: string | null;
+ sections: Section[];
+ version: 3;
+}
+
+export type OriginalMapping = {
+ source: string | null;
+ line: number;
+ column: number;
+ name: string | null;
+};
+
+export type InvalidOriginalMapping = {
+ source: null;
+ line: null;
+ column: null;
+ name: null;
+};
+
+export type GeneratedMapping = {
+ line: number;
+ column: number;
+};
+export type InvalidGeneratedMapping = {
+ line: null;
+ column: null;
+};
+
+export type Bias = typeof GREATEST_LOWER_BOUND | typeof LEAST_UPPER_BOUND;
+
+export type XInput = { x_google_ignoreList?: SourceMapV3['ignoreList'] };
+export type EncodedSourceMapXInput = EncodedSourceMap & XInput;
+export type DecodedSourceMapXInput = DecodedSourceMap & XInput;
+export type SectionedSourceMapXInput = Omit & {
+ sections: SectionXInput[];
+};
+export type SectionXInput = Omit & {
+ map: SectionedSourceMapInput;
+};
+
+export type SourceMapInput = string | EncodedSourceMapXInput | DecodedSourceMapXInput | TraceMap;
+export type SectionedSourceMapInput = SourceMapInput | SectionedSourceMapXInput;
+
+export type Needle = { line: number; column: number; bias?: Bias };
+export type SourceNeedle = { source: string; line: number; column: number; bias?: Bias };
+
+export type EachMapping =
+ | {
+ generatedLine: number;
+ generatedColumn: number;
+ source: null;
+ originalLine: null;
+ originalColumn: null;
+ name: null;
+ }
+ | {
+ generatedLine: number;
+ generatedColumn: number;
+ source: string | null;
+ originalLine: number;
+ originalColumn: number;
+ name: string | null;
+ };
+
+export abstract class SourceMap {
+ declare version: SourceMapV3['version'];
+ declare file: SourceMapV3['file'];
+ declare names: SourceMapV3['names'];
+ declare sourceRoot: SourceMapV3['sourceRoot'];
+ declare sources: SourceMapV3['sources'];
+ declare sourcesContent: SourceMapV3['sourcesContent'];
+ declare resolvedSources: SourceMapV3['sources'];
+ declare ignoreList: SourceMapV3['ignoreList'];
+}
+
+export type Ro =
+ T extends Array
+ ? V[] | Readonly | RoArray | Readonly>
+ : T extends object
+ ? T | Readonly | RoObject | Readonly>
+ : T;
+type RoArray = Ro[];
+type RoObject = { [K in keyof T]: T[K] | Ro };
+
+export function parse(map: T): Exclude {
+ return typeof map === 'string' ? JSON.parse(map) : (map as Exclude);
+}
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/binary-search.d.cts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/binary-search.d.cts
new file mode 100644
index 00000000..b7bb85c9
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/binary-search.d.cts
@@ -0,0 +1,33 @@
+import type { SourceMapSegment, ReverseSegment } from './sourcemap-segment.cts';
+export type MemoState = {
+ lastKey: number;
+ lastNeedle: number;
+ lastIndex: number;
+};
+export declare let found: boolean;
+/**
+ * A binary search implementation that returns the index if a match is found.
+ * If no match is found, then the left-index (the index associated with the item that comes just
+ * before the desired index) is returned. To maintain proper sort order, a splice would happen at
+ * the next index:
+ *
+ * ```js
+ * const array = [1, 3];
+ * const needle = 2;
+ * const index = binarySearch(array, needle, (item, needle) => item - needle);
+ *
+ * assert.equal(index, 0);
+ * array.splice(index + 1, 0, needle);
+ * assert.deepEqual(array, [1, 2, 3]);
+ * ```
+ */
+export declare function binarySearch(haystack: SourceMapSegment[] | ReverseSegment[], needle: number, low: number, high: number): number;
+export declare function upperBound(haystack: SourceMapSegment[] | ReverseSegment[], needle: number, index: number): number;
+export declare function lowerBound(haystack: SourceMapSegment[] | ReverseSegment[], needle: number, index: number): number;
+export declare function memoizedState(): MemoState;
+/**
+ * This overly complicated beast is just to record the last tested line/column and the resulting
+ * index, allowing us to skip a few tests if mappings are monotonically increasing.
+ */
+export declare function memoizedBinarySearch(haystack: SourceMapSegment[] | ReverseSegment[], needle: number, state: MemoState, key: number): number;
+//# sourceMappingURL=binary-search.d.ts.map
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/binary-search.d.cts.map b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/binary-search.d.cts.map
new file mode 100644
index 00000000..648e84c1
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/binary-search.d.cts.map
@@ -0,0 +1 @@
+{"version":3,"file":"binary-search.d.ts","sourceRoot":"","sources":["../src/binary-search.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAG5E,MAAM,MAAM,SAAS,GAAG;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,eAAO,IAAI,KAAK,SAAQ,CAAC;AAEzB;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,YAAY,CAC1B,QAAQ,EAAE,gBAAgB,EAAE,GAAG,cAAc,EAAE,EAC/C,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,GACX,MAAM,CAmBR;AAED,wBAAgB,UAAU,CACxB,QAAQ,EAAE,gBAAgB,EAAE,GAAG,cAAc,EAAE,EAC/C,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,GACZ,MAAM,CAKR;AAED,wBAAgB,UAAU,CACxB,QAAQ,EAAE,gBAAgB,EAAE,GAAG,cAAc,EAAE,EAC/C,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,GACZ,MAAM,CAKR;AAED,wBAAgB,aAAa,IAAI,SAAS,CAMzC;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,gBAAgB,EAAE,GAAG,cAAc,EAAE,EAC/C,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,SAAS,EAChB,GAAG,EAAE,MAAM,GACV,MAAM,CAsBR"}
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/binary-search.d.mts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/binary-search.d.mts
new file mode 100644
index 00000000..19e1e6b9
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/binary-search.d.mts
@@ -0,0 +1,33 @@
+import type { SourceMapSegment, ReverseSegment } from './sourcemap-segment.mts';
+export type MemoState = {
+ lastKey: number;
+ lastNeedle: number;
+ lastIndex: number;
+};
+export declare let found: boolean;
+/**
+ * A binary search implementation that returns the index if a match is found.
+ * If no match is found, then the left-index (the index associated with the item that comes just
+ * before the desired index) is returned. To maintain proper sort order, a splice would happen at
+ * the next index:
+ *
+ * ```js
+ * const array = [1, 3];
+ * const needle = 2;
+ * const index = binarySearch(array, needle, (item, needle) => item - needle);
+ *
+ * assert.equal(index, 0);
+ * array.splice(index + 1, 0, needle);
+ * assert.deepEqual(array, [1, 2, 3]);
+ * ```
+ */
+export declare function binarySearch(haystack: SourceMapSegment[] | ReverseSegment[], needle: number, low: number, high: number): number;
+export declare function upperBound(haystack: SourceMapSegment[] | ReverseSegment[], needle: number, index: number): number;
+export declare function lowerBound(haystack: SourceMapSegment[] | ReverseSegment[], needle: number, index: number): number;
+export declare function memoizedState(): MemoState;
+/**
+ * This overly complicated beast is just to record the last tested line/column and the resulting
+ * index, allowing us to skip a few tests if mappings are monotonically increasing.
+ */
+export declare function memoizedBinarySearch(haystack: SourceMapSegment[] | ReverseSegment[], needle: number, state: MemoState, key: number): number;
+//# sourceMappingURL=binary-search.d.ts.map
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/binary-search.d.mts.map b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/binary-search.d.mts.map
new file mode 100644
index 00000000..648e84c1
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/binary-search.d.mts.map
@@ -0,0 +1 @@
+{"version":3,"file":"binary-search.d.ts","sourceRoot":"","sources":["../src/binary-search.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAG5E,MAAM,MAAM,SAAS,GAAG;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,eAAO,IAAI,KAAK,SAAQ,CAAC;AAEzB;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,YAAY,CAC1B,QAAQ,EAAE,gBAAgB,EAAE,GAAG,cAAc,EAAE,EAC/C,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,GACX,MAAM,CAmBR;AAED,wBAAgB,UAAU,CACxB,QAAQ,EAAE,gBAAgB,EAAE,GAAG,cAAc,EAAE,EAC/C,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,GACZ,MAAM,CAKR;AAED,wBAAgB,UAAU,CACxB,QAAQ,EAAE,gBAAgB,EAAE,GAAG,cAAc,EAAE,EAC/C,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,GACZ,MAAM,CAKR;AAED,wBAAgB,aAAa,IAAI,SAAS,CAMzC;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,gBAAgB,EAAE,GAAG,cAAc,EAAE,EAC/C,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,SAAS,EAChB,GAAG,EAAE,MAAM,GACV,MAAM,CAsBR"}
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/by-source.d.cts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/by-source.d.cts
new file mode 100644
index 00000000..da496939
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/by-source.d.cts
@@ -0,0 +1,4 @@
+import type { ReverseSegment, SourceMapSegment } from './sourcemap-segment.cts';
+export type Source = ReverseSegment[][];
+export = function buildBySources(decoded: readonly SourceMapSegment[][], memos: unknown[]): Source[];
+//# sourceMappingURL=by-source.d.ts.map
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/by-source.d.cts.map b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/by-source.d.cts.map
new file mode 100644
index 00000000..32d2a7a1
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/by-source.d.cts.map
@@ -0,0 +1 @@
+{"version":3,"file":"by-source.d.ts","sourceRoot":"","sources":["../src/by-source.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAE5E,MAAM,MAAM,MAAM,GAAG,cAAc,EAAE,EAAE,CAAC;AAIxC,MAAM,CAAC,OAAO,UAAU,cAAc,CACpC,OAAO,EAAE,SAAS,gBAAgB,EAAE,EAAE,EACtC,KAAK,EAAE,OAAO,EAAE,GACf,MAAM,EAAE,CA4BV"}
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/by-source.d.mts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/by-source.d.mts
new file mode 100644
index 00000000..f3610495
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/by-source.d.mts
@@ -0,0 +1,4 @@
+import type { ReverseSegment, SourceMapSegment } from './sourcemap-segment.mts';
+export type Source = ReverseSegment[][];
+export default function buildBySources(decoded: readonly SourceMapSegment[][], memos: unknown[]): Source[];
+//# sourceMappingURL=by-source.d.ts.map
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/by-source.d.mts.map b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/by-source.d.mts.map
new file mode 100644
index 00000000..32d2a7a1
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/by-source.d.mts.map
@@ -0,0 +1 @@
+{"version":3,"file":"by-source.d.ts","sourceRoot":"","sources":["../src/by-source.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAE5E,MAAM,MAAM,MAAM,GAAG,cAAc,EAAE,EAAE,CAAC;AAIxC,MAAM,CAAC,OAAO,UAAU,cAAc,CACpC,OAAO,EAAE,SAAS,gBAAgB,EAAE,EAAE,EACtC,KAAK,EAAE,OAAO,EAAE,GACf,MAAM,EAAE,CA4BV"}
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/flatten-map.d.cts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/flatten-map.d.cts
new file mode 100644
index 00000000..433d849b
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/flatten-map.d.cts
@@ -0,0 +1,9 @@
+import { TraceMap } from './trace-mapping.cts';
+import type { SectionedSourceMapInput, Ro } from './types.cts';
+type FlattenMap = {
+ new (map: Ro, mapUrl?: string | null): TraceMap;
+ (map: Ro, mapUrl?: string | null): TraceMap;
+};
+export declare const FlattenMap: FlattenMap;
+export {};
+//# sourceMappingURL=flatten-map.d.ts.map
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/flatten-map.d.cts.map b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/flatten-map.d.cts.map
new file mode 100644
index 00000000..994b208a
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/flatten-map.d.cts.map
@@ -0,0 +1 @@
+{"version":3,"file":"flatten-map.d.ts","sourceRoot":"","sources":["../src/flatten-map.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAwC,MAAM,iBAAiB,CAAC;AAUjF,OAAO,KAAK,EAKV,uBAAuB,EAEvB,EAAE,EACH,MAAM,SAAS,CAAC;AAGjB,KAAK,UAAU,GAAG;IAChB,KAAK,GAAG,EAAE,EAAE,CAAC,uBAAuB,CAAC,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,QAAQ,CAAC;IACzE,CAAC,GAAG,EAAE,EAAE,CAAC,uBAAuB,CAAC,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,QAAQ,CAAC;CACtE,CAAC;AAEF,eAAO,MAAM,UAAU,EAAE,UAsCV,CAAC"}
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/flatten-map.d.mts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/flatten-map.d.mts
new file mode 100644
index 00000000..444a1bed
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/flatten-map.d.mts
@@ -0,0 +1,9 @@
+import { TraceMap } from './trace-mapping.mts';
+import type { SectionedSourceMapInput, Ro } from './types.mts';
+type FlattenMap = {
+ new (map: Ro, mapUrl?: string | null): TraceMap;
+ (map: Ro, mapUrl?: string | null): TraceMap;
+};
+export declare const FlattenMap: FlattenMap;
+export {};
+//# sourceMappingURL=flatten-map.d.ts.map
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/flatten-map.d.mts.map b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/flatten-map.d.mts.map
new file mode 100644
index 00000000..994b208a
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/flatten-map.d.mts.map
@@ -0,0 +1 @@
+{"version":3,"file":"flatten-map.d.ts","sourceRoot":"","sources":["../src/flatten-map.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAwC,MAAM,iBAAiB,CAAC;AAUjF,OAAO,KAAK,EAKV,uBAAuB,EAEvB,EAAE,EACH,MAAM,SAAS,CAAC;AAGjB,KAAK,UAAU,GAAG;IAChB,KAAK,GAAG,EAAE,EAAE,CAAC,uBAAuB,CAAC,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,QAAQ,CAAC;IACzE,CAAC,GAAG,EAAE,EAAE,CAAC,uBAAuB,CAAC,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,QAAQ,CAAC;CACtE,CAAC;AAEF,eAAO,MAAM,UAAU,EAAE,UAsCV,CAAC"}
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/resolve.d.cts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/resolve.d.cts
new file mode 100644
index 00000000..62aeedb5
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/resolve.d.cts
@@ -0,0 +1,4 @@
+type Resolve = (source: string | null) => string;
+export = function resolver(mapUrl: string | null | undefined, sourceRoot: string | undefined): Resolve;
+export {};
+//# sourceMappingURL=resolve.d.ts.map
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/resolve.d.cts.map b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/resolve.d.cts.map
new file mode 100644
index 00000000..9f155ace
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/resolve.d.cts.map
@@ -0,0 +1 @@
+{"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../src/resolve.ts"],"names":[],"mappings":"AAGA,KAAK,OAAO,GAAG,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,KAAK,MAAM,CAAC;AACjD,MAAM,CAAC,OAAO,UAAU,QAAQ,CAC9B,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACjC,UAAU,EAAE,MAAM,GAAG,SAAS,GAC7B,OAAO,CAQT"}
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/resolve.d.mts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/resolve.d.mts
new file mode 100644
index 00000000..e2798a19
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/resolve.d.mts
@@ -0,0 +1,4 @@
+type Resolve = (source: string | null) => string;
+export default function resolver(mapUrl: string | null | undefined, sourceRoot: string | undefined): Resolve;
+export {};
+//# sourceMappingURL=resolve.d.ts.map
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/resolve.d.mts.map b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/resolve.d.mts.map
new file mode 100644
index 00000000..9f155ace
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/resolve.d.mts.map
@@ -0,0 +1 @@
+{"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../src/resolve.ts"],"names":[],"mappings":"AAGA,KAAK,OAAO,GAAG,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,KAAK,MAAM,CAAC;AACjD,MAAM,CAAC,OAAO,UAAU,QAAQ,CAC9B,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACjC,UAAU,EAAE,MAAM,GAAG,SAAS,GAC7B,OAAO,CAQT"}
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/sort.d.cts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/sort.d.cts
new file mode 100644
index 00000000..aa14c129
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/sort.d.cts
@@ -0,0 +1,4 @@
+import type { ReverseSegment, SourceMapSegment } from './sourcemap-segment.cts';
+export = function maybeSort(mappings: SourceMapSegment[][], owned: boolean): SourceMapSegment[][];
+export declare function sortComparator(a: T, b: T): number;
+//# sourceMappingURL=sort.d.ts.map
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/sort.d.cts.map b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/sort.d.cts.map
new file mode 100644
index 00000000..48b8e674
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/sort.d.cts.map
@@ -0,0 +1 @@
+{"version":3,"file":"sort.d.ts","sourceRoot":"","sources":["../src/sort.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAE5E,MAAM,CAAC,OAAO,UAAU,SAAS,CAC/B,QAAQ,EAAE,gBAAgB,EAAE,EAAE,EAC9B,KAAK,EAAE,OAAO,GACb,gBAAgB,EAAE,EAAE,CAYtB;AAuBD,wBAAgB,cAAc,CAAC,CAAC,SAAS,gBAAgB,GAAG,cAAc,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,GAAG,MAAM,CAE9F"}
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/sort.d.mts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/sort.d.mts
new file mode 100644
index 00000000..c5b94e64
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/sort.d.mts
@@ -0,0 +1,4 @@
+import type { ReverseSegment, SourceMapSegment } from './sourcemap-segment.mts';
+export default function maybeSort(mappings: SourceMapSegment[][], owned: boolean): SourceMapSegment[][];
+export declare function sortComparator(a: T, b: T): number;
+//# sourceMappingURL=sort.d.ts.map
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/sort.d.mts.map b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/sort.d.mts.map
new file mode 100644
index 00000000..48b8e674
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/sort.d.mts.map
@@ -0,0 +1 @@
+{"version":3,"file":"sort.d.ts","sourceRoot":"","sources":["../src/sort.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAE5E,MAAM,CAAC,OAAO,UAAU,SAAS,CAC/B,QAAQ,EAAE,gBAAgB,EAAE,EAAE,EAC9B,KAAK,EAAE,OAAO,GACb,gBAAgB,EAAE,EAAE,CAYtB;AAuBD,wBAAgB,cAAc,CAAC,CAAC,SAAS,gBAAgB,GAAG,cAAc,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,GAAG,MAAM,CAE9F"}
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/sourcemap-segment.d.cts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/sourcemap-segment.d.cts
new file mode 100644
index 00000000..8d3cabc1
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/sourcemap-segment.d.cts
@@ -0,0 +1,17 @@
+type GeneratedColumn = number;
+type SourcesIndex = number;
+type SourceLine = number;
+type SourceColumn = number;
+type NamesIndex = number;
+type GeneratedLine = number;
+export type SourceMapSegment = [GeneratedColumn] | [GeneratedColumn, SourcesIndex, SourceLine, SourceColumn] | [GeneratedColumn, SourcesIndex, SourceLine, SourceColumn, NamesIndex];
+export type ReverseSegment = [SourceColumn, GeneratedLine, GeneratedColumn];
+export declare const COLUMN = 0;
+export declare const SOURCES_INDEX = 1;
+export declare const SOURCE_LINE = 2;
+export declare const SOURCE_COLUMN = 3;
+export declare const NAMES_INDEX = 4;
+export declare const REV_GENERATED_LINE = 1;
+export declare const REV_GENERATED_COLUMN = 2;
+export {};
+//# sourceMappingURL=sourcemap-segment.d.ts.map
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/sourcemap-segment.d.cts.map b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/sourcemap-segment.d.cts.map
new file mode 100644
index 00000000..0c94a461
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/sourcemap-segment.d.cts.map
@@ -0,0 +1 @@
+{"version":3,"file":"sourcemap-segment.d.ts","sourceRoot":"","sources":["../src/sourcemap-segment.ts"],"names":[],"mappings":"AAAA,KAAK,eAAe,GAAG,MAAM,CAAC;AAC9B,KAAK,YAAY,GAAG,MAAM,CAAC;AAC3B,KAAK,UAAU,GAAG,MAAM,CAAC;AACzB,KAAK,YAAY,GAAG,MAAM,CAAC;AAC3B,KAAK,UAAU,GAAG,MAAM,CAAC;AAEzB,KAAK,aAAa,GAAG,MAAM,CAAC;AAE5B,MAAM,MAAM,gBAAgB,GACxB,CAAC,eAAe,CAAC,GACjB,CAAC,eAAe,EAAE,YAAY,EAAE,UAAU,EAAE,YAAY,CAAC,GACzD,CAAC,eAAe,EAAE,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,UAAU,CAAC,CAAC;AAE1E,MAAM,MAAM,cAAc,GAAG,CAAC,YAAY,EAAE,aAAa,EAAE,eAAe,CAAC,CAAC;AAE5E,eAAO,MAAM,MAAM,IAAI,CAAC;AACxB,eAAO,MAAM,aAAa,IAAI,CAAC;AAC/B,eAAO,MAAM,WAAW,IAAI,CAAC;AAC7B,eAAO,MAAM,aAAa,IAAI,CAAC;AAC/B,eAAO,MAAM,WAAW,IAAI,CAAC;AAE7B,eAAO,MAAM,kBAAkB,IAAI,CAAC;AACpC,eAAO,MAAM,oBAAoB,IAAI,CAAC"}
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/sourcemap-segment.d.mts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/sourcemap-segment.d.mts
new file mode 100644
index 00000000..8d3cabc1
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/sourcemap-segment.d.mts
@@ -0,0 +1,17 @@
+type GeneratedColumn = number;
+type SourcesIndex = number;
+type SourceLine = number;
+type SourceColumn = number;
+type NamesIndex = number;
+type GeneratedLine = number;
+export type SourceMapSegment = [GeneratedColumn] | [GeneratedColumn, SourcesIndex, SourceLine, SourceColumn] | [GeneratedColumn, SourcesIndex, SourceLine, SourceColumn, NamesIndex];
+export type ReverseSegment = [SourceColumn, GeneratedLine, GeneratedColumn];
+export declare const COLUMN = 0;
+export declare const SOURCES_INDEX = 1;
+export declare const SOURCE_LINE = 2;
+export declare const SOURCE_COLUMN = 3;
+export declare const NAMES_INDEX = 4;
+export declare const REV_GENERATED_LINE = 1;
+export declare const REV_GENERATED_COLUMN = 2;
+export {};
+//# sourceMappingURL=sourcemap-segment.d.ts.map
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/sourcemap-segment.d.mts.map b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/sourcemap-segment.d.mts.map
new file mode 100644
index 00000000..0c94a461
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/sourcemap-segment.d.mts.map
@@ -0,0 +1 @@
+{"version":3,"file":"sourcemap-segment.d.ts","sourceRoot":"","sources":["../src/sourcemap-segment.ts"],"names":[],"mappings":"AAAA,KAAK,eAAe,GAAG,MAAM,CAAC;AAC9B,KAAK,YAAY,GAAG,MAAM,CAAC;AAC3B,KAAK,UAAU,GAAG,MAAM,CAAC;AACzB,KAAK,YAAY,GAAG,MAAM,CAAC;AAC3B,KAAK,UAAU,GAAG,MAAM,CAAC;AAEzB,KAAK,aAAa,GAAG,MAAM,CAAC;AAE5B,MAAM,MAAM,gBAAgB,GACxB,CAAC,eAAe,CAAC,GACjB,CAAC,eAAe,EAAE,YAAY,EAAE,UAAU,EAAE,YAAY,CAAC,GACzD,CAAC,eAAe,EAAE,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,UAAU,CAAC,CAAC;AAE1E,MAAM,MAAM,cAAc,GAAG,CAAC,YAAY,EAAE,aAAa,EAAE,eAAe,CAAC,CAAC;AAE5E,eAAO,MAAM,MAAM,IAAI,CAAC;AACxB,eAAO,MAAM,aAAa,IAAI,CAAC;AAC/B,eAAO,MAAM,WAAW,IAAI,CAAC;AAC7B,eAAO,MAAM,aAAa,IAAI,CAAC;AAC/B,eAAO,MAAM,WAAW,IAAI,CAAC;AAE7B,eAAO,MAAM,kBAAkB,IAAI,CAAC;AACpC,eAAO,MAAM,oBAAoB,IAAI,CAAC"}
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/strip-filename.d.cts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/strip-filename.d.cts
new file mode 100644
index 00000000..8b3c0e9b
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/strip-filename.d.cts
@@ -0,0 +1,5 @@
+/**
+ * Removes everything after the last "/", but leaves the slash.
+ */
+export = function stripFilename(path: string | undefined | null): string;
+//# sourceMappingURL=strip-filename.d.ts.map
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/strip-filename.d.cts.map b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/strip-filename.d.cts.map
new file mode 100644
index 00000000..17a25da0
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/strip-filename.d.cts.map
@@ -0,0 +1 @@
+{"version":3,"file":"strip-filename.d.ts","sourceRoot":"","sources":["../src/strip-filename.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,CAAC,OAAO,UAAU,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,MAAM,CAI7E"}
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/strip-filename.d.mts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/strip-filename.d.mts
new file mode 100644
index 00000000..cbbaee0d
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/strip-filename.d.mts
@@ -0,0 +1,5 @@
+/**
+ * Removes everything after the last "/", but leaves the slash.
+ */
+export default function stripFilename(path: string | undefined | null): string;
+//# sourceMappingURL=strip-filename.d.ts.map
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/strip-filename.d.mts.map b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/strip-filename.d.mts.map
new file mode 100644
index 00000000..17a25da0
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/strip-filename.d.mts.map
@@ -0,0 +1 @@
+{"version":3,"file":"strip-filename.d.ts","sourceRoot":"","sources":["../src/strip-filename.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,CAAC,OAAO,UAAU,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,MAAM,CAI7E"}
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/trace-mapping.d.cts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/trace-mapping.d.cts
new file mode 100644
index 00000000..a40f3054
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/trace-mapping.d.cts
@@ -0,0 +1,80 @@
+import type { SourceMapSegment } from './sourcemap-segment.cts';
+import type { SourceMapV3, DecodedSourceMap, EncodedSourceMap, InvalidOriginalMapping, OriginalMapping, InvalidGeneratedMapping, GeneratedMapping, SourceMapInput, Needle, SourceNeedle, SourceMap, EachMapping, Ro } from './types.cts';
+export type { SourceMapSegment } from './sourcemap-segment.cts';
+export type { SourceMap, DecodedSourceMap, EncodedSourceMap, Section, SectionedSourceMap, SourceMapV3, Bias, EachMapping, GeneratedMapping, InvalidGeneratedMapping, InvalidOriginalMapping, Needle, OriginalMapping, OriginalMapping as Mapping, SectionedSourceMapInput, SourceMapInput, SourceNeedle, XInput, EncodedSourceMapXInput, DecodedSourceMapXInput, SectionedSourceMapXInput, SectionXInput, } from './types.cts';
+export declare const LEAST_UPPER_BOUND = -1;
+export declare const GREATEST_LOWER_BOUND = 1;
+export { FlattenMap, FlattenMap as AnyMap } from './flatten-map.cts';
+export declare class TraceMap implements SourceMap {
+ version: SourceMapV3['version'];
+ file: SourceMapV3['file'];
+ names: SourceMapV3['names'];
+ sourceRoot: SourceMapV3['sourceRoot'];
+ sources: SourceMapV3['sources'];
+ sourcesContent: SourceMapV3['sourcesContent'];
+ ignoreList: SourceMapV3['ignoreList'];
+ resolvedSources: string[];
+ private _encoded;
+ private _decoded;
+ private _decodedMemo;
+ private _bySources;
+ private _bySourceMemos;
+ constructor(map: Ro, mapUrl?: string | null);
+}
+/**
+ * Returns the encoded (VLQ string) form of the SourceMap's mappings field.
+ */
+export declare function encodedMappings(map: TraceMap): EncodedSourceMap['mappings'];
+/**
+ * Returns the decoded (array of lines of segments) form of the SourceMap's mappings field.
+ */
+export declare function decodedMappings(map: TraceMap): Readonly;
+/**
+ * A low-level API to find the segment associated with a generated line/column (think, from a
+ * stack trace). Line and column here are 0-based, unlike `originalPositionFor`.
+ */
+export declare function traceSegment(map: TraceMap, line: number, column: number): Readonly | null;
+/**
+ * A higher-level API to find the source/line/column associated with a generated line/column
+ * (think, from a stack trace). Line is 1-based, but column is 0-based, due to legacy behavior in
+ * `source-map` library.
+ */
+export declare function originalPositionFor(map: TraceMap, needle: Needle): OriginalMapping | InvalidOriginalMapping;
+/**
+ * Finds the generated line/column position of the provided source/line/column source position.
+ */
+export declare function generatedPositionFor(map: TraceMap, needle: SourceNeedle): GeneratedMapping | InvalidGeneratedMapping;
+/**
+ * Finds all generated line/column positions of the provided source/line/column source position.
+ */
+export declare function allGeneratedPositionsFor(map: TraceMap, needle: SourceNeedle): GeneratedMapping[];
+/**
+ * Iterates each mapping in generated position order.
+ */
+export declare function eachMapping(map: TraceMap, cb: (mapping: EachMapping) => void): void;
+/**
+ * Retrieves the source content for a particular source, if its found. Returns null if not.
+ */
+export declare function sourceContentFor(map: TraceMap, source: string): string | null;
+/**
+ * Determines if the source is marked to ignore by the source map.
+ */
+export declare function isIgnored(map: TraceMap, source: string): boolean;
+/**
+ * A helper that skips sorting of the input map's mappings array, which can be expensive for larger
+ * maps.
+ */
+export declare function presortedDecodedMap(map: DecodedSourceMap, mapUrl?: string): TraceMap;
+/**
+ * Returns a sourcemap object (with decoded mappings) suitable for passing to a library that expects
+ * a sourcemap, or to JSON.stringify.
+ */
+export declare function decodedMap(map: TraceMap): Omit & {
+ mappings: readonly SourceMapSegment[][];
+};
+/**
+ * Returns a sourcemap object (with encoded mappings) suitable for passing to a library that expects
+ * a sourcemap, or to JSON.stringify.
+ */
+export declare function encodedMap(map: TraceMap): EncodedSourceMap;
+//# sourceMappingURL=trace-mapping.d.ts.map
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/trace-mapping.d.cts.map b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/trace-mapping.d.cts.map
new file mode 100644
index 00000000..b5a874c0
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/trace-mapping.d.cts.map
@@ -0,0 +1 @@
+{"version":3,"file":"trace-mapping.d.ts","sourceRoot":"","sources":["../src/trace-mapping.ts"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EAAE,gBAAgB,EAAkB,MAAM,qBAAqB,CAAC;AAC5E,OAAO,KAAK,EACV,WAAW,EACX,gBAAgB,EAChB,gBAAgB,EAChB,sBAAsB,EACtB,eAAe,EACf,uBAAuB,EACvB,gBAAgB,EAChB,cAAc,EACd,MAAM,EACN,YAAY,EACZ,SAAS,EACT,WAAW,EAIX,EAAE,EACH,MAAM,SAAS,CAAC;AAIjB,YAAY,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,YAAY,EACV,SAAS,EACT,gBAAgB,EAChB,gBAAgB,EAChB,OAAO,EACP,kBAAkB,EAClB,WAAW,EACX,IAAI,EACJ,WAAW,EACX,gBAAgB,EAChB,uBAAuB,EACvB,sBAAsB,EACtB,MAAM,EACN,eAAe,EACf,eAAe,IAAI,OAAO,EAC1B,uBAAuB,EACvB,cAAc,EACd,YAAY,EACZ,MAAM,EACN,sBAAsB,EACtB,sBAAsB,EACtB,wBAAwB,EACxB,aAAa,GACd,MAAM,SAAS,CAAC;AAajB,eAAO,MAAM,iBAAiB,KAAK,CAAC;AACpC,eAAO,MAAM,oBAAoB,IAAI,CAAC;AAEtC,OAAO,EAAE,UAAU,EAAE,UAAU,IAAI,MAAM,EAAE,MAAM,eAAe,CAAC;AAEjE,qBAAa,QAAS,YAAW,SAAS;IAChC,OAAO,EAAE,WAAW,CAAC,SAAS,CAAC,CAAC;IAChC,IAAI,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC1B,KAAK,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;IAC5B,UAAU,EAAE,WAAW,CAAC,YAAY,CAAC,CAAC;IACtC,OAAO,EAAE,WAAW,CAAC,SAAS,CAAC,CAAC;IAChC,cAAc,EAAE,WAAW,CAAC,gBAAgB,CAAC,CAAC;IAC9C,UAAU,EAAE,WAAW,CAAC,YAAY,CAAC,CAAC;IAEtC,eAAe,EAAE,MAAM,EAAE,CAAC;IAClC,QAAgB,QAAQ,CAAqB;IAE7C,QAAgB,QAAQ,CAAmC;IAC3D,QAAgB,YAAY,CAAY;IAExC,QAAgB,UAAU,CAAuB;IACjD,QAAgB,cAAc,CAA0B;gBAE5C,GAAG,EAAE,EAAE,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI;CAmC5D;AAUD;;GAEG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,QAAQ,GAAG,gBAAgB,CAAC,UAAU,CAAC,CAE3E;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,QAAQ,GAAG,QAAQ,CAAC,gBAAgB,CAAC,UAAU,CAAC,CAAC,CAErF;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAC1B,GAAG,EAAE,QAAQ,EACb,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,GACb,QAAQ,CAAC,gBAAgB,CAAC,GAAG,IAAI,CAiBnC;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,QAAQ,EACb,MAAM,EAAE,MAAM,GACb,eAAe,GAAG,sBAAsB,CAiC1C;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,QAAQ,EACb,MAAM,EAAE,YAAY,GACnB,gBAAgB,GAAG,uBAAuB,CAG5C;AAED;;GAEG;AACH,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,YAAY,GAAG,gBAAgB,EAAE,CAIhG;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,IAAI,GAAG,IAAI,CAgCnF;AASD;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAK7E;AAED;;GAEG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAKhE;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,gBAAgB,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,QAAQ,CAIpF;AAED;;;GAGG;AACH,wBAAgB,UAAU,CACxB,GAAG,EAAE,QAAQ,GACZ,IAAI,CAAC,gBAAgB,EAAE,UAAU,CAAC,GAAG;IAAE,QAAQ,EAAE,SAAS,gBAAgB,EAAE,EAAE,CAAA;CAAE,CAElF;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,QAAQ,GAAG,gBAAgB,CAE1D"}
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/trace-mapping.d.mts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/trace-mapping.d.mts
new file mode 100644
index 00000000..bc2ff0f1
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/trace-mapping.d.mts
@@ -0,0 +1,80 @@
+import type { SourceMapSegment } from './sourcemap-segment.mts';
+import type { SourceMapV3, DecodedSourceMap, EncodedSourceMap, InvalidOriginalMapping, OriginalMapping, InvalidGeneratedMapping, GeneratedMapping, SourceMapInput, Needle, SourceNeedle, SourceMap, EachMapping, Ro } from './types.mts';
+export type { SourceMapSegment } from './sourcemap-segment.mts';
+export type { SourceMap, DecodedSourceMap, EncodedSourceMap, Section, SectionedSourceMap, SourceMapV3, Bias, EachMapping, GeneratedMapping, InvalidGeneratedMapping, InvalidOriginalMapping, Needle, OriginalMapping, OriginalMapping as Mapping, SectionedSourceMapInput, SourceMapInput, SourceNeedle, XInput, EncodedSourceMapXInput, DecodedSourceMapXInput, SectionedSourceMapXInput, SectionXInput, } from './types.mts';
+export declare const LEAST_UPPER_BOUND = -1;
+export declare const GREATEST_LOWER_BOUND = 1;
+export { FlattenMap, FlattenMap as AnyMap } from './flatten-map.mts';
+export declare class TraceMap implements SourceMap {
+ version: SourceMapV3['version'];
+ file: SourceMapV3['file'];
+ names: SourceMapV3['names'];
+ sourceRoot: SourceMapV3['sourceRoot'];
+ sources: SourceMapV3['sources'];
+ sourcesContent: SourceMapV3['sourcesContent'];
+ ignoreList: SourceMapV3['ignoreList'];
+ resolvedSources: string[];
+ private _encoded;
+ private _decoded;
+ private _decodedMemo;
+ private _bySources;
+ private _bySourceMemos;
+ constructor(map: Ro, mapUrl?: string | null);
+}
+/**
+ * Returns the encoded (VLQ string) form of the SourceMap's mappings field.
+ */
+export declare function encodedMappings(map: TraceMap): EncodedSourceMap['mappings'];
+/**
+ * Returns the decoded (array of lines of segments) form of the SourceMap's mappings field.
+ */
+export declare function decodedMappings(map: TraceMap): Readonly;
+/**
+ * A low-level API to find the segment associated with a generated line/column (think, from a
+ * stack trace). Line and column here are 0-based, unlike `originalPositionFor`.
+ */
+export declare function traceSegment(map: TraceMap, line: number, column: number): Readonly | null;
+/**
+ * A higher-level API to find the source/line/column associated with a generated line/column
+ * (think, from a stack trace). Line is 1-based, but column is 0-based, due to legacy behavior in
+ * `source-map` library.
+ */
+export declare function originalPositionFor(map: TraceMap, needle: Needle): OriginalMapping | InvalidOriginalMapping;
+/**
+ * Finds the generated line/column position of the provided source/line/column source position.
+ */
+export declare function generatedPositionFor(map: TraceMap, needle: SourceNeedle): GeneratedMapping | InvalidGeneratedMapping;
+/**
+ * Finds all generated line/column positions of the provided source/line/column source position.
+ */
+export declare function allGeneratedPositionsFor(map: TraceMap, needle: SourceNeedle): GeneratedMapping[];
+/**
+ * Iterates each mapping in generated position order.
+ */
+export declare function eachMapping(map: TraceMap, cb: (mapping: EachMapping) => void): void;
+/**
+ * Retrieves the source content for a particular source, if its found. Returns null if not.
+ */
+export declare function sourceContentFor(map: TraceMap, source: string): string | null;
+/**
+ * Determines if the source is marked to ignore by the source map.
+ */
+export declare function isIgnored(map: TraceMap, source: string): boolean;
+/**
+ * A helper that skips sorting of the input map's mappings array, which can be expensive for larger
+ * maps.
+ */
+export declare function presortedDecodedMap(map: DecodedSourceMap, mapUrl?: string): TraceMap;
+/**
+ * Returns a sourcemap object (with decoded mappings) suitable for passing to a library that expects
+ * a sourcemap, or to JSON.stringify.
+ */
+export declare function decodedMap(map: TraceMap): Omit & {
+ mappings: readonly SourceMapSegment[][];
+};
+/**
+ * Returns a sourcemap object (with encoded mappings) suitable for passing to a library that expects
+ * a sourcemap, or to JSON.stringify.
+ */
+export declare function encodedMap(map: TraceMap): EncodedSourceMap;
+//# sourceMappingURL=trace-mapping.d.ts.map
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/trace-mapping.d.mts.map b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/trace-mapping.d.mts.map
new file mode 100644
index 00000000..b5a874c0
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/trace-mapping.d.mts.map
@@ -0,0 +1 @@
+{"version":3,"file":"trace-mapping.d.ts","sourceRoot":"","sources":["../src/trace-mapping.ts"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EAAE,gBAAgB,EAAkB,MAAM,qBAAqB,CAAC;AAC5E,OAAO,KAAK,EACV,WAAW,EACX,gBAAgB,EAChB,gBAAgB,EAChB,sBAAsB,EACtB,eAAe,EACf,uBAAuB,EACvB,gBAAgB,EAChB,cAAc,EACd,MAAM,EACN,YAAY,EACZ,SAAS,EACT,WAAW,EAIX,EAAE,EACH,MAAM,SAAS,CAAC;AAIjB,YAAY,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,YAAY,EACV,SAAS,EACT,gBAAgB,EAChB,gBAAgB,EAChB,OAAO,EACP,kBAAkB,EAClB,WAAW,EACX,IAAI,EACJ,WAAW,EACX,gBAAgB,EAChB,uBAAuB,EACvB,sBAAsB,EACtB,MAAM,EACN,eAAe,EACf,eAAe,IAAI,OAAO,EAC1B,uBAAuB,EACvB,cAAc,EACd,YAAY,EACZ,MAAM,EACN,sBAAsB,EACtB,sBAAsB,EACtB,wBAAwB,EACxB,aAAa,GACd,MAAM,SAAS,CAAC;AAajB,eAAO,MAAM,iBAAiB,KAAK,CAAC;AACpC,eAAO,MAAM,oBAAoB,IAAI,CAAC;AAEtC,OAAO,EAAE,UAAU,EAAE,UAAU,IAAI,MAAM,EAAE,MAAM,eAAe,CAAC;AAEjE,qBAAa,QAAS,YAAW,SAAS;IAChC,OAAO,EAAE,WAAW,CAAC,SAAS,CAAC,CAAC;IAChC,IAAI,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC1B,KAAK,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;IAC5B,UAAU,EAAE,WAAW,CAAC,YAAY,CAAC,CAAC;IACtC,OAAO,EAAE,WAAW,CAAC,SAAS,CAAC,CAAC;IAChC,cAAc,EAAE,WAAW,CAAC,gBAAgB,CAAC,CAAC;IAC9C,UAAU,EAAE,WAAW,CAAC,YAAY,CAAC,CAAC;IAEtC,eAAe,EAAE,MAAM,EAAE,CAAC;IAClC,QAAgB,QAAQ,CAAqB;IAE7C,QAAgB,QAAQ,CAAmC;IAC3D,QAAgB,YAAY,CAAY;IAExC,QAAgB,UAAU,CAAuB;IACjD,QAAgB,cAAc,CAA0B;gBAE5C,GAAG,EAAE,EAAE,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI;CAmC5D;AAUD;;GAEG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,QAAQ,GAAG,gBAAgB,CAAC,UAAU,CAAC,CAE3E;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,QAAQ,GAAG,QAAQ,CAAC,gBAAgB,CAAC,UAAU,CAAC,CAAC,CAErF;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAC1B,GAAG,EAAE,QAAQ,EACb,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,GACb,QAAQ,CAAC,gBAAgB,CAAC,GAAG,IAAI,CAiBnC;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,QAAQ,EACb,MAAM,EAAE,MAAM,GACb,eAAe,GAAG,sBAAsB,CAiC1C;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,QAAQ,EACb,MAAM,EAAE,YAAY,GACnB,gBAAgB,GAAG,uBAAuB,CAG5C;AAED;;GAEG;AACH,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,YAAY,GAAG,gBAAgB,EAAE,CAIhG;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,IAAI,GAAG,IAAI,CAgCnF;AASD;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAK7E;AAED;;GAEG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAKhE;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,gBAAgB,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,QAAQ,CAIpF;AAED;;;GAGG;AACH,wBAAgB,UAAU,CACxB,GAAG,EAAE,QAAQ,GACZ,IAAI,CAAC,gBAAgB,EAAE,UAAU,CAAC,GAAG;IAAE,QAAQ,EAAE,SAAS,gBAAgB,EAAE,EAAE,CAAA;CAAE,CAElF;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,QAAQ,GAAG,gBAAgB,CAE1D"}
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/types.d.cts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/types.d.cts
new file mode 100644
index 00000000..729c2c32
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/types.d.cts
@@ -0,0 +1,107 @@
+import type { SourceMapSegment } from './sourcemap-segment.cts';
+import type { GREATEST_LOWER_BOUND, LEAST_UPPER_BOUND, TraceMap } from './trace-mapping.cts';
+export interface SourceMapV3 {
+ file?: string | null;
+ names: string[];
+ sourceRoot?: string;
+ sources: (string | null)[];
+ sourcesContent?: (string | null)[];
+ version: 3;
+ ignoreList?: number[];
+}
+export interface EncodedSourceMap extends SourceMapV3 {
+ mappings: string;
+}
+export interface DecodedSourceMap extends SourceMapV3 {
+ mappings: SourceMapSegment[][];
+}
+export interface Section {
+ offset: {
+ line: number;
+ column: number;
+ };
+ map: EncodedSourceMap | DecodedSourceMap | SectionedSourceMap;
+}
+export interface SectionedSourceMap {
+ file?: string | null;
+ sections: Section[];
+ version: 3;
+}
+export type OriginalMapping = {
+ source: string | null;
+ line: number;
+ column: number;
+ name: string | null;
+};
+export type InvalidOriginalMapping = {
+ source: null;
+ line: null;
+ column: null;
+ name: null;
+};
+export type GeneratedMapping = {
+ line: number;
+ column: number;
+};
+export type InvalidGeneratedMapping = {
+ line: null;
+ column: null;
+};
+export type Bias = typeof GREATEST_LOWER_BOUND | typeof LEAST_UPPER_BOUND;
+export type XInput = {
+ x_google_ignoreList?: SourceMapV3['ignoreList'];
+};
+export type EncodedSourceMapXInput = EncodedSourceMap & XInput;
+export type DecodedSourceMapXInput = DecodedSourceMap & XInput;
+export type SectionedSourceMapXInput = Omit & {
+ sections: SectionXInput[];
+};
+export type SectionXInput = Omit & {
+ map: SectionedSourceMapInput;
+};
+export type SourceMapInput = string | EncodedSourceMapXInput | DecodedSourceMapXInput | TraceMap;
+export type SectionedSourceMapInput = SourceMapInput | SectionedSourceMapXInput;
+export type Needle = {
+ line: number;
+ column: number;
+ bias?: Bias;
+};
+export type SourceNeedle = {
+ source: string;
+ line: number;
+ column: number;
+ bias?: Bias;
+};
+export type EachMapping = {
+ generatedLine: number;
+ generatedColumn: number;
+ source: null;
+ originalLine: null;
+ originalColumn: null;
+ name: null;
+} | {
+ generatedLine: number;
+ generatedColumn: number;
+ source: string | null;
+ originalLine: number;
+ originalColumn: number;
+ name: string | null;
+};
+export declare abstract class SourceMap {
+ version: SourceMapV3['version'];
+ file: SourceMapV3['file'];
+ names: SourceMapV3['names'];
+ sourceRoot: SourceMapV3['sourceRoot'];
+ sources: SourceMapV3['sources'];
+ sourcesContent: SourceMapV3['sourcesContent'];
+ resolvedSources: SourceMapV3['sources'];
+ ignoreList: SourceMapV3['ignoreList'];
+}
+export type Ro = T extends Array ? V[] | Readonly | RoArray | Readonly> : T extends object ? T | Readonly | RoObject | Readonly> : T;
+type RoArray = Ro[];
+type RoObject = {
+ [K in keyof T]: T[K] | Ro;
+};
+export declare function parse(map: T): Exclude;
+export {};
+//# sourceMappingURL=types.d.ts.map
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/types.d.cts.map b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/types.d.cts.map
new file mode 100644
index 00000000..92247839
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/types.d.cts.map
@@ -0,0 +1 @@
+{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,KAAK,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAEzF,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC;IAC3B,cAAc,CAAC,EAAE,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC;IACnC,OAAO,EAAE,CAAC,CAAC;IACX,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,gBAAiB,SAAQ,WAAW;IACnD,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,gBAAiB,SAAQ,WAAW;IACnD,QAAQ,EAAE,gBAAgB,EAAE,EAAE,CAAC;CAChC;AAED,MAAM,WAAW,OAAO;IACtB,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IACzC,GAAG,EAAE,gBAAgB,GAAG,gBAAgB,GAAG,kBAAkB,CAAC;CAC/D;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,OAAO,EAAE,CAAC,CAAC;CACZ;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,MAAM,EAAE,IAAI,CAAC;IACb,IAAI,EAAE,IAAI,CAAC;IACX,MAAM,EAAE,IAAI,CAAC;IACb,IAAI,EAAE,IAAI,CAAC;CACZ,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AACF,MAAM,MAAM,uBAAuB,GAAG;IACpC,IAAI,EAAE,IAAI,CAAC;IACX,MAAM,EAAE,IAAI,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,IAAI,GAAG,OAAO,oBAAoB,GAAG,OAAO,iBAAiB,CAAC;AAE1E,MAAM,MAAM,MAAM,GAAG;IAAE,mBAAmB,CAAC,EAAE,WAAW,CAAC,YAAY,CAAC,CAAA;CAAE,CAAC;AACzE,MAAM,MAAM,sBAAsB,GAAG,gBAAgB,GAAG,MAAM,CAAC;AAC/D,MAAM,MAAM,sBAAsB,GAAG,gBAAgB,GAAG,MAAM,CAAC;AAC/D,MAAM,MAAM,wBAAwB,GAAG,IAAI,CAAC,kBAAkB,EAAE,UAAU,CAAC,GAAG;IAC5E,QAAQ,EAAE,aAAa,EAAE,CAAC;CAC3B,CAAC;AACF,MAAM,MAAM,aAAa,GAAG,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,GAAG;IACjD,GAAG,EAAE,uBAAuB,CAAC;CAC9B,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,sBAAsB,GAAG,sBAAsB,GAAG,QAAQ,CAAC;AACjG,MAAM,MAAM,uBAAuB,GAAG,cAAc,GAAG,wBAAwB,CAAC;AAEhF,MAAM,MAAM,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,IAAI,CAAA;CAAE,CAAC;AACnE,MAAM,MAAM,YAAY,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,IAAI,CAAA;CAAE,CAAC;AAEzF,MAAM,MAAM,WAAW,GACnB;IACE,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,IAAI,CAAC;IACb,YAAY,EAAE,IAAI,CAAC;IACnB,cAAc,EAAE,IAAI,CAAC;IACrB,IAAI,EAAE,IAAI,CAAC;CACZ,GACD;IACE,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB,CAAC;AAEN,8BAAsB,SAAS;IACrB,OAAO,EAAE,WAAW,CAAC,SAAS,CAAC,CAAC;IAChC,IAAI,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC1B,KAAK,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;IAC5B,UAAU,EAAE,WAAW,CAAC,YAAY,CAAC,CAAC;IACtC,OAAO,EAAE,WAAW,CAAC,SAAS,CAAC,CAAC;IAChC,cAAc,EAAE,WAAW,CAAC,gBAAgB,CAAC,CAAC;IAC9C,eAAe,EAAE,WAAW,CAAC,SAAS,CAAC,CAAC;IACxC,UAAU,EAAE,WAAW,CAAC,YAAY,CAAC,CAAC;CAC/C;AAED,MAAM,MAAM,EAAE,CAAC,CAAC,IACd,CAAC,SAAS,KAAK,CAAC,MAAM,CAAC,CAAC,GACpB,CAAC,EAAE,GAAG,QAAQ,CAAC,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,GACvD,CAAC,SAAS,MAAM,GACd,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,GACrD,CAAC,CAAC;AACV,KAAK,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;AAC1B,KAAK,QAAQ,CAAC,CAAC,IAAI;KAAG,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CAAE,CAAC;AAEvD,wBAAgB,KAAK,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,OAAO,CAAC,CAAC,EAAE,MAAM,CAAC,CAEnD"}
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/types.d.mts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/types.d.mts
new file mode 100644
index 00000000..a26d1866
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/types.d.mts
@@ -0,0 +1,107 @@
+import type { SourceMapSegment } from './sourcemap-segment.mts';
+import type { GREATEST_LOWER_BOUND, LEAST_UPPER_BOUND, TraceMap } from './trace-mapping.mts';
+export interface SourceMapV3 {
+ file?: string | null;
+ names: string[];
+ sourceRoot?: string;
+ sources: (string | null)[];
+ sourcesContent?: (string | null)[];
+ version: 3;
+ ignoreList?: number[];
+}
+export interface EncodedSourceMap extends SourceMapV3 {
+ mappings: string;
+}
+export interface DecodedSourceMap extends SourceMapV3 {
+ mappings: SourceMapSegment[][];
+}
+export interface Section {
+ offset: {
+ line: number;
+ column: number;
+ };
+ map: EncodedSourceMap | DecodedSourceMap | SectionedSourceMap;
+}
+export interface SectionedSourceMap {
+ file?: string | null;
+ sections: Section[];
+ version: 3;
+}
+export type OriginalMapping = {
+ source: string | null;
+ line: number;
+ column: number;
+ name: string | null;
+};
+export type InvalidOriginalMapping = {
+ source: null;
+ line: null;
+ column: null;
+ name: null;
+};
+export type GeneratedMapping = {
+ line: number;
+ column: number;
+};
+export type InvalidGeneratedMapping = {
+ line: null;
+ column: null;
+};
+export type Bias = typeof GREATEST_LOWER_BOUND | typeof LEAST_UPPER_BOUND;
+export type XInput = {
+ x_google_ignoreList?: SourceMapV3['ignoreList'];
+};
+export type EncodedSourceMapXInput = EncodedSourceMap & XInput;
+export type DecodedSourceMapXInput = DecodedSourceMap & XInput;
+export type SectionedSourceMapXInput = Omit & {
+ sections: SectionXInput[];
+};
+export type SectionXInput = Omit & {
+ map: SectionedSourceMapInput;
+};
+export type SourceMapInput = string | EncodedSourceMapXInput | DecodedSourceMapXInput | TraceMap;
+export type SectionedSourceMapInput = SourceMapInput | SectionedSourceMapXInput;
+export type Needle = {
+ line: number;
+ column: number;
+ bias?: Bias;
+};
+export type SourceNeedle = {
+ source: string;
+ line: number;
+ column: number;
+ bias?: Bias;
+};
+export type EachMapping = {
+ generatedLine: number;
+ generatedColumn: number;
+ source: null;
+ originalLine: null;
+ originalColumn: null;
+ name: null;
+} | {
+ generatedLine: number;
+ generatedColumn: number;
+ source: string | null;
+ originalLine: number;
+ originalColumn: number;
+ name: string | null;
+};
+export declare abstract class SourceMap {
+ version: SourceMapV3['version'];
+ file: SourceMapV3['file'];
+ names: SourceMapV3['names'];
+ sourceRoot: SourceMapV3['sourceRoot'];
+ sources: SourceMapV3['sources'];
+ sourcesContent: SourceMapV3['sourcesContent'];
+ resolvedSources: SourceMapV3['sources'];
+ ignoreList: SourceMapV3['ignoreList'];
+}
+export type Ro = T extends Array ? V[] | Readonly | RoArray | Readonly> : T extends object ? T | Readonly | RoObject | Readonly> : T;
+type RoArray = Ro[];
+type RoObject = {
+ [K in keyof T]: T[K] | Ro;
+};
+export declare function parse(map: T): Exclude;
+export {};
+//# sourceMappingURL=types.d.ts.map
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/types.d.mts.map b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/types.d.mts.map
new file mode 100644
index 00000000..92247839
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@jridgewell/trace-mapping/types/types.d.mts.map
@@ -0,0 +1 @@
+{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,KAAK,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAEzF,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC;IAC3B,cAAc,CAAC,EAAE,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC;IACnC,OAAO,EAAE,CAAC,CAAC;IACX,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,gBAAiB,SAAQ,WAAW;IACnD,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,gBAAiB,SAAQ,WAAW;IACnD,QAAQ,EAAE,gBAAgB,EAAE,EAAE,CAAC;CAChC;AAED,MAAM,WAAW,OAAO;IACtB,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IACzC,GAAG,EAAE,gBAAgB,GAAG,gBAAgB,GAAG,kBAAkB,CAAC;CAC/D;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,OAAO,EAAE,CAAC,CAAC;CACZ;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,MAAM,EAAE,IAAI,CAAC;IACb,IAAI,EAAE,IAAI,CAAC;IACX,MAAM,EAAE,IAAI,CAAC;IACb,IAAI,EAAE,IAAI,CAAC;CACZ,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AACF,MAAM,MAAM,uBAAuB,GAAG;IACpC,IAAI,EAAE,IAAI,CAAC;IACX,MAAM,EAAE,IAAI,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,IAAI,GAAG,OAAO,oBAAoB,GAAG,OAAO,iBAAiB,CAAC;AAE1E,MAAM,MAAM,MAAM,GAAG;IAAE,mBAAmB,CAAC,EAAE,WAAW,CAAC,YAAY,CAAC,CAAA;CAAE,CAAC;AACzE,MAAM,MAAM,sBAAsB,GAAG,gBAAgB,GAAG,MAAM,CAAC;AAC/D,MAAM,MAAM,sBAAsB,GAAG,gBAAgB,GAAG,MAAM,CAAC;AAC/D,MAAM,MAAM,wBAAwB,GAAG,IAAI,CAAC,kBAAkB,EAAE,UAAU,CAAC,GAAG;IAC5E,QAAQ,EAAE,aAAa,EAAE,CAAC;CAC3B,CAAC;AACF,MAAM,MAAM,aAAa,GAAG,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,GAAG;IACjD,GAAG,EAAE,uBAAuB,CAAC;CAC9B,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,sBAAsB,GAAG,sBAAsB,GAAG,QAAQ,CAAC;AACjG,MAAM,MAAM,uBAAuB,GAAG,cAAc,GAAG,wBAAwB,CAAC;AAEhF,MAAM,MAAM,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,IAAI,CAAA;CAAE,CAAC;AACnE,MAAM,MAAM,YAAY,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,IAAI,CAAA;CAAE,CAAC;AAEzF,MAAM,MAAM,WAAW,GACnB;IACE,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,IAAI,CAAC;IACb,YAAY,EAAE,IAAI,CAAC;IACnB,cAAc,EAAE,IAAI,CAAC;IACrB,IAAI,EAAE,IAAI,CAAC;CACZ,GACD;IACE,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB,CAAC;AAEN,8BAAsB,SAAS;IACrB,OAAO,EAAE,WAAW,CAAC,SAAS,CAAC,CAAC;IAChC,IAAI,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC1B,KAAK,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;IAC5B,UAAU,EAAE,WAAW,CAAC,YAAY,CAAC,CAAC;IACtC,OAAO,EAAE,WAAW,CAAC,SAAS,CAAC,CAAC;IAChC,cAAc,EAAE,WAAW,CAAC,gBAAgB,CAAC,CAAC;IAC9C,eAAe,EAAE,WAAW,CAAC,SAAS,CAAC,CAAC;IACxC,UAAU,EAAE,WAAW,CAAC,YAAY,CAAC,CAAC;CAC/C;AAED,MAAM,MAAM,EAAE,CAAC,CAAC,IACd,CAAC,SAAS,KAAK,CAAC,MAAM,CAAC,CAAC,GACpB,CAAC,EAAE,GAAG,QAAQ,CAAC,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,GACvD,CAAC,SAAS,MAAM,GACd,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,GACrD,CAAC,CAAC;AACV,KAAK,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;AAC1B,KAAK,QAAQ,CAAC,CAAC,IAAI;KAAG,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CAAE,CAAC;AAEvD,wBAAgB,KAAK,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,OAAO,CAAC,CAAC,EAAE,MAAM,CAAC,CAEnD"}
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@modelcontextprotocol/sdk/LICENSE b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@modelcontextprotocol/sdk/LICENSE
new file mode 100644
index 00000000..3d484354
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@modelcontextprotocol/sdk/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024 Anthropic, PBC
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@modelcontextprotocol/sdk/README.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@modelcontextprotocol/sdk/README.md
new file mode 100644
index 00000000..7a553ebb
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@modelcontextprotocol/sdk/README.md
@@ -0,0 +1,170 @@
+# MCP TypeScript SDK [](https://www.npmjs.com/package/@modelcontextprotocol/sdk) [](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/LICENSE)
+
+
+Table of Contents
+
+- [Overview](#overview)
+- [Installation](#installation)
+- [Quick Start](#quick-start)
+- [Core Concepts](#core-concepts)
+- [Examples](#examples)
+- [Documentation](#documentation)
+- [Contributing](#contributing)
+- [License](#license)
+
+
+
+## Overview
+
+The Model Context Protocol allows applications to provide context for LLMs in a standardized way, separating the concerns of providing context from the actual LLM interaction. This TypeScript SDK implements
+[the full MCP specification](https://modelcontextprotocol.io/specification/draft), making it easy to:
+
+- Create MCP servers that expose resources, prompts and tools
+- Build MCP clients that can connect to any MCP server
+- Use standard transports like stdio and Streamable HTTP
+
+## Installation
+
+```bash
+npm install @modelcontextprotocol/sdk zod
+```
+
+This SDK has a **required peer dependency** on `zod` for schema validation. The SDK internally imports from `zod/v4`, but maintains backwards compatibility with projects using Zod v3.25 or later. You can use either API in your code by importing from `zod/v3` or `zod/v4`:
+
+## Quick Start
+
+To see the SDK in action end-to-end, start from the runnable examples in `src/examples`:
+
+1. **Install dependencies** (from the SDK repo root):
+
+ ```bash
+ npm install
+ ```
+
+2. **Run the example Streamable HTTP server**:
+
+ ```bash
+ npx tsx src/examples/server/simpleStreamableHttp.ts
+ ```
+
+3. **Run the interactive client in another terminal**:
+
+ ```bash
+ npx tsx src/examples/client/simpleStreamableHttp.ts
+ ```
+
+This pair of examples demonstrates tools, resources, prompts, sampling, elicitation, tasks and logging. For a guided walkthrough and variations (stateless servers, JSON-only responses, SSE compatibility, OAuth, etc.), see [docs/server.md](docs/server.md) and
+[docs/client.md](docs/client.md).
+
+## Core Concepts
+
+### Servers and transports
+
+An MCP server is typically created with `McpServer` and connected to a transport such as Streamable HTTP or stdio. The SDK supports:
+
+- **Streamable HTTP** for remote servers (recommended).
+- **HTTP + SSE** for backwards compatibility only.
+- **stdio** for local, process-spawned integrations.
+
+Runnable server examples live under `src/examples/server` and are documented in [docs/server.md](docs/server.md).
+
+### Tools, resources, prompts
+
+- **Tools** let LLMs ask your server to take actions (computation, side effects, network calls).
+- **Resources** expose read-only data that clients can surface to users or models.
+- **Prompts** are reusable templates that help users talk to models in a consistent way.
+
+The detailed APIs, including `ResourceTemplate`, completions, and display-name metadata, are covered in [docs/server.md](docs/server.md#tools-resources-and-prompts), with runnable implementations in [`simpleStreamableHttp.ts`](src/examples/server/simpleStreamableHttp.ts).
+
+### Capabilities: sampling, elicitation, and tasks
+
+The SDK includes higher-level capabilities for richer workflows:
+
+- **Sampling**: server-side tools can ask connected clients to run LLM completions.
+- **Form elicitation**: tools can request non-sensitive input via structured forms.
+- **URL elicitation**: servers can ask users to complete secure flows in a browser (e.g., API key entry, payments, OAuth).
+- **Tasks (experimental)**: long-running tool calls can be turned into tasks that you poll or resume later.
+
+Conceptual overviews and links to runnable examples are in:
+
+- [docs/capabilities.md](docs/capabilities.md)
+
+Key example servers include:
+
+- [`toolWithSampleServer.ts`](src/examples/server/toolWithSampleServer.ts)
+- [`elicitationFormExample.ts`](src/examples/server/elicitationFormExample.ts)
+- [`elicitationUrlExample.ts`](src/examples/server/elicitationUrlExample.ts)
+
+### Clients
+
+The high-level `Client` class connects to MCP servers over different transports and exposes helpers like `listTools`, `callTool`, `listResources`, `readResource`, `listPrompts`, and `getPrompt`.
+
+Runnable clients live under `src/examples/client` and are described in [docs/client.md](docs/client.md), including:
+
+- Interactive Streamable HTTP client ([`simpleStreamableHttp.ts`](src/examples/client/simpleStreamableHttp.ts))
+- Streamable HTTP client with SSE fallback ([`streamableHttpWithSseFallbackClient.ts`](src/examples/client/streamableHttpWithSseFallbackClient.ts))
+- OAuth-enabled clients and polling/parallel examples
+
+### Node.js Web Crypto (globalThis.crypto) compatibility
+
+Some parts of the SDK (for example, JWT-based client authentication in `auth-extensions.ts` via `jose`) rely on the Web Crypto API exposed as `globalThis.crypto`.
+
+See [docs/faq.md](docs/faq.md) for details on supported Node.js versions and how to polyfill `globalThis.crypto` when running on older Node.js runtimes.
+
+## Examples
+
+The SDK ships runnable examples under `src/examples`. Use these tables to find the scenario you care about and jump straight to the corresponding code and docs.
+
+### Server examples
+
+| Scenario | Description | Example file(s) | Related docs |
+| --------------------------------------------------- | ------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
+| Streamable HTTP server (stateful) | Feature-rich server with tools, resources, prompts, logging, tasks, sampling, and optional OAuth. | [`simpleStreamableHttp.ts`](src/examples/server/simpleStreamableHttp.ts) | [`server.md`](docs/server.md), [`capabilities.md`](docs/capabilities.md) |
+| Streamable HTTP server (stateless) | No session tracking; good for simple API-style servers. | [`simpleStatelessStreamableHttp.ts`](src/examples/server/simpleStatelessStreamableHttp.ts) | [`server.md`](docs/server.md) |
+| JSON response mode (no SSE) | Streamable HTTP with JSON responses only and limited notifications. | [`jsonResponseStreamableHttp.ts`](src/examples/server/jsonResponseStreamableHttp.ts) | [`server.md`](docs/server.md) |
+| Server notifications over Streamable HTTP | Demonstrates server-initiated notifications using SSE with Streamable HTTP. | [`standaloneSseWithGetStreamableHttp.ts`](src/examples/server/standaloneSseWithGetStreamableHttp.ts) | [`server.md`](docs/server.md) |
+| Deprecated HTTP+SSE server | Legacy HTTP+SSE transport for backwards-compatibility testing. | [`simpleSseServer.ts`](src/examples/server/simpleSseServer.ts) | [`server.md`](docs/server.md) |
+| Backwards-compatible server (Streamable HTTP + SSE) | Single server that supports both Streamable HTTP and legacy SSE clients. | [`sseAndStreamableHttpCompatibleServer.ts`](src/examples/server/sseAndStreamableHttpCompatibleServer.ts) | [`server.md`](docs/server.md) |
+| Form elicitation server | Uses form elicitation to collect non-sensitive user input. | [`elicitationFormExample.ts`](src/examples/server/elicitationFormExample.ts) | [`capabilities.md`](docs/capabilities.md#elicitation) |
+| URL elicitation server | Demonstrates URL-mode elicitation in an OAuth-protected server. | [`elicitationUrlExample.ts`](src/examples/server/elicitationUrlExample.ts) | [`capabilities.md`](docs/capabilities.md#elicitation) |
+| Sampling and tasks server | Combines tools, logging, sampling, and experimental task-based execution. | [`toolWithSampleServer.ts`](src/examples/server/toolWithSampleServer.ts) | [`capabilities.md`](docs/capabilities.md) |
+| OAuth demo authorization server | In-memory OAuth provider used with the example servers. | [`demoInMemoryOAuthProvider.ts`](src/examples/server/demoInMemoryOAuthProvider.ts) | [`server.md`](docs/server.md) |
+
+### Client examples
+
+| Scenario | Description | Example file(s) | Related docs |
+| --------------------------------------------------- | ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ |
+| Interactive Streamable HTTP client | CLI client that exercises tools, resources, prompts, elicitation, and tasks. | [`simpleStreamableHttp.ts`](src/examples/client/simpleStreamableHttp.ts) | [`client.md`](docs/client.md) |
+| Backwards-compatible client (Streamable HTTP → SSE) | Tries Streamable HTTP first, then falls back to SSE on 4xx responses. | [`streamableHttpWithSseFallbackClient.ts`](src/examples/client/streamableHttpWithSseFallbackClient.ts) | [`client.md`](docs/client.md), [`server.md`](docs/server.md) |
+| SSE polling client | Polls a legacy SSE server and demonstrates notification handling. | [`ssePollingClient.ts`](src/examples/client/ssePollingClient.ts) | [`client.md`](docs/client.md) |
+| Parallel tool calls client | Shows how to run multiple tool calls in parallel. | [`parallelToolCallsClient.ts`](src/examples/client/parallelToolCallsClient.ts) | [`client.md`](docs/client.md) |
+| Multiple clients in parallel | Demonstrates connecting multiple clients concurrently to the same server. | [`multipleClientsParallel.ts`](src/examples/client/multipleClientsParallel.ts) | [`client.md`](docs/client.md) |
+| OAuth clients | Examples of client_credentials (basic and private_key_jwt) and reusable providers. | [`simpleOAuthClient.ts`](src/examples/client/simpleOAuthClient.ts), [`simpleOAuthClientProvider.ts`](src/examples/client/simpleOAuthClientProvider.ts), [`simpleClientCredentials.ts`](src/examples/client/simpleClientCredentials.ts) | [`client.md`](docs/client.md) |
+| URL elicitation client | Works with the URL elicitation server to drive secure browser flows. | [`elicitationUrlExample.ts`](src/examples/client/elicitationUrlExample.ts) | [`capabilities.md`](docs/capabilities.md#elicitation) |
+
+Shared utilities:
+
+- In-memory event store for resumability: [`inMemoryEventStore.ts`](src/examples/shared/inMemoryEventStore.ts) (see [`server.md`](docs/server.md)).
+
+For more details on how to run these examples (including recommended commands and deployment diagrams), see `src/examples/README.md`.
+
+## Documentation
+
+- Local SDK docs:
+ - [docs/server.md](docs/server.md) – building and running MCP servers, transports, tools/resources/prompts, CORS, DNS rebinding, and multi-node deployment.
+ - [docs/client.md](docs/client.md) – using the high-level client, transports, backwards compatibility, and OAuth helpers.
+ - [docs/capabilities.md](docs/capabilities.md) – sampling, elicitation (form and URL), and experimental task-based execution.
+ - [docs/protocol.md](docs/protocol.md) – protocol features: ping, progress, cancellation, pagination, capability negotiation, and JSON Schema.
+ - [docs/faq.md](docs/faq.md) – environment and troubleshooting FAQs (including Node.js Web Crypto support).
+- External references:
+ - [Model Context Protocol documentation](https://modelcontextprotocol.io)
+ - [MCP Specification](https://spec.modelcontextprotocol.io)
+ - [Example Servers](https://github.com/modelcontextprotocol/servers)
+
+## Contributing
+
+Issues and pull requests are welcome on GitHub at .
+
+## License
+
+This project is licensed under the MIT License—see the [LICENSE](LICENSE) file for details.
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@modelcontextprotocol/sdk/package.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@modelcontextprotocol/sdk/package.json
new file mode 100644
index 00000000..5d6c68e2
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@modelcontextprotocol/sdk/package.json
@@ -0,0 +1,155 @@
+{
+ "name": "@modelcontextprotocol/sdk",
+ "version": "1.27.1",
+ "description": "Model Context Protocol implementation for TypeScript",
+ "license": "MIT",
+ "author": "Anthropic, PBC (https://anthropic.com)",
+ "homepage": "https://modelcontextprotocol.io",
+ "bugs": "https://github.com/modelcontextprotocol/typescript-sdk/issues",
+ "type": "module",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/modelcontextprotocol/typescript-sdk.git"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "keywords": [
+ "modelcontextprotocol",
+ "mcp"
+ ],
+ "exports": {
+ ".": {
+ "import": "./dist/esm/index.js",
+ "require": "./dist/cjs/index.js"
+ },
+ "./client": {
+ "import": "./dist/esm/client/index.js",
+ "require": "./dist/cjs/client/index.js"
+ },
+ "./server": {
+ "import": "./dist/esm/server/index.js",
+ "require": "./dist/cjs/server/index.js"
+ },
+ "./validation": {
+ "import": "./dist/esm/validation/index.js",
+ "require": "./dist/cjs/validation/index.js"
+ },
+ "./validation/ajv": {
+ "import": "./dist/esm/validation/ajv-provider.js",
+ "require": "./dist/cjs/validation/ajv-provider.js"
+ },
+ "./validation/cfworker": {
+ "import": "./dist/esm/validation/cfworker-provider.js",
+ "require": "./dist/cjs/validation/cfworker-provider.js"
+ },
+ "./experimental": {
+ "import": "./dist/esm/experimental/index.js",
+ "require": "./dist/cjs/experimental/index.js"
+ },
+ "./experimental/tasks": {
+ "import": "./dist/esm/experimental/tasks/index.js",
+ "require": "./dist/cjs/experimental/tasks/index.js"
+ },
+ "./*": {
+ "import": "./dist/esm/*",
+ "require": "./dist/cjs/*"
+ }
+ },
+ "typesVersions": {
+ "*": {
+ "*": [
+ "./dist/esm/*"
+ ]
+ }
+ },
+ "files": [
+ "dist"
+ ],
+ "scripts": {
+ "fetch:spec-types": "tsx scripts/fetch-spec-types.ts",
+ "typecheck": "tsgo --noEmit",
+ "build": "npm run build:esm && npm run build:cjs",
+ "build:esm": "mkdir -p dist/esm && echo '{\"type\": \"module\"}' > dist/esm/package.json && tsc -p tsconfig.prod.json",
+ "build:esm:w": "npm run build:esm -- -w",
+ "build:cjs": "mkdir -p dist/cjs && echo '{\"type\": \"commonjs\"}' > dist/cjs/package.json && tsc -p tsconfig.cjs.json",
+ "build:cjs:w": "npm run build:cjs -- -w",
+ "examples:simple-server:w": "tsx --watch src/examples/server/simpleStreamableHttp.ts --oauth",
+ "prepack": "npm run build:esm && npm run build:cjs",
+ "lint": "eslint src/ && prettier --check .",
+ "lint:fix": "eslint src/ --fix && prettier --write .",
+ "check": "npm run typecheck && npm run lint",
+ "test": "vitest run",
+ "test:watch": "vitest",
+ "start": "npm run server",
+ "server": "tsx watch --clear-screen=false scripts/cli.ts server",
+ "client": "tsx scripts/cli.ts client",
+ "test:conformance:server": "test/conformance/scripts/run-server-conformance.sh --expected-failures test/conformance/conformance-baseline.yml",
+ "test:conformance:server:all": "test/conformance/scripts/run-server-conformance.sh --suite all --expected-failures test/conformance/conformance-baseline.yml",
+ "test:conformance:server:run": "npx tsx test/conformance/src/everythingServer.ts",
+ "test:conformance:client": "npx @modelcontextprotocol/conformance client --command 'npx tsx test/conformance/src/everythingClient.ts' --expected-failures test/conformance/conformance-baseline.yml",
+ "test:conformance:client:all": "npx @modelcontextprotocol/conformance client --command 'npx tsx test/conformance/src/everythingClient.ts' --suite all --expected-failures test/conformance/conformance-baseline.yml"
+ },
+ "dependencies": {
+ "@hono/node-server": "^1.19.9",
+ "ajv": "^8.17.1",
+ "ajv-formats": "^3.0.1",
+ "content-type": "^1.0.5",
+ "cors": "^2.8.5",
+ "cross-spawn": "^7.0.5",
+ "eventsource": "^3.0.2",
+ "eventsource-parser": "^3.0.0",
+ "express": "^5.2.1",
+ "express-rate-limit": "^8.2.1",
+ "hono": "^4.11.4",
+ "jose": "^6.1.3",
+ "json-schema-typed": "^8.0.2",
+ "pkce-challenge": "^5.0.0",
+ "raw-body": "^3.0.0",
+ "zod": "^3.25 || ^4.0",
+ "zod-to-json-schema": "^3.25.1"
+ },
+ "peerDependencies": {
+ "@cfworker/json-schema": "^4.1.1",
+ "zod": "^3.25 || ^4.0"
+ },
+ "peerDependenciesMeta": {
+ "@cfworker/json-schema": {
+ "optional": true
+ },
+ "zod": {
+ "optional": false
+ }
+ },
+ "devDependencies": {
+ "@cfworker/json-schema": "^4.1.1",
+ "@eslint/js": "^9.39.1",
+ "@modelcontextprotocol/conformance": "^0.1.14",
+ "@types/content-type": "^1.1.8",
+ "@types/cors": "^2.8.17",
+ "@types/cross-spawn": "^6.0.6",
+ "@types/eventsource": "^1.1.15",
+ "@types/express": "^5.0.0",
+ "@types/express-serve-static-core": "^5.1.0",
+ "@types/node": "^22.12.0",
+ "@types/supertest": "^6.0.2",
+ "@types/ws": "^8.5.12",
+ "@typescript/native-preview": "^7.0.0-dev.20251103.1",
+ "eslint": "^9.8.0",
+ "eslint-config-prettier": "^10.1.8",
+ "eslint-plugin-n": "^17.23.1",
+ "prettier": "3.6.2",
+ "supertest": "^7.0.0",
+ "tsx": "^4.16.5",
+ "typescript": "^5.5.4",
+ "typescript-eslint": "^8.48.1",
+ "vitest": "^4.0.8",
+ "ws": "^8.18.0"
+ },
+ "resolutions": {
+ "strip-ansi": "6.0.1"
+ },
+ "overrides": {
+ "qs": "6.14.1"
+ }
+}
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/.editorconfig b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/.editorconfig
new file mode 100644
index 00000000..b1401639
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/.editorconfig
@@ -0,0 +1,14 @@
+# EditorConfig is awesome: http://EditorConfig.org
+
+# top-most EditorConfig file
+root = true
+
+# Copied from Node.js to ease compatibility in PR.
+[*]
+charset = utf-8
+end_of_line = lf
+indent_size = 2
+indent_style = space
+insert_final_newline = true
+trim_trailing_whitespace = true
+quote_type = single
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/CHANGELOG.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/CHANGELOG.md
new file mode 100644
index 00000000..2adc7d32
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/CHANGELOG.md
@@ -0,0 +1,147 @@
+# Changelog
+
+## [0.11.0](https://github.com/pkgjs/parseargs/compare/v0.10.0...v0.11.0) (2022-10-08)
+
+
+### Features
+
+* add `default` option parameter ([#142](https://github.com/pkgjs/parseargs/issues/142)) ([cd20847](https://github.com/pkgjs/parseargs/commit/cd20847a00b2f556aa9c085ac83b942c60868ec1))
+
+## [0.10.0](https://github.com/pkgjs/parseargs/compare/v0.9.1...v0.10.0) (2022-07-21)
+
+
+### Features
+
+* add parsed meta-data to returned properties ([#129](https://github.com/pkgjs/parseargs/issues/129)) ([91bfb4d](https://github.com/pkgjs/parseargs/commit/91bfb4d3f7b6937efab1b27c91c45d1205f1497e))
+
+## [0.9.1](https://github.com/pkgjs/parseargs/compare/v0.9.0...v0.9.1) (2022-06-20)
+
+
+### Bug Fixes
+
+* **runtime:** support node 14+ ([#135](https://github.com/pkgjs/parseargs/issues/135)) ([6a1c5a6](https://github.com/pkgjs/parseargs/commit/6a1c5a6f7cadf2f035e004027e2742e3c4ce554b))
+
+## [0.9.0](https://github.com/pkgjs/parseargs/compare/v0.8.0...v0.9.0) (2022-05-23)
+
+
+### ⚠ BREAKING CHANGES
+
+* drop handling of electron arguments (#121)
+
+### Code Refactoring
+
+* drop handling of electron arguments ([#121](https://github.com/pkgjs/parseargs/issues/121)) ([a2ffd53](https://github.com/pkgjs/parseargs/commit/a2ffd537c244a062371522b955acb45a404fc9f2))
+
+## [0.8.0](https://github.com/pkgjs/parseargs/compare/v0.7.1...v0.8.0) (2022-05-16)
+
+
+### ⚠ BREAKING CHANGES
+
+* switch type:string option arguments to greedy, but with error for suspect cases in strict mode (#88)
+* positionals now opt-in when strict:true (#116)
+* create result.values with null prototype (#111)
+
+### Features
+
+* create result.values with null prototype ([#111](https://github.com/pkgjs/parseargs/issues/111)) ([9d539c3](https://github.com/pkgjs/parseargs/commit/9d539c3d57f269c160e74e0656ad4fa84ff92ec2))
+* positionals now opt-in when strict:true ([#116](https://github.com/pkgjs/parseargs/issues/116)) ([3643338](https://github.com/pkgjs/parseargs/commit/364333826b746e8a7dc5505b4b22fd19ac51df3b))
+* switch type:string option arguments to greedy, but with error for suspect cases in strict mode ([#88](https://github.com/pkgjs/parseargs/issues/88)) ([c2b5e72](https://github.com/pkgjs/parseargs/commit/c2b5e72161991dfdc535909f1327cc9b970fe7e8))
+
+### [0.7.1](https://github.com/pkgjs/parseargs/compare/v0.7.0...v0.7.1) (2022-04-15)
+
+
+### Bug Fixes
+
+* resist pollution ([#106](https://github.com/pkgjs/parseargs/issues/106)) ([ecf2dec](https://github.com/pkgjs/parseargs/commit/ecf2dece0a9f2a76d789384d5d71c68ffe64022a))
+
+## [0.7.0](https://github.com/pkgjs/parseargs/compare/v0.6.0...v0.7.0) (2022-04-13)
+
+
+### Features
+
+* Add strict mode to parser ([#74](https://github.com/pkgjs/parseargs/issues/74)) ([8267d02](https://github.com/pkgjs/parseargs/commit/8267d02083a87b8b8a71fcce08348d1e031ea91c))
+
+## [0.6.0](https://github.com/pkgjs/parseargs/compare/v0.5.0...v0.6.0) (2022-04-11)
+
+
+### ⚠ BREAKING CHANGES
+
+* rework results to remove redundant `flags` property and store value true for boolean options (#83)
+* switch to existing ERR_INVALID_ARG_VALUE (#97)
+
+### Code Refactoring
+
+* rework results to remove redundant `flags` property and store value true for boolean options ([#83](https://github.com/pkgjs/parseargs/issues/83)) ([be153db](https://github.com/pkgjs/parseargs/commit/be153dbed1d488cb7b6e27df92f601ba7337713d))
+* switch to existing ERR_INVALID_ARG_VALUE ([#97](https://github.com/pkgjs/parseargs/issues/97)) ([084a23f](https://github.com/pkgjs/parseargs/commit/084a23f9fde2da030b159edb1c2385f24579ce40))
+
+## [0.5.0](https://github.com/pkgjs/parseargs/compare/v0.4.0...v0.5.0) (2022-04-10)
+
+
+### ⚠ BREAKING CHANGES
+
+* Require type to be specified for each supplied option (#95)
+
+### Features
+
+* Require type to be specified for each supplied option ([#95](https://github.com/pkgjs/parseargs/issues/95)) ([02cd018](https://github.com/pkgjs/parseargs/commit/02cd01885b8aaa59f2db8308f2d4479e64340068))
+
+## [0.4.0](https://github.com/pkgjs/parseargs/compare/v0.3.0...v0.4.0) (2022-03-12)
+
+
+### ⚠ BREAKING CHANGES
+
+* parsing, revisit short option groups, add support for combined short and value (#75)
+* restructure configuration to take options bag (#63)
+
+### Code Refactoring
+
+* parsing, revisit short option groups, add support for combined short and value ([#75](https://github.com/pkgjs/parseargs/issues/75)) ([a92600f](https://github.com/pkgjs/parseargs/commit/a92600fa6c214508ab1e016fa55879a314f541af))
+* restructure configuration to take options bag ([#63](https://github.com/pkgjs/parseargs/issues/63)) ([b412095](https://github.com/pkgjs/parseargs/commit/b4120957d90e809ee8b607b06e747d3e6a6b213e))
+
+## [0.3.0](https://github.com/pkgjs/parseargs/compare/v0.2.0...v0.3.0) (2022-02-06)
+
+
+### Features
+
+* **parser:** support short-option groups ([#59](https://github.com/pkgjs/parseargs/issues/59)) ([882067b](https://github.com/pkgjs/parseargs/commit/882067bc2d7cbc6b796f8e5a079a99bc99d4e6ba))
+
+## [0.2.0](https://github.com/pkgjs/parseargs/compare/v0.1.1...v0.2.0) (2022-02-05)
+
+
+### Features
+
+* basic support for shorts ([#50](https://github.com/pkgjs/parseargs/issues/50)) ([a2f36d7](https://github.com/pkgjs/parseargs/commit/a2f36d7da4145af1c92f76806b7fe2baf6beeceb))
+
+
+### Bug Fixes
+
+* always store value for a=b ([#43](https://github.com/pkgjs/parseargs/issues/43)) ([a85e8dc](https://github.com/pkgjs/parseargs/commit/a85e8dc06379fd2696ee195cc625de8fac6aee42))
+* support single dash as positional ([#49](https://github.com/pkgjs/parseargs/issues/49)) ([d795bf8](https://github.com/pkgjs/parseargs/commit/d795bf877d068fd67aec381f30b30b63f97109ad))
+
+### [0.1.1](https://github.com/pkgjs/parseargs/compare/v0.1.0...v0.1.1) (2022-01-25)
+
+
+### Bug Fixes
+
+* only use arrays in results for multiples ([#42](https://github.com/pkgjs/parseargs/issues/42)) ([c357584](https://github.com/pkgjs/parseargs/commit/c357584847912506319ed34a0840080116f4fd65))
+
+## 0.1.0 (2022-01-22)
+
+
+### Features
+
+* expand scenarios covered by default arguments for environments ([#20](https://github.com/pkgjs/parseargs/issues/20)) ([582ada7](https://github.com/pkgjs/parseargs/commit/582ada7be0eca3a73d6e0bd016e7ace43449fa4c))
+* update readme and include contributing guidelines ([8edd6fc](https://github.com/pkgjs/parseargs/commit/8edd6fc863cd705f6fac732724159ebe8065a2b0))
+
+
+### Bug Fixes
+
+* do not strip excess leading dashes on long option names ([#21](https://github.com/pkgjs/parseargs/issues/21)) ([f848590](https://github.com/pkgjs/parseargs/commit/f848590ebf3249ed5979ff47e003fa6e1a8ec5c0))
+* name & readme ([3f057c1](https://github.com/pkgjs/parseargs/commit/3f057c1b158a1bdbe878c64b57460c58e56e465f))
+* package.json values ([9bac300](https://github.com/pkgjs/parseargs/commit/9bac300e00cd76c77076bf9e75e44f8929512da9))
+* update readme name ([957d8d9](https://github.com/pkgjs/parseargs/commit/957d8d96e1dcb48297c0a14345d44c0123b2883e))
+
+
+### Build System
+
+* first release as minor ([421c6e2](https://github.com/pkgjs/parseargs/commit/421c6e2569a8668ad14fac5a5af5be60479a7571))
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/LICENSE b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/LICENSE
new file mode 100644
index 00000000..261eeb9e
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/README.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/README.md
new file mode 100644
index 00000000..0a041927
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/README.md
@@ -0,0 +1,413 @@
+
+# parseArgs
+
+[![Coverage][coverage-image]][coverage-url]
+
+Polyfill of `util.parseArgs()`
+
+## `util.parseArgs([config])`
+
+
+
+> Stability: 1 - Experimental
+
+* `config` {Object} Used to provide arguments for parsing and to configure
+ the parser. `config` supports the following properties:
+ * `args` {string\[]} array of argument strings. **Default:** `process.argv`
+ with `execPath` and `filename` removed.
+ * `options` {Object} Used to describe arguments known to the parser.
+ Keys of `options` are the long names of options and values are an
+ {Object} accepting the following properties:
+ * `type` {string} Type of argument, which must be either `boolean` or `string`.
+ * `multiple` {boolean} Whether this option can be provided multiple
+ times. If `true`, all values will be collected in an array. If
+ `false`, values for the option are last-wins. **Default:** `false`.
+ * `short` {string} A single character alias for the option.
+ * `default` {string | boolean | string\[] | boolean\[]} The default option
+ value when it is not set by args. It must be of the same type as the
+ the `type` property. When `multiple` is `true`, it must be an array.
+ * `strict` {boolean} Should an error be thrown when unknown arguments
+ are encountered, or when arguments are passed that do not match the
+ `type` configured in `options`.
+ **Default:** `true`.
+ * `allowPositionals` {boolean} Whether this command accepts positional
+ arguments.
+ **Default:** `false` if `strict` is `true`, otherwise `true`.
+ * `tokens` {boolean} Return the parsed tokens. This is useful for extending
+ the built-in behavior, from adding additional checks through to reprocessing
+ the tokens in different ways.
+ **Default:** `false`.
+
+* Returns: {Object} The parsed command line arguments:
+ * `values` {Object} A mapping of parsed option names with their {string}
+ or {boolean} values.
+ * `positionals` {string\[]} Positional arguments.
+ * `tokens` {Object\[] | undefined} See [parseArgs tokens](#parseargs-tokens)
+ section. Only returned if `config` includes `tokens: true`.
+
+Provides a higher level API for command-line argument parsing than interacting
+with `process.argv` directly. Takes a specification for the expected arguments
+and returns a structured object with the parsed options and positionals.
+
+```mjs
+import { parseArgs } from 'node:util';
+const args = ['-f', '--bar', 'b'];
+const options = {
+ foo: {
+ type: 'boolean',
+ short: 'f'
+ },
+ bar: {
+ type: 'string'
+ }
+};
+const {
+ values,
+ positionals
+} = parseArgs({ args, options });
+console.log(values, positionals);
+// Prints: [Object: null prototype] { foo: true, bar: 'b' } []
+```
+
+```cjs
+const { parseArgs } = require('node:util');
+const args = ['-f', '--bar', 'b'];
+const options = {
+ foo: {
+ type: 'boolean',
+ short: 'f'
+ },
+ bar: {
+ type: 'string'
+ }
+};
+const {
+ values,
+ positionals
+} = parseArgs({ args, options });
+console.log(values, positionals);
+// Prints: [Object: null prototype] { foo: true, bar: 'b' } []
+```
+
+`util.parseArgs` is experimental and behavior may change. Join the
+conversation in [pkgjs/parseargs][] to contribute to the design.
+
+### `parseArgs` `tokens`
+
+Detailed parse information is available for adding custom behaviours by
+specifying `tokens: true` in the configuration.
+The returned tokens have properties describing:
+
+* all tokens
+ * `kind` {string} One of 'option', 'positional', or 'option-terminator'.
+ * `index` {number} Index of element in `args` containing token. So the
+ source argument for a token is `args[token.index]`.
+* option tokens
+ * `name` {string} Long name of option.
+ * `rawName` {string} How option used in args, like `-f` of `--foo`.
+ * `value` {string | undefined} Option value specified in args.
+ Undefined for boolean options.
+ * `inlineValue` {boolean | undefined} Whether option value specified inline,
+ like `--foo=bar`.
+* positional tokens
+ * `value` {string} The value of the positional argument in args (i.e. `args[index]`).
+* option-terminator token
+
+The returned tokens are in the order encountered in the input args. Options
+that appear more than once in args produce a token for each use. Short option
+groups like `-xy` expand to a token for each option. So `-xxx` produces
+three tokens.
+
+For example to use the returned tokens to add support for a negated option
+like `--no-color`, the tokens can be reprocessed to change the value stored
+for the negated option.
+
+```mjs
+import { parseArgs } from 'node:util';
+
+const options = {
+ 'color': { type: 'boolean' },
+ 'no-color': { type: 'boolean' },
+ 'logfile': { type: 'string' },
+ 'no-logfile': { type: 'boolean' },
+};
+const { values, tokens } = parseArgs({ options, tokens: true });
+
+// Reprocess the option tokens and overwrite the returned values.
+tokens
+ .filter((token) => token.kind === 'option')
+ .forEach((token) => {
+ if (token.name.startsWith('no-')) {
+ // Store foo:false for --no-foo
+ const positiveName = token.name.slice(3);
+ values[positiveName] = false;
+ delete values[token.name];
+ } else {
+ // Resave value so last one wins if both --foo and --no-foo.
+ values[token.name] = token.value ?? true;
+ }
+ });
+
+const color = values.color;
+const logfile = values.logfile ?? 'default.log';
+
+console.log({ logfile, color });
+```
+
+```cjs
+const { parseArgs } = require('node:util');
+
+const options = {
+ 'color': { type: 'boolean' },
+ 'no-color': { type: 'boolean' },
+ 'logfile': { type: 'string' },
+ 'no-logfile': { type: 'boolean' },
+};
+const { values, tokens } = parseArgs({ options, tokens: true });
+
+// Reprocess the option tokens and overwrite the returned values.
+tokens
+ .filter((token) => token.kind === 'option')
+ .forEach((token) => {
+ if (token.name.startsWith('no-')) {
+ // Store foo:false for --no-foo
+ const positiveName = token.name.slice(3);
+ values[positiveName] = false;
+ delete values[token.name];
+ } else {
+ // Resave value so last one wins if both --foo and --no-foo.
+ values[token.name] = token.value ?? true;
+ }
+ });
+
+const color = values.color;
+const logfile = values.logfile ?? 'default.log';
+
+console.log({ logfile, color });
+```
+
+Example usage showing negated options, and when an option is used
+multiple ways then last one wins.
+
+```console
+$ node negate.js
+{ logfile: 'default.log', color: undefined }
+$ node negate.js --no-logfile --no-color
+{ logfile: false, color: false }
+$ node negate.js --logfile=test.log --color
+{ logfile: 'test.log', color: true }
+$ node negate.js --no-logfile --logfile=test.log --color --no-color
+{ logfile: 'test.log', color: false }
+```
+
+-----
+
+
+## Table of Contents
+- [`util.parseArgs([config])`](#utilparseargsconfig)
+- [Scope](#scope)
+- [Version Matchups](#version-matchups)
+- [🚀 Getting Started](#-getting-started)
+- [🙌 Contributing](#-contributing)
+- [💡 `process.mainArgs` Proposal](#-processmainargs-proposal)
+ - [Implementation:](#implementation)
+- [📃 Examples](#-examples)
+- [F.A.Qs](#faqs)
+- [Links & Resources](#links--resources)
+
+-----
+
+## Scope
+
+It is already possible to build great arg parsing modules on top of what Node.js provides; the prickly API is abstracted away by these modules. Thus, process.parseArgs() is not necessarily intended for library authors; it is intended for developers of simple CLI tools, ad-hoc scripts, deployed Node.js applications, and learning materials.
+
+It is exceedingly difficult to provide an API which would both be friendly to these Node.js users while being extensible enough for libraries to build upon. We chose to prioritize these use cases because these are currently not well-served by Node.js' API.
+
+----
+
+## Version Matchups
+
+| Node.js | @pkgjs/parseArgs |
+| -- | -- |
+| [v18.3.0](https://nodejs.org/docs/latest-v18.x/api/util.html#utilparseargsconfig) | [v0.9.1](https://github.com/pkgjs/parseargs/tree/v0.9.1#utilparseargsconfig) |
+| [v16.17.0](https://nodejs.org/dist/latest-v16.x/docs/api/util.html#utilparseargsconfig), [v18.7.0](https://nodejs.org/docs/latest-v18.x/api/util.html#utilparseargsconfig) | [0.10.0](https://github.com/pkgjs/parseargs/tree/v0.10.0#utilparseargsconfig) |
+
+----
+
+## 🚀 Getting Started
+
+1. **Install dependencies.**
+
+ ```bash
+ npm install
+ ```
+
+2. **Open the index.js file and start editing!**
+
+3. **Test your code by calling parseArgs through our test file**
+
+ ```bash
+ npm test
+ ```
+
+----
+
+## 🙌 Contributing
+
+Any person who wants to contribute to the initiative is welcome! Please first read the [Contributing Guide](CONTRIBUTING.md)
+
+Additionally, reading the [`Examples w/ Output`](#-examples-w-output) section of this document will be the best way to familiarize yourself with the target expected behavior for parseArgs() once it is fully implemented.
+
+This package was implemented using [tape](https://www.npmjs.com/package/tape) as its test harness.
+
+----
+
+## 💡 `process.mainArgs` Proposal
+
+> Note: This can be moved forward independently of the `util.parseArgs()` proposal/work.
+
+### Implementation:
+
+```javascript
+process.mainArgs = process.argv.slice(process._exec ? 1 : 2)
+```
+
+----
+
+## 📃 Examples
+
+```js
+const { parseArgs } = require('@pkgjs/parseargs');
+```
+
+```js
+const { parseArgs } = require('@pkgjs/parseargs');
+// specify the options that may be used
+const options = {
+ foo: { type: 'string'},
+ bar: { type: 'boolean' },
+};
+const args = ['--foo=a', '--bar'];
+const { values, positionals } = parseArgs({ args, options });
+// values = { foo: 'a', bar: true }
+// positionals = []
+```
+
+```js
+const { parseArgs } = require('@pkgjs/parseargs');
+// type:string & multiple
+const options = {
+ foo: {
+ type: 'string',
+ multiple: true,
+ },
+};
+const args = ['--foo=a', '--foo', 'b'];
+const { values, positionals } = parseArgs({ args, options });
+// values = { foo: [ 'a', 'b' ] }
+// positionals = []
+```
+
+```js
+const { parseArgs } = require('@pkgjs/parseargs');
+// shorts
+const options = {
+ foo: {
+ short: 'f',
+ type: 'boolean'
+ },
+};
+const args = ['-f', 'b'];
+const { values, positionals } = parseArgs({ args, options, allowPositionals: true });
+// values = { foo: true }
+// positionals = ['b']
+```
+
+```js
+const { parseArgs } = require('@pkgjs/parseargs');
+// unconfigured
+const options = {};
+const args = ['-f', '--foo=a', '--bar', 'b'];
+const { values, positionals } = parseArgs({ strict: false, args, options, allowPositionals: true });
+// values = { f: true, foo: 'a', bar: true }
+// positionals = ['b']
+```
+
+----
+
+## F.A.Qs
+
+- Is `cmd --foo=bar baz` the same as `cmd baz --foo=bar`?
+ - yes
+- Does the parser execute a function?
+ - no
+- Does the parser execute one of several functions, depending on input?
+ - no
+- Can subcommands take options that are distinct from the main command?
+ - no
+- Does it output generated help when no options match?
+ - no
+- Does it generated short usage? Like: `usage: ls [-ABCFGHLOPRSTUWabcdefghiklmnopqrstuwx1] [file ...]`
+ - no (no usage/help at all)
+- Does the user provide the long usage text? For each option? For the whole command?
+ - no
+- Do subcommands (if implemented) have their own usage output?
+ - no
+- Does usage print if the user runs `cmd --help`?
+ - no
+- Does it set `process.exitCode`?
+ - no
+- Does usage print to stderr or stdout?
+ - N/A
+- Does it check types? (Say, specify that an option is a boolean, number, etc.)
+ - no
+- Can an option have more than one type? (string or false, for example)
+ - no
+- Can the user define a type? (Say, `type: path` to call `path.resolve()` on the argument.)
+ - no
+- Does a `--foo=0o22` mean 0, 22, 18, or "0o22"?
+ - `"0o22"`
+- Does it coerce types?
+ - no
+- Does `--no-foo` coerce to `--foo=false`? For all options? Only boolean options?
+ - no, it sets `{values:{'no-foo': true}}`
+- Is `--foo` the same as `--foo=true`? Only for known booleans? Only at the end?
+ - no, they are not the same. There is no special handling of `true` as a value so it is just another string.
+- Does it read environment variables? Ie, is `FOO=1 cmd` the same as `cmd --foo=1`?
+ - no
+- Do unknown arguments raise an error? Are they parsed? Are they treated as positional arguments?
+ - no, they are parsed, not treated as positionals
+- Does `--` signal the end of options?
+ - yes
+- Is `--` included as a positional?
+ - no
+- Is `program -- foo` the same as `program foo`?
+ - yes, both store `{positionals:['foo']}`
+- Does the API specify whether a `--` was present/relevant?
+ - no
+- Is `-bar` the same as `--bar`?
+ - no, `-bar` is a short option or options, with expansion logic that follows the
+ [Utility Syntax Guidelines in POSIX.1-2017](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html). `-bar` expands to `-b`, `-a`, `-r`.
+- Is `---foo` the same as `--foo`?
+ - no
+ - the first is a long option named `'-foo'`
+ - the second is a long option named `'foo'`
+- Is `-` a positional? ie, `bash some-test.sh | tap -`
+ - yes
+
+## Links & Resources
+
+* [Initial Tooling Issue](https://github.com/nodejs/tooling/issues/19)
+* [Initial Proposal](https://github.com/nodejs/node/pull/35015)
+* [parseArgs Proposal](https://github.com/nodejs/node/pull/42675)
+
+[coverage-image]: https://img.shields.io/nycrc/pkgjs/parseargs
+[coverage-url]: https://github.com/pkgjs/parseargs/blob/main/.nycrc
+[pkgjs/parseargs]: https://github.com/pkgjs/parseargs
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/examples/is-default-value.js b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/examples/is-default-value.js
new file mode 100644
index 00000000..0a67972b
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/examples/is-default-value.js
@@ -0,0 +1,25 @@
+'use strict';
+
+// This example shows how to understand if a default value is used or not.
+
+// 1. const { parseArgs } = require('node:util'); // from node
+// 2. const { parseArgs } = require('@pkgjs/parseargs'); // from package
+const { parseArgs } = require('..'); // in repo
+
+const options = {
+ file: { short: 'f', type: 'string', default: 'FOO' },
+};
+
+const { values, tokens } = parseArgs({ options, tokens: true });
+
+const isFileDefault = !tokens.some((token) => token.kind === 'option' &&
+ token.name === 'file'
+);
+
+console.log(values);
+console.log(`Is the file option [${values.file}] the default value? ${isFileDefault}`);
+
+// Try the following:
+// node is-default-value.js
+// node is-default-value.js -f FILE
+// node is-default-value.js --file FILE
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/examples/limit-long-syntax.js b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/examples/limit-long-syntax.js
new file mode 100644
index 00000000..943e643e
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/examples/limit-long-syntax.js
@@ -0,0 +1,35 @@
+'use strict';
+
+// This is an example of using tokens to add a custom behaviour.
+//
+// Require the use of `=` for long options and values by blocking
+// the use of space separated values.
+// So allow `--foo=bar`, and not allow `--foo bar`.
+//
+// Note: this is not a common behaviour, most CLIs allow both forms.
+
+// 1. const { parseArgs } = require('node:util'); // from node
+// 2. const { parseArgs } = require('@pkgjs/parseargs'); // from package
+const { parseArgs } = require('..'); // in repo
+
+const options = {
+ file: { short: 'f', type: 'string' },
+ log: { type: 'string' },
+};
+
+const { values, tokens } = parseArgs({ options, tokens: true });
+
+const badToken = tokens.find((token) => token.kind === 'option' &&
+ token.value != null &&
+ token.rawName.startsWith('--') &&
+ !token.inlineValue
+);
+if (badToken) {
+ throw new Error(`Option value for '${badToken.rawName}' must be inline, like '${badToken.rawName}=VALUE'`);
+}
+
+console.log(values);
+
+// Try the following:
+// node limit-long-syntax.js -f FILE --log=LOG
+// node limit-long-syntax.js --file FILE
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/examples/negate.js b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/examples/negate.js
new file mode 100644
index 00000000..b6634690
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/examples/negate.js
@@ -0,0 +1,43 @@
+'use strict';
+
+// This example is used in the documentation.
+
+// How might I add my own support for --no-foo?
+
+// 1. const { parseArgs } = require('node:util'); // from node
+// 2. const { parseArgs } = require('@pkgjs/parseargs'); // from package
+const { parseArgs } = require('..'); // in repo
+
+const options = {
+ 'color': { type: 'boolean' },
+ 'no-color': { type: 'boolean' },
+ 'logfile': { type: 'string' },
+ 'no-logfile': { type: 'boolean' },
+};
+const { values, tokens } = parseArgs({ options, tokens: true });
+
+// Reprocess the option tokens and overwrite the returned values.
+tokens
+ .filter((token) => token.kind === 'option')
+ .forEach((token) => {
+ if (token.name.startsWith('no-')) {
+ // Store foo:false for --no-foo
+ const positiveName = token.name.slice(3);
+ values[positiveName] = false;
+ delete values[token.name];
+ } else {
+ // Resave value so last one wins if both --foo and --no-foo.
+ values[token.name] = token.value ?? true;
+ }
+ });
+
+const color = values.color;
+const logfile = values.logfile ?? 'default.log';
+
+console.log({ logfile, color });
+
+// Try the following:
+// node negate.js
+// node negate.js --no-logfile --no-color
+// negate.js --logfile=test.log --color
+// node negate.js --no-logfile --logfile=test.log --color --no-color
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/examples/no-repeated-options.js b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/examples/no-repeated-options.js
new file mode 100644
index 00000000..0c324688
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/examples/no-repeated-options.js
@@ -0,0 +1,31 @@
+'use strict';
+
+// This is an example of using tokens to add a custom behaviour.
+//
+// Throw an error if an option is used more than once.
+
+// 1. const { parseArgs } = require('node:util'); // from node
+// 2. const { parseArgs } = require('@pkgjs/parseargs'); // from package
+const { parseArgs } = require('..'); // in repo
+
+const options = {
+ ding: { type: 'boolean', short: 'd' },
+ beep: { type: 'boolean', short: 'b' }
+};
+const { values, tokens } = parseArgs({ options, tokens: true });
+
+const seenBefore = new Set();
+tokens.forEach((token) => {
+ if (token.kind !== 'option') return;
+ if (seenBefore.has(token.name)) {
+ throw new Error(`option '${token.name}' used multiple times`);
+ }
+ seenBefore.add(token.name);
+});
+
+console.log(values);
+
+// Try the following:
+// node no-repeated-options --ding --beep
+// node no-repeated-options --beep -b
+// node no-repeated-options -ddd
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/examples/ordered-options.mjs b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/examples/ordered-options.mjs
new file mode 100644
index 00000000..8ab7367b
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/examples/ordered-options.mjs
@@ -0,0 +1,41 @@
+// This is an example of using tokens to add a custom behaviour.
+//
+// This adds a option order check so that --some-unstable-option
+// may only be used after --enable-experimental-options
+//
+// Note: this is not a common behaviour, the order of different options
+// does not usually matter.
+
+import { parseArgs } from '../index.js';
+
+function findTokenIndex(tokens, target) {
+ return tokens.findIndex((token) => token.kind === 'option' &&
+ token.name === target
+ );
+}
+
+const experimentalName = 'enable-experimental-options';
+const unstableName = 'some-unstable-option';
+
+const options = {
+ [experimentalName]: { type: 'boolean' },
+ [unstableName]: { type: 'boolean' },
+};
+
+const { values, tokens } = parseArgs({ options, tokens: true });
+
+const experimentalIndex = findTokenIndex(tokens, experimentalName);
+const unstableIndex = findTokenIndex(tokens, unstableName);
+if (unstableIndex !== -1 &&
+ ((experimentalIndex === -1) || (unstableIndex < experimentalIndex))) {
+ throw new Error(`'--${experimentalName}' must be specified before '--${unstableName}'`);
+}
+
+console.log(values);
+
+/* eslint-disable max-len */
+// Try the following:
+// node ordered-options.mjs
+// node ordered-options.mjs --some-unstable-option
+// node ordered-options.mjs --some-unstable-option --enable-experimental-options
+// node ordered-options.mjs --enable-experimental-options --some-unstable-option
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/examples/simple-hard-coded.js b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/examples/simple-hard-coded.js
new file mode 100644
index 00000000..eff04c2a
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/examples/simple-hard-coded.js
@@ -0,0 +1,26 @@
+'use strict';
+
+// This example is used in the documentation.
+
+// 1. const { parseArgs } = require('node:util'); // from node
+// 2. const { parseArgs } = require('@pkgjs/parseargs'); // from package
+const { parseArgs } = require('..'); // in repo
+
+const args = ['-f', '--bar', 'b'];
+const options = {
+ foo: {
+ type: 'boolean',
+ short: 'f'
+ },
+ bar: {
+ type: 'string'
+ }
+};
+const {
+ values,
+ positionals
+} = parseArgs({ args, options });
+console.log(values, positionals);
+
+// Try the following:
+// node simple-hard-coded.js
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/index.js b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/index.js
new file mode 100644
index 00000000..b1004c7b
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/index.js
@@ -0,0 +1,396 @@
+'use strict';
+
+const {
+ ArrayPrototypeForEach,
+ ArrayPrototypeIncludes,
+ ArrayPrototypeMap,
+ ArrayPrototypePush,
+ ArrayPrototypePushApply,
+ ArrayPrototypeShift,
+ ArrayPrototypeSlice,
+ ArrayPrototypeUnshiftApply,
+ ObjectEntries,
+ ObjectPrototypeHasOwnProperty: ObjectHasOwn,
+ StringPrototypeCharAt,
+ StringPrototypeIndexOf,
+ StringPrototypeSlice,
+ StringPrototypeStartsWith,
+} = require('./internal/primordials');
+
+const {
+ validateArray,
+ validateBoolean,
+ validateBooleanArray,
+ validateObject,
+ validateString,
+ validateStringArray,
+ validateUnion,
+} = require('./internal/validators');
+
+const {
+ kEmptyObject,
+} = require('./internal/util');
+
+const {
+ findLongOptionForShort,
+ isLoneLongOption,
+ isLoneShortOption,
+ isLongOptionAndValue,
+ isOptionValue,
+ isOptionLikeValue,
+ isShortOptionAndValue,
+ isShortOptionGroup,
+ useDefaultValueOption,
+ objectGetOwn,
+ optionsGetOwn,
+} = require('./utils');
+
+const {
+ codes: {
+ ERR_INVALID_ARG_VALUE,
+ ERR_PARSE_ARGS_INVALID_OPTION_VALUE,
+ ERR_PARSE_ARGS_UNKNOWN_OPTION,
+ ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL,
+ },
+} = require('./internal/errors');
+
+function getMainArgs() {
+ // Work out where to slice process.argv for user supplied arguments.
+
+ // Check node options for scenarios where user CLI args follow executable.
+ const execArgv = process.execArgv;
+ if (ArrayPrototypeIncludes(execArgv, '-e') ||
+ ArrayPrototypeIncludes(execArgv, '--eval') ||
+ ArrayPrototypeIncludes(execArgv, '-p') ||
+ ArrayPrototypeIncludes(execArgv, '--print')) {
+ return ArrayPrototypeSlice(process.argv, 1);
+ }
+
+ // Normally first two arguments are executable and script, then CLI arguments
+ return ArrayPrototypeSlice(process.argv, 2);
+}
+
+/**
+ * In strict mode, throw for possible usage errors like --foo --bar
+ *
+ * @param {object} token - from tokens as available from parseArgs
+ */
+function checkOptionLikeValue(token) {
+ if (!token.inlineValue && isOptionLikeValue(token.value)) {
+ // Only show short example if user used short option.
+ const example = StringPrototypeStartsWith(token.rawName, '--') ?
+ `'${token.rawName}=-XYZ'` :
+ `'--${token.name}=-XYZ' or '${token.rawName}-XYZ'`;
+ const errorMessage = `Option '${token.rawName}' argument is ambiguous.
+Did you forget to specify the option argument for '${token.rawName}'?
+To specify an option argument starting with a dash use ${example}.`;
+ throw new ERR_PARSE_ARGS_INVALID_OPTION_VALUE(errorMessage);
+ }
+}
+
+/**
+ * In strict mode, throw for usage errors.
+ *
+ * @param {object} config - from config passed to parseArgs
+ * @param {object} token - from tokens as available from parseArgs
+ */
+function checkOptionUsage(config, token) {
+ if (!ObjectHasOwn(config.options, token.name)) {
+ throw new ERR_PARSE_ARGS_UNKNOWN_OPTION(
+ token.rawName, config.allowPositionals);
+ }
+
+ const short = optionsGetOwn(config.options, token.name, 'short');
+ const shortAndLong = `${short ? `-${short}, ` : ''}--${token.name}`;
+ const type = optionsGetOwn(config.options, token.name, 'type');
+ if (type === 'string' && typeof token.value !== 'string') {
+ throw new ERR_PARSE_ARGS_INVALID_OPTION_VALUE(`Option '${shortAndLong} ' argument missing`);
+ }
+ // (Idiomatic test for undefined||null, expecting undefined.)
+ if (type === 'boolean' && token.value != null) {
+ throw new ERR_PARSE_ARGS_INVALID_OPTION_VALUE(`Option '${shortAndLong}' does not take an argument`);
+ }
+}
+
+
+/**
+ * Store the option value in `values`.
+ *
+ * @param {string} longOption - long option name e.g. 'foo'
+ * @param {string|undefined} optionValue - value from user args
+ * @param {object} options - option configs, from parseArgs({ options })
+ * @param {object} values - option values returned in `values` by parseArgs
+ */
+function storeOption(longOption, optionValue, options, values) {
+ if (longOption === '__proto__') {
+ return; // No. Just no.
+ }
+
+ // We store based on the option value rather than option type,
+ // preserving the users intent for author to deal with.
+ const newValue = optionValue ?? true;
+ if (optionsGetOwn(options, longOption, 'multiple')) {
+ // Always store value in array, including for boolean.
+ // values[longOption] starts out not present,
+ // first value is added as new array [newValue],
+ // subsequent values are pushed to existing array.
+ // (note: values has null prototype, so simpler usage)
+ if (values[longOption]) {
+ ArrayPrototypePush(values[longOption], newValue);
+ } else {
+ values[longOption] = [newValue];
+ }
+ } else {
+ values[longOption] = newValue;
+ }
+}
+
+/**
+ * Store the default option value in `values`.
+ *
+ * @param {string} longOption - long option name e.g. 'foo'
+ * @param {string
+ * | boolean
+ * | string[]
+ * | boolean[]} optionValue - default value from option config
+ * @param {object} values - option values returned in `values` by parseArgs
+ */
+function storeDefaultOption(longOption, optionValue, values) {
+ if (longOption === '__proto__') {
+ return; // No. Just no.
+ }
+
+ values[longOption] = optionValue;
+}
+
+/**
+ * Process args and turn into identified tokens:
+ * - option (along with value, if any)
+ * - positional
+ * - option-terminator
+ *
+ * @param {string[]} args - from parseArgs({ args }) or mainArgs
+ * @param {object} options - option configs, from parseArgs({ options })
+ */
+function argsToTokens(args, options) {
+ const tokens = [];
+ let index = -1;
+ let groupCount = 0;
+
+ const remainingArgs = ArrayPrototypeSlice(args);
+ while (remainingArgs.length > 0) {
+ const arg = ArrayPrototypeShift(remainingArgs);
+ const nextArg = remainingArgs[0];
+ if (groupCount > 0)
+ groupCount--;
+ else
+ index++;
+
+ // Check if `arg` is an options terminator.
+ // Guideline 10 in https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html
+ if (arg === '--') {
+ // Everything after a bare '--' is considered a positional argument.
+ ArrayPrototypePush(tokens, { kind: 'option-terminator', index });
+ ArrayPrototypePushApply(
+ tokens, ArrayPrototypeMap(remainingArgs, (arg) => {
+ return { kind: 'positional', index: ++index, value: arg };
+ })
+ );
+ break; // Finished processing args, leave while loop.
+ }
+
+ if (isLoneShortOption(arg)) {
+ // e.g. '-f'
+ const shortOption = StringPrototypeCharAt(arg, 1);
+ const longOption = findLongOptionForShort(shortOption, options);
+ let value;
+ let inlineValue;
+ if (optionsGetOwn(options, longOption, 'type') === 'string' &&
+ isOptionValue(nextArg)) {
+ // e.g. '-f', 'bar'
+ value = ArrayPrototypeShift(remainingArgs);
+ inlineValue = false;
+ }
+ ArrayPrototypePush(
+ tokens,
+ { kind: 'option', name: longOption, rawName: arg,
+ index, value, inlineValue });
+ if (value != null) ++index;
+ continue;
+ }
+
+ if (isShortOptionGroup(arg, options)) {
+ // Expand -fXzy to -f -X -z -y
+ const expanded = [];
+ for (let index = 1; index < arg.length; index++) {
+ const shortOption = StringPrototypeCharAt(arg, index);
+ const longOption = findLongOptionForShort(shortOption, options);
+ if (optionsGetOwn(options, longOption, 'type') !== 'string' ||
+ index === arg.length - 1) {
+ // Boolean option, or last short in group. Well formed.
+ ArrayPrototypePush(expanded, `-${shortOption}`);
+ } else {
+ // String option in middle. Yuck.
+ // Expand -abfFILE to -a -b -fFILE
+ ArrayPrototypePush(expanded, `-${StringPrototypeSlice(arg, index)}`);
+ break; // finished short group
+ }
+ }
+ ArrayPrototypeUnshiftApply(remainingArgs, expanded);
+ groupCount = expanded.length;
+ continue;
+ }
+
+ if (isShortOptionAndValue(arg, options)) {
+ // e.g. -fFILE
+ const shortOption = StringPrototypeCharAt(arg, 1);
+ const longOption = findLongOptionForShort(shortOption, options);
+ const value = StringPrototypeSlice(arg, 2);
+ ArrayPrototypePush(
+ tokens,
+ { kind: 'option', name: longOption, rawName: `-${shortOption}`,
+ index, value, inlineValue: true });
+ continue;
+ }
+
+ if (isLoneLongOption(arg)) {
+ // e.g. '--foo'
+ const longOption = StringPrototypeSlice(arg, 2);
+ let value;
+ let inlineValue;
+ if (optionsGetOwn(options, longOption, 'type') === 'string' &&
+ isOptionValue(nextArg)) {
+ // e.g. '--foo', 'bar'
+ value = ArrayPrototypeShift(remainingArgs);
+ inlineValue = false;
+ }
+ ArrayPrototypePush(
+ tokens,
+ { kind: 'option', name: longOption, rawName: arg,
+ index, value, inlineValue });
+ if (value != null) ++index;
+ continue;
+ }
+
+ if (isLongOptionAndValue(arg)) {
+ // e.g. --foo=bar
+ const equalIndex = StringPrototypeIndexOf(arg, '=');
+ const longOption = StringPrototypeSlice(arg, 2, equalIndex);
+ const value = StringPrototypeSlice(arg, equalIndex + 1);
+ ArrayPrototypePush(
+ tokens,
+ { kind: 'option', name: longOption, rawName: `--${longOption}`,
+ index, value, inlineValue: true });
+ continue;
+ }
+
+ ArrayPrototypePush(tokens, { kind: 'positional', index, value: arg });
+ }
+
+ return tokens;
+}
+
+const parseArgs = (config = kEmptyObject) => {
+ const args = objectGetOwn(config, 'args') ?? getMainArgs();
+ const strict = objectGetOwn(config, 'strict') ?? true;
+ const allowPositionals = objectGetOwn(config, 'allowPositionals') ?? !strict;
+ const returnTokens = objectGetOwn(config, 'tokens') ?? false;
+ const options = objectGetOwn(config, 'options') ?? { __proto__: null };
+ // Bundle these up for passing to strict-mode checks.
+ const parseConfig = { args, strict, options, allowPositionals };
+
+ // Validate input configuration.
+ validateArray(args, 'args');
+ validateBoolean(strict, 'strict');
+ validateBoolean(allowPositionals, 'allowPositionals');
+ validateBoolean(returnTokens, 'tokens');
+ validateObject(options, 'options');
+ ArrayPrototypeForEach(
+ ObjectEntries(options),
+ ({ 0: longOption, 1: optionConfig }) => {
+ validateObject(optionConfig, `options.${longOption}`);
+
+ // type is required
+ const optionType = objectGetOwn(optionConfig, 'type');
+ validateUnion(optionType, `options.${longOption}.type`, ['string', 'boolean']);
+
+ if (ObjectHasOwn(optionConfig, 'short')) {
+ const shortOption = optionConfig.short;
+ validateString(shortOption, `options.${longOption}.short`);
+ if (shortOption.length !== 1) {
+ throw new ERR_INVALID_ARG_VALUE(
+ `options.${longOption}.short`,
+ shortOption,
+ 'must be a single character'
+ );
+ }
+ }
+
+ const multipleOption = objectGetOwn(optionConfig, 'multiple');
+ if (ObjectHasOwn(optionConfig, 'multiple')) {
+ validateBoolean(multipleOption, `options.${longOption}.multiple`);
+ }
+
+ const defaultValue = objectGetOwn(optionConfig, 'default');
+ if (defaultValue !== undefined) {
+ let validator;
+ switch (optionType) {
+ case 'string':
+ validator = multipleOption ? validateStringArray : validateString;
+ break;
+
+ case 'boolean':
+ validator = multipleOption ? validateBooleanArray : validateBoolean;
+ break;
+ }
+ validator(defaultValue, `options.${longOption}.default`);
+ }
+ }
+ );
+
+ // Phase 1: identify tokens
+ const tokens = argsToTokens(args, options);
+
+ // Phase 2: process tokens into parsed option values and positionals
+ const result = {
+ values: { __proto__: null },
+ positionals: [],
+ };
+ if (returnTokens) {
+ result.tokens = tokens;
+ }
+ ArrayPrototypeForEach(tokens, (token) => {
+ if (token.kind === 'option') {
+ if (strict) {
+ checkOptionUsage(parseConfig, token);
+ checkOptionLikeValue(token);
+ }
+ storeOption(token.name, token.value, options, result.values);
+ } else if (token.kind === 'positional') {
+ if (!allowPositionals) {
+ throw new ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL(token.value);
+ }
+ ArrayPrototypePush(result.positionals, token.value);
+ }
+ });
+
+ // Phase 3: fill in default values for missing args
+ ArrayPrototypeForEach(ObjectEntries(options), ({ 0: longOption,
+ 1: optionConfig }) => {
+ const mustSetDefault = useDefaultValueOption(longOption,
+ optionConfig,
+ result.values);
+ if (mustSetDefault) {
+ storeDefaultOption(longOption,
+ objectGetOwn(optionConfig, 'default'),
+ result.values);
+ }
+ });
+
+
+ return result;
+};
+
+module.exports = {
+ parseArgs,
+};
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/internal/errors.js b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/internal/errors.js
new file mode 100644
index 00000000..e1b237b5
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/internal/errors.js
@@ -0,0 +1,47 @@
+'use strict';
+
+class ERR_INVALID_ARG_TYPE extends TypeError {
+ constructor(name, expected, actual) {
+ super(`${name} must be ${expected} got ${actual}`);
+ this.code = 'ERR_INVALID_ARG_TYPE';
+ }
+}
+
+class ERR_INVALID_ARG_VALUE extends TypeError {
+ constructor(arg1, arg2, expected) {
+ super(`The property ${arg1} ${expected}. Received '${arg2}'`);
+ this.code = 'ERR_INVALID_ARG_VALUE';
+ }
+}
+
+class ERR_PARSE_ARGS_INVALID_OPTION_VALUE extends Error {
+ constructor(message) {
+ super(message);
+ this.code = 'ERR_PARSE_ARGS_INVALID_OPTION_VALUE';
+ }
+}
+
+class ERR_PARSE_ARGS_UNKNOWN_OPTION extends Error {
+ constructor(option, allowPositionals) {
+ const suggestDashDash = allowPositionals ? `. To specify a positional argument starting with a '-', place it at the end of the command after '--', as in '-- ${JSON.stringify(option)}` : '';
+ super(`Unknown option '${option}'${suggestDashDash}`);
+ this.code = 'ERR_PARSE_ARGS_UNKNOWN_OPTION';
+ }
+}
+
+class ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL extends Error {
+ constructor(positional) {
+ super(`Unexpected argument '${positional}'. This command does not take positional arguments`);
+ this.code = 'ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL';
+ }
+}
+
+module.exports = {
+ codes: {
+ ERR_INVALID_ARG_TYPE,
+ ERR_INVALID_ARG_VALUE,
+ ERR_PARSE_ARGS_INVALID_OPTION_VALUE,
+ ERR_PARSE_ARGS_UNKNOWN_OPTION,
+ ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL,
+ }
+};
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/internal/primordials.js b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/internal/primordials.js
new file mode 100644
index 00000000..63e23ab1
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/internal/primordials.js
@@ -0,0 +1,393 @@
+/*
+This file is copied from https://github.com/nodejs/node/blob/v14.19.3/lib/internal/per_context/primordials.js
+under the following license:
+
+Copyright Node.js contributors. All rights reserved.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to
+deal in the Software without restriction, including without limitation the
+rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+sell copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+IN THE SOFTWARE.
+*/
+
+'use strict';
+
+/* eslint-disable node-core/prefer-primordials */
+
+// This file subclasses and stores the JS builtins that come from the VM
+// so that Node.js's builtin modules do not need to later look these up from
+// the global proxy, which can be mutated by users.
+
+// Use of primordials have sometimes a dramatic impact on performance, please
+// benchmark all changes made in performance-sensitive areas of the codebase.
+// See: https://github.com/nodejs/node/pull/38248
+
+const primordials = {};
+
+const {
+ defineProperty: ReflectDefineProperty,
+ getOwnPropertyDescriptor: ReflectGetOwnPropertyDescriptor,
+ ownKeys: ReflectOwnKeys,
+} = Reflect;
+
+// `uncurryThis` is equivalent to `func => Function.prototype.call.bind(func)`.
+// It is using `bind.bind(call)` to avoid using `Function.prototype.bind`
+// and `Function.prototype.call` after it may have been mutated by users.
+const { apply, bind, call } = Function.prototype;
+const uncurryThis = bind.bind(call);
+primordials.uncurryThis = uncurryThis;
+
+// `applyBind` is equivalent to `func => Function.prototype.apply.bind(func)`.
+// It is using `bind.bind(apply)` to avoid using `Function.prototype.bind`
+// and `Function.prototype.apply` after it may have been mutated by users.
+const applyBind = bind.bind(apply);
+primordials.applyBind = applyBind;
+
+// Methods that accept a variable number of arguments, and thus it's useful to
+// also create `${prefix}${key}Apply`, which uses `Function.prototype.apply`,
+// instead of `Function.prototype.call`, and thus doesn't require iterator
+// destructuring.
+const varargsMethods = [
+ // 'ArrayPrototypeConcat' is omitted, because it performs the spread
+ // on its own for arrays and array-likes with a truthy
+ // @@isConcatSpreadable symbol property.
+ 'ArrayOf',
+ 'ArrayPrototypePush',
+ 'ArrayPrototypeUnshift',
+ // 'FunctionPrototypeCall' is omitted, since there's 'ReflectApply'
+ // and 'FunctionPrototypeApply'.
+ 'MathHypot',
+ 'MathMax',
+ 'MathMin',
+ 'StringPrototypeConcat',
+ 'TypedArrayOf',
+];
+
+function getNewKey(key) {
+ return typeof key === 'symbol' ?
+ `Symbol${key.description[7].toUpperCase()}${key.description.slice(8)}` :
+ `${key[0].toUpperCase()}${key.slice(1)}`;
+}
+
+function copyAccessor(dest, prefix, key, { enumerable, get, set }) {
+ ReflectDefineProperty(dest, `${prefix}Get${key}`, {
+ value: uncurryThis(get),
+ enumerable
+ });
+ if (set !== undefined) {
+ ReflectDefineProperty(dest, `${prefix}Set${key}`, {
+ value: uncurryThis(set),
+ enumerable
+ });
+ }
+}
+
+function copyPropsRenamed(src, dest, prefix) {
+ for (const key of ReflectOwnKeys(src)) {
+ const newKey = getNewKey(key);
+ const desc = ReflectGetOwnPropertyDescriptor(src, key);
+ if ('get' in desc) {
+ copyAccessor(dest, prefix, newKey, desc);
+ } else {
+ const name = `${prefix}${newKey}`;
+ ReflectDefineProperty(dest, name, desc);
+ if (varargsMethods.includes(name)) {
+ ReflectDefineProperty(dest, `${name}Apply`, {
+ // `src` is bound as the `this` so that the static `this` points
+ // to the object it was defined on,
+ // e.g.: `ArrayOfApply` gets a `this` of `Array`:
+ value: applyBind(desc.value, src),
+ });
+ }
+ }
+ }
+}
+
+function copyPropsRenamedBound(src, dest, prefix) {
+ for (const key of ReflectOwnKeys(src)) {
+ const newKey = getNewKey(key);
+ const desc = ReflectGetOwnPropertyDescriptor(src, key);
+ if ('get' in desc) {
+ copyAccessor(dest, prefix, newKey, desc);
+ } else {
+ const { value } = desc;
+ if (typeof value === 'function') {
+ desc.value = value.bind(src);
+ }
+
+ const name = `${prefix}${newKey}`;
+ ReflectDefineProperty(dest, name, desc);
+ if (varargsMethods.includes(name)) {
+ ReflectDefineProperty(dest, `${name}Apply`, {
+ value: applyBind(value, src),
+ });
+ }
+ }
+ }
+}
+
+function copyPrototype(src, dest, prefix) {
+ for (const key of ReflectOwnKeys(src)) {
+ const newKey = getNewKey(key);
+ const desc = ReflectGetOwnPropertyDescriptor(src, key);
+ if ('get' in desc) {
+ copyAccessor(dest, prefix, newKey, desc);
+ } else {
+ const { value } = desc;
+ if (typeof value === 'function') {
+ desc.value = uncurryThis(value);
+ }
+
+ const name = `${prefix}${newKey}`;
+ ReflectDefineProperty(dest, name, desc);
+ if (varargsMethods.includes(name)) {
+ ReflectDefineProperty(dest, `${name}Apply`, {
+ value: applyBind(value),
+ });
+ }
+ }
+ }
+}
+
+// Create copies of configurable value properties of the global object
+[
+ 'Proxy',
+ 'globalThis',
+].forEach((name) => {
+ // eslint-disable-next-line no-restricted-globals
+ primordials[name] = globalThis[name];
+});
+
+// Create copies of URI handling functions
+[
+ decodeURI,
+ decodeURIComponent,
+ encodeURI,
+ encodeURIComponent,
+].forEach((fn) => {
+ primordials[fn.name] = fn;
+});
+
+// Create copies of the namespace objects
+[
+ 'JSON',
+ 'Math',
+ 'Proxy',
+ 'Reflect',
+].forEach((name) => {
+ // eslint-disable-next-line no-restricted-globals
+ copyPropsRenamed(global[name], primordials, name);
+});
+
+// Create copies of intrinsic objects
+[
+ 'Array',
+ 'ArrayBuffer',
+ 'BigInt',
+ 'BigInt64Array',
+ 'BigUint64Array',
+ 'Boolean',
+ 'DataView',
+ 'Date',
+ 'Error',
+ 'EvalError',
+ 'Float32Array',
+ 'Float64Array',
+ 'Function',
+ 'Int16Array',
+ 'Int32Array',
+ 'Int8Array',
+ 'Map',
+ 'Number',
+ 'Object',
+ 'RangeError',
+ 'ReferenceError',
+ 'RegExp',
+ 'Set',
+ 'String',
+ 'Symbol',
+ 'SyntaxError',
+ 'TypeError',
+ 'URIError',
+ 'Uint16Array',
+ 'Uint32Array',
+ 'Uint8Array',
+ 'Uint8ClampedArray',
+ 'WeakMap',
+ 'WeakSet',
+].forEach((name) => {
+ // eslint-disable-next-line no-restricted-globals
+ const original = global[name];
+ primordials[name] = original;
+ copyPropsRenamed(original, primordials, name);
+ copyPrototype(original.prototype, primordials, `${name}Prototype`);
+});
+
+// Create copies of intrinsic objects that require a valid `this` to call
+// static methods.
+// Refs: https://www.ecma-international.org/ecma-262/#sec-promise.all
+[
+ 'Promise',
+].forEach((name) => {
+ // eslint-disable-next-line no-restricted-globals
+ const original = global[name];
+ primordials[name] = original;
+ copyPropsRenamedBound(original, primordials, name);
+ copyPrototype(original.prototype, primordials, `${name}Prototype`);
+});
+
+// Create copies of abstract intrinsic objects that are not directly exposed
+// on the global object.
+// Refs: https://tc39.es/ecma262/#sec-%typedarray%-intrinsic-object
+[
+ { name: 'TypedArray', original: Reflect.getPrototypeOf(Uint8Array) },
+ { name: 'ArrayIterator', original: {
+ prototype: Reflect.getPrototypeOf(Array.prototype[Symbol.iterator]()),
+ } },
+ { name: 'StringIterator', original: {
+ prototype: Reflect.getPrototypeOf(String.prototype[Symbol.iterator]()),
+ } },
+].forEach(({ name, original }) => {
+ primordials[name] = original;
+ // The static %TypedArray% methods require a valid `this`, but can't be bound,
+ // as they need a subclass constructor as the receiver:
+ copyPrototype(original, primordials, name);
+ copyPrototype(original.prototype, primordials, `${name}Prototype`);
+});
+
+/* eslint-enable node-core/prefer-primordials */
+
+const {
+ ArrayPrototypeForEach,
+ FunctionPrototypeCall,
+ Map,
+ ObjectFreeze,
+ ObjectSetPrototypeOf,
+ Set,
+ SymbolIterator,
+ WeakMap,
+ WeakSet,
+} = primordials;
+
+// Because these functions are used by `makeSafe`, which is exposed
+// on the `primordials` object, it's important to use const references
+// to the primordials that they use:
+const createSafeIterator = (factory, next) => {
+ class SafeIterator {
+ constructor(iterable) {
+ this._iterator = factory(iterable);
+ }
+ next() {
+ return next(this._iterator);
+ }
+ [SymbolIterator]() {
+ return this;
+ }
+ }
+ ObjectSetPrototypeOf(SafeIterator.prototype, null);
+ ObjectFreeze(SafeIterator.prototype);
+ ObjectFreeze(SafeIterator);
+ return SafeIterator;
+};
+
+primordials.SafeArrayIterator = createSafeIterator(
+ primordials.ArrayPrototypeSymbolIterator,
+ primordials.ArrayIteratorPrototypeNext
+);
+primordials.SafeStringIterator = createSafeIterator(
+ primordials.StringPrototypeSymbolIterator,
+ primordials.StringIteratorPrototypeNext
+);
+
+const copyProps = (src, dest) => {
+ ArrayPrototypeForEach(ReflectOwnKeys(src), (key) => {
+ if (!ReflectGetOwnPropertyDescriptor(dest, key)) {
+ ReflectDefineProperty(
+ dest,
+ key,
+ ReflectGetOwnPropertyDescriptor(src, key));
+ }
+ });
+};
+
+const makeSafe = (unsafe, safe) => {
+ if (SymbolIterator in unsafe.prototype) {
+ const dummy = new unsafe();
+ let next; // We can reuse the same `next` method.
+
+ ArrayPrototypeForEach(ReflectOwnKeys(unsafe.prototype), (key) => {
+ if (!ReflectGetOwnPropertyDescriptor(safe.prototype, key)) {
+ const desc = ReflectGetOwnPropertyDescriptor(unsafe.prototype, key);
+ if (
+ typeof desc.value === 'function' &&
+ desc.value.length === 0 &&
+ SymbolIterator in (FunctionPrototypeCall(desc.value, dummy) ?? {})
+ ) {
+ const createIterator = uncurryThis(desc.value);
+ next = next ?? uncurryThis(createIterator(dummy).next);
+ const SafeIterator = createSafeIterator(createIterator, next);
+ desc.value = function() {
+ return new SafeIterator(this);
+ };
+ }
+ ReflectDefineProperty(safe.prototype, key, desc);
+ }
+ });
+ } else {
+ copyProps(unsafe.prototype, safe.prototype);
+ }
+ copyProps(unsafe, safe);
+
+ ObjectSetPrototypeOf(safe.prototype, null);
+ ObjectFreeze(safe.prototype);
+ ObjectFreeze(safe);
+ return safe;
+};
+primordials.makeSafe = makeSafe;
+
+// Subclass the constructors because we need to use their prototype
+// methods later.
+// Defining the `constructor` is necessary here to avoid the default
+// constructor which uses the user-mutable `%ArrayIteratorPrototype%.next`.
+primordials.SafeMap = makeSafe(
+ Map,
+ class SafeMap extends Map {
+ constructor(i) { super(i); } // eslint-disable-line no-useless-constructor
+ }
+);
+primordials.SafeWeakMap = makeSafe(
+ WeakMap,
+ class SafeWeakMap extends WeakMap {
+ constructor(i) { super(i); } // eslint-disable-line no-useless-constructor
+ }
+);
+primordials.SafeSet = makeSafe(
+ Set,
+ class SafeSet extends Set {
+ constructor(i) { super(i); } // eslint-disable-line no-useless-constructor
+ }
+);
+primordials.SafeWeakSet = makeSafe(
+ WeakSet,
+ class SafeWeakSet extends WeakSet {
+ constructor(i) { super(i); } // eslint-disable-line no-useless-constructor
+ }
+);
+
+ObjectSetPrototypeOf(primordials, null);
+ObjectFreeze(primordials);
+
+module.exports = primordials;
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/internal/util.js b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/internal/util.js
new file mode 100644
index 00000000..b9b8fe5b
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/internal/util.js
@@ -0,0 +1,14 @@
+'use strict';
+
+// This is a placeholder for util.js in node.js land.
+
+const {
+ ObjectCreate,
+ ObjectFreeze,
+} = require('./primordials');
+
+const kEmptyObject = ObjectFreeze(ObjectCreate(null));
+
+module.exports = {
+ kEmptyObject,
+};
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/internal/validators.js b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/internal/validators.js
new file mode 100644
index 00000000..b5ac4fb5
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/internal/validators.js
@@ -0,0 +1,89 @@
+'use strict';
+
+// This file is a proxy of the original file located at:
+// https://github.com/nodejs/node/blob/main/lib/internal/validators.js
+// Every addition or modification to this file must be evaluated
+// during the PR review.
+
+const {
+ ArrayIsArray,
+ ArrayPrototypeIncludes,
+ ArrayPrototypeJoin,
+} = require('./primordials');
+
+const {
+ codes: {
+ ERR_INVALID_ARG_TYPE
+ }
+} = require('./errors');
+
+function validateString(value, name) {
+ if (typeof value !== 'string') {
+ throw new ERR_INVALID_ARG_TYPE(name, 'String', value);
+ }
+}
+
+function validateUnion(value, name, union) {
+ if (!ArrayPrototypeIncludes(union, value)) {
+ throw new ERR_INVALID_ARG_TYPE(name, `('${ArrayPrototypeJoin(union, '|')}')`, value);
+ }
+}
+
+function validateBoolean(value, name) {
+ if (typeof value !== 'boolean') {
+ throw new ERR_INVALID_ARG_TYPE(name, 'Boolean', value);
+ }
+}
+
+function validateArray(value, name) {
+ if (!ArrayIsArray(value)) {
+ throw new ERR_INVALID_ARG_TYPE(name, 'Array', value);
+ }
+}
+
+function validateStringArray(value, name) {
+ validateArray(value, name);
+ for (let i = 0; i < value.length; i++) {
+ validateString(value[i], `${name}[${i}]`);
+ }
+}
+
+function validateBooleanArray(value, name) {
+ validateArray(value, name);
+ for (let i = 0; i < value.length; i++) {
+ validateBoolean(value[i], `${name}[${i}]`);
+ }
+}
+
+/**
+ * @param {unknown} value
+ * @param {string} name
+ * @param {{
+ * allowArray?: boolean,
+ * allowFunction?: boolean,
+ * nullable?: boolean
+ * }} [options]
+ */
+function validateObject(value, name, options) {
+ const useDefaultOptions = options == null;
+ const allowArray = useDefaultOptions ? false : options.allowArray;
+ const allowFunction = useDefaultOptions ? false : options.allowFunction;
+ const nullable = useDefaultOptions ? false : options.nullable;
+ if ((!nullable && value === null) ||
+ (!allowArray && ArrayIsArray(value)) ||
+ (typeof value !== 'object' && (
+ !allowFunction || typeof value !== 'function'
+ ))) {
+ throw new ERR_INVALID_ARG_TYPE(name, 'Object', value);
+ }
+}
+
+module.exports = {
+ validateArray,
+ validateObject,
+ validateString,
+ validateStringArray,
+ validateUnion,
+ validateBoolean,
+ validateBooleanArray,
+};
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/package.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/package.json
new file mode 100644
index 00000000..0bcc05c0
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/package.json
@@ -0,0 +1,36 @@
+{
+ "name": "@pkgjs/parseargs",
+ "version": "0.11.0",
+ "description": "Polyfill of future proposal for `util.parseArgs()`",
+ "engines": {
+ "node": ">=14"
+ },
+ "main": "index.js",
+ "exports": {
+ ".": "./index.js",
+ "./package.json": "./package.json"
+ },
+ "scripts": {
+ "coverage": "c8 --check-coverage tape 'test/*.js'",
+ "test": "c8 tape 'test/*.js'",
+ "posttest": "eslint .",
+ "fix": "npm run posttest -- --fix"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git@github.com:pkgjs/parseargs.git"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/pkgjs/parseargs/issues"
+ },
+ "homepage": "https://github.com/pkgjs/parseargs#readme",
+ "devDependencies": {
+ "c8": "^7.10.0",
+ "eslint": "^8.2.0",
+ "eslint-plugin-node-core": "iansu/eslint-plugin-node-core",
+ "tape": "^5.2.2"
+ }
+}
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/utils.js b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/utils.js
new file mode 100644
index 00000000..d7f420a2
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@pkgjs/parseargs/utils.js
@@ -0,0 +1,198 @@
+'use strict';
+
+const {
+ ArrayPrototypeFind,
+ ObjectEntries,
+ ObjectPrototypeHasOwnProperty: ObjectHasOwn,
+ StringPrototypeCharAt,
+ StringPrototypeIncludes,
+ StringPrototypeStartsWith,
+} = require('./internal/primordials');
+
+const {
+ validateObject,
+} = require('./internal/validators');
+
+// These are internal utilities to make the parsing logic easier to read, and
+// add lots of detail for the curious. They are in a separate file to allow
+// unit testing, although that is not essential (this could be rolled into
+// main file and just tested implicitly via API).
+//
+// These routines are for internal use, not for export to client.
+
+/**
+ * Return the named property, but only if it is an own property.
+ */
+function objectGetOwn(obj, prop) {
+ if (ObjectHasOwn(obj, prop))
+ return obj[prop];
+}
+
+/**
+ * Return the named options property, but only if it is an own property.
+ */
+function optionsGetOwn(options, longOption, prop) {
+ if (ObjectHasOwn(options, longOption))
+ return objectGetOwn(options[longOption], prop);
+}
+
+/**
+ * Determines if the argument may be used as an option value.
+ * @example
+ * isOptionValue('V') // returns true
+ * isOptionValue('-v') // returns true (greedy)
+ * isOptionValue('--foo') // returns true (greedy)
+ * isOptionValue(undefined) // returns false
+ */
+function isOptionValue(value) {
+ if (value == null) return false;
+
+ // Open Group Utility Conventions are that an option-argument
+ // is the argument after the option, and may start with a dash.
+ return true; // greedy!
+}
+
+/**
+ * Detect whether there is possible confusion and user may have omitted
+ * the option argument, like `--port --verbose` when `port` of type:string.
+ * In strict mode we throw errors if value is option-like.
+ */
+function isOptionLikeValue(value) {
+ if (value == null) return false;
+
+ return value.length > 1 && StringPrototypeCharAt(value, 0) === '-';
+}
+
+/**
+ * Determines if `arg` is just a short option.
+ * @example '-f'
+ */
+function isLoneShortOption(arg) {
+ return arg.length === 2 &&
+ StringPrototypeCharAt(arg, 0) === '-' &&
+ StringPrototypeCharAt(arg, 1) !== '-';
+}
+
+/**
+ * Determines if `arg` is a lone long option.
+ * @example
+ * isLoneLongOption('a') // returns false
+ * isLoneLongOption('-a') // returns false
+ * isLoneLongOption('--foo') // returns true
+ * isLoneLongOption('--foo=bar') // returns false
+ */
+function isLoneLongOption(arg) {
+ return arg.length > 2 &&
+ StringPrototypeStartsWith(arg, '--') &&
+ !StringPrototypeIncludes(arg, '=', 3);
+}
+
+/**
+ * Determines if `arg` is a long option and value in the same argument.
+ * @example
+ * isLongOptionAndValue('--foo') // returns false
+ * isLongOptionAndValue('--foo=bar') // returns true
+ */
+function isLongOptionAndValue(arg) {
+ return arg.length > 2 &&
+ StringPrototypeStartsWith(arg, '--') &&
+ StringPrototypeIncludes(arg, '=', 3);
+}
+
+/**
+ * Determines if `arg` is a short option group.
+ *
+ * See Guideline 5 of the [Open Group Utility Conventions](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html).
+ * One or more options without option-arguments, followed by at most one
+ * option that takes an option-argument, should be accepted when grouped
+ * behind one '-' delimiter.
+ * @example
+ * isShortOptionGroup('-a', {}) // returns false
+ * isShortOptionGroup('-ab', {}) // returns true
+ * // -fb is an option and a value, not a short option group
+ * isShortOptionGroup('-fb', {
+ * options: { f: { type: 'string' } }
+ * }) // returns false
+ * isShortOptionGroup('-bf', {
+ * options: { f: { type: 'string' } }
+ * }) // returns true
+ * // -bfb is an edge case, return true and caller sorts it out
+ * isShortOptionGroup('-bfb', {
+ * options: { f: { type: 'string' } }
+ * }) // returns true
+ */
+function isShortOptionGroup(arg, options) {
+ if (arg.length <= 2) return false;
+ if (StringPrototypeCharAt(arg, 0) !== '-') return false;
+ if (StringPrototypeCharAt(arg, 1) === '-') return false;
+
+ const firstShort = StringPrototypeCharAt(arg, 1);
+ const longOption = findLongOptionForShort(firstShort, options);
+ return optionsGetOwn(options, longOption, 'type') !== 'string';
+}
+
+/**
+ * Determine if arg is a short string option followed by its value.
+ * @example
+ * isShortOptionAndValue('-a', {}); // returns false
+ * isShortOptionAndValue('-ab', {}); // returns false
+ * isShortOptionAndValue('-fFILE', {
+ * options: { foo: { short: 'f', type: 'string' }}
+ * }) // returns true
+ */
+function isShortOptionAndValue(arg, options) {
+ validateObject(options, 'options');
+
+ if (arg.length <= 2) return false;
+ if (StringPrototypeCharAt(arg, 0) !== '-') return false;
+ if (StringPrototypeCharAt(arg, 1) === '-') return false;
+
+ const shortOption = StringPrototypeCharAt(arg, 1);
+ const longOption = findLongOptionForShort(shortOption, options);
+ return optionsGetOwn(options, longOption, 'type') === 'string';
+}
+
+/**
+ * Find the long option associated with a short option. Looks for a configured
+ * `short` and returns the short option itself if a long option is not found.
+ * @example
+ * findLongOptionForShort('a', {}) // returns 'a'
+ * findLongOptionForShort('b', {
+ * options: { bar: { short: 'b' } }
+ * }) // returns 'bar'
+ */
+function findLongOptionForShort(shortOption, options) {
+ validateObject(options, 'options');
+ const longOptionEntry = ArrayPrototypeFind(
+ ObjectEntries(options),
+ ({ 1: optionConfig }) => objectGetOwn(optionConfig, 'short') === shortOption
+ );
+ return longOptionEntry?.[0] ?? shortOption;
+}
+
+/**
+ * Check if the given option includes a default value
+ * and that option has not been set by the input args.
+ *
+ * @param {string} longOption - long option name e.g. 'foo'
+ * @param {object} optionConfig - the option configuration properties
+ * @param {object} values - option values returned in `values` by parseArgs
+ */
+function useDefaultValueOption(longOption, optionConfig, values) {
+ return objectGetOwn(optionConfig, 'default') !== undefined &&
+ values[longOption] === undefined;
+}
+
+module.exports = {
+ findLongOptionForShort,
+ isLoneLongOption,
+ isLoneShortOption,
+ isLongOptionAndValue,
+ isOptionValue,
+ isOptionLikeValue,
+ isShortOptionAndValue,
+ isShortOptionGroup,
+ useDefaultValueOption,
+ objectGetOwn,
+ optionsGetOwn,
+};
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@rollup/rollup-linux-x64-gnu/README.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@rollup/rollup-linux-x64-gnu/README.md
new file mode 100644
index 00000000..cabe280f
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@rollup/rollup-linux-x64-gnu/README.md
@@ -0,0 +1,3 @@
+# `@rollup/rollup-linux-x64-gnu`
+
+This is the **x86_64-unknown-linux-gnu** binary for `rollup`
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@rollup/rollup-linux-x64-gnu/package.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@rollup/rollup-linux-x64-gnu/package.json
new file mode 100644
index 00000000..1ca44457
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@rollup/rollup-linux-x64-gnu/package.json
@@ -0,0 +1,25 @@
+{
+ "name": "@rollup/rollup-linux-x64-gnu",
+ "version": "4.59.0",
+ "os": [
+ "linux"
+ ],
+ "cpu": [
+ "x64"
+ ],
+ "files": [
+ "rollup.linux-x64-gnu.node"
+ ],
+ "description": "Native bindings for Rollup",
+ "author": "Lukas Taegert-Atkinson",
+ "homepage": "https://rollupjs.org/",
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/rollup/rollup.git"
+ },
+ "libc": [
+ "glibc"
+ ],
+ "main": "./rollup.linux-x64-gnu.node"
+}
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@rollup/rollup-linux-x64-gnu/rollup.linux-x64-gnu.node b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@rollup/rollup-linux-x64-gnu/rollup.linux-x64-gnu.node
new file mode 100644
index 00000000..59216bda
Binary files /dev/null and b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@rollup/rollup-linux-x64-gnu/rollup.linux-x64-gnu.node differ
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@rollup/rollup-linux-x64-musl/README.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@rollup/rollup-linux-x64-musl/README.md
new file mode 100644
index 00000000..5848a6c6
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@rollup/rollup-linux-x64-musl/README.md
@@ -0,0 +1,3 @@
+# `@rollup/rollup-linux-x64-musl`
+
+This is the **x86_64-unknown-linux-musl** binary for `rollup`
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@rollup/rollup-linux-x64-musl/package.json b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@rollup/rollup-linux-x64-musl/package.json
new file mode 100644
index 00000000..31ff5f55
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@rollup/rollup-linux-x64-musl/package.json
@@ -0,0 +1,25 @@
+{
+ "name": "@rollup/rollup-linux-x64-musl",
+ "version": "4.59.0",
+ "os": [
+ "linux"
+ ],
+ "cpu": [
+ "x64"
+ ],
+ "files": [
+ "rollup.linux-x64-musl.node"
+ ],
+ "description": "Native bindings for Rollup",
+ "author": "Lukas Taegert-Atkinson",
+ "homepage": "https://rollupjs.org/",
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/rollup/rollup.git"
+ },
+ "libc": [
+ "musl"
+ ],
+ "main": "./rollup.linux-x64-musl.node"
+}
\ No newline at end of file
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@rollup/rollup-linux-x64-musl/rollup.linux-x64-musl.node b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@rollup/rollup-linux-x64-musl/rollup.linux-x64-musl.node
new file mode 100644
index 00000000..0f891abd
Binary files /dev/null and b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@rollup/rollup-linux-x64-musl/rollup.linux-x64-musl.node differ
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/diff/LICENSE b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/diff/LICENSE
new file mode 100644
index 00000000..9e841e7a
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/diff/LICENSE
@@ -0,0 +1,21 @@
+ MIT License
+
+ Copyright (c) Microsoft Corporation.
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/diff/README.md b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/diff/README.md
new file mode 100644
index 00000000..0c319f8a
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/diff/README.md
@@ -0,0 +1,15 @@
+# Installation
+> `npm install --save @types/diff`
+
+# Summary
+This package contains type definitions for diff (https://github.com/kpdecker/jsdiff).
+
+# Details
+Files were exported from https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/diff.
+
+### Additional Details
+ * Last updated: Mon, 07 Oct 2024 22:07:58 GMT
+ * Dependencies: none
+
+# Credits
+These definitions were written by [vvakame](https://github.com/vvakame), [szdc](https://github.com/szdc), [BendingBender](https://github.com/BendingBender), and [Piotr Błażejewicz](https://github.com/peterblazejewicz).
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/diff/index.d.mts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/diff/index.d.mts
new file mode 100644
index 00000000..8e893de1
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/diff/index.d.mts
@@ -0,0 +1 @@
+export * from "./index.js";
diff --git a/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/diff/index.d.ts b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/diff/index.d.ts
new file mode 100644
index 00000000..b55974ee
--- /dev/null
+++ b/sandbox/server/backends/resources/mcp/vendor/local_servers/filesystem/node_modules/@types/diff/index.d.ts
@@ -0,0 +1,415 @@
+export as namespace Diff;
+
+export type Callback = (err: undefined, value?: Change[]) => void;
+
+export interface CallbackOptions {
+ /**
+ * Callback to call with the result instead of returning the result directly.
+ */
+ callback: Callback;
+}
+
+export interface BaseOptions {
+ /**
+ * `true` to ignore casing difference.
+ * @default false
+ */
+ ignoreCase?: boolean | undefined;
+
+ /**
+ * a number specifying the maximum edit distance to consider between the old and new texts. If the edit distance is higher than this, jsdiff will return `undefined` instead of a diff. You can use this to limit the computational cost of diffing large, very different texts by giving up early if the cost will be huge. Works for functions that return change objects and also for `structuredPatch`, but not other patch-generation functions.
+ */
+ maxEditLength?: number | undefined;
+}
+
+export interface WordsOptions extends BaseOptions {
+ /**
+ * `true` to ignore leading and trailing whitespace. This is the same as `diffWords()`.
+ */
+ ignoreWhitespace?: boolean | undefined;
+}
+
+export interface LinesOptions extends BaseOptions {
+ /**
+ * `true` to ignore leading and trailing whitespace. This is the same as `diffTrimmedLines()`.
+ */
+ ignoreWhitespace?: boolean | undefined;
+
+ /**
+ * `true` to treat newline characters as separate tokens. This allows for changes to the newline structure
+ * to occur independently of the line content and to be treated as such. In general this is the more
+ * human friendly form of `diffLines()` and `diffLines()` is better suited for patches and other computer
+ * friendly output.
+ */
+ newlineIsToken?: boolean | undefined;
+}
+
+export interface JsonOptions extends LinesOptions {
+ /**
+ * Replacer used to stringify the properties of the passed objects.
+ */
+ stringifyReplacer?: ((key: string, value: any) => any) | undefined;
+
+ /**
+ * The value to use when `undefined` values in the passed objects are encountered during stringification.
+ * Will only be used if `stringifyReplacer` option wasn't specified.
+ * @default undefined
+ */
+ undefinedReplacement?: any;
+}
+
+export interface ArrayOptions extends BaseOptions {
+ /**
+ * Comparator for custom equality checks.
+ */
+ comparator?: ((left: TLeft, right: TRight) => boolean) | undefined;
+}
+
+export interface PatchOptions extends LinesOptions {
+ /**
+ * Describes how many lines of context should be included.
+ * @default 4
+ */
+ context?: number | undefined;
+}
+
+export interface ApplyPatchOptions {
+ /**
+ * Number of lines that are allowed to differ before rejecting a patch.
+ * @default 0
+ */
+ fuzzFactor?: number | undefined;
+
+ /**
+ * Callback used to compare to given lines to determine if they should be considered equal when patching.
+ * Should return `false` if the lines should be rejected.
+ *
+ * @default strict equality
+ */
+ compareLine?:
+ | ((
+ lineNumber: number,
+ line: string,
+ operation: "-" | " ",
+ patchContent: string,
+ ) => boolean)
+ | undefined;
+}
+
+export interface ApplyPatchesOptions extends ApplyPatchOptions {
+ loadFile(index: ParsedDiff, callback: (err: any, data: string) => void): void;
+ patched(index: ParsedDiff, content: string, callback: (err: any) => void): void;
+ complete(err: any): void;
+}
+
+export interface Change {
+ count?: number | undefined;
+ /**
+ * Text content.
+ */
+ value: string;
+ /**
+ * `true` if the value was inserted into the new string.
+ */
+ added?: boolean | undefined;
+ /**
+ * `true` if the value was removed from the old string.
+ */
+ removed?: boolean | undefined;
+}
+
+export interface ArrayChange {
+ value: T[];
+ count?: number | undefined;
+ added?: boolean | undefined;
+ removed?: boolean | undefined;
+}
+
+export interface ParsedDiff {
+ index?: string | undefined;
+ oldFileName?: string | undefined;
+ newFileName?: string | undefined;
+ oldHeader?: string | undefined;
+ newHeader?: string | undefined;
+ hunks: Hunk[];
+}
+
+export interface Hunk {
+ oldStart: number;
+ oldLines: number;
+ newStart: number;
+ newLines: number;
+ lines: string[];
+ // Line Delimiters is only returned by parsePatch()
+ linedelimiters?: string[];
+}
+
+export interface BestPath {
+ newPos: number;
+ components: Change[];
+}
+
+export class Diff {
+ diff(
+ oldString: string,
+ newString: string,
+ options?: Callback | (ArrayOptions & Partial),
+ ): Change[];
+
+ pushComponent(components: Change[], added: boolean, removed: boolean): void;
+
+ extractCommon(
+ basePath: BestPath,
+ newString: string,
+ oldString: string,
+ diagonalPath: number,
+ ): number;
+
+ equals(left: any, right: any): boolean;
+
+ removeEmpty(array: any[]): any[];
+
+ castInput(value: any): any;
+
+ join(chars: string[]): string;
+
+ tokenize(value: string): any; // return types are string or string[]
+}
+
+/**
+ * Diffs two blocks of text, comparing character by character.
+ *
+ * @returns A list of change objects.
+ */
+export function diffChars(oldStr: string, newStr: string, options?: BaseOptions): Change[];
+export function diffChars(
+ oldStr: string,
+ newStr: string,
+ options: Callback | (BaseOptions & CallbackOptions),
+): void;
+
+/**
+ * Diffs two blocks of text, comparing word by word, ignoring whitespace.
+ *
+ * @returns A list of change objects.
+ */
+export function diffWords(oldStr: string, newStr: string, options?: WordsOptions): Change[];
+export function diffWords(
+ oldStr: string,
+ newStr: string,
+ options: Callback | (WordsOptions & CallbackOptions),
+): void;
+
+/**
+ * Diffs two blocks of text, comparing word by word, treating whitespace as significant.
+ *
+ * @returns A list of change objects.
+ */
+export function diffWordsWithSpace(
+ oldStr: string,
+ newStr: string,
+ options?: WordsOptions,
+): Change[];
+export function diffWordsWithSpace(
+ oldStr: string,
+ newStr: string,
+ options: Callback | (WordsOptions & CallbackOptions),
+): void;
+
+/**
+ * Diffs two blocks of text, comparing line by line.
+ *
+ * @returns A list of change objects.
+ */
+export function diffLines(oldStr: string, newStr: string, options?: LinesOptions): Change[];
+export function diffLines(
+ oldStr: string,
+ newStr: string,
+ options: Callback | (LinesOptions & CallbackOptions),
+): void;
+
+/**
+ * Diffs two blocks of text, comparing line by line, ignoring leading and trailing whitespace.
+ *
+ * @returns A list of change objects.
+ */
+export function diffTrimmedLines(oldStr: string, newStr: string, options?: LinesOptions): Change[];
+export function diffTrimmedLines(
+ oldStr: string,
+ newStr: string,
+ options: Callback | (LinesOptions & CallbackOptions),
+): void;
+
+/**
+ * Diffs two blocks of text, comparing sentence by sentence.
+ *
+ * @returns A list of change objects.
+ */
+export function diffSentences(oldStr: string, newStr: string, options?: BaseOptions): Change[];
+export function diffSentences(
+ oldStr: string,
+ newStr: string,
+ options: Callback | (BaseOptions & CallbackOptions),
+): void;
+
+/**
+ * Diffs two blocks of text, comparing CSS tokens.
+ *
+ * @returns A list of change objects.
+ */
+export function diffCss(oldStr: string, newStr: string, options?: BaseOptions): Change[];
+export function diffCss(
+ oldStr: string,
+ newStr: string,
+ options: Callback | (BaseOptions & CallbackOptions),
+): void;
+
+/**
+ * Diffs two JSON objects, comparing the fields defined on each. The order of fields, etc does not matter
+ * in this comparison.
+ *
+ * @returns A list of change objects.
+ */
+export function diffJson(
+ oldObj: string | object,
+ newObj: string | object,
+ options?: JsonOptions,
+): Change[];
+export function diffJson(
+ oldObj: string | object,
+ newObj: string | object,
+ options: Callback | (JsonOptions & CallbackOptions),
+): void;
+
+/**
+ * Diffs two arrays, comparing each item for strict equality (`===`).
+ *
+ * @returns A list of change objects.
+ */
+export function diffArrays(
+ oldArr: TOld[],
+ newArr: TNew[],
+ options?: ArrayOptions,
+): Array