Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package mcp-redmine for openSUSE:Factory 
checked in at 2026-09-08 16:58:23
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/mcp-redmine (Old)
 and      /work/SRC/openSUSE:Factory/.mcp-redmine.new.1265 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "mcp-redmine"

Tue Sep  8 16:58:23 2026 rev:3 rq:1376343 version:2026.9.5.185311

Changes:
--------
--- /work/SRC/openSUSE:Factory/mcp-redmine/mcp-redmine.changes  2026-08-21 
16:55:02.754621523 +0200
+++ /work/SRC/openSUSE:Factory/.mcp-redmine.new.1265/mcp-redmine.changes        
2026-09-08 17:02:02.290802679 +0200
@@ -1,0 +2,16 @@
+Tue Sep  8 09:30:19 UTC 2026 - Martin Pluskal <[email protected]>
+
+- Update to version 2026.9.5.185311:
+  * Refuse model-controlled API paths that escape REDMINE_URL
+    so the API key is not sent to an arbitrary host
+  * Add streamable-http transport (stdio remains the default)
+    and keep legacy SSE for old clients
+  * REDMINE_READ_ONLY=1 refuses non-GET requests
+  * REDMINE_CA_BUNDLE for a private CA
+  * redmine_attachment_image tool returns image bytes
+  * Persistent httpx.Client with keepalive pooling
+  * Ship and run the pytest suite
+- Add mcp-redmine-fastmcp-1.x.patch: keep running on Factory
+  python-mcp 1.28 FastMCP (MCPServer is the 2.x SDK)
+
+-------------------------------------------------------------------

Old:
----
  mcp_redmine-2026.8.1.2543.tar.gz

New:
----
  mcp-redmine-fastmcp-1.x.patch
  mcp_redmine-2026.9.5.185311.tar.gz

----------(New B)----------
  New:  * Ship and run the pytest suite
- Add mcp-redmine-fastmcp-1.x.patch: keep running on Factory
  python-mcp 1.28 FastMCP (MCPServer is the 2.x SDK)
----------(New E)----------

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ mcp-redmine.spec ++++++
--- /var/tmp/diff_new_pack.RpHtv8/_old  2026-09-08 17:02:02.945830058 +0200
+++ /var/tmp/diff_new_pack.RpHtv8/_new  2026-09-08 17:02:02.947830142 +0200
@@ -20,18 +20,21 @@
 # it against the distribution's primary Python flavour.
 %define pythons %{primary_python}
 Name:           mcp-redmine
-Version:        2026.8.1.2543
+Version:        2026.9.5.185311
 Release:        0
 Summary:        Model Context Protocol server for Redmine
 License:        MPL-2.0
 URL:            https://github.com/runekaagaard/mcp-redmine
 Source:         
https://files.pythonhosted.org/packages/source/m/mcp_redmine/mcp_redmine-%{version}.tar.gz
+# PATCH-FIX-OPENSUSE mcp-redmine-fastmcp-1.x.patch [email protected] -- 
Factory python-mcp is still 1.28 FastMCP
+Patch0:         mcp-redmine-fastmcp-1.x.patch
 BuildRequires:  %{primary_python}-PyYAML >= 6.0.2
 BuildRequires:  %{primary_python}-hatchling
 BuildRequires:  %{primary_python}-httpx >= 0.28.1
 BuildRequires:  %{primary_python}-mcp >= 1.9.0
 BuildRequires:  %{primary_python}-openapi-core >= 0.19.4
 BuildRequires:  %{primary_python}-pip
+BuildRequires:  %{primary_python}-pytest
 BuildRequires:  %{primary_python}-wheel
 BuildRequires:  fdupes
 BuildRequires:  python-rpm-macros
@@ -63,10 +66,7 @@
 %fdupes %{buildroot}%{python_sitelib}
 
 %check
-# server.py reads REDMINE_URL/REDMINE_API_KEY at import time; provide dummies
-# for this smoke-import test (upstream ships no test suite)
-REDMINE_URL=http://localhost REDMINE_API_KEY=dummy \
-    PYTHONPATH=%{buildroot}%{python_sitelib} python%{python_version} -Bc 
"import mcp_redmine.server"
+%pytest tests
 
 %files
 %license LICENSE

++++++ mcp-redmine-fastmcp-1.x.patch ++++++
--- a/mcp_redmine/server.py     2026-09-08 11:31:59.728737460 +0200
+++ b/mcp_redmine/server.py     2026-09-08 11:31:59.732737911 +0200
@@ -2,8 +2,8 @@
 from urllib.parse import urljoin
 
 import httpx
-from mcp.server.mcpserver import MCPServer, Image
-from mcp.server.mcpserver.utilities.logging import get_logger
+from mcp.server.fastmcp import FastMCP, Image
+from mcp.server.fastmcp.utilities.logging import get_logger
 
 ### Constants ###
 
@@ -160,7 +160,7 @@
 
 
 # Tools
-mcp = MCPServer("Redmine MCP server", version=VERSION)
+mcp = FastMCP("Redmine MCP server")
 get_logger(__name__).info(f"Starting MCP Redmine version {VERSION}")
 
 @mcp.tool(description="""
@@ -288,7 +288,7 @@
 ATTACHMENT_IMAGE_MAX_BYTES = 5 * 1024 * 1024
 
 @mcp.tool()
-def redmine_attachment_image(attachment_id: int) -> Image | str:
+def redmine_attachment_image(attachment_id: int):
     """
     Fetch an image attachment (e.g. an inline screenshot like 
!screenshot.png!) and return it as viewable
     image content. Use redmine_request on '/issues/{id}.json' with params 
{'include': 'attachments'} to find
@@ -338,10 +338,11 @@
     parser.add_argument("--port", type=int, default=8000, help="Port for HTTP 
transports (default: 8000)")
     args = parser.parse_args()
 
-    if args.transport == "stdio":
-        mcp.run(transport="stdio")
-    else:
-        mcp.run(transport=args.transport, host=args.host, port=args.port)
+    if args.transport != "stdio":
+        # FastMCP 1.x takes host/port on the constructor/settings, not run()
+        mcp.settings.host = args.host
+        mcp.settings.port = args.port
+    mcp.run(transport=args.transport)
 
 if __name__ == "__main__":
     main()

++++++ mcp_redmine-2026.8.1.2543.tar.gz -> mcp_redmine-2026.9.5.185311.tar.gz 
++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/mcp_redmine-2026.8.1.2543/.github/workflows/publish-container.yml 
new/mcp_redmine-2026.9.5.185311/.github/workflows/publish-container.yml
--- old/mcp_redmine-2026.8.1.2543/.github/workflows/publish-container.yml       
1970-01-01 01:00:00.000000000 +0100
+++ new/mcp_redmine-2026.9.5.185311/.github/workflows/publish-container.yml     
2020-02-02 01:00:00.000000000 +0100
@@ -0,0 +1,37 @@
+name: Publish container image
+
+on:
+  push:
+    tags: ["v*"]
+  workflow_dispatch:
+
+permissions:
+  contents: read
+  packages: write
+
+jobs:
+  build-and-push:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+
+      - uses: docker/login-action@v3
+        with:
+          registry: ghcr.io
+          username: ${{ github.actor }}
+          password: ${{ secrets.GITHUB_TOKEN }}
+
+      - uses: docker/metadata-action@v5
+        id: meta
+        with:
+          images: ghcr.io/${{ github.repository }}
+          tags: |
+            type=match,pattern=v(.*),group=1
+            type=raw,value=latest
+
+      - uses: docker/build-push-action@v6
+        with:
+          context: .
+          push: true
+          tags: ${{ steps.meta.outputs.tags }}
+          labels: ${{ steps.meta.outputs.labels }}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/mcp_redmine-2026.8.1.2543/Makefile 
new/mcp_redmine-2026.9.5.185311/Makefile
--- old/mcp_redmine-2026.8.1.2543/Makefile      2020-02-02 01:00:00.000000000 
+0100
+++ new/mcp_redmine-2026.9.5.185311/Makefile    2020-02-02 01:00:00.000000000 
+0100
@@ -1,51 +1,63 @@
 SHELL := /bin/bash
 .SHELLFLAGS := -ec
 
-PROJECT := "mcp-redmine" # overloads as binary name, too
-PACKAGE := "mcp_redmine"
+PROJECT := $(shell grep '^name = ' pyproject.toml | cut -d '"' -f2)
+PACKAGE := $(shell echo $(PROJECT) | tr '-' '_')
+# Pass VERSION explicitly to sub-makes: it is timestamp-based, so a 
re-evaluation
+# in a sub-make would produce a different version than the one 
committed/tagged.
 VERSION := $(shell date +%Y.%m.%d.%H%M%S)
 
 version-bump:
-       sed -i "s/mcp-redmine==[0-9.]*\"/mcp-redmine==$(VERSION)\"/g" README.md
+       sed -i 's/$(PROJECT)==[0-9.]*"/$(PROJECT)==$(VERSION)"/g' README.md
        sed -i "s/version = \"[^\"]*\"/version = \"$(VERSION)\"/" pyproject.toml
        sed -i "s/VERSION = \"[^\"]*\"/VERSION = \"$(VERSION)\"/" 
$(PACKAGE)/server.py
 
 version-bump-claude-desktop:
-       sed -i "s/mcp-redmine==[0-9.]*\"/mcp-redmine==$(VERSION)\"/g" 
~/.config/Claude/claude_desktop_config.json
+       sed -i "s/$(PROJECT)==[0-9.]*\"/$(PROJECT)==$(VERSION)\"/g" 
~/.config/Claude/claude_desktop_config.json
+
+tests-run:
+       uv run --group dev python -m pytest tests/ -q
 
 publish-test:
        rm -rf dist/*
-       $(MAKE) version-bump
+       $(MAKE) version-bump VERSION=$(VERSION)
        uv build
        uv publish --token "$$PYPI_TOKEN_TEST" --publish-url 
https://test.pypi.org/legacy/
        git checkout README.md pyproject.toml $(PACKAGE)/server.py
 
-publish-prod:
+publish-prod: tests-run
        rm -rf dist/*
-       $(MAKE) version-bump
-       $(MAKE) version-bump-claude-desktop
+       $(MAKE) version-bump VERSION=$(VERSION)
+       $(MAKE) version-bump-claude-desktop VERSION=$(VERSION)
        uv build
        uv lock
        uv publish --token "$$PYPI_TOKEN_PROD"
        git commit -am "Published version $(VERSION) to PyPI"
+       git tag "v$(VERSION)"
        git push
+       git push origin "v$(VERSION)"
 
 package-inspect-test:
-       rm -rf /tmp/test-mcp-redmine
-       uv venv /tmp/test-mcp-redmine --python 3.12
-       source /tmp/test-mcp-redmine/bin/activate && uv pip install --index-url 
https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ 
mcp-redmine
-       tree /tmp/test-mcp-redmine/lib/python3.12/site-packages/$(PACKAGE)
-       source /tmp/test-mcp-redmine/bin/activate && which $(PROJECT)
+       rm -rf /tmp/test-$(PROJECT)
+       uv venv /tmp/test-$(PROJECT) --python 3.12
+       source /tmp/test-$(PROJECT)/bin/activate && uv pip install --index-url 
https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ 
$(PROJECT)
+       tree /tmp/test-$(PROJECT)/lib/python3.12/site-packages/$(PACKAGE)
+       source /tmp/test-$(PROJECT)/bin/activate && which $(PROJECT)
 
 package-inspect-prod:
-       rm -rf /tmp/test-mcp-redmine
-       uv venv /tmp/test-mcp-redmine --python 3.12
-       source /tmp/test-mcp-redmine/bin/activate && uv pip install mcp-redmine
-       tree /tmp/test-mcp-redmine/lib/python3.12/site-packages/$(PACKAGE)
-       source /tmp/test-mcp-redmine/bin/activate && which $(PROJECT)
+       rm -rf /tmp/test-$(PROJECT)
+       uv venv /tmp/test-$(PROJECT) --python 3.12
+       source /tmp/test-$(PROJECT)/bin/activate && uv pip install $(PROJECT)
+       tree /tmp/test-$(PROJECT)/lib/python3.12/site-packages/$(PACKAGE)
+       source /tmp/test-$(PROJECT)/bin/activate && which $(PROJECT)
 
 package-run-test:
-       uvx --default-index https://test.pypi.org/simple/ --index 
https://pypi.org/simple/ --from mcp-redmine mcp-redmine
+       uvx --default-index https://test.pypi.org/simple/ --index 
https://pypi.org/simple/ --from $(PROJECT) $(PROJECT)
 
 package-run-prod:
-       uvx --from mcp-redmine mcp-redmine
\ No newline at end of file
+       uvx --from $(PROJECT) $(PROJECT)
+
+debug-constants:
+       @echo "PROJECT='$(PROJECT)'"
+       @echo "PACKAGE='$(PACKAGE)'"
+       @echo "VERSION='$(VERSION)'"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/mcp_redmine-2026.8.1.2543/PKG-INFO 
new/mcp_redmine-2026.9.5.185311/PKG-INFO
--- old/mcp_redmine-2026.8.1.2543/PKG-INFO      2020-02-02 01:00:00.000000000 
+0100
+++ new/mcp_redmine-2026.9.5.185311/PKG-INFO    2020-02-02 01:00:00.000000000 
+0100
@@ -1,6 +1,6 @@
-Metadata-Version: 2.4
+Metadata-Version: 2.5
 Name: mcp-redmine
-Version: 2026.8.1.2543
+Version: 2026.9.5.185311
 Summary: A MCP server that connects to your Redmine instance
 Project-URL: Homepage, https://github.com/runekaagaard/mcp-redmine
 Project-URL: Issues, https://github.com/runekaagaard/mcp-redmine/issues
@@ -11,7 +11,7 @@
 Classifier: Programming Language :: Python :: 3
 Requires-Python: >=3.10
 Requires-Dist: httpx==0.28.1
-Requires-Dist: mcp[cli]==1.25.0
+Requires-Dist: mcp[cli]==2.1.1
 Requires-Dist: openapi-core==0.19.4
 Requires-Dist: pyyaml==6.0.2
 Description-Content-Type: text/markdown
@@ -67,7 +67,7 @@
     "mcpServers": {
       "redmine": {
         "command": "uvx",
-        "args": ["--from", "mcp-redmine==2026.08.01.002543",
+        "args": ["--from", "mcp-redmine==2026.09.05.185311",
                 "--refresh-package", "mcp-redmine", "mcp-redmine"],
         "env": {
           "REDMINE_URL": "https://your-redmine-instance.example.com";,
@@ -87,11 +87,16 @@
 docker --version
 ```
 
-Build docker image:
+Pull the published image from GitHub Container Registry:
+```bash
+docker pull ghcr.io/runekaagaard/mcp-redmine:latest
+```
+
+Or build the image yourself:
 ```bash
 git clone [email protected]:runekaagaard/mcp-redmine.git
 cd mcp-redmine
-docker build -t mcp-redmine .
+docker build -t ghcr.io/runekaagaard/mcp-redmine .
 ```
 Add to your `claude_desktop_config.json`:
   ```json
@@ -109,7 +114,7 @@
             "-e", "REDMINE_ALLOWED_DIRECTORIES",
             "-v", "/path/to/instructions.md:/app/INSTRUCTIONS.md",
             "-v", "/path/to/uploads:/app/uploads",
-            "mcp-redmine"
+            "ghcr.io/runekaagaard/mcp-redmine:latest"
         ],
         "env": {
           "REDMINE_URL": "https://your-redmine-instance.example.com";,
@@ -122,6 +127,23 @@
   }
   ```
 
+### 3. Other MCP clients and transports
+
+MCP Redmine works with any MCP client (Claude Code, Cursor, IntelliJ-based 
IDEs, n8n, etc.) - point your
+client at the same command and environment variables as above, following your 
client's documentation for
+registering MCP servers.
+
+By default the server speaks stdio. For clients or setups that connect over 
HTTP, start the server with one
+of the HTTP transports:
+
+```bash
+# Recommended HTTP transport (serves on http://HOST:PORT/mcp)
+mcp-redmine --transport streamable-http --host 0.0.0.0 --port 8000
+
+# Legacy SSE transport, for older clients (serves on http://HOST:PORT/sse)
+mcp-redmine --transport sse --host 0.0.0.0 --port 8000
+```
+
 ## Environment Variables
 
 | Variable | Required | Default | Description |
@@ -133,6 +155,8 @@
 | `REDMINE_RESPONSE_FORMAT` | No | `yaml` | Response format: `yaml` or `json`. 
Controls how API responses are formatted |
 | `REDMINE_ALLOWED_DIRECTORIES` | For upload/download | (disabled) | 
**Required for file operations.** Comma-separated list of directories where 
upload/download are allowed (e.g., `/tmp,/home/user/uploads`). Upload/download 
are disabled if not set for security |
 | `REDMINE_DANGEROUSLY_ACCEPT_INVALID_CERTS` | No | (disabled) | Set to `1` to 
disable SSL certificate verification. Use only for self-signed certs in trusted 
environments |
+| `REDMINE_CA_BUNDLE` | No | (system CAs) | Path to a custom CA certificate 
bundle (e.g. `/path/to/ca.crt`) for Redmine instances using a private 
certificate chain. Prefer this over disabling verification |
+| `REDMINE_READ_ONLY` | No | (disabled) | Set to `1` to only allow GET 
requests. All writing operations (POST/PUT/DELETE, including uploads) are 
refused |
 
 > **Note**: When running via Docker, the `REDMINE_REQUEST_INSTRUCTIONS` 
 > environment variable must point to a **path inside the container**, not a 
 > path on the host machine.
 > Therefore, if you want to use a local file, you need to **mount it into the 
 > container** at the correct location.
@@ -210,6 +234,12 @@
   error: ""
   ```
 
+- **redmine_attachment_image**
+  - Fetch an image attachment (e.g. an inline screenshot referenced as 
`!screenshot.png!` in an issue
+    description) and return it as viewable image content, letting the LLM 
actually see it
+  - Input: `attachment_id` (integer)
+  - Returns image content on success (max 5 MB, images only), or a YAML error 
string on failure
+
 - **redmine_download**
   - Download an attachment from Redmine and save it to a local file
   - **Requires `REDMINE_ALLOWED_DIRECTORIES` to be set**
@@ -279,7 +309,7 @@
 ```
 ...
 "command": "uv",
-"args": ["run", "--directory", "/path/to/mcp-redmine", "-m", 
"mcp_redmine.server", "main"],
+"args": ["run", "--directory", "/path/to/mcp-redmine", "-m", 
"mcp_redmine.server"],
 ...
 ```
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/mcp_redmine-2026.8.1.2543/README.md 
new/mcp_redmine-2026.9.5.185311/README.md
--- old/mcp_redmine-2026.8.1.2543/README.md     2020-02-02 01:00:00.000000000 
+0100
+++ new/mcp_redmine-2026.9.5.185311/README.md   2020-02-02 01:00:00.000000000 
+0100
@@ -49,7 +49,7 @@
     "mcpServers": {
       "redmine": {
         "command": "uvx",
-        "args": ["--from", "mcp-redmine==2026.08.01.002543",
+        "args": ["--from", "mcp-redmine==2026.09.05.185311",
                 "--refresh-package", "mcp-redmine", "mcp-redmine"],
         "env": {
           "REDMINE_URL": "https://your-redmine-instance.example.com";,
@@ -69,11 +69,16 @@
 docker --version
 ```
 
-Build docker image:
+Pull the published image from GitHub Container Registry:
+```bash
+docker pull ghcr.io/runekaagaard/mcp-redmine:latest
+```
+
+Or build the image yourself:
 ```bash
 git clone [email protected]:runekaagaard/mcp-redmine.git
 cd mcp-redmine
-docker build -t mcp-redmine .
+docker build -t ghcr.io/runekaagaard/mcp-redmine .
 ```
 Add to your `claude_desktop_config.json`:
   ```json
@@ -91,7 +96,7 @@
             "-e", "REDMINE_ALLOWED_DIRECTORIES",
             "-v", "/path/to/instructions.md:/app/INSTRUCTIONS.md",
             "-v", "/path/to/uploads:/app/uploads",
-            "mcp-redmine"
+            "ghcr.io/runekaagaard/mcp-redmine:latest"
         ],
         "env": {
           "REDMINE_URL": "https://your-redmine-instance.example.com";,
@@ -104,6 +109,23 @@
   }
   ```
 
+### 3. Other MCP clients and transports
+
+MCP Redmine works with any MCP client (Claude Code, Cursor, IntelliJ-based 
IDEs, n8n, etc.) - point your
+client at the same command and environment variables as above, following your 
client's documentation for
+registering MCP servers.
+
+By default the server speaks stdio. For clients or setups that connect over 
HTTP, start the server with one
+of the HTTP transports:
+
+```bash
+# Recommended HTTP transport (serves on http://HOST:PORT/mcp)
+mcp-redmine --transport streamable-http --host 0.0.0.0 --port 8000
+
+# Legacy SSE transport, for older clients (serves on http://HOST:PORT/sse)
+mcp-redmine --transport sse --host 0.0.0.0 --port 8000
+```
+
 ## Environment Variables
 
 | Variable | Required | Default | Description |
@@ -115,6 +137,8 @@
 | `REDMINE_RESPONSE_FORMAT` | No | `yaml` | Response format: `yaml` or `json`. 
Controls how API responses are formatted |
 | `REDMINE_ALLOWED_DIRECTORIES` | For upload/download | (disabled) | 
**Required for file operations.** Comma-separated list of directories where 
upload/download are allowed (e.g., `/tmp,/home/user/uploads`). Upload/download 
are disabled if not set for security |
 | `REDMINE_DANGEROUSLY_ACCEPT_INVALID_CERTS` | No | (disabled) | Set to `1` to 
disable SSL certificate verification. Use only for self-signed certs in trusted 
environments |
+| `REDMINE_CA_BUNDLE` | No | (system CAs) | Path to a custom CA certificate 
bundle (e.g. `/path/to/ca.crt`) for Redmine instances using a private 
certificate chain. Prefer this over disabling verification |
+| `REDMINE_READ_ONLY` | No | (disabled) | Set to `1` to only allow GET 
requests. All writing operations (POST/PUT/DELETE, including uploads) are 
refused |
 
 > **Note**: When running via Docker, the `REDMINE_REQUEST_INSTRUCTIONS` 
 > environment variable must point to a **path inside the container**, not a 
 > path on the host machine.
 > Therefore, if you want to use a local file, you need to **mount it into the 
 > container** at the correct location.
@@ -192,6 +216,12 @@
   error: ""
   ```
 
+- **redmine_attachment_image**
+  - Fetch an image attachment (e.g. an inline screenshot referenced as 
`!screenshot.png!` in an issue
+    description) and return it as viewable image content, letting the LLM 
actually see it
+  - Input: `attachment_id` (integer)
+  - Returns image content on success (max 5 MB, images only), or a YAML error 
string on failure
+
 - **redmine_download**
   - Download an attachment from Redmine and save it to a local file
   - **Requires `REDMINE_ALLOWED_DIRECTORIES` to be set**
@@ -261,7 +291,7 @@
 ```
 ...
 "command": "uv",
-"args": ["run", "--directory", "/path/to/mcp-redmine", "-m", 
"mcp_redmine.server", "main"],
+"args": ["run", "--directory", "/path/to/mcp-redmine", "-m", 
"mcp_redmine.server"],
 ...
 ```
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/mcp_redmine-2026.8.1.2543/mcp_redmine/server.py 
new/mcp_redmine-2026.9.5.185311/mcp_redmine/server.py
--- old/mcp_redmine-2026.8.1.2543/mcp_redmine/server.py 2020-02-02 
01:00:00.000000000 +0100
+++ new/mcp_redmine-2026.9.5.185311/mcp_redmine/server.py       2020-02-02 
01:00:00.000000000 +0100
@@ -2,12 +2,12 @@
 from urllib.parse import urljoin
 
 import httpx
-from mcp.server.fastmcp import FastMCP
-from mcp.server.fastmcp.utilities.logging import get_logger
+from mcp.server.mcpserver import MCPServer, Image
+from mcp.server.mcpserver.utilities.logging import get_logger
 
 ### Constants ###
 
-VERSION = "2026.08.01.002543"
+VERSION = "2026.09.05.185311"
 
 # Load OpenAPI spec
 current_dir = pathlib.Path(__file__).parent
@@ -37,6 +37,30 @@
 # SSL verification (disabled only when explicitly set to "1")
 REDMINE_DANGEROUSLY_ACCEPT_INVALID_CERTS = 
os.environ.get('REDMINE_DANGEROUSLY_ACCEPT_INVALID_CERTS') == '1'
 
+# Custom CA bundle for private certificate chains (path to a ca.crt / bundle 
file)
+REDMINE_CA_BUNDLE = os.environ.get('REDMINE_CA_BUNDLE', '')
+
+# Read-only mode (enabled when set to "1") - only GET requests are allowed
+REDMINE_READ_ONLY = os.environ.get('REDMINE_READ_ONLY') == '1'
+
+if REDMINE_DANGEROUSLY_ACCEPT_INVALID_CERTS:
+    _ssl_verify = False
+elif REDMINE_CA_BUNDLE:
+    _ssl_verify = REDMINE_CA_BUNDLE
+else:
+    _ssl_verify = True
+
+# Persistent HTTP client — reuses TCP/TLS connections across calls instead of 
opening a new one each time.
+# Using httpx.request() (top-level function) creates a new connection per 
call, which adds ~2 minutes of
+# TLS handshake overhead on each request when connecting to internal/corporate 
Redmine servers.
+# keepalive_expiry=120 keeps connections alive for 2 minutes; the httpx 
default of 5s means every call
+# after a short pause pays a full ~600ms TLS reconnect cost.
+_http_client = httpx.Client(
+    timeout=60.0,
+    verify=_ssl_verify,
+    limits=httpx.Limits(max_keepalive_connections=5, keepalive_expiry=120),
+)
+
 if "REDMINE_REQUEST_INSTRUCTIONS" in os.environ:
     with open(os.environ["REDMINE_REQUEST_INSTRUCTIONS"]) as f:
         REDMINE_REQUEST_INSTRUCTIONS = f.read()
@@ -47,16 +71,27 @@
 # Core
 def request(path: str, method: str = 'get', data: dict = None, params: dict = 
None,
             content_type: str = 'application/json', content: bytes = None) -> 
dict:
+    if REDMINE_READ_ONLY and method.lower() != 'get':
+        return {"status_code": 0, "body": None,
+                "error": f"REDMINE_READ_ONLY is enabled: refusing 
{method.upper()} request"}
+
     headers = {
         'X-Redmine-API-Key': REDMINE_API_KEY,
         'Content-Type': content_type,
         **REDMINE_HEADERS
     }
+
+    # Security: path is model-controlled. urljoin returns absolute URLs in 
`path` unchanged, which would
+    # redirect the request (and the API key header) to an arbitrary host. Only 
ever allow URLs that stay
+    # under REDMINE_URL (which is normalized to end with '/'), also catching 
'../' path traversal.
     url = urljoin(REDMINE_URL, path.lstrip('/'))
+    if not url.startswith(REDMINE_URL):
+        return {"status_code": 0, "body": None,
+                "error": f"Path escapes REDMINE_URL, refusing to send API key 
to: {url}"}
 
     try:
-        response = httpx.request(method=method.lower(), url=url, json=data, 
params=params, headers=headers,
-                                 content=content, timeout=60.0, verify=not 
REDMINE_DANGEROUSLY_ACCEPT_INVALID_CERTS)
+        response = _http_client.request(method=method.lower(), url=url, 
json=data, params=params, headers=headers,
+                                       content=content)
         response.raise_for_status()
 
         body = None
@@ -125,7 +160,7 @@
 
 
 # Tools
-mcp = FastMCP("Redmine MCP server")
+mcp = MCPServer("Redmine MCP server", version=VERSION)
 get_logger(__name__).info(f"Starting MCP Redmine version {VERSION}")
 
 @mcp.tool(description="""
@@ -249,20 +284,64 @@
     except Exception as e:
         return format_response({"status_code": 0, "body": None, "error": 
f"{e.__class__.__name__}: {e}"})
 
+# Max size for images returned inline as tool content (base64 roughly x1.33, 
so keep this modest)
+ATTACHMENT_IMAGE_MAX_BYTES = 5 * 1024 * 1024
+
[email protected]()
+def redmine_attachment_image(attachment_id: int) -> Image | str:
+    """
+    Fetch an image attachment (e.g. an inline screenshot like 
!screenshot.png!) and return it as viewable
+    image content. Use redmine_request on '/issues/{id}.json' with params 
{'include': 'attachments'} to find
+    attachment ids.
+
+    Args:
+        attachment_id: The ID of the image attachment to fetch
+
+    Returns:
+        Image content on success, or a YAML error string on failure
+    """
+    try:
+        attachment_response = request(f"attachments/{attachment_id}.json", 
"get")
+        if attachment_response["status_code"] != 200:
+            return format_response(attachment_response)
+
+        attachment = attachment_response["body"]["attachment"]
+        content_type = attachment.get("content_type") or ""
+        if not content_type.startswith("image/"):
+            return format_response({
+                "status_code": 0, "body": None,
+                "error": f"Attachment is not an image (content_type: 
{content_type}). "
+                         "Use redmine_download to save it to disk instead."})
+        if attachment.get("filesize", 0) > ATTACHMENT_IMAGE_MAX_BYTES:
+            return format_response({
+                "status_code": 0, "body": None,
+                "error": f"Image too large ({attachment['filesize']} bytes, 
max {ATTACHMENT_IMAGE_MAX_BYTES}). "
+                         "Use redmine_download to save it to disk instead."})
+
+        response = 
request(f"attachments/download/{attachment_id}/{attachment['filename']}", "get",
+                           content_type="application/octet-stream")
+        if response["status_code"] != 200 or not response["body"]:
+            return format_response(response)
+
+        return Image(data=response["body"], 
format=content_type.removeprefix("image/"))
+    except Exception as e:
+        return format_response({"status_code": 0, "body": None, "error": 
f"{e.__class__.__name__}: {e}"})
+
 def main():
     """Main entry point for the mcp-redmine package."""
     import argparse
     parser = argparse.ArgumentParser(description="MCP Redmine Server")
-    parser.add_argument("--transport", choices=["stdio", "sse"], 
default="stdio",
-                        help="Transport type (default: stdio)")
-    parser.add_argument("--host", default="0.0.0.0", help="Host for SSE 
transport (default: 0.0.0.0)")
-    parser.add_argument("--port", type=int, default=8000, help="Port for SSE 
transport (default: 8000)")
+    parser.add_argument("--transport", choices=["stdio", "streamable-http", 
"sse"], default="stdio",
+                        help="Transport type (default: stdio). streamable-http 
is the recommended HTTP "
+                             "transport, sse is supported for legacy clients.")
+    parser.add_argument("--host", default="0.0.0.0", help="Host for HTTP 
transports (default: 0.0.0.0)")
+    parser.add_argument("--port", type=int, default=8000, help="Port for HTTP 
transports (default: 8000)")
     args = parser.parse_args()
 
-    if args.transport == "sse":
-        mcp.settings.host = args.host
-        mcp.settings.port = args.port
-    mcp.run(transport=args.transport)
+    if args.transport == "stdio":
+        mcp.run(transport="stdio")
+    else:
+        mcp.run(transport=args.transport, host=args.host, port=args.port)
 
 if __name__ == "__main__":
     main()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/mcp_redmine-2026.8.1.2543/pyproject.toml 
new/mcp_redmine-2026.9.5.185311/pyproject.toml
--- old/mcp_redmine-2026.8.1.2543/pyproject.toml        2020-02-02 
01:00:00.000000000 +0100
+++ new/mcp_redmine-2026.9.5.185311/pyproject.toml      2020-02-02 
01:00:00.000000000 +0100
@@ -1,12 +1,12 @@
 [project]
 name = "mcp-redmine"
-version = "2026.08.01.002543"
+version = "2026.09.05.185311"
 description = "A MCP server that connects to your Redmine instance"
 readme = "README.md"
 requires-python = ">=3.10"
 dependencies = [
     "httpx==0.28.1",
-    "mcp[cli]==1.25.0",
+    "mcp[cli]==2.1.1",
     "openapi-core==0.19.4",
     "pyyaml==6.0.2",
 ]
@@ -39,6 +39,7 @@
 
 [dependency-groups]
 dev = [
+    "pytest==8.3.4",
     "build==1.2.2.post1",
     "hatchling==1.27.0",
 ]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/mcp_redmine-2026.8.1.2543/tests/test_server.py 
new/mcp_redmine-2026.9.5.185311/tests/test_server.py
--- old/mcp_redmine-2026.8.1.2543/tests/test_server.py  1970-01-01 
01:00:00.000000000 +0100
+++ new/mcp_redmine-2026.9.5.185311/tests/test_server.py        2020-02-02 
01:00:00.000000000 +0100
@@ -0,0 +1,151 @@
+"""Tests for mcp_redmine.server. Run with: make tests-run"""
+import os
+
+# Server module reads config from environment at import time
+os.environ.setdefault("REDMINE_URL", "https://redmine.example.org";)
+os.environ.setdefault("REDMINE_API_KEY", "test-api-key-123")
+
+import pytest
+
+from mcp_redmine import server
+
+
+class FakeResponse:
+    def __init__(self, status_code=200, json_body=None, content=b""):
+        self.status_code = status_code
+        self._json_body = json_body
+        self.content = content if not json_body else b"x"
+
+    def raise_for_status(self):
+        pass
+
+    def json(self):
+        if self._json_body is None:
+            raise ValueError("no json")
+        return self._json_body
+
+
[email protected]
+def capture_requests(monkeypatch):
+    """Capture outgoing requests instead of hitting the network."""
+    calls = []
+
+    def fake_request(method, url, **kwargs):
+        calls.append({"method": method, "url": url, **kwargs})
+        return FakeResponse(json_body={"ok": True})
+
+    monkeypatch.setattr(server._http_client, "request", fake_request)
+    return calls
+
+
+# Security: model-controlled `path` must never escape REDMINE_URL (issue #44)
+
+def test_normal_path_stays_on_redmine(capture_requests):
+    result = server.request("issues.json")
+    assert result["status_code"] == 200
+    assert capture_requests[0]["url"] == 
"https://redmine.example.org/issues.json";
+
+
+def test_leading_slash_path_stays_on_redmine(capture_requests):
+    server.request("/issues.json")
+    assert capture_requests[0]["url"] == 
"https://redmine.example.org/issues.json";
+
+
[email protected]("path", [
+    "https://attacker.example/collect";,
+    "http://attacker.example/collect";,
+    "https://redmine.example.org.attacker.example/collect";,
+    "ftp://attacker.example/collect";,
+])
+def test_escaping_paths_are_refused(capture_requests, path):
+    result = server.request(path)
+    assert result["status_code"] == 0
+    assert "escapes REDMINE_URL" in result["error"]
+    assert capture_requests == []  # nothing left the building
+
+
[email protected]("path", [
+    "//attacker.example/collect",  # neutralized by lstrip('/')
+    "../../../collect",  # dot segments resolved by urljoin, can't climb above 
the host
+    "/../collect",
+])
+def test_tricky_paths_stay_on_redmine_host(capture_requests, path):
+    server.request(path)
+    assert len(capture_requests) == 1
+    assert 
capture_requests[0]["url"].startswith("https://redmine.example.org/";)
+
+
+def test_api_key_only_sent_to_redmine(capture_requests):
+    server.request("issues.json")
+    assert capture_requests[0]["headers"]["X-Redmine-API-Key"] == 
"test-api-key-123"
+
+
+# Read-only mode (REDMINE_READ_ONLY)
+
+def test_read_only_blocks_writes(capture_requests, monkeypatch):
+    monkeypatch.setattr(server, "REDMINE_READ_ONLY", True)
+    for method in ["post", "put", "PATCH", "delete"]:
+        result = server.request("issues.json", method=method)
+        assert result["status_code"] == 0
+        assert "REDMINE_READ_ONLY" in result["error"]
+    assert capture_requests == []
+
+
+def test_read_only_allows_get(capture_requests, monkeypatch):
+    monkeypatch.setattr(server, "REDMINE_READ_ONLY", True)
+    result = server.request("issues.json", method="get")
+    assert result["status_code"] == 200
+    assert len(capture_requests) == 1
+
+
+def test_writes_allowed_by_default(capture_requests):
+    result = server.request("issues.json", method="post", data={"issue": {}})
+    assert result["status_code"] == 200
+
+
+# Tool plumbing
+
+def test_redmine_request_tool_wraps_insecure_content(capture_requests):
+    result = server.redmine_request("issues.json")
+    assert "<insecure-content-" in result
+    assert "status_code: 200" in result
+
+
+def test_paths_list_returns_spec_paths():
+    result = server.format_response(list(server.SPEC["paths"].keys()))
+    assert "/issues.json" in result
+
+
+def test_attachment_image_rejects_non_image(monkeypatch):
+    def fake_request(path, method="get", **kwargs):
+        return {"status_code": 200, "error": "",
+                "body": {"attachment": {"content_type": "application/pdf", 
"filename": "a.pdf", "filesize": 10}}}
+
+    monkeypatch.setattr(server, "request", fake_request)
+    result = server.redmine_attachment_image(1)
+    assert isinstance(result, str) and "not an image" in result
+
+
+def test_attachment_image_rejects_oversize(monkeypatch):
+    def fake_request(path, method="get", **kwargs):
+        return {"status_code": 200, "error": "",
+                "body": {"attachment": {"content_type": "image/png", 
"filename": "a.png",
+                                        "filesize": 
server.ATTACHMENT_IMAGE_MAX_BYTES + 1}}}
+
+    monkeypatch.setattr(server, "request", fake_request)
+    result = server.redmine_attachment_image(1)
+    assert isinstance(result, str) and "too large" in result
+
+
+def test_attachment_image_returns_image(monkeypatch):
+    png_bytes = b"\x89PNG\r\n\x1a\nfakepngdata"
+
+    def fake_request(path, method="get", **kwargs):
+        if path.endswith(".json"):
+            return {"status_code": 200, "error": "",
+                    "body": {"attachment": {"content_type": "image/png", 
"filename": "a.png", "filesize": 20}}}
+        return {"status_code": 200, "error": "", "body": png_bytes}
+
+    monkeypatch.setattr(server, "request", fake_request)
+    result = server.redmine_attachment_image(1)
+    assert isinstance(result, server.Image)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/mcp_redmine-2026.8.1.2543/uv.lock 
new/mcp_redmine-2026.9.5.185311/uv.lock
--- old/mcp_redmine-2026.8.1.2543/uv.lock       2020-02-02 01:00:00.000000000 
+0100
+++ new/mcp_redmine-2026.9.5.185311/uv.lock     2020-02-02 01:00:00.000000000 
+0100
@@ -1,6 +1,11 @@
 version = 1
 revision = 1
 requires-python = ">=3.10"
+resolution-markers = [
+    "python_full_version >= '3.14'",
+    "python_full_version >= '3.12' and python_full_version < '3.14' and 
sys_platform == 'emscripten'",
+    "(python_full_version < '3.14' and sys_platform != 'emscripten') or 
(python_full_version < '3.12' and sys_platform == 'emscripten')",
+]
 
 [[package]]
 name = "annotated-types"
@@ -13,17 +18,16 @@
 
 [[package]]
 name = "anyio"
-version = "4.8.0"
+version = "4.15.1"
 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'" },
+    { name = "typing-extensions", marker = "python_full_version < '3.15'" },
 ]
-sdist = { url = 
"https://files.pythonhosted.org/packages/a3/73/199a98fc2dae33535d6b8e8e6ec01f8c1d76c9adb096c6b7d64823038cde/anyio-4.8.0.tar.gz";,
 hash = 
"sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a", size 
= 181126 }
+sdist = { url = 
"https://files.pythonhosted.org/packages/a9/d2/f4d173e22df740bc37b1db102b386ba719b66e95b0f0d751f556b387e6d2/anyio-4.15.1.tar.gz";,
 hash = 
"sha256:9f28306018cbd6d329e64a36d58256edff76dd996fe423bc957326e578b82a94", size 
= 276966 }
 wheels = [
-    { url = 
"https://files.pythonhosted.org/packages/46/eb/e7f063ad1fec6b3178a3cd82d1a3c4de82cccf283fc42746168188e1cdd5/anyio-4.8.0-py3-none-any.whl";,
 hash = 
"sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a", size 
= 96041 },
+    { url = 
"https://files.pythonhosted.org/packages/12/b8/4bd346e22b28902df4d651910f5242c28d84e4a5c2435ca5c3f797ed7e2e/anyio-4.15.1-py3-none-any.whl";,
 hash = 
"sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101", size 
= 132079 },
 ]
 
 [[package]]
@@ -305,12 +309,28 @@
 name = "h11"
 version = "0.14.0"
 source = { registry = "https://pypi.org/simple"; }
+resolution-markers = [
+    "python_full_version >= '3.12' and python_full_version < '3.14' and 
sys_platform == 'emscripten'",
+]
 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 = "h11"
+version = "0.16.0"
+source = { registry = "https://pypi.org/simple"; }
+resolution-markers = [
+    "python_full_version >= '3.14'",
+    "(python_full_version < '3.14' and sys_platform != 'emscripten') or 
(python_full_version < '3.12' and sys_platform == 'emscripten')",
+]
+sdist = { url = 
"https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz";,
 hash = 
"sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size 
= 101250 }
+wheels = [
+    { url = 
"https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl";,
 hash = 
"sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size 
= 37515 },
+]
+
+[[package]]
 name = "hatchling"
 version = "1.27.0"
 source = { registry = "https://pypi.org/simple"; }
@@ -330,9 +350,12 @@
 name = "httpcore"
 version = "1.0.7"
 source = { registry = "https://pypi.org/simple"; }
+resolution-markers = [
+    "python_full_version >= '3.12' and python_full_version < '3.14' and 
sys_platform == 'emscripten'",
+]
 dependencies = [
-    { name = "certifi" },
-    { name = "h11" },
+    { name = "certifi", marker = "python_full_version >= '3.12' and 
python_full_version < '3.14' and sys_platform == 'emscripten'" },
+    { name = "h11", version = "0.14.0", source = { registry = 
"https://pypi.org/simple"; }, marker = "python_full_version >= '3.12' and 
python_full_version < '3.14' and sys_platform == 'emscripten'" },
 ]
 sdist = { url = 
"https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz";,
 hash = 
"sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size 
= 85196 }
 wheels = [
@@ -340,13 +363,44 @@
 ]
 
 [[package]]
+name = "httpcore"
+version = "1.0.9"
+source = { registry = "https://pypi.org/simple"; }
+resolution-markers = [
+    "python_full_version >= '3.14'",
+    "(python_full_version < '3.14' and sys_platform != 'emscripten') or 
(python_full_version < '3.12' and sys_platform == 'emscripten')",
+]
+dependencies = [
+    { name = "certifi", marker = "python_full_version < '3.12' or 
python_full_version >= '3.14' or sys_platform != 'emscripten'" },
+    { name = "h11", version = "0.16.0", source = { registry = 
"https://pypi.org/simple"; }, marker = "python_full_version < '3.12' or 
python_full_version >= '3.14' or sys_platform != 'emscripten'" },
+]
+sdist = { url = 
"https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz";,
 hash = 
"sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size 
= 85484 }
+wheels = [
+    { url = 
"https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl";,
 hash = 
"sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size 
= 78784 },
+]
+
+[[package]]
+name = "httpcore2"
+version = "2.12.0"
+source = { registry = "https://pypi.org/simple"; }
+dependencies = [
+    { name = "h11", version = "0.16.0", source = { registry = 
"https://pypi.org/simple"; }, marker = "python_full_version < '3.12' or 
python_full_version >= '3.14' or sys_platform != 'emscripten'" },
+    { name = "truststore", marker = "python_full_version < '3.12' or 
python_full_version >= '3.14' or sys_platform != 'emscripten'" },
+]
+sdist = { url = 
"https://files.pythonhosted.org/packages/be/ad/f4f0e57345f1870f3e8cb624e058d7eca6e5a27d33bcc3311d9b618734cd/httpcore2-2.12.0.tar.gz";,
 hash = 
"sha256:9293522bba0aa7c4c8e9e3f040c16575bd8868e155a77fa30c7a9085a5eae648", size 
= 67548 }
+wheels = [
+    { url = 
"https://files.pythonhosted.org/packages/d2/74/d370e55600d9bcfa0d9794b0166126d49291a3d2b20c268fc98c453a4948/httpcore2-2.12.0-py3-none-any.whl";,
 hash = 
"sha256:7e04258ce01013d7d615e5b910a3b27fac937d7a95038227e79652b4ba3b4ceb", size 
= 83074 },
+]
+
+[[package]]
 name = "httpx"
 version = "0.28.1"
 source = { registry = "https://pypi.org/simple"; }
 dependencies = [
     { name = "anyio" },
     { name = "certifi" },
-    { name = "httpcore" },
+    { name = "httpcore", version = "1.0.7", source = { registry = 
"https://pypi.org/simple"; }, marker = "python_full_version >= '3.12' and 
python_full_version < '3.14' and sys_platform == 'emscripten'" },
+    { name = "httpcore", version = "1.0.9", source = { registry = 
"https://pypi.org/simple"; }, marker = "python_full_version < '3.12' or 
python_full_version >= '3.14' or sys_platform != 'emscripten'" },
     { name = "idna" },
 ]
 sdist = { url = 
"https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz";,
 hash = 
"sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size 
= 141406 }
@@ -355,21 +409,38 @@
 ]
 
 [[package]]
-name = "httpx-sse"
-version = "0.4.0"
+name = "httpx2"
+version = "2.12.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 }
+dependencies = [
+    { name = "anyio", marker = "sys_platform != 'emscripten'" },
+    { name = "httpcore2", marker = "sys_platform != 'emscripten'" },
+    { name = "httpx2-jsfetch", marker = "python_full_version >= '3.12' and 
sys_platform == 'emscripten'" },
+    { name = "idna" },
+    { name = "truststore", marker = "sys_platform != 'emscripten'" },
+    { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = 
"https://files.pythonhosted.org/packages/7f/f8/579a8b51e42e38ee32647df9f08aa25643ae788e275cc625b199829c4671/httpx2-2.12.0.tar.gz";,
 hash = 
"sha256:7631fe9887a8a2275f4a2540e053aa670fcc50742864a9ae7c66e609fdcf12cf", size 
= 100040 }
 wheels = [
-    { url = 
"https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl";,
 hash = 
"sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size 
= 7819 },
+    { url = 
"https://files.pythonhosted.org/packages/c8/95/411ba65569158e862368917aaf56597f3e5fa3b91b0502919638465a08f3/httpx2-2.12.0-py3-none-any.whl";,
 hash = 
"sha256:cc8b6eecb8661c146b8f89a60e97456ee086e91a784ed31ac450c3a9e613dd36", size 
= 95427 },
+]
+
+[[package]]
+name = "httpx2-jsfetch"
+version = "1.0"
+source = { registry = "https://pypi.org/simple"; }
+sdist = { url = 
"https://files.pythonhosted.org/packages/cd/c4/0e5636363151a2a1795e0a77617168b9ca438e1748ec05fc9b5687f93d64/httpx2_jsfetch-1.0.tar.gz";,
 hash = 
"sha256:70a0e3eabfef7cce5ad9c629f7d01ca05e418f586646f4ddf14782e4c1454c60", size 
= 6872 }
+wheels = [
+    { url = 
"https://files.pythonhosted.org/packages/9b/43/832f631d32e4f1211caa2ba368317739fe71f0b8530e4c9d15dc454bac2a/httpx2_jsfetch-1.0-py3-none-any.whl";,
 hash = 
"sha256:cb916b707601e69a07721aabc8f3f6659be3a6893bc1ff5c6f9e02241df2da32", size 
= 6382 },
 ]
 
 [[package]]
 name = "idna"
-version = "3.10"
+version = "3.19"
 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 }
+sdist = { url = 
"https://files.pythonhosted.org/packages/5f/f7/abb373e5757eaec4b922b92f97ec8d6d7e057cf06778247604fbc4e7c3f3/idna-3.19.tar.gz";,
 hash = 
"sha256:5e0811a4383b21dc5838069f801c4fb62113b7447663d2530d2bd6e77b49bf15", size 
= 215237 }
 wheels = [
-    { url = 
"https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl";,
 hash = 
"sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size 
= 70442 },
+    { url = 
"https://files.pythonhosted.org/packages/57/b0/0e52c878c53f245edd3a11020f20979b3f490f245af532c7cae3027754b5/idna-3.19-py3-none-any.whl";,
 hash = 
"sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4", size 
= 68550 },
 ]
 
 [[package]]
@@ -377,7 +448,7 @@
 version = "8.7.0"
 source = { registry = "https://pypi.org/simple"; }
 dependencies = [
-    { name = "zipp" },
+    { name = "zipp", marker = "python_full_version < '3.14'" },
 ]
 sdist = { url = 
"https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz";,
 hash = 
"sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size 
= 56641 }
 wheels = [
@@ -385,6 +456,15 @@
 ]
 
 [[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 }
+wheels = [
+    { url = 
"https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl";,
 hash = 
"sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size 
= 7484 },
+]
+
+[[package]]
 name = "isodate"
 version = "0.7.2"
 source = { registry = "https://pypi.org/simple"; }
@@ -537,27 +617,29 @@
 
 [[package]]
 name = "mcp"
-version = "1.25.0"
+version = "2.1.1"
 source = { registry = "https://pypi.org/simple"; }
 dependencies = [
     { name = "anyio" },
-    { name = "httpx" },
-    { name = "httpx-sse" },
+    { name = "httpx2" },
     { name = "jsonschema" },
+    { name = "mcp-types" },
+    { name = "opentelemetry-api" },
     { name = "pydantic" },
-    { name = "pydantic-settings" },
     { name = "pyjwt", extra = ["crypto"] },
     { name = "python-multipart" },
     { name = "pywin32", marker = "sys_platform == 'win32'" },
-    { name = "sse-starlette" },
-    { name = "starlette" },
+    { name = "sse-starlette", version = "3.0.3", source = { registry = 
"https://pypi.org/simple"; }, marker = "python_full_version < '3.14'" },
+    { name = "sse-starlette", version = "3.4.11", source = { registry = 
"https://pypi.org/simple"; }, marker = "python_full_version >= '3.14'" },
+    { name = "starlette", version = "0.46.0", source = { registry = 
"https://pypi.org/simple"; }, marker = "python_full_version < '3.14'" },
+    { name = "starlette", version = "1.6.0", source = { registry = 
"https://pypi.org/simple"; }, marker = "python_full_version >= '3.14'" },
     { name = "typing-extensions" },
     { name = "typing-inspection" },
     { name = "uvicorn", marker = "sys_platform != 'emscripten'" },
 ]
-sdist = { url = 
"https://files.pythonhosted.org/packages/d5/2d/649d80a0ecf6a1f82632ca44bec21c0461a9d9fc8934d38cb5b319f2db5e/mcp-1.25.0.tar.gz";,
 hash = 
"sha256:56310361ebf0364e2d438e5b45f7668cbb124e158bb358333cd06e49e83a6802", size 
= 605387 }
+sdist = { url = 
"https://files.pythonhosted.org/packages/d4/6e/21fb8e5d579dbe21d96ea4d5034200d46d8bdf2261053b5bd041f3c2f612/mcp-2.1.1.tar.gz";,
 hash = 
"sha256:50b7ba1ebbe117008ea7bdd288234043e69c20b403d6851d19661e6d431a75ef", size 
= 3984589 }
 wheels = [
-    { url = 
"https://files.pythonhosted.org/packages/e2/fc/6dc7659c2ae5ddf280477011f4213a74f806862856b796ef08f028e664bf/mcp-1.25.0-py3-none-any.whl";,
 hash = 
"sha256:b37c38144a666add0862614cc79ec276e97d72aa8ca26d622818d4e278b9721a", size 
= 233076 },
+    { url = 
"https://files.pythonhosted.org/packages/50/af/8644cc5fa26a59afd2df2e98eeb19e72926887fa4b7441aba4ff661140db/mcp-2.1.1-py3-none-any.whl";,
 hash = 
"sha256:1c6c31c5d6471c58db76af3af8af67f46d11d01f0a59077d0a308cbdb3d3e915", size 
= 357912 },
 ]
 
 [package.optional-dependencies]
@@ -568,7 +650,7 @@
 
 [[package]]
 name = "mcp-redmine"
-version = "2026.1.13.152335"
+version = "2026.8.1.2543"
 source = { editable = "." }
 dependencies = [
     { name = "httpx" },
@@ -581,12 +663,13 @@
 dev = [
     { name = "build" },
     { name = "hatchling" },
+    { name = "pytest" },
 ]
 
 [package.metadata]
 requires-dist = [
     { name = "httpx", specifier = "==0.28.1" },
-    { name = "mcp", extras = ["cli"], specifier = "==1.25.0" },
+    { name = "mcp", extras = ["cli"], specifier = "==2.1.1" },
     { name = "openapi-core", specifier = "==0.19.4" },
     { name = "pyyaml", specifier = "==6.0.2" },
 ]
@@ -595,6 +678,20 @@
 dev = [
     { name = "build", specifier = "==1.2.2.post1" },
     { name = "hatchling", specifier = "==1.27.0" },
+    { name = "pytest", specifier = "==8.3.4" },
+]
+
+[[package]]
+name = "mcp-types"
+version = "2.1.1"
+source = { registry = "https://pypi.org/simple"; }
+dependencies = [
+    { name = "pydantic" },
+    { name = "typing-extensions" },
+]
+sdist = { url = 
"https://files.pythonhosted.org/packages/6a/dd/1c4417dc0b722c23a1669032d5f044e41170fe5d4773b488a50fcce98c32/mcp_types-2.1.1.tar.gz";,
 hash = 
"sha256:77dcbe48fba73cca71a673f2646a5f037a017b7a0a07ac89cec1113028890eda", size 
= 66674 }
+wheels = [
+    { url = 
"https://files.pythonhosted.org/packages/71/d0/242e63c510f4a17381f55b1549a3f94f5687a0595984febd2b6f87a687a0/mcp_types-2.1.1-py3-none-any.whl";,
 hash = 
"sha256:26f9f7f03f2a5730717a5b98e2ab7eb640ac352d05a00cdc725c311864778295", size 
= 69656 },
 ]
 
 [[package]]
@@ -664,6 +761,18 @@
 ]
 
 [[package]]
+name = "opentelemetry-api"
+version = "1.44.0"
+source = { registry = "https://pypi.org/simple"; }
+dependencies = [
+    { name = "typing-extensions" },
+]
+sdist = { url = 
"https://files.pythonhosted.org/packages/ee/8b/aa9e2d8b8dfa7c946f7dec5d1f8f6ba8eca062f43509a06bdb5ce93d26c0/opentelemetry_api-1.44.0.tar.gz";,
 hash = 
"sha256:67647e5e9566edcf421166fdf022b3537f818635daa852b289e34604dc6fb33a", size 
= 72406 }
+wheels = [
+    { url = 
"https://files.pythonhosted.org/packages/ca/6f/a04e900f465ff3221ccc395522503e2d10e79fa21f2723c8e177aae1e0d1/opentelemetry_api-1.44.0-py3-none-any.whl";,
 hash = 
"sha256:94b98c893a91b88657eaac1e3ba89618cdb85be6918196705354f34728b2cdef", size 
= 60018 },
+]
+
+[[package]]
 name = "packaging"
 version = "25.0"
 source = { registry = "https://pypi.org/simple"; }
@@ -851,19 +960,6 @@
 ]
 
 [[package]]
-name = "pydantic-settings"
-version = "2.8.0"
-source = { registry = "https://pypi.org/simple"; }
-dependencies = [
-    { name = "pydantic" },
-    { name = "python-dotenv" },
-]
-sdist = { url = 
"https://files.pythonhosted.org/packages/ca/a2/ad2511ede77bb424f3939e5148a56d968cdc6b1462620d24b2a1f4ab65b4/pydantic_settings-2.8.0.tar.gz";,
 hash = 
"sha256:88e2ca28f6e68ea102c99c3c401d6c9078e68a5df600e97b43891c34e089500a", size 
= 83347 }
-wheels = [
-    { url = 
"https://files.pythonhosted.org/packages/c1/a9/3b9642025174bbe67e900785fb99c9bfe91ea584b0b7126ff99945c24a0e/pydantic_settings-2.8.0-py3-none-any.whl";,
 hash = 
"sha256:c782c7dc3fb40e97b238e713c25d26f64314aece2e91abcff592fcac15f71820", size 
= 30746 },
-]
-
-[[package]]
 name = "pygments"
 version = "2.19.1"
 source = { registry = "https://pypi.org/simple"; }
@@ -896,6 +992,23 @@
 ]
 
 [[package]]
+name = "pytest"
+version = "8.3.4"
+source = { registry = "https://pypi.org/simple"; }
+dependencies = [
+    { name = "colorama", marker = "sys_platform == 'win32'" },
+    { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
+    { name = "iniconfig" },
+    { name = "packaging" },
+    { name = "pluggy" },
+    { name = "tomli", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = 
"https://files.pythonhosted.org/packages/05/35/30e0d83068951d90a01852cb1cef56e5d8a09d20c7f511634cc2f7e0372a/pytest-8.3.4.tar.gz";,
 hash = 
"sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761", size 
= 1445919 }
+wheels = [
+    { url = 
"https://files.pythonhosted.org/packages/11/92/76a1c94d3afee238333bc0a42b82935dd8f9cf8ce9e336ff87ee14d9e1cf/pytest-8.3.4-py3-none-any.whl";,
 hash = 
"sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", size 
= 343083 },
+]
+
+[[package]]
 name = "python-dotenv"
 version = "1.0.1"
 source = { registry = "https://pypi.org/simple"; }
@@ -1138,33 +1251,47 @@
 ]
 
 [[package]]
-name = "sniffio"
-version = "1.3.1"
+name = "sse-starlette"
+version = "3.0.3"
 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 }
+resolution-markers = [
+    "python_full_version >= '3.12' and python_full_version < '3.14' and 
sys_platform == 'emscripten'",
+    "(python_full_version < '3.14' and sys_platform != 'emscripten') or 
(python_full_version < '3.12' and sys_platform == 'emscripten')",
+]
+dependencies = [
+    { name = "anyio", marker = "python_full_version < '3.14'" },
+]
+sdist = { url = 
"https://files.pythonhosted.org/packages/db/3c/fa6517610dc641262b77cc7bf994ecd17465812c1b0585fe33e11be758ab/sse_starlette-3.0.3.tar.gz";,
 hash = 
"sha256:88cfb08747e16200ea990c8ca876b03910a23b547ab3bd764c0d8eb81019b971", size 
= 21943 }
 wheels = [
-    { url = 
"https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl";,
 hash = 
"sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size 
= 10235 },
+    { url = 
"https://files.pythonhosted.org/packages/23/a0/984525d19ca5c8a6c33911a0c164b11490dd0f90ff7fd689f704f84e9a11/sse_starlette-3.0.3-py3-none-any.whl";,
 hash = 
"sha256:af5bf5a6f3933df1d9c7f8539633dc8444ca6a97ab2e2a7cd3b6e431ac03a431", size 
= 11765 },
 ]
 
 [[package]]
 name = "sse-starlette"
-version = "2.2.1"
+version = "3.4.11"
 source = { registry = "https://pypi.org/simple"; }
+resolution-markers = [
+    "python_full_version >= '3.14'",
+]
 dependencies = [
-    { name = "anyio" },
-    { name = "starlette" },
+    { name = "anyio", marker = "python_full_version >= '3.14'" },
+    { name = "starlette", version = "1.6.0", source = { registry = 
"https://pypi.org/simple"; }, marker = "python_full_version >= '3.14'" },
 ]
-sdist = { url = 
"https://files.pythonhosted.org/packages/71/a4/80d2a11af59fe75b48230846989e93979c892d3a20016b42bb44edb9e398/sse_starlette-2.2.1.tar.gz";,
 hash = 
"sha256:54470d5f19274aeed6b2d473430b08b4b379ea851d953b11d7f1c4a2c118b419", size 
= 17376 }
+sdist = { url = 
"https://files.pythonhosted.org/packages/2b/54/6767bb789b2f2fed6e0f953df949cd39dc263a384c1b65a95232598621d6/sse_starlette-3.4.11.tar.gz";,
 hash = 
"sha256:1bae716c02f3e6f294be41ff333220692dae7c3cbab077c900f159676719dade", size 
= 34972 }
 wheels = [
-    { url = 
"https://files.pythonhosted.org/packages/d9/e0/5b8bd393f27f4a62461c5cf2479c75a2cc2ffa330976f9f00f5f6e4f50eb/sse_starlette-2.2.1-py3-none-any.whl";,
 hash = 
"sha256:6410a3d3ba0c89e7675d4c273a301d64649c03a5ef1ca101f10b47f895fd0e99", size 
= 10120 },
+    { url = 
"https://files.pythonhosted.org/packages/98/6a/2ba3ed4a69babf3afdddf7d8314a48d87562c0a442206bbc2a1b50d5efc0/sse_starlette-3.4.11-py3-none-any.whl";,
 hash = 
"sha256:c7b2244bdff016fe7f64e10075e89a3e6bbf899649cc89b0fe884b5545042453", size 
= 17122 },
 ]
 
 [[package]]
 name = "starlette"
 version = "0.46.0"
 source = { registry = "https://pypi.org/simple"; }
+resolution-markers = [
+    "python_full_version >= '3.12' and python_full_version < '3.14' and 
sys_platform == 'emscripten'",
+    "(python_full_version < '3.14' and sys_platform != 'emscripten') or 
(python_full_version < '3.12' and sys_platform == 'emscripten')",
+]
 dependencies = [
-    { name = "anyio" },
+    { name = "anyio", marker = "python_full_version < '3.14'" },
 ]
 sdist = { url = 
"https://files.pythonhosted.org/packages/44/b6/fb9a32e3c5d59b1e383c357534c63c2d3caa6f25bf3c59dd89d296ecbaec/starlette-0.46.0.tar.gz";,
 hash = 
"sha256:b359e4567456b28d473d0193f34c0de0ed49710d75ef183a74a5ce0499324f50", size 
= 2575568 }
 wheels = [
@@ -1172,6 +1299,21 @@
 ]
 
 [[package]]
+name = "starlette"
+version = "1.6.0"
+source = { registry = "https://pypi.org/simple"; }
+resolution-markers = [
+    "python_full_version >= '3.14'",
+]
+dependencies = [
+    { name = "anyio", marker = "python_full_version >= '3.14'" },
+]
+sdist = { url = 
"https://files.pythonhosted.org/packages/b5/b4/205b0d5241d934e8add0c38aa924c4f9fb7330834ff11e5444db964ec3f9/starlette-1.6.0.tar.gz";,
 hash = 
"sha256:d4e3ac5e546444960c710297a3c9fc3f7ebae1b7e963f3d36173b49da535be9b", size 
= 2716969 }
+wheels = [
+    { url = 
"https://files.pythonhosted.org/packages/c8/cb/6a6a47d5b464bd08695d254f3da6e7986cc70c9fa5d778eda57538edfe56/starlette-1.6.0-py3-none-any.whl";,
 hash = 
"sha256:a86dd39d14bb45f85a3d18525215a9ef0cfd1f192ac793220e72598c90335f0c", size 
= 75969 },
+]
+
+[[package]]
 name = "tomli"
 version = "2.2.1"
 source = { registry = "https://pypi.org/simple"; }
@@ -1220,6 +1362,15 @@
 ]
 
 [[package]]
+name = "truststore"
+version = "0.10.4"
+source = { registry = "https://pypi.org/simple"; }
+sdist = { url = 
"https://files.pythonhosted.org/packages/53/a3/1585216310e344e8102c22482f6060c7a6ea0322b63e026372e6dcefcfd6/truststore-0.10.4.tar.gz";,
 hash = 
"sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301", size 
= 26169 }
+wheels = [
+    { url = 
"https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl";,
 hash = 
"sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981", size 
= 18660 },
+]
+
+[[package]]
 name = "typer"
 version = "0.21.1"
 source = { registry = "https://pypi.org/simple"; }
@@ -1236,11 +1387,11 @@
 
 [[package]]
 name = "typing-extensions"
-version = "4.15.0"
+version = "4.16.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 }
+sdist = { url = 
"https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz";,
 hash = 
"sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size 
= 113555 }
 wheels = [
-    { url = 
"https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl";,
 hash = 
"sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size 
= 44614 },
+    { url = 
"https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl";,
 hash = 
"sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size 
= 45571 },
 ]
 
 [[package]]
@@ -1270,7 +1421,8 @@
 source = { registry = "https://pypi.org/simple"; }
 dependencies = [
     { name = "click" },
-    { name = "h11" },
+    { name = "h11", version = "0.14.0", source = { registry = 
"https://pypi.org/simple"; }, marker = "python_full_version >= '3.12' and 
python_full_version < '3.14' and sys_platform == 'emscripten'" },
+    { name = "h11", version = "0.16.0", source = { registry = 
"https://pypi.org/simple"; }, marker = "python_full_version < '3.12' or 
python_full_version >= '3.14' or sys_platform != 'emscripten'" },
     { 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 }

Reply via email to