@teatak/mcp-server-browser
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@@teatak/mcp-server-browserSubmit the feedback form with the message: "Great job!""
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
@teatak/mcp-server-browser
An server that runs in the browser.
Register tools and prompts on a web page; expose them to a local MCP client (such as an agent daemon or sidecar process) over WebSocket. The browser acts as the MCP server — your tool handlers run client-side and the agent calls into them.
Why a "browser-side server"?
In the usual MCP topology, servers run as local processes and expose filesystem / database / API tools. This package flips that: the browser exposes capabilities to the agent. Useful when you want the agent to:
Drive a UI you're rendering (a canvas, a chart, a form).
Call into APIs that are only reachable from the user's browser session (authenticated SaaS, page-scoped APIs).
Get human-in-the-loop confirmation through DOM affordances.
At the wire level the browser dials a WebSocket to the agent; at the MCP
protocol level the browser is the server (handles tools/list,
tools/call, prompts/list, etc.).
How this differs from browser-automation MCP servers
If you've seen packages like , , , or , those go in the opposite direction from this one.
Browser-automation MCP servers |
| |
Where the MCP server runs | A local Node process (or cloud) | The browser page itself |
Who defines the tools | The package author (fixed set) | You — the page registers its own tools |
Browser's role | Target of automation (driven by agent) | Active producer of capabilities |
Typical tools |
| Anything your page can do — UI rendering, page-scoped APIs, etc. |
Bridge | Chrome extension / CDP / Playwright |
|
Short version: those packages give an agent a browser. This package lets your browser app give an agent custom tools.
The two patterns compose — you can use Playwright MCP to let an agent drive a page and have the same page expose its own MCP server (via this package) for higher-level domain operations.
Install
npm install @teatak/mcp-server-browserQuick start
import { createServer } from "@teatak/mcp-server-browser";
const server = createServer({
endpoint: "ws://127.0.0.1:9669/mcp/ws",
serverInfo: { name: "my-page", version: "1.0.0" },
});
server.registerTool({
name: "demo.echo",
description: "Echo back whatever the caller passed.",
inputSchema: {
type: "object",
properties: { text: { type: "string" } },
required: ["text"],
},
handler: async ({ text }) => ({ ok: true, text }),
});
server.connect();Tool metadata
Since 0.0.2, tool definitions may include MCP's _meta extension
object. It is passed through unchanged in tools/list, so clients can carry
private namespaced metadata without adding non-standard top-level fields.
server.registerTool({
name: "demo.echo",
description: "Echo back whatever the caller passed.",
inputSchema: { type: "object", properties: {} },
_meta: {
"example.com/tier": "lite",
},
handler: async () => ({ ok: true }),
});The other side — a minimal Go agent
The snippet above is only half the picture. Here's the matching MCP
client side — a Go program that accepts the WebSocket from the browser
and drives it over plain JSON-RPC 2.0. No external MCP library required;
the only dependency is gorilla/websocket.
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"github.com/gorilla/websocket"
)
type rpcMessage struct {
JSONRPC string `json:"jsonrpc"`
ID json.RawMessage `json:"id,omitempty"`
Method string `json:"method,omitempty"`
Params json.RawMessage `json:"params,omitempty"`
Result json.RawMessage `json:"result,omitempty"`
Error *struct {
Code int `json:"code"`
Message string `json:"message"`
} `json:"error,omitempty"`
}
// Single-flight roundtrip — sends one request and reads the next frame as
// its response. For concurrent calls, track pending requests by `id` in a
// sync.Map and dispatch from a dedicated read loop.
func roundtrip(conn *websocket.Conn, id int, method string, params any) (json.RawMessage, error) {
p, _ := json.Marshal(params)
idRaw, _ := json.Marshal(id)
if err := conn.WriteJSON(rpcMessage{
JSONRPC: "2.0", ID: idRaw, Method: method, Params: p,
}); err != nil {
return nil, err
}
var resp rpcMessage
if err := conn.ReadJSON(&resp); err != nil {
return nil, err
}
if resp.Error != nil {
return nil, fmt.Errorf("rpc %d: %s", resp.Error.Code, resp.Error.Message)
}
return resp.Result, nil
}
var upgrader = websocket.Upgrader{
// Tighten in production: pin Origin and validate a session token.
CheckOrigin: func(r *http.Request) bool { return true },
}
func main() {
http.HandleFunc("/mcp/ws", func(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return
}
defer conn.Close()
// 1. Handshake.
if _, err := roundtrip(conn, 1, "initialize", map[string]any{
"protocolVersion": "2025-03-26",
"clientInfo": map[string]any{"name": "demo-agent", "version": "0.1"},
"capabilities": map[string]any{},
}); err != nil {
log.Printf("initialize: %v", err)
return
}
// 2. Discover what the page exposes.
tools, err := roundtrip(conn, 2, "tools/list", struct{}{})
if err != nil {
log.Printf("tools/list: %v", err)
return
}
log.Printf("browser exposes: %s", tools)
// 3. Invoke one.
result, err := roundtrip(conn, 3, "tools/call", map[string]any{
"name": "demo.echo",
"arguments": map[string]any{"text": "hello from go"},
})
if err != nil {
log.Printf("tools/call: %v", err)
return
}
log.Printf("result: %s", result)
})
log.Println("listening on ws://127.0.0.1:9669/mcp/ws")
log.Fatal(http.ListenAndServe("127.0.0.1:9669", nil))
}Run this next to the Quick start snippet above: the page dials in, gets
initialized, and has its demo.echo tool called once. From here a
real agent typically grows a pending-request map keyed by id for
concurrent calls, a hub holding multiple browser sessions (one per tab),
and a notifications/tools/list_changed handler so the tool set can be
hot-reloaded as the page registers new tools.
Authentication
This package is unopinionated about auth. The browser's WebSocket
constructor only exposes two knobs (url and protocols); any auth scheme
ultimately rides on one of those. Instead of baking in a specific mechanism,
the library exposes a createSocket factory and lets you decide.
The factory is called on every (re)connect — perfect for short-lived tokens.
No auth (default)
createServer({
endpoint: "ws://127.0.0.1:9669/mcp/ws",
serverInfo: { name: "demo", version: "1.0.0" },
});Bearer token in URL
createServer({
endpoint: "ws://127.0.0.1:9669/mcp/ws",
serverInfo: { name: "demo", version: "1.0.0" },
createSocket: ({ endpoint }) =>
new WebSocket(`${endpoint}?token=${encodeURIComponent(TOKEN)}`),
});Bearer token in Sec-WebSocket-Protocol
Avoids tokens leaking into logs / browser history.
createServer({
endpoint: "ws://127.0.0.1:9669/mcp/ws",
serverInfo: { name: "demo", version: "1.0.0" },
createSocket: ({ endpoint }) =>
new WebSocket(endpoint, ["mcp.v1", `bearer.${TOKEN}`]),
});The MCP client side should validate the subprotocol on upgrade and echo the chosen one back.
Fresh token per connection
createServer({
endpoint: "ws://127.0.0.1:9669/mcp/ws",
serverInfo: { name: "demo", version: "1.0.0" },
createSocket: async ({ endpoint, attempt }) => {
const token = await fetch("/mcp/session-token").then((r) => r.text());
return new WebSocket(endpoint, [`bearer.${token}`]);
},
});attempt is 0 on the first connect and increments on each reconnect, in
case you want to short-circuit retries after some bound.
A note on threat model
Localhost WebSocket endpoints are not protected by the browser's
same-origin policy — any tab on the user's machine can dial ws://127.0.0.1.
For real deployments the MCP client side should pair token validation with
an Origin header allowlist.
Entry points
Import path | What's there |
| High-level |
| Raw |
| Wire-level JSON-RPC / MCP types and constants. |
Prompts
In addition to tools, this package supports a lightweight prompts capability
— a chunk of guidance text that the MCP client should append to its LLM
system instruction. Compared to MCP's standard prompts, this variant is
deliberately simpler: no arguments, no prompts/get round-trip — content
is delivered inline in prompts/list.
server.registerPrompt({
name: "ui-render-table.usage",
description: "Constraints for the ui_render_table tool.",
content: `When calling ui_render_table, only pass rows from real data. Never invent values.`,
});Status
Pre-1.0. API may evolve. Tested against MCP protocol version 2025-03-26.
License
MIT
This server cannot be installed
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Latest Blog Posts
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/teatak/mcp-server-browser'
If you have feedback or need assistance with the MCP directory API, please join our Discord server