@uplink-code/mcp 0.0.1 → 0.1.0
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.
- package/lib/mcp/index.js +37 -1
- package/lib/mcp/src/resources.d.ts +8 -4
- package/lib/mcp/src/server.d.ts +3 -7
- package/lib/mcp/src/session.d.ts +115 -0
- package/lib/mcp/src/tools.d.ts +8 -6
- package/package.json +16 -12
package/lib/mcp/index.js
CHANGED
|
@@ -1,3 +1,39 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
var
|
|
2
|
+
var S=Object.defineProperty;var o=(i,n)=>S(i,"name",{value:n,configurable:!0});import{McpServer as q}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as j}from"@modelcontextprotocol/sdk/server/stdio.js";import{z as s}from"zod";import{handleGetCurrentUrl as b,handleGetDomSnapshot as T,handleEvaluateJavascript as R,handleAwaitNetworkRequest as C,handleRunScript as I}from"@uplink-code/agent-tools";import P from"qrcode-terminal";function h(i){return/^\d{6}$/.test(i)?`${i.slice(0,3)}-${i.slice(3)}`:i}o(h,"formatPairingCode");function x(i){return new Promise(n=>{P.generate(i,{small:!0},e=>{n(e)})})}o(x,"renderQr");function c(i){return{content:[{type:"text",text:JSON.stringify(i,null,2)}]}}o(c,"ok");function a(i,n){return{content:[{type:"text",text:JSON.stringify({error:{code:i,message:n}},null,2)}],isError:!0}}o(a,"err");async function u(i,n){try{let e=await i.getPage(),t=await n(e);return c(t)}catch(e){let t=e instanceof Error?e.message:String(e);return t.includes("No active session")?a("not_connected",t):t.includes("No device paired")?a("no_device",t):t.includes("Device disconnected")?a("device_disconnected",t):a("page_error",t)}}o(u,"withPage");function m(i,n){i.registerTool("uplink_connect",{description:`Start a new Uplink session and return a 6-digit pairing code (primary) + URL (fallback) the user can use to pair a device via the Uplink Connect app. Call this before any other tool that drives the device.
|
|
3
|
+
|
|
4
|
+
AFTER this tool returns, in the SAME response turn, IMMEDIATELY call uplink_wait_for_device \u2014 do NOT end your turn saying "let me know when you've paired" or wait for the user to confirm. wait_for_device is the pairing-detection mechanism; see its description for the timeout-and-refresh loop pattern. Never poll uplink_session_status as a substitute.
|
|
5
|
+
|
|
6
|
+
CRITICAL: Claude Code collapses ALL MCP tool output behind ctrl+o by default \u2014 the tool result is invisible to the user unless they explicitly expand. Your response prose is the only thing the user sees. So:
|
|
7
|
+
|
|
8
|
+
1. Render this in YOUR OWN response prose (not the tool result):
|
|
9
|
+
\`\`\`
|
|
10
|
+
^ Uplink session started!
|
|
11
|
+
Pair with code: 123-456 (expires in 30s)
|
|
12
|
+
Or open: http://localhost:4100/link/s/<uuid>
|
|
13
|
+
\`\`\`
|
|
14
|
+
Use the exact code and URL from the tool result. Both lines MUST be visible without expanding anything.
|
|
15
|
+
|
|
16
|
+
2. The pairing code is the PRIMARY UX \u2014 lead with it. The URL is fallback if the user can't type the code.
|
|
17
|
+
|
|
18
|
+
3. Mention to the user that they can ask for a QR code if they want one \u2014 only call uplink_get_qr if they explicitly ask.
|
|
19
|
+
|
|
20
|
+
4. If the user mentions a custom SDK implementation, also include the JWT line from the tool result in your prose.
|
|
21
|
+
|
|
22
|
+
5. Do not paraphrase ("the pairing link above", "session is up"). Print the code + URL verbatim.`,inputSchema:{projectId:s.string().optional().describe("Uplink project ID. Defaults to the API key owner's Playground project.")}},async e=>{try{let t=await n.connect({projectId:e.projectId}),r=t.pairingCode!==null?`Pair with code: ${h(t.pairingCode)} (expires in ${t.pairingCodeExpiresInSeconds??"?"}s)
|
|
23
|
+
`:"",l=t.jwt!==null?`JWT (for custom SDK implementation): ${t.jwt}`:"";return{content:[{type:"text",text:`^ Uplink session started!
|
|
24
|
+
`+r+`Or open: ${t.qrUrl}
|
|
25
|
+
`+l}]}}catch(t){return a("connect_failed",t instanceof Error?t.message:String(t))}}),i.registerTool("uplink_refresh_pairing_code",{description:"Mint a fresh 6-digit pairing code for the active session. Use this if the previous code expired (30s TTL) before the user typed it \u2014 the session itself stays valid, only the OTP rotates. Surface the new code in YOUR OWN prose, same format as uplink_connect (XXX-XXX), e.g.:\n `New pairing code: 123-456 (expires in 30s)`\nClaude Code collapses tool output; the code must be visible in your prose.",inputSchema:{}},async()=>{try{let e=await n.refreshPairingCode();return{content:[{type:"text",text:`New pairing code: ${h(e.code)} (expires in ${e.ttlSeconds}s)`}]}}catch(e){let t=e instanceof Error?e.message:String(e);return t.includes("No active session")?a("not_connected",t):t.includes("Not signed in")?a("not_authenticated",t):a("refresh_failed",t)}}),i.registerTool("uplink_get_qr",{description:`Render the active session's pairing URL as an ASCII QR code. Opt-in: only call when the user explicitly asks to see a QR (e.g., 'show me a QR' / 'I want to scan').
|
|
26
|
+
|
|
27
|
+
CRITICAL: Claude Code collapses tool output behind ctrl+o. After calling this tool, you MUST paste the returned QR ASCII art VERBATIM into your own response prose \u2014 every line of unicode half-blocks, exactly as returned. Do not say 'scan above' or 'see the QR' \u2014 those refer to the collapsed tool output which the user cannot see. The user will only see what you write directly. Yes, this is verbose, but the user explicitly asked for the QR; render it.`,inputSchema:{}},async()=>{let e=n.currentQrUrl();if(!e)return a("not_connected","No active session. Call uplink_connect first.");let t=await x(e);return{content:[{type:"text",text:`Scan this QR with the Uplink Connect app to open ${e}:
|
|
28
|
+
|
|
29
|
+
${t}`}]}}),i.registerTool("uplink_wait_for_device",{description:`Block until a device finishes pairing on the active session, then return its metadata (deviceModel, platform, platformVersion, deviceType). Resolves immediately if a device is already paired.
|
|
30
|
+
|
|
31
|
+
Default timeout is intentionally short (25s, under the 30s OTP TTL) so each call returns control to you in time to refresh the code before it expires. When this errors with \`pairing_timeout\` and the user hasn't indicated they're done:
|
|
32
|
+
|
|
33
|
+
1. Call \`uplink_refresh_pairing_code\` to mint a fresh code.
|
|
34
|
+
2. Surface the new code in YOUR OWN prose (same XXX-XXX format as uplink_connect):
|
|
35
|
+
\`New pairing code: 123-456 (expires in 30s)\`
|
|
36
|
+
3. Call \`uplink_wait_for_device\` again to keep waiting.
|
|
37
|
+
|
|
38
|
+
Keep looping until either (a) the call returns successfully (device paired), (b) the user says to stop, or (c) the user says they've paired (still call wait_for_device once more \u2014 it returns immediately if the device is already there). This loop gives the user windows between iterations to interrupt or change approach.`,inputSchema:{timeout_ms:s.number().optional().describe("How long to wait for a device to pair. Default 25000 (25s, intentionally under OTP TTL). Max 600000 (10 min).")}},async e=>{try{let t=Math.min(e.timeout_ms??25e3,6e5),r=await n.waitForDevice(t);return c(r)}catch(t){let r=t instanceof Error?t.message:String(t);return r.includes("No active session")?a("not_connected",r):r.includes("No device paired")?a("pairing_timeout",r):a("wait_failed",r)}}),i.registerTool("uplink_session_status",{description:"Return whether a session is active and whether a device is paired to it.",inputSchema:{}},()=>c(n.status())),i.registerTool("uplink_get_current_url",{description:"Returns the current URL of the device's active page.",inputSchema:{}},()=>u(n,e=>b(e))),i.registerTool("uplink_get_dom_snapshot",{description:"Snapshot the DOM of the active device. Returns the body tree (formatted for LLM consumption) plus head metadata (title, base, meta tags, link rels).",inputSchema:{}},()=>u(n,e=>T(e))),i.registerTool("uplink_evaluate_javascript",{description:"Run a JS expression in the device's page context and return the result. Can take either a single expression (`document.title`) or a statement block (`const r = await fetch(...); return r.json()`).",inputSchema:{expression:s.string().describe("JS expression or statement block to evaluate")}},e=>u(n,t=>R(t,e))),i.registerTool("uplink_await_network_request",{description:"Wait for the next network request whose URL contains `url_pattern` (substring match). Returns method, URL, status, headers, and a body preview.",inputSchema:{url_pattern:s.string().describe("Substring to match against the request URL"),timeout_ms:s.number().optional().describe("How long to wait. Default 15000, max 60000.")}},e=>u(n,t=>C(t,e))),i.registerTool("uplink_run_script",{description:"Execute a script against the device. The script body has `page` and `console` bound. Returns counts of captured flows + logs plus any error.",inputSchema:{code:s.string().describe("JS source to execute. `page` and `console` are bound in scope."),wait_for_idle_ms:s.number().optional().describe("Settling window after script resolves; default 3000, max 10000.")}},e=>u(n,t=>I(t,e.code,{wait_for_idle_ms:e.wait_for_idle_ms},{onLog:o(r=>{n.recorder.recordLog(r)},"onLog")}))),i.registerTool("uplink_read_logs",{description:"Return captured console logs from `uplink_run_script` invocations on the active session. Optional `level` filter and `limit`.",inputSchema:{level:s.enum(["log","info","warn","error","debug"]).optional().describe("Filter by log level."),limit:s.number().optional().describe("Most-recent N entries. Defaults to all retained.")}},e=>c(n.recorder.readLogs(e))),i.registerTool("uplink_search_network",{description:"Search captured network requests by URL substring. Returns matches with method, URL, status, headers, and body previews. Use uplink_get_network_request to fetch a single entry by id.",inputSchema:{url_pattern:s.string().describe("Substring to match against the request URL."),limit:s.number().optional().describe("Most-recent N matches.")}},e=>c(n.recorder.searchNetwork(e.url_pattern,{limit:e.limit}))),i.registerTool("uplink_get_network_request",{description:"Fetch a single captured network request by id (the id from uplink_search_network).",inputSchema:{id:s.number().describe("Network request id.")}},e=>{let t=n.recorder.getNetworkRequest(e.id);return t?c(t):a("not_found",`No network request with id ${e.id}.`)}),i.registerTool("uplink_list_commands",{description:"List captured page commands (method, args, result/error, timing) emitted while uplink_run_script executed. Optional `method` filter and `limit`.",inputSchema:{method:s.string().optional().describe('Filter by page method name (e.g., "goto", "click").'),limit:s.number().optional().describe("Most-recent N entries.")}},e=>c(n.recorder.listCommands(e))),i.registerTool("uplink_get_command_result",{description:"Fetch a single captured page command by id (the id from uplink_list_commands).",inputSchema:{id:s.number().describe("Command id.")}},e=>{let t=n.recorder.getCommandResult(e.id);return t?c(t):a("not_found",`No command with id ${e.id}.`)})}o(m,"registerTools");import{UPLINK_API_REFERENCE as U,UPLINK_LIFECYCLE_REFERENCE as N,buildSystemPrompt as E}from"@uplink-code/agent-tools";function g(i){i.registerResource("uplink-api-reference","uplink://api-reference",{title:"Uplink API Reference",description:"Methods available on the Uplink `page` and `browser` objects (the surface user scripts run against). Read this before writing or editing scripts so you don't guess at non-existent methods (no waitForTimeout, no locator, etc.).",mimeType:"text/markdown"},n=>Promise.resolve({contents:[{uri:n.toString(),mimeType:"text/markdown",text:U}]})),i.registerResource("uplink-lifecycle-reference","uplink://lifecycle-reference",{title:"Uplink SDK Lifecycle Reference",description:"Session / Client / ClientWorker / Browser bootstrap surface for standalone Uplink scripts. Read this BEFORE writing any standalone script so you don't invent methods like `client.waitForDevice()` or `session.refreshPairingCode()` that don't exist.",mimeType:"text/markdown"},n=>Promise.resolve({contents:[{uri:n.toString(),mimeType:"text/markdown",text:N}]})),i.registerResource("uplink-system-prompt","uplink://system-prompt",{title:"Uplink Scripting Conventions",description:"Conventions, idioms, and anti-patterns for writing Uplink automation scripts. Built into the prompt automatically by clients that consume MCP resources at session start.",mimeType:"text/markdown"},n=>Promise.resolve({contents:[{uri:n.toString(),mimeType:"text/markdown",text:E({})}]}))}o(g,"registerResources");import D from"@uplink-code/uplink";import{SessionRecorder as O,attachRecorder as L}from"@uplink-code/agent-tools";import{readCredentials as w,refreshAccessToken as M,resolveApiHost as f}from"@uplink-code/cli";function A(i){let n="/session/",e=i.lastIndexOf(n);if(e===-1)return null;let t=i.slice(e+n.length);return t.length>0?t:null}o(A,"extractJwt");var p=class{static{o(this,"SessionManager")}session=null;client=null;browser=null;page=null;recorder=new O;async connect(n={}){let e=w();if(!e)throw new Error("Not signed in. Run `uplink login` from @uplink-code/cli to authenticate.");let t=f(e),r=await y(t,e,n.projectId);if(r==="unauthorized"){e=await M();let d=await y(t,e,n.projectId);if(d==="unauthorized")throw new Error("Authorization failed even after refreshing. Run `uplink login` again.");r=d}this.session=r,this.client=await D.client.fromSession(r);let l=await _(t,e,r.sessionId);return{sessionId:r.sessionId,qrUrl:r.qrUrl,sessionUrl:r.sessionUrl,jwt:A(r.sessionUrl),pairingCode:l?.code??null,pairingCodeExpiresInSeconds:l?.ttlSeconds??null}}async getPage(n=12e4){if(this.page){if(!((this.client?.workers().length??0)>0))throw this.page=null,this.browser=null,new Error("Device disconnected. Have the user re-open the Uplink Connect app to re-pair (the session is still active), then call uplink_wait_for_device.");return this.page}if(!this.client)throw new Error("No active session. Call uplink_connect first to start a session and pair a device.");let e=await k(this.client.worker(),n,`No device paired within ${n}ms. Have the user scan the QR (qrUrl from uplink_connect).`);this.browser=await e.launch();let t=await this.browser.newPage();this.page=await L(t,this.recorder);try{await this.page.show()}catch{}return this.page}async waitForDevice(n=12e4){if(!this.client)throw new Error("No active session. Call uplink_connect first to start a session.");return await(await k(this.client.worker(),n,`No device paired within ${n}ms. Have the user open the pairing URL in the Uplink Connect app.`)).getDeviceInfo()}currentQrUrl(){return this.session?.qrUrl??null}async refreshPairingCode(){if(!this.session)throw new Error("No active session. Call uplink_connect first.");let n=w();if(!n)throw new Error("Not signed in. Run `uplink login`.");let e=await _(f(n),n,this.session.sessionId);if(!e)throw new Error("Failed to generate a new pairing code. Try uplink_connect again.");return e}status(){let n=(this.client?.workers().length??0)>0;return{connected:this.client!==null,sessionId:this.session?.sessionId??null,hasDevice:n||this.page!==null}}async disconnect(){try{await this.page?.close()}catch{}try{await this.browser?.close()}catch{}try{await this.client?.close()}catch{}this.page=null,this.browser=null,this.client=null,this.session=null}};function k(i,n,e){return Promise.race([i,new Promise((t,r)=>setTimeout(()=>{r(new Error(e))},n))])}o(k,"withTimeout");async function y(i,n,e){let t=await fetch(`${i}/sessions`,{method:"POST",headers:{authorization:`Bearer ${n.access_token}`,"content-type":"application/json"},body:JSON.stringify({projectId:e??null,organizationId:n.organization_id,include:{ecdh:!0,ecdsa:!0}})});if(t.status===401)return"unauthorized";if(!t.ok){let r=await t.text();throw new Error(`Failed to create session: ${t.status} ${t.statusText}. ${r}`)}return await t.json()}o(y,"postCreateSession");async function _(i,n,e){try{let t=await fetch(`${i}/sessions/${e}/codes`,{method:"POST",headers:{authorization:`Bearer ${n.access_token}`,"content-type":"application/json"}});if(!t.ok)return null;let r=await t.json();return{code:typeof r.code=="number"?String(r.code).padStart(6,"0"):r.code,ttlSeconds:r.ttlSeconds}}catch{return null}}o(_,"tryGeneratePairingCode");var $="@uplink-code/mcp",K="0.0.1",F="Uplink is a device-driven browser automation system for REVERSE-ENGINEERING the real web traffic of consumer websites \u2014 NOT for consuming official OAuth APIs.\n\nA user pairs a real device (phone or browser) via the Uplink Connect app. You then drive that device's page and observe the real network traffic, console output, and DOM that the site produces for a normal logged-in user. There is no concept of `client_id`, `client_secret`, redirect URIs, or \"API app registration\" in this workflow. DO NOT ask the user for OAuth app credentials or to set up an official API integration.\n\nTypical flow when the user wants to understand or automate site X:\n\n1. `uplink_connect` \u2192 returns a pairing URL; the user opens it on their device in the Uplink Connect app (or their own SDK).\n2. `uplink_wait_for_device` \u2192 block until pairing completes.\n3. `uplink_run_script` \u2192 drive the page (navigate, click, type, evaluate JS). When authentication is required, ask the user to log in interactively on their device \u2014 they're already at the real site.\n4. `uplink_search_network` / `uplink_get_network_request` / `uplink_read_logs` / `uplink_get_dom_snapshot` / `uplink_list_commands` \u2192 inspect what happened. This is how you learn the site's real API shape from observed traffic.\n\nThe goal is almost always to figure out how the site actually works from observed network/DOM, not to find a documented API.\n\n---\n\nWRITING SCRIPTS \u2014 ALWAYS assume the user is going to run the script OUTSIDE these tools (in their own project, with their own API key). Any time the user asks for \"a script\", \"code I can run\", \"an example\", or anything similar, produce a standalone SDK script \u2014 never code that depends on the MCP runtime. (The `uplink_run_script` tool is a separate thing: it takes a code string as an argument; \"writing a script for the user\" is different.)\n\nCRITICAL: the MCP tool names (`uplink_connect`, `uplink_wait_for_device`, `uplink_run_script`, `uplink_get_dom_snapshot`, `uplink_refresh_pairing_code`, etc.) ARE NOT SDK methods. They exist only inside this MCP server.\n\nThe SDK surface below is everything a script needs \u2014 you do NOT need to WebSearch, WebFetch, or read any resource to write a correct script. Just use what's here. This is the same shape as the quickstart at `https://docs.uplink.build/introduction/quickstart`.\n\nKeep scripts MINIMAL \u2014 match the quickstart. `client.launch()` already waits for a device to pair and launches a browser on it in one call. Do NOT add QR-code rendering libraries, pairing-code handling, pairing timeouts, or explicit `client.worker()` discovery loops UNLESS the user explicitly asks for that pairing UX. The default script just prints `session.qrUrl` and calls `client.launch()`.\n\nStandalone script skeleton (complete, authoritative shape \u2014 fill in the page automation in the middle):\n\n```ts\nimport uplink from '@uplink-code/uplink'\n// import ai from '@uplink-code/ai' // optional: DOM extraction / page.act / page.extract\n\nconst session = await uplink.session(process.env.UPLINK_API_KEY, {\n projectId: process.env.UPLINK_PROJECT_ID, // optional; null/omitted \u2192 Playground project\n include: { ecdsa: true, ecdh: true }\n})\nconst client = await uplink.client.fromSession(session)\n\n// session.qrUrl is the pairing link the USER opens in the Connect app.\n// (session.sessionUrl is the internal relay WebSocket URL \u2014 never show it to the user.)\nconsole.log('Pair a device by opening this URL in the Uplink Connect app:', session.qrUrl)\n\n// client.launch() waits for a device to pair, then launches a browser on it.\n// (It calls client.worker() internally \u2014 you don't need to call worker() yourself\n// unless you specifically need the device handle, e.g. worker.getDeviceInfo().)\nconst browser = await client.launch()\nconst page = await browser.newPage()\n\n// --- your automation (Page methods) ---\nawait page.goto('https://example.com')\nconst title = await page.evaluate(() => document.title)\n// also: page.click(sel), page.input(sel, text), page.waitForSelector(sel),\n// page.waitForRequest(matcher), page.cookies(url), page.screenshot(),\n// page.on('xhr', fn), page.act(instruction)/page.extract(...) (need agent)\n// DOM scrape: const { tree, headMetadata } = await ai.extractDomTree(page)\n\nawait page.close()\nawait browser.close()\nawait client.close()\n```\n\nSession fields (`session` is plain data: `{ sessionId, sessionUrl, qrUrl, keys?, credential? }`):\n- `session.qrUrl` \u2014 the PAIRING link the user opens in the Connect app. This is the only URL you ever show the user.\n- `session.sessionUrl` \u2014 the internal relay WebSocket URL (`wss://\u2026/session/<jwt>`) the SDK connects to. NEVER print this as the pairing link; it's not something the user opens.\n\nThese SDK members DO NOT EXIST \u2014 never write them (they're confusions with MCP tool names):\n- `session.pairingCode`, `session.pairingUrl`, `session.refreshPairingCode()`, `session.close()` \u2014 OTP pairing codes are an MCP-only feature; scripts use `session.qrUrl`.\n- `client.waitForDevice()`, `client.runScript()`, `client.getDomSnapshot()` \u2014 use `client.worker()` \u2192 `worker.launch()` \u2192 `browser.newPage()` \u2192 `page.*` instead.\n\nCommon mistakes to avoid:\n- `fromSession()` does NOT wait for a device \u2014 it connects to the relay and returns immediately. The call that BLOCKS until a device pairs is `client.worker()` (or `client.launch()`, which wraps it). Don't log \"device paired\" right after `fromSession()`; log it after `worker()`/`launch()` resolves.\n- DON'T hand-roll device discovery with `client.workers()` + a `worker-connected` listener. Workers are NOT auto-discovered on connect, so a manual listener can hang forever even when a device is paired. `client.worker()` is the only correct way \u2014 it does the discovery broadcast for you. (Wrap it in `Promise.race` if you want a timeout.)\n- `projectId` is OPTIONAL \u2014 omit it (or pass null) to use the Playground project. Don't make it a required env var unless the user specifically wants to target a non-default project.\n- The SDK is fully typed. DON'T annotate `client` / `worker` / `browser` / `page` as `any` \u2014 let TypeScript infer from `uplink.client.fromSession(session)` etc. Casting to `any` hides exactly the typos this section is warning about.\n\nFor deeper detail (full Page signatures, auth flows) the authoritative reference is the MCP resource `uplink://api-reference` / `uplink://lifecycle-reference`, or the docs at `https://docs.uplink.build` \u2014 but the skeleton above is enough for the common case; don't go looking unless you need a method not listed here.\n\nDO NOT use these in user scripts \u2014 they are MCP/console runtime internals, not part of the public SDK:\n- `@uplink-code/cli` (`readCredentials`, `refreshAccessToken`, `resolveApiHost`, \u2026).\n- `@uplink-code/agent-tools` (`SessionRecorder`, `attachRecorder`, handlers, \u2026).\n- Direct `fetch` against `/sessions` with a Bearer JWT \u2014 that's the MCP's auth path. Scripts call `uplink.session(apiKey, \u2026)` and let the SDK handle the `Authorization: ApiKey <\u2026>` header.";async function v(){let i=new q({name:$,version:K},{capabilities:{tools:{},resources:{}},instructions:F}),n=new p;m(i,n),g(i);let e=new j;await i.connect(e);let t=o(()=>{n.disconnect().finally(()=>process.exit(0))},"shutdown");process.on("SIGINT",t),process.on("SIGTERM",t)}o(v,"runServer");v().catch(i=>{process.stderr.write(`[uplink-mcp] fatal: ${i instanceof Error?i.message:String(i)}
|
|
3
39
|
`),process.exit(1)});
|
|
@@ -4,10 +4,14 @@ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
|
4
4
|
* (Claude Code, Cursor, etc.) can fetch these and include them in
|
|
5
5
|
* the model's context — no separate "skill install" step needed.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
* uplink://api-reference — the
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* Three resources:
|
|
8
|
+
* uplink://api-reference — the page/browser API surface (used both
|
|
9
|
+
* inside uplink_run_script and inside standalone scripts).
|
|
10
|
+
* uplink://lifecycle-reference — Session / Client / ClientWorker /
|
|
11
|
+
* Browser bootstrap, needed only by standalone scripts. Separate
|
|
12
|
+
* resource so console (which pre-binds these globals in the
|
|
13
|
+
* playground) doesn't pull it in via buildSystemPrompt.
|
|
14
|
+
* uplink://system-prompt — Uplink scripting conventions + idioms.
|
|
11
15
|
*/
|
|
12
16
|
export declare function registerResources(server: McpServer): void;
|
|
13
17
|
//# sourceMappingURL=resources.d.ts.map
|
package/lib/mcp/src/server.d.ts
CHANGED
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Boot the stdio MCP server.
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* v0.0.1 ships the scaffold + tool registration only; actual
|
|
7
|
-
* tool handlers wire up after agent-tools v0.2.0/v0.3.0 are
|
|
8
|
-
* published.
|
|
2
|
+
* Boot the stdio MCP server. Holds a single SessionManager for the
|
|
3
|
+
* process lifetime; tool calls dispatch through it to
|
|
4
|
+
* @uplink-code/agent-tools handlers running against the active Page.
|
|
9
5
|
*/
|
|
10
6
|
export declare function runServer(): Promise<void>;
|
|
11
7
|
//# sourceMappingURL=server.d.ts.map
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { Page } from '@uplink-code/uplink';
|
|
2
|
+
import { SessionRecorder } from '@uplink-code/agent-tools';
|
|
3
|
+
export interface ConnectResult {
|
|
4
|
+
sessionId: string;
|
|
5
|
+
/**
|
|
6
|
+
* Pairing URL the user opens in the Uplink Connect app. Format:
|
|
7
|
+
* `${API_HOST}/link/s/${sessionId}`. Also what the QR encodes. Named
|
|
8
|
+
* to match the `qrUrl` field on the `/sessions` API response + SDK
|
|
9
|
+
* `Session`.
|
|
10
|
+
*/
|
|
11
|
+
qrUrl: string;
|
|
12
|
+
/**
|
|
13
|
+
* WebSocket URL the relay-side SDK connects to. Format:
|
|
14
|
+
* `${RELAY_WS_HOST}/session/${jwt}`. Not what the end user opens.
|
|
15
|
+
* Named to match the `sessionUrl` field on the API response + SDK
|
|
16
|
+
* `Session`.
|
|
17
|
+
*/
|
|
18
|
+
sessionUrl: string;
|
|
19
|
+
/** Session JWT extracted from `sessionUrl` for SDKs that consume it standalone. */
|
|
20
|
+
jwt: string | null;
|
|
21
|
+
/**
|
|
22
|
+
* 6-digit one-time pairing code the user types into the Uplink
|
|
23
|
+
* Connect app. Primary pairing UX — easier than scanning a QR or
|
|
24
|
+
* pasting a URL. `null` if code generation failed (the URL still
|
|
25
|
+
* works as a fallback).
|
|
26
|
+
*/
|
|
27
|
+
pairingCode: string | null;
|
|
28
|
+
/** Seconds until the code expires. Typically 30. */
|
|
29
|
+
pairingCodeExpiresInSeconds: number | null;
|
|
30
|
+
}
|
|
31
|
+
export interface SessionStatus {
|
|
32
|
+
connected: boolean;
|
|
33
|
+
sessionId: string | null;
|
|
34
|
+
hasDevice: boolean;
|
|
35
|
+
}
|
|
36
|
+
export interface DeviceInfo {
|
|
37
|
+
deviceModel?: string;
|
|
38
|
+
platform?: string;
|
|
39
|
+
platformVersion?: string;
|
|
40
|
+
deviceType?: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Per-process session state for the MCP server. Holds the active
|
|
44
|
+
* Client + Browser + Page so each tool call can call into agent-tools
|
|
45
|
+
* handlers against a real device.
|
|
46
|
+
*
|
|
47
|
+
* Lifecycle:
|
|
48
|
+
* 1. `connect()` — creates a session via the Uplink API, returns
|
|
49
|
+
* QR URL + session URL so the model can prompt the user to
|
|
50
|
+
* pair a device.
|
|
51
|
+
* 2. `getPage()` — waits up to `pageTimeoutMs` for a worker to
|
|
52
|
+
* register (the device scans the QR) and a page to be opened,
|
|
53
|
+
* then returns it. Subsequent tool calls reuse the same page.
|
|
54
|
+
* 3. `disconnect()` — closes the page/browser/client.
|
|
55
|
+
*/
|
|
56
|
+
export declare class SessionManager {
|
|
57
|
+
private session;
|
|
58
|
+
private client;
|
|
59
|
+
private browser;
|
|
60
|
+
private page;
|
|
61
|
+
/**
|
|
62
|
+
* Per-session capture of logs, network, and commands. Lives for the
|
|
63
|
+
* whole session lifetime; `attachRecorder` wires it to the page on
|
|
64
|
+
* first `getPage()`. Backs the 5 MCP query tools (`read_logs`,
|
|
65
|
+
* `search_network`, etc.).
|
|
66
|
+
*/
|
|
67
|
+
readonly recorder: SessionRecorder;
|
|
68
|
+
/**
|
|
69
|
+
* Create a new session against the Uplink API and connect the
|
|
70
|
+
* client. After this resolves, the user should scan the returned
|
|
71
|
+
* QR with their device — `getPage()` will block until they do.
|
|
72
|
+
*
|
|
73
|
+
* Auth: reads ~/.config/uplink/credentials.json (written by
|
|
74
|
+
* `uplink login`). Bypasses the SDK's `uplink.session()` because
|
|
75
|
+
* that hardcodes `Authorization: ApiKey <…>` — we need
|
|
76
|
+
* `Bearer <jwt>`. Console does the same dance for the same reason.
|
|
77
|
+
* On 401, refreshes the access token once and retries.
|
|
78
|
+
*/
|
|
79
|
+
connect(opts?: {
|
|
80
|
+
projectId?: string;
|
|
81
|
+
}): Promise<ConnectResult>;
|
|
82
|
+
/**
|
|
83
|
+
* Returns the active page, waiting up to `pageTimeoutMs` for a
|
|
84
|
+
* worker to register if one isn't connected yet. Lazy-initializes
|
|
85
|
+
* the browser + page on first call.
|
|
86
|
+
*
|
|
87
|
+
* Throws a structured error if no session has been started or the
|
|
88
|
+
* device doesn't pair in time.
|
|
89
|
+
*/
|
|
90
|
+
getPage(pageTimeoutMs?: number): Promise<Page>;
|
|
91
|
+
/**
|
|
92
|
+
* Block until a worker (device) registers on the active client, then
|
|
93
|
+
* return its metadata. Resolves immediately if a worker is already
|
|
94
|
+
* connected. Throws on timeout or if no session has been started.
|
|
95
|
+
*/
|
|
96
|
+
waitForDevice(timeoutMs?: number): Promise<DeviceInfo>;
|
|
97
|
+
/**
|
|
98
|
+
* Pairing URL (`qrUrl`) of the active session, or null if no session
|
|
99
|
+
* is active. Used by uplink_get_qr to render an opt-in QR after the
|
|
100
|
+
* model is asked for one.
|
|
101
|
+
*/
|
|
102
|
+
currentQrUrl(): string | null;
|
|
103
|
+
/**
|
|
104
|
+
* Mint a fresh 6-digit OTP for the active session. Used when the
|
|
105
|
+
* previous code expired (30s TTL) before the user typed it. Throws
|
|
106
|
+
* if no session exists or the codes endpoint refuses.
|
|
107
|
+
*/
|
|
108
|
+
refreshPairingCode(): Promise<{
|
|
109
|
+
code: string;
|
|
110
|
+
ttlSeconds: number;
|
|
111
|
+
}>;
|
|
112
|
+
status(): SessionStatus;
|
|
113
|
+
disconnect(): Promise<void>;
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=session.d.ts.map
|
package/lib/mcp/src/tools.d.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import type { SessionManager } from './session.ts';
|
|
2
3
|
/**
|
|
3
4
|
* Register Uplink's agent tools on the MCP server. Tool names are
|
|
4
|
-
* namespaced `uplink_*` so they don't collide with other MCP servers
|
|
5
|
-
* a user might have installed (filesystem, github, etc.).
|
|
5
|
+
* namespaced `uplink_*` so they don't collide with other MCP servers.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* Page-driven tools dispatch to agent-tools handlers against the
|
|
8
|
+
* recorder-wrapped Page from `SessionManager.getPage()`. Session-scoped
|
|
9
|
+
* tools (read_logs, search_network, get_network_request, list_commands,
|
|
10
|
+
* get_command_result) read from `session.recorder`, which captures
|
|
11
|
+
* logs/network/commands across the session's lifetime.
|
|
10
12
|
*/
|
|
11
|
-
export declare function registerTools(server: McpServer): void;
|
|
13
|
+
export declare function registerTools(server: McpServer, session: SessionManager): void;
|
|
12
14
|
//# sourceMappingURL=tools.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uplink-code/mcp",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
],
|
|
26
26
|
"scripts": {
|
|
27
27
|
"build": "npm run build:declarations && npm run build:transpile",
|
|
28
|
-
"build:transpile": "esbuild src/index.ts --bundle --keep-names --minify --format=esm --platform=node --external:@uplink-code/uplink --external:@uplink-code/ai --external:@uplink-code/agent-tools --external:@modelcontextprotocol/sdk --outfile=./lib/mcp/index.js",
|
|
28
|
+
"build:transpile": "esbuild src/index.ts --bundle --keep-names --minify --format=esm --platform=node --external:@uplink-code/uplink --external:@uplink-code/ai --external:@uplink-code/agent-tools --external:@uplink-code/cli --external:@modelcontextprotocol/sdk --external:zod --external:qrcode-terminal --outfile=./lib/mcp/index.js",
|
|
29
29
|
"build:declarations": "tsc -b",
|
|
30
30
|
"clean": "rm -rf lib",
|
|
31
31
|
"format:check": "prettier --check src/ package.json tsconfig.json",
|
|
@@ -34,14 +34,6 @@
|
|
|
34
34
|
"lint:fix": "eslint --fix src/",
|
|
35
35
|
"prepare": "npm run build"
|
|
36
36
|
},
|
|
37
|
-
"devDependencies": {
|
|
38
|
-
"@types/node": "^24.3.1",
|
|
39
|
-
"esbuild": "^0.25.9",
|
|
40
|
-
"eslint-config-prettier": "^10.1.8",
|
|
41
|
-
"prettier": "^3.6.2",
|
|
42
|
-
"typescript": "^5.9.2",
|
|
43
|
-
"typescript-eslint": "^8.56.0"
|
|
44
|
-
},
|
|
45
37
|
"keywords": [
|
|
46
38
|
"uplink",
|
|
47
39
|
"mcp",
|
|
@@ -55,8 +47,20 @@
|
|
|
55
47
|
"description": "Model Context Protocol server for Uplink — exposes Uplink's browser automation tools to any MCP-capable AI client (Claude Code, Cursor, etc.)",
|
|
56
48
|
"dependencies": {
|
|
57
49
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
58
|
-
"@uplink-code/agent-tools": "^0.
|
|
50
|
+
"@uplink-code/agent-tools": "^0.3.0",
|
|
59
51
|
"@uplink-code/ai": "^0.3.0",
|
|
60
|
-
"@uplink-code/
|
|
52
|
+
"@uplink-code/cli": "^0.1.0",
|
|
53
|
+
"@uplink-code/uplink": ">=0.9.0 <1.0.0",
|
|
54
|
+
"qrcode-terminal": "^0.12.0",
|
|
55
|
+
"zod": "^4.1.12"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@types/node": "^24.3.1",
|
|
59
|
+
"@types/qrcode-terminal": "^0.12.2",
|
|
60
|
+
"esbuild": "^0.25.9",
|
|
61
|
+
"eslint-config-prettier": "^10.1.8",
|
|
62
|
+
"prettier": "^3.6.2",
|
|
63
|
+
"typescript": "^5.9.2",
|
|
64
|
+
"typescript-eslint": "^8.56.0"
|
|
61
65
|
}
|
|
62
66
|
}
|