Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion claude/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "glance-claude",
"version": "0.1.0",
"version": "0.1.1",
"description": "glance.sh MCP tools for Claude Code",
"author": {
"name": "Modem"
Expand Down
8 changes: 5 additions & 3 deletions claude/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Adds two MCP tools:
- **`glance`** — creates/reuses a live session and returns a URL like `https://glance.sh/s/<id>`
- **`glance_wait`** — waits for the next pasted image and returns `Screenshot: https://glance.sh/<token>.<ext>`

The server keeps a background SSE listener alive, reconnects automatically, and refreshes sessions before they expire.
The server opens an SSE listener on demand and stops after one image, timeout, expiry, cancellation, or a small number of transient retries.

## Install

Expand Down Expand Up @@ -64,8 +64,8 @@ Prerequisite: configure `NPM_TOKEN` in the `glance-agent-plugins` repository wit
3. Create and push a matching tag:

```bash
git tag claude-v0.1.0
git push origin claude-v0.1.0
git tag claude-v0.1.1
git push origin claude-v0.1.1
```

The `Release claude package` workflow validates tag/version alignment, checks for already-published versions, runs `npm pack --dry-run`, and publishes with npm provenance.
Expand All @@ -75,6 +75,7 @@ The `Release claude package` workflow validates tag/version alignment, checks fo
```text
Claude calls glance
└─▶ MCP server POST /api/session
└─▶ connects SSE for one wait window
└─▶ returns session URL

Claude calls glance_wait
Expand All @@ -83,6 +84,7 @@ Claude calls glance_wait
User pastes image at /s/<id>
└─▶ glance.sh emits image event
└─▶ tool returns Screenshot: <url>
└─▶ listener stops
```

## Requirements
Expand Down
28 changes: 24 additions & 4 deletions claude/common/mcp/glance-mcp.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@ import { realpathSync } from "node:fs"
import { fileURLToPath, pathToFileURL } from "node:url"

const DEFAULT_BASE_URL = process.env.GLANCE_BASE_URL?.trim() || "https://glance.sh"
const USER_AGENT = "glance-mcp/0.1.2"

/** How long to wait on one SSE connection before reconnecting. */
const SSE_TIMEOUT_MS = 305_000

/** Pause between reconnect attempts on transient errors. */
const RECONNECT_DELAY_MS = 3_000

/** Maximum transient SSE retries for one user-triggered wait window. */
const MAX_RECONNECT_ATTEMPTS = 3

/** How often to mint a fresh session (sessions have 10-minute TTL). */
const SESSION_REFRESH_MS = 8 * 60 * 1000

Expand Down Expand Up @@ -106,7 +110,10 @@ export function createGlanceRuntime(options = {}) {
}

async function createSession() {
const res = await fetchImpl(`${baseUrl}/api/session`, { method: "POST" })
const res = await fetchImpl(`${baseUrl}/api/session`, {
method: "POST",
headers: { "User-Agent": USER_AGENT },
})
if (!res.ok) {
throw new Error(`HTTP ${res.status}`)
}
Expand Down Expand Up @@ -154,7 +161,10 @@ export function createGlanceRuntime(options = {}) {
async function listenForImages(sessionId, signal) {
const res = await fetchImpl(`${baseUrl}/api/session/${sessionId}/events`, {
signal,
headers: { Accept: "text/event-stream" },
headers: {
Accept: "text/event-stream",
"User-Agent": USER_AGENT,
},
})

if (!res.ok || !res.body) {
Expand Down Expand Up @@ -214,6 +224,7 @@ export function createGlanceRuntime(options = {}) {
typeof image.expiresAt === "number"
) {
dispatchToWaiters(image)
return
}
} catch {
log("Failed to parse image event payload")
Expand All @@ -240,13 +251,22 @@ export function createGlanceRuntime(options = {}) {
}

async function backgroundLoop(signal) {
let reconnectAttempts = 0

while (!signal.aborted) {
try {
const session = await ensureSession()
await listenForImages(session.id, signal)

// Stop after one active wait window (image, timeout, or expiry). Idle
// agents must not keep refreshing sessions indefinitely.
break
} catch (err) {
if (signal.aborted) break
await sleep(RECONNECT_DELAY_MS, signal)
reconnectAttempts += 1
if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) break

await sleep(RECONNECT_DELAY_MS * 2 ** (reconnectAttempts - 1), signal)
}
}

Expand Down Expand Up @@ -475,7 +495,7 @@ export function createMcpServer(options = {}) {
protocolVersion: params?.protocolVersion ?? "2024-11-05",
serverInfo: {
name: "glance-sh",
version: "0.1.0",
version: "0.1.2",
},
capabilities: {
tools: {
Expand Down
2 changes: 1 addition & 1 deletion claude/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@modemdev/glance-claude",
"version": "0.1.0",
"version": "0.1.1",
"description": "glance.sh plugin package for Claude Code",
"license": "MIT",
"type": "module",
Expand Down
28 changes: 21 additions & 7 deletions claude/servers/glance-mcp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,9 +155,20 @@ describe("claude glance runtime", () => {

expect(result.content[0].text).toContain("Session ready")
expect(result.content[0].text).toContain("https://glance.sh/s/sess-1")
expect(fetchMock).toHaveBeenCalledWith("https://glance.sh/api/session", {
expect(fetchMock).toHaveBeenNthCalledWith(1, "https://glance.sh/api/session", {
method: "POST",
headers: { "User-Agent": "glance-mcp/0.1.2" },
})
expect(fetchMock).toHaveBeenNthCalledWith(
2,
"https://glance.sh/api/session/sess-1/events",
expect.objectContaining({
headers: {
Accept: "text/event-stream",
"User-Agent": "glance-mcp/0.1.2",
},
}),
)
})

it("reuses active sessions and refreshes stale sessions", async () => {
Expand Down Expand Up @@ -236,7 +247,7 @@ describe("claude glance runtime", () => {
await sse.close()
})

it("handles session expiry by rotating to a fresh session", async () => {
it("handles session expiry without reconnecting until the next tool call", async () => {
let sessionCalls = 0

const fetchMock = vi.fn((input: string | URL, init?: RequestInit) => {
Expand Down Expand Up @@ -268,13 +279,16 @@ describe("claude glance runtime", () => {
await runtime.executeTool("glance")

await vi.waitFor(() => {
expect(sessionCalls).toBe(2)
expect(runtime.getState().running).toBe(false)
})

expect(runtime.getState().currentSession).toEqual({
id: "sess-2",
url: "https://glance.sh/s/sess-2",
})
expect(sessionCalls).toBe(1)
expect(runtime.getState().currentSession).toBeNull()

const refreshed = (await runtime.executeTool("glance")) as ToolResult

expect(sessionCalls).toBe(2)
expect(refreshed.content[0].text).toContain("https://glance.sh/s/sess-2")
})

it("returns a helpful error when glance_wait is called before glance", async () => {
Expand Down
10 changes: 6 additions & 4 deletions codex/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Adds two MCP tools:
- **`glance`** — creates/reuses a live session and returns a URL like `https://glance.sh/s/<id>`
- **`glance_wait`** — waits for the next pasted image and returns `Screenshot: https://glance.sh/<token>.<ext>`

The server keeps a background SSE listener alive, reconnects automatically, and refreshes sessions before they expire.
The server opens an SSE listener on demand and stops after one image, timeout, expiry, cancellation, or a small number of transient retries.

## Install

Expand All @@ -22,7 +22,7 @@ codex mcp add glance -- npx -y @modemdev/glance-codex
Optional: pin a specific version:

```bash
codex mcp add glance -- npx -y @modemdev/glance-codex@0.1.1
codex mcp add glance -- npx -y @modemdev/glance-codex@0.1.2
```

Local development / manual install:
Expand Down Expand Up @@ -71,8 +71,8 @@ Prerequisite: configure `NPM_TOKEN` in the `glance-agent-plugins` repository wit
3. Create and push a matching tag:

```bash
git tag codex-v0.1.1
git push origin codex-v0.1.1
git tag codex-v0.1.2
git push origin codex-v0.1.2
```

The `Release codex package` workflow validates tag/version alignment, checks for already-published versions, runs `npm pack --dry-run`, and publishes with npm provenance.
Expand All @@ -82,6 +82,7 @@ The `Release codex package` workflow validates tag/version alignment, checks for
```text
Codex calls glance
└─▶ MCP server POST /api/session
└─▶ connects SSE for one wait window
└─▶ returns session URL

Codex calls glance_wait
Expand All @@ -90,6 +91,7 @@ Codex calls glance_wait
User pastes image at /s/<id>
└─▶ glance.sh emits image event
└─▶ tool returns Screenshot: <url>
└─▶ listener stops
```

## Requirements
Expand Down
28 changes: 24 additions & 4 deletions codex/common/mcp/glance-mcp.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@ import { realpathSync } from "node:fs"
import { fileURLToPath, pathToFileURL } from "node:url"

const DEFAULT_BASE_URL = process.env.GLANCE_BASE_URL?.trim() || "https://glance.sh"
const USER_AGENT = "glance-mcp/0.1.2"

/** How long to wait on one SSE connection before reconnecting. */
const SSE_TIMEOUT_MS = 305_000

/** Pause between reconnect attempts on transient errors. */
const RECONNECT_DELAY_MS = 3_000

/** Maximum transient SSE retries for one user-triggered wait window. */
const MAX_RECONNECT_ATTEMPTS = 3

/** How often to mint a fresh session (sessions have 10-minute TTL). */
const SESSION_REFRESH_MS = 8 * 60 * 1000

Expand Down Expand Up @@ -106,7 +110,10 @@ export function createGlanceRuntime(options = {}) {
}

async function createSession() {
const res = await fetchImpl(`${baseUrl}/api/session`, { method: "POST" })
const res = await fetchImpl(`${baseUrl}/api/session`, {
method: "POST",
headers: { "User-Agent": USER_AGENT },
})
if (!res.ok) {
throw new Error(`HTTP ${res.status}`)
}
Expand Down Expand Up @@ -154,7 +161,10 @@ export function createGlanceRuntime(options = {}) {
async function listenForImages(sessionId, signal) {
const res = await fetchImpl(`${baseUrl}/api/session/${sessionId}/events`, {
signal,
headers: { Accept: "text/event-stream" },
headers: {
Accept: "text/event-stream",
"User-Agent": USER_AGENT,
},
})

if (!res.ok || !res.body) {
Expand Down Expand Up @@ -214,6 +224,7 @@ export function createGlanceRuntime(options = {}) {
typeof image.expiresAt === "number"
) {
dispatchToWaiters(image)
return
}
} catch {
log("Failed to parse image event payload")
Expand All @@ -240,13 +251,22 @@ export function createGlanceRuntime(options = {}) {
}

async function backgroundLoop(signal) {
let reconnectAttempts = 0

while (!signal.aborted) {
try {
const session = await ensureSession()
await listenForImages(session.id, signal)

// Stop after one active wait window (image, timeout, or expiry). Idle
// agents must not keep refreshing sessions indefinitely.
break
} catch (err) {
if (signal.aborted) break
await sleep(RECONNECT_DELAY_MS, signal)
reconnectAttempts += 1
if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) break

await sleep(RECONNECT_DELAY_MS * 2 ** (reconnectAttempts - 1), signal)
}
}

Expand Down Expand Up @@ -475,7 +495,7 @@ export function createMcpServer(options = {}) {
protocolVersion: params?.protocolVersion ?? "2024-11-05",
serverInfo: {
name: "glance-sh",
version: "0.1.0",
version: "0.1.2",
},
capabilities: {
tools: {
Expand Down
2 changes: 1 addition & 1 deletion codex/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@modemdev/glance-codex",
"version": "0.1.1",
"version": "0.1.2",
"description": "glance.sh MCP server package for Codex",
"license": "MIT",
"type": "module",
Expand Down
Loading
Loading