pathgate 0.1.1__tar.gz → 0.2.1__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.
- {pathgate-0.1.1 → pathgate-0.2.1}/PKG-INFO +16 -23
- {pathgate-0.1.1 → pathgate-0.2.1}/README.md +14 -21
- pathgate-0.2.1/pathgate/__main__.py +3 -0
- {pathgate-0.1.1 → pathgate-0.2.1}/pathgate/config.py +0 -5
- pathgate-0.2.1/pathgate/install.py +254 -0
- {pathgate-0.1.1 → pathgate-0.2.1}/pathgate/prompts.py +28 -29
- pathgate-0.2.1/pathgate/resources.py +55 -0
- pathgate-0.2.1/pathgate/server.py +75 -0
- {pathgate-0.1.1 → pathgate-0.2.1}/pathgate/tool_schemas.py +3 -4
- {pathgate-0.1.1 → pathgate-0.2.1}/pathgate/tools.py +44 -46
- {pathgate-0.1.1 → pathgate-0.2.1}/pyproject.toml +2 -2
- pathgate-0.1.1/pathgate/__main__.py +0 -0
- pathgate-0.1.1/pathgate/resources.py +0 -90
- pathgate-0.1.1/pathgate/server.py +0 -42
- {pathgate-0.1.1 → pathgate-0.2.1}/.gitignore +0 -0
- {pathgate-0.1.1 → pathgate-0.2.1}/LICENSE +0 -0
- {pathgate-0.1.1 → pathgate-0.2.1}/pathgate/__init__.py +0 -0
- {pathgate-0.1.1 → pathgate-0.2.1}/tests/test_server.py +0 -0
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: pathgate
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Filesystem MCP server with allow-listed directories
|
|
5
5
|
License: MIT
|
|
6
6
|
License-File: LICENSE
|
|
7
7
|
Requires-Python: >=3.10
|
|
8
|
-
Requires-Dist: mcp
|
|
8
|
+
Requires-Dist: mcp<3,>=2
|
|
9
9
|
Description-Content-Type: text/markdown
|
|
10
10
|
|
|
11
|
-
# Pathgate
|
|
11
|
+
# Pathgate
|
|
12
12
|
|
|
13
13
|
`pathgate` is a Python Model Context Protocol (MCP) server that exposes a controlled filesystem interface to MCP clients such as Claude Desktop, Cursor, VS Code, and inspector tools. The server only permits access to directories that are explicitly allow-listed at startup.
|
|
14
14
|
|
|
@@ -31,13 +31,20 @@ Access is restricted by allow-listed paths. At startup, the server resolves the
|
|
|
31
31
|
|
|
32
32
|
## Installation
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
Pathgate package is published on PyPI. To use it, for example with Claude Desktop, add this in the
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"mcpServers": {
|
|
39
|
+
"pathgate": {
|
|
40
|
+
"command": "uvx",
|
|
41
|
+
"args": ["pathgate", "C:\\Users\\me\\Documents"]
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
38
45
|
```
|
|
39
46
|
|
|
40
|
-
This
|
|
47
|
+
This is just an example script
|
|
41
48
|
|
|
42
49
|
## Usage
|
|
43
50
|
|
|
@@ -77,20 +84,6 @@ The current implementation exposes these tools:
|
|
|
77
84
|
- `search_files_tool` — search for files matching a pattern
|
|
78
85
|
- `get_file_info_tool` — return metadata for a file or directory
|
|
79
86
|
|
|
80
|
-
## MCP client configuration
|
|
81
|
-
|
|
82
|
-
Example Claude Desktop configuration:
|
|
83
|
-
|
|
84
|
-
```json
|
|
85
|
-
{
|
|
86
|
-
"mcpServers": {
|
|
87
|
-
"filesystem": {
|
|
88
|
-
"command": "pathgate",
|
|
89
|
-
"args": ["C:\\Users\\<username>\\Documents"]
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
```
|
|
94
87
|
|
|
95
88
|
## Example requests
|
|
96
89
|
|
|
@@ -135,7 +128,7 @@ pathgate/
|
|
|
135
128
|
## Requirements
|
|
136
129
|
|
|
137
130
|
- Python 3.10+
|
|
138
|
-
- `mcp>=
|
|
131
|
+
- `mcp>=2`
|
|
139
132
|
|
|
140
133
|
## References
|
|
141
134
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Pathgate
|
|
1
|
+
# Pathgate
|
|
2
2
|
|
|
3
3
|
`pathgate` is a Python Model Context Protocol (MCP) server that exposes a controlled filesystem interface to MCP clients such as Claude Desktop, Cursor, VS Code, and inspector tools. The server only permits access to directories that are explicitly allow-listed at startup.
|
|
4
4
|
|
|
@@ -21,13 +21,20 @@ Access is restricted by allow-listed paths. At startup, the server resolves the
|
|
|
21
21
|
|
|
22
22
|
## Installation
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
Pathgate package is published on PyPI. To use it, for example with Claude Desktop, add this in the
|
|
25
|
+
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"mcpServers": {
|
|
29
|
+
"pathgate": {
|
|
30
|
+
"command": "uvx",
|
|
31
|
+
"args": ["pathgate", "C:\\Users\\me\\Documents"]
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
28
35
|
```
|
|
29
36
|
|
|
30
|
-
This
|
|
37
|
+
This is just an example script
|
|
31
38
|
|
|
32
39
|
## Usage
|
|
33
40
|
|
|
@@ -67,20 +74,6 @@ The current implementation exposes these tools:
|
|
|
67
74
|
- `search_files_tool` — search for files matching a pattern
|
|
68
75
|
- `get_file_info_tool` — return metadata for a file or directory
|
|
69
76
|
|
|
70
|
-
## MCP client configuration
|
|
71
|
-
|
|
72
|
-
Example Claude Desktop configuration:
|
|
73
|
-
|
|
74
|
-
```json
|
|
75
|
-
{
|
|
76
|
-
"mcpServers": {
|
|
77
|
-
"filesystem": {
|
|
78
|
-
"command": "pathgate",
|
|
79
|
-
"args": ["C:\\Users\\<username>\\Documents"]
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
```
|
|
84
77
|
|
|
85
78
|
## Example requests
|
|
86
79
|
|
|
@@ -125,7 +118,7 @@ pathgate/
|
|
|
125
118
|
## Requirements
|
|
126
119
|
|
|
127
120
|
- Python 3.10+
|
|
128
|
-
- `mcp>=
|
|
121
|
+
- `mcp>=2`
|
|
129
122
|
|
|
130
123
|
## References
|
|
131
124
|
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
2
|
import os
|
|
3
3
|
import logging
|
|
4
|
-
from mcp.server import Server
|
|
5
4
|
|
|
6
5
|
logging.basicConfig(
|
|
7
6
|
level=logging.INFO,
|
|
@@ -10,10 +9,6 @@ logging.basicConfig(
|
|
|
10
9
|
)
|
|
11
10
|
logger = logging.getLogger(__name__)
|
|
12
11
|
|
|
13
|
-
# Create the MCP server
|
|
14
|
-
server = Server("pathgate")
|
|
15
|
-
|
|
16
|
-
|
|
17
12
|
# pathgate/config.py
|
|
18
13
|
ALLOWED_PATHS: list[str] = [] # filled in by configure() at startup
|
|
19
14
|
_state = {"read_only": False}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"""`pathgate install` - add Pathgate to Claude Desktop's config file.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
uvx pathgate install C:\\Users\\me\\Documents C:\\Users\\me\\Projects
|
|
5
|
+
uvx pathgate install (prompts for folders)
|
|
6
|
+
uvx pathgate install --dry-run ~/Projects
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import shutil
|
|
15
|
+
import sys
|
|
16
|
+
import tempfile
|
|
17
|
+
from datetime import datetime
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
DEFAULT_NAME = "pathgate"
|
|
21
|
+
CONFIG_FILENAME = "claude_desktop_config.json"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class InstallError(Exception):
|
|
25
|
+
"""A problem we can explain to the user in one line."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# --------------------------------------------------------------------------
|
|
29
|
+
# Finding the config file
|
|
30
|
+
# --------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
def claude_desktop_candidates() -> list[Path]:
|
|
33
|
+
"""Every place Claude Desktop might keep its config on this OS."""
|
|
34
|
+
if sys.platform == "win32":
|
|
35
|
+
found: list[Path] = []
|
|
36
|
+
appdata = os.environ.get("APPDATA")
|
|
37
|
+
if appdata:
|
|
38
|
+
found.append(Path(appdata) / "Claude" / CONFIG_FILENAME)
|
|
39
|
+
local = os.environ.get("LOCALAPPDATA")
|
|
40
|
+
if local:
|
|
41
|
+
# Microsoft Store (MSIX) installs keep their own private copy.
|
|
42
|
+
pattern = "Claude_*/LocalCache/Roaming/Claude"
|
|
43
|
+
for folder in sorted(Path(local, "Packages").glob(pattern)):
|
|
44
|
+
found.append(folder / CONFIG_FILENAME)
|
|
45
|
+
return found
|
|
46
|
+
if sys.platform == "darwin":
|
|
47
|
+
base = Path.home() / "Library" / "Application Support" / "Claude"
|
|
48
|
+
return [base / CONFIG_FILENAME]
|
|
49
|
+
xdg = os.environ.get("XDG_CONFIG_HOME")
|
|
50
|
+
base = Path(xdg) if xdg else Path.home() / ".config"
|
|
51
|
+
return [base / "Claude" / CONFIG_FILENAME]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def resolve_config_path(explicit: str | None) -> Path:
|
|
55
|
+
if explicit:
|
|
56
|
+
return Path(explicit).expanduser().resolve()
|
|
57
|
+
|
|
58
|
+
candidates = claude_desktop_candidates()
|
|
59
|
+
usable = [c for c in candidates if c.exists() or c.parent.exists()]
|
|
60
|
+
existing = [c for c in usable if c.exists()]
|
|
61
|
+
|
|
62
|
+
if len(existing) == 1:
|
|
63
|
+
return existing[0]
|
|
64
|
+
if not existing and len(usable) == 1:
|
|
65
|
+
return usable[0]
|
|
66
|
+
if not usable:
|
|
67
|
+
raise InstallError(
|
|
68
|
+
"couldn't find Claude Desktop's config folder. Is Claude Desktop "
|
|
69
|
+
"installed? You can point at a config file yourself with --config PATH."
|
|
70
|
+
)
|
|
71
|
+
listing = "\n ".join(str(c) for c in usable)
|
|
72
|
+
raise InstallError(
|
|
73
|
+
"found more than one possible Claude Desktop config:\n "
|
|
74
|
+
f"{listing}\nRe-run with --config PATH to say which one."
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# --------------------------------------------------------------------------
|
|
79
|
+
# Building the entry
|
|
80
|
+
# --------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
def build_server_entry(paths: list[str], read_only: bool) -> dict:
|
|
83
|
+
"""The `mcpServers` entry that launches Pathgate."""
|
|
84
|
+
uvx = shutil.which("uvx")
|
|
85
|
+
uv = shutil.which("uv")
|
|
86
|
+
if uvx:
|
|
87
|
+
command, args = uvx, ["pathgate"]
|
|
88
|
+
elif uv:
|
|
89
|
+
command, args = uv, ["tool", "run", "pathgate"]
|
|
90
|
+
else:
|
|
91
|
+
command, args = sys.executable, ["-m", "pathgate"]
|
|
92
|
+
args = args + paths
|
|
93
|
+
if read_only:
|
|
94
|
+
args.append("--read-only")
|
|
95
|
+
return {"command": command, "args": args}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# --------------------------------------------------------------------------
|
|
99
|
+
# Reading / writing the config safely
|
|
100
|
+
# --------------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
def load_config(path: Path) -> dict:
|
|
103
|
+
if not path.exists():
|
|
104
|
+
return {}
|
|
105
|
+
raw = path.read_text(encoding="utf-8-sig") # tolerate a BOM
|
|
106
|
+
if not raw.strip():
|
|
107
|
+
return {}
|
|
108
|
+
try:
|
|
109
|
+
data = json.loads(raw)
|
|
110
|
+
except json.JSONDecodeError as e:
|
|
111
|
+
raise InstallError(
|
|
112
|
+
f"{path} isn't valid JSON ({e}). I haven't changed anything - "
|
|
113
|
+
"fix the file by hand, then run this again."
|
|
114
|
+
) from None
|
|
115
|
+
if not isinstance(data, dict):
|
|
116
|
+
raise InstallError(f"{path} doesn't contain a JSON object, so I left it alone.")
|
|
117
|
+
return data
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def write_config(path: Path, data: dict) -> Path | None:
|
|
121
|
+
"""Back up the old file, then replace it atomically. Returns the backup path."""
|
|
122
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
123
|
+
|
|
124
|
+
backup = None
|
|
125
|
+
if path.exists():
|
|
126
|
+
stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
|
|
127
|
+
backup = path.with_name(f"{path.name}.pathgate-{stamp}.bak")
|
|
128
|
+
shutil.copy2(path, backup)
|
|
129
|
+
|
|
130
|
+
fd, tmp = tempfile.mkstemp(dir=path.parent, prefix=path.name + ".", suffix=".tmp")
|
|
131
|
+
try:
|
|
132
|
+
with os.fdopen(fd, "w", encoding="utf-8", newline="\n") as f:
|
|
133
|
+
json.dump(data, f, indent=2, ensure_ascii=False)
|
|
134
|
+
f.write("\n")
|
|
135
|
+
os.replace(tmp, path)
|
|
136
|
+
except BaseException:
|
|
137
|
+
try:
|
|
138
|
+
os.unlink(tmp)
|
|
139
|
+
except OSError:
|
|
140
|
+
pass
|
|
141
|
+
raise
|
|
142
|
+
return backup
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# --------------------------------------------------------------------------
|
|
146
|
+
# The command
|
|
147
|
+
# --------------------------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
def _prompt_for_paths() -> list[str]:
|
|
150
|
+
print("Which folders should Pathgate be allowed to access?")
|
|
151
|
+
print("Enter one folder per line. Press Enter on an empty line when done.")
|
|
152
|
+
paths: list[str] = []
|
|
153
|
+
while True:
|
|
154
|
+
line = input("> ").strip().strip('"').strip("'")
|
|
155
|
+
if not line:
|
|
156
|
+
break
|
|
157
|
+
paths.append(line)
|
|
158
|
+
return paths
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _clean_paths(raw: list[str]) -> list[str]:
|
|
162
|
+
cleaned: list[str] = []
|
|
163
|
+
for item in raw:
|
|
164
|
+
p = Path(item).expanduser().resolve()
|
|
165
|
+
if not p.is_dir():
|
|
166
|
+
raise InstallError(f"not a folder: {p}")
|
|
167
|
+
s = str(p)
|
|
168
|
+
if s not in cleaned:
|
|
169
|
+
cleaned.append(s)
|
|
170
|
+
return cleaned
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _confirm(question: str) -> bool:
|
|
174
|
+
answer = input(f"{question} [Y/n] ").strip().lower()
|
|
175
|
+
return answer in ("", "y", "yes")
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _install(args: argparse.Namespace) -> int:
|
|
179
|
+
interactive = sys.stdin.isatty()
|
|
180
|
+
|
|
181
|
+
raw_paths = list(args.paths)
|
|
182
|
+
if not raw_paths:
|
|
183
|
+
if not interactive:
|
|
184
|
+
raise InstallError(
|
|
185
|
+
"no folders given. Example: pathgate install C:\\Users\\me\\Documents"
|
|
186
|
+
)
|
|
187
|
+
raw_paths = _prompt_for_paths()
|
|
188
|
+
if not raw_paths:
|
|
189
|
+
raise InstallError("no folders given, nothing to do.")
|
|
190
|
+
paths = _clean_paths(raw_paths)
|
|
191
|
+
|
|
192
|
+
config_path = resolve_config_path(args.config)
|
|
193
|
+
data = load_config(config_path)
|
|
194
|
+
entry = build_server_entry(paths, args.read_only)
|
|
195
|
+
|
|
196
|
+
servers = data.setdefault("mcpServers", {})
|
|
197
|
+
if not isinstance(servers, dict):
|
|
198
|
+
raise InstallError(f'"mcpServers" in {config_path} isn\'t an object, so I left it alone.')
|
|
199
|
+
|
|
200
|
+
existing = servers.get(args.name)
|
|
201
|
+
if existing == entry:
|
|
202
|
+
print(f'"{args.name}" is already set up in {config_path}. Nothing to change.')
|
|
203
|
+
return 0
|
|
204
|
+
|
|
205
|
+
print(f"Config file : {config_path}")
|
|
206
|
+
print(f"Server name : {args.name}")
|
|
207
|
+
print(f"Command : {entry['command']} {' '.join(entry['args'])}")
|
|
208
|
+
print("Folders : " + ", ".join(paths))
|
|
209
|
+
print("Mode : " + ("read-only" if args.read_only else "read/write"))
|
|
210
|
+
if existing is not None:
|
|
211
|
+
print(f'Note : this replaces your current "{args.name}" entry.')
|
|
212
|
+
|
|
213
|
+
if args.dry_run:
|
|
214
|
+
print("\n--dry-run: nothing was written.")
|
|
215
|
+
return 0
|
|
216
|
+
|
|
217
|
+
if not args.yes:
|
|
218
|
+
if not interactive:
|
|
219
|
+
raise InstallError("not running in a terminal; pass --yes to proceed.")
|
|
220
|
+
if not _confirm("\nWrite this to the config?"):
|
|
221
|
+
print("Cancelled. Nothing was changed.")
|
|
222
|
+
return 1
|
|
223
|
+
|
|
224
|
+
servers[args.name] = entry
|
|
225
|
+
backup = write_config(config_path, data)
|
|
226
|
+
|
|
227
|
+
print(f'\nDone. Added "{args.name}" to {config_path}.')
|
|
228
|
+
if backup:
|
|
229
|
+
print(f"Backup of your old config: {backup}")
|
|
230
|
+
print("Now fully quit Claude Desktop (system tray -> Quit) and reopen it.")
|
|
231
|
+
return 0
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def main(argv: list[str] | None = None) -> int:
|
|
235
|
+
p = argparse.ArgumentParser(
|
|
236
|
+
prog="pathgate install",
|
|
237
|
+
description="Add Pathgate to Claude Desktop's config file.",
|
|
238
|
+
)
|
|
239
|
+
p.add_argument("paths", nargs="*", help="Folders Pathgate may access")
|
|
240
|
+
p.add_argument("--read-only", action="store_true", help="Disable write/delete/move/copy tools")
|
|
241
|
+
p.add_argument("--name", default=DEFAULT_NAME, help=f"Entry name in the config (default: {DEFAULT_NAME})")
|
|
242
|
+
p.add_argument("--config", metavar="PATH", help="Config file to edit (default: auto-detect Claude Desktop)")
|
|
243
|
+
p.add_argument("--dry-run", action="store_true", help="Show what would change without writing")
|
|
244
|
+
p.add_argument("-y", "--yes", action="store_true", help="Don't ask for confirmation")
|
|
245
|
+
args = p.parse_args(argv)
|
|
246
|
+
|
|
247
|
+
try:
|
|
248
|
+
return _install(args)
|
|
249
|
+
except InstallError as e:
|
|
250
|
+
print(f"error: {e}", file=sys.stderr)
|
|
251
|
+
return 1
|
|
252
|
+
except (KeyboardInterrupt, EOFError):
|
|
253
|
+
print("\nCancelled. Nothing was changed.", file=sys.stderr)
|
|
254
|
+
return 130
|
|
@@ -1,54 +1,53 @@
|
|
|
1
|
-
from
|
|
2
|
-
from mcp.
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
async def list_prompts() -> ListPromptsResult:
|
|
1
|
+
from mcp import MCPError
|
|
2
|
+
from mcp.server import ServerRequestContext
|
|
3
|
+
from mcp.types import (INVALID_PARAMS, GetPromptRequestParams, GetPromptResult, ListPromptsResult,
|
|
4
|
+
PaginatedRequestParams, Prompt, PromptArgument, PromptMessage, TextContent)
|
|
5
|
+
async def list_prompts(ctx: ServerRequestContext, params: PaginatedRequestParams | None) -> ListPromptsResult:
|
|
6
6
|
"""List available prompts"""
|
|
7
7
|
prompts = [
|
|
8
8
|
Prompt(
|
|
9
9
|
name="analyze_directory",
|
|
10
10
|
description="Generate a prompt to analyze a directory structure",
|
|
11
11
|
arguments=[
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
PromptArgument(
|
|
13
|
+
name="directory",
|
|
14
|
+
description="Directory path to analyze",
|
|
15
|
+
required=True
|
|
16
|
+
)
|
|
17
17
|
]
|
|
18
18
|
),
|
|
19
19
|
Prompt(
|
|
20
20
|
name="find_and_read",
|
|
21
21
|
description="Generate a prompt to find and read files matching a pattern",
|
|
22
22
|
arguments=[
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
23
|
+
PromptArgument(
|
|
24
|
+
name="directory",
|
|
25
|
+
description="Directory to search in",
|
|
26
|
+
required=True
|
|
27
|
+
),
|
|
28
|
+
PromptArgument(
|
|
29
|
+
name="pattern",
|
|
30
|
+
description="File pattern to search for",
|
|
31
|
+
required=True
|
|
32
|
+
)
|
|
33
33
|
]
|
|
34
34
|
)
|
|
35
35
|
]
|
|
36
36
|
|
|
37
37
|
return ListPromptsResult(prompts=prompts)
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
async def get_prompt(ctx: ServerRequestContext, params: GetPromptRequestParams) -> GetPromptResult:
|
|
40
|
+
name, arguments = params.name, params.arguments or {}
|
|
41
41
|
"""Get a prompt"""
|
|
42
42
|
if name == "analyze_directory":
|
|
43
43
|
directory = arguments["directory"]
|
|
44
44
|
content = f"""Please analyze the directory structure at: {directory}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
5. Any potential issues or recommendations"""
|
|
45
|
+
Use the list_directory_tool to explore the directory and provide insights about:
|
|
46
|
+
1. Total number of files and directories
|
|
47
|
+
2. File types and their distribution
|
|
48
|
+
3. Largest files
|
|
49
|
+
4. Directory organization
|
|
50
|
+
5. Any potential issues or recommendations"""
|
|
52
51
|
|
|
53
52
|
return GetPromptResult(
|
|
54
53
|
description=f"Analysis prompt for directory: {directory}",
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
from mcp import MCPError
|
|
5
|
+
from mcp.server import ServerRequestContext
|
|
6
|
+
from mcp.types import (INVALID_PARAMS, ListResourcesResult, PaginatedRequestParams,
|
|
7
|
+
ReadResourceRequestParams, ReadResourceResult, Resource,
|
|
8
|
+
TextResourceContents)
|
|
9
|
+
|
|
10
|
+
from pathgate.config import is_path_allowed
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
async def list_resources(ctx: ServerRequestContext, params: PaginatedRequestParams | None) -> ListResourcesResult:
|
|
14
|
+
return ListResourcesResult(resources=[
|
|
15
|
+
Resource(uri="file://*", name="File Contents",
|
|
16
|
+
description="Read the contents of any file by providing its path",
|
|
17
|
+
mime_type="text/plain"),
|
|
18
|
+
Resource(uri="directory://*", name="Directory Listing",
|
|
19
|
+
description="List the contents of any directory by providing its path",
|
|
20
|
+
mime_type="application/json"),
|
|
21
|
+
])
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
async def read_resource(ctx: ServerRequestContext, params: ReadResourceRequestParams) -> ReadResourceResult:
|
|
25
|
+
uri = params.uri
|
|
26
|
+
|
|
27
|
+
if uri.startswith("file://"):
|
|
28
|
+
path = uri[len("file://"):]
|
|
29
|
+
if not is_path_allowed(path):
|
|
30
|
+
raise MCPError(INVALID_PARAMS, f"Access denied to {path}")
|
|
31
|
+
if not os.path.isfile(path):
|
|
32
|
+
raise MCPError(INVALID_PARAMS, f"File not found or not a file: {path}")
|
|
33
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
34
|
+
text = f.read()
|
|
35
|
+
return ReadResourceResult(contents=[
|
|
36
|
+
TextResourceContents(uri=uri, text=text, mime_type="text/plain")])
|
|
37
|
+
|
|
38
|
+
if uri.startswith("directory://"):
|
|
39
|
+
path = uri[len("directory://"):]
|
|
40
|
+
if not is_path_allowed(path):
|
|
41
|
+
raise MCPError(INVALID_PARAMS, f"Access denied to {path}")
|
|
42
|
+
if not os.path.isdir(path):
|
|
43
|
+
raise MCPError(INVALID_PARAMS, f"Directory not found: {path}")
|
|
44
|
+
entries = []
|
|
45
|
+
for item in os.listdir(path):
|
|
46
|
+
item_path = os.path.join(path, item)
|
|
47
|
+
stat = os.stat(item_path)
|
|
48
|
+
entries.append({"name": item,
|
|
49
|
+
"type": "directory" if os.path.isdir(item_path) else "file",
|
|
50
|
+
"size": stat.st_size, "modified": stat.st_mtime})
|
|
51
|
+
return ReadResourceResult(contents=[
|
|
52
|
+
TextResourceContents(uri=uri, text=json.dumps(entries, indent=2),
|
|
53
|
+
mime_type="application/json")])
|
|
54
|
+
|
|
55
|
+
raise MCPError(INVALID_PARAMS, f"Unsupported resource URI: {uri}")
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import asyncio
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from mcp.server import Server
|
|
9
|
+
from mcp.server.stdio import stdio_server
|
|
10
|
+
|
|
11
|
+
from pathgate import prompts, resources, tool_schemas, tools
|
|
12
|
+
from pathgate.config import configure
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _version() -> str:
|
|
16
|
+
try:
|
|
17
|
+
return version("pathgate")
|
|
18
|
+
except PackageNotFoundError:
|
|
19
|
+
return "0.0.0"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
server = Server(
|
|
23
|
+
"pathgate",
|
|
24
|
+
version=_version(),
|
|
25
|
+
on_list_tools=tool_schemas.list_tools,
|
|
26
|
+
on_call_tool=tools.call_tool,
|
|
27
|
+
on_list_resources=resources.list_resources,
|
|
28
|
+
on_read_resource=resources.read_resource,
|
|
29
|
+
on_list_prompts=prompts.list_prompts,
|
|
30
|
+
on_get_prompt=prompts.get_prompt,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def parse_args(argv=None):
|
|
35
|
+
p = argparse.ArgumentParser(
|
|
36
|
+
prog="pathgate",
|
|
37
|
+
epilog="To add Pathgate to Claude Desktop automatically: pathgate install <folders>",
|
|
38
|
+
)
|
|
39
|
+
p.add_argument("paths", nargs="*", help="Directories the server may access")
|
|
40
|
+
p.add_argument("--read-only", action="store_true", help="Disable write/delete/move/copy tools")
|
|
41
|
+
args = p.parse_args(argv)
|
|
42
|
+
|
|
43
|
+
env_paths = [x for x in os.environ.get("FS_MCP_ALLOWED_PATHS", "").split(os.pathsep) if x]
|
|
44
|
+
raw = args.paths or env_paths
|
|
45
|
+
if not raw:
|
|
46
|
+
p.error("no allowed directories given. Pass at least one path "
|
|
47
|
+
"or set FS_MCP_ALLOWED_PATHS.")
|
|
48
|
+
|
|
49
|
+
paths = [str(Path(x).expanduser().resolve()) for x in raw]
|
|
50
|
+
for x in paths:
|
|
51
|
+
if not Path(x).is_dir():
|
|
52
|
+
p.error(f"not a directory: {x}")
|
|
53
|
+
return paths, args.read_only
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
async def _run():
|
|
57
|
+
async with stdio_server() as (read_stream, write_stream):
|
|
58
|
+
await server.run(read_stream, write_stream, server.create_initialization_options())
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def main():
|
|
62
|
+
# `pathgate install ...` edits the Claude Desktop config instead of
|
|
63
|
+
# starting the server. (A folder literally named "install" can still be
|
|
64
|
+
# passed as ./install.)
|
|
65
|
+
if sys.argv[1:2] == ["install"]:
|
|
66
|
+
from pathgate.install import main as install_main
|
|
67
|
+
raise SystemExit(install_main(sys.argv[2:]))
|
|
68
|
+
|
|
69
|
+
paths, read_only = parse_args()
|
|
70
|
+
configure(paths, read_only)
|
|
71
|
+
asyncio.run(_run())
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
if __name__ == "__main__":
|
|
75
|
+
main()
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
from
|
|
2
|
-
from mcp.types import
|
|
1
|
+
from mcp.server import ServerRequestContext
|
|
2
|
+
from mcp.types import ListToolsResult, PaginatedRequestParams, Tool
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
async def list_tools() -> ListToolsResult:
|
|
4
|
+
async def list_tools(ctx: ServerRequestContext, params: PaginatedRequestParams | None) -> ListToolsResult:
|
|
6
5
|
"""List available tools"""
|
|
7
6
|
tools = [
|
|
8
7
|
Tool(
|
|
@@ -1,44 +1,35 @@
|
|
|
1
|
-
from mcp.
|
|
2
|
-
import
|
|
3
|
-
from pathgate.config import is_path_allowed, server, is_read_only
|
|
1
|
+
from mcp.server import ServerRequestContext
|
|
2
|
+
from mcp.types import CallToolRequestParams, CallToolResult, TextContent
|
|
4
3
|
import logging
|
|
4
|
+
import os
|
|
5
5
|
import json
|
|
6
|
+
from pathgate.config import is_path_allowed, is_read_only
|
|
6
7
|
import shutil
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
|
|
9
10
|
logger = logging.getLogger(__name__)
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
async def call_tool(ctx: ServerRequestContext, params: CallToolRequestParams) -> CallToolResult:
|
|
13
|
+
handler = HANDLERS.get(params.name)
|
|
14
|
+
if handler is None:
|
|
15
|
+
return _error(f"Unknown tool: {params.name}")
|
|
14
16
|
try:
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
return await write_file_tool(arguments)
|
|
19
|
-
elif name == "list_directory_tool":
|
|
20
|
-
return await list_directory_tool(arguments)
|
|
21
|
-
elif name == "create_directory_tool":
|
|
22
|
-
return await create_directory_tool(arguments)
|
|
23
|
-
elif name == "delete_file_tool":
|
|
24
|
-
return await delete_file_tool(arguments)
|
|
25
|
-
elif name == "delete_directory_tool":
|
|
26
|
-
return await delete_directory_tool(arguments)
|
|
27
|
-
elif name == "move_file_tool":
|
|
28
|
-
return await move_file_tool(arguments)
|
|
29
|
-
elif name == "copy_file_tool":
|
|
30
|
-
return await copy_file_tool(arguments)
|
|
31
|
-
elif name == "search_files_tool":
|
|
32
|
-
return await search_files_tool(arguments)
|
|
33
|
-
elif name == "get_file_info_tool":
|
|
34
|
-
return await get_file_info_tool(arguments)
|
|
35
|
-
else:
|
|
36
|
-
raise ValueError(f"Unknown tool: {name}")
|
|
17
|
+
result = await handler(params.arguments or {})
|
|
18
|
+
except KeyError as e:
|
|
19
|
+
return _error(f"Missing required argument: {e.args[0]}")
|
|
37
20
|
except Exception as e:
|
|
38
|
-
logger.
|
|
39
|
-
return
|
|
40
|
-
|
|
41
|
-
|
|
21
|
+
logger.exception("Error in %s", params.name)
|
|
22
|
+
return _error(str(e))
|
|
23
|
+
|
|
24
|
+
# existing handlers return "Error..." text without the error flag
|
|
25
|
+
text = result.content[0].text if result.content else ""
|
|
26
|
+
if text.startswith("Error") and not result.is_error:
|
|
27
|
+
result = result.model_copy(update={"is_error": True})
|
|
28
|
+
return result
|
|
29
|
+
|
|
30
|
+
def _error(msg: str) -> CallToolResult:
|
|
31
|
+
return CallToolResult(content=[TextContent(type="text", text=f"Error: {msg}")], is_error=True)
|
|
32
|
+
|
|
42
33
|
|
|
43
34
|
# Tool implementations
|
|
44
35
|
async def read_file_tool(arguments: dict) -> CallToolResult:
|
|
@@ -90,8 +81,7 @@ async def write_file_tool(arguments: dict) -> CallToolResult:
|
|
|
90
81
|
content = arguments["content"]
|
|
91
82
|
encoding = arguments.get("encoding", "utf-8")
|
|
92
83
|
|
|
93
|
-
|
|
94
|
-
if not iro:
|
|
84
|
+
if is_read_only():
|
|
95
85
|
return CallToolResult(
|
|
96
86
|
content=[TextContent(type="text", text=f"Error: Write operations are disabled in read-only mode.")]
|
|
97
87
|
)
|
|
@@ -187,10 +177,9 @@ async def create_directory_tool(arguments: dict) -> CallToolResult:
|
|
|
187
177
|
"""Create a new directory"""
|
|
188
178
|
path = arguments["path"]
|
|
189
179
|
|
|
190
|
-
|
|
191
|
-
if not iro:
|
|
180
|
+
if is_read_only():
|
|
192
181
|
return CallToolResult(
|
|
193
|
-
content=[TextContent(type="text", text=f"Error:
|
|
182
|
+
content=[TextContent(type="text", text=f"Error: Write operations are disabled in read-only mode.")]
|
|
194
183
|
)
|
|
195
184
|
|
|
196
185
|
if not is_path_allowed(path):
|
|
@@ -217,8 +206,7 @@ async def delete_file_tool(arguments: dict) -> CallToolResult:
|
|
|
217
206
|
"""Delete a file"""
|
|
218
207
|
path = arguments["path"]
|
|
219
208
|
|
|
220
|
-
|
|
221
|
-
if not iro:
|
|
209
|
+
if is_read_only():
|
|
222
210
|
return CallToolResult(
|
|
223
211
|
content=[TextContent(type="text", text=f"Error: Delete operations are disabled in read-only mode.")]
|
|
224
212
|
)
|
|
@@ -258,8 +246,7 @@ async def delete_directory_tool(arguments: dict) -> CallToolResult:
|
|
|
258
246
|
path = arguments["path"]
|
|
259
247
|
recursive = arguments.get("recursive", False)
|
|
260
248
|
|
|
261
|
-
|
|
262
|
-
if not iro:
|
|
249
|
+
if is_read_only():
|
|
263
250
|
return CallToolResult(
|
|
264
251
|
content=[TextContent(type="text", text=f"Error: Delete operations are disabled in read-only mode.")]
|
|
265
252
|
)
|
|
@@ -303,8 +290,7 @@ async def move_file_tool(arguments: dict) -> CallToolResult:
|
|
|
303
290
|
source = arguments["source"]
|
|
304
291
|
destination = arguments["destination"]
|
|
305
292
|
|
|
306
|
-
|
|
307
|
-
if not iro:
|
|
293
|
+
if is_read_only():
|
|
308
294
|
return CallToolResult(
|
|
309
295
|
content=[TextContent(type="text", text=f"Error: Move operations are disabled in read-only mode.")]
|
|
310
296
|
)
|
|
@@ -340,8 +326,7 @@ async def copy_file_tool(arguments: dict) -> CallToolResult:
|
|
|
340
326
|
source = arguments["source"]
|
|
341
327
|
destination = arguments["destination"]
|
|
342
328
|
|
|
343
|
-
|
|
344
|
-
if not iro:
|
|
329
|
+
if is_read_only():
|
|
345
330
|
return CallToolResult(
|
|
346
331
|
content=[TextContent(type="text", text=f"Error: Copy operations are disabled in read-only mode.")]
|
|
347
332
|
)
|
|
@@ -399,7 +384,7 @@ async def search_files_tool(arguments: dict) -> CallToolResult:
|
|
|
399
384
|
else:
|
|
400
385
|
matches = list(path_obj.glob(pattern))
|
|
401
386
|
|
|
402
|
-
results = [str(
|
|
387
|
+
results = [str(m) for m in matches if is_path_allowed(str(m))]
|
|
403
388
|
|
|
404
389
|
result = {
|
|
405
390
|
"directory": directory,
|
|
@@ -455,3 +440,16 @@ async def get_file_info_tool(arguments: dict) -> CallToolResult:
|
|
|
455
440
|
return CallToolResult(
|
|
456
441
|
content=[TextContent(type="text", text=f"Error getting file info: {str(e)}")]
|
|
457
442
|
)
|
|
443
|
+
|
|
444
|
+
HANDLERS = {
|
|
445
|
+
"read_file_tool": read_file_tool,
|
|
446
|
+
"write_file_tool": write_file_tool,
|
|
447
|
+
"list_directory_tool": list_directory_tool,
|
|
448
|
+
"create_directory_tool": create_directory_tool,
|
|
449
|
+
"delete_file_tool": delete_file_tool,
|
|
450
|
+
"delete_directory_tool": delete_directory_tool,
|
|
451
|
+
"move_file_tool": move_file_tool,
|
|
452
|
+
"copy_file_tool": copy_file_tool,
|
|
453
|
+
"search_files_tool": search_files_tool,
|
|
454
|
+
"get_file_info_tool": get_file_info_tool,
|
|
455
|
+
}
|
|
@@ -4,12 +4,12 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pathgate"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.1"
|
|
8
8
|
description = "Filesystem MCP server with allow-listed directories"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
11
11
|
license = { text = "MIT" }
|
|
12
|
-
dependencies = ["mcp>=
|
|
12
|
+
dependencies = ["mcp>=2,<3"]
|
|
13
13
|
|
|
14
14
|
[project.scripts]
|
|
15
15
|
pathgate = "pathgate.server:main"
|
|
File without changes
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
from pathgate.config import server, is_path_allowed
|
|
2
|
-
from mcp.types import (Resource, ListResourcesResult, ReadResourceResult, TextContent)
|
|
3
|
-
import os
|
|
4
|
-
import json
|
|
5
|
-
|
|
6
|
-
# RESOURCES HANDLERS
|
|
7
|
-
@server.list_resources()
|
|
8
|
-
async def list_resources() -> ListResourcesResult:
|
|
9
|
-
"""List available resources"""
|
|
10
|
-
resources = [
|
|
11
|
-
Resource(
|
|
12
|
-
uri="file://*",
|
|
13
|
-
name="File Contents",
|
|
14
|
-
description="Read the contents of any file by providing its path",
|
|
15
|
-
mimeType="text/plain"
|
|
16
|
-
),
|
|
17
|
-
Resource(
|
|
18
|
-
uri="directory://*",
|
|
19
|
-
name="Directory Listing",
|
|
20
|
-
description="List the contents of any directory by providing its path",
|
|
21
|
-
mimeType="application/json"
|
|
22
|
-
)
|
|
23
|
-
]
|
|
24
|
-
|
|
25
|
-
return ListResourcesResult(resources=resources)
|
|
26
|
-
|
|
27
|
-
@server.read_resource()
|
|
28
|
-
async def read_resource(uri: str) -> ReadResourceResult:
|
|
29
|
-
"""Read a resource"""
|
|
30
|
-
if uri.startswith("file://"):
|
|
31
|
-
path = uri[7:] # Remove "file://" prefix
|
|
32
|
-
|
|
33
|
-
if not is_path_allowed(path):
|
|
34
|
-
return ReadResourceResult(
|
|
35
|
-
contents=[TextContent(type="text", text=f"Error: Access denied to {path}")]
|
|
36
|
-
)
|
|
37
|
-
|
|
38
|
-
if not os.path.exists(path) or not os.path.isfile(path):
|
|
39
|
-
return ReadResourceResult(
|
|
40
|
-
contents=[TextContent(type="text", text=f"Error: File not found or not a file: {path}")]
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
try:
|
|
44
|
-
with open(path, 'r', encoding='utf-8') as f:
|
|
45
|
-
content = f.read()
|
|
46
|
-
return ReadResourceResult(
|
|
47
|
-
contents=[TextContent(type="text", text=content)]
|
|
48
|
-
)
|
|
49
|
-
except Exception as e:
|
|
50
|
-
return ReadResourceResult(
|
|
51
|
-
contents=[TextContent(type="text", text=f"Error reading file: {str(e)}")]
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
elif uri.startswith("directory://"):
|
|
55
|
-
path = uri[12:] # Remove "directory://" prefix
|
|
56
|
-
|
|
57
|
-
if not is_path_allowed(path):
|
|
58
|
-
return ReadResourceResult(
|
|
59
|
-
contents=[TextContent(type="text", text=f"Error: Access denied to {path}")]
|
|
60
|
-
)
|
|
61
|
-
|
|
62
|
-
if not os.path.exists(path) or not os.path.isdir(path):
|
|
63
|
-
return ReadResourceResult(
|
|
64
|
-
contents=[TextContent(type="text", text=f"Error: Directory not found or not a directory: {path}")]
|
|
65
|
-
)
|
|
66
|
-
|
|
67
|
-
try:
|
|
68
|
-
entries = []
|
|
69
|
-
for item in os.listdir(path):
|
|
70
|
-
item_path = os.path.join(path, item)
|
|
71
|
-
stat = os.stat(item_path)
|
|
72
|
-
entries.append({
|
|
73
|
-
"name": item,
|
|
74
|
-
"type": "directory" if os.path.isdir(item_path) else "file",
|
|
75
|
-
"size": stat.st_size,
|
|
76
|
-
"modified": stat.st_mtime
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
return ReadResourceResult(
|
|
80
|
-
contents=[TextContent(type="text", text=json.dumps(entries, indent=2))]
|
|
81
|
-
)
|
|
82
|
-
except Exception as e:
|
|
83
|
-
return ReadResourceResult(
|
|
84
|
-
contents=[TextContent(type="text", text=f"Error listing directory: {str(e)}")]
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
else:
|
|
88
|
-
return ReadResourceResult(
|
|
89
|
-
contents=[TextContent(type="text", text="Error: Unsupported resource URI")]
|
|
90
|
-
)
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
from pathgate.config import configure, server
|
|
2
|
-
from mcp.server.stdio import stdio_server
|
|
3
|
-
from pathgate import tools, resources, prompts, tool_schemas
|
|
4
|
-
import asyncio
|
|
5
|
-
import os
|
|
6
|
-
import argparse
|
|
7
|
-
from pathlib import Path
|
|
8
|
-
|
|
9
|
-
server.list_tools()(tool_schemas.list_tools)
|
|
10
|
-
server.call_tool()(tools.call_tool)
|
|
11
|
-
server.list_resources()(resources.list_resources)
|
|
12
|
-
server.read_resource()(resources.read_resource)
|
|
13
|
-
server.list_prompts()(prompts.list_prompts)
|
|
14
|
-
server.get_prompt()(prompts.get_prompt)
|
|
15
|
-
|
|
16
|
-
def parse_args(argv=None):
|
|
17
|
-
p = argparse.ArgumentParser(prog="pathgate")
|
|
18
|
-
p.add_argument("paths", nargs="*", help="Directories the server may access")
|
|
19
|
-
p.add_argument("--read-only", action="store_true", help="Disable write/delete/move/copy tools")
|
|
20
|
-
args = p.parse_args(argv)
|
|
21
|
-
|
|
22
|
-
env_paths = [x for x in os.environ.get("FS_MCP_ALLOWED_PATHS", "").split(os.pathsep) if x]
|
|
23
|
-
raw = args.paths or env_paths
|
|
24
|
-
if not raw:
|
|
25
|
-
p.error("no allowed directories given. Pass at least one path "
|
|
26
|
-
"or set FS_MCP_ALLOWED_PATHS.")
|
|
27
|
-
|
|
28
|
-
paths = [str(Path(x).expanduser().resolve()) for x in raw]
|
|
29
|
-
for x in paths:
|
|
30
|
-
if not Path(x).is_dir():
|
|
31
|
-
p.error(f"not a directory: {x}")
|
|
32
|
-
return paths, args.read_only
|
|
33
|
-
|
|
34
|
-
async def main():
|
|
35
|
-
async with stdio_server() as (read_stream, write_stream):
|
|
36
|
-
await server.run(read_stream, write_stream, server.create_initialization_options())
|
|
37
|
-
|
|
38
|
-
if __name__ == "__main__":
|
|
39
|
-
paths, read_only = parse_args()
|
|
40
|
-
configure(paths, read_only)
|
|
41
|
-
asyncio.run(main())
|
|
42
|
-
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|