openai/gpt-oss
Publicmirrored fromhttps://github.com/openai/gpt-ossAvailable
gpt-oss-mcp-server/browser_server.py
120lines · modecode
| 1 | import os |
| 2 | from collections.abc import AsyncIterator |
| 3 | from contextlib import asynccontextmanager |
| 4 | from dataclasses import dataclass, field |
| 5 | from typing import Union, Optional |
| 6 | |
| 7 | from mcp.server.fastmcp import Context, FastMCP |
| 8 | from gpt_oss.tools.simple_browser import SimpleBrowserTool |
| 9 | from gpt_oss.tools.simple_browser.backend import YouComBackend, ExaBackend |
| 10 | |
| 11 | @dataclass |
| 12 | class AppContext: |
| 13 | browsers: dict[str, SimpleBrowserTool] = field(default_factory=dict) |
| 14 | |
| 15 | def create_or_get_browser(self, session_id: str) -> SimpleBrowserTool: |
| 16 | if session_id not in self.browsers: |
| 17 | tool_backend = os.getenv("BROWSER_BACKEND", "youcom") |
| 18 | if tool_backend == "youcom": |
| 19 | backend = YouComBackend(source="web") |
| 20 | elif tool_backend == "exa": |
| 21 | backend = ExaBackend(source="web") |
| 22 | else: |
| 23 | raise ValueError(f"Invalid tool backend: {tool_backend}") |
| 24 | self.browsers[session_id] = SimpleBrowserTool(backend=backend) |
| 25 | return self.browsers[session_id] |
| 26 | |
| 27 | def remove_browser(self, session_id: str) -> None: |
| 28 | self.browsers.pop(session_id, None) |
| 29 | |
| 30 | |
| 31 | @asynccontextmanager |
| 32 | async def app_lifespan(_server: FastMCP) -> AsyncIterator[AppContext]: |
| 33 | yield AppContext() |
| 34 | |
| 35 | |
| 36 | # Pass lifespan to server |
| 37 | mcp = FastMCP( |
| 38 | name="browser", |
| 39 | instructions=r""" |
| 40 | Tool for browsing. |
| 41 | The `cursor` appears in brackets before each browsing display: `[{cursor}]`. |
| 42 | Cite information from the tool using the following format: |
| 43 | `【{cursor}†L{line_start}(-L{line_end})?】`, for example: `【6†L9-L11】` or `【8†L3】`. |
| 44 | Do not quote more than 10 words directly from the tool output. |
| 45 | sources=web |
| 46 | """.strip(), |
| 47 | lifespan=app_lifespan, |
| 48 | port=8001, |
| 49 | ) |
| 50 | |
| 51 | |
| 52 | @mcp.tool( |
| 53 | name="search", |
| 54 | title="Search for information", |
| 55 | description= |
| 56 | "Searches for information related to `query` and displays `topn` results.", |
| 57 | ) |
| 58 | async def search(ctx: Context, |
| 59 | query: str, |
| 60 | topn: int = 10, |
| 61 | source: Optional[str] = None) -> str: |
| 62 | """Search for information related to a query""" |
| 63 | browser = ctx.request_context.lifespan_context.create_or_get_browser( |
| 64 | ctx.client_id) |
| 65 | messages = [] |
| 66 | async for message in browser.search(query=query, topn=topn, source=source): |
| 67 | if message.content and hasattr(message.content[0], 'text'): |
| 68 | messages.append(message.content[0].text) |
| 69 | return "\n".join(messages) |
| 70 | |
| 71 | |
| 72 | @mcp.tool( |
| 73 | name="open", |
| 74 | title="Open a link or page", |
| 75 | description=""" |
| 76 | Opens the link `id` from the page indicated by `cursor` starting at line number `loc`, showing `num_lines` lines. |
| 77 | Valid link ids are displayed with the formatting: `【{id}†.*】`. |
| 78 | If `cursor` is not provided, the most recent page is implied. |
| 79 | If `id` is a string, it is treated as a fully qualified URL associated with `source`. |
| 80 | If `loc` is not provided, the viewport will be positioned at the beginning of the document or centered on the most relevant passage, if available. |
| 81 | Use this function without `id` to scroll to a new location of an opened page. |
| 82 | """.strip(), |
| 83 | ) |
| 84 | async def open_link(ctx: Context, |
| 85 | id: Union[int, str] = -1, |
| 86 | cursor: int = -1, |
| 87 | loc: int = -1, |
| 88 | num_lines: int = -1, |
| 89 | view_source: bool = False, |
| 90 | source: Optional[str] = None) -> str: |
| 91 | """Open a link or navigate to a page location""" |
| 92 | browser = ctx.request_context.lifespan_context.create_or_get_browser( |
| 93 | ctx.client_id) |
| 94 | messages = [] |
| 95 | async for message in browser.open(id=id, |
| 96 | cursor=cursor, |
| 97 | loc=loc, |
| 98 | num_lines=num_lines, |
| 99 | view_source=view_source, |
| 100 | source=source): |
| 101 | if message.content and hasattr(message.content[0], 'text'): |
| 102 | messages.append(message.content[0].text) |
| 103 | return "\n".join(messages) |
| 104 | |
| 105 | |
| 106 | @mcp.tool( |
| 107 | name="find", |
| 108 | title="Find pattern in page", |
| 109 | description= |
| 110 | "Finds exact matches of `pattern` in the current page, or the page given by `cursor`.", |
| 111 | ) |
| 112 | async def find_pattern(ctx: Context, pattern: str, cursor: int = -1) -> str: |
| 113 | """Find exact matches of a pattern in the current page""" |
| 114 | browser = ctx.request_context.lifespan_context.create_or_get_browser( |
| 115 | ctx.client_id) |
| 116 | messages = [] |
| 117 | async for message in browser.find(pattern=pattern, cursor=cursor): |
| 118 | if message.content and hasattr(message.content[0], 'text'): |
| 119 | messages.append(message.content[0].text) |
| 120 | return "\n".join(messages) |
| 121 | |