返回提交历史
Modified
g4f/Provider/DeepInfra.py
+70
-4
Modified
g4f/Provider/Qwen.py
+14
-24
Modified
g4f/Provider/needs_auth/DeepSeekAPI.py
+6
-3
Modified
g4f/Provider/template/OpenaiTemplate.py
+2
-1
Modified
g4f/api/stubs.py
+1
-1
Modified
g4f/mcp/__init__.py
+0
-8
Modified
g4f/mcp/pa_provider.py
+8
-1
Modified
g4f/mcp/server.py
+3
-7
Modified
g4f/mcp/tools.py
+9
-376
XFEstudio/gpt4free
Refactor DeepInfra and Qwen providers to enhance token retrieval and reasoning effort handling; remove unused tools from MCP
fb30f83a
代码差异
9 个文件
+113
-425
@@ -1,17 +1,18 @@
1
1
from __future__ import annotations
2
2
3
import time
3
4
import asyncio
4
5
import requests
5
6
6
7
from ..requests import get_nodriver_session
8
from ..errors import MissingRequirementsError
7
9
from .template import OpenaiTemplate
8
10
9
11
async def get_turnstile_token_async() -> str:
10
12
try:
11
13
import zendriver as zd
12
14
except ImportError:
13
from ..errors import MissingRequirementsError
14
raise MissingRequirementsError('Install "zendriver" package to use DeepInfra without an API key | pip install zendriver')
15
return None
15
16
16
17
async with get_nodriver_session() as session:
17
18
# Generate a token on any model's page; it is valid for the entire domain
@@ -49,6 +50,69 @@ async def get_turnstile_token_async() -> str:
49
50
50
51
return token
51
52
53
def get_turnstile_token() -> str:
54
"""
55
Opens the DeepInfra page using DrissionPage to obtain a Turnstile token.
56
Raises MissingRequirementsError if DrissionPage is not installed.
57
"""
58
try:
59
from DrissionPage import ChromiumPage, ChromiumOptions
60
except ImportError:
61
raise MissingRequirementsError('Install "DrissionPage" package to use DeepInfra without an API key | pip install DrissionPage')
62
63
co = ChromiumOptions()
64
# Hide the window off-screen
65
co.set_argument('--window-position=-2000,-2000')
66
co.set_argument('--window-size=800,600')
67
co.set_argument('--log-level=3')
68
69
page = ChromiumPage(co)
70
71
try:
72
# Generate a token on any model's page; it is valid for the entire domain
73
page.get('https://deepinfra.com/' + DeepInfra.default_model)
74
75
# Inject JS to block the original request
76
js_block_fetch = """
77
const origFetch = window.fetch;
78
window.fetch = async function(...args) {
79
let url = args[0];
80
if (typeof url === 'string' && url.includes('/chat/completions')) {
81
return new Response('{}', {status: 200});
82
}
83
return origFetch.apply(this, args);
84
};
85
"""
86
page.run_js(js_block_fetch)
87
88
# Initiate Turnstile
89
textarea = page.ele('tag:textarea', timeout=15)
90
if not textarea:
91
return ""
92
93
textarea.input('Test')
94
textarea.input('\n')
95
96
# Wait for the challenge to be solved
97
token_input = page.ele('@name=cf-turnstile-response', timeout=20)
98
99
if not token_input:
100
return ""
101
102
token = ""
103
for _ in range(40):
104
token = token_input.attr('value')
105
if token:
106
break
107
time.sleep(0.5)
108
109
return token
110
finally:
111
try:
112
page.quit()
113
except:
114
pass
115
52
116
class DeepInfra(OpenaiTemplate):
53
117
url = "https://deepinfra.com"
54
118
login_url = "https://deepinfra.com/dash/api_keys"
@@ -70,7 +134,6 @@ class DeepInfra(OpenaiTemplate):
70
134
cls.image_models = [model["model_name"] for model in models if model.get("reported_type") == "text-to-image"]
71
135
if cls.live == 0 and cls.models:
72
136
cls.live += 1
73
74
137
return cls.models
75
138
76
139
@classmethod
@@ -82,7 +145,6 @@ class DeepInfra(OpenaiTemplate):
82
145
if headers is None:
83
146
headers = {}
84
147
headers["X-DeepInfra-Turnstile"] = token
85
86
148
async for chunk in super().create_async_generator(model, messages, api_key=api_key, headers=headers, **kwargs):
87
149
yield chunk
88
150
@@ -93,4 +155,8 @@ class DeepInfra(OpenaiTemplate):
93
155
headers["X-Deepinfra-Source"] = "model-embed"
94
156
headers["Origin"] = "https://deepinfra.com"
95
157
headers["Referer"] = "https://deepinfra.com/"
158
if not headers.get("X-DeepInfra-Turnstile"):
159
token = get_turnstile_token()
160
if token:
161
headers["X-DeepInfra-Turnstile"] = token
96
162
return headers
@@ -26,13 +26,11 @@ from ..typing import AsyncResult, Messages, MediaListType
26
26
27
27
try:
28
28
import curl_cffi
29
30
29
has_curl_cffi = True
31
30
except ImportError:
32
31
has_curl_cffi = False
33
32
try:
34
33
import zendriver as nodriver
35
36
34
has_nodriver = True
37
35
except ImportError:
38
36
has_nodriver = False
@@ -124,8 +122,6 @@ class Qwen(AsyncGeneratorProvider, ProviderModelMixin):
124
122
url = "https://chat.qwen.ai"
125
123
working = True
126
124
active_by_default = True
127
supports_stream = True
128
supports_message_history = False
129
125
image_cache = True
130
126
_models_loaded = True
131
127
image_models = image_models
@@ -372,19 +368,19 @@ class Qwen(AsyncGeneratorProvider, ProviderModelMixin):
372
368
373
369
@classmethod
374
370
async def create_async_generator(
375
cls,
376
model: str,
377
messages: Messages,
378
media: MediaListType = None,
379
conversation: JsonConversation = None,
380
proxy: str = None,
381
stream: bool = True,
382
reasoning_effort: Optional[Literal["low", "medium", "high"]] = "medium",
383
chat_type: Literal[
384
"t2t", "search", "artifacts", "web_dev", "deep_research", "t2i", "image_edit", "t2v"
385
] = "t2t",
386
aspect_ratio: Optional[Literal["1:1", "4:3", "3:4", "16:9", "9:16"]] = None,
387
**kwargs
371
cls,
372
model: str,
373
messages: Messages,
374
media: MediaListType = None,
375
conversation: JsonConversation = None,
376
proxy: str = None,
377
stream: bool = True,
378
reasoning_effort: Optional[Literal["none", "low", "medium", "high", "x-high"]] = "medium",
379
chat_type: Literal[
380
"t2t", "search", "artifacts", "web_dev", "deep_research", "t2i", "image_edit", "t2v"
381
] = "t2t",
382
aspect_ratio: Optional[Literal["1:1", "4:3", "3:4", "16:9", "9:16"]] = None,
383
**kwargs
388
384
) -> AsyncResult:
389
385
"""
390
386
chat_type:
@@ -539,7 +535,7 @@ class Qwen(AsyncGeneratorProvider, ProviderModelMixin):
539
535
except (json.JSONDecodeError, KeyError, IndexError):
540
536
continue
541
537
if usage:
542
yield Usage(**usage)
538
yield Usage.from_dict(usage)
543
539
return
544
540
545
541
except (aiohttp.ClientResponseError, RuntimeError) as e:
@@ -556,10 +552,4 @@ class Qwen(AsyncGeneratorProvider, ProviderModelMixin):
556
552
else:
557
553
raise e
558
554
raise RateLimitError("The Qwen provider reached the request limit after 5 attempts.")
559
560
# except CloudflareError as e:
561
# debug.error(f"{cls.__name__}: {e}")
562
# args = await cls.get_args(proxy, **kwargs)
563
# cookie = "; ".join([f"{k}={v}" for k, v in args["cookies"].items()])
564
# continue
565
555
raise RateLimitError("The Qwen provider reached the limit Cloudflare.")
@@ -2,12 +2,11 @@ from __future__ import annotations
2
2
3
3
import os
4
4
import json
5
import time
6
5
import uuid
7
6
import base64
8
7
from datetime import datetime
9
from typing import AsyncIterator
10
8
from pathlib import Path
9
from typing import Optional, Literal
11
10
12
11
from g4f.typing import AsyncResult, Messages, Cookies
13
12
from g4f.requests import StreamSession, raise_for_status, sse_stream, FormData
@@ -335,6 +334,7 @@ class DeepSeekAPI(AsyncGeneratorProvider, ProviderModelMixin):
335
334
conversation: JsonConversation = None,
336
335
web_search: bool = False,
337
336
media: list = None,
337
reasoning_effort: Optional[Literal["none", "low", "medium", "high", "x-high"]] = None,
338
338
delete_session: bool = False,
339
339
**kwargs
340
340
) -> AsyncResult:
@@ -412,7 +412,10 @@ class DeepSeekAPI(AsyncGeneratorProvider, ProviderModelMixin):
412
412
prompt = get_last_user_message(messages)
413
413
414
414
# Determine thinking mode
415
thinking_enabled = bool(model) and "deepseek-r1" in model
415
if reasoning_effort is not None and reasoning_effort != "none":
416
thinking_enabled = True
417
else:
418
thinking_enabled = bool(model) and "deepseek-r1" in model
416
419
417
420
yield JsonRequest.from_dict({
418
421
"prompt": prompt,
@@ -10,7 +10,6 @@ from ...image import use_aspect_ratio
10
10
from ...image.copy_images import save_response_media
11
11
from ...providers.response import *
12
12
from ...tools.media import render_messages
13
from ...tools.run_tools import AuthManager
14
13
from ...config import AppConfig
15
14
from ...errors import MissingAuthError
16
15
from ... import debug
@@ -37,6 +36,7 @@ class OpenaiTemplate(AsyncGeneratorProvider, ProviderModelMixin, RaiseErrorMixin
37
36
async def get_quota(cls, api_key: Optional[str] = None, **kwargs) -> dict:
38
37
"""Get the quota information for the API key."""
39
38
if not api_key:
39
from ...tools.run_tools import AuthManager
40
40
api_key = AuthManager.load_api_key(cls)
41
41
if api_key and cls.models_needs_auth and cls.quota_url is None:
42
42
cls.quota_url = f"{cls.base_url}/models"
@@ -84,6 +84,7 @@ class OpenaiTemplate(AsyncGeneratorProvider, ProviderModelMixin, RaiseErrorMixin
84
84
if api_key is None and cls.api_key is not None:
85
85
api_key = cls.api_key
86
86
if not api_key or AppConfig.disable_custom_api_key or not cls.is_provider_api_key(api_key):
87
from ...tools.run_tools import AuthManager
87
88
api_key = AuthManager.load_api_key(cls) or api_key
88
89
if base_url is None:
89
90
base_url = cls.base_url
@@ -43,7 +43,7 @@ class RequestConfig(BaseModel):
43
43
]
44
44
],
45
45
)
46
reasoning_effort: Optional[Literal["low", "medium", "high"]] = None
46
reasoning_effort: Optional[Literal["none", "low", "medium", "high", "x-high"]] = None
47
47
logit_bias: Optional[dict] = None
48
48
audio: Optional[dict] = None
49
49
response_format: Optional[dict] = None
@@ -15,13 +15,9 @@ from .tools import (
15
15
MarkItDownTool,
16
16
TextToAudioTool,
17
17
WebSearchTool,
18
WebScrapeTool,
19
18
ImageGenerationTool,
20
19
PythonExecuteTool,
21
20
FileReadTool,
22
FileReadLinesTool,
23
FileSearchTool,
24
FileWriteTool,
25
21
FileListTool,
26
22
FileDeleteTool,
27
23
CreateDirectoryTool,
@@ -49,14 +45,10 @@ __all__ = [
49
45
'MarkItDownTool',
50
46
'TextToAudioTool',
51
47
'WebSearchTool',
52
'WebScrapeTool',
53
48
'ImageGenerationTool',
54
49
# New tools
55
50
'PythonExecuteTool',
56
51
'FileReadTool',
57
'FileReadLinesTool',
58
'FileSearchTool',
59
'FileWriteTool',
60
52
'FileListTool',
61
53
'FileDeleteTool',
62
54
'CreateDirectoryTool',
@@ -78,6 +78,9 @@ def get_workspace_dir() -> Path:
78
78
workspace.mkdir(parents=True, exist_ok=True)
79
79
return workspace
80
80
81
def is_hidden_file(path: str) -> bool:
82
"""Return True if *path* is a hidden file (starts with a dot)."""
83
return any(part.startswith(".") for part in str(path).replace("\\", "/").split("/"))
81
84
82
85
# ---------------------------------------------------------------------------
83
86
# Whitelisted modules
@@ -599,6 +602,10 @@ class PaProviderRegistry:
599
602
models_list = list(getattr(cls, "models") or [])
600
603
except Exception:
601
604
pass
605
relative_path = pa_path.relative_to(directory).as_posix()
606
print(f"Loaded PA provider: {provider_id} ({relative_path})")
607
if is_hidden_file(relative_path):
608
relative_path = None
602
609
entries.append((
603
610
provider_id,
604
611
getattr(cls, "label", cls.__name__),
@@ -606,7 +613,7 @@ class PaProviderRegistry:
606
613
bool(getattr(cls, "working", True)),
607
614
getattr(cls, "url", None),
608
615
cls,
609
str(pa_path)[len(str(directory))+1:]
616
relative_path
610
617
))
611
618
except Exception as e:
612
619
debug.error(f"Failed to load PA provider from {pa_path}:", e)
@@ -27,9 +27,9 @@ from ..image import EXTENSIONS_MAP
27
27
from ..image.copy_images import get_media_dir, copy_media, get_source_url
28
28
29
29
from .tools import (
30
MarkItDownTool, TextToAudioTool, WebSearchTool, WebScrapeTool, ImageGenerationTool,
31
PythonExecuteTool, FileReadTool, FileReadLinesTool, FileSearchTool,
32
FileWriteTool, FileListTool, FileDeleteTool, ApplyPatchTool,
30
MarkItDownTool, TextToAudioTool, WebSearchTool, ImageGenerationTool,
31
PythonExecuteTool, FileReadTool,
32
FileListTool, FileDeleteTool, ApplyPatchTool,
33
33
CreateDirectoryTool, CreateFileTool, FetchWebpageTool,
34
34
FileSearchGlobTool, GrepSearchTool, GithubRepoTool, GithubTextSearchTool,
35
35
)
@@ -72,16 +72,12 @@ class MCPServer:
72
72
self.safe_mode = safe_mode
73
73
self.tools = {
74
74
'web_search': WebSearchTool(),
75
'web_scrape': WebScrapeTool(),
76
75
'image_generation': ImageGenerationTool(),
77
76
'text_to_audio': TextToAudioTool(),
78
77
'mark_it_down': MarkItDownTool(),
79
78
'python_execute': PythonExecuteTool(safe_mode=safe_mode),
80
79
'apply_patch': ApplyPatchTool(),
81
80
'file_read': FileReadTool(),
82
'file_read_lines': FileReadLinesTool(),
83
'file_search': FileSearchTool(),
84
'file_write': FileWriteTool(),
85
81
'file_list': FileListTool(safe_mode=safe_mode),
86
82
'file_delete': FileDeleteTool(),
87
83
'create_directory': CreateDirectoryTool(),
@@ -6,7 +6,6 @@ This module provides MCP tool implementations that wrap gpt4free capabilities:
6
6
- ImageGenerationTool: Image generation using various AI providers
7
7
- PythonExecuteTool: Safe Python code execution with whitelisted modules
8
8
- FileReadTool: Read files from the ~/.g4f/workspace directory (supports startLine/endLine)
9
- FileReadLinesTool: Read a range of lines from a workspace file
10
9
- FileSearchTool: Search files and file contents in the workspace
11
10
- FileWriteTool: Write files to the ~/.g4f/workspace directory
12
11
- FileListTool: List files in the ~/.g4f/workspace directory
@@ -130,75 +129,6 @@ class WebSearchTool(MCPTool):
130
129
}
131
130
132
131
133
class WebScrapeTool(MCPTool):
134
"""Web scraping tool using gpt4free's scraping capabilities"""
135
136
@property
137
def description(self) -> str:
138
return "Scrape and extract text content from a web page URL. Returns cleaned text content with optional word limit."
139
140
@property
141
def input_schema(self) -> Dict[str, Any]:
142
return {
143
"type": "object",
144
"properties": {
145
"url": {
146
"type": "string",
147
"description": "The URL of the web page to scrape"
148
},
149
"max_words": {
150
"type": "integer",
151
"description": "Maximum number of words to extract (default: 1000)",
152
"default": 1000
153
}
154
},
155
"required": ["url"]
156
}
157
158
async def execute(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
159
"""Execute web scraping
160
161
Returns:
162
Dict[str, Any]: Scraped content or error message
163
"""
164
from ..tools.fetch_and_scrape import fetch_and_scrape
165
from aiohttp import ClientSession
166
167
url = arguments.get("url", "")
168
max_words = arguments.get("max_words", 1000)
169
170
if not url:
171
return {
172
"error": "URL parameter is required"
173
}
174
175
try:
176
# Scrape the URL
177
async with ClientSession() as session:
178
content = await fetch_and_scrape(
179
session=session,
180
url=url,
181
max_words=max_words,
182
add_metadata=True
183
)
184
185
if not content:
186
return {
187
"error": "Failed to scrape content from URL"
188
}
189
190
return {
191
"url": url,
192
"content": content,
193
"word_count": len(content.split())
194
}
195
196
except Exception as e:
197
return {
198
"error": f"Scraping failed: {str(e)}"
199
}
200
201
202
132
class ImageGenerationTool(MCPTool):
203
133
"""Image generation tool using gpt4free's image generation capabilities"""
204
134
@@ -674,309 +604,6 @@ class FileReadTool(MCPTool):
674
604
return {"error": f"Read failed: {exc}"}
675
605
676
606
677
class FileReadLinesTool(MCPTool):
678
"""Read a range of lines from a file inside the workspace."""
679
680
@property
681
def description(self) -> str:
682
return (
683
"Read a range of lines from a text file inside the ~/.g4f/workspace directory. "
684
"Provide a relative path and optional start/end line indexes."
685
)
686
687
@property
688
def input_schema(self) -> Dict[str, Any]:
689
return {
690
"type": "object",
691
"properties": {
692
"path": {
693
"type": "string",
694
"description": "Relative path to the file inside the workspace",
695
},
696
"start_line": {
697
"type": "integer",
698
"description": "1-based first line to read (default: 1)",
699
"default": 1,
700
},
701
"end_line": {
702
"type": "integer",
703
"description": "1-based last line to read (inclusive)",
704
},
705
"max_lines": {
706
"type": "integer",
707
"description": "Maximum number of lines to return when end_line is not provided",
708
"default": 1000,
709
},
710
},
711
"required": ["path"],
712
}
713
714
async def execute(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
715
from .pa_provider import get_workspace_dir
716
717
rel_path = arguments.get("path", "")
718
if not rel_path:
719
return {"error": "path parameter is required"}
720
721
start_line = int(arguments.get("start_line", 1))
722
end_line = arguments.get("end_line")
723
max_lines = int(arguments.get("max_lines", 1000))
724
725
if start_line < 1:
726
return {"error": "start_line must be >= 1"}
727
728
if end_line is not None:
729
try:
730
end_line = int(end_line)
731
except (TypeError, ValueError):
732
return {"error": "end_line must be an integer"}
733
if end_line < start_line:
734
return {"error": "end_line must be greater than or equal to start_line"}
735
736
workspace = get_workspace_dir().resolve()
737
try:
738
target = (workspace / rel_path).resolve()
739
if not str(target).startswith(str(workspace)):
740
return {"error": "Access outside the workspace is not allowed"}
741
if not target.exists():
742
return {"error": f"File not found: {rel_path}"}
743
if not target.is_file():
744
return {"error": f"Path is not a file: {rel_path}"}
745
746
lines = target.read_text(encoding="utf-8").splitlines(True)
747
total_lines = len(lines)
748
if start_line > total_lines:
749
return {
750
"path": rel_path,
751
"start_line": start_line,
752
"end_line": end_line,
753
"lines": [],
754
"total_lines": total_lines,
755
}
756
757
start_index = start_line - 1
758
if end_line is None:
759
end_index = min(total_lines, start_index + max_lines)
760
else:
761
end_index = min(total_lines, end_line)
762
763
selected = lines[start_index:end_index]
764
return {
765
"path": rel_path,
766
"start_line": start_line,
767
"end_line": end_index,
768
"lines": selected,
769
"total_lines": total_lines,
770
"returned_lines": len(selected),
771
}
772
except Exception as exc:
773
return {"error": f"Read lines failed: {exc}"}
774
775
776
class FileWriteTool(MCPTool):
777
"""Write (or create) a file inside the ``~/.g4f/workspace`` directory."""
778
779
@property
780
def description(self) -> str:
781
return (
782
"Write text content to a file inside the ~/.g4f/workspace directory. "
783
"Creates parent directories as needed. "
784
"Provide a relative path from the workspace root and the content to write."
785
)
786
787
@property
788
def input_schema(self) -> Dict[str, Any]:
789
return {
790
"type": "object",
791
"properties": {
792
"path": {
793
"type": "string",
794
"description": "Relative path to the file inside the workspace",
795
},
796
"content": {
797
"type": "string",
798
"description": "Text content to write to the file",
799
},
800
"append": {
801
"type": "boolean",
802
"description": "If true, append to existing file instead of overwriting (default: false)",
803
"default": False,
804
},
805
},
806
"required": ["path", "content"],
807
}
808
809
async def execute(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
810
from .pa_provider import get_workspace_dir
811
812
rel_path = arguments.get("path", "")
813
content = arguments.get("content")
814
append = bool(arguments.get("append", False))
815
816
if not rel_path:
817
return {"error": "path parameter is required"}
818
if content is None:
819
return {"error": "content parameter is required"}
820
821
workspace = get_workspace_dir().resolve()
822
try:
823
target = (workspace / rel_path).resolve()
824
if not str(target).startswith(str(workspace)):
825
return {"error": "Access outside the workspace is not allowed"}
826
target.parent.mkdir(parents=True, exist_ok=True)
827
if append:
828
with open(target, "a", encoding="utf-8") as f:
829
f.write(content)
830
else:
831
target.write_text(content, encoding="utf-8")
832
result: Dict[str, Any] = {
833
"path": rel_path,
834
"size": len(content),
835
"appended": append,
836
}
837
origin = arguments.get("origin")
838
if origin:
839
result["url"] = f"{origin}/pa/files/{rel_path}"
840
return result
841
except Exception as exc:
842
return {"error": f"Write failed: {exc}"}
843
844
845
class FileSearchTool(MCPTool):
846
"""Search files and file contents inside the ``~/.g4f/workspace`` directory."""
847
848
@property
849
def description(self) -> str:
850
return (
851
"Search files and contents inside the ~/.g4f/workspace directory. "
852
"Provide a relative path, filename pattern, or text query to search."
853
)
854
855
@property
856
def input_schema(self) -> Dict[str, Any]:
857
return {
858
"type": "object",
859
"properties": {
860
"path": {
861
"type": "string",
862
"description": (
863
"Relative path to a workspace directory to search in "
864
"(default: workspace root)"
865
),
866
"default": "",
867
},
868
"pattern": {
869
"type": "string",
870
"description": (
871
"File name glob or substring pattern to filter files "
872
"(example: '*.py' or 'README')."
873
),
874
},
875
"query": {
876
"type": "string",
877
"description": "Text query to search inside file contents.",
878
},
879
"regex": {
880
"type": "boolean",
881
"description": "Interpret the query as a regular expression.",
882
"default": False,
883
},
884
"case_sensitive": {
885
"type": "boolean",
886
"description": "Perform case-sensitive matching for names and content.",
887
"default": False,
888
},
889
"recursive": {
890
"type": "boolean",
891
"description": "Search directories recursively (default: true).",
892
"default": True,
893
},
894
"max_results": {
895
"type": "integer",
896
"description": "Maximum number of matched files to return.",
897
"default": 100,
898
},
899
},
900
"required": [],
901
}
902
903
async def execute(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
904
from .pa_provider import get_workspace_dir
905
906
rel_path = arguments.get("path", "") or ""
907
pattern = arguments.get("pattern")
908
query = arguments.get("query")
909
regex = bool(arguments.get("regex", False))
910
case_sensitive = bool(arguments.get("case_sensitive", False))
911
recursive = bool(arguments.get("recursive", True))
912
max_results = int(arguments.get("max_results", 100))
913
914
if not pattern and not query:
915
return {"error": "At least one of pattern or query is required"}
916
917
workspace = get_workspace_dir().resolve()
918
try:
919
target = (workspace / rel_path).resolve() if rel_path else workspace
920
if not str(target).startswith(str(workspace)):
921
return {"error": "Access outside the workspace is not allowed"}
922
if not target.exists():
923
return {"error": f"Directory not found: {rel_path or '/'}"}
924
if not target.is_dir():
925
return {"error": f"Path is not a directory: {rel_path}"}
926
927
if pattern and not case_sensitive:
928
pattern_lower = pattern.lower()
929
930
if query and regex:
931
flags = 0 if case_sensitive else re.IGNORECASE
932
try:
933
query_re = re.compile(query, flags)
934
except re.error as exc:
935
return {"error": f"Invalid regular expression: {exc}"}
936
937
matches = []
938
iterator = target.rglob("*") if recursive else target.iterdir()
939
for entry in sorted(iterator):
940
if not entry.is_file():
941
continue
942
943
name = entry.name
944
if pattern:
945
candidate = name if case_sensitive else name.lower()
946
pattern_to_match = pattern if case_sensitive else pattern_lower
947
if not fnmatch.fnmatch(candidate, pattern_to_match):
948
continue
949
950
if query:
951
try:
952
content = entry.read_text(encoding="utf-8", errors="ignore")
953
except Exception:
954
continue
955
if regex:
956
if not query_re.search(content):
957
continue
958
else:
959
haystack = content if case_sensitive else content.lower()
960
needle = query if case_sensitive else query.lower()
961
if needle not in haystack:
962
continue
963
964
matches.append({
965
"path": str(entry.relative_to(workspace)),
966
"name": name,
967
})
968
if len(matches) >= max_results:
969
break
970
971
return {
972
"path": str(target.relative_to(workspace)),
973
"matches": matches,
974
"count": len(matches),
975
}
976
except Exception as exc:
977
return {"error": f"Search failed: {exc}"}
978
979
980
607
class FileListTool(MCPTool):
981
608
"""List files and directories inside the ``~/.g4f/workspace`` directory."""
982
609
@@ -1011,7 +638,7 @@ class FileListTool(MCPTool):
1011
638
}
1012
639
1013
640
async def execute(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
1014
from .pa_provider import get_workspace_dir
641
from .pa_provider import get_workspace_dir, is_hidden_file
1015
642
1016
643
rel_path = arguments.get("path", "") or ""
1017
644
recursive = bool(arguments.get("recursive", False))
@@ -1033,6 +660,8 @@ class FileListTool(MCPTool):
1033
660
iterator = target.rglob("*") if recursive else target.iterdir()
1034
661
for entry in sorted(iterator):
1035
662
try:
663
if is_hidden_file(entry):
664
continue
1036
665
rel = str(entry.relative_to(workspace))
1037
666
info: Dict[str, Any] = {
1038
667
"path": rel,
@@ -1294,7 +923,7 @@ class FileSearchGlobTool(MCPTool):
1294
923
}
1295
924
1296
925
async def execute(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
1297
from .pa_provider import get_workspace_dir
926
from .pa_provider import get_workspace_dir, is_hidden_file
1298
927
1299
928
pattern = arguments.get("query", "")
1300
929
max_results = int(arguments.get("maxResults", 50))
@@ -1310,6 +939,8 @@ class FileSearchGlobTool(MCPTool):
1310
939
continue
1311
940
rel = entry.relative_to(workspace)
1312
941
rel_str = rel.as_posix()
942
if is_hidden_file(rel_str):
943
continue
1313
944
if fnmatch.fnmatch(rel_str, pattern) or fnmatch.fnmatch(entry.name, pattern):
1314
945
matches.append(rel_str)
1315
946
if len(matches) >= max_results:
@@ -1359,7 +990,7 @@ class GrepSearchTool(MCPTool):
1359
990
}
1360
991
1361
992
async def execute(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
1362
from .pa_provider import get_workspace_dir
993
from .pa_provider import get_workspace_dir, is_hidden_file
1363
994
1364
995
pattern = arguments.get("query", "")
1365
996
is_regexp = bool(arguments.get("isRegexp", False))
@@ -1384,6 +1015,8 @@ class GrepSearchTool(MCPTool):
1384
1015
if not entry.is_file():
1385
1016
continue
1386
1017
rel_str = entry.relative_to(workspace).as_posix()
1018
if is_hidden_file(rel_str):
1019
continue
1387
1020
if include_pattern and not fnmatch.fnmatch(rel_str, include_pattern):
1388
1021
continue
1389
1022
try: