Gallery
Stores gallery photos and videos in a configured Nextcloud account and folder, using Nextcloud as the backing store for originals and generated thumbnail/large WebP media served by the gallery.
Click on "Deploy 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., "@Galleryshow me photos tagged 迎新 from last year's orientation"
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.
██████╗ █████╗ ██╗ ██╗ ███████╗██████╗ ██╗ ██╗
██╔════╝ ██╔══██╗██║ ██║ ██╔════╝██╔══██╗╚██╗ ██╔╝
██║ ███╗███████║██║ ██║ █████╗ ██████╔╝ ╚████╔╝
██║ ██║██╔══██║██║ ██║ ██╔══╝ ██╔══██╗ ╚██╔╝
╚██████╔╝██║ ██║███████╗███████╗███████╗██║ ██║ ██║
╚═════╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚══════╝╚═╝ ╚═╝ ╚═╝WinLab Gallery
The lab's photo wall for members: photos and short videos in Nextcloud, everything else in its own Postgres, and every action also an MCP tool.
nextjs postgres nextcloud docker mcp
WinLab members share lab photos here: upload from a phone, react, and find who is in them. Only signed-in members see anything; every file passes through the app, which checks the session first.
> "What did we post from last year's orientation?"
⚡ list_photos { "query": "迎新" }
✓ { "photos": [{ "name": "合照", "sequenceSize": 6, ... }], "nextOffset": null }What it does
Shows the wall: newest taken first, photos uploaded together as one card, filtered by search, uploader or person.
Searches by meaning: "生日蛋糕" finds the cake photos even when nobody named them, through EmbeddingGemma 2 vectors of each photo (optional; off, search matches text only).
Takes uploads: photos up to 30 MB (HEIC turns into JPEG) and videos up to 60 seconds and 100 MB (re-encoded to 720p H.264).
Stores files in Nextcloud under one folder of one account, with a 480 px and a 2048 px WebP made at upload.
Lets members react: 8 reactions.
Serves MCP at
/mcp, with the gallery as the OAuth 2.1 authorization server.
Related MCP server: eq-mcp-gateway
Deploy
With Docker Compose, on any machine with Docker:
curl -fsSLO https://raw.githubusercontent.com/NYCU-WinLab/gallery/main/compose.yaml
curl -fsSL -o .env https://raw.githubusercontent.com/NYCU-WinLab/gallery/main/.env.example
# set POSTGRES_PASSWORD, BETTER_AUTH_*, KEYCLOAK_* and NEXTCLOUD_* in .env
docker compose up -dThis starts Postgres, runs the migrations once, then serves the gallery from ghcr.io/nycu-winlab/gallery on 127.0.0.1:3000.
Put a reverse proxy with TLS in front, at the host inBETTER_AUTH_URL, and let it pass request bodies of 100 MB. MCP clients only accept an HTTPS resource, and the Keycloak redirect URI must match it.
Use
In your Keycloak realm, create a confidential OpenID Connect client with the redirect URI
<BETTER_AUTH_URL>/api/auth/callback/keycloak, and put its ID and secret in.env. Give its service accountview-usersso/mcpcan check that a member still exists.In Nextcloud, create an app password for the account that holds the files, create the folder
NEXTCLOUD_ROOTin it, and put both in.env.Open
BETTER_AUTH_URLand sign in.Make the first admin, once, after that member has signed in (later admins are added on /admins):
docker compose exec postgres psql -U gallery -d gallery -c \ "insert into admins (user_id) select id from users where username = '<their account>'"Add the MCP server to a client, for example
claude mcp add --transport http gallery <BETTER_AUTH_URL>/mcp, then ask it to callwhoami.
Configure
Set these in .env; .env.example documents every one.
Key | What it sets | Default |
| The database password, used by every service | required |
| The public URL; the MCP resource is | required |
| Signs sessions and OAuth queries | required |
| The realm URL, e.g. | required |
| The gallery's Keycloak client | required |
| Where the files live | required |
| The folder in that account the gallery owns |
|
| Image tag |
|
| Where web listens on the host |
|
|
| empty |
| OTLP/HTTP endpoint for traces and error logs; off when empty | empty |
How it works
flowchart LR
member[Member] -->|browser| pages[Pages]
agent[MCP client] -->|bearer token| mcp["/mcp"]
pages --> actions[Actions]
mcp --> actions
actions --> pg[(Postgres)]
member -->|"PUT /api/upload"| media[sharp and ffmpeg]
media --> nc[(Nextcloud)]
member -->|"GET /media"| cache[Disk cache]
cache --> ncAn upload streams to a temp file, becomes an original plus two WebP sizes, and goes to Nextcloud as <root>/<photo id>/; only then is the row written. /media/<id>/<thumb|large|original> checks the session, keeps thumb and large on local disk after the first read, and streams originals with Range so videos seek. Only lib/actions, lib/auth and lib/db touch the database, and CI checks that every action is in the registry the MCP tools come from.
Develop
bun install
cp .env.example .env.local # point DATABASE_URL at a local Postgres
bun run db:migrate
bun run devUploads need ffmpeg and ffprobe on the PATH. CI runs typecheck, lint, format, the design token check, the action registry check and a build, then builds the image and probes a real docker compose up. See CLAUDE.md for the conventions.
Limitations
One web container: the media cache and upload temp files are on its own disk.
Search by meaning scores every photo per search and embeds on CPU (about 5 seconds a photo on 2 cores), which suits a lab's few thousand photos, not a large archive.
Video re-encoding runs inside the upload request, so a 60 second clip takes a while on a small machine.
Contributing
Issues and PRs welcome: start with CONTRIBUTING.md.
License
MIT, made in a lab that keeps every group photo.
This server cannot be deployed
Maintenance
Related MCP Connectors
Collective, moderated event photo spaces: read status, list media, save a guest photo, react, star.
Read-only MCP server over the JENRIKS daily photoblog: one photo per day since 2006. No auth.
OAuth-protected, read-only-by-default MCP server for provenance-labeled QuillCaddie project memory.
- WoWSQLOAuthcom.wowsql
Managed Postgres MCP: projects, SQL, docs search, and storage buckets via OAuth.
Related MCP Servers
- AlicenseAqualityAmaintenanceRun all of WordPress + SEO, Security, WooCommerce Shop & Elementor: 10 websites in one connector. Posts, pages, media, comments and updates from Claude, ChatGPT or Cursor. Start free.95GPL 2.0

eq-mcp-gatewayofficial
FlicenseNot gradedqualityBmaintenanceA remote MCP server that lets MCP clients self-register over OAuth 2.1 and call read-only tools for organization identity, place and region browsing, dashboards, metrics, and ranking, forwarding the caller's own Auth0 token so each service still sees the real user. It also exposes a bearer pass-through surface for first-party apps holding an existing token.-- AlicenseNot gradedqualityBmaintenanceEnables MCP-capable agents to coordinate through shared Postgres-backed state, including registrations, tickets, memories, and discussions, over authenticated streamable HTTP.MIT
- AlicenseNot gradedqualityCmaintenanceEnables any MCP client to read and edit database schemas (DBML/SQL), Mermaid diagrams, and shared board layouts, along with the organizations, projects, documents, and marketplace listings around them. It runs as a hosted OAuth 2.1 endpoint, so there is nothing to install and access is limited to what the signed-in user can already see.MIT