Skip to content

Run a shared server

A shared server runs one AI Assist container as a service for your whole team. Developers connect to it over HTTPS and sign in with your identity provider, so the database password stays on the server, and every call is audited in one place under each person’s own identity.

A request travels like this:

  1. The IDE sends MCP messages to https://mcp.example.com/mcp, with an access token from your identity provider.

  2. nginx terminates TLS and forwards the request to the container on 127.0.0.1:8095.

  3. The container validates the token, runs the tool with its read-only database account, and writes an audit entry under the person’s identity.

Start from the configuration file and add the settings for HTTP:

config.yaml (additions for a shared server)
server:
transport: http
http:
port: 8080 # inside the container
path: /mcp
allowed_origins: [] # web origins allowed to call the server
auth:
issuer: https://login.example.com/
audience: api://aipeoplesoft-mcp
scopes:
metadata: aips.metadata.read # needed for every tool
data: aips.data.read # also needed for run_select
audit:
destination: /var/log/aipeoplesoft/audit.jsonl
  • allowed_origins: the MCP specification requires servers to check the Origin header, which protects them from malicious web pages. IDEs don’t send one; a request from a web page whose origin isn’t listed is rejected.
  • auth: the server accepts only access tokens issued by issuer for audience. There’s no HTTP mode without authentication. scopes names the two permissions the server checks in each token: every tool needs the metadata scope, and run_select also needs the data scope, so you can give data access to fewer people.
  • audit: write the audit log to a file on a mounted volume, so it outlives the container.

The server runs in the background with the serve --transport http command, published only on the host’s loopback interface. Only nginx listens publicly.

Run the server as a dedicated, non-root user, such as aips, with a systemd service that Podman’s Quadlet generates. As that user:

  1. Create the database password secret, as in Store the database password.
  2. Save config.yaml as ~/aips/config.yaml, and create the ~/aips/audit directory.
  3. Save this unit file as ~/.config/containers/systemd/aipeoplesoft-mcp.container:
aipeoplesoft-mcp.container
[Unit]
Description=aiPeopleSoft MCP server
[Container]
Image=localhost/aipeoplesoft-mcp:1.0
Exec=serve --transport http
PublishPort=127.0.0.1:8095:8080
Volume=%h/aips/config.yaml:/etc/aipeoplesoft/config.yaml:ro,Z
Volume=%h/aips/audit:/var/log/aipeoplesoft:Z
Secret=aips_db_password,type=env,target=AIPS_DB_PASSWORD
[Service]
Restart=always
[Install]
WantedBy=default.target

Then start it. enable-linger keeps the user’s services running after you log out:

Terminal window
systemctl --user daemon-reload
systemctl --user start aipeoplesoft-mcp
sudo loginctl enable-linger aips

Check it with systemctl --user status aipeoplesoft-mcp, and read its log with journalctl --user -u aipeoplesoft-mcp.

nginx terminates TLS and forwards requests to the container. Streamable HTTP can stream a response as it’s produced, so turn off response buffering for the MCP path:

nginx server block
server {
listen 443 ssl;
server_name mcp.example.com;
ssl_certificate /etc/ssl/mcp.example.com.pem;
ssl_certificate_key /etc/ssl/mcp.example.com.key;
location /mcp {
proxy_pass http://127.0.0.1:8095;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_buffering off;
proxy_read_timeout 120s;
}
# OAuth protected resource metadata, which tells IDEs
# where to sign in
location /.well-known/oauth-protected-resource {
proxy_pass http://127.0.0.1:8095;
}
}

proxy_read_timeout must be longer than the server’s query timeout, 30 seconds by default. Any TLS certificate works, including a Cloudflare origin certificate when the site sits behind Cloudflare’s proxy.

A shared server uses MCP’s authorization specification, which is based on OAuth 2.1. The server is an OAuth resource server: it publishes metadata that names your identity provider, and accepts only access tokens issued for it.

  1. Register the server with your identity provider as an API. Its identifier is the audience in config.yaml, such as api://aipeoplesoft-mcp.

  2. Decide who can use it, for example by assigning your developers’ group to the API. Grant the data scope, which run_select needs, only to the people who need to see data.

  3. Connect an IDE. The first time it connects, the IDE reads the server’s metadata, sends you to your identity provider to sign in, and stores the token.

Many identity providers only accept clients that are registered in advance. In that case, register a client for each IDE your team uses, and give the IDE its client ID. For Claude Code, that’s the --client-id option shown below.

Each call is audited under the identity in the token, and the rate limit of 60 calls per minute applies per person.

Terminal window
claude mcp add --transport http aipeoplesoft \
https://mcp.example.com/mcp
claude mcp login aipeoplesoft

If your identity provider needs a registered client, add --client-id <client-id> to claude mcp add.

Two requests from any machine show that nginx, the server, and sign-in are wired together:

Terminal window
# Without a token, the server refuses the request with 401
# and a WWW-Authenticate header that points to its metadata
curl -si -X POST https://mcp.example.com/mcp | head -n 5
# The metadata names your identity provider
curl -s https://mcp.example.com/.well-known/oauth-protected-resource

Then connect an IDE, sign in, and ask a first question, as in Before you start.