SAP Business One (b1-mcp)

Secure MCP server for SAP Business One database (HANA / MS SQL) and Service Layer operations

DatabasesTypeScriptv1.5.1

b1-mcp-server

A secure Model Context Protocol server that gives an AI client (e.g. Claude Code) guarded access to SAP Business One via both the database (HANA / MS SQL) and the Service Layer OData API.

Features

  • 7 MCP tools: connect_database, execute_sql, execute_service_layer (GET), execute_service_layer_write (PATCH / POST / DELETE), execute_service_layer_write_unattended (the same writes without approval, development profiles only), get_schema_info, check_connection.
  • Multi-environment: switch between client databases at runtime via named connection profiles. No restart needed.
  • Read-only by design: the SQL path executes only SELECT. Every INSERT/UPDATE/DELETE/DROP/CREATE/ALTER/EXEC is blocked server-side with no confirmation bypass — writes are handed to a human to run in a real DB client.
  • Server-side guardrails: all raw SQL submitted to execute_sql goes through the same validation whether the user or the AI wrote it. Anonymous DO BEGIN..END blocks are classified by the most dangerous statement inside them, so a write hidden behind a trailing SELECT is still a write.
  • Audit trail: selected-profile connection attempts and database/Service Layer calls that reach policy or execution are recorded as JSON Lines, including policy denials.
  • Rate limiting per tool with sliding window.

Install

Requires Node.js 20 or newer.

npx b1-mcp-server

Or add to your Claude Code MCP config:

{
  "mcpServers": {
    "b1-db": {
      "command": "npx",
      "args": ["-y", "b1-mcp-server"]
    }
  }
}

Dependencies

Five runtime dependencies: @modelcontextprotocol/sdk, zod, @sap/hana-client and mssql (plus generic-pool, which backs the HANA connection pool). Database access is a local driver — src/db/directDb.ts — built directly on the HANA and MS SQL clients; Service Layer traffic goes through the strict-TLS adapter in src/sl/serviceLayerAdapter.ts using Node's own fetch and https. No HTTP framework, no axios, no PostgreSQL driver.

A clean install audits at found 0 vulnerabilities with no overrides block, which also means npx -y b1-mcp-server is clean — overrides never applied there, since npx runs without a root package.json.

Connection profiles

The server starts unconnected. Connections are loaded from ~/.claude/connections.json (one profile per environment) and selected at runtime by the AI via the connect_database tool.

Profile matching behavior in connect_database:

  • Exact match by id or dbName is preferred.
  • Partial matches are accepted only when they resolve to a single profile.
  • Ambiguous partial matches are rejected with a list of candidates.

Example ~/.claude/connections.json:

[
  {
    "id": "client_a_prd",
    "dbType": "hana",
    "dbServer": "10.0.0.10:30015",
    "dbName": "SBO_CLIENT_A",
    "dbUser": "B1ADMIN",
    "dbPassword": "•••",
    "slUrl": "https://10.0.0.10:50000/b1s/v2",
    "slUser": "manager",
    "slPassword": "•••",
    "slAllowedMethods": ["GET", "PATCH", "POST"]
  },
  {
    "id": "client_b_hmg",
    "dbType": "mssql",
    "dbServer": "10.0.0.20:1433",
    "dbName": "SBO_CLIENT_B_HMG",
    "dbUser": "SA",
    "dbPassword": "•••"
  }
]

Every profile requires id, dbType and dbName. Add dbServer and dbUser to configure DirectDb; add slUrl and slUser to configure Service Layer. Each configured side also requires its non-empty password (dbPassword or slPassword). Missing/empty passwords fail locally without a login attempt, avoiding an unnecessary failure that may count toward account lockout under the configured login policy. Non-empty password strings are passed through without trimming. Either side, or both sides, can be configured in one profile.

slAllowedMethods (optional) lists the HTTP verbs the Service Layer tools may send for that profile: any of GET, PATCH, POST, DELETE (case-insensitive). Without it a profile is read-only (["GET"]); every write verb must be listed explicitly. PUT is not supported, because it replaces the whole entity and wipes every field the body omits. An invalid value (unknown verb, empty list, not an array) skips that profile instead of guessing. The list is applied on connect and printed by connect_database; editing it re-logs the Service Layer on the next connect_database call. Allowing a write verb does not waive approval: every write still needs the per-call human acceptance described under the security posture.

