Skip to content

feat: migrate REST → GraphQL, add CI/CD (v0.2.0)#1

Open
alexandresaiz wants to merge 2 commits intomainfrom
feat/graphql-migration
Open

feat: migrate REST → GraphQL, add CI/CD (v0.2.0)#1
alexandresaiz wants to merge 2 commits intomainfrom
feat/graphql-migration

Conversation

@alexandresaiz
Copy link
Copy Markdown

@alexandresaiz alexandresaiz commented Apr 9, 2026

Summary by CodeRabbit

  • New Features

    • Switched backend operations to GraphQL; added payments KPI and send-payment-link tools.
    • New MCP server package and Docker support.
  • Improvements

    • Simplified OAuth/PKCE state handling and streamlined middleware.
    • Revised rate limiting and audit logging behavior.
  • Infrastructure

    • Added CI matrix, scheduled schema-check workflow, Dependabot config, and deployment health checks.
  • Documentation

    • New README, CONTRIBUTING, SECURITY, and CHANGELOG entries.
  • Tests

    • Updated/added test suites reflecting new client and tooling behavior.

@alexandresaiz alexandresaiz requested a review from jimmyn April 9, 2026 12:45
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 9, 2026

📝 Walkthrough

Walkthrough

Transition from a REST-based MCP server to a GraphQL-native v0.2.0 design: new GraphQL client and tools, simplified OAuth/PKCE state handling, consolidated tool definitions and dispatch, streamlined middleware/audit/rate-limiter, updated CI/deployment, and substantial test-suite rework.

Changes

Cohort / File(s) Summary
Repository config & CI
.github/workflows/ci.yml, .github/workflows/schema-check.yml, .github/dependabot.yml, railway.json, Dockerfile, .dockerignore, .gitignore, .node-version
Add CI matrix and weekly GraphQL schema-check workflow; Dependabot config; Dockerfile and dockerignore; update railway startup/healthcheck; add ignore rules and Node version change.
Root package & metadata
package.json, CHANGELOG.md, README.md, CONTRIBUTING.md, SECURITY.md
Bump to v0.2.0, update description/homepage/keywords; extensive README, changelog, contributing, and security documentation revisions.
MCP server subpackage
MONEI-MCP-Server/package.json, MONEI-MCP-Server/tsconfig.json, MONEI-MCP-Server/vitest.config.ts, MONEI-MCP-Server/.env.example, MONEI-MCP-Server/LICENSE, MONEI-MCP-Server/README.md, MONEI-MCP-Server/SECURITY.md, MONEI-MCP-Server/.gitignore, MONEI-MCP-Server/.node-version, MONEI-MCP-Server/railway.json
Add MCP subpackage with ESM package manifest, scripts, TypeScript config, Vitest config (80% coverage thresholds), env example, license, README, security doc, and deployment hints.
GraphQL client
src/api/monei-client.ts
Replace REST MoneiApiClient with MoneiGraphQLClient; add execute/introspect and typed input/filter interfaces; new GraphQL-based methods for account, charges, payments, subscriptions, and KPIs.
Auth / PKCE / Session
src/auth/oauth.ts, src/auth/pkce.ts, src/auth/session.ts
Simplify OAuth flow: external-state-driven getAuthorizationUrl, remove refresh/expiry/scope validation, introduce generateState(), simplify PKCE verifier generation, replace session-based PKCE store with a TTL stateStore and validateAndConsumeState.
Server & HTTP layer
src/index.ts, src/server.ts, src/types/index.ts
Replace MCP server class usage with lower-level Server handlers; remove session-reuse endpoints; export app/config named exports; switch config to moneiGraphqlEndpoint; change server-level tool dispatch to instantiate MoneiGraphQLClient.
Middleware & logging
src/middleware/security.ts, src/middleware/rate-limiter.ts, src/middleware/audit-logger.ts
Replace detailed CORS/HTTPS/session validators with corsHeaders, securityHeaders, and bodySizeGuard; simplify rate limiter API and state; replace audit context API with withAudit and in-memory recent-entry retrieval; adjust exported types/shapes.
Tools & routing
src/tools/index.ts, src/tools/payments.ts, src/tools/subscriptions.ts, src/tools/account.ts
Consolidate tool definitions into arrays and ALL_TOOL_DEFINITIONS; replace per-tool Zod schemas with JSON-schema-like descriptors; add get_payments_kpi and send_payment_link; add per-domain handler dispatch (handlePaymentTool, handleSubscriptionTool, handleAccountTool) using MoneiGraphQLClient; unify restricted-operation checks.
Types & restrictions
src/types/index.ts
Remove domain model types (payments/subscriptions/etc.); replace string-union restricted list with RestrictedOperation objects and isRestricted lookup; change ServerConfig property to moneiGraphqlEndpoint.
Tests — added and removed
tests/api/monei-client.test.ts, tests/auth/auth.test.ts, tests/middleware/middleware.test.ts, tests/tools/tools.test.ts, (many deleted tests listed in diff)
Add GraphQL client, PKCE/state, middleware, and tools integrity tests; remove older RFC-7636 PKCE tests, scope/session tests, many middleware and tool routing/validation tests reflecting prior architecture.
Miscellaneous files
.env.example, CHANGELOG.md, SECURITY.md, CONTRIBUTING.md
Env example updated to API-key first flow; new changelog and contributor/security docs added.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client (MCP inspector / UI)
    participant Server as MCP Server
    participant ClientLib as MoneiGraphQLClient
    participant GraphQL as Monei GraphQL API

    Client->>Server: POST /call-tool { toolName, args, apiKey? }
    Server->>ClientLib: new MoneiGraphQLClient({ apiKey, endpoint })
    Server->>ClientLib: execute(graphqlQuery, variables)
    ClientLib->>GraphQL: HTTP POST /graphql (Authorization: ApiKey ...)
    GraphQL-->>ClientLib: 200 { data / errors }
    ClientLib-->>Server: parsed data or thrown error
    Server-->>Client: 200 { content } or 4xx/5xx error
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

