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
75 changes: 75 additions & 0 deletions .github/workflows/release-codex.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
name: Release codex package

on:
push:
tags:
- 'codex-v*'
workflow_dispatch:
inputs:
publish:
description: Publish to npm (unchecked = dry run only)
type: boolean
required: false
default: false

permissions:
contents: read
id-token: write

concurrency:
group: release-codex
cancel-in-progress: false

jobs:
release:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 22
registry-url: https://registry.npmjs.org

- name: Read package metadata
id: pkg
run: |
name=$(node -p "require('./codex/package.json').name")
version=$(node -p "require('./codex/package.json').version")
echo "name=$name" >> "$GITHUB_OUTPUT"
echo "version=$version" >> "$GITHUB_OUTPUT"

- name: Validate release tag matches package version
if: github.event_name == 'push'
run: |
expected="codex-v${{ steps.pkg.outputs.version }}"
if [ "${GITHUB_REF_NAME}" != "$expected" ]; then
echo "::error title=Tag/version mismatch::Expected tag $expected for version ${{ steps.pkg.outputs.version }}, got ${GITHUB_REF_NAME}."
exit 1
fi

- name: Ensure npm version is not already published
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.publish)
run: |
if npm view "${{ steps.pkg.outputs.name }}@${{ steps.pkg.outputs.version }}" version >/dev/null 2>&1; then
echo "::error title=Version already published::${{ steps.pkg.outputs.name }}@${{ steps.pkg.outputs.version }} already exists on npm."
exit 1
fi

- name: Validate package contents
working-directory: codex
run: npm pack --dry-run

- name: Publish to npm
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.publish)
working-directory: codex
run: npm publish --access public --provenance
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

- name: Dry-run summary
if: github.event_name == 'workflow_dispatch' && !inputs.publish
run: |
echo "Dry run complete for ${{ steps.pkg.outputs.name }}@${{ steps.pkg.outputs.version }}."
echo "Re-run this workflow with publish=true to publish."
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

[![Integration](https://github.com/modem-dev/glance-agent-plugins/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/modem-dev/glance-agent-plugins/actions/workflows/test.yml)
[![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
[![Supported Agents](https://img.shields.io/badge/agents-pi%20%7C%20OpenCode%20%7C%20Claude%20Code-blue)](#available-plugins)
[![Supported Agents](https://img.shields.io/badge/agents-pi%20%7C%20OpenCode%20%7C%20Claude%20Code%20%7C%20Codex-blue)](#available-plugins)

Agent integrations for [glance.sh](https://glance.sh) — temporary image sharing for coding agents.

Expand All @@ -15,6 +15,7 @@ Paste a screenshot in your browser, your agent gets the URL instantly.
| [pi](https://github.com/mariozechner/pi) | [`pi/`](pi/) | `@modemdev/glance-pi` | `pi install npm:@modemdev/glance-pi` |
| [OpenCode](https://github.com/anomalyco/opencode) | [`opencode/`](opencode/) | `@modemdev/glance-opencode` | Add `"@modemdev/glance-opencode"` to `opencode.json` `plugin` list |
| [Claude Code](https://github.com/anthropics/claude-code) | [`claude/`](claude/) | `@modemdev/glance-claude` | `/plugin marketplace add modem-dev/glance-agent-plugins` then `/plugin install glance-claude@glance-agent-plugins` |
| [Codex](https://developers.openai.com/codex) | [`codex/`](codex/) | `@modemdev/glance-codex` | `codex mcp add glance -- npx -y @modemdev/glance-codex` |

## How it works

Expand Down
5 changes: 5 additions & 0 deletions codex/.codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "glance-codex",
"description": "glance.sh MCP tools for Codex",
"mcpServers": "./.mcp.json"
}
9 changes: 9 additions & 0 deletions codex/.mcp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"mcpServers": {
"glance": {
"command": "node",
"args": ["servers/glance-mcp.js"],
"cwd": "."
}
}
}
104 changes: 104 additions & 0 deletions codex/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# glance.sh plugin for Codex

[Codex CLI](https://developers.openai.com/codex) integration that adds glance.sh screenshot tools via MCP.

## What it does

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.

## Install

Recommended (npm package):

```bash
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.0
```

Local development / manual install:

```bash
codex mcp add glance -- node /absolute/path/to/glance-agent-plugins/codex/servers/glance-mcp.js
```

## Verify

1. Confirm the MCP server is configured:

```bash
codex mcp list
codex mcp get glance --json
```

2. Ask Codex to call `glance`.
3. Open the returned `https://glance.sh/s/<id>` URL and paste an image.
4. Ask Codex to call `glance_wait`.
5. Confirm Codex receives `Screenshot: <url>`.

## Update / remove

Update:

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

Remove:

```bash
codex mcp remove glance
```

## Publishing (maintainers)

Releases are automated via GitHub Actions.

Prerequisite: configure `NPM_TOKEN` in the `glance-agent-plugins` repository with publish access to `@modemdev/glance-codex`.

1. Bump `version` in `codex/package.json`.
2. Commit and push to `main`.
3. Create and push a matching tag:

```bash
git tag codex-v0.1.0
git push origin codex-v0.1.0
```

The `Release codex package` workflow validates tag/version alignment, checks for already-published versions, runs `npm pack --dry-run`, and publishes with npm provenance.

## How it works

```text
Codex calls glance
└─▶ MCP server POST /api/session
└─▶ returns session URL

Codex calls glance_wait
└─▶ waits for SSE image event

User pastes image at /s/<id>
└─▶ glance.sh emits image event
└─▶ tool returns Screenshot: <url>
```

## Requirements

- Codex CLI with MCP support
- Node.js runtime available to launch the stdio MCP server

## Configuration

Optional environment variable:

- `GLANCE_BASE_URL` (default: `https://glance.sh`)
131 changes: 131 additions & 0 deletions codex/glance.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { readFileSync } from "node:fs"
import { fileURLToPath } from "node:url"

import { afterEach, describe, expect, it, vi } from "vitest"

import { createCodexMcpServer, createGlanceRuntime } from "./servers/glance-mcp.js"

type ToolResult = {
content: Array<{ type: string; text: string }>
}

function jsonResponse(body: unknown, status = 200): Response {
return new Response(JSON.stringify(body), {
headers: { "content-type": "application/json" },
status,
})
}

function pendingSseResponse(signal?: AbortSignal): Promise<Response> {
return new Promise((_resolve, reject) => {
signal?.addEventListener(
"abort",
() => {
reject(new DOMException("Aborted", "AbortError"))
},
{ once: true },
)
})
}

describe("codex glance plugin", () => {
afterEach(() => {
vi.restoreAllMocks()
})

it("ships codex plugin manifest, MCP config, and npm package metadata", () => {
const pluginJsonPath = fileURLToPath(new URL("./.codex-plugin/plugin.json", import.meta.url))
const mcpJsonPath = fileURLToPath(new URL("./.mcp.json", import.meta.url))
const packageJsonPath = fileURLToPath(new URL("./package.json", import.meta.url))

const manifest = JSON.parse(readFileSync(pluginJsonPath, "utf8")) as Record<string, unknown>
const mcpConfig = JSON.parse(readFileSync(mcpJsonPath, "utf8")) as {
mcpServers?: Record<string, Record<string, unknown>>
}
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8")) as {
name?: string
bin?: Record<string, string>
main?: string
}

expect(manifest.name).toBe("glance-codex")
expect(manifest.mcpServers).toBe("./.mcp.json")

expect(mcpConfig.mcpServers?.glance?.command).toBe("node")
expect(mcpConfig.mcpServers?.glance?.args).toEqual(["servers/glance-mcp.js"])
expect(mcpConfig.mcpServers?.glance?.cwd).toBe(".")

expect(packageJson.name).toBe("@modemdev/glance-codex")
expect(packageJson.main).toBe("servers/glance-mcp.js")
expect(packageJson.bin?.["glance-codex"]).toBe("servers/glance-mcp.js")
})

it("returns a session URL from glance", async () => {
const fetchMock = vi.fn((input: RequestInfo | URL, init?: RequestInit) => {
const url = String(input)

if (url === "https://glance.sh/api/session") {
return Promise.resolve(jsonResponse({ id: "sess-1", url: "/s/sess-1" }))
}

if (url === "https://glance.sh/api/session/sess-1/events") {
return pendingSseResponse(init?.signal as AbortSignal | undefined)
}

throw new Error(`Unexpected fetch URL: ${url}`)
})

const runtime = createGlanceRuntime({ fetchImpl: fetchMock, quietLogs: true })

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

expect(result.content[0].text).toContain("Session ready")
expect(result.content[0].text).toContain("https://glance.sh/s/sess-1")

runtime.stopBackground()
})

it("creates an MCP server that routes JSON-RPC tool calls", async () => {
const runtime = {
getTools: vi.fn(() => [
{
name: "glance",
description: "glance tool",
inputSchema: { type: "object", properties: {}, additionalProperties: false },
},
]),
executeTool: vi.fn(async () => ({
content: [{ type: "text", text: "Session ready" }],
})),
stopBackground: vi.fn(),
}

const sent: Array<Record<string, any>> = []

const server = createCodexMcpServer({
runtime,
serverOptions: {
sendMessage: (message: Record<string, any>) => {
sent.push(message)
},
quietLogs: true,
},
})

await server.handleMessage({
jsonrpc: "2.0",
id: 1,
method: "tools/call",
params: { name: "glance", arguments: {} },
})

expect(runtime.executeTool).toHaveBeenCalledWith("glance", {}, expect.any(AbortSignal))
expect(sent).toContainEqual({
jsonrpc: "2.0",
id: 1,
result: {
content: [{ type: "text", text: "Session ready" }],
},
})
})
})
34 changes: 34 additions & 0 deletions codex/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"name": "@modemdev/glance-codex",
"version": "0.1.0",
"description": "glance.sh MCP server package for Codex",
"license": "MIT",
"type": "module",
"repository": {
"type": "git",
"url": "git+https://github.com/modem-dev/glance-agent-plugins.git",
"directory": "codex"
},
"homepage": "https://github.com/modem-dev/glance-agent-plugins/tree/main/codex",
"bugs": {
"url": "https://github.com/modem-dev/glance-agent-plugins/issues"
},
"keywords": [
"codex",
"mcp",
"glance",
"screenshot",
"agent"
],
"files": [
".codex-plugin",
".mcp.json",
"servers/glance-mcp.js",
"servers/glance-mcp.d.ts",
"README.md"
],
"main": "servers/glance-mcp.js",
"bin": {
"glance-codex": "servers/glance-mcp.js"
}
}
Loading