slWriteApproval (optional) chooses who approves a write. "elicitation" (default): the server asks through an MCP approval form. "client": the server sends no form and relies on the MCP client's own permission prompt — for setups where the form cannot reach the user, such as Claude Code Remote Control, which forwards permission prompts but not MCP forms. client is honoured only when the MCP client identifies itself as Claude Code; any other client falls back to elicitation. The server cannot see that prompt, so the mode is only as strong as the client's permission settings: list mcp__<server-name>__execute_service_layer_write under permissions.ask, never allow, and do not use it with --dangerously-skip-permissions. Without that rule, answering the first prompt with "Yes, don't ask again" saves an allow rule and every later write runs unprompted. Keep production profiles on elicitation. connect_database prints the mode that applies to the current client; editing it re-logs the Service Layer.

"none" is for development companies where a prompt on every write slows the work down. execute_service_layer_write behaves as in client mode, and the profile also unlocks execute_service_layer_write_unattended, which runs the same writes with no approval at all, for any MCP client. That tool refuses every profile not set to "none", so you can list mcp__<server-name>__execute_service_layer_write_unattended under permissions.allow and keep the normal write tool under permissions.ask: development profiles run unprompted and every other profile still prompts. The opt-in lives only in the connections file, so it is only as safe as that file: never set "none" on a profile that points at production data.

Both SAP Service Layer roots are supported: /b1s/v1 for OData v3 and /b1s/v2 for OData v4. Login, health checks and relative Service Layer requests use the exact root configured by the profile. Request validation is version-neutral; callers pass only the relative endpoint (for example, Items?$select=ItemCode&$top=1). OData query and payload differences remain the caller's responsibility; configure the root supported by the target SAP B1 installation. SAP deprecated OData v3 starting with SAP Business One 10.0 FP 2405 and made OData v4 the primary protocol, so prefer /b1s/v2 when the target installation supports it; see the Service Layer API reference.

Security model

The server does not intentionally interpolate profile passwords into MCP tool responses or audit entries. Passwords are read from ~/.claude/connections.json inside the server process and passed directly to the configured database or Service Layer endpoint for login. The Service Layer adapter retains only the session cookie afterward, not the username or password. Database-driver and Service Layer error messages are returned and audited verbatim for diagnosis, however, so there is no general-purpose secret redaction boundary around arbitrary upstream error text. Do not place secrets in profile IDs, database names, URLs or other non-password metadata. This protocol-level protection also does not stop an AI host that has unrestricted same-user shell or filesystem access from opening the file directly; see the hook guidance below.

Service Layer connections require HTTPS with normal certificate validation by default. For an internal certificate authority, set NODE_EXTRA_CA_CERTS to the CA bundle before starting the MCP process.

For a legacy SAP installation whose certificate cannot be renewed, the first connect_database call inspects TLS without sending credentials and requests explicit approval through MCP form elicitation. An accepted certificate is stored by canonical https://host:port origin in ~/.claude/service-layer-trust.json (mode 0600), so profiles sharing one SL endpoint share one pin. The file is created after explicit approval or after a matching legacy manual profile pin is migrated successfully, and contains no credentials. A changed certificate requires approval again; clients without elicitation support fail closed. Legacy manual profile pin fields remain accepted only for that one-time migration into this local store.

Once a certificate is pinned, that exact fingerprint is the whole check — hostname verification is not applied on top of it. SAP B1 issues its self-signed Service Layer certificate to the machine's short name (DNS:MYSERVER) while clients normally connect by FQDN, so re-verifying the name would reject an already-approved certificate with no way to accept it. slTlsServerName is therefore only the SNI name sent during the handshake, for hosts that serve a different certificate per name; it is not needed to make a pinned FQDN connection work. Service Layer URLs may use DNS names, IPv4 or IPv6. For an IP URL, SNI is omitted unless slTlsServerName supplies a DNS hostname; a legacy IP-valued slTlsServerName is accepted as a no-op. Older trust-store records may still carry serverName as a compatibility fallback, but new SNI configuration belongs in the connection profile rather than in certificate trust metadata.

Profiles reload on every connect_database call. A DB-only profile can gain slUrl, slUser and slPassword while its DB connection is active; the next call keeps that DB connection and initializes only Service Layer.

Global certificate verification disablement is never supported: the adapter refuses to connect when NODE_TLS_REJECT_UNAUTHORIZED=0 is present.

What the AI does see when it calls connect_database with "list":

  • profile id
  • dbName, dbType
  • a capability flag (DB, SL, DB+SL)
  • an active marker when either configured side is currently connected

Recommendations for your connections.json:

  • Lock the file to your user only.
    • Linux/macOS: chmod 600 ~/.claude/connections.json.
    • Windows: icacls "%USERPROFILE%\.claude\connections.json" /inheritance:r /grant:r "%USERNAME%:F".
  • Do not commit it to any git repository or share it via cloud sync.
  • On POSIX systems, the server fails closed and refuses to load a profile file that grants any permission to group or other users.
  • Each user maintains their own copy. Credentials never come from this npm package.

Claude Code hook and sandbox guidance

Use a centrally managed Claude Code PreToolUse hook or equivalent host policy to deny AI-issued access to the canonical resolved path of connections.json. Cover every filesystem-capable tool (Read, Grep, Glob, edit/write tools and filesystem MCP servers), not only literal filename matches. Shell commands are the difficult case: matching the path text is not sufficient because commands, working directories and symlinks can access it indirectly. Prefer a host filesystem sandbox that denies the credential path; otherwise require approval for shell access outside the workspace.

The hook/policy must live outside AI-writable project and user settings. For Claude Code enterprise deployments, install it in the platform's managed settings so project instructions cannot override it. Deny AI writes to service-layer-trust.json as well; reading that file is not secret, but its integrity controls which legacy certificate is trusted.

Hooks are defense in depth, not an OS security boundary. They govern tool calls issued through the AI host; they cannot stop this MCP process, another local process, or a malicious npm package running as the same OS user from reading a mode-0600 file. Strong isolation requires a dedicated OS identity, sandbox, or remotely hosted credential broker. See the Claude Code hooks and server-managed settings guidance.

Configuration (env vars)

All optional.

There is no .env file. The server reads process.env directly and never loads a dotenv file, so dropping a .env next to it does nothing. For a stdio MCP server the client owns the environment — set variables in the env block of your MCP configuration, which the client passes to the spawned process:

{
  "mcpServers": {
    "b1-db": {
      "command": "npx",
      "args": ["-y", "b1-mcp-server"],
      "env": {
        "MCP_QUERY_TIMEOUT_MS": "120000",
        "MCP_SL_WRITES_ENABLED": "false"
      }
    }
  }
}

Credentials are not configured this way — they live only in connections.json, which is resolved from your home directory and therefore found regardless of where npx runs from.

VariableDefaultPurpose
MCP_CONNECTIONS_FILE~/.claude/connections.jsonPath to profile file
MCP_AUDIT_LOG_PATH~/.claude/logs/b1-mcp-audit.jsonlAudit log JSONL file. Unset or empty uses the default path
MCP_LOG_LEVELinfoStderr verbosity: debug / info / warn / error. JSONL file logging is unaffected
MCP_MAX_QUERY_LENGTH8000Max SQL length in characters
MCP_RATE_LIMIT_MAX_CALLS60Max calls per tool per window
MCP_RATE_LIMIT_WINDOW_MS60000Rate-limit window
MCP_QUERY_TIMEOUT_MS60000Statement ceiling. A query still running at this point is aborted. Raise it if a legitimate report needs longer
MCP_SL_TIMEOUT_MS30000Service Layer login/request timeout
MCP_SL_TRUST_FILE~/.claude/service-layer-trust.jsonLocal non-secret certificate trust store
MCP_SL_MAX_URL_LENGTH2048Maximum relative OData URL length
MCP_SL_MAX_BODY_CHARS50000Maximum serialised PATCH/POST body length
MCP_SL_WRITES_ENABLEDtrueEmergency kill switch for every Service Layer write (PATCH, POST, DELETE); true/false are case-insensitive and invalid values disable writes. Replaces MCP_SL_PATCH_ENABLED, which is no longer read — except that any value other than true in the old name still disables writes, so an upgrade never re-opens them
MCP_ELICITATION_TIMEOUT_MS120000How long a human gets to answer an approval form (certificate trust, Service Layer write) before it fails closed
MCP_MAX_RESULT_ROWS500Max rows returned to the model; extra rows are cut and announced
MCP_MAX_RESULT_CHARS100000Max characters of result JSON, applied after the row cap
MCP_DRY_RUNfalseThe exact value true validates raw SQL and Service Layer writes without executing them; connections and Service Layer GET still run

The numeric limits above must be positive integers. An unset or whitespace-only value uses the default. Any other invalid value (including 0, a negative or a non-integer) reports the fallback on stderr and uses the default — a misconfigured limit never means "no limit". MCP_LOG_LEVEL likewise uses info when unset/empty and reports a fallback for any other invalid value.

Guardrail summary

The SQL path is read-only, so the live answer does not depend on which table you touch:

OperationAny table (SAP core, UDT, custom, temp)
SELECTallow, subject to the SELECT-specific restrictions below
INSERT / UPDATE / DELETE / DROPblock — run it yourself in a DB client
CREATE / ALTERblock
EXEC / EXECUTE / CALLblock — stored procedures cannot be run through this server

Also blocked: multi-statement queries (semicolons outside BEGIN..END), unterminated quotes or brackets, sequence advancement (NEXTVAL / NEXT VALUE FOR), OPENQUERY / OPENROWSET / OPENDATASOURCE, SELECT ... INTO <table>, and high-impact SQL Server lock hints (UPDLOCK, XLOCK, TABLOCK, TABLOCKX, HOLDLOCK, SERIALIZABLE, REPEATABLEREAD).

Service Layer requests allow only the verbs in the profile's slAllowedMethods (default GET only); PUT is never available. PATCH and DELETE require one directly keyed entity endpoint; POST accepts an entity set, a service operation or one action on a keyed entity (Orders(12)/Close); writes take no query options. Writes go through a separate tool, execute_service_layer_write, so a client can gate them by tool name while GETs stay unprompted. Every write names its target database, which must match the connected Service Layer. By default every write requires explicit user acceptance through MCP form elicitation. The approval screen is bound to the database, Service Layer root, endpoint, exact body, field list and SHA-256 body hash. A client without elicitation support cannot write. A profile with slWriteApproval: "client" replaces the form with Claude Code's permission prompt, and one with slWriteApproval: "none" also accepts unapproved writes through execute_service_layer_write_unattended (see the profile fields above). MCP_DRY_RUN=true never executes a write, and MCP_SL_WRITES_ENABLED=false disables every write globally.

The per-table classification model (SAP_CORE / SAP_USER / CUSTOM / TEMP with per-operation rules) still exists in src/guardrails/rules/ and is fully tested, but on the read-only server it is not reachable from the live path. See SECURITY.md for the full model and its known limitations.

Agent skill

skills/b1-mcp-server/SKILL.md teaches an AI agent how to use these tools: connect first, what each refusal means and what to do about it, why a HANA timeout reports a dropped socket, how truncated results look, and how Service Layer writes are approved. To install it in Claude Code, copy the folder into ~/.claude/skills/; other agents that read SKILL.md files take it from their own skills directory.

Development

npm install
npm run build
npm run test
npm run test:watch
# Optional aliases (same behavior, useful if you prefer explicit naming):
npm run test:bash
npm run test:watch:bash

From a source checkout only, run this quick connection sanity check using a profile from connections.json (scripts/ is not shipped in the npm tarball):

npx tsx scripts/test-connection.ts <profile-id>

License

MIT — see LICENSE.

Installation

Source-derived launch command. Check the maintainer’s required arguments and credentials before running:

bash
npx -y b1-mcp-server

Set up in your AI client

Merge this template into ~/Library/Application Support/Claude/claude_desktop_config.json. Keep existing servers. Add any arguments, credentials, and permissions required by the maintainer; this template has not been install-tested.

json
{
  "mcpServers": {
    "io-github-bersimao-b1-mcp-server": {
      "command": "npx",
      "args": [
        "-y",
        "b1-mcp-server"
      ]
    }
  }
}

Restart Claude Desktop completely for changes to take effect. Confirm the server appears connected in the client’s tool list, then try a read-only example from its documentation.

Claude Desktop setup reference

Package

b1-mcp-servernpm

Compatible MCP Clients

SAP Business One (b1-mcp) works with any MCP-compatible client. Copy the config snippet from the Configuration section above and add it to the file shown for your client, then restart the application.

  • Claude Desktop~/Library/Application Support/Claude/claude_desktop_config.jsonRestart Claude Desktop completely for changes to take effect.
  • Cursor~/.cursor/mcp.jsonRestart Cursor for changes to take effect.
  • VS Code.vscode/mcp.jsonReload VS Code window for changes to take effect.
  • Windsurf~/.codeium/windsurf/mcp_config.jsonRestart Windsurf for changes to take effect.
  • Claude Code.mcp.jsonSave at the project root, then start Claude Code in that project and review the MCP server approval prompt. Keep real credentials out of shared files.

Learn More