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
5 changes: 5 additions & 0 deletions .changeset/mcp-tool-annotations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@buildinternet/uploads": patch
---

Advertise safety hints and OAuth security schemes on every MCP tool, and return a www-authenticate challenge when a token is missing the required scope.
6 changes: 3 additions & 3 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,23 @@
{
"name": "uploads",
"source": "./",
"description": "Get screenshots, GIFs, recordings, and files into GitHub PRs and issues via uploads.sh. Bundles the github-screenshots, annotate-screenshots, and uploads-cli skills, a local MCP server, and the /uploads:attach command.",
"description": "Get screenshots, GIFs, recordings, and files into GitHub PRs and issues via uploads.sh. Bundles the github-screenshots, annotate-screenshots, and uploads-cli skills, the hosted MCP server, and the /uploads:attach command.",
"version": "0.2.0",
"author": {
"name": "Build Internet"
},
"homepage": "https://uploads.sh",
"repository": "https://github.com/buildinternet/uploads",
"license": "Apache-2.0",
"icon": "./assets/logo.png",
"keywords": ["uploads", "screenshots", "github", "file-hosting", "images", "mcp"],
"strict": false,
"skills": [
"./skills/github-screenshots",
"./skills/annotate-screenshots",
"./skills/uploads-cli"
],
"commands": "./plugins/claude/uploads/commands",
"mcpServers": "./plugins/claude/uploads/.mcp.json",
"mcpServers": "./.mcp.json",
"hooks": "./hooks/hooks.json"
}
]
Expand Down
18 changes: 18 additions & 0 deletions .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "uploads",
"version": "0.2.0",
"description": "Host screenshots, GIFs, recordings, and files on uploads.sh and embed them in GitHub PRs and issues.",
"author": {
"name": "Build Internet",
"url": "https://uploads.sh"
},
"homepage": "https://uploads.sh",
"repository": "https://github.com/buildinternet/uploads",
"license": "Apache-2.0",
"icon": "./assets/logo.png",
"keywords": ["uploads", "screenshots", "github", "file-hosting", "images", "mcp"],
"skills": "./skills/",
"commands": "./plugins/claude/uploads/commands",
"mcpServers": "./.mcp.json",
"hooks": "./hooks/hooks.json"
}
29 changes: 23 additions & 6 deletions .codex-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,27 @@
"repository": "https://github.com/buildinternet/uploads",
"license": "Apache-2.0",
"keywords": ["uploads", "screenshots", "github", "file-hosting", "images", "mcp"],
"skills": [
"./skills/github-screenshots",
"./skills/annotate-screenshots",
"./skills/uploads-cli"
],
"hooks": "./hooks/hooks.json"
"skills": "./skills/",
"mcpServers": "./.mcp.json",
"hooks": "./hooks/hooks.json",
"interface": {
"displayName": "uploads.sh",
"shortDescription": "Host files for GitHub PRs",
"longDescription": "Host screenshots, GIFs, recordings, and files on uploads.sh and embed them in GitHub PRs and issues. Bundles the github-screenshots, annotate-screenshots, and uploads-cli skills, the hosted MCP server at agents.uploads.sh, and a pre-PR screenshot reminder hook.",
"developerName": "Build Internet",
"category": "Developer Tools",
"capabilities": ["Host files on a public CDN", "Attach media to GitHub PRs"],
"websiteURL": "https://uploads.sh",
"supportURL": "https://github.com/buildinternet/uploads/issues",
"privacyPolicyURL": "https://uploads.sh/privacy",
"termsOfServiceURL": "https://uploads.sh/terms",
"defaultPrompt": [
"Attach this screenshot to the current pull request",
"Stage a before/after of the settings page for this branch",
"Give me a public URL for this image"
],
"brandColor": "#c27eff",
"composerIcon": "./assets/logo.png",
"logo": "./assets/logo.png"
}
}
6 changes: 6 additions & 0 deletions .mcp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"uploads": {
"type": "http",
"url": "https://agents.uploads.sh/mcp"
}
}
1 change: 1 addition & 0 deletions .oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"unicorn/prefer-set-has": "off",
"unicorn/no-array-sort": "off",
"unicorn/no-array-reverse": "off",
"oxc/no-async-endpoint-handlers": "off",

