Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# CardDAV Server Configuration
CARDDAV_BASE_URL=https://your-carddav-server.com/dav
CARDDAV_USERNAME=your-username
CARDDAV_PASSWORD=your-password

# Note: Copy this file to .env and fill in your actual credentials
# Always use HTTPS for CARDDAV_BASE_URL
60 changes: 60 additions & 0 deletions .github/workflows/claude-code-review.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
name: Claude Code Review

on:
pull_request:
types: [opened, synchronize, ready_for_review, reopened]
# Optional: Only run on specific file changes
# paths:
# - "src/**/*.ts"
# - "src/**/*.tsx"
# - "src/**/*.js"
# - "src/**/*.jsx"

jobs:
# Secrets are not readable in job-level `if` conditions, so a guard job
# exposes whether CLAUDE_CODE_OAUTH_TOKEN is configured. Without it the
# review job would fail on repos that have not set up the secret yet.
check-token:
runs-on: ubuntu-latest
outputs:
available: ${{ steps.check.outputs.available }}
steps:
- id: check
env:
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
run: |
if [ -n "$CLAUDE_CODE_OAUTH_TOKEN" ]; then
echo "available=true" >> "$GITHUB_OUTPUT"
else
echo "available=false" >> "$GITHUB_OUTPUT"
echo "CLAUDE_CODE_OAUTH_TOKEN secret not configured; skipping Claude Code Review." >> "$GITHUB_STEP_SUMMARY"
fi

claude-review:
needs: check-token
# Skip fork PRs — OIDC auth unavailable. Remove once anthropics/claude-code-action#939 lands.
if: needs.check-token.outputs.available == 'true' && github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
issues: read
id-token: write

steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1

- name: Run Claude Code Review
id: claude-review
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
plugin_marketplaces: 'https://github.com/anthropics/claude-code.git'
plugins: 'code-review@claude-code-plugins'
prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}'
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
# or https://code.claude.com/docs/en/cli-reference for available options

50 changes: 50 additions & 0 deletions .github/workflows/claude.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: Claude Code

on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
issues:
types: [opened, assigned]
pull_request_review:
types: [submitted]

jobs:
claude:
if: |
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: read
id-token: write
actions: read # Required for Claude to read CI results on PRs
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1

- name: Run Claude Code
id: claude
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}

# This is an optional setting that allows Claude to read CI results on PRs
additional_permissions: |
actions: read

# Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
# prompt: 'Update the pull request description to include a summary of changes.'

# Optional: Add claude_args to customize behavior and configuration
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
# or https://code.claude.com/docs/en/cli-reference for available options
# claude_args: '--allowed-tools Bash(gh pr *)'

25 changes: 25 additions & 0 deletions .github/workflows/publish-mcp-registry.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: Publish to MCP Registry
on:
release:
types: [published]

permissions:
contents: read
id-token: write

jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Set version in server.json
run: |
VERSION=${GITHUB_REF#refs/tags/v}
jq --arg v "$VERSION" '.packages[0].version = $v | .version = $v' server.json > server.tmp && mv server.tmp server.json
- name: Install mcp-publisher
run: |
curl -fsSL "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_linux_amd64.tar.gz" | tar xz mcp-publisher
- name: Publish to MCP Registry
run: |
./mcp-publisher login github-oidc
./mcp-publisher publish
167 changes: 167 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
name: Release
on:
push:
branches: [main]
pull_request:
branches: [main]

permissions:
contents: write # Create releases and commit version bumps
issues: write # Comment on resolved issues
pull-requests: write # Comment on merged PRs
id-token: write # Generate OIDC token for npm trusted publishing

jobs:
lint-and-format:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Setup Node.js
uses: actions/setup-node@v5
with:
node-version: "22"
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Setup Biome
uses: biomejs/setup-biome@v2
- name: Run Biome CI
run: npm run check:ci

knip:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version: "22"
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Run Knip
run: npm run knip

docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version: "22"
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Check generated tool docs
run: npm run docs:check

build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version: "22"
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build

test:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version: "22"
- run: npm ci
- run: npm test

smoke:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version: "22"
cache: "npm"
- uses: actions/setup-python@v6
with:
python-version: "3.12"
- run: npm ci

- name: Install Radicale
run: pip install radicale

- name: Configure Radicale
run: |
mkdir -p "$RUNNER_TEMP/radicale/collections"
printf 'test:test\n' > "$RUNNER_TEMP/radicale/htpasswd"

- name: Start Radicale
run: |
radicale \
--storage-filesystem-folder "$RUNNER_TEMP/radicale/collections" \
--auth-type htpasswd \
--auth-htpasswd-filename "$RUNNER_TEMP/radicale/htpasswd" \
--auth-htpasswd-encryption plain \
--server-hosts 127.0.0.1:5232 &
for _ in $(seq 1 30); do
curl -fsS -u test:test http://127.0.0.1:5232/ >/dev/null && exit 0
sleep 0.5
done
echo "Radicale failed to start" >&2
exit 1

- name: Create test address book
run: |
curl -fsS -X MKCOL -u test:test \
-H "Content-Type: application/xml" \
--data '<?xml version="1.0" encoding="utf-8" ?>
<create xmlns="DAV:" xmlns:CR="urn:ietf:params:xml:ns:carddav">
<set><prop>
<resourcetype><collection/><CR:addressbook/></resourcetype>
<displayname>Test</displayname>
</prop></set>
</create>' \
http://127.0.0.1:5232/test/contacts/

- name: Run smoke
run: |
cat > .env <<EOF
CARDDAV_BASE_URL=http://127.0.0.1:5232
CARDDAV_USERNAME=test
CARDDAV_PASSWORD=test
EOF
npm run smoke

release:
needs: [test, knip, docs, smoke]
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
# semantic-release defaults the very first release to 1.0.0. Seeding a
# v0.0.0 baseline tag on the root commit makes the initial feat commits
# produce 0.1.0 instead. No-op once any v* tag exists.
- name: Seed v0.0.0 baseline tag for the first release
run: |
git fetch --tags --quiet
if [ -z "$(git tag -l 'v*')" ]; then
root=$(git rev-list --max-parents=0 HEAD | tail -n1)
git tag v0.0.0 "$root"
git push origin v0.0.0
fi
- uses: actions/setup-node@v5
with:
node-version: "22"
- run: npm ci
- run: npm run build
- run: npx semantic-release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
LEFTHOOK: "0"
13 changes: 13 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
.env

.idea

.claude/settings.local.json

node_modules

# build
dist

# test coverage
coverage
9 changes: 9 additions & 0 deletions .mcp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"mcpServers": {
"carddav": {
"type": "stdio",
"command": "node",
"args": ["--env-file=.env", "dist/index.js"]
}
}
}
1 change: 1 addition & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
registry = "https://registry.npmjs.org/"
17 changes: 17 additions & 0 deletions .releaserc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"branches": ["main"],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/changelog",
"@semantic-release/npm",
"@semantic-release/github",
[
"@semantic-release/git",
{
"assets": ["package.json", "package-lock.json", "CHANGELOG.md"],
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
}
]
]
}
26 changes: 26 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# carddav-mcp

MCP server exposing CardDAV contact operations as tools for AI assistants.

## Tech Stack
- TypeScript, ESM (`"type": "module"`), Node ≥18, compiled with `tsc` to `dist/`.
- Biome for lint + format (`npm run check` / `check:fix`) — do not add ESLint or Prettier.
- Vitest for tests, lefthook for git hooks, semantic-release for publishing.
- Runtime deps: `@modelcontextprotocol/sdk`, `tsdav`, `zod`.

## Layout
- `src/index.ts` — server entry; wires up `StdioServerTransport` and registers tools.
- `src/tools/<tool>.ts` — one file per MCP tool, with `*.test.ts` next to it.
- `src/tools/vcard.ts` — minimal vCard 3.0 parse/serialize/partial-update helpers shared by the tools.
- Credentials come from `.env` (see `.env.example`); dev loads it via `tsx --env-file=.env`, not `dotenv`.

## Workflow
- Dev: `npm run dev` (watch). Build: `npm run build`. Test: `npm test`.
- After implementing a new feature run the smoke tests (`npm run smoke` and `npm run smoke:agent`) to verify it works as expected.

## Smoke tests
Two complementary end-to-end checks against a real CardDAV server (uses `.env`):
- `npm run smoke` — deterministic SDK harness (`scripts/smoke.ts`). Spawns the built server over stdio via the MCP client SDK and asserts the create → list → get → update → delete round-trip. Fast, no LLM cost; safe to run in CI on every PR.
- `npm run smoke:agent` — agent-ergonomics harness (`scripts/smoke-agent.sh`). Drives the server via `claude -p` with a JSON output schema, validating that tool names, descriptions, schemas, and error messages are usable by an LLM.

`.mcp.json` at the repo root registers the server (loads `.env` via `node --env-file`) so both the agent smoke and interactive Claude Code sessions pick it up automatically.
Loading