AI agents (MCP)
The same capabilities the REST API exposes are also published as Model Context Protocol tools, so an AI agent can use them directly.
A tool is not a parallel implementation: it dispatches the same handler as its REST counterpart, under the same authentication, the same scopes, the same quota and the same idempotency rules. Anything an agent cannot do over REST, it cannot do over MCP.
tools/list goes one step further than REST discovery: it returns only the tools your token holds the
scopes for. Where the OpenAPI document lists every route and refuses only on call, a tool outside your
scopes simply does not appear — called by name anyway, it answers naming the scope you are missing.
Connecting
| Endpoint | /mcp on the Staff & Go API host |
| Transport | Streamable HTTP |
| Authentication | OAuth 2.1 resource server — the same partner credentials as REST |
Discovery follows RFC 9728: GET /.well-known/oauth-protected-resource returns the resource identifier, its authorization server and the
scopes it supports. A conformant MCP client reads that and knows where to authenticate.
The token must be minted for this API's audience. We never accept a credential passed through from somewhere else, and a token issued for another audience is rejected — that is the whole point of audience binding.
Setting it up
Partner access is machine-to-machine, so the designed grant is client_credentials. Many MCP clients only
implement the interactive, redirect-based flow and cannot perform it; pasting a static Authorization
header into a config file is not a fix either, since the token expires.
We publish a small stdio proxy for exactly that: it holds your client credentials, mints and refreshes the token, and speaks plain MCP stdio to your client. Node 20 or later is the only prerequisite — the proxy has no dependencies to install.
| Download | |
|---|---|
mcp-stdio-proxy.mjs |
the proxy — the only file strictly required |
install.mjs |
registers the proxy with Claude Desktop, and writes a settings template if no config.yml is present |
install.ps1 · install.sh |
launchers for the above |
Every file is served as plain text, so you can read a script in the browser before you run it. Please do.
Settings
The installer reads its settings from a config.yml beside itself. The easiest way is to download it from
Staffngo: Applications (API), in the secret dialog of your application, the Download the MCP configuration
(config.yml) button. If no config.yml is present, the installer writes a commented template in its
place, to replace with the downloaded file or fill in by hand. Four values are yours to fill in:
| Setting | |
|---|---|
EXTERNAL_MCP_URL |
https://<your-external-api-host>/mcp |
KEYCLOAK_TOKEN_URL |
the token endpoint of your authorization server — published, with the scopes it supports, at GET /.well-known/oauth-protected-resource on the same host, so you do not have to guess it |
PARTNER_CLIENT_ID · PARTNER_CLIENT_SECRET |
the partner credentials you were issued |
There is nothing to declare about scopes: the ones granted to your application are carried by your credentials and enter the token without being asked for. That is also what keeps your configuration working the day an administrator changes what your integration may read.
One setting matters in local development only: NODE_TLS_REJECT_UNAUTHORIZED: "0", carried in the template
as a commented-out (#) line that you uncomment only against a self-signed development certificate. Node
does not read the operating system certificate store, so trusting a self-signed development certificate at
the OS level does not make Node accept it. Against a host with a real certificate, leave it commented out.
The file is flat key: value — the reader refuses nesting and lists rather than misreading them.
Claude Desktop
Four steps:
Node 20 or later.
node --versiontells you what is installed.Download your
config.ymlfrom Staffngo: Applications (API), in the secret dialog of your application, the Download the MCP configuration (config.yml) button. It already holds the four values.Download the scripts (table above) into the same folder as
config.yml, then run the installer:./install.sh # macOS / Linux .\install.ps1 # Windows (PowerShell)On Windows, if PowerShell refuses to run
install.ps1(Restrictedexecution policy, or an "unapproved publisher" prompt), runnode install.mjsinstead: Node is not subject to that policy, and it is exactly the same installer.Quit Claude Desktop completely — from the system tray, not just the window — and start it again. The configuration is only read at startup. The connector appears under the name
staffngo.
The installer copies the proxy next to Claude's own configuration and merges one entry into
claude_desktop_config.json, preserving every other key and every other server. It backs the file up first
(.bak-<timestamp>), and --dry-run shows exactly what it would change without writing anything.
Without a config.yml, the installer writes a template beside itself, tells you so and stops without an
error: replace it with the downloaded file (or fill in the four values), then run it again. That is a
fallback, not a step of the path.
Already installed the MCP before? The installer does not rename the old external entry. Delete it by
hand from claude_desktop_config.json (a .bak-<timestamp> backup is made on every write), then run the
installer again. To register several environments side by side, give each its own name with --name — for
example --name staffngo-staging --config config.staging.yml.
To do it by hand, the file lives here:
| Platform | Path |
|---|---|
| Windows — Store build (MSIX) | %LOCALAPPDATA%\Packages\Claude_*\LocalCache\Roaming\Claude\ |
| Windows — classic installer | %APPDATA%\Claude\ |
| macOS | ~/Library/Application Support/Claude/ |
| Linux | ~/.config/Claude/ |
The Store build is an MSIX package, and MSIX redirects %APPDATA% — its configuration is never under
%APPDATA%\Claude, which is why looking there finds nothing. The folder appears only after Claude Desktop
has been launched once.
Claude Code
Claude Code reads .mcp.json in the project root, under an mcpServers key:
{
"mcpServers": {
"staffngo": {
"command": "node",
"args": ["/absolute/path/to/mcp-stdio-proxy.mjs"],
"env": {
"EXTERNAL_MCP_URL": "https://<your-external-api-host>/mcp",
"KEYCLOAK_TOKEN_URL": "https://<your-authorization-server>/realms/<realm>/protocol/openid-connect/token",
"PARTNER_CLIENT_ID": "<your client id>",
"PARTNER_CLIENT_SECRET": "<your client secret>"
}
}
}
}
.mcp.json is committed by convention, so put a file holding a secret in your untracked configuration
instead.
VS Code
VS Code reads .vscode/mcp.json, whose servers live under a servers key rather than mcpServers. The
command / args / env triple is identical to the block above. Check your VS Code version's
documentation for the exact schema — it has moved more than once.
Any other stdio client
Everything above is the same three values in different wrappers. Whatever your client calls its configuration, it needs:
- command — the absolute path to your
nodeexecutable - args — the absolute path to
mcp-stdio-proxy.mjs - env — the four settings from
config.yml
Use absolute paths. Your client starts the process itself, outside any shell that would resolve a relative path, a symlink or an alias.
If the server does not appear, the proxy writes its diagnostics ([mcp-proxy] …) to stderr, never to
stdout, which carries the JSON-RPC stream. Look for them in your client's logs.
The one thing that will otherwise cost you an afternoon
Declare no scopes on the client side. The scopes granted to your application are carried by your
credentials and enter the token on their own: there is nothing to list in config.yml or in your client's
configuration. A tool whose scopes your token lacks is absent from tools/list altogether — you get no
refusal to explain it, you simply never see the tool. If an agent tells you a capability does not exist,
first check in Staffngo, under Applications (API), the scopes granted to your application.
Using it in a chat
You do not call tools yourself — you ask in plain language and the agent picks. Start by making it say what it has, which doubles as the check that your scopes came through:
What tools does the staffngo connector give you? List them with what each one does.
Then work by asking for the thing, not the tool:
| What you type | What it reaches for |
|---|---|
| What is my partner access entitled to? Which modules are active? | the partner profile |
| List the leave types available. | a referential read |
| Give me the first ten employees, with their job title and manager. | the employee list |
| Take the first one and show me their employment contracts. | employments for that employee |
| Which absences between the 1st and the 30th of September? | leave request lines, with a date window |
| And that person's leave balances? | balances for that employee |
| Show me the audit trail of what you just did. | the audit trail |
Chaining is where it earns its keep. Identifiers are opaque, so you cannot type one from memory: the natural flow is list → « take that one » → detail, and the agent carries the identifier across for you. That is the same sequence an integration has to perform; the chat just makes it comfortable.
Three things will look like faults and are not:
- A
nullfield named in arestrictedenvelope is a deliberate answer, not a failure. Ask what was withheld and why — the reason is in the payload. - A tool that is missing rather than refusing. If the agent tells you a capability does not exist, the scope is very likely absent from your token, so the tool was never offered to it. Check the scopes first.
- Write operations that predate the partner API are never exposed as tools, whatever your token holds. An agent cannot trigger them.
One caution worth stating plainly: these answers carry personal data, and in a chat they land in the conversation and stay there. To show someone the shape of a response, prefer a test record over a real file.
Tool naming and versions
Tool names are stable and carry their version when more than one exists — get_partner_profile and
get_partner_profile_v2 are distinct tools with distinct contracts. Pin the one you mean; a name never
silently changes meaning.
Safety hints
Each tool declares what it does, so an agent framework can decide whether to ask a human first:
| Hint | Meaning |
|---|---|
ReadOnly |
Does not modify anything. |
Idempotent |
Repeating the call has the same effect as making it once. |
Destructive |
Has an effect that is not trivially undone. |
OpenWorld |
Touches state beyond this API. |
Use them. A tool marked Destructive is one where an agent's retry loop has real-world consequences —
require a human confirmation, and send an Idempotency-Key so a retry cannot double the effect. See
Idempotency.
Quota
MCP calls and REST calls share one counter per tenant. An agent that polls in a loop consumes the
budget your integration also needs, and the RateLimit headers apply the same way — see
Rate limits.