# How Openship MCP Endpoint Exposes API Routes to AI Agents with Permission Checking

> Discover how the Openship MCP endpoint at /api/mcp exposes API routes to AI agents using stateless JSON-RPC 2.0 tools and robust permission checking via Hono middleware. Learn more.

- Repository: [oblien/openship](https://github.com/oblien/openship)
- Tags: deep-dive
- Published: 2026-07-30

---

**The Openship MCP endpoint at `/api/mcp` exposes API routes as stateless JSON-RPC 2.0 tools, enforcing identical permission checks as standard HTTP requests by forwarding bearer tokens through the full Hono middleware stack.**

Openship is an open-source logistics and project management platform available in the `oblien/openship` repository. The Openship MCP endpoint exposes API routes to AI agents with permission checking by transforming eligible HTTP routes into discoverable JSON-RPC tools while routing every invocation through the application's native authentication and authorization layers, ensuring no security logic is duplicated.

## Authentication and Principal Resolution

Before an AI agent can discover or invoke tools, the MCP layer establishes identity through the same bearer token authentication used by standard API clients.

### Token Parsing and Identity Extraction

In [`apps/api/src/modules/mcp/mcp.routes.ts`](https://github.com/oblien/openship/blob/main/apps/api/src/modules/mcp/mcp.routes.ts) (lines 27-57), the `parseBearerToken` function extracts the Bearer token from the `Authorization` header. This token—whether a Personal Access Token (PAT) or OAuth token—is mapped to a **principal** object containing the user's role, read-only status, granted resource-type set, and the special **"own-projects"** flag. This principal resolution mirrors the standard `authMiddleware` flow, ensuring that MCP requests use the exact same identity model as regular HTTP API calls according to the oblien/openship source code.

## Tool Discovery and Capability Filtering

Once authenticated, the system determines which API routes the AI agent may invoke by inspecting the route registry and filtering against the principal's permissions.

### Route Registry Inspection and MCP Opt-In

The `getRouteRegistry` function scans the application's HTTP routes using helpers from [`apps/api/src/lib/route-permission.ts`](https://github.com/oblien/openship/blob/main/apps/api/src/lib/route-permission.ts), including `isPublicSpec` and `parsePermissionTag`, to identify candidates for MCP exposure. A route becomes available as a tool **only if** its OpenAPI specification contains an `mcp` block, making exposure opt-in rather than default. The `getMcpTools()` function in [`apps/api/src/modules/mcp/mcp-tools.ts`](https://github.com/oblien/openship/blob/main/apps/api/src/modules/mcp/mcp-tools.ts) (lines 14-78) constructs a `McpToolDef` for each eligible route, extracting path parameters, body schemas, and permission metadata.

### Principal-Based Tool Filtering

After generating the complete tool list, `filterToolsForPrincipal()` applies capability-based filtering using the principal's role, read-only status, granted root types, and the own-projects create-grant. This ensures the AI agent receives a **capability-aware catalogue** containing only the tools it is explicitly authorized to invoke. The filtering logic references `roleAllowsResourceType` and other permission checks from [`apps/api/src/lib/permission.ts`](https://github.com/oblien/openship/blob/main/apps/api/src/lib/permission.ts).

## Tool Execution with Full Permission Enforcement

When an AI agent invokes a tool, the MCP layer executes the underlying HTTP request while preserving all security boundaries.

### Internal HTTP Request Dispatch

The `dispatchTool()` function in [`apps/api/src/modules/mcp/mcp-dispatch.ts`](https://github.com/oblien/openship/blob/main/apps/api/src/modules/mcp/mcp-dispatch.ts) (lines 21-70) handles tool execution by creating an **internal HTTP request** that re-uses the real Hono application instance (`app.fetch`). The original bearer token is forwarded via the `authorization` header, along with optional `x-organization-id` and relevant body or query parameters. Because this request traverses the **full middleware stack**, including `authMiddleware` and the permission logic in [`permission.ts`](https://github.com/oblien/openship/blob/main/permission.ts), every tool call undergoes the exact same validation as external API requests.

### JSON-RPC Response Wrapping

The internal request's result (or error) is wrapped into a JSON-RPC 2.0 response format. If the bearer token lacks permission for the requested operation, the internal request fails and returns a JSON-RPC response with `isError` set to `true`, preserving the HTTP error code and message within the response payload.

## JSON-RPC Method Examples

The MCP endpoint accepts standard JSON-RPC 2.0 requests. Below are examples of the three primary interactions: initialization, tool discovery, and tool invocation.

### Initialize an MCP Session

Request:

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": { "protocolVersion": "2025-06-18" }
}

```

Response:

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2025-06-18",
    "capabilities": { "tools": { "listChanged": false }, "prompts": { "listChanged": false } },
    "serverInfo": { "name": "openship", "version": "1.0.0" }
  }
}

```

### List Available Tools

Request:

```json
{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/list"
}

```

Response (filtered by principal permissions):

```json
{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "tools": [
      {
        "name": "get_project",
        "description": "GET /api/projects/:id",
        "inputSchema": { "type":"object", "properties":{ "id":{"type":"string"} },"required":["id"] },
        "annotations": { "readOnlyHint": true, "destructiveHint": false }
      }
    ]
  }
}

```

### Call a Tool

Request:

```json
{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "tools/call",
  "params": {
    "name": "get_project",
    "arguments": { "id": "proj_123" }
  }
}

```

Response:

```json
{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "content": [{ "type":"text", "text":"{…project JSON…}" }],
    "isError": false
  }
}

```

## Key Implementation Files

The MCP layer spans several modules that work together to expose routes safely:

- **[`apps/api/src/modules/mcp/mcp.routes.ts`](https://github.com/oblien/openship/blob/main/apps/api/src/modules/mcp/mcp.routes.ts)** – Mounts the JSON-RPC endpoint at `/api/mcp` and handles bearer token parsing via `parseBearerToken`.
- **[`apps/api/src/modules/mcp/mcp-server.ts`](https://github.com/oblien/openship/blob/main/apps/api/src/modules/mcp/mcp-server.ts)** – Implements JSON-RPC method handlers for `initialize`, `tools/list`, and `tools/call`.
- **[`apps/api/src/modules/mcp/mcp-tools.ts`](https://github.com/oblien/openship/blob/main/apps/api/src/modules/mcp/mcp-tools.ts)** – Generates tool definitions from the route registry using `getMcpTools()` and filters them via `filterToolsForPrincipal()`.
- **[`apps/api/src/modules/mcp/mcp-dispatch.ts`](https://github.com/oblien/openship/blob/main/apps/api/src/modules/mcp/mcp-dispatch.ts)** – Performs internal HTTP request dispatch through `dispatchTool()` to execute tools through the full middleware stack.
- **[`apps/api/src/lib/route-permission.ts`](https://github.com/oblien/openship/blob/main/apps/api/src/lib/route-permission.ts)** – Contains the route registry, `isPublicSpec`, and `parsePermissionTag` helpers used during tool generation.
- **[`apps/api/src/lib/permission.ts`](https://github.com/oblien/openship/blob/main/apps/api/src/lib/permission.ts)** – Centralizes role-to-resource-type validation logic via `roleAllowsResourceType`.

## Summary

- The **Openship MCP endpoint** at `/api/mcp` uses **stateless JSON-RPC 2.0** to expose HTTP routes as AI-invokable tools.
- **Authentication** leverages the same bearer token parsing as standard API requests, resolving to a principal with role, read-only status, and resource grants.
- **Tool discovery** filters the route registry using `filterToolsForPrincipal()`, ensuring AI agents receive only permitted capabilities.
- **Execution** routes requests through the **full Hono middleware stack**, including `authMiddleware` and permission checks, by using `app.fetch` with forwarded authorization headers.
- No business logic is duplicated; the MCP layer acts as a thin façade over the existing REST API.

## Frequently Asked Questions

### What protocol does the Openship MCP endpoint use?

The endpoint implements **JSON-RPC 2.0** over HTTP at the path `/api/mcp`. This stateless transport allows AI agents to initialize sessions, discover available tools via `tools/list`, and invoke them using standard JSON-RPC request/response formats.

### How does the MCP layer differ from standard REST API authentication?

There is no difference in authentication logic. The MCP layer calls `parseBearerToken` and resolves principals using the same code path as `authMiddleware`. The bearer token is forwarded to internal requests via the `authorization` header, ensuring **identical permission enforcement** for AI agents and human users.

### Which API routes are exposed as MCP tools?

Routes are exposed **only when explicitly opted in** via an `mcp` block in their OpenAPI specification. The `getRouteRegistry` function identifies these candidates, and `filterToolsForPrincipal()` further restricts the list based on the requesting principal's role, read-only status, and granted resource types.

### What happens when an AI agent invokes a tool without proper permissions?

The `dispatchTool()` function creates an internal HTTP request that traverses the full middleware stack. If the bearer token lacks permission for the requested route, the standard permission layer in [`apps/api/src/lib/permission.ts`](https://github.com/oblien/openship/blob/main/apps/api/src/lib/permission.ts) rejects the request. The MCP wrapper captures this failure and returns a JSON-RPC response with `isError: true`, containing the appropriate HTTP error code and message.