✨ Finishing Touches
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch feat/graphql-migration

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@MONEI-MCP-Server/.env.example`:
- Around line 16-18: The .env example documents MONEI_API_BASE_URL but the
runtime reads MONEI_GRAPHQL_ENDPOINT (see src/index.ts), causing
misconfiguration; update the environment template to either replace
MONEI_API_BASE_URL with MONEI_GRAPHQL_ENDPOINT (set to the GraphQL URL, e.g.
https://api.monei.com/graphql) or add MONEI_GRAPHQL_ENDPOINT alongside the
existing key and include an explanatory comment so the runtime picks up the
correct variable name referenced in src/index.ts.

In `@MONEI-MCP-Server/.node-version`:
- Line 1: Remove the stale MONEI-MCP-Server subdirectory and its leftover files
(including the subdirectory's .node-version containing "22.14.0" and the v0.1.0
package.json) from the repository, commit the deletion, and ensure the root
.node-version (20) and root package.json v0.2.0 remain as the single source of
truth; also check and update any CI/config references that point to
MONEI-MCP-Server so they no longer reference the removed directory.

In `@MONEI-MCP-Server/railway.json`:
- Around line 5-8: The deploy config uses startCommand "node dist/index.js" but
buildCommand currently skips building, so ensure the build produces the dist
artifacts or change startup to run the source directly; specifically update
buildCommand to run your project's build step (e.g., run the package.json build
script to emit dist) or alter startCommand to point to the compiled output you
actually produce (or to a runtime entry like src/index.js) so that dist/index.js
exists at deploy time; modify the "buildCommand" or "startCommand" entries
accordingly and keep names buildCommand, startCommand, and dist/index.js in
sync.

In `@MONEI-MCP-Server/SECURITY.md`:
- Around line 49-53: The SECURITY.md claims rate limiting and audit logging are
active but src/server.ts currently calls handleToolCall directly; import and
apply the existing middleware from src/middleware/rate-limiter.ts (e.g.,
rateLimiter) and src/middleware/audit-logger.ts (e.g., auditLogger or
auditLogger.log) into the request flow that invokes handleToolCall so requests
pass through rateLimiter and are recorded by auditLogger before/after
handleToolCall, or if you opt not to enable them, update SECURITY.md to remove
or revise the Operational Security bullets to reflect the actual implemented
controls; ensure you reference the existing symbols rateLimiter, auditLogger and
handleToolCall when making the change.
- Around line 36-40: SECURITY.md currently lists only five operations and
misnames capturePayment; update the document to list all eight operations
defined in src/types/index.ts — refundPayment, capturePayment, cancelPayment,
cancelSubscription, updateAccount, deleteWebhook, createApiKey, deleteApiKey —
and provide accurate one-line descriptions for each (e.g., capturePayment:
capture an authorized payment to finalize the charge; cancelPayment: void or
cancel an in-flight authorization; refundPayment: issue refunds;
cancelSubscription: cancel/delete subscriptions; updateAccount: modify account
settings; deleteWebhook: remove webhook endpoints; createApiKey: generate new
API keys; deleteApiKey: revoke existing API keys). Ensure the terminology
matches the function intent and replace the misleading "Charge cards or Bizum"
wording with the precise description for capturePayment.

In `@README.md`:
- Around line 143-145: Update the README middleware list to reflect current
implementation: change the description for src/middleware/security.ts to mention
bodySizeGuard (e.g., "body size guard") instead of "input guard", and update
src/middleware/rate-limiter.ts description to indicate it uses a fixed-window
limiter (e.g., "fixed-window per-account limiter") rather than "sliding window";
ensure the entries reference the actual exported symbol names like bodySizeGuard
and the rate-limiter module so the docs match v0.2.0 behavior.

In `@src/middleware/security.ts`:
- Around line 3-8: The corsHeaders middleware currently sets
Access-Control-Allow-Origin to "*" which opens browser-facing routes to any
origin; update the corsHeaders function to enforce a configured whitelist (e.g.,
from process.env.ALLOWED_ORIGINS or app config), check the incoming
req.headers.origin and if it matches the whitelist set
Access-Control-Allow-Origin to that origin (otherwise omit the header or return
a 403 for disallowed origins), keep Access-Control-Allow-Methods/Headers and
handle OPTIONS responses as now; reference the corsHeaders middleware to locate
and replace the wildcard-origin behavior with origin validation against the
whitelist.
- Around line 20-24: The bodySizeGuard middleware cannot prevent large bodies
because express.json() already consumes the stream and content-length is
untrusted; instead either (A) move the bodySizeGuard registration to run before
express.json() in your middleware chain (so bodySizeGuard sees the raw request
stream) and change bodySizeGuard to count bytes from the incoming request stream
rather than trusting req.headers["content-length"], or (B) replace the custom
guard with express.json({ limit: <maxBytes> }) so express itself enforces the
limit; update references to bodySizeGuard and ensure whichever approach you pick
is applied before the JSON body parser (express.json) in the server setup.

In `@src/tools/payments.ts`:
- Line 95: The call in the "list_payments" case uses the `||` operator so
explicit zero values are treated as falsy and replaced (e.g., `(args.limit as
number) || 20`), losing an intentional `limit: 0`; change these to use nullish
coalescing or an explicit undefined check (e.g., `(args.limit as number) ?? 20`
or `args.limit !== undefined ? args.limit : 20`) and do the same for
`args.offset` so `client.listCharges` receives a preserved 0 when callers pass
it.

In `@src/tools/subscriptions.ts`:
- Line 35: The return in the "list_subscriptions" case uses logical OR which
overwrites explicit zero values; update the arguments passed to
client.listSubscriptions so that size uses nullish coalescing for args.limit and
from uses nullish coalescing for args.offset (i.e., keep explicit 0 from
args.limit/args.offset but fall back to 20 and 0 only when those fields are null
or undefined), referencing the "list_subscriptions" case and the
args.limit/args.offset parameters used for size and from.

In `@tests/middleware/middleware.test.ts`:
- Around line 42-55: The tests for withAudit currently only check return/throw
and not that an audit record was emitted; update both specs to assert the audit
logging was invoked: in the "withAudit wraps successful calls" test, after
calling withAudit("get_account_info", ..., async () => ({ name: "Test" })),
assert the console (or the audit logger) spy was called with an entry containing
the action "get_account_info", the account id "acc_1", the input/context object,
and the successful result; in the "withAudit logs errors and re-throws" test,
after expecting the rejection for withAudit("get_payment", { paymentId: "bad" },
"acc_1", ...), assert the console/audit spy was called with an entry containing
action "get_payment", account "acc_1", the context payload, and the error
message "Not found". Ensure you keep the existing spy setup/restore
(vi.spyOn(console, "log").mockImplementation(...)) and use its call assertions
to validate the audit payload rather than only checking return values.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e3f1f5de-20d6-432c-9c13-7a68b85a0586

📥 Commits

Reviewing files that changed from the base of the PR and between 93c39e3 and 58b051a.

⛔ Files ignored due to path filters (58)
  • MONEI-MCP-Server/package-lock.json is excluded by !**/package-lock.json
  • dist/api/monei-client.d.ts is excluded by !**/dist/**
  • dist/api/monei-client.d.ts.map is excluded by !**/dist/**, !**/*.map
  • dist/api/monei-client.js is excluded by !**/dist/**
  • dist/api/monei-client.js.map is excluded by !**/dist/**, !**/*.map
  • dist/auth/oauth.d.ts is excluded by !**/dist/**
  • dist/auth/oauth.d.ts.map is excluded by !**/dist/**, !**/*.map
  • dist/auth/oauth.js is excluded by !**/dist/**
  • dist/auth/oauth.js.map is excluded by !**/dist/**, !**/*.map
  • dist/auth/pkce.d.ts is excluded by !**/dist/**
  • dist/auth/pkce.d.ts.map is excluded by !**/dist/**, !**/*.map
  • dist/auth/pkce.js is excluded by !**/dist/**
  • dist/auth/pkce.js.map is excluded by !**/dist/**, !**/*.map
  • dist/auth/session.d.ts is excluded by !**/dist/**
  • dist/auth/session.d.ts.map is excluded by !**/dist/**, !**/*.map
  • dist/auth/session.js is excluded by !**/dist/**
  • dist/auth/session.js.map is excluded by !**/dist/**, !**/*.map
  • dist/index.d.ts is excluded by !**/dist/**
  • dist/index.d.ts.map is excluded by !**/dist/**, !**/*.map
  • dist/index.js is excluded by !**/dist/**
  • dist/index.js.map is excluded by !**/dist/**, !**/*.map
  • dist/middleware/audit-logger.d.ts is excluded by !**/dist/**
  • dist/middleware/audit-logger.d.ts.map is excluded by !**/dist/**, !**/*.map
  • dist/middleware/audit-logger.js is excluded by !**/dist/**
  • dist/middleware/audit-logger.js.map is excluded by !**/dist/**, !**/*.map
  • dist/middleware/rate-limiter.d.ts is excluded by !**/dist/**
  • dist/middleware/rate-limiter.d.ts.map is excluded by !**/dist/**, !**/*.map
  • dist/middleware/rate-limiter.js is excluded by !**/dist/**
  • dist/middleware/rate-limiter.js.map is excluded by !**/dist/**, !**/*.map
  • dist/middleware/security.d.ts is excluded by !**/dist/**
  • dist/middleware/security.d.ts.map is excluded by !**/dist/**, !**/*.map
  • dist/middleware/security.js is excluded by !**/dist/**
  • dist/middleware/security.js.map is excluded by !**/dist/**, !**/*.map
  • dist/server.d.ts is excluded by !**/dist/**
  • dist/server.d.ts.map is excluded by !**/dist/**, !**/*.map
  • dist/server.js is excluded by !**/dist/**
  • dist/server.js.map is excluded by !**/dist/**, !**/*.map
  • dist/tools/account.d.ts is excluded by !**/dist/**
  • dist/tools/account.d.ts.map is excluded by !**/dist/**, !**/*.map
  • dist/tools/account.js is excluded by !**/dist/**
  • dist/tools/account.js.map is excluded by !**/dist/**, !**/*.map
  • dist/tools/index.d.ts is excluded by !**/dist/**
  • dist/tools/index.d.ts.map is excluded by !**/dist/**, !**/*.map
  • dist/tools/index.js is excluded by !**/dist/**
  • dist/tools/index.js.map is excluded by !**/dist/**, !**/*.map
  • dist/tools/payments.d.ts is excluded by !**/dist/**
  • dist/tools/payments.d.ts.map is excluded by !**/dist/**, !**/*.map
  • dist/tools/payments.js is excluded by !**/dist/**
  • dist/tools/payments.js.map is excluded by !**/dist/**, !**/*.map
  • dist/tools/subscriptions.d.ts is excluded by !**/dist/**
  • dist/tools/subscriptions.d.ts.map is excluded by !**/dist/**, !**/*.map
  • dist/tools/subscriptions.js is excluded by !**/dist/**
  • dist/tools/subscriptions.js.map is excluded by !**/dist/**, !**/*.map
  • dist/types/index.d.ts is excluded by !**/dist/**
  • dist/types/index.d.ts.map is excluded by !**/dist/**, !**/*.map
  • dist/types/index.js is excluded by !**/dist/**
  • dist/types/index.js.map is excluded by !**/dist/**, !**/*.map
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (45)
  • .github/dependabot.yml
  • .github/workflows/ci.yml
  • .github/workflows/schema-check.yml
  • .gitignore
  • .node-version
  • MONEI-MCP-Server/.env.example
  • MONEI-MCP-Server/.gitignore
  • MONEI-MCP-Server/.node-version
  • MONEI-MCP-Server/LICENSE
  • MONEI-MCP-Server/README.md
  • MONEI-MCP-Server/SECURITY.md
  • MONEI-MCP-Server/package.json
  • MONEI-MCP-Server/railway.json
  • MONEI-MCP-Server/tsconfig.json
  • MONEI-MCP-Server/vitest.config.ts
  • README.md
  • package.json
  • railway.json
  • src/api/monei-client.ts
  • src/auth/oauth.ts
  • src/auth/pkce.ts
  • src/auth/session.ts
  • src/index.ts
  • src/middleware/audit-logger.ts
  • src/middleware/rate-limiter.ts
  • src/middleware/security.ts
  • src/server.ts
  • src/tools/account.ts
  • src/tools/index.ts
  • src/tools/payments.ts
  • src/tools/subscriptions.ts
  • src/types/index.ts
  • tests/api/monei-client.test.ts
  • tests/auth/auth.test.ts
  • tests/auth/pkce.test.ts
  • tests/auth/scopes.test.ts
  • tests/auth/session.test.ts
  • tests/middleware/audit-logger.test.ts
  • tests/middleware/middleware.test.ts
  • tests/middleware/rate-limiter.test.ts
  • tests/middleware/security.test.ts
  • tests/tools/restrictions.test.ts
  • tests/tools/routing.test.ts
  • tests/tools/tools.test.ts
  • tests/tools/validation.test.ts
💤 Files with no reviewable changes (9)
  • tests/auth/scopes.test.ts
  • tests/tools/routing.test.ts
  • tests/tools/validation.test.ts
  • tests/middleware/rate-limiter.test.ts
  • tests/auth/pkce.test.ts
  • tests/middleware/security.test.ts
  • tests/tools/restrictions.test.ts
  • tests/auth/session.test.ts
  • tests/middleware/audit-logger.test.ts

Comment thread MONEI-MCP-Server/.env.example
@@ -0,0 +1 @@
22.14.0
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check all .node-version files in the repository
fd -H '.node-version' --exec sh -c 'echo "{}:"; cat "{}"'

Repository: MONEI/MONEI-MCP-Server

Length of output: 127


🏁 Script executed:

#!/bin/bash
# Check repository structure and package.json files
echo "=== Root package.json (first 30 lines) ==="
head -30 package.json 2>/dev/null || echo "No root package.json"

echo -e "\n=== MONEI-MCP-Server package.json (first 30 lines) ==="
head -30 MONEI-MCP-Server/package.json 2>/dev/null || echo "No MONEI-MCP-Server/package.json"

echo -e "\n=== Git log for .node-version files ==="
git log --oneline -n 5 -- '.node-version' 'MONEI-MCP-Server/.node-version' 2>/dev/null || echo "No git history available"

echo -e "\n=== Repository structure ==="
ls -la | head -20

Repository: MONEI/MONEI-MCP-Server

Length of output: 2993


🏁 Script executed:

#!/bin/bash
# Look for CI configuration
find . -maxdepth 2 -type f \( -name '*.yml' -o -name '*.yaml' \) | grep -E '(github|ci|workflow)' | head -5

Repository: MONEI/MONEI-MCP-Server

Length of output: 90


🏁 Script executed:

#!/bin/bash
# Check if MONEI-MCP-Server is a submodule or regular directory
echo "=== Check .gitmodules ==="
cat .gitmodules 2>/dev/null || echo "No .gitmodules file"

echo -e "\n=== Check if MONEI-MCP-Server is a git submodule ==="
ls -la MONEI-MCP-Server/.git 2>/dev/null || echo "Not a submodule (no .git in MONEI-MCP-Server)"

echo -e "\n=== Check engines field in both package.json files ==="
echo "Root package.json engines:"
grep -A 2 '"engines"' package.json || echo "No engines field"

echo -e "\nMONEI-MCP-Server/package.json engines:"
grep -A 2 '"engines"' MONEI-MCP-Server/package.json || echo "No engines field"

echo -e "\n=== List all files in MONEI-MCP-Server directory ==="
ls -la MONEI-MCP-Server/

Repository: MONEI/MONEI-MCP-Server

Length of output: 1293


🏁 Script executed:

#!/bin/bash
# Check if MONEI-MCP-Server directory is a duplicate/copy or the actual source
echo "=== Compare file counts ==="
echo "Root directory source files (src/):"
find src -type f | wc -l

echo -e "\nMONEI-MCP-Server source files:"
find MONEI-MCP-Server -type f | wc -l

echo -e "\n=== Check git status of MONEI-MCP-Server directory ==="
git status MONEI-MCP-Server/ 2>/dev/null || echo "Directory check failed"

echo -e "\n=== Check git history for MONEI-MCP-Server directory ==="
git log --oneline -n 3 -- MONEI-MCP-Server/ 2>/dev/null | head -5

Repository: MONEI/MONEI-MCP-Server

Length of output: 404


Remove the outdated MONEI-MCP-Server subdirectory—it appears to be a leftover from the v0.1.0 to v0.2.0 migration.

The MONEI-MCP-Server/.node-version file (22.14.0) conflicts with the root .node-version (20) because the subdirectory is a stale copy. It contains package.json v0.1.0 while the root specifies v0.2.0, and the migration commit "migrate REST → GraphQL, add CI/CD (v0.2.0)" indicates the root directory is the current source of truth. This directory should be removed to eliminate the version confusion and prevent accidental use of outdated code.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@MONEI-MCP-Server/.node-version` at line 1, Remove the stale MONEI-MCP-Server
subdirectory and its leftover files (including the subdirectory's .node-version
containing "22.14.0" and the v0.1.0 package.json) from the repository, commit
the deletion, and ensure the root .node-version (20) and root package.json
v0.2.0 remain as the single source of truth; also check and update any CI/config
references that point to MONEI-MCP-Server so they no longer reference the
removed directory.

