cdcasasagi 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 nikkie
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,98 @@
1
+ Metadata-Version: 2.3
2
+ Name: cdcasasagi
3
+ Version: 0.1.0
4
+ Summary: 鵲 - Bridge Claude Desktop to Streamable HTTP MCP servers via mcp-proxy
5
+ Author: ftnext
6
+ Author-email: ftnext <takuyafjp+develop@gmail.com>
7
+ License: MIT License
8
+
9
+ Copyright (c) 2026 nikkie
10
+
11
+ Permission is hereby granted, free of charge, to any person obtaining a copy
12
+ of this software and associated documentation files (the "Software"), to deal
13
+ in the Software without restriction, including without limitation the rights
14
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15
+ copies of the Software, and to permit persons to whom the Software is
16
+ furnished to do so, subject to the following conditions:
17
+
18
+ The above copyright notice and this permission notice shall be included in all
19
+ copies or substantial portions of the Software.
20
+
21
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27
+ SOFTWARE.
28
+ Classifier: Development Status :: 3 - Alpha
29
+ Classifier: Environment :: Console
30
+ Classifier: Intended Audience :: Developers
31
+ Classifier: License :: OSI Approved :: MIT License
32
+ Classifier: Operating System :: MacOS
33
+ Classifier: Operating System :: Microsoft :: Windows
34
+ Classifier: Programming Language :: Python
35
+ Classifier: Programming Language :: Python :: 3 :: Only
36
+ Classifier: Programming Language :: Python :: 3.10
37
+ Classifier: Programming Language :: Python :: 3.11
38
+ Classifier: Programming Language :: Python :: 3.12
39
+ Classifier: Programming Language :: Python :: 3.13
40
+ Classifier: Programming Language :: Python :: 3.14
41
+ Classifier: Topic :: Utilities
42
+ Requires-Dist: typer>=0.9
43
+ Requires-Dist: mcp-proxy
44
+ Requires-Python: >=3.10
45
+ Description-Content-Type: text/markdown
46
+
47
+ # cdcasasagi
48
+ 鵲 - Bridge Claude Desktop to Streamable HTTP MCP servers via mcp-proxy
49
+
50
+ ## Install
51
+
52
+ ```
53
+ uv tool install cdcasasagi
54
+ ```
55
+
56
+ ## Usage
57
+
58
+ Preview what will be written to `claude_desktop_config.json`:
59
+
60
+ ```
61
+ cdcasasagi add https://developers.openai.com/mcp --name openai-developer-docs
62
+ ```
63
+
64
+ This shows a unified diff of the proposed change. No files are modified.
65
+
66
+ Apply the change:
67
+
68
+ ```
69
+ cdcasasagi add https://developers.openai.com/mcp --name openai-developer-docs --write
70
+ ```
71
+
72
+ This writes the following entry to your Claude Desktop config:
73
+
74
+ ```json
75
+ {
76
+ "mcpServers": {
77
+ "openai-developer-docs": {
78
+ "command": "/Users/you/.local/bin/mcp-proxy",
79
+ "args": [
80
+ "--transport",
81
+ "streamablehttp",
82
+ "https://developers.openai.com/mcp"
83
+ ]
84
+ }
85
+ }
86
+ }
87
+ ```
88
+
89
+ The `--name` flag is optional. If omitted, a name is automatically derived from the URL hostname (e.g. `developers` for the URL above).
90
+
91
+ ### Options
92
+
93
+ | Option | Description |
94
+ |---|---|
95
+ | `--name` | Key name for the `mcpServers` entry. Auto-derived from URL if omitted. |
96
+ | `--transport` | Transport type passed to mcp-proxy. Default: `streamablehttp`. |
97
+ | `--force` | Overwrite an existing entry with the same name. |
98
+ | `--write` | Actually write to the config file. Without this flag, only a preview is shown. |
@@ -0,0 +1,52 @@
1
+ # cdcasasagi
2
+ 鵲 - Bridge Claude Desktop to Streamable HTTP MCP servers via mcp-proxy
3
+
4
+ ## Install
5
+
6
+ ```
7
+ uv tool install cdcasasagi
8
+ ```
9
+
10
+ ## Usage
11
+
12
+ Preview what will be written to `claude_desktop_config.json`:
13
+
14
+ ```
15
+ cdcasasagi add https://developers.openai.com/mcp --name openai-developer-docs
16
+ ```
17
+
18
+ This shows a unified diff of the proposed change. No files are modified.
19
+
20
+ Apply the change:
21
+
22
+ ```
23
+ cdcasasagi add https://developers.openai.com/mcp --name openai-developer-docs --write
24
+ ```
25
+
26
+ This writes the following entry to your Claude Desktop config:
27
+
28
+ ```json
29
+ {
30
+ "mcpServers": {
31
+ "openai-developer-docs": {
32
+ "command": "/Users/you/.local/bin/mcp-proxy",
33
+ "args": [
34
+ "--transport",
35
+ "streamablehttp",
36
+ "https://developers.openai.com/mcp"
37
+ ]
38
+ }
39
+ }
40
+ }
41
+ ```
42
+
43
+ The `--name` flag is optional. If omitted, a name is automatically derived from the URL hostname (e.g. `developers` for the URL above).
44
+
45
+ ### Options
46
+
47
+ | Option | Description |
48
+ |---|---|
49
+ | `--name` | Key name for the `mcpServers` entry. Auto-derived from URL if omitted. |
50
+ | `--transport` | Transport type passed to mcp-proxy. Default: `streamablehttp`. |
51
+ | `--force` | Overwrite an existing entry with the same name. |
52
+ | `--write` | Actually write to the config file. Without this flag, only a preview is shown. |
@@ -0,0 +1,42 @@
1
+ [project]
2
+ name = "cdcasasagi"
3
+ version = "0.1.0"
4
+ description = "鵲 - Bridge Claude Desktop to Streamable HTTP MCP servers via mcp-proxy "
5
+ authors = [
6
+ { name = "ftnext", email = "takuyafjp+develop@gmail.com" }
7
+ ]
8
+ license = { file = "LICENSE" }
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ classifiers = [
12
+ "Development Status :: 3 - Alpha",
13
+ "Environment :: Console",
14
+ "Intended Audience :: Developers",
15
+ "License :: OSI Approved :: MIT License",
16
+ "Operating System :: MacOS",
17
+ "Operating System :: Microsoft :: Windows",
18
+ "Programming Language :: Python",
19
+ "Programming Language :: Python :: 3 :: Only",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Programming Language :: Python :: 3.14",
25
+ "Topic :: Utilities",
26
+ ]
27
+ dependencies = [
28
+ "typer>=0.9",
29
+ "mcp-proxy",
30
+ ]
31
+
32
+ [project.scripts]
33
+ cdcasasagi = "cdcasasagi:main"
34
+
35
+ [build-system]
36
+ requires = ["uv_build>=0.11.2,<0.12.0"]
37
+ build-backend = "uv_build"
38
+
39
+ [dependency-groups]
40
+ dev = [
41
+ "pytest>=7",
42
+ ]
@@ -0,0 +1,5 @@
1
+ from .cli import app
2
+
3
+
4
+ def main() -> None:
5
+ app()
@@ -0,0 +1,79 @@
1
+ from __future__ import annotations
2
+
3
+ from urllib.parse import urlparse
4
+
5
+ import typer
6
+
7
+ from . import desktop_config, mcp_proxy, output, server_name
8
+
9
+ app = typer.Typer(pretty_exceptions_enable=False)
10
+
11
+
12
+ @app.callback()
13
+ def _callback() -> None:
14
+ pass
15
+
16
+
17
+ def _validate_url(url: str) -> None:
18
+ parsed = urlparse(url)
19
+ if parsed.scheme not in ("http", "https"):
20
+ typer.echo("Please specify a valid HTTP(S) URL", err=True)
21
+ raise typer.Exit(code=1)
22
+ if not parsed.hostname:
23
+ typer.echo("Invalid URL format", err=True)
24
+ raise typer.Exit(code=1)
25
+
26
+
27
+ @app.command()
28
+ def add(
29
+ url: str = typer.Argument(..., help="URL of the MCP server"),
30
+ name: str | None = typer.Option(None, help="Key name for mcpServers"),
31
+ transport: str = typer.Option(
32
+ "streamablehttp", help="Transport type passed to mcp-proxy"
33
+ ),
34
+ force: bool = typer.Option(False, help="Overwrite existing entry"),
35
+ write: bool = typer.Option(False, help="Actually write to the file"),
36
+ ) -> None:
37
+ _validate_url(url)
38
+
39
+ name_was_derived = name is None
40
+ if name is None:
41
+ try:
42
+ name = server_name.derive_server_name(url)
43
+ except server_name.NameDerivationError as e:
44
+ typer.echo(str(e), err=True)
45
+ raise typer.Exit(code=1)
46
+
47
+ try:
48
+ proxy_path = mcp_proxy.resolve_path()
49
+ except mcp_proxy.McpProxyNotFoundError as e:
50
+ typer.echo(str(e), err=True)
51
+ raise typer.Exit(code=1)
52
+
53
+ cfg_path = desktop_config.config_path()
54
+
55
+ try:
56
+ current_config = desktop_config.load_config(cfg_path)
57
+ except desktop_config.ConfigError as e:
58
+ typer.echo(str(e), err=True)
59
+ raise typer.Exit(code=1)
60
+
61
+ entry = desktop_config.build_entry(proxy_path, transport, url)
62
+
63
+ try:
64
+ merged = desktop_config.merge_entry(current_config, name, entry, force)
65
+ except desktop_config.EntryExistsError as e:
66
+ typer.echo(str(e), err=True)
67
+ raise typer.Exit(code=1)
68
+
69
+ file_existed = cfg_path.exists()
70
+
71
+ if not write:
72
+ original = None if not file_existed else current_config
73
+ diff_text = output.format_diff(original, merged)
74
+ msg = output.preview_message(name, name_was_derived, cfg_path, diff_text)
75
+ typer.echo(msg)
76
+ else:
77
+ desktop_config.write_config(cfg_path, merged)
78
+ msg = output.write_message(name, name_was_derived, cfg_path, file_existed)
79
+ typer.echo(msg)
@@ -0,0 +1,97 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import platform
6
+ import shutil
7
+ import tempfile
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+
12
+ class ConfigError(Exception):
13
+ pass
14
+
15
+
16
+ class EntryExistsError(Exception):
17
+ pass
18
+
19
+
20
+ def config_path() -> Path:
21
+ env = os.environ.get("CLAUDE_DESKTOP_CONFIG")
22
+ if env:
23
+ return Path(env)
24
+ if platform.system() == "Windows":
25
+ appdata = os.environ.get("APPDATA", "")
26
+ return Path(appdata) / "Claude" / "claude_desktop_config.json"
27
+ return (
28
+ Path.home()
29
+ / "Library"
30
+ / "Application Support"
31
+ / "Claude"
32
+ / "claude_desktop_config.json"
33
+ )
34
+
35
+
36
+ def load_config(path: Path) -> dict[str, Any]:
37
+ if not path.exists():
38
+ return {"mcpServers": {}}
39
+ try:
40
+ text = path.read_text(encoding="utf-8")
41
+ return json.loads(text)
42
+ except (json.JSONDecodeError, ValueError) as e:
43
+ raise ConfigError(
44
+ f"Failed to parse JSON config file: {path}\nPlease check the file: {e}"
45
+ ) from e
46
+
47
+
48
+ def build_entry(mcp_proxy_path: Path, transport: str, url: str) -> dict[str, Any]:
49
+ return {
50
+ "command": str(mcp_proxy_path),
51
+ "args": ["--transport", transport, url],
52
+ }
53
+
54
+
55
+ def merge_entry(
56
+ config: dict[str, Any],
57
+ name: str,
58
+ entry: dict[str, Any],
59
+ force: bool,
60
+ ) -> dict[str, Any]:
61
+ config = json.loads(json.dumps(config)) # deep copy
62
+ if "mcpServers" not in config:
63
+ config["mcpServers"] = {}
64
+
65
+ if name in config["mcpServers"] and not force:
66
+ raise EntryExistsError(f'"{name}" already exists. Use --force to overwrite')
67
+
68
+ config["mcpServers"][name] = entry
69
+ return config
70
+
71
+
72
+ def serialize_config(config: dict[str, Any]) -> str:
73
+ return json.dumps(config, indent=2, ensure_ascii=False) + "\n"
74
+
75
+
76
+ def write_config(path: Path, config: dict[str, Any]) -> None:
77
+ path.parent.mkdir(parents=True, exist_ok=True)
78
+
79
+ if path.exists():
80
+ backup = path.with_suffix(path.suffix + ".bak")
81
+ shutil.copy2(path, backup)
82
+
83
+ content = serialize_config(config)
84
+ dir_ = path.parent
85
+ fd, tmp = tempfile.mkstemp(dir=dir_, suffix=".tmp")
86
+ fd_closed = False
87
+ try:
88
+ os.write(fd, content.encode("utf-8"))
89
+ os.close(fd)
90
+ fd_closed = True
91
+ os.replace(tmp, path)
92
+ except BaseException:
93
+ if not fd_closed:
94
+ os.close(fd)
95
+ if os.path.exists(tmp):
96
+ os.unlink(tmp)
97
+ raise
@@ -0,0 +1,22 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import sys
5
+ from pathlib import Path
6
+
7
+
8
+ class McpProxyNotFoundError(Exception):
9
+ pass
10
+
11
+
12
+ def resolve_path() -> Path:
13
+ bin_dir = Path(sys.executable).parent
14
+ name = "mcp-proxy.exe" if os.name == "nt" else "mcp-proxy"
15
+ candidate = bin_dir / name
16
+ if candidate.exists() and candidate.is_file():
17
+ return candidate
18
+ raise McpProxyNotFoundError(
19
+ "mcp-proxy not found. "
20
+ "Please install this tool via 'uv tool install cdcasasagi'. "
21
+ "Temporary execution with uvx will not persist the path written to the config file."
22
+ )
@@ -0,0 +1,76 @@
1
+ from __future__ import annotations
2
+
3
+ import difflib
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ from . import desktop_config
8
+
9
+
10
+ def format_diff(
11
+ current_config: dict[str, Any] | None,
12
+ proposed_config: dict[str, Any],
13
+ ) -> str:
14
+ if current_config is None:
15
+ current_lines: list[str] = []
16
+ header = "--- (file does not exist; will be created)\n+++ proposed\n"
17
+ else:
18
+ current_text = desktop_config.serialize_config(current_config)
19
+ current_lines = current_text.splitlines(keepends=True)
20
+ header = ""
21
+
22
+ proposed_text = desktop_config.serialize_config(proposed_config)
23
+ proposed_lines = proposed_text.splitlines(keepends=True)
24
+
25
+ diff = difflib.unified_diff(
26
+ current_lines,
27
+ proposed_lines,
28
+ fromfile="current",
29
+ tofile="proposed",
30
+ )
31
+ if header:
32
+ diff_lines = list(diff)
33
+ # Drop the default --- / +++ lines from unified_diff
34
+ content = "".join(
35
+ line
36
+ for line in diff_lines
37
+ if not line.startswith("--- ") and not line.startswith("+++ ")
38
+ )
39
+ return header + content
40
+ return "".join(diff)
41
+
42
+
43
+ def preview_message(
44
+ name: str,
45
+ name_was_derived: bool,
46
+ config_path: Path,
47
+ diff_text: str,
48
+ ) -> str:
49
+ lines: list[str] = []
50
+ if name_was_derived:
51
+ lines.append(f'Derived name from URL: "{name}"')
52
+ lines.append(f"Target: {config_path}")
53
+ lines.append("")
54
+ lines.append(diff_text.rstrip())
55
+ lines.append("")
56
+ lines.append("This is a preview. Re-run with --write to apply.")
57
+ lines.append("To use a different name: --name <your-name>")
58
+ return "\n".join(lines)
59
+
60
+
61
+ def write_message(
62
+ name: str,
63
+ name_was_derived: bool,
64
+ config_path: Path,
65
+ file_existed: bool,
66
+ ) -> str:
67
+ lines: list[str] = []
68
+ if name_was_derived:
69
+ lines.append(f'Derived name from URL: "{name}"')
70
+ if file_existed:
71
+ backup = config_path.with_suffix(config_path.suffix + ".bak")
72
+ lines.append(f"Backup: {backup}")
73
+ lines.append(f"Wrote: {config_path}")
74
+ lines.append("")
75
+ lines.append(f'Added "{name}". Restart Claude Desktop to take effect.')
76
+ return "\n".join(lines)
@@ -0,0 +1,57 @@
1
+ from __future__ import annotations
2
+
3
+ import ipaddress
4
+ from urllib.parse import urlparse
5
+
6
+ _GENERIC_SUBDOMAINS = {"mcp", "api", "app", "www"}
7
+ _MCP_API_FRAGMENTS = {"mcp", "api"}
8
+
9
+
10
+ class NameDerivationError(Exception):
11
+ pass
12
+
13
+
14
+ def derive_server_name(url: str) -> str:
15
+ hostname = urlparse(url).hostname
16
+ if hostname is None:
17
+ raise NameDerivationError("Invalid URL format")
18
+
19
+ if hostname == "localhost" or _is_ip(hostname):
20
+ raise NameDerivationError(
21
+ "Cannot derive a name from the hostname. Please specify --name explicitly"
22
+ )
23
+
24
+ labels = hostname.split(".")
25
+ if len(labels) < 2:
26
+ raise NameDerivationError(
27
+ "Cannot derive a name from the hostname. Please specify --name explicitly"
28
+ )
29
+
30
+ sld = labels[-2]
31
+ head = labels[0]
32
+
33
+ if (
34
+ head != sld
35
+ and head not in _GENERIC_SUBDOMAINS
36
+ and not any(frag in head for frag in _MCP_API_FRAGMENTS)
37
+ ):
38
+ candidate = head
39
+ else:
40
+ candidate = sld
41
+
42
+ candidate = candidate.lower()
43
+
44
+ if not candidate or len(candidate) == 1 or candidate.isdigit():
45
+ raise NameDerivationError(
46
+ "Cannot derive a name from the hostname. Please specify --name explicitly"
47
+ )
48
+
49
+ return candidate
50
+
51
+
52
+ def _is_ip(hostname: str) -> bool:
53
+ try:
54
+ ipaddress.ip_address(hostname)
55
+ except ValueError:
56
+ return False
57
+ return True