Skip to content

Commit 1203595

Browse files
authored
chore(mcp): import the FerrVault MCP servers from FerrLabs/MCP (#304)
* chore(mcp): import the FerrVault MCP servers from FerrLabs/MCP Moves @ferrvault/mcp and @ferrvault/api-mcp into mcp/ as a standalone pnpm workspace that depends on the published @ferrlabs/mcp-core. They release on their own mcp-v* tags, publish to npm and GHCR from mcp-publish.yml. * chore(mcp): keep tests out of the build output and drop the setup-node zizmor ignore * chore(mcp): keep a single server, the one on the FerrVault API Drops the server built on the unified FerrLabs API and renames @ferrvault/api-mcp to @ferrvault/mcp, like the other product servers. * chore(mcp): publish the image under its own name * chore(mcp): drop the stale README section and pin setup-node without cache
1 parent e928551 commit 1203595

37 files changed

Lines changed: 4651 additions & 0 deletions

‎.ferrflow‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,22 @@
5959
}
6060
],
6161
"updateLockfiles": true
62+
},
63+
{
64+
"name": "ferrvault-mcp",
65+
"path": "mcp",
66+
"tagTemplate": "mcp-v{version}",
67+
"changelog": "mcp/CHANGELOG.md",
68+
"versionedFiles": [
69+
{
70+
"path": "mcp/package.json",
71+
"format": "json"
72+
},
73+
{
74+
"path": "mcp/packages/ferrvault-mcp/package.json",
75+
"format": "json"
76+
}
77+
]
6278
}
6379
]
6480
}

‎.github/workflows/mcp-ci.yml‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
name: MCP CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
paths:
7+
- "mcp/**"
8+
- ".github/workflows/mcp-ci.yml"
9+
pull_request:
10+
branches: [main]
11+
paths:
12+
- "mcp/**"
13+
- ".github/workflows/mcp-ci.yml"
14+
workflow_dispatch:
15+
16+
concurrency:
17+
group: ${{ github.workflow }}-${{ github.ref }}
18+
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
19+
20+
jobs:
21+
test:
22+
permissions:
23+
contents: read
24+
pull-requests: write
25+
packages: read
26+
name: Test
27+
uses: FerrLabs/.github/.github/workflows/reusable-ci-node.yml@9796e976e6b1a071578aed08b164fb483bf01a86 # main
28+
with:
29+
working-directory: mcp
30+
node-version: "24"
31+
pnpm-version: "12"
32+
enable-sonar: true
33+
sonar-project-key: FerrVault-MCP
34+
install-args: ""
35+
enable-lint: false
36+
enable-knip: true
37+
enable-madge: true
38+
madge-paths: packages/*/src
39+
enable-audit: true
40+
secrets: inherit

‎.github/workflows/mcp-publish.yml‎

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
name: MCP publish
2+
3+
on:
4+
release:
5+
types: [published]
6+
workflow_dispatch:
7+
inputs:
8+
tag:
9+
description: Release tag to publish, e.g. mcp-v10.14.0
10+
required: true
11+
type: string
12+
13+
concurrency:
14+
group: mcp-publish-${{ inputs.tag || github.event.release.tag_name }}
15+
cancel-in-progress: false
16+
17+
jobs:
18+
publish-npm:
19+
name: Publish npm
20+
if: github.event_name == 'workflow_dispatch' || startsWith(github.event.release.tag_name, 'mcp-v')
21+
runs-on: ubuntu-latest
22+
permissions:
23+
contents: read
24+
id-token: write
25+
defaults:
26+
run:
27+
working-directory: mcp
28+
steps:
29+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
30+
with:
31+
persist-credentials: false
32+
ref: ${{ inputs.tag || github.event.release.tag_name }}
33+
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6
34+
with:
35+
version: "12"
36+
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
37+
with:
38+
node-version: "24"
39+
package-manager-cache: false
40+
- run: pnpm install --frozen-lockfile
41+
- run: pnpm build
42+
- name: Publish
43+
run: |
44+
packdir="$RUNNER_TEMP/pack"
45+
for dir in packages/*; do
46+
name=$(node -p "require('./$dir/package.json').name")
47+
version=$(node -p "require('./$dir/package.json').version")
48+
if npm view "$name@$version" version >/dev/null 2>&1; then
49+
echo "$name@$version already on npm, skipping"
50+
continue
51+
fi
52+
rm -rf "$packdir" && mkdir -p "$packdir"
53+
(cd "$dir" && pnpm pack --pack-destination "$packdir")
54+
npx --yes npm@11 publish "$packdir"/*.tgz --access public --provenance
55+
done
56+
57+
publish-ghcr:
58+
name: Publish GHCR image (${{ matrix.image }})
59+
if: github.event_name == 'workflow_dispatch' || startsWith(github.event.release.tag_name, 'mcp-v')
60+
runs-on: ubuntu-latest
61+
strategy:
62+
fail-fast: false
63+
matrix:
64+
include:
65+
- package: "@ferrvault/mcp"
66+
image: ferrvault-mcp-server
67+
permissions:
68+
contents: read
69+
packages: write
70+
steps:
71+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
72+
with:
73+
persist-credentials: false
74+
ref: ${{ inputs.tag || github.event.release.tag_name }}
75+
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4
76+
- uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4
77+
with:
78+
registry: ghcr.io
79+
username: ${{ github.actor }}
80+
password: ${{ secrets.GITHUB_TOKEN }}
81+
- name: Compute version
82+
id: ver
83+
env:
84+
TAG: ${{ inputs.tag || github.event.release.tag_name }}
85+
run: |
86+
v="${TAG#mcp-v}"
87+
echo "version=$v" >> "$GITHUB_OUTPUT"
88+
- uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7
89+
with:
90+
context: mcp
91+
file: ./mcp/Dockerfile
92+
push: true
93+
build-args: |
94+
PACKAGE=${{ matrix.package }}
95+
tags: |
96+
ghcr.io/ferrlabs/${{ matrix.image }}:${{ steps.ver.outputs.version }}
97+
ghcr.io/ferrlabs/${{ matrix.image }}:latest
98+
labels: |
99+
org.opencontainers.image.source=https://github.com/FerrLabs/FerrVault
100+
org.opencontainers.image.version=${{ steps.ver.outputs.version }}
101+
org.opencontainers.image.licenses=MPL-2.0

‎README.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,10 @@ The operator relies on endpoints in [`FerrLabs/FerrVault-Cloud`](https://github.
170170
- `secrets:read` scope enforcement on all secrets routes (#268)
171171
- Bulk reveal endpoint (#277)
172172

173+
## MCP server
174+
175+
[`mcp/`](mcp) holds the Model Context Protocol server for FerrVault, [`@ferrvault/mcp`](https://www.npmjs.com/package/@ferrvault/mcp). It talks to the FerrVault API directly and is released on its own `mcp-v*` tags.
176+
173177
## Contributing
174178

175179
See [CONTRIBUTING.md](CONTRIBUTING.md). Code of conduct in [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md). Vulnerability reports via [SECURITY.md](SECURITY.md).

‎mcp/.dockerignore‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
**/node_modules
2+
**/dist
3+
**/coverage
4+
**/*.tsbuildinfo
5+
.git
6+
.github
7+
.husky
8+
scripts
9+
*.md

‎mcp/.gitignore‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
node_modules/
2+
dist/
3+
coverage/
4+
.env
5+
*.local

‎mcp/.prettierignore‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
node_modules
2+
dist
3+
build
4+
.astro
5+
.vite
6+
pnpm-lock.yaml
7+
*.tsbuildinfo
8+
CHANGELOG.md
9+
coverage

‎mcp/.prettierrc.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{ "semi": true, "singleQuote": true, "trailingComma": "all", "printWidth": 100 }

‎mcp/Dockerfile‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
ARG PACKAGE=@ferrvault/mcp
2+
3+
FROM node:24-alpine AS build
4+
ARG PACKAGE
5+
WORKDIR /app
6+
ENV HUSKY=0 \
7+
CI=true
8+
RUN corepack enable
9+
COPY pnpm-workspace.yaml pnpm-lock.yaml package.json ./
10+
RUN pnpm fetch
11+
COPY packages packages
12+
RUN pnpm install --frozen-lockfile --offline
13+
RUN pnpm run build
14+
RUN pnpm --filter "${PACKAGE}" deploy --prod --legacy /out
15+
16+
FROM node:24-alpine
17+
WORKDIR /app
18+
ENV NODE_ENV=production \
19+
FERRLABS_MCP_MODE=http \
20+
PORT=3000 \
21+
HOST=0.0.0.0
22+
COPY --from=build --chown=1000:1000 /out/node_modules ./node_modules
23+
COPY --from=build --chown=1000:1000 /out/dist ./dist
24+
COPY --from=build --chown=1000:1000 /out/package.json ./
25+
USER 1000:1000
26+
EXPOSE 3000
27+
HEALTHCHECK --interval=15s --timeout=3s --start-period=5s --retries=3 \
28+
CMD wget -qO- http://127.0.0.1:3000/health || exit 1
29+
CMD ["node", "dist/index.js"]

‎mcp/README.md‎

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# FerrVault MCP
2+
3+
FerrVault as a Model Context Protocol server, so an AI assistant can drive it.
4+
5+
[![npm @ferrvault/mcp](https://img.shields.io/npm/v/@ferrvault/mcp)](https://www.npmjs.com/package/@ferrvault/mcp)
6+
7+
## Quick start
8+
9+
Add to your MCP client configuration (Claude Code, Cursor, etc.):
10+
11+
```json
12+
{
13+
"mcpServers": {
14+
"ferrvault": { "command": "npx", "args": ["-y", "@ferrvault/mcp"] }
15+
}
16+
}
17+
```
18+
19+
The first tool call that needs auth opens a browser for an OAuth loopback flow and saves the token. To skip it (CI, headless), set `FERRLABS_API_TOKEN`. Details in the [FerrLabs MCP README](https://github.com/FerrLabs/MCP#how-auth-resolution-works).
20+
21+
## Tools
22+
23+
Tools marked **destructive** are irreversible or high-impact. MCP clients should gate them behind a confirmation.
24+
25+
### `@ferrvault/mcp`
26+
27+
Targets the FerrVault API (`api.ferrvault.com`, or a self-hosted instance through `FERRVAULT_API_URL`): vaults, their environments, and the secrets, secret requests and service tokens of each environment. Vaults belong to the organization the token is signed in to.
28+
29+
The FerrVault API accepts FerrLabs session JWTs, not `fft_*` API tokens. Over HTTP, register the client with the IdP as an `eddsa_session_jwt` OAuth client and pass its id to the MCP client (`claude mcp add --client-id`); a dynamically registered client gets an `fft_*` token, which the API refuses.
30+
31+
| Tool | Notes |
32+
| ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
33+
| `list_ferrvault_vaults`, `list_ferrvault_environments` | read |
34+
| `create_ferrvault_vault` | slug and display name, in the signed-in organization; FerrVault provisions its key, makes you admin and creates a `default` environment |
35+
| `create_ferrvault_environment` | slug and display name in an existing vault; vault admin |
36+
| `list_ferrvault_secrets` | names, versions, tags and expiry; never values |
37+
| `reveal_ferrvault_secret` | returns the plaintext value, audit-logged by FerrVault |
38+
| `set_ferrvault_secret` | creates the secret, or stores a new version when it exists |
39+
| `generate_ferrvault_secret` | random value generated in the MCP server, stored like `set_ferrvault_secret`, never returned |
40+
| `delete_ferrvault_secret` | **destructive** |
41+
| `list_ferrvault_secret_requests` | pending requests only: names clients asked for that have no value, with counts, timestamps and requester kind |
42+
| `archive_ferrvault_secret_request` | by id, or by name (resolved to the pending request); stays archived even if clients keep asking |
43+
| `copy_ferrvault_secret` | copies a value to another vault, environment or name (name defaults to the source), stored like `set_ferrvault_secret`; read and written in the MCP server, never returned; refuses source equal to target |
44+
| `list_ferrvault_service_tokens` | operator tokens of one environment: id, name, role, creator, created, expiry, last use; never the token; vault admin |
45+
| `create_ferrvault_service_token` | name, role, optional expiry; the token is written into a target secret (created or versioned) and never returned, and revoked again if that write fails; vault admin |
46+
| `revoke_ferrvault_service_token` | by id; **destructive**, workloads using it lose access; vault admin |
47+
48+
## Configuration
49+
50+
| Variable | Description | Default |
51+
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------- |
52+
| `FERRVAULT_API_URL` | Base URL for the FerrVault API (`@ferrvault/mcp`). Its host must be allowed (see `FERRLABS_MCP_ALLOWED_API_HOSTS`). For an internal CA, point `NODE_EXTRA_CA_CERTS` at the CA certificate. | `https://api.ferrvault.com` |
53+
| `FERRVAULT_API_VERSION` | Contract version sent as `x-ferrvault-api-version` by `@ferrvault/mcp`. | `2026-08-04` |
54+
55+
Shared variables (token, OAuth, host allowlist, HTTP transport, output cap) are read by [`@ferrlabs/mcp-core`](https://github.com/FerrLabs/MCP/tree/main/packages/mcp-core) and documented in the [FerrLabs MCP README](https://github.com/FerrLabs/MCP#configuration).
56+
57+
## Development
58+
59+
```bash
60+
cd mcp
61+
pnpm install
62+
pnpm build
63+
pnpm test
64+
pnpm smoke
65+
```
66+
67+
`pnpm smoke` boots the server over stdio and checks the handshake and the advertised tools.
68+
69+
## License
70+
71+
Mozilla Public License 2.0, see [LICENSE](../LICENSE).

0 commit comments

Comments
 (0)