Comment on lines +5 to +8
"buildCommand": "echo 'skipping build - dist prebuilt'"
},
"deploy": {
"startCommand": "node dist/index.js",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Build is skipped while startup requires dist, which can break deployment.

startCommand runs node dist/index.js, but buildCommand never creates dist. If artifacts are not externally injected, the service will fail to boot.

🔧 Proposed fix
   "build": {
     "builder": "NIXPACKS",
-    "buildCommand": "echo 'skipping build - dist prebuilt'"
+    "buildCommand": "npm ci && npm run build"
   },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"buildCommand": "echo 'skipping build - dist prebuilt'"
},
"deploy": {
"startCommand": "node dist/index.js",
"build": {
"builder": "NIXPACKS",
"buildCommand": "npm ci && npm run build"
},
"deploy": {
"startCommand": "node dist/index.js",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@MONEI-MCP-Server/railway.json` around lines 5 - 8, The deploy config uses
startCommand "node dist/index.js" but buildCommand currently skips building, so
ensure the build produces the dist artifacts or change startup to run the source
directly; specifically update buildCommand to run your project's build step
(e.g., run the package.json build script to emit dist) or alter startCommand to
point to the compiled output you actually produce (or to a runtime entry like
src/index.js) so that dist/index.js exists at deploy time; modify the
"buildCommand" or "startCommand" entries accordingly and keep names
buildCommand, startCommand, and dist/index.js in sync.

Comment on lines +36 to +40
- Refund payments
- Charge cards or Bizum
- Card or Bizum payouts
- Cancel/delete subscriptions
- Modify account settings
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find all restricted operations defined in the codebase
rg -n "RESTRICTED_OPERATIONS" --type=ts -A 20

Repository: MONEI/MONEI-MCP-Server

Length of output: 4988


🏁 Script executed:

#!/bin/bash
# Read the SECURITY.md file
cat -n SECURITY.md

Repository: MONEI/MONEI-MCP-Server

Length of output: 2519


SECURITY.md documentation of restricted operations is incomplete and uses misleading terminology.

Lines 36-40 document only 5 restricted operations, but src/types/index.ts:31-40 defines 8:

  1. refundPayment — matches "Refund payments" ✓
  2. capturePayment — called "Charge cards or Bizum" (misleading; this is payment authorization/capture, not charging)
  3. cancelPayment — not documented in SECURITY.md
  4. cancelSubscription — matches "Cancel/delete subscriptions" ✓
  5. updateAccount — matches "Modify account settings" ✓
  6. deleteWebhook — not documented
  7. createApiKey — not documented
  8. deleteApiKey — not documented

Update SECURITY.md to list all 8 operations with accurate descriptions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@MONEI-MCP-Server/SECURITY.md` around lines 36 - 40, SECURITY.md currently
lists only five operations and misnames capturePayment; update the document to
list all eight operations defined in src/types/index.ts — refundPayment,
capturePayment, cancelPayment, cancelSubscription, updateAccount, deleteWebhook,
createApiKey, deleteApiKey — and provide accurate one-line descriptions for each
(e.g., capturePayment: capture an authorized payment to finalize the charge;
cancelPayment: void or cancel an in-flight authorization; refundPayment: issue
refunds; cancelSubscription: cancel/delete subscriptions; updateAccount: modify
account settings; deleteWebhook: remove webhook endpoints; createApiKey:
generate new API keys; deleteApiKey: revoke existing API keys). Ensure the
terminology matches the function intent and replace the misleading "Charge cards
or Bizum" wording with the precise description for capturePayment.

Comment on lines +49 to +53
### Operational Security
- Per-account sliding-window rate limiting
- Structured audit logging for every tool invocation
- Session validation on all `/messages` requests
- Automatic session cleanup on disconnect
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Documentation claims security controls that are not implemented.

The documented "Operational Security" features are not currently wired into the server:

  • Rate limiting (src/middleware/rate-limiter.ts) exists but is not integrated into the request handler in src/server.ts
  • Audit logging (src/middleware/audit-logger.ts) exists but is not called from the request handler

Per src/server.ts:42-62, handleToolCall is invoked directly without rate limit checks or audit wrapping. Either integrate these middleware into the request flow or update the documentation to reflect the actual security posture.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@MONEI-MCP-Server/SECURITY.md` around lines 49 - 53, The SECURITY.md claims
rate limiting and audit logging are active but src/server.ts currently calls
handleToolCall directly; import and apply the existing middleware from
src/middleware/rate-limiter.ts (e.g., rateLimiter) and
src/middleware/audit-logger.ts (e.g., auditLogger or auditLogger.log) into the
request flow that invokes handleToolCall so requests pass through rateLimiter
and are recorded by auditLogger before/after handleToolCall, or if you opt not
to enable them, update SECURITY.md to remove or revise the Operational Security
bullets to reflect the actual implemented controls; ensure you reference the
existing symbols rateLimiter, auditLogger and handleToolCall when making the
change.

Comment on lines +3 to +8
export function corsHeaders(_req: Request, res: Response, next: NextFunction): void {
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Session-Id");
res.setHeader("Access-Control-Max-Age", "86400");
next();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't open every browser-facing route to every origin.

In src/index.ts, Lines 23-28, this middleware is installed globally, so Access-Control-Allow-Origin: * makes /mcp, /messages, and the OAuth routes callable from any site. That weakens the origin boundary around browser-held session/token traffic.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/middleware/security.ts` around lines 3 - 8, The corsHeaders middleware
currently sets Access-Control-Allow-Origin to "*" which opens browser-facing
routes to any origin; update the corsHeaders function to enforce a configured
whitelist (e.g., from process.env.ALLOWED_ORIGINS or app config), check the
incoming req.headers.origin and if it matches the whitelist set
Access-Control-Allow-Origin to that origin (otherwise omit the header or return
a 403 for disallowed origins), keep Access-Control-Allow-Methods/Headers and
handle OPTIONS responses as now; reference the corsHeaders middleware to locate
and replace the wildcard-origin behavior with origin validation against the
whitelist.

Comment on lines +20 to 24
export function bodySizeGuard(maxBytes = 512 * 1024) {
return (req: Request, res: Response, next: NextFunction): void => {
if (process.env.NODE_ENV !== "production") {
next();
return;
}

const proto = req.headers["x-forwarded-proto"] ?? req.protocol;
if (proto !== "https") {
res.redirect(301, `https://${req.headers.host}${req.url}`);
return;
}

// HSTS header
res.setHeader(
"Strict-Transport-Security",
"max-age=63072000; includeSubDomains; preload"
);
next();
};
}

// ─── Session Validation for /messages ──────────────────────

/**
* Validates that the session ID on /messages requests
* corresponds to an active, authenticated SSE connection.
*/
export function createSessionValidator(
activeSessions: Map<string, unknown>
) {
return (req: Request, res: Response, next: NextFunction): void => {
const sessionId = req.query.sessionId as string | undefined;

if (!sessionId) {
res.status(400).json({
error: "Missing sessionId parameter",
hint: "Connect via /sse first to establish a session.",
});
return;
}

if (!activeSessions.has(sessionId)) {
res.status(401).json({
error: "Invalid or expired session",
hint: "Your session may have expired. Reconnect via /sse.",
});
return;
}

next();
};
}

// ─── Input Sanitization ────────────────────────────────────

/**
* Reject oversized payloads and non-JSON content types on POST.
*/
export function inputGuard() {
return (req: Request, res: Response, next: NextFunction): void => {
if (req.method === "POST") {
const contentType = req.headers["content-type"];
if (contentType && !contentType.includes("application/json")) {
res.status(415).json({ error: "Content-Type must be application/json" });
return;
}
}
next();
};
}

// ─── API Key Validation (development mode) ─────────────────

/**
* For development/testing: validate that a MONEI API key is present.
* In production, OAuth tokens are used instead.
*/
export function requireApiKeyInDev() {
return (_req: Request, res: Response, next: NextFunction): void => {
if (process.env.NODE_ENV === "production") {
next();
return;
}

if (!process.env.MONEI_API_KEY) {
console.warn(
"[Security] No MONEI_API_KEY set. Tool calls will fail. Set it in .env"
);
}
const len = parseInt(req.headers["content-length"] || "0", 10);
if (len > maxBytes) { res.status(413).json({ error: "Request body too large" }); return; }
next();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

bodySizeGuard cannot stop oversized bodies in the current pipeline.

In src/index.ts, Lines 23-28, express.json() runs before this middleware, so the body has already been read into memory before Line 23 executes. This check also trusts Content-Length, which means chunked requests can bypass it entirely.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/middleware/security.ts` around lines 20 - 24, The bodySizeGuard
middleware cannot prevent large bodies because express.json() already consumes
the stream and content-length is untrusted; instead either (A) move the
bodySizeGuard registration to run before express.json() in your middleware chain
(so bodySizeGuard sees the raw request stream) and change bodySizeGuard to count
bytes from the incoming request stream rather than trusting
req.headers["content-length"], or (B) replace the custom guard with
express.json({ limit: <maxBytes> }) so express itself enforces the limit; update
references to bodySizeGuard and ensure whichever approach you pick is applied
before the JSON body parser (express.json) in the server setup.

Comment thread src/tools/payments.ts
return client.createPayment(input as any);
}
case "get_payment": return client.getCharge(args.paymentId as string);
case "list_payments": return client.listCharges({ search: args.search as string | undefined, status: args.status as string | undefined, size: (args.limit as number) || 20, from: (args.offset as number) || 0 });
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, let's look at the file structure and the specific line in question
cat -n src/tools/payments.ts | sed -n '85,105p'

Repository: MONEI/MONEI-MCP-Server

Length of output: 1452


🏁 Script executed:

#!/bin/bash
# Let's also see the full tool definition around list_payments to understand the schema
cat -n src/tools/payments.ts | sed -n '1,100p'

Repository: MONEI/MONEI-MCP-Server

Length of output: 6498


Preserve explicit limit: 0 in list_payments.

Line 95 uses (args.limit as number) || 20, so if a caller explicitly passes limit: 0, it will be treated as falsy and replaced with 20. This changes the requested page size and returns a different number of payments than requested.

Suggested fix
-    case "list_payments": return client.listCharges({ search: args.search as string | undefined, status: args.status as string | undefined, size: (args.limit as number) || 20, from: (args.offset as number) || 0 });
+    case "list_payments": return client.listCharges({ search: args.search as string | undefined, status: args.status as string | undefined, size: (args.limit as number | undefined) ?? 20, from: (args.offset as number | undefined) ?? 0 });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
case "list_payments": return client.listCharges({ search: args.search as string | undefined, status: args.status as string | undefined, size: (args.limit as number) || 20, from: (args.offset as number) || 0 });
case "list_payments": return client.listCharges({ search: args.search as string | undefined, status: args.status as string | undefined, size: (args.limit as number | undefined) ?? 20, from: (args.offset as number | undefined) ?? 0 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/tools/payments.ts` at line 95, The call in the "list_payments" case uses
the `||` operator so explicit zero values are treated as falsy and replaced
(e.g., `(args.limit as number) || 20`), losing an intentional `limit: 0`; change
these to use nullish coalescing or an explicit undefined check (e.g.,
`(args.limit as number) ?? 20` or `args.limit !== undefined ? args.limit : 20`)
and do the same for `args.offset` so `client.listCharges` receives a preserved 0
when callers pass it.

): Promise<unknown> {
switch (toolName) {
case "get_subscription": return client.getSubscription(args.subscriptionId as string);
case "list_subscriptions": return client.listSubscriptions({ search: args.search as string | undefined, status: args.status as string | undefined, size: (args.limit as number) || 20, from: (args.offset as number) || 0 });
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check the file structure and read the relevant section
head -n 40 src/tools/subscriptions.ts | cat -n

Repository: MONEI/MONEI-MCP-Server

Length of output: 2141


Use nullish coalescing (??) instead of logical OR (||) to preserve explicit zero values.

Line 35 uses (args.limit as number) || 20 and (args.offset as number) || 0. This means a caller-supplied limit: 0 is silently rewritten to 20, changing the caller's explicit intent. The schema defines no minimum constraint for these fields, so 0 is a valid value that should be preserved.

Suggested fix
-    case "list_subscriptions": return client.listSubscriptions({ search: args.search as string | undefined, status: args.status as string | undefined, size: (args.limit as number) || 20, from: (args.offset as number) || 0 });
+    case "list_subscriptions": return client.listSubscriptions({ search: args.search as string | undefined, status: args.status as string | undefined, size: (args.limit as number | undefined) ?? 20, from: (args.offset as number | undefined) ?? 0 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/tools/subscriptions.ts` at line 35, The return in the
"list_subscriptions" case uses logical OR which overwrites explicit zero values;
update the arguments passed to client.listSubscriptions so that size uses
nullish coalescing for args.limit and from uses nullish coalescing for
args.offset (i.e., keep explicit 0 from args.limit/args.offset but fall back to
20 and 0 only when those fields are null or undefined), referencing the
"list_subscriptions" case and the args.limit/args.offset parameters used for
size and from.

Comment on lines +42 to +55
it("withAudit wraps successful calls", async () => {
const spy = vi.spyOn(console, "log").mockImplementation(() => {});
const result = await withAudit("get_account_info", {}, "acc_1", async () => ({ name: "Test" }));
expect(result).toEqual({ name: "Test" });
spy.mockRestore();
});

it("withAudit logs errors and re-throws", async () => {
const spy = vi.spyOn(console, "log").mockImplementation(() => {});
await expect(
withAudit("get_payment", { paymentId: "bad" }, "acc_1", async () => { throw new Error("Not found"); })
).rejects.toThrow("Not found");
spy.mockRestore();
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

withAudit tests are false positives for audit behavior.

Both cases validate return/throw but never assert that an audit record was actually emitted. The tests would still pass if logging is removed or broken.

🔧 Proposed fix
   it("withAudit wraps successful calls", async () => {
     const spy = vi.spyOn(console, "log").mockImplementation(() => {});
     const result = await withAudit("get_account_info", {}, "acc_1", async () => ({ name: "Test" }));
     expect(result).toEqual({ name: "Test" });
+    expect(spy).toHaveBeenCalledTimes(1);
+    const logged = JSON.parse(spy.mock.calls[0][0] as string);
+    expect(logged.type).toBe("audit");
+    expect(logged.tool).toBe("get_account_info");
+    expect(logged.success).toBe(true);
     spy.mockRestore();
   });

   it("withAudit logs errors and re-throws", async () => {
     const spy = vi.spyOn(console, "log").mockImplementation(() => {});
     await expect(
       withAudit("get_payment", { paymentId: "bad" }, "acc_1", async () => { throw new Error("Not found"); })
     ).rejects.toThrow("Not found");
+    expect(spy).toHaveBeenCalledTimes(1);
+    const logged = JSON.parse(spy.mock.calls[0][0] as string);
+    expect(logged.type).toBe("audit");
+    expect(logged.tool).toBe("get_payment");
+    expect(logged.success).toBe(false);
+    expect(logged.error).toBe("Not found");
     spy.mockRestore();
   });

As per coding guidelines, "**/*.test.ts: Only comment on: tests that would pass when the code is broken (false positives), or tests with actual runtime errors."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it("withAudit wraps successful calls", async () => {
const spy = vi.spyOn(console, "log").mockImplementation(() => {});
const result = await withAudit("get_account_info", {}, "acc_1", async () => ({ name: "Test" }));
expect(result).toEqual({ name: "Test" });
spy.mockRestore();
});
it("withAudit logs errors and re-throws", async () => {
const spy = vi.spyOn(console, "log").mockImplementation(() => {});
await expect(
withAudit("get_payment", { paymentId: "bad" }, "acc_1", async () => { throw new Error("Not found"); })
).rejects.toThrow("Not found");
spy.mockRestore();
});
it("withAudit wraps successful calls", async () => {
const spy = vi.spyOn(console, "log").mockImplementation(() => {});
const result = await withAudit("get_account_info", {}, "acc_1", async () => ({ name: "Test" }));
expect(result).toEqual({ name: "Test" });
expect(spy).toHaveBeenCalledTimes(1);
const logged = JSON.parse(spy.mock.calls[0][0] as string);
expect(logged.type).toBe("audit");
expect(logged.tool).toBe("get_account_info");
expect(logged.success).toBe(true);
spy.mockRestore();
});
it("withAudit logs errors and re-throws", async () => {
const spy = vi.spyOn(console, "log").mockImplementation(() => {});
await expect(
withAudit("get_payment", { paymentId: "bad" }, "acc_1", async () => { throw new Error("Not found"); })
).rejects.toThrow("Not found");
expect(spy).toHaveBeenCalledTimes(1);
const logged = JSON.parse(spy.mock.calls[0][0] as string);
expect(logged.type).toBe("audit");
expect(logged.tool).toBe("get_payment");
expect(logged.success).toBe(false);
expect(logged.error).toBe("Not found");
spy.mockRestore();
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/middleware/middleware.test.ts` around lines 42 - 55, The tests for
withAudit currently only check return/throw and not that an audit record was
emitted; update both specs to assert the audit logging was invoked: in the
"withAudit wraps successful calls" test, after calling
withAudit("get_account_info", ..., async () => ({ name: "Test" })), assert the
console (or the audit logger) spy was called with an entry containing the action
"get_account_info", the account id "acc_1", the input/context object, and the
successful result; in the "withAudit logs errors and re-throws" test, after
expecting the rejection for withAudit("get_payment", { paymentId: "bad" },
"acc_1", ...), assert the console/audit spy was called with an entry containing
action "get_payment", account "acc_1", the context payload, and the error
message "Not found". Ensure you keep the existing spy setup/restore
(vi.spyOn(console, "log").mockImplementation(...)) and use its call assertions
to validate the audit payload rather than only checking return values.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

♻️ Duplicate comments (1)
README.md (1)

172-173: ⚠️ Potential issue | 🟡 Minor

Middleware labels don’t match current implementation names.

security.ts now exposes bodySizeGuard; “input guard” is stale. Keep these labels in sync with actual exported symbols.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 172 - 173, Update the README middleware labels to
match the actual exported symbols: replace the stale "input guard" label with
the current name(s) exported from security.ts (e.g., bodySizeGuard) so the
README reflects the real implementation; ensure the line listing middleware
(currently showing "security.ts # CORS, security headers, input guard") is
changed to something like "security.ts # CORS, security headers, bodySizeGuard"
or include all exported middleware names from security.ts to keep docs and code
in sync.
🧹 Nitpick comments (2)
.env.example (1)

6-7: Reorder HOST and PORT to satisfy dotenv-linter.

dotenv-linter reports UnorderedKey here. Swap these two lines to keep lint output clean.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.env.example around lines 6 - 7, The .env example has keys out of
alphabetical order causing dotenv-linter's UnorderedKey; swap the two
environment keys so HOST appears before PORT in the file (update the lines
containing the HOST and PORT entries so they are alphabetically ordered: HOST
then PORT).
README.md (1)

16-18: Add language identifiers to fenced code blocks (MD040).

These fenced blocks are missing language tags and trigger markdownlint warnings.

Suggested markdown fix
-```
+```text
 MCP Server URL: https://mcp.monei.com/mcp

- +text
You: "Generate a €25 payment link for order #1234 — customer is alex@example.com"
...


-```
+```text
src/
...
</details>


Also applies to: 26-46, 156-177

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @README.md around lines 16 - 18, Add language identifiers to the fenced code
blocks in README.md: update the triple-backtick blocks (including the block
containing "MCP Server URL: https://mcp.monei.com/mcp" and the other example
blocks such as the conversation example, the src/ snippet, and the blocks in the
ranges referenced) to include an appropriate language tag (e.g., text, bash,
json) immediately after the opening ``` so markdownlint MD040 is satisfied;
ensure each fenced block uses the most specific tag that matches its contents.


</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>🤖 Prompt for all review comments with AI agents</summary>

Verify each finding against the current code and only fix it if needed.

Inline comments:
In @Dockerfile:

  • Around line 36-37: The HEALTHCHECK currently hardcodes localhost:3000; update
    the HEALTHCHECK instruction so it runs in a shell (use the shell form of CMD)
    and constructs the health URL using the runtime PORT environment variable with a
    fallback to 3000, e.g., have the shell read $PORT (or default to 3000) and call
    /health (use wget or curl) so the check honors the container's PORT setting;
    edit the HEALTHCHECK line that currently uses wget and localhost:3000
    accordingly.

In @README.md:

  • Line 59: The README table incorrectly lists the KPI tool as
    chargesDateRangeKPI; update the table entry to use the actual exported tool name
    get_payments_kpi to match the implementation (get_payments_kpi) so docs and code
    align.
  • Around line 133-142: The README table lists blocked operations that don't
    match the actual enforced server-side blocklist; update the README table to
    reflect the exact entries in the RESTRICTED_OPERATIONS constant (refer to the
    RESTRICTED_OPERATIONS export in src/types/index.ts) so the docs mirror the code,
    or if the intention was to block the extra operations (e.g., direct
    charge/payout names), add those exact operation names to the
    RESTRICTED_OPERATIONS array instead; ensure the README wording and the
    RESTRICTED_OPERATIONS symbol remain consistent and use the same operation
    identifiers.

In @SECURITY.md:

  • Line 16: The SECURITY.md sentence lists “charges” and “payouts” as
    hard-blocked categories but the canonical enforced list is the
    RESTRICTED_OPERATIONS set; update the sentence to exactly reflect the operations
    named in RESTRICTED_OPERATIONS (referencing the constant in src/types/index.ts)
    — replace the current category words with the concrete operation names from
    RESTRICTED_OPERATIONS or state that the hard-blocked actions are "the exact
    operations listed in RESTRICTED_OPERATIONS (see src/types/index.ts)" so the doc
    matches the enforced blocklist.
  • Line 31: The SECURITY.md claim about rate limiting is inaccurate because
    checkRateLimit from src/middleware/rate-limiter.ts is never wired into
    production paths; either remove the "Per-account sliding window" statement from
    SECURITY.md or integrate the middleware by importing checkRateLimit and
    registering it on the Express app routes (e.g., the /mcp and /sse handlers)
    and/or invoking it inside handleToolCall in src/tools/index.ts so tool calls and
    SSE streams enforce the per-account 60 req/min sliding window; update or keep
    existing tests to reflect the chosen approach.

Duplicate comments:
In @README.md:

  • Around line 172-173: Update the README middleware labels to match the actual
    exported symbols: replace the stale "input guard" label with the current name(s)
    exported from security.ts (e.g., bodySizeGuard) so the README reflects the real
    implementation; ensure the line listing middleware (currently showing
    "security.ts # CORS, security headers, input guard") is changed to something
    like "security.ts # CORS, security headers, bodySizeGuard" or include all
    exported middleware names from security.ts to keep docs and code in sync.

Nitpick comments:
In @.env.example:

  • Around line 6-7: The .env example has keys out of alphabetical order causing
    dotenv-linter's UnorderedKey; swap the two environment keys so HOST appears
    before PORT in the file (update the lines containing the HOST and PORT entries
    so they are alphabetically ordered: HOST then PORT).

In @README.md:

  • Around line 16-18: Add language identifiers to the fenced code blocks in
    README.md: update the triple-backtick blocks (including the block containing
    "MCP Server URL: https://mcp.monei.com/mcp" and the other example blocks such as
    the conversation example, the src/ snippet, and the blocks in the ranges
    referenced) to include an appropriate language tag (e.g., text, bash, json)
    immediately after the opening ``` so markdownlint MD040 is satisfied; ensure
    each fenced block uses the most specific tag that matches its contents.

</details>

<details>
<summary>🪄 Autofix (Beta)</summary>

Fix all unresolved CodeRabbit comments on this PR:

- [ ] <!-- {"checkboxId": "4b0d0e0a-96d7-4f10-b296-3a18ea78f0b9"} --> Push a commit to this branch (recommended)
- [ ] <!-- {"checkboxId": "ff5b1114-7d8c-49e6-8ac1-43f82af23a33"} --> Create a new PR with the fixes

</details>

---

<details>
<summary>ℹ️ Review info</summary>

<details>
<summary>⚙️ Run configuration</summary>

**Configuration used**: Organization UI

**Review profile**: CHILL

**Plan**: Pro

**Run ID**: `b7bad4e4-504f-4453-87e7-760ca65a496a`

</details>

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 58b051a9ba13be023d294c452532788fd8522334 and 0e7db8be8301b01a32c174b84b183c7b23d15408.

</details>

<details>
<summary>⛔ Files ignored due to path filters (56)</summary>

* `dist/api/monei-client.d.ts` is excluded by `!**/dist/**`
* `dist/api/monei-client.d.ts.map` is excluded by `!**/dist/**`, `!**/*.map`
* `dist/api/monei-client.js` is excluded by `!**/dist/**`
* `dist/api/monei-client.js.map` is excluded by `!**/dist/**`, `!**/*.map`
* `dist/auth/oauth.d.ts` is excluded by `!**/dist/**`
* `dist/auth/oauth.d.ts.map` is excluded by `!**/dist/**`, `!**/*.map`
* `dist/auth/oauth.js` is excluded by `!**/dist/**`
* `dist/auth/oauth.js.map` is excluded by `!**/dist/**`, `!**/*.map`
* `dist/auth/pkce.d.ts` is excluded by `!**/dist/**`
* `dist/auth/pkce.d.ts.map` is excluded by `!**/dist/**`, `!**/*.map`
* `dist/auth/pkce.js` is excluded by `!**/dist/**`
* `dist/auth/pkce.js.map` is excluded by `!**/dist/**`, `!**/*.map`
* `dist/auth/session.d.ts` is excluded by `!**/dist/**`
* `dist/auth/session.d.ts.map` is excluded by `!**/dist/**`, `!**/*.map`
* `dist/auth/session.js` is excluded by `!**/dist/**`
* `dist/auth/session.js.map` is excluded by `!**/dist/**`, `!**/*.map`
* `dist/index.d.ts` is excluded by `!**/dist/**`
* `dist/index.d.ts.map` is excluded by `!**/dist/**`, `!**/*.map`
* `dist/index.js` is excluded by `!**/dist/**`
* `dist/index.js.map` is excluded by `!**/dist/**`, `!**/*.map`
* `dist/middleware/audit-logger.d.ts` is excluded by `!**/dist/**`
* `dist/middleware/audit-logger.d.ts.map` is excluded by `!**/dist/**`, `!**/*.map`
* `dist/middleware/audit-logger.js` is excluded by `!**/dist/**`
* `dist/middleware/audit-logger.js.map` is excluded by `!**/dist/**`, `!**/*.map`
* `dist/middleware/rate-limiter.d.ts` is excluded by `!**/dist/**`
* `dist/middleware/rate-limiter.d.ts.map` is excluded by `!**/dist/**`, `!**/*.map`
* `dist/middleware/rate-limiter.js` is excluded by `!**/dist/**`
* `dist/middleware/rate-limiter.js.map` is excluded by `!**/dist/**`, `!**/*.map`
* `dist/middleware/security.d.ts` is excluded by `!**/dist/**`
* `dist/middleware/security.d.ts.map` is excluded by `!**/dist/**`, `!**/*.map`
* `dist/middleware/security.js` is excluded by `!**/dist/**`
* `dist/middleware/security.js.map` is excluded by `!**/dist/**`, `!**/*.map`
* `dist/server.d.ts` is excluded by `!**/dist/**`
* `dist/server.d.ts.map` is excluded by `!**/dist/**`, `!**/*.map`
* `dist/server.js` is excluded by `!**/dist/**`
* `dist/server.js.map` is excluded by `!**/dist/**`, `!**/*.map`
* `dist/tools/account.d.ts` is excluded by `!**/dist/**`
* `dist/tools/account.d.ts.map` is excluded by `!**/dist/**`, `!**/*.map`
* `dist/tools/account.js` is excluded by `!**/dist/**`
* `dist/tools/account.js.map` is excluded by `!**/dist/**`, `!**/*.map`
* `dist/tools/index.d.ts` is excluded by `!**/dist/**`
* `dist/tools/index.d.ts.map` is excluded by `!**/dist/**`, `!**/*.map`
* `dist/tools/index.js` is excluded by `!**/dist/**`
* `dist/tools/index.js.map` is excluded by `!**/dist/**`, `!**/*.map`
* `dist/tools/payments.d.ts` is excluded by `!**/dist/**`
* `dist/tools/payments.d.ts.map` is excluded by `!**/dist/**`, `!**/*.map`
* `dist/tools/payments.js` is excluded by `!**/dist/**`
* `dist/tools/payments.js.map` is excluded by `!**/dist/**`, `!**/*.map`
* `dist/tools/subscriptions.d.ts` is excluded by `!**/dist/**`
* `dist/tools/subscriptions.d.ts.map` is excluded by `!**/dist/**`, `!**/*.map`
* `dist/tools/subscriptions.js` is excluded by `!**/dist/**`
* `dist/tools/subscriptions.js.map` is excluded by `!**/dist/**`, `!**/*.map`
* `dist/types/index.d.ts` is excluded by `!**/dist/**`
* `dist/types/index.d.ts.map` is excluded by `!**/dist/**`, `!**/*.map`
* `dist/types/index.js` is excluded by `!**/dist/**`
* `dist/types/index.js.map` is excluded by `!**/dist/**`, `!**/*.map`

</details>

<details>
<summary>📒 Files selected for processing (7)</summary>

* `.dockerignore`
* `.env.example`
* `CHANGELOG.md`
* `CONTRIBUTING.md`
* `Dockerfile`
* `README.md`
* `SECURITY.md`

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (3)</summary>

* .dockerignore
* CONTRIBUTING.md
* CHANGELOG.md

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

Comment thread Dockerfile
Comment on lines +36 to +37
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD wget -qO- http://localhost:3000/health || exit 1
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Make healthcheck honor runtime PORT instead of hardcoding 3000.

Line [37] always checks localhost:3000, but the app reads PORT from env. If PORT is overridden, the container can be marked unhealthy even when the server is fine.

🔧 Proposed fix
-HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
-  CMD wget -qO- http://localhost:3000/health || exit 1
+HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
+  CMD sh -c 'wget -qO- "http://127.0.0.1:${PORT}/health" >/dev/null || exit 1'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD wget -qO- http://localhost:3000/health || exit 1
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD sh -c 'wget -qO- "http://127.0.0.1:${PORT}/health" >/dev/null || exit 1'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Dockerfile` around lines 36 - 37, The HEALTHCHECK currently hardcodes
localhost:3000; update the HEALTHCHECK instruction so it runs in a shell (use
the shell form of CMD) and constructs the health URL using the runtime PORT
environment variable with a fallback to 3000, e.g., have the shell read $PORT
(or default to 3000) and call /health (use wget or curl) so the check honors the
container's PORT setting; edit the HEALTHCHECK line that currently uses wget and
localhost:3000 accordingly.

Comment thread README.md
| **API** | GraphQL native (AppSync) | REST | REST |
| **Open source** | ✅ MIT | Partial | ❌ |
| **Safety guardrails** | Hard-blocked restricted ops | Configurable | Configurable |
| **Analytics built-in** | `chargesDateRangeKPI` tool | Separate API | Separate API |
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use the actual KPI tool name (get_payments_kpi).

The table references chargesDateRangeKPI but the implemented tool is get_payments_kpi (src/tools/payments.ts:43-58). Please align docs with the exported tool name.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` at line 59, The README table incorrectly lists the KPI tool as
chargesDateRangeKPI; update the table entry to use the actual exported tool name
get_payments_kpi to match the implementation (get_payments_kpi) so docs and code
align.

Comment thread README.md
Comment on lines +133 to +142
| Blocked Operation | Why |
|---|---|
| Refund payments | Financial risk — requires manual review |
| Capture/cancel payments | Requires authorization context |
| Charge cards/Bizum directly | Requires PCI context + cardholder consent |
| Card/Bizum payouts | Funds disbursement needs compliance controls |
| Cancel subscriptions | Destructive — affects recurring revenue |
| Modify account settings | Security-sensitive configuration |
| Create/delete API keys | Credential management |
| Delete webhooks | Can break integrations |
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Blocked-operations table is broader than the enforced blocklist.

This section lists direct charge and payout blocks, but server-side enforced restrictions are the entries in RESTRICTED_OPERATIONS (src/types/index.ts:28-46), which currently do not include explicit charge/payout operation names. Please align this table with the real enforced operations.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 133 - 142, The README table lists blocked operations
that don't match the actual enforced server-side blocklist; update the README
table to reflect the exact entries in the RESTRICTED_OPERATIONS constant (refer
to the RESTRICTED_OPERATIONS export in src/types/index.ts) so the docs mirror
the code, or if the intention was to block the extra operations (e.g., direct
charge/payout names), add those exact operation names to the
RESTRICTED_OPERATIONS array instead; ensure the README wording and the
RESTRICTED_OPERATIONS symbol remain consistent and use the same operation
identifiers.

Comment thread SECURITY.md

### Restricted Operations
The following operations are **hard-blocked at the server level** and cannot be executed through the MCP server under any circumstances:
Refunds, charges, payouts, subscription cancellations, and account modifications are **hard-blocked at the server level**. Even manually crafted tool calls are rejected with a clear message pointing to the [MONEI Dashboard](https://dashboard.monei.com). This is not configurable — it's enforced in code.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Restricted-action statement should match the enforced blocklist exactly.

This sentence includes “charges” and “payouts” as hard-blocked categories, but the enforced server list is the explicit RESTRICTED_OPERATIONS set in src/types/index.ts:28-46. Please align wording to those concrete operations.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@SECURITY.md` at line 16, The SECURITY.md sentence lists “charges” and
“payouts” as hard-blocked categories but the canonical enforced list is the
RESTRICTED_OPERATIONS set; update the sentence to exactly reflect the operations
named in RESTRICTED_OPERATIONS (referencing the constant in src/types/index.ts)
— replace the current category words with the concrete operation names from
RESTRICTED_OPERATIONS or state that the hard-blocked actions are "the exact
operations listed in RESTRICTED_OPERATIONS (see src/types/index.ts)" so the doc
matches the enforced blocklist.

Comment thread SECURITY.md
- **Body size limits** — Requests over 512KB are rejected

### Monitoring
- **Rate limiting** — Per-account sliding window (60 req/min) prevents abuse
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Searching for checkRateLimit() call sites..."
rg -n -C3 '\bcheckRateLimit\s*\('

echo
echo "Searching for rate-limiter imports..."
rg -n -C3 'from .*rate-limiter|require\(.*rate-limiter'

echo
echo "Searching for middleware wiring in server entry points..."
rg -n -C3 'app\.use|handleToolCall|/mcp|/sse|/oauth' src

Repository: MONEI/MONEI-MCP-Server

Length of output: 8060


Remove the rate limiting claim from SECURITY.md or integrate rate limiting into the request/tool path.

The rate limiter is defined in src/middleware/rate-limiter.ts and has tests, but it is not actually wired into the codebase. The checkRateLimit() function is never imported or called in any production code—only in tests. It is not registered as middleware on the Express app and not invoked during tool calls. Either integrate the rate limiter into the MCP request handlers (e.g., in the /mcp and /sse endpoints, or within handleToolCall() in src/tools/index.ts) or remove the rate limiting claim from SECURITY.md to avoid misleading users about enforced security controls.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@SECURITY.md` at line 31, The SECURITY.md claim about rate limiting is
inaccurate because checkRateLimit from src/middleware/rate-limiter.ts is never
wired into production paths; either remove the "Per-account sliding window"
statement from SECURITY.md or integrate the middleware by importing
checkRateLimit and registering it on the Express app routes (e.g., the /mcp and
/sse handlers) and/or invoking it inside handleToolCall in src/tools/index.ts so
tool calls and SSE streams enforce the per-account 60 req/min sliding window;
update or keep existing tests to reflect the chosen approach.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant