Skip to content

Commit 3a67d27

Browse files
committed
cleanup
1 parent 0e5b69b commit 3a67d27

File tree

3 files changed

+219
-3
lines changed

3 files changed

+219
-3
lines changed

examples/README.md

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,15 +43,30 @@ See [ollama/docs/api.md](https://github.com/ollama/ollama/blob/main/docs/api.md)
4343

4444
`OLLAMA_API_KEY` is required. You can get one from [ollama.com/settings/keys](https://ollama.com/settings/keys).
4545

46-
- [web-search-fetch.py](web-search-fetch.py)
46+
- [web-search.py](web-search.py)
4747

4848
#### MCP server
4949

5050
```sh
51-
uv run examples/mcp-web-search-and-fetch.py
51+
uv run examples/web-search-mcp.py
5252
```
5353

54-
- [mcp_web_search_crawl_server.py](mcp_web_search_crawl_server.py)
54+
Configuration to use with an MCP client:
55+
56+
```json
57+
{
58+
"mcpServers": {
59+
"web_search": {
60+
"type": "stdio",
61+
"command": "uv",
62+
"args": ["run", "path/to/ollama-python/examples/web-search-mcp.py"],
63+
"env": { "OLLAMA_API_KEY": "api-key" }
64+
}
65+
}
66+
}
67+
```
68+
69+
- [web-search-mcp.py](web-search-mcp.py)
5570

5671
### Multimodal with Images - Chat with a multimodal (image chat) model
5772

examples/web-search-mcp.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
# /// script
2+
# requires-python = ">=3.11"
3+
# dependencies = [
4+
# "mcp",
5+
# "rich",
6+
# "ollama",
7+
# ]
8+
# ///
9+
"""
10+
MCP stdio server exposing Ollama web_search and web_fetch as tools.
11+
12+
Environment:
13+
- OLLAMA_API_KEY (required): if set, will be used as Authorization header.
14+
"""
15+
16+
from __future__ import annotations
17+
18+
import asyncio
19+
from typing import Any, Dict
20+
21+
from ollama import Client
22+
23+
try:
24+
# Preferred high-level API (if available)
25+
from mcp.server.fastmcp import FastMCP # type: ignore
26+
27+
_FASTMCP_AVAILABLE = True
28+
except Exception:
29+
_FASTMCP_AVAILABLE = False
30+
31+
if not _FASTMCP_AVAILABLE:
32+
# Fallback to the low-level stdio server API
33+
from mcp.server import Server # type: ignore
34+
from mcp.server.stdio import stdio_server # type: ignore
35+
36+
37+
client = Client()
38+
39+
40+
def _web_search_impl(query: str, max_results: int = 3) -> Dict[str, Any]:
41+
res = client.web_search(query=query, max_results=max_results)
42+
return res.model_dump()
43+
44+
45+
def _web_fetch_impl(url: str) -> Dict[str, Any]:
46+
res = client.web_fetch(url=url)
47+
return res.model_dump()
48+
49+
50+
if _FASTMCP_AVAILABLE:
51+
app = FastMCP('ollama-search-fetch')
52+
53+
@app.tool()
54+
def web_search(query: str, max_results: int = 3) -> Dict[str, Any]:
55+
"""
56+
Perform a web search using Ollama's hosted search API.
57+
58+
Args:
59+
query: The search query to run.
60+
max_results: Maximum results to return (default: 3).
61+
62+
Returns:
63+
JSON-serializable dict matching ollama.WebSearchResponse.model_dump()
64+
"""
65+
66+
return _web_search_impl(query=query, max_results=max_results)
67+
68+
@app.tool()
69+
def web_fetch(url: str) -> Dict[str, Any]:
70+
"""
71+
Fetch the content of a web page for the provided URL.
72+
73+
Args:
74+
url: The absolute URL to fetch.
75+
76+
Returns:
77+
JSON-serializable dict matching ollama.WebFetchResponse.model_dump()
78+
"""
79+
80+
return _web_fetch_impl(url=url)
81+
82+
if __name__ == '__main__':
83+
app.run()
84+
85+
else:
86+
server = Server('ollama-search-fetch') # type: ignore[name-defined]
87+
88+
@server.tool() # type: ignore[attr-defined]
89+
async def web_search(query: str, max_results: int = 3) -> Dict[str, Any]:
90+
"""
91+
Perform a web search using Ollama's hosted search API.
92+
93+
Args:
94+
query: The search query to run.
95+
max_results: Maximum results to return (default: 3).
96+
"""
97+
98+
return await asyncio.to_thread(_web_search_impl, query, max_results)
99+
100+
@server.tool() # type: ignore[attr-defined]
101+
async def web_fetch(url: str) -> Dict[str, Any]:
102+
"""
103+
Fetch the content of a web page for the provided URL.
104+
105+
Args:
106+
url: The absolute URL to fetch.
107+
"""
108+
109+
return await asyncio.to_thread(_web_fetch_impl, url)
110+
111+
async def _main() -> None:
112+
async with stdio_server() as (read, write): # type: ignore[name-defined]
113+
await server.run(read, write) # type: ignore[attr-defined]
114+
115+
if __name__ == '__main__':
116+
asyncio.run(_main())

examples/web-search.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# /// script
2+
# requires-python = ">=3.11"
3+
# dependencies = [
4+
# "rich",
5+
# "ollama",
6+
# ]
7+
# ///
8+
from typing import Union
9+
10+
from rich import print
11+
12+
from ollama import WebFetchResponse, WebSearchResponse, chat, web_fetch, web_search
13+
14+
15+
def format_tool_results(
16+
results: Union[WebSearchResponse, WebFetchResponse],
17+
user_search: str,
18+
):
19+
output = []
20+
if isinstance(results, WebSearchResponse):
21+
output.append(f'Search results for "{user_search}":')
22+
for result in results.results:
23+
output.append(f'{result.title}' if result.title else f'{result.content}')
24+
output.append(f' URL: {result.url}')
25+
output.append(f' Content: {result.content}')
26+
output.append('')
27+
return '\n'.join(output).rstrip()
28+
29+
elif isinstance(results, WebFetchResponse):
30+
output.append(f'Fetch results for "{user_search}":')
31+
output.extend(
32+
[
33+
f'Title: {results.title}',
34+
f'URL: {user_search}' if user_search else '',
35+
f'Content: {results.content}',
36+
]
37+
)
38+
if results.links:
39+
output.append(f'Links: {", ".join(results.links)}')
40+
output.append('')
41+
return '\n'.join(output).rstrip()
42+
43+
44+
# client = Client(headers={'Authorization': f"Bearer {os.getenv('OLLAMA_API_KEY')}"} if api_key else None)
45+
available_tools = {'web_search': web_search, 'web_fetch': web_fetch}
46+
47+
query = "what is ollama's new engine"
48+
print('Query: ', query)
49+
50+
messages = [{'role': 'user', 'content': query}]
51+
while True:
52+
response = chat(model='qwen3', messages=messages, tools=[web_search, web_fetch], think=True)
53+
if response.message.thinking:
54+
print('Thinking: ')
55+
print(response.message.thinking + '\n\n')
56+
if response.message.content:
57+
print('Content: ')
58+
print(response.message.content + '\n')
59+
60+
messages.append(response.message)
61+
62+
if response.message.tool_calls:
63+
for tool_call in response.message.tool_calls:
64+
function_to_call = available_tools.get(tool_call.function.name)
65+
if function_to_call:
66+
args = tool_call.function.arguments
67+
result: Union[WebSearchResponse, WebFetchResponse] = function_to_call(**args)
68+
print('Result from tool call name:', tool_call.function.name, 'with arguments:')
69+
print(args)
70+
print()
71+
72+
user_search = args.get('query', '') or args.get('url', '')
73+
formatted_tool_results = format_tool_results(result, user_search=user_search)
74+
75+
print(formatted_tool_results[:300])
76+
print()
77+
78+
# caps the result at ~2000 tokens
79+
messages.append({'role': 'tool', 'content': formatted_tool_results[: 2000 * 4], 'tool_name': tool_call.function.name})
80+
else:
81+
print(f'Tool {tool_call.function.name} not found')
82+
messages.append({'role': 'tool', 'content': f'Tool {tool_call.function.name} not found', 'tool_name': tool_call.function.name})
83+
else:
84+
# no more tool calls, we can stop the loop
85+
break

0 commit comments

Comments
 (0)