sn-mcp-server
AGENTS.md8 starsChanged 7 months agoMITArchived repositoryView on GitHub
Stateless translation layer between AI agents and the SignNow API. Every tool response is curated through tools/models.py — never pass raw API JSON. Omit nulls, empty lists, and metadata the agent won't act on. 100% of the code is agent-written. When something comes out wrong, the fix belongs in automatic feedback (lint rules, type constraints, tests, import-linter contracts), not only in the code. Prose guidance in this file is the fallback, not the first line of defence. Before closing a…
- Reads credentials
- Installs packages
- Commits and pushes
Where files are headingNew AGENTS.md files have more rules about keeping secrets out of the code, and fewer about code style.
Of the AGENTS.md files we read, the 31,774 first seen in the last six months, against the older ones: the kind of rule that gained most share, and the kind that lost most. About files like this in general, not this one, and an estimate, since a kind is a pattern in the words.
In this repository8 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- .github/copilot-instructions.mdCopilot instructions
- git-commitSkill
- sn-architectureSkill
- sn-implementationSkill
- sn-planningSkill
- sn-spec-writingSkill
- sn-testingSkill
The file
We split this AGENTS.md into the rules it gives an agent
10 rules. About 1 is common, 2 are distinctive, and 7 are very specific to this project.
≈1
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
7
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 9
Before closing a task where the human corrected you, ask: "what rule would have prevented this?" and propose adding it.
- Distinctive: line 34
When adding a new invariant, prefer the same pattern: a contract / lint rule / parametrized test over a new line in the prose.
- Very specific: line 48
RSA key regenerated silently: If OAUTH_RSA_PRIVATE_PEM is missing, get_rsa_private_key() generates a new key each restart, invalidating all …
- Common: line 53
BearerJWT middleware bypassed in password-grant mode: When config credentials are set, HTTP endpoints have zero token validation.
- Very specific: line 56
redirect_targetexclusion: All request models override model_dump() to drop redirect_target when redirect_uri is absent. - Very specific: line 73
Add integration tests (tests/integration/) for every new tool function — one happy path, one error path per tool
- Very specific: line 74
Add API-layer tests (tests/api/) for every new signnow_client method — assert URL, headers, model type, and each error variant
- Very specific: line 76
Report progress for every API call in a pagination loop: Use ctx.report_progress(progress=loaded, total=api_total, message=f"Loading X") …
- Very specific: line 83
Import from sn_mcp_server inside signnow_client — dependency arrow is strictly downward
- Very specific: line 86
Add infrastructure-specific code (no AWS/GCP/Azure assumptions)
≈ “Common” is an estimate: at least 1 in 5 of the 40,246 AGENTS.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
SignNow MCP Server
Stateless translation layer between AI agents and the SignNow API. Every tool response is curated through tools/models.py — never pass raw API JSON. Omit nulls, empty lists, and metadata the agent won't act on.
Agent-first
100% of the code is agent-written. When something comes out wrong, the fix belongs in automatic feedback (lint rules, type constraints, tests, import-linter contracts), not only in the code. Prose guidance in this file is the fallback, not the first line of defence.
Before closing a task where the human corrected you, ask: "what rule would have prevented this?" and propose adding it.
Self-check pyramid
Three layers, each bounded by what stays fast for its trigger. The point is to catch in the same conversation what CI would catch minutes later.
| Trigger | What runs | Where defined | Budget |
git commit | ruff-format + ruff on staged files; mypy src/ (~1 s); pytest tests/unit (~4 s) | .pre-commit-config.yaml (stages: [pre-commit]) | <10 s |
git push | full pytest (unit + integration + api) with coverage ≥70 %; lint-imports full-graph boundary check | .pre-commit-config.yaml (stages: [pre-push]) | <30 s |
PR / push to main | everything above + full-repo ruff + mypy + diff-coverage ≥80 % vs origin/main, posted as sticky PR comment | .github/workflows/ci.yml | <10 min |
Install hooks with pre-commit install --install-hooks (SessionStart hook does this automatically in a fresh Claude Code session). Bypass with --no-verify only in genuine emergencies — if a hook is consistently wrong, fix the hook.
Enforced invariants
Rules in ## Boundaries below that can be checked mechanically, are — an edit that violates one fails immediately:
signnow_client → sn_mcp_serveris forbidden —import-lintercontract inpyproject.toml.sn_mcp_server.tools → starlette/uvicornis forbidden — same contract set.- Every MCP tool returns a curated Pydantic model from
sn_mcp_server.tools—tests/unit/sn_mcp_server/test_tool_response_shapes.pyparametrizes overregister_tools()and fails on rawdict/list/Anyreturns. - Static types are clean —
mypy src/instrictmode, zero errors. New# type: ignorerequires an inline reason. - Lint is clean repo-wide —
ruff check src/ tests/(no diff-only escape hatch). New# noqarequires a reason. - Coverage doesn't regress — global ≥70 % (
[tool.coverage.report] fail_under), patch ≥80 % (CIdiff-cover).
When adding a new invariant, prefer the same pattern: a contract / lint rule / parametrized test over a new line in the prose.
Commands
make upinstalls editable + starts HTTP on port 8001 (NOT 8000).sn-mcp httpand Docker use 8000.- Build requires
.git/directory —hatch-vcsreads git tags for version. Without it: version becomes0.0.0. pytest— asyncio-mode=auto, no@pytest.mark.asynciodecorators needed. Warnings are suppressed.run_tests.pyhas a side effect: runspip install -e .[test]before tests.- Three test layers:
tests/unit/(mockSignNowAPIClient),tests/integration/(mock HTTP layer, test tool→client),tests/api/(mock HTTP layer, test client method + request construction). All userespx— no real API calls. Bothintegration/andapi/conftest fixtures useSignNowConfig.model_construct()to bypass the credential@model_validator. CallingSignNowConfig()without any valid credential combination (noclient_id/client_secret, nobasic_token, nouser_email/password) raisesValidationError—model_construct()is used in tests to allow a config with all credential fields absent or placeholder.
Gotchas
- auth.py module-level side effects. Importing
auth.pytriggers: config load → RSA keygen (if no PEM) → stdout print of all config → SignNowAPIClient creation. In tests, mock or avoid importing directly. - No token caching. Password grant fires a network call on EVERY tool invocation. Can hit SignNow rate limits under load.
- RSA key regenerated silently. If
OAUTH_RSA_PRIVATE_PEMis missing,get_rsa_private_key()generates a new key each restart, invalidating all issued JWTs. No warning logged. .envread from CWD. Both config classes useenv_file=".env"relative to current working directory, not project root.- Empty env vars become defaults silently (string fields only). String field validators convert
""toNoneor default values — e.g.OAUTH_ISSUER=""silently becomeshttp://localhost:8000. Boolean fields without a custom validator (e.g.FASTMCP_STATELESS_HTTP) will raise a PydanticValidationErroron empty string. - Dead env vars in
.env.example.SIGNNOW_TOKEN,RESOURCE_HTTP_URL,RESOURCE_SSE_URLare not consumed by any config class. - Middleware order matters. Starlette wraps LIFO: actual execution is Bearer → TrailingSlash → CORS → App. Reordering in
app.pybreaks auth. - BearerJWT middleware bypassed in password-grant mode. When config credentials are set, HTTP endpoints have zero token validation.
- Both
/sseand/mcpmounted. Legacy SSE transport and modern Streamable HTTP both active./sseuses deprecated FastMCP API. - Custom CORS middleware.
_CORSMiddlewareWithExposeInPreflightaddsExpose-Headersto OPTIONS responses — required for Claude MCP client to readMcp-Session-Id. redirect_targetexclusion. All request models overridemodel_dump()to dropredirect_targetwhenredirect_uriis absent. SignNow API rejects it otherwise.signing_link.pyputs access_token in URL query string. Security concern (browser history, logs, referrer headers).upload_documentimplemented but commented out insignnow.py. Business logic indocument.pyis ready.
Known issues (need fix)
REGISTERED_CLIENTSmutable dict inauth.py. Module-level mutable state, violates stateless principle.cfgparameter unused.register_tools(mcp, cfg)passescfgtobind(), which ignores it.
Boundaries
Always
- Curate every API response through
tools/models.py— never return raw SignNow JSON - Combine related document/document_group operations into one tool — the tool picks the API path
- Include entity IDs in all error messages — name what failed, why, and which entity
- Unit test every
tools/<feature>.pyfunction with a mockedSignNowAPIClient - Add integration tests (
tests/integration/) for every new tool function — one happy path, one error path per tool - Add API-layer tests (
tests/api/) for every newsignnow_clientmethod — assert URL, headers, model type, and each error variant - Mask secrets with
_mask_secret_valuein any diagnostic output - Report progress for every API call in a pagination loop. Use
ctx.report_progress(progress=loaded, total=api_total, message=f"Loading X")before each subsequent batch so the client knows work is ongoing, not stalled. - Entity type auto-detection order: document_group first, document second. document_group is the modern entity type; document is legacy. All auto-detection across ALL tools must try
get_document_group_v2first, fall back toget_document. Exceptions must be explicitly justified in code comments. - Tool versioning: contract changes and new tools go through the latest MCP version only. Every
mcp.toolregistration carries an explicitversion=(enforced bytests/unit/sn_mcp_server/test_tool_versions.py). Released contracts are frozen: never change a v1.0 or v2.0 tool's input/output parameters in place. Any contract change (adding/removing/renaming an argument or response field) or brand-new tool is introduced as a v3.0 registration intools/signnow_v3.py, leaving the prior version registered and unchanged. FastMCP serves the highest registered version by default, so clients transparently get the newest contract while a client pinning an older version keeps the old one. Module layout:tools/signnow_v1.py(frozen v1.0 compat),tools/signnow.py(frozen v2.0),tools/signnow_v3.py(v3.0 — where new contract changes land). Business logic intools/<feature>.pyis version-agnostic and shared across versions. When a new tool has no released predecessor, register it directly at the current latest version (v3.0) rather than v2.0. - Signing reminders use the native resend endpoints.
send_invite_reminderresends pending invites the same way the SignNow web app's "Send reminder" action does: documents viaPUT /fieldinvite/{field_invite_id}/resend(one call per pending field invite), document groups viaPOST /documentgroup/{id}/groupinvite/{invite_id}/resendinvites(one call per pending signer). These reuse the invite's original email template, so the tool'ssubject/messageparameters are accepted for input-contract stability but not applied. SignNow may reject a resend with400 "could not resend"(invite not in a resendable state) — surfaced per-recipient infailed— or rate-limit it with429 "Too Many Attempts"; because the loops resend once per pending recipient, a 429 is retried (_resend_with_retry) before being recorded as failed. The retry honors the server'sRetry-Afterheader when it is short (≤_RESEND_MAX_RETRY_AFTER_SECONDS); a longerRetry-Afterfails fast (recorded as failed, retryable by the agent) rather than blocking the tool call, and when noRetry-Afteris sent it falls back to exponential backoff.Retry-Afteris parsed once in the client (_parse_retry_after) ontoSignNowAPIError.retry_after. While waiting out a backoff the wait is split into chunks of at most_RESEND_KEEPALIVE_INTERVAL_SECONDS(_backoff_sleep), each followed by a keepalivectx.report_progress(_report_resend_wait) — clients often abort a call that goes silent for more than ~2s, so a multi-second wait (e.g. an honored Retry-After) must keep emitting progress rather than block silently. (Both endpoints are undocumented but live-verified; the olderemail2"send document copy" client method remains available but is no longer used for reminders.)
Never
- Import from
sn_mcp_serverinsidesignnow_client— dependency arrow is strictly downward - Expose tokens, passwords, or PEM content in logs, errors, or tool responses
- Reference Starlette, ASGI, or transport objects in tool functions — use
TokenProviderfor auth - Add infrastructure-specific code (no AWS/GCP/Azure assumptions)
Refactoring strategy
Aggressive. When touching code, enforce current standards. Refactor adjacent issues. Don't preserve bad patterns for consistency.
Reference docs
ARCHITECTURE.md— Authoritative architectural specification: layers, dependency rules, component diagrams, cross-cutting concerns.pre-commit-config.yaml— local self-check pipeline (pre-commit + pre-push stages).github/workflows/ci.yml— CI pipeline: lint-on-diff, import-linter, pytest + coverage, diff-coverage gatepyproject.toml—[tool.importlinter]contracts encode the dependency arrows;[tool.coverage.report] fail_underis the global threshold.claude/settings.json+.claude/hooks/session-start.sh— Claude Code session bootstrap (installs dev extras, registers hooks)
Last updated: 2026-07-13
Maintained by: AI Agents under human supervision
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

