Connect an agent to DYNIK
Three steps: create a key, paste one line of configuration, ask your agent to confirm which account it is on.
1. Create a key
In DYNIK, open Settings → Agents and choose Create key. Tick only what the agent needs: a key that can only read cannot change anything, whatever it is asked to do. The secret is shown once and stored only as a fingerprint, so copy it before closing the dialog.
Open DYNIK settings2. Configure your client
Claude Code
# Read the key without echoing it or storing it in shell history.
printf 'DYNIK key: ' >&2
IFS= read -r -s DYNIK_MCP_KEY
printf '\n' >&2
export DYNIK_MCP_KEY
claude mcp add-json --scope user dynik \
'{"type":"http","url":"https://api.dynik.com/api/v1/mcp","headers":{"Authorization":"Bearer ${DYNIK_MCP_KEY}"}}'Then run /mcp inside Claude Code. The configuration stores an environment-variable reference, so set DYNIK_MCP_KEY again when you start a new terminal session.
Codex CLI
In a terminal:
# In Bash or zsh: this prompt neither echoes nor records the key in history.
printf 'DYNIK key: ' >&2
IFS= read -r -s DYNIK_MCP_KEY
printf '\n' >&2
export DYNIK_MCP_KEYThen add this separately to ~/.codex/config.toml:
# ~/.codex/config.toml
[shell_environment_policy]
# The MCP transport can still read this variable; Codex shell commands cannot.
exclude = ["DYNIK_MCP_KEY"]
[mcp_servers.dynik]
url = "https://api.dynik.com/api/v1/mcp"
bearer_token_env_var = "DYNIK_MCP_KEY"
default_tools_approval_mode = "writes"Start or restart Codex from that terminal, then run codex mcp list to confirm. Set DYNIK_MCP_KEY securely whenever you start Codex; it is never stored in config.toml or passed to Codex shell commands. Codex asks before tools not marked read-only.
Cursor
// ~/.cursor/mcp.json
{
"mcpServers": {
"dynik": {
"url": "https://api.dynik.com/api/v1/mcp",
"headers": { "Authorization": "Bearer ${env:DYNIK_MCP_KEY}" }
}
}
}Export DYNIK_MCP_KEY before starting Cursor. Cursor CLI reads this same file; run cursor-agent mcp enable dynik, then cursor-agent mcp list-tools dynik to check discovery.
GitHub Copilot CLI
// ~/.copilot/mcp-config.json
{
"mcpServers": {
"dynik": {
"type": "http",
"url": "https://api.dynik.com/api/v1/mcp",
"headers": { "Authorization": "Bearer ${DYNIK_MCP_KEY}" },
"tools": ["*"]
}
}
}Export DYNIK_MCP_KEY before starting Copilot CLI, then run copilot mcp get dynik. Copilot CLI expands the environment variable in the header.
Grok CLI
grok mcp add --transport http --scope user dynik https://api.dynik.com/api/v1/mcp \
--header 'Authorization: Bearer ${DYNIK_MCP_KEY}'
grok mcp doctor dynik --jsonExport DYNIK_MCP_KEY before starting Grok. The CLI stores an environment-variable reference in its configuration.
Any other MCP client
{
"mcpServers": {
"dynik": {
"type": "http",
"url": "https://api.dynik.com/api/v1/mcp",
"headers": { "Authorization": "Bearer dnk_live_YOUR_KEY" }
}
}
}Use the HTTP transport and an Authorization bearer header in a client that supports them. Check that client’s configuration syntax before copying this generic shape.
3. Check it works
Ask the agent: “Use the dynik tools to tell me which account I am on and what you are allowed to do.” It should call get_profile and read back your email and the key’s scopes. If it cannot, the key is wrong or revoked, and the server answers 401 and says so.
Or check by hand after setting DYNIK_MCP_KEYin your shell:
curl -s https://api.dynik.com/api/v1/mcp \
-H "Authorization: Bearer $DYNIK_MCP_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "MCP-Protocol-Version: 2026-07-28" \
-H "Mcp-Method: server/discover" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "server/discover",
"params": {
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": {"name": "manual-probe", "version": "1.0"},
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}'Scopes
A key holds exactly the scopes you ticked, and a tool the key cannot use is not even listed to the agent, so it never plans around a capability it does not have. Employer scopes require an employer account; if your account loses that role, existing keys lose those scopes on the very next call.
| profile:read | Read your name, email, role and key scopes. |
| jobs:read | Search the catalogue and read jobs, matches and saved jobs. |
| jobs:write | Save and unsave jobs on your account. Requires jobs:read. |
| resumes:read | Read your uploaded resumes and their analysis. |
| resumes:write | Upload a resume file to your account, and delete one from it. Requires resumes:read. |
| applications:read | Read the applications you have submitted. |
| applications:write | Submit and update applications on your behalf. Requires applications:read. |
| cover_letters:read | Read the cover letters saved on your account. |
| cover_letters:write | Write new cover letters, which uses your plan's monthly AI allowance. Requires cover_letters:read and resumes:read. |
| employer:read | Read your job postings and the applications to them. |
| employer:write | Create, edit, publish and close postings, and move candidates. Requires employer:read. |
Job-seeker tools
| search_jobs | Search the catalogue by keyword, location, skills, salary and recency. |
| get_job | Read one job: apply URL, quick-apply and sponsorship flags, and description_truncated for syndicated postings. |
| list_job_matches | Score catalogue jobs against one of your resumes. |
| list_saved_jobs | List the jobs you have saved. |
| save_job | Save a job, optionally with a note. |
| unsave_job | Remove a job from your saved list. |
| list_applications | List your applications, filterable by status and company. |
| get_application | Read one application in full. |
| create_application | Record an application. Attaching a cover letter also needs cover_letters:read. Requires confirm: true. |
| generate_cover_letter | Write a cover letter for one job. Needs cover_letters:write and resumes:read; spends the plan's monthly AI allowance; requires confirm: true. |
| list_cover_letters | List your cover letters, without their text. |
| get_cover_letter | Read one cover letter in full. |
| list_resumes | List your uploaded resumes. |
| upload_resume | Upload a PDF, DOCX, RTF or plain-text resume, up to 1 MiB, sent inline. Parsing finishes after the call returns. |
| delete_resume | Delete a resume, its file and its analysis. Requires confirm: true. |
| get_resume_analysis | Read the parsed skills, experience and analysis for one resume. |
| get_profile | Confirm which account and scopes the key carries, and what is left of this month’s allowances. |
Employer tools
| list_job_postings | List your postings, filterable by status. |
| get_job_posting | Read one posting in full. |
| create_job_posting | Create a posting. Always created as a draft. |
| update_job_posting | Edit a posting, with an optional expected_title concurrency check. |
| publish_job_posting | Make a draft live. Requires confirm: true. |
| close_job_posting | Close a posting. Requires confirm: true. |
| list_candidates | List applicants, filterable by posting, stage and match score. |
| get_candidate | Read one application, with cover letter and screening answers. |
| update_candidate_status | Move a candidate. Rejection requires confirm: true. |
Use DYNIK from a terminal or CI job
The beta CLI is a single Python 3.11+ file with no extra packages. It uses the same scoped MCP endpoint as the agent clients above. Download it over HTTPS, verify its checksum, and provide a key through your environment or CI secret store. The key is never a command-line argument.
curl -fsS https://www.dynik.com/downloads/dynik-cli.py -o dynik-cli.py
curl -fsS https://www.dynik.com/downloads/dynik-cli.py.sha256 -o dynik-cli.py.sha256
sha256sum -c dynik-cli.py.sha256
# In CI, inject DYNIK_MCP_KEY from a scoped secret instead.
printf 'DYNIK key: ' >&2
IFS= read -r -s DYNIK_MCP_KEY
printf '\n' >&2
export DYNIK_MCP_KEY
python3 dynik-cli.py discover
python3 dynik-cli.py profile
python3 dynik-cli.py tools
python3 dynik-cli.py jobs search --query engineer --page 1 --limit 20
python3 dynik-cli.py jobs get JOB_ID
python3 dynik-cli.py call list_applications --arguments '{"page":1,"limit":20}'
# Upload a resume, then poll until it is parsed.
python3 dynik-cli.py resume upload --file ./jane-doe.pdf
python3 dynik-cli.py resume status RESUME_ID
python3 dynik-cli.py resume delete RESUME_ID --confirmFor other product tools, inspect tools for the input schema, then use call TOOL_NAME --arguments @request.json or --arguments - to read JSON from stdin. Writes still require the tool’s own scopes and confirmation fields. Output is JSON on stdout; diagnostics go to stderr. Exit codes: 0 success, 2 invalid local input, 3 authentication, 4 product or tool refusal, and 5 transport, rate-limit, or server failure. The CLI never retries a write after a timeout; inspect current state before deciding whether to repeat it.
resume upload reads the file and encodes it for you, up to 1 MiB; larger files go through the web app. Parsing finishes after the call returns, so the upload answers with status: pending and the tool to poll. Keep polling resume status until it reads completed or error, usually inside a minute, longer for a scanned resume, which takes the OCR path. A parse can also fail in a way that leaves the record pending, so that status alone means neither “still working” nor “failed”. There is deliberately no “treat it as failed after N seconds”: the pipeline is triggered asynchronously and a delayed event can be retried for hours before processing starts, so any cutoff near enough to be useful would have an agent delete a resume mid-flight and destroy the file the pipeline is reading. Do not delete a resume that is still pending, and do not upload a replacement: it counts against the plan’s resume limit. If it has not reached a terminal status long after the usual time, report it as stuck and let the person decide.
Confirm high-impact writes
Submitting an application, writing a cover letter, publishing or closing a posting, and rejecting a candidate require confirmation. Without it, the tool returns CONFIRMATION_REQUIRED describing the effect. After showing that preview to the person, call again with confirm: true to go ahead. Treat the refusal as a preview, not an error to route around.
New job postings are always created as drafts, so an agent cannot make a role live by accident. Writing a cover letter is gated for a different reason: it spends one of the plan’s monthly AI generations, which do not refill on request.
Errors
Tool failures come back as MCP tool errors carrying code, message and retryable, so an agent never has to parse prose to decide what to do next.
A JSON-RPC request ID links a call to its response; it does not prevent a duplicate write indefinitely. Identical calls with the same ID and arguments are blocked for 24 hours while the replay cache is available. If a call with idempotentHint: false times out, inspect the current state before deciding whether to call it again. Check list_applications after creating an application, list_cover_letters after writing a cover letter, list_job_postings after creating a posting, get_candidate after moving a candidate, or list_resumes after uploading a resume. Ask the account owner if the result is still unclear.
For a write you may need to retry, pass operation_key: a unique string you choose (a UUID works well). The five non-idempotent tools (create_application, create_job_posting, update_candidate_status, upload_resume and generate_cover_letter) run at most once per key: a later call with the same key and identical arguments returns the remembered outcome (replayed: true on success) instead of running again, and reusing the key with different arguments is a CONFLICT. A call whose outcome is not yet known answers OPERATION_IN_PROGRESS, retryable while the original may still be running, not retryable once it is stale. Keys are remembered for 7 days from last use. A refusal (validation, confirmation, quota, not-found) is not remembered, so the confirming retry with the same key still runs.
| VALIDATION_FAILED | An argument is wrong. The field is named. Not retryable as sent. |
| NOT_FOUND | No such record is visible to this account. |
| INSUFFICIENT_SCOPE | The key lacks a scope. The required ones are listed. |
| FORBIDDEN | The account may not do this at all. It is not a scope or a limit. Not retryable. |
| CONFIRMATION_REQUIRED | A high-impact call needs confirm: true. |
| QUOTA_EXCEEDED | A plan limit was reached. |
| RATE_LIMITED | Too many calls. Carries retry_after_seconds. |
| CONFLICT | The record changed, or is in the wrong state for this operation. |
| DUPLICATE_REQUEST | An identical non-idempotent call was received within 24 hours. Inspect state. |
| OPERATION_IN_PROGRESS | An identical call with the same operation key is still running (retryable), or never recorded an outcome (inspect state). |
| DEPENDENCY_UNAVAILABLE | A dependency failed. Inspect state if retryable is false. |
| INTERNAL_ERROR | A server error. Inspect state if retryable is false. |
| REQUEST_FAILED | A failure with no more specific code. Read the message; do not retry blindly. |
Rate limits
120 calls per minute per key, and 300 per minute from one IP address across all keys. Exceeding either returns HTTP 429 with a Retry-After header and a RATE_LIMITED body carrying retry_after_seconds. Requests are refused, never silently dropped.
Protocol details
- Transport: Streamable HTTP, POST only. The endpoint is stateless: no session id is minted, and GET and DELETE answer 405. Send one message per POST with an id for every request. Legacy clients may batch up to ten messages.
- Protocol versions:
2026-07-28(current), and2025-11-25,2025-06-18and2025-03-26for clients that still open with aninitializehandshake. - Authentication:
Authorization: Beareron every request. Never in a query string, and cookies are ignored. DYNIK currently uses scoped keys created in Settings; interactive OAuth login is not available. - Discovery:
server/discover, plus RFC 9728 metadata at/.well-known/oauth-protected-resource.
If something goes wrong
- 401 on every call. The key is wrong, revoked or expired. Check Settings → Agents; the status column says which.
- A tool you expected is missing. The key lacks its scope. Scopes cannot be added to an existing key. Create a new one and revoke the old.
- 400 with
-32022. Your client asked for a protocol version we do not speak; the error lists the ones we do. - 403 with “Origin not allowed”. Something is sending a browser
Originheader. MCP clients send none; this is the DNS-rebinding guard doing its job.
Ready?
Create a key in your DYNIK settings and paste one line into your agent.