"typescript/await-thenable": "error",
"typescript/consistent-return": "off",
Expand Down
4 changes: 4 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,10 @@ records; any future global secrets go through `wrangler secret put` (prod) or
`--fix` flags are global, so there is no way to keep a rule's diagnostic
while suppressing only its fix. If you re-enable any of them, re-check that
`pnpm lint:fix` still leaves a clean tree.
- `oxc/no-async-endpoint-handlers` is off because it is an Express 4 rule
(unhandled rejections from `async` route handlers). This repo is Hono on
Workers; Hono awaits handlers and `onError` catches throws. The diagnostic
is a false positive on every `app.post(..., async (c) => …)` middleware.
- A Husky pre-commit hook runs `pnpm types` then `lint-staged` (oxlint + oxfmt
on staged files; Prettier for `*.astro` — oxfmt has no Astro parser); it's
installed via the `prepare` script on `pnpm install`.
Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,10 @@ REST routes are in [docs/api.md](docs/api.md).
| `skills/uploads-cli/` | Agent skill for driving the CLI |
| `hooks/` | Shared pre-PR screenshot hook (`uploads hook pre-pr-screenshot`) for Claude + Codex |
| `plugins/claude/` | Claude Code plugin config (skills path, MCP, commands) |
| `.codex-plugin/` | Codex plugin manifest — same skills + shared hook |
| `.claude-plugin/` | Claude marketplace catalog + plugin manifest |
| `.codex-plugin/` | Codex plugin manifest — skills, hosted MCP, and shared hook |
| `.mcp.json` | Hosted MCP server for both plugins (`https://agents.uploads.sh/mcp`) |
| `assets/logo.png` | Pixel chevron mark for the Codex / OpenAI plugin listing |

The workers and web app are separate deployables. All storage access goes
through `createStorage()` in `packages/storage` — adding a provider is one new
Expand Down
5 changes: 5 additions & 0 deletions apps/mcp/.dev.vars.example
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,8 @@
# port and apps/web's UPLOADS_AUTH_ORIGIN convention (cookies/redirect_uri
# treat the two as different sites).
AUTH_ORIGIN=http://127.0.0.1:8788

# Optional. Public OpenAI plugin domain-verification token, served at
# /.well-known/openai-apps-challenge. Leave unset locally unless you are
# testing that route.
# OPENAI_APPS_CHALLENGE=
5 changes: 5 additions & 0 deletions apps/mcp/src/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,9 @@ interface Env {
GITHUB_APP_ID?: string;
GITHUB_APP_PRIVATE_KEY?: string;
GITHUB_APP_HOME_INSTALLATION_ID?: string;
// Public OpenAI plugin domain-verification token. Served as raw text at
// /.well-known/openai-apps-challenge. Set with `wrangler secret put
// OPENAI_APPS_CHALLENGE --config apps/mcp/wrangler.jsonc` when submitting
// the plugin; unset or blank → 404.
OPENAI_APPS_CHALLENGE?: string;
}
12 changes: 12 additions & 0 deletions apps/mcp/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ function buildServer(c: Context<WorkspaceVars>): McpServer {
workspaceName: c.get("workspaceName"),
authScopes: c.get("authScopes"),
mintingUserId: c.get("mintingUserId") ?? null,
resourceMetadataUrl: `${requestOrigin(c.req.url)}/.well-known/oauth-protected-resource`,
}),
validator,
});
Expand Down Expand Up @@ -305,6 +306,17 @@ const app = new Hono<WorkspaceVars>()
// client derives from `resource` = `<origin>/mcp`.
.get("/.well-known/oauth-protected-resource", respondProtectedResource)
.get("/.well-known/oauth-protected-resource/mcp", respondProtectedResource)
// OpenAI plugin portal domain verification. Must return only the token as
// text/plain — no JSON, no extra bytes. 404 when the secret is unset so a
// draft that hasn't been issued a token yet doesn't serve an empty body.
.get("/.well-known/openai-apps-challenge", (c) => {
const token = c.env.OPENAI_APPS_CHALLENGE?.trim();
if (!token) throw new NotFoundError();
return c.text(token, 200, {
"Cache-Control": "public, max-age=60",
"Content-Type": "text/plain; charset=utf-8",
});
})
// Primary endpoint: the workspace is inferred from the bearer token
// (up_<workspace>_…) or, for a JWT-shaped bearer, the OAuth token's
// `workspace` claim — so clients only need the URL and the token.
Expand Down
1 change: 1 addition & 0 deletions apps/mcp/src/robots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ export const ROBOTS_TXT = `# https://agents.uploads.sh — MCP server only; do n
# Public docs and marketing: https://uploads.sh

User-agent: *
Allow: /.well-known/openai-apps-challenge
Disallow: /
`;
Loading
Loading