
MintHCM MCP
MCPMCP module of MintHCM, an open-source self-hosted HR system: lets agents query, count and edit employee, recruiting, onboarding, offboarding, attendance, leave and performance records and run reports, keeping data on your server. Deploy MintHCM first; sign in via browser on first connect.
Mint MCP
Mint MCP is a module in Mint that acts as an MCP server and enables integration with MCP clients such as Visual Studio Code, Copilot Chat, etc. Thanks to this, you can use MCP tools in Mint and add your own.
Authentication Flow
Mint MCP implements OAuth 2.1 with PKCE. The user-facing consent page is rendered by the Vue SPA (not by mcp/) so the experience matches the rest of Mint. The PHP shared session is used as coordination state between the MCP host and the Mint/Vue host.
Key pieces:
mcp/oauth.php— entry point for/oauth/authorize,/oauth/token, etc. Only the first redirect hits it; after that everything happens on the Mint host.OAuth2Server::redirectToLogin()— stores the pending request under$_SESSION['oauth_flows'][<flow_id>]with a 15-minuteexpires_atand a randomconsent_token, then redirects the browser to<site_url>#/mcp/login?flow=<flow_id>. Usingsite_url(notHTTP_HOST) is what lets MCP live on a separate domain.McpLoginView.vue— readsflowfrom the URL, fetchesGET /mcp/oauth-info?flow=…to populate client name and scopes, and shows explicit [Authorize] / [Cancel] buttons. No auto-authorization.McpController::authorize()/McpController::deny()— validateflow_id+consent_token(CSRF protection,hash_equals), enforce the TTL, and return the finalredirect_urlback to the MCP client.
Sequence
sequenceDiagram
participant C as MCP Client
participant B as Browser
participant MCP as mcp/oauth.php
participant Vue as Vue SPA (Mint host)
participant API as api/ (Mint host)
C->>B: redirect to /oauth/authorize?client_id=…&code_challenge=…
B->>MCP: GET /oauth/authorize
MCP->>MCP: store oauth_flows[flow_id] + consent_token
MCP-->>B: 302 to <site_url>#/mcp/login?flow=<id>
B->>Vue: load consent page
Vue->>API: GET /mcp/oauth-info?flow=<id>
API-->>Vue: { client_name, scope, consent_token }
alt not logged in
Vue->>API: POST /login (username, password)
API-->>Vue: session established
end
alt user clicks Authorize
Vue->>API: POST /mcp/authorize { flow, consent_token }
API->>MCP: completeAuthorization(flow, userId)
API-->>Vue: { redirect_url: redirect_uri?code=…&state=… }
else user clicks Cancel
Vue->>API: POST /mcp/deny { flow, consent_token }
API-->>Vue: { redirect_url: redirect_uri?error=access_denied&state=… }
end
Vue->>B: window.location = redirect_url
B->>C: redirect with code / error
C->>MCP: POST /oauth/token (code + code_verifier)
MCP-->>C: access_token, refresh_token
Security properties
consent_tokenis a 32-byte random string compared withhash_equals. Attacker JS on a third-party origin cannot read it — it is never placed in a URL, only in the JSON body returned from/mcp/oauth-infoand in the POST body to/mcp/authorize//mcp/deny./mcp/oauth-infois served withCache-Control: no-store.- Flows live for 15 minutes (
OAuth2Server::FLOW_TTL_SECONDS). Expired flows are garbage-collected on the nextredirectToLogin()call. - Concurrent flows do not collide: each authorization gets its own random
flow_idand its own slot in$_SESSION['oauth_flows']. Cancelroutes throughMcpController::deny()which re-validates the redirect_uri against the registered client, so it cannot be turned into an open redirect even if the session were tampered with.
Server Configuration
MintHCM MCP is served at <site_url>/mcp (e.g. https://your-mint-domain/mcp). Apache must have mod_rewrite loaded and AllowOverride All (or at minimum AllowOverride FileInfo) on the directory containing mcp/.
Installation requirement: the root .htaccess is generated from the template legacy/MintCLI/src/Assets/.htaccess during installation — both by MintCLI and by the browser-based installer. If for any reason the file is missing or outdated, re-run the installation's htaccess step or manually copy the template, replace __BASE_PATH__ with the installation path (e.g. /MintHCM or /), and place the result at the MintHCM root.
Discovery endpoints
OAuth 2.1 clients discover the authorization server by fetching metadata under .well-known/. Different clients use different conventions:
| URL pattern | Spec | Handler |
|---|---|---|
<site_url>/mcp/.well-known/oauth-protected-resource | RFC 9728 path-appending | mcp/.htaccess |
<site_url>/mcp/.well-known/oauth-authorization-server | RFC 8414 path-appending | mcp/.htaccess |
<site_url>/mcp/.well-known/openid-configuration | OIDC-style path-appending | mcp/.htaccess |
<host>/.well-known/oauth-authorization-server/mcp | RFC 8414 path-insertion (single instance at DocumentRoot) | root .htaccess (MintCLI) |
<host>/.well-known/oauth-protected-resource/mcp | RFC 9728 path-insertion (single instance at DocumentRoot) | root .htaccess (MintCLI) |
<host>/.well-known/openid-configuration/mcp | OIDC path-insertion (single instance at DocumentRoot) | root .htaccess (MintCLI) |
Path-insertion URLs land outside the mcp/ directory — mcp/.htaccess cannot reach them. They are handled by the root .htaccess generated by MintCLI. This rule only matches when a MintHCM installation occupies the entire DocumentRoot (no <path> segment between .well-known/... and mcp) — see below for hosts running several instances under one DocumentRoot.
Multi-instance hosting (shared DocumentRoot)
Some environments run several MintHCM installations as subdirectories of one Apache DocumentRoot, e.g.:
/var/www/ ← DocumentRoot
├── aiagent/ ← MintHCM instance #1
├── develop/ ← MintHCM instance #2
└── MintHCM/ ← MintHCM instance #3
A path-insertion discovery request for instance aiagent looks like <host>/.well-known/oauth-authorization-server/aiagent/mcp — the .well-known/ segment is resolved directly under the shared DocumentRoot, so it never reaches any instance's own .htaccess (Apache resolves .htaccess files by walking the filesystem path of the request, and aiagent here is not a path prefix — it comes after .well-known/...). No .htaccess inside aiagent/ can intercept this request.
This requires a one-time, manual addition to the .htaccess in the shared DocumentRoot itself (/var/www/.htaccess in the example above — not any instance's own .htaccess, and not tracked in any single instance's repository):
RewriteEngine On
RewriteRule ^\.well-known/oauth-protected-resource/(.+)/mcp/?$ $1/mcp/oauth.php [L,QSA,E=OAUTH_ENDPOINT:discovery]
RewriteRule ^\.well-known/oauth-authorization-server/(.+)/mcp/?$ $1/mcp/oauth.php [L,QSA,E=OAUTH_ENDPOINT:authorization]
RewriteRule ^\.well-known/openid-configuration/(.+)/mcp/?$ $1/mcp/oauth.php [L,QSA,E=OAUTH_ENDPOINT:authorization]
The (.+) capture group matches the instance's subdirectory (aiagent, develop, ...) and forwards the request into that instance's own mcp/oauth.php. Adding a new instance under a new subdirectory requires no further Apache changes — the rule is generic. This block is independent of, and does not replace, the per-instance .htaccess generated by MintCLI.
MCP Client Configuration
The following example describes configuration for the MCP client in Visual Studio Code; it should be similar for other clients.
Adding MCP Server in Visual Studio Code
- Run command
MCP: Add Server...in Visual Studio Code. - Choose HTTP as the server type.
- Enter the URL of your Mint MCP server, e.g.,
https://your-mint-domain/mcp. - Enter a name for the server, e.g.,
my-mcp-server-mint1. Your configuration should look like this in yourmcp.jsonfile:
{
"servers": {
"my-mcp-server-mint1": {
"type": "http",
"url": "https://your-mint-domain/mcp"
}
}
}
Using the MCP Server in Visual Studio Code
- Run the command
MCP: List Serversand select your servermy-mcp-server-mint1. - Select
Start server. If this is your first time using Mint MCP via this client, you will be asked to log in to your MintHCM. After a successful login wait for the messageConnection state: Runningand thenDiscovered X tools. - Open Copilot Chat, set the mode to
Agent, and select"Configure tools..."(the tools icon under the chat input field). - Make sure that in the
my-mcp-server-mint1section, the tools you want to use are checked.
After completing these steps, your MCP client should be properly configured and ready to use.
Adding MCP Server in Claude Code CLI
- Register the server:
claude mcp add --transport http mint-mcp https://your-mint-domain/mcp/ - Start Claude Code:
claude - On first connection Claude Code will open a browser tab with the Mint login and consent screen. Log in and click Authorize.
- After authorization, verify the connection:
You should see/mcpmint-mcpwith statusconnectedand the list of discovered tools.
To remove the server: claude mcp remove mint-mcp
Claude Code CLI inside a local Docker container
When Claude Code CLI runs inside the same Docker container as MintHCM (typical local dev setup), a few things differ from a remote deployment.
MCP URL to use
Inside the container Apache listens on port 80 — use the internal URL, not the Windows-side mapped port:
http://localhost/mint/mcp/
site_url mismatch
MintHCM's site_url in legacy/config.php is typically set to the Windows-side URL (e.g. http://localhost:8085/mint). The OAuth discovery documents return that URL in authorization_endpoint, token_endpoint, etc. From inside the container port 8085 is unreachable — token exchange fails.
Two options:
- Run Claude Code on Windows (recommended) — use
http://localhost:8085/mint/mcpwhere all OAuth endpoints are reachable. - Change
site_urltohttp://localhost/mintinlegacy/config_override.php— discovery returns internal URLs, OAuth works inside the container, but browser redirects during login also uselocalhost(reachable from Windows via the same port-mapping).
Steps
- Register and start the server:
claude mcp add --transport http mint-mcp http://localhost/mint/mcp/ claude - Claude Code prints an OAuth authorization URL — open it in the browser on Windows, log in to Mint and click
Authorize.
- If you changed
site_urltohttp://localhost/mint, the browser will redirect tohttp://localhost/mint/mcp/oauth/callback?code=…&state=…. Changelocalhosttolocalhost:8085(use your port) in the URL and hit Enter to reach the MintHCM host on Windows. The callback will complete the OAuth flow.
- If you changed
- Verify the connection: type
/mcpin Claude Code —mint-mcpshould show statusconnected.
Troubleshooting — POST /mcp returns 301 then GET returns 405/404
Check added url to claude mcp add command. It should be the same as the one in the mcp.json file. If you added http://localhost/mint/mcp without the trailing slash, the client will POST to http://localhost/mint/mcp (no slash) which Apache redirects to http://localhost/mint/mcp/ (with slash). The client then GETs the new URL, which is not a valid endpoint and returns 405 or 404. Always add the trailing slash when adding the server URL to the client, correct example: http://localhost/mint/mcp/
**Troubleshooting — discovery returns `localhost:8085` URLs but client can't reach them**
The `site_url` mismatch described above. Either switch to running Claude Code on Windows or change `site_url`.
---
### Troubleshooting
Start with `mcp/mintmcp.log` — it records key OAuth lifecycle events (client registered, token issued, error responses) without any secrets. Then compare with Apache's `access.log` to see URLs the client hit *before* reaching PHP.
**"Dynamic Client Registration not supported" popup, or `SDK auth failed: HTTP 404` with HTML body in the error message**
The client requested a `.well-known/...` URL that Apache served 404 HTML for, and the client tried to parse it as JSON.
1. Check `AllowOverride All` (or `FileInfo`) is set for the `mcp/` directory in Apache's vhost config — without it, `.htaccess` is ignored entirely.
2. Fire a curl at the discovery URL — e.g. `curl -i https://your-mint-domain/mcp/.well-known/oauth-authorization-server`. Expect `200 OK` with JSON; `404` means the rewrite rule isn't matching.
**Connection stuck at "Waiting for server to respond to `initialize` request..."**
The client is waiting for you to complete the browser login. If no browser tab opened, clear the client's cached auth state:
- **VS Code**: Command Palette → MCP: List Servers → *your server* → Sign Out (if available).
- **Claude Code CLI**: `claude mcp remove <server-name>` and re-add it.
- **Any client**: close it, remove the stored credential entry (Windows Credential Manager / macOS Keychain / `~/.claude.json`), restart.
**`POST /register` returns 404 in Apache access log (without `/mcp/oauth/register`)**
The client is using hard-coded RFC 7591 defaults because it couldn't read `registration_endpoint` from the authorization server metadata — which means discovery failed earlier. Same fix as the "Dynamic Client Registration not supported" case above.
**Nothing shows up in `mintmcp.log` after connecting**
The request isn't reaching PHP. Check Apache `access.log` to see what URL the client hit and what status it got. If the URL is correct but returns 404, `AllowOverride` is likely too restrictive in vhost config.
## MCP Client Compatibility
Different MCP clients implement different subsets of the OAuth 2.1 and discovery specs. Below is what was tested, what the root cause of each quirk is, and what was done to address it.
### VS Code Extension (GitHub Copilot)
**Status: ✅ Supported**
VS Code uses path-appending discovery exclusively: it queries `<mcp-url>/.well-known/oauth-protected-resource`, then falls back to `<mcp-url>/.well-known/oauth-authorization-server` and `<mcp-url>/.well-known/openid-configuration`. All three are handled by `mcp/.htaccess` and require no root `.htaccess` rules.
### Claude Code CLI
**Status: ✅ Supported** (requires root `.htaccess` generated by MintCLI)
Claude Code CLI embeds the Bun runtime which implements a stricter OAuth 2.1 discovery flow. Two issues were found and fixed:
**Issue 1 — Path-insertion discovery returns HTML instead of 404**
Bun tries RFC 8414/RFC 9728 path-insertion URLs before the MCP request:
/.well-known/oauth-authorization-server/ /.well-known/oauth-protected-resource/
These URLs have `.well-known/` at the host root, outside the `mcp/` directory. If the root `.htaccess` has a catch-all Vue route without an exception, these URLs return `200 + HTML` instead of `404`. Bun does not fall back to path-appending when it receives a `200` — it considers discovery done but fails to parse the HTML as JSON and silently aborts the flow (no browser popup, no error message).
Fix: root `.htaccess` includes explicit `RewriteRule ^\.well-known/...mcp/?$` rules that route to `mcp/oauth.php`, plus a `[R=404]` catch-all for all other `.well-known/` paths so Bun can fall back correctly.
**Issue 2 — OIDC Discovery fields missing from authorization server metadata**
Bun validates the authorization server metadata document against the OIDC Discovery 1.0 schema (Zod). Two fields are required by the OIDC spec but were missing from `handleAuthorization()`:
- `subject_types_supported` — must be an array, e.g. `["public"]`
- `id_token_signing_alg_values_supported` — must be an array, e.g. `["RS256"]`
Without these fields, Bun's Zod validation throws and the client silently aborts — same symptom as Issue 1 (no browser popup). The error is only visible if you run Claude Code with `--debug`.
Fix: both fields are now returned from `handleAuthorization()`.
### Postman
**Status: ✅ Supported** (tested on Postman 12.17.3)
#### Prerequisites — site_url and frontend build
Two things must be true before the Postman login page works correctly:
1. **`site_url` in `legacy/config.php` must match the base URL you use in Postman.** The OAuth consent page redirect uses `site_url` — if it points to a different host or port than what Postman sends requests to, the browser will open the wrong address. This matters most for local Docker setups where `site_url` is often set to the Windows-side mapped port (e.g. `http://localhost:8085/mint`) but Apache inside the container listens on port 80.
2. **The Vue frontend must be built** (`npm run build:repo`), not running via the Vite dev server. Postman opens the consent page in a browser; the Vite dev server is not a reliable target for that browser session and may not serve the page correctly.
#### Step 1 — Register an OAuth client
A client with Postman's callback URL must exist in MintHCM before the first login. Replace `<site_url>` with the value from `legacy/config.php` (e.g. `http://localhost:8085/mint`).
- **Register via DCR** — send a `POST` to `<site_url>/mcp/oauth/register` (no auth required):
```json
{
"client_name": "Postman",
"redirect_uris": ["https://oauth.pstmn.io/v1/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"]
}
The response contains client_id — copy it for the next step.
- Register manually — go to MintHCM → Admin → OAuth2 Clients & Tokens, create a new client and set
Redirect URLto exactlyhttps://oauth.pstmn.io/v1/callback.
Step 2 — Add an MCP request in Postman
In Postman, create a new request and set its type to MCP (available in the request type dropdown next to the URL bar). Enter the server URL:
<site_url>/mcp/
Go to the Authorization tab, select OAuth 2.0, and fill in the fields:
| Field | Value |
|---|---|
| Grant type | Authorization Code (with PKCE) |
| Auth URL | <site_url>/mcp/oauth/authorize |
| Access Token URL | <site_url>/mcp/oauth/token |
| Client ID | (client_id from Step 1) |
| Callback URL | https://oauth.pstmn.io/v1/callback |
| Scope | mcp:read mcp:write |
| Code Challenge Method | SHA-256 |
Click Get New Access Token. A browser window opens with the MintHCM login and consent screen — log in and click Authorize. Postman receives the token automatically.
Note: Do not use
Client Credentialsgrant type — it is not supported. Do not point the token URL atlegacy/Api/...— the MCP OAuth server is at/mcp/oauth/token.
Step 3 — Connect and discover tools
With the token set, send a GET request to <site_url>/mcp/ — the server returns a list of all available MCP tools. This confirms the connection is working and the token is valid.
Internal session token
The route internal/session-token allows CORS from the app origin (site_url) and the local Vite dev server (http://localhost:5173). Both are listed in Auth/Internal/TrustedOrigins.php.
MCP Apps
This server supports the MCP Apps extension and allows attaching UI resources to tool responses via _meta. Each app is loaded in a sandbox; tools can return structured data (e.g. structuredContent) for the app to render.
Tool response with UI resource metadata
When a tool has a registered UI resource, tools/call returns normal content and _meta with the linked ui:// URI. The app receives the tool result (including any structuredContent) and can render it.
Example:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"content": [{ "type": "text", "text": "..." }],
"structuredContent": { "columns": ["name", "id"], "records": [] },
"_meta": {
"ui": {
"resourceUri": "ui://mint-mcp/search-records"
}
}
}
}
Adding a UI resource to a tool
Use a dedicated resource class in Capabilities/Resources/ with the #[McpResource] attribute, then link the tool to that resource via #[McpTool(meta: ...)].
Example resource class:
namespace MintMCP\Capabilities\Resources;
use Mcp\Capability\Attribute\McpResource;
final class SearchRecordsResource extends AbstractMCPAppResource
{
public const URI = 'ui://mint-mcp/search-records';
#[McpResource(
uri: self::URI,
name: 'search_records_app',
description: 'Table view for search_records results with links to each record.',
mimeType: 'text/html;profile=mcp-app',
meta: [
'ui' => [
'prefersBorder' => true,
],
],
)]
public function getSearchRecordsApp(): string
{
return (string) file_get_contents(self::appDistPath('search-records'));
}
}
Example tool linking to the resource:
use Mcp\Capability\Attribute\McpTool;
use MintMCP\Capabilities\Resources\SearchRecordsResource;
#[McpTool(
name: 'search_records',
description: 'Retrieve records from a MintHCM module.',
meta: [
'ui' => [
'resourceUri' => SearchRecordsResource::URI,
'visibility' => ['model', 'app'],
],
],
)]
resourceUri should follow the ui://mint-mcp/<tool-name> convention used by Mint MCP resources.
Developing and building MCP Apps
From mcp/apps/:
npm run "dev mcp-apps"— Vite dev server with HMR. Open an app athttp://localhost:5173/<app-dir>/<app-file>.html(e.g.http://localhost:5173/search-records/search-records.html).npm run "build mcp-apps"— Type-check, build, and flatten to single-file HTML inmcp/apps/dist/(e.g.search-records.html).
When rendering server-supplied data (e.g. structuredContent.columns, record fields), use safe DOM APIs (createElement, textContent, replaceChildren) instead of innerHTML to avoid XSS, even when the server is trusted.
For the full spec: MCP Apps (ext-apps).
관련 리소스

OneWave AI Claude Skills
Skill200+ single-file skills, mostly for sales, marketing, consulting and operations: deal strategy, forecasts, outreach emails, proposals and SOWs, contract review, resume screening, expense checks and RFP responses, plus personal finance and job search. Contract and tax output needs expert review.
OneWave-AI/claude-skills348
Himalayas Remote Jobs MCP Server
MCPHimalayas' official MCP: employers post remote jobs free (with screening questions), search open-to-work candidates and check salary benchmarks by role and country; job seekers search jobs and manage their profile and application board. Searching needs no account; managing needs a browser login.
Himalayas-App/himalayas-mcp21
Apideck MCP Server
MCPApideck's official MCP: one unified API to read and write invoices, ledgers and reports in QuickBooks, Xero, NetSuite and other accounting systems, plus HR, CRM and ATS data. Needs a paid Apideck account (30-day trial), API key and App ID, with your systems connected. An accountant should review.
apideck-libraries/mcp20
100Hires MCP Server
MCP100Hires' official hosted ATS MCP: HR can manage candidates, job postings, pipeline stages, interview scheduling, email templates and scorecards from chat, covering its full API. Sign in via browser OAuth, nothing to install. Needs a paid 100Hires account (14-day free trial).
100Hires/mcp0
Employment Contract Templates
SkillTemplates and drafting guidance for offer letters, employment agreements and handbook policies, helping HR and legal produce consistent employment documents. Written for US employment rules; adapt them for other jurisdictions and have a lawyer review final drafts. No account or API key.
wshobson/agents40.9k
HR Legal Compliance
PluginHR and compliance plugin with HR and legal advisor subagents plus employment contract and GDPR skills, for drafting hiring materials, onboarding, offboarding and performance policies, employment contracts, privacy policies, terms of service and DPAs. Mainly US and EU rules.
wshobson/agents40.9k