Server-Side Implementation for an OpenAI Plugin: Architecture and Code Examples
The server-side implementation for an OpenAI plugin requires three core artifacts: a JSON manifest located at .codex-plugin/plugin.json, an OpenAPI specification defining your API contract, and executable skill scripts that handle incoming HTTP requests.
The openai/plugins repository provides reference implementations showing how to build a lightweight, modular server that exposes plugin capabilities to the OpenAI platform. This architecture separates the plugin declaration from the business logic, allowing the platform to discover and invoke your endpoints through a standardized protocol.
Core Server Components
The Plugin Manifest (.codex-plugin/plugin.json)
Every OpenAI plugin server must serve a manifest file at the .codex-plugin/plugin.json path. This JSON file declares the plugin's identity, required authentication scopes, and the location of the OpenAPI specification.
In the Zoom plugin example located at plugins/zoom/.codex-plugin/plugin.json, the manifest references the API specification and defines available skills. The evaluation harness in plugins/plugin-eval/src/evaluators/plugin.js validates this structure by reading the manifest and checking for required fields:
const manifestPath = path.join(pluginRoot, ".codex-plugin", "plugin.json");
// Validation logic checks manifest fields and discovers skill directories
const skillDirs = await discoverPluginSkillDirectories(pluginRoot, manifest);
Key manifest fields include:
- name and description – Human-readable plugin metadata
- api.spec – Path to the OpenAPI YAML file (e.g.,
"openapi.yaml") - auth.type – Authentication flow (e.g.,
"oauth") - skills – Array of available capabilities
The OpenAPI Specification
The server must serve an OpenAPI document (typically openapi.yaml or openapi.json) that formally describes every HTTP endpoint the plugin exposes. The Zoom plugin stores this at plugins/zoom/openapi.yaml, referenced from the manifest's api.spec field.
This specification allows the OpenAI platform to understand:
- Available routes and HTTP methods
- Request/response schemas
- Required parameters and authentication headers
Skill Scripts and Runtime Code
The actual business logic lives in skill scripts – modular, language-agnostic handlers that the server invokes based on the request path. The repository organizes these under skills/{skill_name}/scripts/.
For example, the Zoom scribe skill implements its handler at plugins/zoom/skills/scribe/scripts/handler.mjs. Authentication logic similarly resides in dedicated scripts like plugins/zoom/skills/auth/scripts/oauth.mjs.
These scripts process the HTTP request, interact with external APIs, and return JSON responses conforming to the OpenAPI schema.
How the Server Processes Requests
The server implementation follows a specific lifecycle to bridge the OpenAI platform with your custom logic:
-
Manifest Loading – On startup, the server reads
.codex-plugin/plugin.jsonto determine available capabilities and authentication requirements. -
OpenAPI Serving – The server exposes the OpenAPI specification at the URL declared in the manifest, typically at
/openapi.yamlor the root path. -
Dynamic Routing – Incoming requests are routed to the appropriate skill script based on the URL path. The generic server framework maps OpenAPI operations to the corresponding files in the
skills/directory. -
Authentication Handling – If the manifest specifies
"auth": { "type": "oauth" }, the server implementsGET /authfor the initial redirect andPOST /tokenfor token exchange, as demonstrated in the Zoom plugin's OAuth scripts. -
Response Serialization – Skill scripts return JSON payloads that the server formats according to the OpenAPI schema before sending back to the OpenAI runtime.
Complete Server Implementation Examples
Below are production-ready skeleton implementations following the patterns found in the openai/plugins repository.
Node.js with Express
This implementation loads the manifest, serves the OpenAPI spec, and dynamically routes requests to skill scripts:
// server.js
import express from "express";
import fs from "fs";
import path from "path";
const app = express();
app.use(express.json());
// Load the plugin manifest from .codex-plugin/plugin.json
const manifest = JSON.parse(
fs.readFileSync(path.join(process.cwd(), ".codex-plugin", "plugin.json"), "utf8")
);
// Serve the OpenAPI specification referenced in the manifest
app.get("/openapi.yaml", (req, res) => {
const specPath = path.join(process.cwd(), manifest.api?.spec || "openapi.yaml");
res.type("yaml").send(fs.readFileSync(specPath, "utf8"));
});
// Generic route handler that forwards to skill scripts
app.all("*", async (req, res) => {
const skillPath = req.path.replace(/^\//, "");
const scriptPath = path.join(process.cwd(), "skills", skillPath, "scripts", "handler.mjs");
try {
const { default: handler } = await import(`file://${scriptPath}`);
const result = await handler(req);
res.json(result);
} catch (error) {
res.status(404).json({ error: "Skill not found" });
}
});
// OAuth endpoints (implemented only if manifest requires auth)
if (manifest.auth?.type === "oauth") {
app.get("/auth", (req, res) => {
// Redirect to OAuth provider authorization URL
});
app.post("/token", (req, res) => {
// Exchange authorization code for access token
});
}
app.listen(3000, () => console.log("Plugin server listening on port 3000"));
Python with FastAPI
This FastAPI implementation provides equivalent functionality using Python's async framework:
# server.py
import json
from pathlib import Path
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import JSONResponse, PlainTextResponse
import importlib.util
app = FastAPI()
# Load plugin manifest from .codex-plugin/plugin.json
manifest_path = Path(__file__).parent / ".codex-plugin" / "plugin.json"
manifest = json.loads(manifest_path.read_text())
# Serve OpenAPI specification
@app.get("/openapi.yaml")
def openapi():
spec_path = Path(__file__).parent / manifest.get("api", {}).get("spec", "openapi.yaml")
return PlainTextResponse(spec_path.read_text(), media_type="application/yaml")
# Generic route handler for all skill endpoints
@app.api_route("/{full_path:path}", methods=["GET", "POST", "PUT", "DELETE"])
async def skill_handler(request: Request, full_path: str):
script_path = Path(__file__).parent / "skills" / full_path / "scripts" / "handler.py"
if not script_path.exists():
raise HTTPException(status_code=404, detail="Skill not found")
# Dynamically import and execute the skill handler
spec = importlib.util.spec_from_file_location("handler", script_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
result = await module.handle(request)
return JSONResponse(result)
# OAuth endpoints (conditional on manifest configuration)
if manifest.get("auth", {}).get("type") == "oauth":
@app.get("/auth")
async def authorize():
# Handle OAuth authorization redirect
pass
@app.post("/token")
async def token_exchange():
# Handle authorization code exchange
pass
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
Summary
The server-side implementation for an OpenAI plugin centers on three interconnected components:
.codex-plugin/plugin.json– The manifest that declares your plugin's capabilities, authentication requirements, and API specification location- OpenAPI specification – The formal contract describing available endpoints and data schemas, typically served as
openapi.yaml - Skill scripts – Modular handler files (e.g.,
plugins/zoom/skills/scribe/scripts/handler.mjs) containing the actual implementation logic
The generic server framework loads the manifest, exposes the OpenAPI document for platform discovery, routes requests to the appropriate skill scripts, and handles OAuth flows when configured.
Frequently Asked Questions
What are the minimum required files for an OpenAI plugin server?
You need at least three files: a manifest at .codex-plugin/plugin.json declaring the plugin metadata, an OpenAPI specification (referenced by the manifest) describing your endpoints, and a skill script implementing the logic for at least one endpoint. The evaluation harness in plugins/plugin-eval/src/evaluators/plugin.js validates that these files exist and that the manifest references valid skill directories.
How does the server handle authentication with the OpenAI platform?
If the manifest specifies "auth": { "type": "oauth" }, your server must implement two specific endpoints: GET /auth to initiate the OAuth flow and redirect to the provider, and POST /token to exchange the authorization code for an access token. The Zoom plugin example demonstrates this pattern in plugins/zoom/skills/auth/scripts/oauth.mjs.
Can I build the server in any programming language?
Yes. While the openai/plugins repository provides examples in Node.js and Python, the protocol only requires that your server serves valid JSON manifests, YAML/JSON OpenAPI specifications, and handles HTTP requests according to that specification. The skill scripts can be written in any language, provided the server can execute them and return JSON responses.
How does request routing work between the OpenAI platform and skill scripts?
The OpenAI platform reads your OpenAPI specification to determine available paths, then sends requests to your server's corresponding URLs. Your server implementation must map these incoming requests to the correct skill scripts in the skills/{name}/scripts/ directory. The generic handlers in the repository examples demonstrate dynamic routing based on the request path, loading the appropriate handler module (e.g., handler.mjs or handler.py) at runtime.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →