Feature: support vault mcp human in the loop with Oauth 2.x Authorization - #123
Feature: support vault mcp human in the loop with Oauth 2.x Authorization#123marcellodesales wants to merge 19 commits into
Conversation
|
Thank you for your submission! We require that all contributors sign our Contributor License Agreement ("CLA") before we can accept the contribution. Read and sign the agreement Learn more about why HashiCorp requires a CLA and what the CLA includes Have you signed the CLA already but the status is still pending? Recheck it. |
|
I still have a couple things to fix on this one and will reopen when ready |
There was a problem hiding this comment.
Pull request overview
Adds optional browser-based OAuth login to the Vault MCP HTTP transport, enabling stateless Vault authentication (LDAP/userpass/token/OIDC) and sealed bearer tokens, while also improving Vault KV/mount listing behavior and TLS trust bootstrapping for private CAs.
Changes:
- Introduces a stateless OAuth 2.1 authorization server (dynamic registration, auth-code+PKCE, token minting) plus OIDC login support and bearer middleware.
- Improves Vault tool behavior: KV list now works without
sys/mountsaccess;list_mountsfalls back tosys/internal/ui/mounts. - Adds private CA bundle bootstrap + CA selection logic, plus updated docs and a docker-compose quickstart.
Reviewed changes
Copilot reviewed 23 out of 24 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| README.md | Documents the new browser OAuth login flow, env vars, and TLS/private CA guidance. |
| pkg/tools/sys/list_mounts.go | Adds fallback mount listing via sys/internal/ui/mounts when sys/mounts is forbidden. |
| pkg/tools/kv/list_secrets.go | Updates KV list to try KV v1 then fall back to KV v2 metadata path; improves key decoding robustness. |
| pkg/tools/kv/kv_test.go | Adds tests for KV v1 listing and KV v2 fallback behavior. |
| pkg/oauth/types.go | Adds typed payload structures for sealed OAuth artifacts. |
| pkg/oauth/service.go | Adds typed token minting/parsing with TTL enforcement on top of the sealer. |
| pkg/oauth/service_test.go | Tests token mint/parse, type mismatch, and expiry enforcement. |
| pkg/oauth/securetoken.go | Implements AES-256-GCM sealing for opaque tokens (base64url encoding). |
| pkg/oauth/securetoken_test.go | Tests sealing roundtrip, wrong-key failure, invalid token handling, and key validation. |
| pkg/oauth/router.go | Wires discovery + OAuth routes and provides bearer middleware to inject Vault creds into request context. |
| pkg/oauth/pkce.go | Implements PKCE S256 challenge computation/verification. |
| pkg/oauth/pkce_test.go | Adds RFC 7636 test vector coverage for PKCE. |
| pkg/oauth/oidc.go | Implements Vault OIDC start/callback flow with shared pending-state map for cross-port callback. |
| pkg/oauth/metadata.go | Serves OAuth authorization server metadata and protected resource metadata plus MCP server card. |
| pkg/oauth/login.go | Implements the interactive login page (LDAP/userpass/token + OIDC start) and auth-code issuance. |
| pkg/oauth/handlers.go | Implements dynamic client registration, /authorize, and /token. |
| pkg/oauth/config.go | Adds env-driven OAuth config and base URL derivation logic. |
| pkg/client/vaultauth.go | Adds Vault login helpers for userpass/ldap/token lookup + Vault OIDC auth_url/callback helpers. |
| pkg/client/client.go | Adds context injection helper and TLS CA bundle selection; refactors Vault client creation to use Vault API TLS config. |
| pkg/client/cabundle.go | Adds bootstrap logic to fetch/cache the Viasat private CA bundle on startup when missing. |
| docker-compose.yaml | Adds a StreamableHTTP + optional OAuth quickstart configuration with CA bundle volume priming. |
| cmd/vault-mcp-server/main.go | Wires OAuth router/middleware, optional OIDC callback listener, CA bootstrap, and improved startup logging summary. |
| cmd/vault-mcp-server/init.go | Switches logging to JSON+stdout by default and adds LOG_LEVEL support. |
| .gitignore | Ignores local data/ directory (cert cache). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| import ( | ||
| "crypto/sha256" | ||
| "encoding/base64" | ||
| "strings" | ||
| ) | ||
|
|
||
| // CodeChallengeS256 computes the PKCE S256 challenge for a verifier. | ||
| func CodeChallengeS256(codeVerifier string) string { | ||
| sum := sha256.Sum256([]byte(codeVerifier)) | ||
| return base64.RawURLEncoding.EncodeToString(sum[:]) | ||
| } | ||
|
|
||
| // VerifyCodeChallengeS256 reports whether codeVerifier matches expectedChallenge | ||
| // under the PKCE S256 method. | ||
| func VerifyCodeChallengeS256(codeVerifier string, expectedChallenge string) bool { | ||
| got := CodeChallengeS256(codeVerifier) | ||
| return strings.EqualFold(got, expectedChallenge) | ||
| } |
| callbackServer := &http.Server{ | ||
| Addr: fmt.Sprintf(":%d", oauthCfg.OIDCCallbackPort), | ||
| Handler: oauthRouter.OIDCCallbackMux(), |
| secret, readErr := vault.Logical().Read("sys/internal/ui/mounts") | ||
| if readErr != nil { | ||
| logger.WithError(readErr).Error("Failed to list mounts via sys/internal/ui/mounts") | ||
| return mcp.NewToolResultError(fmt.Sprintf("Failed to list mounts: %v", err)), nil | ||
| } | ||
| if secret == nil || secret.Data == nil { | ||
| return mcp.NewToolResultError(fmt.Sprintf("Failed to list mounts: %v", err)), nil | ||
| } |
| vaultState := u.Query().Get("state") | ||
|
|
||
| oidcStateMap.Store(vaultState, oidcPending{ | ||
| SealedAuthState: encState, | ||
| ClientNonce: clientNonce, | ||
| ExpiresAt: time.Now().Add(r.cfg.AuthCodeTTL), | ||
| }) |
| vaultToken, err := client.OIDCCallback(ctx, r.vaultAuthParams(), r.cfg.OIDCMount, vaultState, code, pending.ClientNonce) | ||
| if err != nil { | ||
| r.logger.WithError(err).Warn("vault oidc callback failed") | ||
| // Can't render the login page from the callback port — redirect to it. | ||
| loginURL := r.cfg.BaseURL(req) + "/vault/login?auth_state=" + | ||
| url.QueryEscape(pending.SealedAuthState) + "&error=" + | ||
| url.QueryEscape("OIDC sign-in failed: "+truncateErr(err)) | ||
| // BaseURL from port 8250 would be wrong; use the configured server URL. | ||
| if r.cfg.ServerURL != "" { | ||
| loginURL = r.cfg.ServerURL + "/vault/login?auth_state=" + | ||
| url.QueryEscape(pending.SealedAuthState) + "&error=" + | ||
| url.QueryEscape("OIDC sign-in failed: "+truncateErr(err)) | ||
| } | ||
| http.Redirect(w, req, loginURL, http.StatusFound) | ||
| return | ||
| } |
| method := in.TokenEndpointAuthMethod | ||
| if method == "" { | ||
| method = "none" | ||
| } | ||
|
|
|
|
||
| writeJSON(w, tokenResponse{ | ||
| AccessToken: accessToken, | ||
| TokenType: "Bearer", | ||
| ExpiresIn: int64(time.Until(meta.ExpiresAt).Seconds()), | ||
| Scope: strings.Join(cd.Scopes, " "), | ||
| }) |
| import ( | ||
| "crypto/sha256" | ||
| "encoding/base64" | ||
| "strings" | ||
| ) | ||
|
|
||
| // CodeChallengeS256 computes the PKCE S256 challenge for a verifier. | ||
| func CodeChallengeS256(codeVerifier string) string { | ||
| sum := sha256.Sum256([]byte(codeVerifier)) | ||
| return base64.RawURLEncoding.EncodeToString(sum[:]) | ||
| } | ||
|
|
||
| // VerifyCodeChallengeS256 reports whether codeVerifier matches expectedChallenge | ||
| // under the PKCE S256 method. | ||
| func VerifyCodeChallengeS256(codeVerifier string, expectedChallenge string) bool { | ||
| got := CodeChallengeS256(codeVerifier) | ||
| return strings.EqualFold(got, expectedChallenge) | ||
| } |
| secret, readErr := vault.Logical().Read("sys/internal/ui/mounts") | ||
| if readErr != nil { | ||
| logger.WithError(readErr).Error("Failed to list mounts via sys/internal/ui/mounts") | ||
| return mcp.NewToolResultError(fmt.Sprintf("Failed to list mounts: %v", err)), nil | ||
| } |
| if secret == nil || secret.Data == nil { | ||
| return mcp.NewToolResultError(fmt.Sprintf("Failed to list mounts: %v", err)), nil | ||
| } | ||
|
|
| method := in.TokenEndpointAuthMethod | ||
| if method == "" { | ||
| method = "none" | ||
| } | ||
|
|
| oidcStateMap.Store(vaultState, oidcPending{ | ||
| SealedAuthState: encState, | ||
| ClientNonce: clientNonce, | ||
| ExpiresAt: time.Now().Add(r.cfg.AuthCodeTTL), | ||
| }) | ||
|
|
| callbackServer := &http.Server{ | ||
| Addr: fmt.Sprintf(":%d", oauthCfg.OIDCCallbackPort), | ||
| Handler: oauthRouter.OIDCCallbackMux(), |
| // Apply middleware (innermost first). The bearer middleware unseals the OAuth | ||
| // token into the context; VaultContextMiddleware then leaves those values | ||
| // intact (it only overrides when a header/env value is present). |
| # --- TLS trust (Viasat private CA) --- | ||
| VAULT_CACERT: ${VAULT_CACERT:-/company/certs/company.io.pem} | ||
| VIASAT_IO_CACERT_FILE: ${VIASAT_IO_CACERT_FILE:-/company/certs/company.io.pem} | ||
| VIASAT_IO_CACERT_URL: ${VIASAT_IO_CACERT_URL:-https://company.com/all-certs.crt} |
| func (r *Router) authorize(w http.ResponseWriter, req *http.Request) { | ||
| if req.Method != http.MethodGet { | ||
| w.WriteHeader(http.StatusMethodNotAllowed) | ||
| return | ||
| } |
This commit shows how to run the MCP server that performs a localhost OIDC authentication. When you setup the server, make sure that your server will point to your enterprise Vault and so you can run it locally. Then, as shown in README.md of the fetc-certs dir, the container that loads the local cert will download and store it in a private volume share where the Vault container can bootstrap and fully load.
| import ( | ||
| "crypto/sha256" | ||
| "encoding/base64" | ||
| "strings" | ||
| ) | ||
|
|
||
| // CodeChallengeS256 computes the PKCE S256 challenge for a verifier. | ||
| func CodeChallengeS256(codeVerifier string) string { | ||
| sum := sha256.Sum256([]byte(codeVerifier)) | ||
| return base64.RawURLEncoding.EncodeToString(sum[:]) | ||
| } | ||
|
|
||
| // VerifyCodeChallengeS256 reports whether codeVerifier matches expectedChallenge | ||
| // under the PKCE S256 method. | ||
| func VerifyCodeChallengeS256(codeVerifier string, expectedChallenge string) bool { | ||
| got := CodeChallengeS256(codeVerifier) | ||
| return strings.EqualFold(got, expectedChallenge) | ||
| } |
| logger.WithFields(log.Fields{ | ||
| "mcp_auth_secret": secret, | ||
| "hint": "set MCP_AUTH_SECRET to persist bearer tokens across restarts", | ||
| }).Info("generated MCP_AUTH_SECRET") | ||
| } |
| // Require the configured private CA bundle to be present on disk before booting. | ||
| // Bootstrap (download) should be performed out-of-process (e.g. docker-compose-viasat.yaml's | ||
| // certs-puller or scripts/fetch-secrets). | ||
| if err := requireConfiguredCACert(logger); err != nil { |
| } | ||
| } | ||
|
|
||
| return fmt.Errorf("private ca root missing: expected one of %v to exist; bootstrap it (e.g. scripts/fetch-secrets) or mount it into the container", candidates) |
| @@ -0,0 +1,75 @@ | |||
| # fetch-secrets (certs-puller) | |||
| secret, readErr := vault.Logical().Read("sys/internal/ui/mounts") | ||
| if readErr != nil { | ||
| logger.WithError(readErr).Error("Failed to list mounts via sys/internal/ui/mounts") | ||
| return mcp.NewToolResultError(fmt.Sprintf("Failed to list mounts: %v", err)), nil | ||
| } | ||
| if secret == nil || secret.Data == nil { | ||
| return mcp.NewToolResultError(fmt.Sprintf("Failed to list mounts: %v", err)), nil | ||
| } |
| oidcStateMap.Store(vaultState, oidcPending{ | ||
| SealedAuthState: encState, | ||
| ClientNonce: clientNonce, | ||
| ExpiresAt: time.Now().Add(r.cfg.AuthCodeTTL), | ||
| }) |
| - `VAULT_CACERT`: PEM CA bundle path used to verify the upstream Vault TLS cert (e.g. `/viasat/certs/viasat.io.pem`) | ||
| - `VIASAT_IO_CACERT_FILE` / `VIASAT_IO_CACERT_URL`: location and source URL of the Viasat private CA bundle; bootstrap it out-of-process (see `scripts/fetch-secrets/` or `docker-compose-viasat.yaml`) | ||
| - `VAULT_AUTH_LDAP_MOUNT` (default `ldap`), `VAULT_AUTH_USERPASS_MOUNT` (default `userpass`), `VAULT_OIDC_MOUNT` (default `oidc`), `VAULT_OIDC_ROLE` (optional): Vault auth method mounts used by the login page |
| OAuth / browser login (see [Browser OAuth](#browser-oauth-vault-login)): | ||
|
|
||
| - `MCP_AUTH_SECRET`: base64url (32-byte) key that seals OAuth tokens. Optional; if unset the server generates one at startup and logs it at INFO. Set it to keep bearer tokens valid across restarts. | ||
| - `MCP_SERVER_URL`: public base URL advertised in OAuth metadata/redirects (default: derived per-request from the `Host` / `X-Forwarded-*` headers) |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
| func testOAuthSecret() string { | ||
| key := make([]byte, 32) | ||
| return base64.RawURLEncoding.EncodeToString(key) | ||
| } |
| // External token bypass (for clients that cannot do OAuth). | ||
| if strings.TrimSpace(req.Header.Get(client.VaultHeaderToken)) != "" || | ||
| strings.TrimSpace(req.Header.Get(client.VaultToken)) != "" || | ||
| strings.TrimSpace(os.Getenv(client.VaultToken)) != "" { | ||
| next.ServeHTTP(w, req) | ||
| return | ||
| } |
| method := in.TokenEndpointAuthMethod | ||
| if method == "" { | ||
| method = "none" | ||
| } | ||
|
|
| oidcStateMap.Store(vaultState, oidcPending{ | ||
| SealedAuthState: encState, | ||
| ClientNonce: clientNonce, | ||
| ExpiresAt: time.Now().Add(r.cfg.AuthCodeTTL), | ||
| }) |
| // environment. OAuth is enabled only when MCPAuthSecret is set. | ||
| type Config struct { | ||
| // MCPAuthSecret is the base64url (32-byte) symmetric key used to seal all | ||
| // stateless OAuth tokens. Empty disables OAuth entirely. | ||
| MCPAuthSecret string |
| } | ||
| } | ||
|
|
||
| return fmt.Errorf("private ca root missing: expected one of %v to exist; bootstrap it (e.g. scripts/fetch-secrets) or mount it into the container", candidates) |
| - `MCP_AUTH_CODE_TTL`: lifetime of authorization codes / login state (default: `5m`) | ||
| - `MCP_AUTH_ACCESS_TTL`: lifetime of bearer access tokens (default: `12h`) | ||
| - `VAULT_CACERT`: PEM CA bundle path used to verify the upstream Vault TLS cert (e.g. `/viasat/certs/viasat.io.pem`) | ||
| - `VIASAT_IO_CACERT_FILE` / `VIASAT_IO_CACERT_URL`: location and source URL of the Viasat private CA bundle; bootstrap it out-of-process (see `scripts/fetch-secrets/` or `docker-compose-viasat.yaml`) |
| @@ -0,0 +1,75 @@ | |||
| # fetch-secrets (certs-puller) | |||
| if _, err := os.Stat(path); err == nil { | ||
| return path | ||
| } |
| writeJSON(w, tokenResponse{ | ||
| AccessToken: accessToken, | ||
| TokenType: "Bearer", | ||
| ExpiresIn: int64(time.Until(meta.ExpiresAt).Seconds()), | ||
| Scope: strings.Join(cd.Scopes, " "), | ||
| }) |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 31 out of 32 changed files in this pull request and generated 13 comments.
Comments suppressed due to low confidence (1)
pkg/tools/kv/read_secret.go:93
- The new optional
keyargument is not used when constructing the response. Ifkeyis intended to scope the output to a single field, apply it before marshaling; otherwise remove the parameter to avoid dead/lying API surface.
jsonData, err := json.Marshal(kvSecret.Data)
if err != nil {
logger.WithError(err).Error("Failed to marshal secret to JSON")
return mcp.NewToolResultError(fmt.Sprintf("Error marshaling JSON: %v", err)), nil
}
| logger.WithFields(log.Fields{ | ||
| "mcp_auth_secret": secret, | ||
| "hint": "set MCP_AUTH_SECRET to persist bearer tokens across restarts", | ||
| }).Info("generated MCP_AUTH_SECRET") |
| // Require the configured private CA bundle to be present on disk before booting. | ||
| // Bootstrap (download) should be performed out-of-process (e.g. docker-compose-viasat.yaml's | ||
| // certs-puller or scripts/fetch-secrets). | ||
| if err := requireConfiguredCACert(logger); err != nil { |
| } | ||
| } | ||
|
|
||
| return fmt.Errorf("private ca root missing: expected one of %v to exist; bootstrap it (e.g. scripts/fetch-secrets) or mount it into the container", candidates) |
| @@ -0,0 +1,75 @@ | |||
| # fetch-secrets (certs-puller) | |||
| - `VAULT_CACERT`: PEM CA bundle path used to verify the upstream Vault TLS cert (e.g. `/viasat/certs/viasat.io.pem`) | ||
| - `VIASAT_IO_CACERT_FILE` / `VIASAT_IO_CACERT_URL`: location and source URL of the Viasat private CA bundle; bootstrap it out-of-process (see `scripts/fetch-secrets/` or `docker-compose-viasat.yaml`) | ||
| - `VAULT_AUTH_LDAP_MOUNT` (default `ldap`), `VAULT_AUTH_USERPASS_MOUNT` (default `userpass`), `VAULT_OIDC_MOUNT` (default `oidc`), `VAULT_OIDC_ROLE` (optional): Vault auth method mounts used by the login page |
| secret, readErr := vault.Logical().Read("sys/internal/ui/mounts") | ||
| if readErr != nil { | ||
| logger.WithError(readErr).Error("Failed to list mounts via sys/internal/ui/mounts") | ||
| return mcp.NewToolResultError(fmt.Sprintf("Failed to list mounts: %v", err)), nil | ||
| } | ||
| if secret == nil || secret.Data == nil { | ||
| return mcp.NewToolResultError(fmt.Sprintf("Failed to list mounts: %v", err)), nil | ||
| } |
| if cfg, ok := m["config"].(map[string]interface{}); ok { | ||
| if v, ok := cfg["default_lease_ttl"].(float64); ok { | ||
| mt.DefaultLeaseTTL = int(v) | ||
| } | ||
| if v, ok := cfg["max_lease_ttl"].(float64); ok { | ||
| mt.MaxLeaseTTL = int(v) | ||
| } | ||
| } |
| // Config holds the OAuth Authorization Server configuration, loaded from the | ||
| // environment. OAuth is enabled only when MCPAuthSecret is set. | ||
| type Config struct { | ||
| // MCPAuthSecret is the base64url (32-byte) symmetric key used to seal all | ||
| // stateless OAuth tokens. Empty disables OAuth entirely. | ||
| MCPAuthSecret string |
| oidcStateMap.Store(vaultState, oidcPending{ | ||
| SealedAuthState: encState, | ||
| ClientNonce: clientNonce, | ||
| ExpiresAt: time.Now().Add(r.cfg.AuthCodeTTL), | ||
| }) |
| Tool: mcp.NewTool("read_folder", | ||
| mcp.WithDescription("Read ALL secrets under a KV mount path in one call. "+ | ||
| "Defaults to key names + SHA-256 of each value (safe, no plaintext exposure). "+ | ||
| "Set include_values=true to return actual secret values."), | ||
| mcp.WithToolAnnotation(mcp.ToolAnnotation{ReadOnlyHint: utils.ToBoolPtr(true)}), |
The interactive login page hard-coded root-relative form actions
(action="/vault/login", action="/vault/oidc/start"). Behind a reverse-proxy path
prefix (e.g. https://host/mcps/vault-mcp), the browser resolves those against the
HOST ROOT and POSTs to /vault/oidc/start -> 404, so OIDC/LDAP sign-in never starts.
Terminal MCP clients (Claude Code, WARP) surfaced this after OAuth discovery
succeeded. It only worked on localhost because the server is at the root there.
Fix: derive Config.BasePath() from MCP_SERVER_URL (the public mount point; a proxy
strips the prefix before it reaches us, so the request can't reveal it) and prefix
the login form actions with it. Empty at the host root, so localhost is unchanged.
All server-issued redirects already build from ServerURL and were unaffected.
- pkg/oauth/config.go: add Config.BasePath()
- pkg/oauth/login.go: loginPageData.BasePath; forms use {{.BasePath}}/vault/...
- tests: BasePath cases + login template renders prefixed vs root actions
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| logger.WithFields(log.Fields{ | ||
| "mcp_auth_secret": secret, | ||
| "hint": "set MCP_AUTH_SECRET to persist bearer tokens across restarts", | ||
| }).Info("generated MCP_AUTH_SECRET") |
| var reg ClientRegistrationData | ||
| if _, err := r.tokenSvc.Parse(string(TokenTypeClientID), clientID, ®); err != nil { | ||
| // The client_id is a sealed token minted with the server's current | ||
| // MCP_AUTH_SECRET. If decryption fails the server most likely restarted | ||
| // (generating a new secret), invalidating the stored registration. | ||
| // Rather than surfacing an error the user cannot act on, re-register the | ||
| // client on-the-fly using the redirect_uri already present in the request | ||
| // so the OIDC flow continues uninterrupted. | ||
| if redirectURI == "" { | ||
| r.renderCallbackError(w, "MCP client registration is no longer valid and no redirect_uri was provided — please reconnect your MCP client.") | ||
| return | ||
| } | ||
| u, parseErr := url.Parse(redirectURI) | ||
| if parseErr != nil || u.Scheme == "" || u.Host == "" { | ||
| r.renderCallbackError(w, "MCP client registration is no longer valid and the redirect_uri is not a valid URL — please reconnect your MCP client.") | ||
| return | ||
| } | ||
| r.logger.WithField("redirect_uri", redirectURI).Info("stale client_id — transparently re-registering MCP client for this authorize request") | ||
| reg = ClientRegistrationData{ | ||
| ClientIDIssuedAt: time.Now().UTC().Unix(), | ||
| RedirectURIs: []string{redirectURI}, | ||
| TokenEndpointAuthMethod: "none", | ||
| } | ||
| } |
| method := in.TokenEndpointAuthMethod | ||
| if method == "" { | ||
| method = "none" | ||
| } |
| @@ -0,0 +1,75 @@ | |||
| # fetch-secrets (certs-puller) | |||
| - `MCP_AUTH_CODE_TTL`: lifetime of authorization codes / login state (default: `5m`) | ||
| - `MCP_AUTH_ACCESS_TTL`: lifetime of bearer access tokens (default: `12h`) | ||
| - `VAULT_CACERT`: PEM CA bundle path used to verify the upstream Vault TLS cert (e.g. `/viasat/certs/viasat.io.pem`) | ||
| - `VIASAT_IO_CACERT_FILE` / `VIASAT_IO_CACERT_URL`: location and source URL of the Viasat private CA bundle; bootstrap it out-of-process (see `scripts/fetch-secrets/` or `docker-compose-viasat.yaml`) |
| mcp.WithString("key", | ||
| mcp.DefaultString(""), | ||
| mcp.Description("A optional key in the secret to delete. If not specified, all keys in the the secret will be deleted."), | ||
| ), |
|
|
||
| // Read the secret | ||
| secret, err := vault.Logical().Read(fullPath) | ||
| kvSecret, err := vault.KVv2(mount).Get(ctx, strings.TrimPrefix(path, "/")) |
| Tool: mcp.NewTool("read_folder", | ||
| mcp.WithDescription("Read ALL secrets under a KV mount path in one call. "+ | ||
| "Defaults to key names + SHA-256 of each value (safe, no plaintext exposure). "+ | ||
| "Set include_values=true to return actual secret values."), | ||
| mcp.WithToolAnnotation(mcp.ToolAnnotation{ReadOnlyHint: utils.ToBoolPtr(true)}), |
| func readFolderHandler(ctx context.Context, req mcp.CallToolRequest, logger *log.Logger) (*mcp.CallToolResult, error) { | ||
| logger.Debug("Handling read_folder request") | ||
|
|
| if cfg, ok := m["config"].(map[string]interface{}); ok { | ||
| if v, ok := cfg["default_lease_ttl"].(float64); ok { | ||
| mt.DefaultLeaseTTL = int(v) | ||
| } | ||
| if v, ok := cfg["max_lease_ttl"].(float64); ok { | ||
| mt.MaxLeaseTTL = int(v) | ||
| } | ||
| } |
…rm@main Reusable workflow re-runs pin the original resolved SHA; an empty commit forces a fresh pull_request run that resolves docker-multiarch-cicd.yaml@main (now containing the env-based bake-metadata digest fix).
PCI review checklist
I have documented a clear reason for, and description of, the change I am making.
If applicable, I've documented a plan to revert these changes if they require more than reverting the pull request.
If applicable, I've documented the impact of any changes to security controls.
Examples of changes to security controls include using new access control methods, adding or removing logging pipelines, etc.
If you have any questions, please contact your direct supervisor, GRC (#team-grc), or the PCI working group (#proj-pci-reboot). You can also find more information at PCI Compliance.
🔧 MCP Server Config
docker-compose-ENTERPRISE_EXAMPLE.yaml... I would copy it to docker-compose-enterprise.yaml✅ Testing
🔧 MCP Client Config
{ "vault-company": { "timeout": 30, "url": "http://localhost:8250/mcp" } }🖼️ Oauth 2
🖼️ MCP Use after Oauth 2