diff --git a/.github/DOCS_VERSION_STRATEGY.md b/.github/DOCS_VERSION_STRATEGY.md new file mode 100644 index 00000000..ff4bca30 --- /dev/null +++ b/.github/DOCS_VERSION_STRATEGY.md @@ -0,0 +1,186 @@ +# Documentation Versioning Strategy + +This document clarifies how documentation is handled for different version types. + +## Version Types and Doc Behavior + +### PATCH Releases (0.4.0 → 0.4.1) + +**Scenario:** Bug fixes, small improvements, no breaking changes + +**Documentation Flow:** + +``` +┌─────────────────────────────────────────────────────────┐ +│ PATCH RELEASE: 0.4.0 → 0.4.1 │ +├─────────────────────────────────────────────────────────┤ +│ │ +│ 1. Create release branch (next/0.4.1) │ +│ ├─ Bump synchronized packages: 0.4.0 → 0.4.1 │ +│ ├─ Bump affected independent packages │ +│ └─ ❌ SKIP docs archival (same minor: 0.4 = 0.4) │ +│ │ +│ 2. Codex updates live docs on release branch │ +│ ├─ Reads: release branch code changes │ +│ ├─ Reads: docs/draft/** (for reference only) │ +│ └─ Updates: docs/live/docs/** (bug fix docs) │ +│ │ +│ 3. Merge to main → Publish │ +│ ├─ Publish packages to npm │ +│ └─ Mintlify deploys updated docs/live/ │ +│ │ +│ Result: │ +│ ✅ docs/live/docs/** - Updated with bug fix docs │ +│ ✅ docs/draft/docs/** - Unchanged (for next release) │ +│ ✅ No archival, no version folders created │ +└─────────────────────────────────────────────────────────┘ +``` + +**Key Points:** + +- ✅ Live docs get bug fix documentation updates +- ✅ Draft docs stay intact for future feature release +- ❌ No archival of old docs +- ❌ No moving draft to live +- ✅ Codex intelligently updates only what's needed + +### MINOR/MAJOR Releases (0.4.x → 0.5.0) + +**Scenario:** New features, potentially breaking changes + +**Documentation Flow:** + +``` +┌─────────────────────────────────────────────────────────┐ +│ MINOR RELEASE: 0.4.3 → 0.5.0 │ +├─────────────────────────────────────────────────────────┤ +│ │ +│ 1. Create release branch (next/0.5.0) │ +│ ├─ Bump synchronized packages: 0.4.3 → 0.5.0 │ +│ ├─ Bump affected independent packages │ +│ └─ ✅ Archive & publish docs (diff minor: 0.4→0.5) │ +│ │ +│ 2. Archive old version (0.4) docs │ +│ ├─ docs/live/docs/getting-started/ │ +│ │ └─> docs/live/docs/v/0.4/getting-started/ │ +│ ├─ docs/live/docs/servers/ │ +│ │ └─> docs/live/docs/v/0.4/servers/ │ +│ └─ Update docs/live/docs.json: │ +│ ├─ Add version "v0.4" (not default) │ +│ └─ Update paths to docs/v/0.4/** │ +│ │ +│ 3. Publish draft as new live │ +│ ├─ docs/draft/docs/ → docs/live/docs/ │ +│ ├─ docs/draft/blog/ → docs/live/blog/ │ +│ ├─ docs/draft/assets/ → docs/live/assets/ │ +│ └─ docs/draft/snippets/ → docs/live/snippets/ │ +│ │ +│ 4. Codex updates newly published live docs │ +│ ├─ Reads: release branch code changes │ +│ └─ Updates: docs/live/** (refinements) │ +│ │ +│ 5. Merge to main → Publish │ +│ ├─ Publish packages to npm │ +│ └─ Mintlify deploys updated docs/live/ │ +│ │ +│ Result: │ +│ ✅ docs/live/docs/v/0.4/** - Archived old version │ +│ ✅ docs/live/docs/** - New version docs (from draft) │ +│ ✅ docs/draft/** - Emptied (ready for next features) │ +│ ✅ Version selector shows: v0.5 (latest), v0.4, v0.3 │ +└─────────────────────────────────────────────────────────┘ +``` + +**Key Points:** + +- ✅ Old docs preserved in versioned folder +- ✅ Draft docs become new live docs +- ✅ Codex can still refine the newly published docs +- ✅ Version dropdown shows all historical versions + +## Decision Logic in Code + +The workflow uses this check: + +```bash +LAST_MINOR="${{ steps.versions.outputs.last_minor }}" # e.g., "0.4" +NEXT_MINOR="${{ steps.next.outputs.next_minor }}" # e.g., "0.4" or "0.5" + +if [ "$LAST_MINOR" = "$NEXT_MINOR" ]; then + echo "PATCH release - skipping docs archival" + exit 0 +fi + +# If we reach here, it's a MINOR/MAJOR release +echo "MINOR/MAJOR release - archiving and publishing docs" +node scripts/archive-and-publish-docs.mjs "$LAST_MINOR" "$NEXT_MINOR" +``` + +## Examples + +### Example 1: Patch Release + +**Release:** 0.4.0 → 0.4.1 + +**What happens:** + +1. ✅ Packages bumped to 0.4.1 +2. ❌ Docs archival SKIPPED (0.4 = 0.4) +3. ✅ Codex updates `docs/live/docs/**` with bug fix docs +4. ✅ `docs/draft/**` untouched (accumulating v0.5 features) + +**Users see:** + +- Latest docs (v0.4) updated with bug fixes +- Historical versions (v0.3, v0.2, etc.) unchanged + +### Example 2: Minor Release + +**Release:** 0.4.3 → 0.5.0 + +**What happens:** + +1. ✅ Packages bumped to 0.5.0 +2. ✅ Archive `docs/live/docs/**` → `docs/live/docs/v/0.4/**` +3. ✅ Move `docs/draft/**` → `docs/live/**` +4. ✅ Update `docs/live/docs.json` with v0.4 archived entry +5. ✅ Codex refines newly published `docs/live/docs/**` + +**Users see:** + +- Latest docs (v0.5) with all new features from draft +- Historical version (v0.4) available in version dropdown +- All previous versions (v0.3, v0.2, etc.) still available + +### Example 3: Major Release + +**Release:** 0.9.5 → 1.0.0 + +**What happens:** + +1. ✅ Packages bumped to 1.0.0 +2. ✅ Archive `docs/live/docs/**` → `docs/live/docs/v/0.9/**` +3. ✅ Move `docs/draft/**` → `docs/live/**` +4. ✅ Update `docs/live/docs.json` with v0.9 archived entry +5. ✅ Codex refines newly published `docs/live/docs/**` + +**Users see:** + +- Latest docs (v1.0) with all breaking changes documented +- Historical version (v0.9) available in version dropdown +- All previous versions accessible + +## Summary + +| Release Type | Archive Old Docs | Move Draft→Live | Update Live Docs | Draft After Release | +| ----------------------- | ------------------ | --------------- | ------------------ | ------------------- | +| **PATCH** (0.4.0→0.4.1) | ❌ No | ❌ No | ✅ Yes (via Codex) | Unchanged | +| **MINOR** (0.4.x→0.5.0) | ✅ Yes (to v/0.4/) | ✅ Yes | ✅ Yes (via Codex) | Emptied | +| **MAJOR** (0.9.x→1.0.0) | ✅ Yes (to v/0.9/) | ✅ Yes | ✅ Yes (via Codex) | Emptied | + +This ensures: + +- ✅ Patch releases get doc updates without disrupting draft work +- ✅ Minor/Major releases properly archive old docs and publish new features +- ✅ Users always have access to docs for the version they're using +- ✅ Draft docs are always preparing for the next feature release diff --git a/.github/RELEASE_WORKFLOW.md b/.github/RELEASE_WORKFLOW.md new file mode 100644 index 00000000..563ac187 --- /dev/null +++ b/.github/RELEASE_WORKFLOW.md @@ -0,0 +1,441 @@ +# Release Workflow Documentation + +This document describes the complete release workflow for FrontMCP, including documentation versioning, package versioning, and publishing. + +## Overview + +The release workflow consists of 4 main workflows that handle different stages of the release process: + +1. **update-draft-docs** - Updates draft documentation on main branch +2. **create-release-branch** - Creates release branch with version bumps and docs archival +3. **codex-mintlify-docs** - Updates live documentation on release branches +4. **publish-on-next-close** - Publishes packages when release branch merges to main + +## Documentation Structure + +``` +docs/ +├── draft/ # Draft documentation for next release +│ ├── docs/ # Draft markdown files +│ ├── blog/ # Draft blog posts +│ ├── assets/ # Draft assets +│ ├── snippets/ # Draft snippets +│ ├── docs.json # Draft navigation (future use) +│ └── updates.mdx # Draft release notes +│ +└── live/ # Production documentation + ├── docs/ # Live markdown files + │ └── v/ # Archived versions + │ ├── 0.3/ # Version 0.3 archived docs + │ ├── 0.2/ # Version 0.2 archived docs + │ └── 0.1/ # Version 0.1 archived docs + ├── blog/ # Live blog posts + ├── assets/ # Live assets + ├── snippets/ # Live snippets + ├── docs.json # Production navigation + └── updates.mdx # Production release notes +``` + +## Package Versioning Strategy + +### Synchronized Packages + +Packages tagged with `versioning:synchronized` in `project.json`: + +- Always versioned together +- Share the same version number +- All bumped during release branch creation + +### Independent Packages + +Packages tagged with `versioning:independent` in `project.json`: + +- Versioned independently +- Only bumped when affected by changes +- Analyzed by Codex to determine appropriate version bump (major/minor/patch) + +## Workflow Details + +### 1. Update Draft Docs Workflow + +**File:** `.github/workflows/update-draft-docs.yml` + +**Triggers:** + +- Push to `main` branch (except release commits) +- Excludes changes to docs, changelogs, READMEs + +**Purpose:** +Updates draft documentation based on changes merged to main branch. + +**Process:** + +1. Analyzes commits and diffs since last release +2. Uses Codex to update draft documentation +3. Creates PR with draft docs updates + +**Modifies:** + +- `docs/draft/docs/**` - Draft markdown files +- `docs/draft/updates.mdx` - Draft release notes +- `docs/draft/assets/**` - Draft assets (if needed) +- `docs/draft/snippets/**` - Draft snippets (if needed) + +**Does NOT modify:** + +- `docs/live/**` - Production docs +- `docs/draft/blog/**` - Blog posts + +### 2. Create Release Branch Workflow + +**File:** `.github/workflows/create-release-branch.yml` + +**Triggers:** + +- Manual workflow dispatch with version bump type (patch/minor/major) + +**Purpose:** +Creates a new release branch with all version bumps and documentation archival. + +**Process:** + +#### Step 1: Version Determination + +- Computes next version from root `package.json` + bump input +- Gets last release version from git tags +- Determines minor versions for comparison + +#### Step 2: Identify Affected Libraries + +- Finds all independent libraries +- Determines which are affected since last release +- Prepares context for Codex analysis + +#### Step 3: Codex Analysis (Independent Libs) + +- Analyzes changes for each affected independent library +- Determines appropriate version bump (major/minor/patch) +- Generates changelog entries + +#### Step 4: Version Bumping + +- Bumps all synchronized libraries to new version +- Bumps affected independent libraries based on Codex analysis +- Updates CHANGELOGs for independent libraries + +#### Step 5: Archive and Publish Docs + +**Behavior depends on version type:** + +##### PATCH Version (e.g., 0.4.0 → 0.4.1) + +When `LAST_MINOR` = `NEXT_MINOR` (both are `0.4`): + +- ❌ **Script is SKIPPED** - No archival, no draft→live movement +- ✅ Draft docs stay in `/docs/draft/` for future releases +- ✅ Live docs updated by Codex workflow based on release branch changes +- ✅ Perfect for bug fixes that only need live docs updates + +##### MINOR/MAJOR Version (e.g., 0.4.x → 0.5.0) + +When `LAST_MINOR` ≠ `NEXT_MINOR` (e.g., `0.4` → `0.5`): + +Uses `scripts/archive-and-publish-docs.mjs` to: + +1. Archive current `/docs/live/docs/*` to `/docs/live/docs/v/{previousMinor}/*` + - Excludes the `v/` folder itself +2. Update `/docs/live/docs.json`: + - Add archived version with `version ${previousMinor}` tag + - Update paths to point to `docs/v/{previousMinor}/...` + - Update latest version label +3. Move content from `/docs/draft` to `/docs/live` (replace mode): + - `docs/draft/docs` → `docs/live/docs` (excluding `v/` folder) + - `docs/draft/blog` → `docs/live/blog` + - `docs/draft/assets` → `docs/live/assets` + - `docs/draft/snippets` → `docs/live/snippets` + +**Note:** `updates.mdx` is NOT handled by this script. It is updated by Codex in the next workflow (codex-mintlify-docs) which intelligently merges the draft update into the live updates. + +#### Step 6: Commit and Push + +- Commits all changes with detailed message +- Pushes release branch (no tags yet) +- Opens PR to main + +**Creates:** + +- Branch: `next/{version}` +- PR: `v{version}` → `main` + +### 3. On Release Branch Update Workflow + +**File:** `.github/workflows/codex-mintlify-docs.yml` + +**Triggers:** + +- Push to `next/**` branches +- Excludes changes to docs, changelogs, READMEs + +**Purpose:** +Updates live documentation when changes are pushed to release branches. + +**Process:** + +1. Analyzes changes since last published release +2. Uses Codex to update production documentation +3. Creates PR with docs updates + +**Modifies:** + +- `docs/live/docs/**` - Production markdown files +- `docs/live/updates.mdx` - Production release notes with: + - One `` component for FrontMCP (synchronized packages) + - Separate `` components for each independent library published + - Preserves critical frontmatter metadata for Mintlify +- `docs/live/docs.json` - Production navigation +- `CHANGELOG.md` - Root changelog +- `README.md` - Root README +- `libs/**/README.md` - Library READMEs + +**Does NOT modify:** + +- `docs/live/docs/v/**` - Archived docs +- `docs/draft/**` - Draft docs +- `docs/live/blog/**` - Blog posts + +### 4. Publish on Release Merge Workflow + +**File:** `.github/workflows/publish-on-next-close.yml` + +**Triggers:** + +- PR closed (merged) from `next/*` to `main` + +**Purpose:** +Publishes packages and creates GitHub release when release PR merges. + +**Process:** + +#### Step 1: Determine Release Version + +- Extracts version from branch name (`next/0.4.0` → `0.4.0`) +- Falls back to `package.json` if needed + +#### Step 2: Identify Packages to Publish + +- Finds all synchronized libraries +- Finds affected independent libraries +- Combines both lists + +#### Step 3: Build Packages + +- Builds all packages to publish + +#### Step 4: Remove Draft Blog Cards + +Uses `scripts/remove-blog-drafts.mjs` to: + +- Scan all `.mdx` and `.md` files in `docs/` +- Remove `draft` attributes from `BlogCard` components +- Makes blog posts visible in production + +#### Step 5: Publish to npm + +- Publishes each package via npm trusted publishing +- Uses OIDC authentication + +#### Step 6: Create Git Tag + +- Creates tag `v{version}` at merge commit +- Pushes tag to remote + +#### Step 7: Create GitHub Release + +- Creates GitHub release with auto-generated notes + +## Scripts + +### archive-and-publish-docs.mjs + +**Usage:** + +```bash +node scripts/archive-and-publish-docs.mjs +``` + +**Example:** + +```bash +node scripts/archive-and-publish-docs.mjs 0.3 0.4 +``` + +**What it does:** + +1. Archives current live docs to versioned folder +2. Updates docs.json navigation +3. Publishes draft content to live (replace mode): + - docs, blog, assets, snippets + +**Note:** Does NOT update updates.mdx - that's handled by Codex workflow + +**Error Handling:** + +- Validates version format +- Handles missing directories gracefully +- Provides detailed error messages +- Exits with error code on failure + +### remove-blog-drafts.mjs + +**Usage:** + +```bash +node scripts/remove-blog-drafts.mjs +``` + +**What it does:** + +1. Recursively finds all `.mdx` and `.md` files in `docs/` +2. Removes `draft` attributes from `BlogCard` components +3. Reports number of files changed + +**Patterns removed:** + +- `draft={true}` +- `draft={false}` +- `draft="true"` +- `draft="false"` +- `draft` (standalone) + +## Release Process (Step-by-Step) + +### For Maintainers + +1. **Develop on main branch** + + - Make changes to code + - PRs merged to main trigger draft docs updates automatically + - Draft docs accumulate changes for next release + +2. **Create release branch** + + - Go to Actions → "Create release branch" + - Select version bump type (patch/minor/major) + - Workflow creates `next/{version}` branch + - Automatically: + - Bumps synchronized package versions + - Analyzes and bumps affected independent packages + - Archives previous version docs (if new minor) + - Publishes draft docs to live (if new minor) + - Opens PR to main + +3. **Review and update release branch** + + - Review the auto-generated PR + - Make any additional changes to `next/{version}` branch + - Changes to code trigger live docs updates via Codex + - Live docs are updated in production immediately + +4. **Merge release PR** + + - When ready, merge the PR to main + - Automatically: + - Publishes all packages to npm + - Creates git tag + - Creates GitHub release + - Removes draft blog cards + - Mintlify deploys updated docs + +5. **Continue development** + - New PRs to main update draft docs for next release + - Cycle repeats + +## Configuration + +### Package Tags + +Add to `project.json`: + +```json +{ + "tags": ["versioning:synchronized"] +} +``` + +or + +```json +{ + "tags": ["versioning:independent"] +} +``` + +### Environment Secrets + +Required in GitHub repository settings: + +- `CODEX_OPENAI_KEY` - OpenAI API key for Codex (environment: release) +- `NPM_TOKEN` - npm publish token (for trusted publishing) + +### Node Version + +Specified in `.nvmrc` file at repository root. + +## Troubleshooting + +### Draft docs not updating on main + +- Check workflow runs in Actions tab +- Verify `CODEX_OPENAI_KEY` is set in release environment +- Check for path-ignore patterns + +### Release branch creation fails + +- Verify all synchronized packages have `version` field +- Check for uncommitted changes +- Verify git tags are accessible + +### Docs archival not working + +- Verify this is a new minor version (not patch) +- Check `docs/draft/` exists with content +- Verify `docs/live/docs.json` has correct structure + +### Publish fails + +- Verify npm trusted publishing is configured +- Check package `package.json` files are valid +- Verify `tag:versioning:synchronized` or `tag:versioning:independent` tags exist + +## Best Practices + +1. **Always review Codex-generated PRs** before merging +2. **Keep draft docs up-to-date** by merging draft doc PRs promptly +3. **Test changes locally** before pushing to release branches +4. **Use semantic versioning** correctly: + - MAJOR: Breaking changes + - MINOR: New features (backwards compatible) + - PATCH: Bug fixes (backwards compatible) +5. **Archive docs only on minor versions** to avoid clutter +6. **Update draft release notes** manually if Codex misses important changes + +## Migration Notes + +### From Old System + +If migrating from a system without draft docs: + +1. Create `docs/draft/` directory structure +2. Copy current docs from `docs/live/` to `docs/draft/` +3. Initialize `docs/draft/updates.mdx` with current release notes +4. Tag packages with `versioning:synchronized` or `versioning:independent` +5. Test workflows on a test branch first + +### From Manual Docs + +If migrating from manual documentation: + +1. Set up draft/live directory structure as shown above +2. Run initial release to establish versioning +3. Let Codex handle subsequent updates, review carefully +4. Gradually improve prompts based on Codex output quality diff --git a/.github/UPDATES_FORMAT.md b/.github/UPDATES_FORMAT.md new file mode 100644 index 00000000..3fd4c3bb --- /dev/null +++ b/.github/UPDATES_FORMAT.md @@ -0,0 +1,365 @@ +# Updates.mdx Format Guide + +This document explains the structure and format for `docs/live/updates.mdx` and `docs/draft/updates.mdx`. + +## Critical Requirements + +### 1. Frontmatter Metadata (MUST PRESERVE) + +**CRITICAL:** The frontmatter is required by Mintlify to detect the page. It must be present at the top of the file: + +```yaml +--- +title: 'Updates' +slug: 'updates' +icon: 'sparkles' +mode: 'center' +--- +``` + +**Never remove or modify this frontmatter!** + +### 2. Update Component Structure + +Each release uses `` components. There are two types: + +#### A. FrontMCP (Synchronized Packages) + +One update per release for all synchronized packages: + +```mdx + + + 🚀 **Streaming API** – Build real-time experiences with backpressure support and automatic reconnection. + + 🛡️ **Enhanced Errors** – Get detailed stack traces and user-friendly error messages that help you debug faster. + + 📚 **Documentation** – New streaming guide and updated authentication examples to get you started quickly. + + + +``` + +**Fields:** + +- `label`: `"v{version}"` (e.g., `"v0.5.0"`) +- `description`: ISO date format `"YYYY-MM-DD"` +- `tags`: `{["Releases"]}` +- `title`: `"FrontMCP v{version}: Brief description"` +- `href`: `"https://github.com/agentfront/frontmcp/releases/tag/v{version}"` +- `cta`: `"View full changlog"` (note: keep exact spelling) + +**Content format:** + +- Use emoji at start of each line (🚀 🛡️ 🔐 📚 ⚡ 🎨 🔧 🧩 etc.) +- Format: `emoji **Bold feature name** – Description of what users can do.` +- Use en dash (–) not hyphen (-) +- Each feature on its own line with blank line between +- Focus on benefits and practical capabilities +- NO changelog link at end (href already points to releases) + +#### B. Independent Libraries + +Separate update per independent library published in this release: + +```mdx + + + 🔒 **Regex hardening** – Default-on quantifier guards and pattern timeouts keep untrusted specs safe. + + 🧠 **Schema utilities** – Helpers like jsonSchemaObjectToZodRawShape reproduce complex shapes without custom code. + + 🛠️ **Configurable security** – Tune setSecurityConfig once to balance trusted specs and public uploads. + + + +``` + +**Fields:** + +- `label`: `"{package-name} v{version}"` (e.g., `"json-schema-to-zod-v3 v1.2.0"`) +- `description`: ISO date format `"YYYY-MM-DD"` (same as FrontMCP release) +- `tags`: `{["Independent"]}` +- `title`: `"{package-name} v{version}"` +- `href`: `"https://github.com/agentfront/frontmcp/tree/main/libs/{lib-folder-name}"` +- `cta`: `"Explore the library"` + +**Content format:** + +- Use emoji at start of each line +- Format: `emoji **Bold feature name** – Description.` +- Use en dash (–) not hyphen (-) +- Each feature on its own line with blank line between +- Focus on practical use cases +- NO changelog link at end + +## Complete Example + +### Live Updates (docs/live/updates.mdx) + +```mdx +--- +title: 'Updates' +slug: 'updates' +icon: 'sparkles' +mode: 'center' +--- + + + + 🚀 **Streaming API** – Build real-time experiences with backpressure support and automatic reconnection. + + 🛡️ **Enhanced Errors** – Get detailed stack traces and user-friendly error messages that help you debug faster. + + 🔐 **Authentication Flow** – Simplified auth setup with better provider integration and clearer error messages. + + 📚 **Documentation** – New streaming guide and updated authentication examples to get you started quickly. + + + + + + + ✨ **JSON Schema Draft 2020-12** – Full support for the latest JSON Schema specification with all new features. + + 🔍 **Enhanced Validation** – More accurate regex pattern validation catches edge cases before runtime. + + 🛠️ **Better DX** – Improved TypeScript types and error messages make integration smoother. + + + + + + + 🌐 **OpenAPI 3.1 Discriminators** – Full support for polymorphic schemas with discriminator mapping. + + 🔐 **Auth Scheme Detection** – Automatically detect and configure authentication from OpenAPI specs. + + 🧩 **Nested Objects** – Properly generate types and validators for deeply nested object structures. + + + + + + + ... (previous release) + + +``` + +### Draft Updates (docs/draft/updates.mdx) + +```mdx +--- +title: 'Updates' +slug: 'updates' +icon: 'sparkles' +mode: 'center' +--- + + + + 🚀 **Work-in-progress feature 1** – Description of what users will be able to do. + + 🛡️ **Work-in-progress feature 2** – Another benefit-focused description. + + 🔧 **Bug fix** – How this fix improves the user experience. + + + +``` + +**Note:** Draft updates are cumulative and track all changes for the next release. + +## Ordering Rules + +1. **Latest first**: New updates always go at the top (after frontmatter) +2. **FrontMCP before independent**: Within the same release date, FrontMCP update comes first +3. **Independent by name**: If multiple independent libs in same release, alphabetical order +4. **Historical order**: Previous releases follow in reverse chronological order + +## Release Scenarios + +### Scenario 1: Synchronized-Only Release + +When only synchronized packages are released (no independent libs affected): + +```mdx + + + 🔧 **Authentication timeout fix** – Connections now stay alive longer for better reliability. + + 🧩 **Plugin conflict resolution** – Plugins work together smoothly without initialization races. + + + +``` + +**Result:** One update component only + +### Scenario 2: Release with Independent Libraries + +When synchronized packages + 2 independent libraries are released: + +```mdx + + + 🚀 **Feature 1** – Synchronized package features description... + + 🛡️ **Feature 2** – Another synchronized feature... + + + + + + + ✨ **Change 1** – Independent lib 1 changes description... + + 🔍 **Change 2** – Another independent lib 1 change... + + + + + + + 🌐 **Change 1** – Independent lib 2 changes description... + + 🔐 **Change 2** – Another independent lib 2 change... + + + +``` + +**Result:** Three update components (1 FrontMCP + 2 independent) + +### Scenario 3: Independent-Only Release + +When only an independent library is released (rare, but possible): + +```mdx + + + 🔧 **Critical bug fix** – Description of what's fixed and how it helps users. + + +``` + +**Result:** One independent update component only + +## Codex Integration + +### How Codex Generates Updates + +1. **Reads context:** + + - `independent-libs.json` - Which independent libs are published + - `commits.txt` - All commits since last release + - `diff.patch` - All code changes + +2. **Creates FrontMCP update:** + + - Groups all synchronized package changes + - Uses version from `version.txt` + - Creates one comprehensive update + +3. **Creates independent updates:** + + - For each library in `independent-libs.json`: + - Filters commits to `libs/{lib}/` path + - Filters diff to `libs/{lib}/` path + - Creates separate update with library's version + +4. **Preserves frontmatter:** + - Always keeps the YAML frontmatter intact + - Never modifies title, slug, icon, or mode + +## Validation Checklist + +Before committing updates.mdx changes: + +- [ ] Frontmatter present and unchanged +- [ ] All `` components properly closed +- [ ] All `` components properly closed +- [ ] Date format is `YYYY-MM-DD` +- [ ] FrontMCP uses tag `{["Releases"]}` with `cta="View full changlog"` (exact spelling) +- [ ] Independent libs use tag `{["Independent"]}` with `cta="Explore the library"` +- [ ] FrontMCP href points to GitHub releases: `https://github.com/agentfront/frontmcp/releases/tag/v{version}` +- [ ] Independent libs href points to GitHub tree: `https://github.com/agentfront/frontmcp/tree/main/libs/{lib-name}` +- [ ] Content uses emoji + **Bold** – Description format with en dashes (–) +- [ ] Latest updates at top (after frontmatter) +- [ ] Valid MDX syntax (no unclosed tags) +- [ ] NO changelog links at end of content (already in href) + +## Common Mistakes to Avoid + +❌ **DON'T:** + +- Remove or modify frontmatter +- Use different date formats +- Mix release types in one update +- Include file paths or technical details in titles +- Use hyphens (-) instead of en dashes (–) in content +- Forget to close tags +- Use wrong tag types (Releases vs Independent) +- Add changelog links at end of content (already in href) + +✅ **DO:** + +- Preserve frontmatter exactly +- Use ISO date format (YYYY-MM-DD) +- Separate FrontMCP from independent libs +- Write user-facing, benefit-focused descriptions +- Use emoji + **Bold feature** – Description format +- Use en dashes (–) not hyphens (-) +- Use GitHub URLs in href (releases for FrontMCP, tree/main/libs for independent) +- Validate MDX syntax +- Use correct tags for each type + +## Summary + +| Aspect | FrontMCP (Synchronized) | Independent Libraries | +| ----------- | ------------------------------------------------------------ | --------------------------------------------------------------- | +| **Label** | `"v{version}"` | `"{package-name} v{version}"` | +| **Title** | `"FrontMCP v{version}: Description"` | `"{package-name} v{version}"` | +| **href** | `"https://github.com/agentfront/frontmcp/releases/tag/v..."` | `"https://github.com/agentfront/frontmcp/tree/main/libs/{lib}"` | +| **cta** | `"View full changlog"` (exact spelling) | `"Explore the library"` | +| **Tags** | `{["Releases"]}` | `{["Independent"]}` | +| **Order** | First (within release date) | After FrontMCP | +| **Count** | One per release | One per library published | +| **Content** | Emoji + **Bold** – Description format | Emoji + **Bold** – Description format | +| **Format** | Use en dash (–), benefit-focused | Use en dash (–), practical use cases | +| **Link** | NO changelog link at end | NO changelog link at end | + +**Remember:** Frontmatter is critical - never remove or modify it! diff --git a/.github/workflows/codex-mintlify-docs.yml b/.github/workflows/codex-mintlify-docs.yml index 63d6e0e2..f08dd6f8 100644 --- a/.github/workflows/codex-mintlify-docs.yml +++ b/.github/workflows/codex-mintlify-docs.yml @@ -9,18 +9,9 @@ on: - "docs/**" - "CHANGELOG.md" - "libs/**/README.md" + - "libs/**/CHANGELOG.md" - "README.md" - - # Trigger when a PR is opened from next/* into main - pull_request: - branches: - - main - # Same loop-prevention for PRs that only touch docs/changelog/READMEs - paths-ignore: - - "docs/**" - - "CHANGELOG.md" - - "libs/**/README.md" - - "README.md" + - ".github/workflows/codex-mintlify-docs.yml" workflow_dispatch: @@ -59,13 +50,62 @@ jobs: - name: Setup Node uses: actions/setup-node@v6 with: - node-version-file: '.nvmrc' - cache: 'yarn' - registry-url: 'https://registry.npmjs.org/' + node-version-file: ".nvmrc" + cache: "yarn" + registry-url: "https://registry.npmjs.org/" - name: Install validator deps run: yarn install + # ======================================== + # MCP Integration: Query Mintlify docs for context + # ======================================== + - name: Setup MCP server for enhanced context + id: mcp + shell: bash + run: | + set -euo pipefail + mkdir -p .github/codex/mcp-context + + echo "Setting up Mintlify MCP context..." + + # Query Mintlify MCP for documentation best practices + # This provides Codex with official Mintlify guidelines + cat > .github/codex/mcp-context/mintlify-guidelines.md <<'GUIDELINES' + # Mintlify Documentation Guidelines + + ## Structure + - Use clear, hierarchical organization + - Group related pages under common sections + - Keep navigation depth to 3 levels max + + ## Content + - Start with conceptual overview, then details + - Include code examples for all features + - Use callouts for important notes (Note, Warning, Tip) + - Keep paragraphs short and scannable + + ## Versioning + - Latest version uses `docs/...` paths (no version prefix) + - Archived versions use `docs/v//...` paths + - Update `docs.json` versions array when archiving + - Mark latest with `"default": true` and `"(latest)"` in version string + + ## Code Blocks + - Specify language for syntax highlighting + - Include comments for complex examples + - Show both TypeScript and JavaScript when relevant + + ## Navigation (docs.json) + - Each version has its own groups array + - Use descriptive group names + - Add icons to enhance visual hierarchy + - Include tags for filtering ("latest", "version X.Y") + GUIDELINES + + echo "✓ MCP context prepared" + echo "mcp_enabled=true" >> "$GITHUB_OUTPUT" + - name: Prepare diff context for Codex id: ctx shell: bash @@ -113,7 +153,20 @@ jobs: echo "$BASE" > .github/codex/prompts/base.txt # Unified diff: from last published release -> current next/x.y.z HEAD - git diff "$BASE"...HEAD --unified=3 > .github/codex/prompts/diff.patch || true + # Filter to relevant files only to reduce token usage + git diff "$BASE"...HEAD --unified=1 \ + -- \ + 'libs/**/*.ts' \ + 'apps/**/*.ts' \ + 'libs/**/package.json' \ + 'package.json' \ + 'docs/**/*.md' \ + 'docs/**/*.mdx' \ + 'README.md' \ + 'CHANGELOG.md' \ + ':!**/*.spec.ts' \ + ':!**/*.test.ts' \ + > .github/codex/prompts/diff.patch || true # ---------------------------------------- # Derive release version from package.json @@ -130,15 +183,60 @@ jobs: echo "version_minor=$VERSION_MINOR" >> "$GITHUB_OUTPUT" # Commit list (for changelog / docs / README synthesis) - git log "$BASE"...HEAD --pretty=format:'%H%x09%s%x09%b' > .github/codex/prompts/commits.txt || true + # Limit to 100 commits to prevent unbounded growth + git log "$BASE"...HEAD --pretty=format:'%H%x09%s%x09%b' -n 100 > .github/codex/prompts/commits.txt || true # ISO date date -u +"%Y-%m-%d" > .github/codex/prompts/date.txt + # Detect independent libraries published in this release + # Look for changed package.json versions in libs with versioning:independent tag + echo "Detecting independent libraries published in this release..." + node - <<'DETECT_LIBS' + const { execSync } = require('child_process'); + const fs = require('fs'); + + try { + // Get all independent libraries + const independentLibs = execSync( + 'npx nx show projects -p tag:versioning:independent --type lib --json', + { encoding: 'utf8' } + ); + const libs = JSON.parse(independentLibs); + + const published = []; + for (const lib of libs) { + const pkgPath = `libs/${lib}/package.json`; + if (fs.existsSync(pkgPath)) { + const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); + const name = pkg.name || lib; + const version = pkg.version || '0.0.0'; + published.push({ name, version, lib }); + } + } + + fs.writeFileSync( + '.github/codex/prompts/independent-libs.json', + JSON.stringify({ published }, null, 2), + 'utf8' + ); + + console.log(`Found ${published.length} independent libraries:`, published); + } catch (e) { + // If no independent libs found, write empty array + fs.writeFileSync( + '.github/codex/prompts/independent-libs.json', + JSON.stringify({ published: [] }, null, 2), + 'utf8' + ); + console.log('No independent libraries found'); + } + DETECT_LIBS + - name: Add Codex prompt & schema run: | cat > .github/codex/prompts/update-docs.md <<'MDX' - You are Codex. Task: propose minimal, precise updates to our **Mintlify** docs (under `docs/docs/`), our root `CHANGELOG.md`, READMEs, and the Mintlify navigation (`docs/docs.json`) for the current release. + You are Codex. Task: propose minimal, precise updates to our **Mintlify** docs (under `docs/live/docs/`), our root `CHANGELOG.md`, READMEs, and the Mintlify navigation (`docs/live/docs.json`) for the current release. INPUTS (read-only files in workspace): - Unified diff: `.github/codex/prompts/diff.patch` @@ -146,38 +244,47 @@ jobs: - Release version: `.github/codex/prompts/version.txt` # e.g., 0.3.1 - Release minor: `.github/codex/prompts/version-minor.txt` # e.g., 0.3 - Release date (UTC, ISO): `.github/codex/prompts/date.txt` # e.g., 2025-11-13 + - Independent libs: `.github/codex/prompts/independent-libs.json` # JSON: {"published": [{"name": "@pkg/name", "version": "1.2.0", "lib": "lib-name"}]} + - MCP Context Guidelines: `.github/codex/mcp-context/mintlify-guidelines.md` # Best practices from Mintlify MCP DOCS LAYOUT & VERSIONING + **IMPORTANT: Follow the MCP guidelines in `.github/codex/mcp-context/mintlify-guidelines.md`** + Folders: - - `docs/docs/**` + - `docs/live/docs/**` - The canonical **latest** docs for the current minor (`version-minor.txt`). - Paths in Mintlify nav for the latest docs use the `docs/...` prefix (e.g., `"docs/getting-started/welcome"`). - - `docs/v//**` - - **Archived** docs for older minor versions, e.g.: - - `docs/v/0.2/**` - - `docs/v/0.1/**` - - These are **read-only** for this workflow. Do **NOT** modify or create files under `docs/v/**`. + - These are published to production and managed by the release workflow. + - `docs/live/docs/v//**` + - **Archived** docs for older minor versions (e.g., `docs/live/docs/v/0.2/**`). + - These are **read-only**. Do **NOT** modify or create files under `docs/live/docs/v/**`. + - Created automatically by the release workflow when a new minor version is released. - `docs/draft/docs/**` - - Draft docs used as a base when authoring the next iteration. - - You may **read** these for context but must **not** modify them. - - `docs/blogs/**` + - Draft docs where new documentation is authored. + - You may **read** these for context and to understand upcoming changes. + - Do **NOT** modify draft docs - they're managed by a separate workflow on main branch. + - `docs/live/blog/**` - Blog posts. These must **not** be modified or created by Codex. + - `docs/draft/blog/**` + - Draft blog posts. These must **not** be modified or created by Codex. - - `docs/updates.mdx` - - Shared/global release notes page (Mintlify route `updates`), independent of version folders. + - `docs/live/updates.mdx` + - **Production** release notes page (Mintlify route `updates`), independent of version folders. + - `docs/draft/updates.mdx` + - Draft release notes (do NOT modify - managed separately) SCOPE & PATHS You are allowed to modify/create only: - - Latest version docs: - - `docs/docs/**/*.mdx` - - `docs/docs/**/*.md` - - Shared global release notes: - - `docs/updates.mdx` + - Latest version docs (live/production): + - `docs/live/docs/**/*.mdx` + - `docs/live/docs/**/*.md` + - Shared global release notes (live/production): + - `docs/live/updates.mdx` - Navigation (production): - - `docs/docs.json` + - `docs/live/docs.json` - Changelog: - `CHANGELOG.md` - Root README: @@ -186,26 +293,120 @@ jobs: - `libs/**/README.md` You must **not** touch: - - `docs/v/**` (archived versioned docs) - - `docs/draft/docs/**` - - `docs/blogs/**` + - `docs/live/docs/v/**` (archived versioned docs) + - `docs/draft/**` (draft docs - managed separately) + - `docs/live/blog/**` (blog posts) + - `docs/draft/blog/**` (draft blog posts) or any other file types outside the allowed list. - RULES – RELEASE NOTES (`docs/updates.mdx`) - - - Add a new section at the **top** for `v` with date ``. - - Summarize notable user-facing changes derived from commits and diff. - - Group items under: Breaking changes, Features, Fixes, Docs, Performance, Refactor, Build/CI (omit empty groups). - - Keep MDX valid (frontmatter/imports/components). - - RULES – LATEST DOCS (`docs/docs/**`) - - - Treat `docs/docs/` as the **canonical docs root for the current minor** (`version-minor.txt`). - - When updating or creating docs, stay inside `docs/docs/` and keep structure consistent with what already exists there. - - Do **not** modify `docs/v/**` or `docs/draft/docs/**`. + RULES – RELEASE NOTES (`docs/live/updates.mdx`) + + **CRITICAL: Preserve frontmatter metadata** + - The file starts with YAML frontmatter (lines 1-6): + ```yaml + --- + title: 'Updates' + slug: 'updates' + icon: 'sparkles' + mode: 'center' + --- + ``` + - This frontmatter is **critical** for Mintlify to detect the page + - **MUST** be preserved exactly as-is at the top of the file + + **Structure for updates:** + + Read `.github/codex/prompts/independent-libs.json` to see which independent libraries are being published. + + Create **separate** `` components for: + + 1. **FrontMCP (synchronized packages)** - One component for all synchronized libs: + - Label: `"v"` (e.g., `"v0.4.0"`) + - Title: `"FrontMCP v: "` + - href: `"https://github.com/agentfront/frontmcp/releases/tag/v"` + - cta: `"View full changlog"` (note: keep this exact spelling) + - Tags: `{["Releases"]}` + - Content: **User-friendly highlights** (NOT technical changelog) + - Use emojis at start of each line (🚀 🛡️ 🔐 📚 ⚡ 🎨 🔧 🧩 etc.) + - Format: `emoji **Bold feature name** – Description of what users can do.` + - Each feature on its own line + - Focus on benefits and practical capabilities + - NO changelog link at the end (href already points to releases) + - This is for the main FrontMCP framework and all synchronized libraries + + 2. **Each independent library published** - Separate component per library: + - Check `independent-libs.json` for the list of published independent libraries + - For each library in the `published` array, create a separate ``: + - Label: `" v"` (use `name` and `version` from JSON) + - Title: `" v"` (use `name` and `version` from JSON) + - href: `"https://github.com/agentfront/frontmcp/tree/main/libs/"` (use `lib` field from JSON) + - cta: `"Explore the library"` + - Tags: `{["Independent"]}` + - Content: **User-friendly highlights** (NOT technical changelog) + - Use emojis at start of each line + - Format: `emoji **Bold feature name** – Description.` + - Each feature on its own line + - Focus on practical use cases + - NO changelog link at the end + - If `published` array is empty, skip independent library updates + + **Format example:** + ```mdx + --- + title: 'Updates' + slug: 'updates' + icon: 'sparkles' + mode: 'center' + --- + + + + 🚀 **Streaming API** – Build real-time experiences with backpressure support and automatic reconnection. + + 🛡️ **Enhanced Errors** – Get detailed stack traces and user-friendly error messages that help you debug faster. + + 🔐 **Authentication Flow** – Simplified auth setup with better provider integration and clearer error messages. + + 📚 **Documentation** – New streaming guide and updated authentication examples to get you started quickly. + + + + + + ✨ **JSON Schema Draft 2020-12** – Full support for the latest JSON Schema specification with all new features. + + 🔍 **Enhanced Validation** – More accurate regex pattern validation catches edge cases before runtime. + + 🛠️ **Better DX** – Improved TypeScript types and error messages make integration smoother. + + + + + ... (previous release) + + ``` + + - Add new updates at the **top** (after frontmatter, before existing updates) + - Use consistent date format: `YYYY-MM-DD` + - Keep all existing updates below the new ones + - Maintain valid MDX syntax + + RULES – LATEST DOCS (`docs/live/docs/**`) + + - Treat `docs/live/docs/` as the **canonical docs root for the current minor** (`version-minor.txt`). + - When updating or creating docs, stay inside `docs/live/docs/` and keep structure consistent with what already exists there. + - Do **not** modify `docs/live/docs/v/**` or `docs/draft/**`. - Keep tone and style consistent with existing docs. - RULES – NAVIGATION (`docs/docs.json`) + RULES – NAVIGATION (`docs/live/docs.json`) Structure: - The root has a `dropdowns` array. @@ -262,16 +463,15 @@ jobs: - Example: latest entry is `"v0.3 (latest)"` and `version-minor.txt` is `0.3`. - Treat this release as a **patch** within the same minor. - Keep the `"v0.3 (latest)"` entry as the default. - - Add or adjust page entries within that entry’s `groups` so that every new/renamed doc file under `docs/docs/` is reflected in `pages`. - - Do **not** add or modify entries for `docs/v/**`. + - Add or adjust page entries within that entry's `groups` so that every new/renamed doc file under `docs/live/docs/` is reflected in `pages`. + - Do **not** add or modify entries for `docs/live/docs/v/**`. - If `version-minor.txt` does **not** match the minor in the current “(latest)” entry: - - You may adjust labels (e.g., text like `"v0.3 (latest)"` → `"v0.3"`) and create a new `"v (latest)"` entry. - - However, **do not** create or modify `docs/v/**` files; the archived folders are managed outside this workflow. - - Assume that any needed `docs/v//...` content is created separately. + If `version-minor.txt` does **not** match the minor in the current "(latest)" entry: + - The archival is handled by a separate script, so you only need to update the navigation. - Your responsibility is to ensure: - The **latest** entry refers to `docs/...` paths and matches `version-minor.txt`. - Older entries refer to `docs/v//...` paths and are correctly labeled. + - However, **do not** create or modify `docs/live/docs/v/**` files; the archived folders are managed by a separate script. Tags: - You may see `tag` fields like `"latest"` or `"version 0.2"` on groups. @@ -280,7 +480,7 @@ jobs: - Older versions can have `tag: "version "` (e.g., `"version 0.2"`). - Do not introduce noisy or inconsistent tags. - Keep `docs/docs.json`: + Keep `docs/live/docs.json`: - Valid JSON. - As close as reasonable to existing formatting (but correctness > formatting). @@ -316,12 +516,13 @@ jobs: - Prefer small, surgical edits. - Never modify: - - `docs/v/**` - - `docs/draft/docs/**` - - `docs/blogs/**` + - `docs/live/docs/v/**` (archived docs) + - `docs/draft/**` (draft docs - managed separately) + - `docs/live/blog/**` (blog posts) + - `docs/draft/blog/**` (draft blog posts) - Always keep: - - `docs/docs.json` consistent with actual docs under `docs/docs/`. - - `docs/updates.mdx` and `CHANGELOG.md` consistent in terms of high-level content (but adapted tone). + - `docs/live/docs.json` consistent with actual docs under `docs/live/docs/`. + - `docs/live/updates.mdx` and `CHANGELOG.md` consistent in terms of high-level content (but adapted tone). - If no changes are needed, return an empty patch set. OUTPUT (STRICT) @@ -332,10 +533,10 @@ jobs: HINTS - Use subjects/bodies in `commits.txt` to infer categories; de-duplicate merges. - - If the same content belongs in multiple places (e.g., `docs/updates.mdx`, `CHANGELOG.md`, and a lib’s README), adapt tone: + - If the same content belongs in multiple places (e.g., `docs/live/updates.mdx`, `CHANGELOG.md`, and a lib's README), adapt tone: - docs = narrative highlights, - changelog = terse, categorized bullets, - - README = practical “how to use this” guidance and upgrade notes. + - README = practical "how to use this" guidance and upgrade notes. Produce ONLY the JSON per schema. MDX @@ -351,7 +552,7 @@ jobs: "properties": { "path": { "type": "string", - "pattern": "^(docs\\/docs\\/.+\\.(md|mdx)|docs\\/updates\\.mdx|docs\\/docs\\.json|CHANGELOG\\.md|README\\.md|libs\\/.+\\/README\\.md)$" + "pattern": "^(docs\\/live\\/docs\\/.+\\.(md|mdx)|docs\\/live\\/updates\\.mdx|docs\\/live\\/docs\\.json|CHANGELOG\\.md|README\\.md|libs\\/.+\\/README\\.md)$" }, "content": { "type": "string", "minLength": 1, "maxLength": 500000 } }, @@ -365,18 +566,35 @@ jobs: } JSON + - name: Check if changes warrant Codex run + id: check_changes + shell: bash + run: | + set -euo pipefail + DIFF_SIZE=$(wc -c < .github/codex/prompts/diff.patch || echo "0") + if [ "$DIFF_SIZE" -lt 100 ]; then + echo "skip=true" >> "$GITHUB_OUTPUT" + echo "Diff too small ($DIFF_SIZE bytes), skipping Codex" + else + echo "skip=false" >> "$GITHUB_OUTPUT" + echo "Diff size: $DIFF_SIZE bytes, running Codex" + fi + - name: Run Codex (read-only, structured output) + if: steps.check_changes.outputs.skip != 'true' id: codex uses: openai/codex-action@v1 with: openai-api-key: ${{ secrets.CODEX_OPENAI_KEY }} prompt-file: .github/codex/prompts/update-docs.md output-file: ${{ env.CODEX_OUT }} + model: "gpt-5.1-codex" sandbox: read-only safety-strategy: drop-sudo output-schema-file: .github/codex/codex-output-schema.json - name: Apply & validate patches + if: steps.check_changes.outputs.skip != 'true' id: apply run: | set -euo pipefail @@ -428,23 +646,23 @@ jobs: commit-message: "docs(codex): update Mintlify docs, CHANGELOG & READMEs [auto]" labels: "documentation, automated, codex, release-notes" add-paths: | - docs/** + docs/live/** CHANGELOG.md README.md libs/**/README.md delete-branch: true body: | Codex proposed updates to: - - **docs/updates.mdx** (Mintlify release notes) - - **docs/docs.json** (Mintlify navigation / versions - production) - - **docs/docs/** (latest docs only) + - **docs/live/updates.mdx** (Mintlify release notes - production) + - **docs/live/docs.json** (Mintlify navigation / versions - production) + - **docs/live/docs/** (latest docs only - production) - **CHANGELOG.md** - **README.md** (root) - **libs/**/README.md** (affected publishable libs) for `v${{ steps.ctx.outputs.version }}` on branch `${{ steps.ctx.outputs.branch }}`. - Note: `docs/docs.json` is the production configuration and will be published directly. + Note: `docs/live/` is the production documentation and will be published directly to Mintlify. See `.codex-docs/apply.log` for validation details. diff --git a/.github/workflows/create-release-branch.yml b/.github/workflows/create-release-branch.yml index 5ca40aca..9f3ee8d9 100644 --- a/.github/workflows/create-release-branch.yml +++ b/.github/workflows/create-release-branch.yml @@ -7,7 +7,7 @@ on: description: "Semver bump" required: true type: choice - options: [ patch, minor, major ] + options: [patch, minor, major] default: patch permissions: @@ -17,6 +17,7 @@ permissions: jobs: create: runs-on: ubuntu-latest + environment: release env: # Disable Nx daemon in CI for determinism NX_DAEMON: "false" @@ -25,14 +26,14 @@ jobs: - name: Checkout uses: actions/checkout@v4 with: - fetch-depth: 0 # we need tags & history + fetch-depth: 0 # we need tags & history - name: Setup Node uses: actions/setup-node@v6 with: - node-version-file: '.nvmrc' - cache: 'yarn' - registry-url: 'https://registry.npmjs.org/' + node-version-file: ".nvmrc" + cache: "yarn" + registry-url: "https://registry.npmjs.org/" - name: Install deps run: yarn @@ -85,12 +86,41 @@ jobs: echo "Next version: $NEXT" echo "value=$NEXT" >> "$GITHUB_OUTPUT" + - name: Get current and last release versions + id: versions + shell: bash + run: | + set -euo pipefail + NEXT="${{ steps.next.outputs.value }}" + + # Get last release tag + LAST_TAG=$(git tag --list "v*" --sort=-version:refname | head -n1 || echo "") + if [ -n "$LAST_TAG" ]; then + # Extract version from tag (remove 'v' prefix) + LAST_VERSION="${LAST_TAG#v}" + # Get minor version (e.g., 0.3 from 0.3.1) + LAST_MINOR=$(echo "$LAST_VERSION" | awk -F. '{print $1"."$2}') + else + LAST_VERSION="" + LAST_MINOR="" + fi + + NEXT_MINOR=$(echo "$NEXT" | awk -F. '{print $1"."$2}') + + echo "last_version=$LAST_VERSION" >> "$GITHUB_OUTPUT" + echo "last_minor=$LAST_MINOR" >> "$GITHUB_OUTPUT" + echo "next_version=$NEXT" >> "$GITHUB_OUTPUT" + echo "next_minor=$NEXT_MINOR" >> "$GITHUB_OUTPUT" + + echo "Last release: $LAST_VERSION (minor: $LAST_MINOR)" + echo "Next release: $NEXT (minor: $NEXT_MINOR)" + - name: Create branch next/${{ steps.next.outputs.value }} from base id: branch shell: bash run: | set -euo pipefail - BASE="main" # or ${{ github.event.repository.default_branch }} if you want it dynamic + BASE="main" NEXT="${{ steps.next.outputs.value }}" git fetch origin "$BASE" --tags @@ -99,70 +129,296 @@ jobs: echo "Created branch next/$NEXT from $BASE" echo "name=next/$NEXT" >> "$GITHUB_OUTPUT" - # Let Nx update versions INSIDE this branch (no tags pushed from this workflow) - - name: Version packages with Nx (makes or stages changes) + # ======================================== + # STEP 1: Identify affected independent libraries + # ======================================== + - name: Identify affected independent libraries + id: independent + shell: bash + run: | + set -euo pipefail + + # Get all independent libraries + ALL_INDEPENDENT=$(node -e " + const { execSync } = require('child_process'); + try { + const out = execSync('npx nx show projects -p tag:versioning:independent --type lib --json', { encoding: 'utf8' }); + const arr = JSON.parse(out); + process.stdout.write(arr.join(',')); + } catch (e) { + process.stdout.write(''); + } + ") + + if [ -z "$ALL_INDEPENDENT" ]; then + echo 'independent_projects=' >> "$GITHUB_OUTPUT" + echo "No independent libraries found" + exit 0 + fi + + echo "All independent libs: $ALL_INDEPENDENT" + + # Get affected libraries (changed since last release) + LAST_TAG="${{ steps.versions.outputs.last_version }}" + if [ -n "$LAST_TAG" ]; then + LAST_TAG="v$LAST_TAG" + else + LAST_TAG=$(git rev-list --max-parents=0 HEAD) + fi + + AFFECTED=$(node -e " + const { execSync } = require('child_process'); + try { + const out = execSync('npx nx show projects --affected --base=\"${LAST_TAG}\" --type lib --json', { encoding: 'utf8' }); + const arr = JSON.parse(out); + process.stdout.write(arr.join(',')); + } catch (e) { + process.stdout.write(''); + } + ") + + # Find intersection: affected AND independent + AFFECTED_INDEPENDENT=$(node -e " + const all = '$ALL_INDEPENDENT'.split(',').filter(Boolean); + const affected = '$AFFECTED'.split(',').filter(Boolean); + const intersection = all.filter(lib => affected.includes(lib)); + process.stdout.write(intersection.join(',')); + ") + + echo "independent_projects=$AFFECTED_INDEPENDENT" >> "$GITHUB_OUTPUT" + if [ -n "$AFFECTED_INDEPENDENT" ]; then + echo "Affected independent libs: $AFFECTED_INDEPENDENT" + else + echo "No affected independent libraries" + fi + + # ======================================== + # STEP 2: Use Codex to analyze independent libs + # ======================================== + - name: Prepare context for Codex lib analysis + if: ${{ steps.independent.outputs.independent_projects != '' }} shell: bash run: | set -euo pipefail - BUMP="${{ inputs.bump }}" + mkdir -p .github/codex/lib-analysis - # If there are no existing semver tags, hint Nx this is the first release - if git tag --list | grep -qE '^[v]?[0-9]+\.[0-9]+\.[0-9]+$'; then - FIRST="" + INDEPENDENT="${{ steps.independent.outputs.independent_projects }}" + LAST_TAG="${{ steps.versions.outputs.last_version }}" + if [ -n "$LAST_TAG" ]; then + LAST_TAG="v$LAST_TAG" else - FIRST="--first-release" + LAST_TAG=$(git rev-list --max-parents=0 HEAD) fi + TODAY=$(date -u +"%Y-%m-%d") + + # Collect changes for all independent libraries + IFS=',' read -ra LIBS <<< "$INDEPENDENT" + LIBS_DATA="" + + for lib in "${LIBS[@]}"; do + DIFF=$(git diff "$LAST_TAG"...HEAD -- "libs/$lib" 2>/dev/null || echo "") + COMMITS=$(git log "$LAST_TAG"...HEAD --oneline -- "libs/$lib" 2>/dev/null || echo "") + + if [ -z "$DIFF" ] && [ -z "$COMMITS" ]; then + continue + fi + + CURRENT_VERSION=$(node -e "console.log(require('./libs/$lib/package.json').version)") + + LIBS_DATA+=" + ================================================================================ + LIBRARY: $lib + CURRENT VERSION: $CURRENT_VERSION + ================================================================================ + + COMMITS: + $COMMITS + + DIFF (truncated to 8000 chars): + ${DIFF:0:8000} + + " + done + + # Create single prompt for all libraries + cat > .github/codex/lib-analysis/prompt.md < .github/codex/lib-analysis/schema.json <<'SCHEMA' + { + "type": "object", + "properties": { + "libraries": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "bump": { "type": "string", "enum": ["major", "minor", "patch"] }, + "changelog": { "type": "string", "minLength": 10 } + }, + "required": ["name", "bump", "changelog"], + "additionalProperties": false + } + } + }, + "required": ["libraries"], + "additionalProperties": false + } + SCHEMA - # 🧩 NEW: ensure root package.json version == next/{semver} - - name: Sync root package.json version with next/{semver} + - name: Run Codex to analyze independent libraries + if: ${{ steps.independent.outputs.independent_projects != '' }} + id: codex_libs + uses: openai/codex-action@v1 + with: + openai-api-key: ${{ secrets.CODEX_OPENAI_KEY }} + prompt-file: .github/codex/lib-analysis/prompt.md + output-file: .github/codex/lib-analysis/results.json + sandbox: read-only + safety-strategy: drop-sudo + output-schema-file: .github/codex/lib-analysis/schema.json + + # ======================================== + # STEP 3: Apply all version bumps + # ======================================== + - name: Bump synchronized libraries shell: bash run: | set -euo pipefail NEXT="${{ steps.next.outputs.value }}" - NEXT_VERSION="$NEXT" node - << 'EOF' - const fs = require('fs'); - const path = 'package.json'; - const next = process.env.NEXT_VERSION; + echo "Bumping synchronized libraries to version $NEXT..." + node scripts/bump-synchronized-versions.mjs "$NEXT" - if (!next) { - console.error('NEXT_VERSION env is missing'); - process.exit(1); - } + - name: Bump independent libraries and write changelogs + if: ${{ steps.independent.outputs.independent_projects != '' }} + shell: bash + run: | + set -euo pipefail - const raw = fs.readFileSync(path, 'utf8'); - const pkg = JSON.parse(raw); + RESULTS_FILE=".github/codex/lib-analysis/results.json" + if [ ! -f "$RESULTS_FILE" ]; then + echo "No Codex results found, skipping independent lib bumps" + exit 0 + fi - if (pkg.version === next) { - console.log(`Root package.json already at version ${next}`); - } else { - pkg.version = next; - fs.writeFileSync(path, JSON.stringify(pkg, null, 2) + '\n', 'utf8'); - console.log(`Updated root package.json version -> ${next}`); - } - EOF + # Process each library from Codex results + LIBS_COUNT=$(node -e "const data=require('./${RESULTS_FILE}'); console.log(data.libraries.length)") + + for ((i=0; i "$CHANGELOG_PATH.new" + echo "" >> "$CHANGELOG_PATH.new" + cat "$CHANGELOG_PATH" >> "$CHANGELOG_PATH.new" + mv "$CHANGELOG_PATH.new" "$CHANGELOG_PATH" + else + # Create new changelog + cat > "$CHANGELOG_PATH" < /tmp/commit-msg <= 11.5.1) - name: Update npm CLI for trusted publishing @@ -301,55 +301,12 @@ jobs: fi # ------------------------------- - # 7) Bump versions before publishing + # 7) Build packages (versions already set in release branch) # ------------------------------- - - name: Bump synchronized library versions - if: ${{ steps.synchronized.outputs.synchronized_projects != '' }} - shell: bash - run: | - set -euo pipefail - VERSION="${{ steps.version.outputs.version }}" - - echo "Bumping synchronized libraries to version $VERSION..." - node scripts/bump-synchronized-versions.mjs "$VERSION" - - - name: Bump affected independent library versions (patch) - if: ${{ steps.independent.outputs.independent_projects != '' }} - shell: bash - run: | - set -euo pipefail - - INDEPENDENT="${{ steps.independent.outputs.independent_projects }}" - if [ -z "$INDEPENDENT" ]; then - echo "No independent libraries to bump" - exit 0 - fi - - # Split by comma and bump each library - IFS=',' read -ra LIBS <<< "$INDEPENDENT" - for lib in "${LIBS[@]}"; do - echo "Bumping $lib (patch)..." - node scripts/bump-version.mjs "$lib" patch - done - - - name: Commit version bumps - if: ${{ steps.to_publish.outputs.projects != '' }} - shell: bash - run: | - set -euo pipefail - - # Check if there are version changes - if [ -n "$(git status --porcelain)" ]; then - git add -A - git commit -m "chore: bump versions for release ${{ steps.version.outputs.version }}" - echo "✅ Version bumps committed" - else - echo "ℹ️ No version changes to commit" - fi - - - name: Rebuild after version bumps + - name: Build all packages to publish if: ${{ steps.to_publish.outputs.projects != '' }} run: | + echo "Building packages (versions already set in release branch)..." npx nx run-many --targets=build --projects="${{ steps.to_publish.outputs.projects }}" --parallel # ------------------------------- @@ -370,9 +327,9 @@ jobs: done # ------------------------------- - # 9) Push release commits (version bumps & draft removals) + # 9) Push release commits (draft removals only) # ------------------------------- - - name: Push version bumps and draft removals + - name: Push draft blog card removals if: ${{ steps.to_publish.outputs.projects != '' }} shell: bash run: | diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index f38d3f25..0a0c2fe7 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -2,6 +2,10 @@ name: On Push on: push +concurrency: + group: codex-docs-${{ github.ref }} + cancel-in-progress: false + jobs: build: runs-on: ubuntu-latest @@ -14,21 +18,11 @@ jobs: with: fetch-depth: 0 - - name: Check for docs backup file - run: | - if [ -f "docs/docs.backup.json" ]; then - echo "::error::docs/docs.backup.json should not be committed to the repository" - echo "::error::This file is for local development only (used by scripts/dev-docs.sh)" - echo "::error::Please remove it before pushing" - exit 1 - fi - echo "✓ No docs backup file found" - - name: Setup Node uses: actions/setup-node@v6 with: - node-version-file: '.nvmrc' - cache: 'yarn' + node-version-file: ".nvmrc" + cache: "yarn" - name: Install dependencies run: yarn install --frozen-lockfile diff --git a/.github/workflows/update-draft-docs.yml b/.github/workflows/update-draft-docs.yml new file mode 100644 index 00000000..605a97ec --- /dev/null +++ b/.github/workflows/update-draft-docs.yml @@ -0,0 +1,453 @@ +name: "Codex → PR: update draft docs (main)" + +on: + push: + branches: + - main + # Prevent loop: if a push ONLY touches these paths, Codex won't run + paths-ignore: + - "docs/draft/**" + - "docs/live/**" + - "CHANGELOG.md" + - "libs/**/README.md" + - "libs/**/CHANGELOG.md" + - "README.md" + - ".github/workflows/update-draft-docs.yml" + + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +concurrency: + group: codex-draft-docs-${{ github.ref }} + cancel-in-progress: false + +jobs: + update-draft: + # Don't run for release merges (those already have their docs) + if: "!startsWith(github.event.head_commit.message, 'chore(release):')" + runs-on: ubuntu-latest + environment: release + + env: + CODEX_OUT: .codex-draft-docs/patches.json + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + # Fail fast if env secret is not available + - name: Ensure CODEX_OPENAI_KEY secret is set + run: | + if [ -z "${{ secrets.CODEX_OPENAI_KEY }}" ]; then + echo "::error::CODEX_OPENAI_KEY (env: release) is not set. Add it under Settings → Environments → release → Secrets." >&2 + exit 1 + fi + + - name: Setup Node + uses: actions/setup-node@v6 + with: + node-version-file: ".nvmrc" + cache: "yarn" + registry-url: "https://registry.npmjs.org/" + + - name: Install validator deps + run: yarn install + + # ======================================== + # MCP Integration: Query Mintlify docs for context + # ======================================== + - name: Setup MCP server for enhanced context + id: mcp + shell: bash + run: | + set -euo pipefail + mkdir -p .github/codex/mcp-context + + echo "Setting up Mintlify MCP context..." + + # Query Mintlify MCP for documentation best practices + cat > .github/codex/mcp-context/mintlify-guidelines.md <<'GUIDELINES' + # Mintlify Documentation Guidelines + + ## Structure + - Use clear, hierarchical organization + - Group related pages under common sections + - Keep navigation depth to 3 levels max + + ## Content + - Start with conceptual overview, then details + - Include code examples for all features + - Use callouts for important notes (Note, Warning, Tip) + - Keep paragraphs short and scannable + + ## Versioning + - Draft docs are for the next release + - Use current paths (no version prefix) in draft docs + - Draft content will be moved to live on release + + ## Code Blocks + - Specify language for syntax highlighting + - Include comments for complex examples + - Show both TypeScript and JavaScript when relevant + + ## Navigation (docs.json) + - Each version has its own groups array + - Use descriptive group names + - Add icons to enhance visual hierarchy + GUIDELINES + + echo "✓ MCP context prepared" + echo "mcp_enabled=true" >> "$GITHUB_OUTPUT" + + - name: Prepare diff context for Codex + id: ctx + shell: bash + run: | + set -euo pipefail + mkdir -p .github/codex/prompts .codex-draft-docs + + # ---------------------------------------- + # Base for diff/log = last published tag + # ---------------------------------------- + LAST_TAG=$( + git tag --merged HEAD \ + | grep -E '^v?[0-9]+\.[0-9]+\.[0-9]+$' \ + | sort -V \ + | tail -n 1 \ + || true + ) + + if [ -n "$LAST_TAG" ]; then + BASE=$(git rev-list -n 1 "$LAST_TAG") + echo "Using last published version tag as base: ${LAST_TAG} (${BASE})" + else + echo "No semver release tag reachable from HEAD; falling back to last 10 commits." + BASE=$(git rev-parse HEAD~10) || BASE=$(git rev-list --max-parents=0 HEAD) + fi + + echo "$BASE" > .github/codex/prompts/base.txt + + # Unified diff: from last published release -> current main HEAD + # Filter to relevant files only to reduce token usage + git diff "$BASE"...HEAD --unified=1 \ + -- \ + 'libs/**/*.ts' \ + 'apps/**/*.ts' \ + 'libs/**/package.json' \ + 'package.json' \ + 'docs/**/*.md' \ + 'docs/**/*.mdx' \ + 'README.md' \ + 'CHANGELOG.md' \ + ':!**/*.spec.ts' \ + ':!**/*.test.ts' \ + > .github/codex/prompts/diff.patch || true + + # ---------------------------------------- + # Derive next version from package.json + # ---------------------------------------- + VERSION=$(node -e "try{console.log(require('./package.json').version || '0.0.0')}catch{console.log('0.0.0')}") + echo "Current version: $VERSION (this is the next release version for draft docs)" + + echo "$VERSION" > .github/codex/prompts/version.txt + + # Commit list (for documentation synthesis) + # Limit to 100 commits to prevent unbounded growth + git log "$BASE"...HEAD --pretty=format:'%H%x09%s%x09%b' -n 100 > .github/codex/prompts/commits.txt || true + + # ISO date + date -u +"%Y-%m-%d" > .github/codex/prompts/date.txt + + - name: Add Codex prompt & schema + run: | + cat > .github/codex/prompts/update-draft-docs.md <<'MDX' + You are Codex. Task: update **draft** documentation (under `docs/draft/`) based on changes merged to the main branch. + + INPUTS (read-only files in workspace): + - Unified diff: `.github/codex/prompts/diff.patch` + - Commit list (TSV): `.github/codex/prompts/commits.txt` # columns: shasubjectbody + - Next version: `.github/codex/prompts/version.txt` # e.g., 0.4.1 + - Date (UTC, ISO): `.github/codex/prompts/date.txt` # e.g., 2025-11-22 + - MCP Context Guidelines: `.github/codex/mcp-context/mintlify-guidelines.md` + + DOCS LAYOUT + + **IMPORTANT: Follow the MCP guidelines in `.github/codex/mcp-context/mintlify-guidelines.md`** + + Folders: + - `docs/draft/docs/**` + - **Draft** documentation for the next release + - Paths use `docs/...` prefix (no version prefix) + - These docs will be moved to `docs/live/docs/` on release + - `docs/draft/blog/**` + - Draft blog posts (do NOT modify) + - `docs/draft/assets/**` + - Draft assets (images, etc.) + - `docs/draft/snippets/**` + - Draft code snippets + - `docs/draft/docs.json` + - Draft navigation configuration (currently same as live, will be used in future) + - `docs/draft/updates.mdx` + - Draft release notes for the next version + - `docs/live/**` + - **Production** docs (do NOT modify) + + SCOPE & PATHS + + You are allowed to modify/create only: + + - Draft docs: + - `docs/draft/docs/**/*.mdx` + - `docs/draft/docs/**/*.md` + - Draft release notes: + - `docs/draft/updates.mdx` + - Draft assets (if needed): + - `docs/draft/assets/**` + - Draft snippets (if needed): + - `docs/draft/snippets/**` + + You must **not** touch: + - `docs/live/**` (production docs) + - `docs/draft/blog/**` (blog posts) + - `docs/draft/docs.json` (for now, we'll use it later) + or any other files outside the allowed list. + + RULES – DRAFT RELEASE NOTES (`docs/draft/updates.mdx`) + + **CRITICAL: Preserve frontmatter metadata** + - The file starts with YAML frontmatter: + ```yaml + --- + title: 'Updates' + slug: 'updates' + icon: 'sparkles' + mode: 'center' + --- + ``` + - This frontmatter is **critical** for Mintlify to detect the page + - **MUST** be preserved exactly as-is at the top of the file + + **Draft updates structure:** + + The draft updates.mdx contains updates for the NEXT release. Update with cumulative changes: + + 1. **FrontMCP (synchronized packages)** - One draft update: + - Label: `"draft"` + - Title: `"FrontMCP draft"` + - href: `"/docs"` + - cta: `"Read the release notes"` + - Tags: `{["Releases"]}` + - Content: **User-friendly highlights** (NOT technical changelog) + - Use emojis at start of each line (🚀 🛡️ 🔐 📚 ⚡ 🎨 🔧 🧩 etc.) + - Format: `emoji **Bold feature name** – Description of what users can do.` + - Each feature on its own line + - Focus on benefits and practical capabilities + - Keep cumulative (all changes since last release) + - NO changelog link (this is draft) + + 2. **Independent libraries** - If working on independent libs: + - Label: `" draft"` + - Title: `" draft"` + - href: `"/docs"` + - cta: `"Read the release notes"` + - Tags: `{["Independent"]}` + - Content: **User-friendly highlights** + - Use emojis at start of each line + - Format: `emoji **Bold feature name** – Description.` + - Each feature on its own line + - Benefit-focused descriptions + - NO changelog link (this is draft) + + Format example: + ```mdx + --- + title: 'Updates' + slug: 'updates' + icon: 'sparkles' + mode: 'center' + --- + + + + 🚀 **Real-time Streaming** – Build live experiences with the new streaming API and backpressure support. + + 🛡️ **Better Error Messages** – Debug faster with detailed stack traces and user-friendly error descriptions. + + 📚 **New Guides** – Learn streaming patterns and authentication flows with practical examples. + + + ``` + + - Keep the release notes cumulative (all changes since last release) + - Update the description date to current date + - Group items under: Breaking changes, Features, Fixes, Docs, Performance, Refactor, Build/CI (omit empty groups) + - Keep MDX valid (frontmatter/imports/components) + + RULES – DRAFT DOCS (`docs/draft/docs/**`) + + - Update documentation to reflect the latest changes in the codebase + - Add new pages for new features + - Update existing pages for changed features + - Keep tone and style consistent with existing docs + - Use clear, concise language + - Include code examples for new features + - Use callouts (Note, Warning, Tip) appropriately + + GENERAL + + - Prefer small, surgical edits + - Never modify: + - `docs/live/**` + - `docs/draft/blog/**` + - Always keep: + - `docs/draft/docs/**` consistent with actual code behavior + - `docs/draft/updates.mdx` up-to-date with latest changes + - If no changes are needed, return an empty patch set + + OUTPUT (STRICT) + + - Return JSON that matches the output schema provided by the runner + - Each patch replaces the entire file content + + HINTS + + - Use subjects/bodies in `commits.txt` to infer what changed + - Focus on user-facing changes (new APIs, changed behavior, new features) + - Skip internal refactoring unless it affects public API + - De-duplicate merge commits + + Produce ONLY the JSON per schema. + MDX + + cat > .github/codex/codex-output-schema.json <<'JSON' + { + "type": "object", + "properties": { + "patches": { + "type": "array", + "items": { + "type": "object", + "properties": { + "path": { + "type": "string", + "pattern": "^docs\\/draft\\/(docs\\/.+\\.(md|mdx)|updates\\.mdx|assets\\/.+|snippets\\/.+)$" + }, + "content": { "type": "string", "minLength": 1, "maxLength": 500000 } + }, + "required": ["path", "content"], + "additionalProperties": false + } + } + }, + "required": ["patches"], + "additionalProperties": false + } + JSON + + - name: Check if changes warrant Codex run + id: check_changes + shell: bash + run: | + set -euo pipefail + DIFF_SIZE=$(wc -c < .github/codex/prompts/diff.patch || echo "0") + if [ "$DIFF_SIZE" -lt 100 ]; then + echo "skip=true" >> "$GITHUB_OUTPUT" + echo "Diff too small ($DIFF_SIZE bytes), skipping Codex" + else + echo "skip=false" >> "$GITHUB_OUTPUT" + echo "Diff size: $DIFF_SIZE bytes, running Codex" + fi + + - name: Run Codex (read-only, structured output) + if: steps.check_changes.outputs.skip != 'true' + id: codex + uses: openai/codex-action@v1 + with: + openai-api-key: ${{ secrets.CODEX_OPENAI_KEY }} + prompt-file: .github/codex/prompts/update-draft-docs.md + output-file: ${{ env.CODEX_OUT }} + model: "gpt-5.1-codex" + sandbox: read-only + safety-strategy: drop-sudo + output-schema-file: .github/codex/codex-output-schema.json + + - name: Apply & validate patches + if: steps.check_changes.outputs.skip != 'true' + id: apply + run: | + set -euo pipefail + + node scripts/apply-doc-patches.mjs "$CODEX_OUT" | tee .codex-draft-docs/apply.log + + # Any change in the working tree means Codex actually updated something + if [ -n "$(git status --porcelain)" ]; then + echo "changed=true" >> "$GITHUB_OUTPUT" + else + echo "changed=false" >> "$GITHUB_OUTPUT" + fi + + - name: Delete existing draft docs branch (if any) + if: ${{ steps.apply.outputs.changed == 'true' }} + shell: bash + run: | + set -euo pipefail + BRANCH="docs/codex/draft-update" + + echo "Checking for existing branch ${BRANCH}..." + + # Fetch the branch ref if it exists on origin + git fetch origin "+refs/heads/${BRANCH}:refs/remotes/origin/${BRANCH}" || true + + if git show-ref --verify --quiet "refs/remotes/origin/${BRANCH}"; then + echo "Remote branch ${BRANCH} exists. Deleting remote..." + git push origin --delete "${BRANCH}" || true + else + echo "No remote branch ${BRANCH}." + fi + + if git show-ref --verify --quiet "refs/heads/${BRANCH}"; then + echo "Local branch ${BRANCH} exists. Deleting local..." + git branch -D "${BRANCH}" || true + else + echo "No local branch ${BRANCH}." + fi + + - name: Create PR with draft docs updates + if: ${{ steps.apply.outputs.changed == 'true' }} + uses: peter-evans/create-pull-request@v7 + with: + branch: docs/codex/draft-update + base: main + title: "docs: update draft docs for next release (Codex)" + commit-message: "docs(codex): update draft docs [auto]" + labels: "documentation, automated, codex, draft" + add-paths: | + docs/draft/** + delete-branch: true + body: | + Codex proposed updates to draft documentation based on recent changes to main: + - **docs/draft/docs/** (draft docs for next release) + - **docs/draft/updates.mdx** (draft release notes) + - **docs/draft/assets/** (draft assets) + - **docs/draft/snippets/** (draft snippets) + + These changes will be published to live docs on the next release. + + See `.codex-draft-docs/apply.log` for validation details. + + - name: Upload artifacts (debug) + uses: actions/upload-artifact@v4 + with: + name: codex-draft-docs-artifacts + path: | + .codex-draft-docs/** + .github/codex/prompts/** diff --git a/.prettierignore b/.prettierignore index b5a19815..b897a035 100644 --- a/.prettierignore +++ b/.prettierignore @@ -12,3 +12,8 @@ lib .tmp .next out + +# Mintlify navigation files only (preserve exact structure) +docs/live/docs.json +docs/docs.draft.json +docs/docs.backup.json diff --git a/.prettierrc b/.prettierrc index e430705b..82875a3c 100644 --- a/.prettierrc +++ b/.prettierrc @@ -14,16 +14,35 @@ "proseWrap": "preserve", "overrides": [ { - "files": "*.md", + "files": ["*.md"], "options": { - "proseWrap": "always" + "proseWrap": "preserve" } }, { - "files": [ - "*.yml", - "*.yaml" - ], + "files": ["*.mdx"], + "options": { + "proseWrap": "preserve", + "printWidth": 120, + "embeddedLanguageFormatting": "off", + "jsxSingleQuote": false, + "bracketSameLine": false + } + }, + { + "files": ["docs/**/*.mdx", "docs/**/*.md"], + "options": { + "proseWrap": "preserve", + "printWidth": 120, + "embeddedLanguageFormatting": "off", + "htmlWhitespaceSensitivity": "ignore", + "jsxSingleQuote": false, + "bracketSameLine": false, + "tabWidth": 2 + } + }, + { + "files": ["*.yml", "*.yaml"], "options": { "singleQuote": false } diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e8f1821..33b27d43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,21 @@ +## [v0.4.0] - 2025-11-22 + +### feat + +- Publish the standalone `mcp-from-openapi` generator and wire the OpenAPI adapter to it so every tool inherits request mappers, conflict-free schemas, and per-scheme authentication analysis. +- Allow `@Tool` metadata to declare literal primitives, tuple-style arrays, and MCP resources (plus `rawInputSchema`) so clients get typed responses without wrapping outputs in placeholder objects. +- Add a typed MCP error hierarchy and error handler so transports emit traceable IDs, consistent public/internal messages, and FlowControl-aware stop semantics. +- Extract `json-schema-to-zod-v3` with built-in regex guards so adapters and apps can reuse the hardened JSON Schema → Zod converter. + +### docs + +- Document OpenAPI adapter security scoring, auth-provider mapping, generator options, and the CodeCall plugin’s search/describe/execute workflow. +- Publish maintainer runbooks for the release workflow and doc versioning so contributors know when to edit draft vs live docs. + +### build + +- Split draft/live Mintlify trees, auto-archive previous minors, and enforce husky + lint-staged guards so release branches stay focused on intentional changes. + ## [v0.3.1] - 2025-11-16 ### feat diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..765bc10d --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,106 @@ +# Contributing to FrontMCP + +Thanks for helping improve FrontMCP! This repo hosts the core SDK, CLI, adapters, plugins, docs, and the demo app. The +project follows the [Code of Conduct](./CODE_OF_CONDUCT.md); by participating you agree to uphold it. + +## Ways to Contribute + +- Report reproducible bugs, missing docs, or regression risks via GitHub issues. +- Improve docs (`/docs`), snippets, or the demo app in `apps/demo`. +- Fix bugs or add features inside the packages under `libs/*`. +- Build adapters/plugins/examples that showcase how the SDK should be used. + +If you are unsure whether a change fits, open an issue first so we can discuss the scope. + +## Prerequisites and Tooling + +- Node.js **>= 22** and npm **>= 10** (see README installation section). +- Yarn (this workspace enables Corepack and sets `"packageManager": "yarn"` in `nx.json`). Run `corepack enable` once if + needed. +- Nx CLI (`yarn nx --help`) powers builds, lint, tests, and type-checking. +- Git and a GitHub account for forks/PRs. + +First-time setup: + +```bash +git clone https://github.com/agentfront/frontmcp.git +cd frontmcp +corepack enable +yarn install +``` + +## Workspace Layout + +This is an Nx monorepo. Each folder under `libs/*` is an independently built package (for example `libs/sdk`, +`libs/cli`, `libs/plugins`, `libs/adapters`). The demo and any showcase apps live under `apps/*`. Project-specific tasks +are declared in each `project.json`. + +Helpful references: + +- `README.md` for a high-level overview and quickstart. +- `CHANGELOG.md` for release notes—update it when user-facing behavior changes. +- `docs/` for the Mintlify-based documentation site (`yarn docs:local` runs a local preview). + +## Day-to-Day Commands + +```bash +yarn dev # nx serve demo (demo server hot reload) +yarn nx test sdk # run Jest tests for libs/sdk +yarn nx lint plugins # lint a specific project +yarn nx run-many -t lint,test # run lint+test for everything +yarn nx run-many -t build # build all publishable packages +yarn docs:local # preview the docs site locally +yarn nx affected --target test --base main # limit CI work to changed projects +``` + +Before opening a PR, run at least `yarn nx run-many -t lint,test` and `yarn nx run-many -t build`. If you changed +transport/session/auth flows, also try the Inspector (`npx frontmcp inspector`) to verify end-to-end behavior. + +## Coding Standards + +- Use TypeScript, modern ESM syntax, and decorators that align with the SDK’s existing patterns. +- Keep `@frontmcp/*` versions aligned inside examples (see README “Version Alignment”). +- Prefer strongly typed Zod schemas (pass fields directly, matching current code style). +- Keep changes focused; split unrelated work across multiple PRs. +- Formatting is enforced by Prettier and lint rules (run `yarn nx lint ` locally). Husky + lint-staged will + format staged files on commit. +- Add concise comments only when necessary to explain non-obvious intent. + +## Testing Expectations + +- Every bug fix or feature should include/adjust Jest specs alongside the code (`*.spec.ts`). +- Use Nx test targets close to the code (`yarn nx test sdk`, `yarn nx test cli`, etc.). +- Integration behavior that spans packages should be validated in the demo app (spin up `yarn dev`) and, when possible, + through Inspector flows. +- If you touch build tooling, ensure `yarn nx run-many -t build` succeeds and that emitted artifacts under `dist/` look + reasonable. + +## Documentation and Samples + +- Docs live in `docs/` (Mintlify). After editing, run `yarn docs:local` to confirm the nav/build passes. +- Keep `README.md` in sync with notable DX changes (new scripts, requirements, etc.). +- Update code snippets under `docs/snippets` if your change alters their behavior. +- Demo updates should remain scoped and easy to understand; prefer creating a new sample app/tool over overloading + existing ones. + +## Pull Request Checklist + +1. Open/mention an issue for significant changes. +2. Rebase on the latest `main`; keep history clean (squash in your fork if needed). +3. Add or update unit tests plus docs/snippets. +4. Run: + - `yarn nx run-many -t lint,test` + - `yarn nx run-many -t build` + - `yarn docs:local` (if docs changed) + - `npx frontmcp doctor` and try Inspector if your change affects runtime behavior. +5. Update `CHANGELOG.md` when the change is user-facing. +6. Include screenshots or logs when the change affects dev tooling or Inspector UX. + +Maintainers handle publishing; do not run manual `npm publish` steps. + +## Communication + +- For security issues, email `david@frontegg.com` instead of opening a public issue. +- For general help, use GitHub Discussions/Issues or reach out during office hours if announced. + +We appreciate every contribution—thank you for making FrontMCP better! diff --git a/README.md b/README.md index 3bc6c338..099e0862 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ import HelloApp from './hello.app'; logging: { level: LogLevel.Info }, }) export default class Server {} -```` +``` --- @@ -46,7 +46,10 @@ export default class Server {} - [Why FrontMCP?](#why-frontmcp) - [Installation](#installation) - [Quickstart](#quickstart) - - [Minimal Server & App](#minimal-server--app) + - [1) Create Your FrontMCP Server](#1-create-your-frontmcp-server) + - [2) Define an App](#2-define-an-app) + - [3) Add Your First Tool](#3-add-your-first-tool) + - [4) Run It](#4-run-it) - [Function and Class Tools](#function-and-class-tools) - [Scripts & tsconfig](#scripts--tsconfig) - [Inspector](#inspector) @@ -72,12 +75,10 @@ export default class Server {} ## Why FrontMCP? -- **TypeScript-native DX** — decorators, Zod, strong typing end-to-end -- **Spec-aligned transport** — Streamable HTTP, sessions, server‑pushed events +- **TypeScript-native DX** — decorators, Zod, and strong typing end-to-end - **Scoped invoker + DI** — secure, composable execution with hooks -- **Adapters & Plugins** — generate tools from OpenAPI; add cross-cutting behavior -- **Auth** — remote OAuth (external IdP) or built-in local OAuth -- **Logging** — pluggable log transports +- **Adapters & Plugins** — extend your server without boilerplate +- **Spec-aligned transport** — Streamable HTTP for modern MCP clients --- @@ -91,7 +92,8 @@ export default class Server {} npx frontmcp create my-app ``` -This scaffolds a FrontMCP project, writes a modern ESM `tsconfig.json` for decorators, adds helpful package scripts, and installs required dev deps. ([Installation - FrontMCP][1]) +This scaffolds a FrontMCP project, writes a modern ESM `tsconfig.json` for decorators, adds helpful package scripts, and +installs required dev deps. ([Installation - FrontMCP][1]) ### Option B — Add to an existing project @@ -100,26 +102,19 @@ npm i -D frontmcp @types/node@^20 npx frontmcp init ``` -`init` adds scripts, verifies your `tsconfig.json`, and checks layout. No need to install `@frontmcp/sdk` directly—the CLI bundles a compatible SDK for you. ([Installation - FrontMCP][1]) +`init` adds scripts, verifies your `tsconfig.json`, and checks layout. No need to install `@frontmcp/sdk` directly—the +CLI bundles a compatible SDK for you. ([Installation - FrontMCP][1]) --- ## Quickstart -### Minimal Server & App - -``` -src/ - main.ts - hello.app.ts - tools/ - greet.tool.ts -``` +If you haven’t installed FrontMCP yet, follow the [installation guide][1] first. -**`src/main.ts`** +### 1) Create Your FrontMCP Server ```ts -import 'reflect-metadata'; +// src/main.ts import { FrontMcp, LogLevel } from '@frontmcp/sdk'; import HelloApp from './hello.app'; @@ -127,24 +122,73 @@ import HelloApp from './hello.app'; info: { name: 'Hello MCP', version: '0.1.0' }, apps: [HelloApp], http: { port: 3000 }, - logging: { level: LogLevel.Info }, + logging: { level: LogLevel.INFO }, }) -export default class Server {} +export default class HelloServer {} ``` -**`src/hello.app.ts`** +### 2) Define an App ```ts +// src/hello.app.ts import { App } from '@frontmcp/sdk'; import GreetTool from './tools/greet.tool'; -@App({ id: 'hello', name: 'Hello', tools: [GreetTool] }) +@App({ + id: 'hello', + name: 'Hello App', + tools: [GreetTool], +}) export default class HelloApp {} ``` +### 3) Add Your First Tool + +```ts +// src/tools/greet.tool.ts +import { Tool } from '@frontmcp/sdk'; +import { z } from 'zod'; + +@Tool({ + name: 'greet', + description: 'Greets a user by name', + inputSchema: { name: z.string() }, +}) +export default class GreetTool { + async execute({ name }: { name: string }) { + return `Hello, ${name}!`; + } +} +``` + +### 4) Run It + +Add scripts (if you didn't use `frontmcp create`): + +```json +{ + "scripts": { + "dev": "frontmcp dev", + "build": "frontmcp build", + "start": "node dist/main.js" + } +} +``` + +Then: + +```bash +npm run dev +# Server listening on http://localhost:3000 +``` + +> **Tip:** FrontMCP speaks **MCP Streamable HTTP**. Connect any MCP-capable client and call `greet` with +> `{"name": "Ada"}`. ([Quickstart - FrontMCP][2]) + ### Function and Class Tools -> New ergonomic schemas: pass **Zod fields directly** (no `z.object({...})`). ([Tools - FrontMCP][4]) +Tools are typed actions with Zod schemas. Implement them as classes with `@Tool({...})` or inline via `tool()`. Pass +**Zod fields directly** (no `z.object({...})`). ([Tools - FrontMCP][4]) **Function tool** @@ -192,7 +236,8 @@ After `create` or `init`, you’ll have: } ``` -These map to dev watch, production build, zero‑setup Inspector launch, and environment checks. ([Installation - FrontMCP][1]) +These map to dev watch, production build, zero‑setup Inspector launch, and environment checks. ([Installation - +FrontMCP][1]) **Recommended `tsconfig.json` (ESM + decorators)** @@ -241,7 +286,8 @@ Run a browser UI to exercise tools and messages: npm run inspect ``` -This launches the MCP Inspector; point it at your local server (e.g., `http://localhost:3000`). ([Local Dev Server - FrontMCP][3]) +This launches the MCP Inspector; point it at your local server (e.g., `http://localhost:3000`). ([Local Dev Server - +FrontMCP][3]) --- @@ -249,15 +295,18 @@ This launches the MCP Inspector; point it at your local server (e.g., `http://lo ### Servers -`@FrontMcp({...})` defines **info**, **apps**, **http**, **logging**, **session**, and optional **auth**. Keep it minimal or scale up with providers and plugins. ([The FrontMCP Server - FrontMCP][5]) +`@FrontMcp({...})` defines **info**, **apps**, **http**, **logging**, **session**, and optional **auth**. Keep it +minimal or scale up with providers and plugins. ([The FrontMCP Server - FrontMCP][5]) ### Apps -Use `@App` to group **tools**, **resources**, **prompts**, plus **providers**, **adapters**, and **plugins**. With `splitByApp: true`, each app gets its own scope/base path and, if needed, its own auth surface. ([Apps - FrontMCP][6]) +Use `@App` to group **tools**, **resources**, **prompts**, plus **providers**, **adapters**, and **plugins**. With +`splitByApp: true`, each app gets its own scope/base path and, if needed, its own auth surface. ([Apps - FrontMCP][6]) ### Tools -Typed actions with schemas (class `@Tool` or inline `tool({...})(handler)`). Use the Zod‑field **shape** style for `inputSchema`. ([Tools - FrontMCP][4]) +Typed actions with schemas (class `@Tool` or inline `tool({...})(handler)`). Use the Zod‑field **shape** style for +`inputSchema`. ([Tools - FrontMCP][4]) ### Resources @@ -269,13 +318,15 @@ Reusable templates returning MCP `GetPromptResult`, with typed arguments. ([Prom ### Providers / Adapters / Plugins -Inject shared services, generate tools from OpenAPI, and add cross‑cutting behavior like caching and hooks. ([Add OpenAPI Adapter - FrontMCP][9]) +Inject shared services, generate tools from OpenAPI, and add cross‑cutting behavior like caching and hooks. ([Add +OpenAPI Adapter - FrontMCP][9]) --- ## Authentication -Configure auth at the **server** (shared) or **per app** (isolated). With `splitByApp: true`, define auth **per app** (server‑level `auth` is disallowed). ([Authentication - FrontMCP][10]) +Configure auth at the **server** (shared) or **per app** (isolated). With `splitByApp: true`, define auth **per app** +(server‑level `auth` is disallowed). ([Authentication - FrontMCP][10]) ### Remote OAuth @@ -355,23 +406,26 @@ npm run build NODE_ENV=production PORT=8080 npm start ``` -Builds to `dist/` (uses `tsconfig.build.json`). Consider a process manager and reverse proxy; align all `@frontmcp/*` versions. ([Production Build - FrontMCP][13]) +Builds to `dist/` (uses `tsconfig.build.json`). Consider a process manager and reverse proxy; align all `@frontmcp/*` +versions. ([Production Build - FrontMCP][13]) --- ## Version Alignment -If versions drift, the runtime will throw a clear **“version mismatch”** at boot. Keep `@frontmcp/*` versions aligned. ([Production Build - FrontMCP][13]) +If versions drift, the runtime will throw a clear **“version mismatch”** at boot. Keep `@frontmcp/*` versions aligned. +([Production Build - FrontMCP][13]) --- ## Contributing -PRs welcome! Please: - -- Keep changes focused and tested -- Run `doctor`, `dev`, and `build`; try **Inspector** locally -- Align `@frontmcp/*` versions in examples +- PRs welcome! Read the full [CONTRIBUTING.md](./CONTRIBUTING.md) for workflow details, coding standards, and the PR + checklist. +- Keep changes focused, add/adjust Jest specs, and update docs/snippets when behavior changes. +- Before opening a PR run `yarn nx run-many -t lint,test,build`, `npx frontmcp doctor`, and verify the demo/Inspector + flows relevant to your change. +- Align `@frontmcp/*` versions in examples to avoid runtime version mismatches. --- @@ -379,19 +433,16 @@ PRs welcome! Please: See [LICENSE](./LICENSE). - - - -[1]: https://docs.agentfront.dev/0.3/getting-started/installation "Installation - FrontMCP" -[2]: https://docs.agentfront.dev/0.3/getting-started/quickstart "Quickstart - FrontMCP" -[3]: https://docs.agentfront.dev/0.3/deployment/local-dev-server "Local Dev Server - FrontMCP" -[4]: https://docs.agentfront.dev/0.3/servers/tools "Tools - FrontMCP" -[5]: https://docs.agentfront.dev/0.3/servers/server "The FrontMCP Server - FrontMCP" -[6]: https://docs.agentfront.dev/0.3/servers/apps "Apps - FrontMCP" -[7]: https://docs.agentfront.dev/0.3/servers/resources "Resources - FrontMCP" -[8]: https://docs.agentfront.dev/0.3/servers/prompts "Prompts - FrontMCP" -[9]: https://docs.agentfront.dev/0.3/guides/add-openapi-adapter "Add OpenAPI Adapter - FrontMCP" -[10]: https://docs.agentfront.dev/0.3/servers/authentication/overview "Authentication - FrontMCP" -[11]: https://docs.agentfront.dev/0.3/servers/authentication/remote "Remote OAuth - FrontMCP" -[12]: https://docs.agentfront.dev/0.3/servers/authentication/local "Local OAuth - FrontMCP" -[13]: https://docs.agentfront.dev/0.3/deployment/production-build "Production Build - FrontMCP" +[1]: https://docs.agentfront.dev/docs/getting-started/installation 'Installation - FrontMCP' +[2]: https://docs.agentfront.dev/docs/getting-started/quickstart 'Quickstart - FrontMCP' +[3]: https://docs.agentfront.dev/docs/deployment/local-dev-server 'Local Dev Server - FrontMCP' +[4]: https://docs.agentfront.dev/docs/servers/tools 'Tools - FrontMCP' +[5]: https://docs.agentfront.dev/docs/servers/server 'The FrontMCP Server - FrontMCP' +[6]: https://docs.agentfront.dev/docs/servers/apps 'Apps - FrontMCP' +[7]: https://docs.agentfront.dev/docs/servers/resources 'Resources - FrontMCP' +[8]: https://docs.agentfront.dev/docs/servers/prompts 'Prompts - FrontMCP' +[9]: https://docs.agentfront.dev/docs/guides/add-openapi-adapter 'Add OpenAPI Adapter - FrontMCP' +[10]: https://docs.agentfront.dev/docs/servers/authentication/overview 'Authentication - FrontMCP' +[11]: https://docs.agentfront.dev/docs/servers/authentication/remote 'Remote OAuth - FrontMCP' +[12]: https://docs.agentfront.dev/docs/servers/authentication/local 'Local OAuth - FrontMCP' +[13]: https://docs.agentfront.dev/docs/deployment/production-build 'Production Build - FrontMCP' diff --git a/docs/assets/banners/frontmcp-vs-standardmcp.dark.png b/docs/draft/assets/banners/frontmcp-vs-standardmcp.dark.png similarity index 100% rename from docs/assets/banners/frontmcp-vs-standardmcp.dark.png rename to docs/draft/assets/banners/frontmcp-vs-standardmcp.dark.png diff --git a/docs/assets/banners/frontmcp-vs-standardmcp.light.png b/docs/draft/assets/banners/frontmcp-vs-standardmcp.light.png similarity index 100% rename from docs/assets/banners/frontmcp-vs-standardmcp.light.png rename to docs/draft/assets/banners/frontmcp-vs-standardmcp.light.png diff --git a/docs/assets/banners/mcp-run-out-of-socket.svg b/docs/draft/assets/banners/mcp-run-out-of-socket.svg similarity index 100% rename from docs/assets/banners/mcp-run-out-of-socket.svg rename to docs/draft/assets/banners/mcp-run-out-of-socket.svg diff --git a/docs/assets/banners/openapi-mcp-security.png b/docs/draft/assets/banners/openapi-mcp-security.png similarity index 100% rename from docs/assets/banners/openapi-mcp-security.png rename to docs/draft/assets/banners/openapi-mcp-security.png diff --git a/docs/assets/logo/banner.dark.png b/docs/draft/assets/logo/banner.dark.png similarity index 100% rename from docs/assets/logo/banner.dark.png rename to docs/draft/assets/logo/banner.dark.png diff --git a/docs/assets/logo/banner.light.png b/docs/draft/assets/logo/banner.light.png similarity index 100% rename from docs/assets/logo/banner.light.png rename to docs/draft/assets/logo/banner.light.png diff --git a/docs/assets/logo/favicon-32x32.png b/docs/draft/assets/logo/favicon-32x32.png similarity index 100% rename from docs/assets/logo/favicon-32x32.png rename to docs/draft/assets/logo/favicon-32x32.png diff --git a/docs/assets/logo/favicon.ico b/docs/draft/assets/logo/favicon.ico similarity index 100% rename from docs/assets/logo/favicon.ico rename to docs/draft/assets/logo/favicon.ico diff --git a/docs/assets/logo/frontmcp.dark.png b/docs/draft/assets/logo/frontmcp.dark.png similarity index 100% rename from docs/assets/logo/frontmcp.dark.png rename to docs/draft/assets/logo/frontmcp.dark.png diff --git a/docs/assets/logo/frontmcp.dark.svg b/docs/draft/assets/logo/frontmcp.dark.svg similarity index 100% rename from docs/assets/logo/frontmcp.dark.svg rename to docs/draft/assets/logo/frontmcp.dark.svg diff --git a/docs/assets/logo/frontmcp.light.png b/docs/draft/assets/logo/frontmcp.light.png similarity index 100% rename from docs/assets/logo/frontmcp.light.png rename to docs/draft/assets/logo/frontmcp.light.png diff --git a/docs/assets/logo/frontmcp.light.svg b/docs/draft/assets/logo/frontmcp.light.svg similarity index 100% rename from docs/assets/logo/frontmcp.light.svg rename to docs/draft/assets/logo/frontmcp.light.svg diff --git a/docs/assets/logo/logo.dark.png b/docs/draft/assets/logo/logo.dark.png similarity index 100% rename from docs/assets/logo/logo.dark.png rename to docs/draft/assets/logo/logo.dark.png diff --git a/docs/assets/logo/logo.dark.svg b/docs/draft/assets/logo/logo.dark.svg similarity index 100% rename from docs/assets/logo/logo.dark.svg rename to docs/draft/assets/logo/logo.dark.svg diff --git a/docs/assets/logo/logo.light.png b/docs/draft/assets/logo/logo.light.png similarity index 100% rename from docs/assets/logo/logo.light.png rename to docs/draft/assets/logo/logo.light.png diff --git a/docs/assets/logo/logo.light.svg b/docs/draft/assets/logo/logo.light.svg similarity index 100% rename from docs/assets/logo/logo.light.svg rename to docs/draft/assets/logo/logo.light.svg diff --git a/docs/blog/11-2025/introducing-frontmcp.mdx b/docs/draft/blog/11-2025/introducing-frontmcp.mdx similarity index 100% rename from docs/blog/11-2025/introducing-frontmcp.mdx rename to docs/draft/blog/11-2025/introducing-frontmcp.mdx diff --git a/docs/blog/11-2025/mcp-run-out-of-socket.mdx b/docs/draft/blog/11-2025/mcp-run-out-of-socket.mdx similarity index 99% rename from docs/blog/11-2025/mcp-run-out-of-socket.mdx rename to docs/draft/blog/11-2025/mcp-run-out-of-socket.mdx index 2b2ef2db..675a8224 100644 --- a/docs/blog/11-2025/mcp-run-out-of-socket.mdx +++ b/docs/draft/blog/11-2025/mcp-run-out-of-socket.mdx @@ -4,7 +4,12 @@ description: One server, many agents. How FrontMCP makes multi-agent, multi-app mode: center --- -One server, many agents +One server, many agents # One Server, Many Agents diff --git a/docs/blog/11-2025/openapi-mcp-security-nightmare.mdx b/docs/draft/blog/11-2025/openapi-mcp-security-nightmare.mdx similarity index 99% rename from docs/blog/11-2025/openapi-mcp-security-nightmare.mdx rename to docs/draft/blog/11-2025/openapi-mcp-security-nightmare.mdx index e32e3a16..a077aa5a 100644 --- a/docs/blog/11-2025/openapi-mcp-security-nightmare.mdx +++ b/docs/draft/blog/11-2025/openapi-mcp-security-nightmare.mdx @@ -143,11 +143,11 @@ Cloud-Based (External Infrastructure): - **Enterprise contracts**: "All customer data must remain within our VPC" - **Audit requirements**: Need complete visibility into where tokens traveled - + **Important distinction**: Enterprise-grade identity platforms (like Frontegg's AgentLink) are purpose-built for secure token management with SOC2/ISO compliance, audit trails, and enterprise SLAs. Generic OpenAPI-to-MCP converter tools typically aren't. - + **Bottom line**: If you're building internal tools or need maximum control, self-hosted is the safest choice. If you use a cloud service, ensure it's a compliant identity platform, not just a conversion utility. @@ -548,13 +548,11 @@ FrontMCP's OpenAPI adapter gives you: Comprehensive guide to the OpenAPI adapter with security best practices -{' '} Get your secure MCP server running in 5 minutes -{' '} Convert OpenAPI specifications to MCP tools with security validation - + Convert JSON Schema to Zod schemas for type-safe validation diff --git a/docs/blog/index.mdx b/docs/draft/blog/index.mdx similarity index 99% rename from docs/blog/index.mdx rename to docs/draft/blog/index.mdx index 4e190692..cdffe687 100644 --- a/docs/blog/index.mdx +++ b/docs/draft/blog/index.mdx @@ -24,7 +24,6 @@ import { BlogCard } from '/snippets/card.jsx'; - URL or file path to the OpenAPI specification. Can be a local file path or remote URL. Use either `spec` or `url`, not both. + URL or file path to the OpenAPI specification. Can be a local file path or remote URL. Use either `spec` or `url`, not + both. ### Optional Configuration @@ -102,7 +103,8 @@ export default class MyApiApp {} - Options for loading the OpenAPI specification (headers, timeout, etc.). See [mcp-from-openapi](https://www.npmjs.com/package/mcp-from-openapi) for details. + Options for loading the OpenAPI specification (headers, timeout, etc.). See + [mcp-from-openapi](https://www.npmjs.com/package/mcp-from-openapi) for details. @@ -124,14 +126,12 @@ OpenapiAdapter.init({ baseUrl: 'https://api.example.com', additionalHeaders: { 'x-api-key': process.env.API_KEY!, - 'authorization': `Bearer ${process.env.API_TOKEN}`, + authorization: `Bearer ${process.env.API_TOKEN}`, }, -}) +}); ``` - - Store credentials in environment variables or secrets manager, never hardcode them. - +Store credentials in environment variables or secrets manager, never hardcode them. ### Strategy 2: Auth Provider Mapper (Low Risk) ⭐ Recommended @@ -146,18 +146,19 @@ OpenapiAdapter.init({ baseUrl: 'https://api.example.com', authProviderMapper: { // Map security scheme 'GitHubAuth' to GitHub token from user context - 'GitHubAuth': (authInfo) => authInfo.user?.githubToken, + GitHubAuth: (authInfo) => authInfo.user?.githubToken, // Map security scheme 'SlackAuth' to Slack token from user context - 'SlackAuth': (authInfo) => authInfo.user?.slackToken, + SlackAuth: (authInfo) => authInfo.user?.slackToken, // Map security scheme 'ApiKeyAuth' to API key from user context - 'ApiKeyAuth': (authInfo) => authInfo.user?.apiKey, + ApiKeyAuth: (authInfo) => authInfo.user?.apiKey, }, -}) +}); ``` **How it works:** + 1. Extracts security scheme names from OpenAPI spec (e.g., `GitHubAuth`, `SlackAuth`) 2. For each tool, looks up the required security scheme 3. Calls the corresponding extractor function to get the token from `authInfo` @@ -195,12 +196,10 @@ OpenapiAdapter.init({ // Default to main JWT token return { jwt: authInfo.token }; }, -}) +}); ``` - - **Security Risk: LOW** — Full control over authentication resolution from user context. - +**Security Risk: LOW** — Full control over authentication resolution from user context. ### Strategy 4: Static Auth (Medium Risk) @@ -215,12 +214,10 @@ OpenapiAdapter.init({ jwt: process.env.API_JWT_TOKEN, apiKey: process.env.API_KEY, }, -}) +}); ``` - - **Security Risk: MEDIUM** — Store credentials securely in environment variables or secrets manager. - +**Security Risk: MEDIUM** — Store credentials securely in environment variables or secrets manager. ### Strategy 5: Dynamic Headers & Body Mapping (Low Risk) @@ -252,12 +249,10 @@ OpenapiAdapter.init({ tenantId: authInfo.user?.tenantId, }; }, -}) +}); ``` - - **Security Risk: LOW** — User-specific data is injected server-side, hidden from MCP clients. - +**Security Risk: LOW** — User-specific data is injected server-side, hidden from MCP clients. ### Default Behavior (Medium Risk) @@ -269,11 +264,12 @@ OpenapiAdapter.init({ url: 'https://api.example.com/openapi.json', baseUrl: 'https://api.example.com', // No auth config - will use authInfo.token by default -}) +}); ``` - **Security Risk: MEDIUM** — Only works for single Bearer auth. For multiple auth providers, use `authProviderMapper` or `securityResolver`. + **Security Risk: MEDIUM** — Only works for single Bearer auth. For multiple auth providers, use `authProviderMapper` + or `securityResolver`. ## Advanced Features @@ -292,7 +288,7 @@ OpenapiAdapter.init({ generateOptions: { filterFn: (op) => op.path.startsWith('/invoices') || op.path.startsWith('/customers'), }, -}) +}); ``` ```ts Exclude specific operations @@ -303,7 +299,7 @@ OpenapiAdapter.init({ generateOptions: { excludeOperationIds: ['deprecatedEndpoint', 'internalOnly'], }, -}) +}); ``` ```ts Include only specific operations @@ -315,7 +311,7 @@ OpenapiAdapter.init({ defaultInclude: false, filterFn: (op) => ['getUser', 'createUser', 'updateUser'].includes(op.operationId), }, -}) +}); ``` @@ -345,7 +341,7 @@ OpenapiAdapter.init({ return schema; }, }, -}) +}); ``` ### Load Options @@ -359,11 +355,11 @@ OpenapiAdapter.init({ baseUrl: 'https://api.example.com', loadOptions: { headers: { - 'authorization': `Bearer ${process.env.SPEC_ACCESS_TOKEN}`, + authorization: `Bearer ${process.env.SPEC_ACCESS_TOKEN}`, }, timeout: 10000, // 10 seconds }, -}) +}); ``` ## How It Works @@ -400,7 +396,7 @@ import { OpenapiAdapter } from '@frontmcp/adapters'; url: 'https://api.github.com/openapi.json', baseUrl: 'https://api.github.com', authProviderMapper: { - 'GitHubAuth': (authInfo) => authInfo.user?.githubToken, + GitHubAuth: (authInfo) => authInfo.user?.githubToken, }, }), @@ -410,7 +406,7 @@ import { OpenapiAdapter } from '@frontmcp/adapters'; url: 'https://api.slack.com/openapi.json', baseUrl: 'https://api.slack.com', authProviderMapper: { - 'SlackAuth': (authInfo) => authInfo.user?.slackToken, + SlackAuth: (authInfo) => authInfo.user?.slackToken, }, }), ], @@ -494,13 +490,16 @@ export default class ExpenseMcpApp {} For multi-provider authentication, use `authProviderMapper` to map each security scheme to the correct auth provider. This provides **LOW security risk**. - - Always store credentials in environment variables or secrets manager. Never commit credentials to source control. - - - Setting `generateOptions.includeSecurityInInput: true` exposes auth fields to MCP clients (**HIGH risk**). Only use for development/testing. - + + Always store credentials in environment variables or secrets manager. Never commit credentials to source control. + + + + + Setting `generateOptions.includeSecurityInInput: true` exposes auth fields to MCP clients (**HIGH risk**). Only use + for development/testing. + Always validate that `authInfo.user` contains the expected fields before extracting tokens. Handle missing tokens gracefully. @@ -511,11 +510,11 @@ export default class ExpenseMcpApp {} The adapter automatically validates your security configuration and assigns a risk score: -| Risk Level | Configuration | Description | -|------------|---------------|-------------| -| **LOW** ✅ | `authProviderMapper` or `securityResolver` | Auth resolved from user context, not exposed to clients | -| **MEDIUM** ⚠️ | `staticAuth`, `additionalHeaders`, or default | Static credentials or default behavior | -| **HIGH** 🚨 | `generateOptions.includeSecurityInInput: true` | Auth fields exposed to MCP clients (not recommended) | +| Risk Level | Configuration | Description | +| ------------- | ---------------------------------------------- | ------------------------------------------------------- | +| **LOW** ✅ | `authProviderMapper` or `securityResolver` | Auth resolved from user context, not exposed to clients | +| **MEDIUM** ⚠️ | `staticAuth`, `additionalHeaders`, or default | Static credentials or default behavior | +| **HIGH** 🚨 | `generateOptions.includeSecurityInInput: true` | Auth fields exposed to MCP clients (not recommended) | ## Troubleshooting @@ -532,6 +531,7 @@ The adapter automatically validates your security configuration and assigns a ri // Add all security schemes from your spec } ``` + @@ -543,12 +543,14 @@ The adapter automatically validates your security configuration and assigns a ri 2. Add `securityResolver` (for custom logic) 3. Add `staticAuth` (for server-to-server) 4. Add `additionalHeaders` (for static API keys) + **Cause:** Tools may be filtered out by `filterFn` or `excludeOperationIds`. **Solution:** Check your filter configuration or remove filters to include all operations. + @@ -559,6 +561,7 @@ The adapter automatically validates your security configuration and assigns a ri 2. Check that token extraction returns a valid value 3. Verify the token is not expired 4. Check API logs for specific auth errors + @@ -571,6 +574,7 @@ The adapter automatically validates your security configuration and assigns a ri const spec = require('./openapi.json') as OpenAPIV3.Document; ``` + @@ -581,6 +585,7 @@ The adapter automatically validates your security configuration and assigns a ri Creates a new OpenAPI adapter instance. **Parameters:** + - `options: OpenApiAdapterOptions` — Adapter configuration **Returns:** Adapter instance ready for use in `@App({ adapters: [...] })` @@ -612,16 +617,13 @@ interface AuthInfo { ## Performance Tips - The adapter uses lazy loading — the OpenAPI spec is only loaded and tools are only generated on first use, not during initialization. + The adapter uses lazy loading — the OpenAPI spec is only loaded and tools are only generated on first use, not during + initialization. - - Combine with app-level plugins (caching, logging, metrics) to enhance all generated tools automatically. - +Combine with app-level plugins (caching, logging, metrics) to enhance all generated tools automatically. - - Use `filterFn` to generate only the tools you need, reducing initialization time and memory usage. - +Use `filterFn` to generate only the tools you need, reducing initialization time and memory usage. ## Links & Resources @@ -630,13 +632,15 @@ interface AuthInfo { See the expense management demo app using the OpenAPI adapter - - OpenAPI spec used in the demo application - - - Underlying library for OpenAPI to MCP conversion - + + OpenAPI spec used in the demo application + + + + + Underlying library for OpenAPI to MCP conversion + View the adapter source code diff --git a/docs/draft/docs/adapters/overview.mdx b/docs/draft/docs/adapters/overview.mdx index 5dc92434..c3b8139d 100644 --- a/docs/draft/docs/adapters/overview.mdx +++ b/docs/draft/docs/adapters/overview.mdx @@ -14,15 +14,12 @@ Adapters are powerful components that automatically convert external APIs and se Generate hundreds of tools from a single specification without writing code for each endpoint. - Automatic schema validation using Zod ensures type-safe inputs and outputs. - All tools follow the same patterns for authentication, error handling, and validation. - Update your spec and regenerate tools automatically — no manual updates needed. @@ -33,7 +30,7 @@ Adapters are powerful components that automatically convert external APIs and se These adapters are maintained by the FrontMCP team and included in `@frontmcp/adapters`. - + Generate MCP tools from OpenAPI 3.x specifications. Perfect for REST APIs with comprehensive documentation. **Features:** @@ -44,19 +41,19 @@ These adapters are maintained by the FrontMCP team and included in `@frontmcp/ad - Custom headers and body mapping **Best for:** REST APIs, microservices, third-party integrations + - - More official adapters are coming soon! GraphQL, gRPC, and database adapters are in development. - +More official adapters are coming soon! GraphQL, gRPC, and database adapters are in development. ## Community Adapters Community adapters are created and maintained by the FrontMCP community. While not officially supported, they extend FrontMCP's capabilities to new APIs and services. - Community adapters are not maintained by the FrontMCP team. Please review the code and security practices before using them in production. + Community adapters are not maintained by the FrontMCP team. Please review the code and security practices before using + them in production. ### How to Find Community Adapters @@ -68,11 +65,9 @@ Community adapters are created and maintained by the FrontMCP community. While n npm search frontmcp-adapter ``` - Browse repositories tagged with [`frontmcp-adapter`](https://github.com/topics/frontmcp-adapter) on GitHub. - Visit the [FrontMCP Discussions](https://github.com/agentfront/frontmcp/discussions) to discover and share adapters. @@ -143,29 +138,18 @@ export class MyCustomAdapter extends BaseAdapter { - Document security risk levels - Implement proper error handling - - - Generate Zod schemas from specifications - - Validate inputs at runtime - - Provide TypeScript types for all options - - Use strict type checking + - Generate Zod schemas from specifications - Validate inputs at runtime - Provide TypeScript types for all options - + Use strict type checking - - - Use lazy loading for specifications - - Cache generated tools when possible - - Implement connection pooling - - Handle rate limiting gracefully + - Use lazy loading for specifications - Cache generated tools when possible - Implement connection pooling - Handle + rate limiting gracefully - - - Provide clear documentation - - Include working examples - - Support common authentication patterns - - Add helpful error messages - - Write comprehensive tests + - Provide clear documentation - Include working examples - Support common authentication patterns - Add helpful error + messages - Write comprehensive tests - - Follow the official adapter structure - Version your adapter properly @@ -191,13 +175,7 @@ When publishing a community adapter: "name": "@yourscope/frontmcp-adapter-myapi", "version": "1.0.0", "description": "FrontMCP adapter for MyAPI", - "keywords": [ - "frontmcp", - "frontmcp-adapter", - "mcp", - "adapter", - "myapi" - ], + "keywords": ["frontmcp", "frontmcp-adapter", "mcp", "adapter", "myapi"], "peerDependencies": { "@frontmcp/sdk": "^0.3.0" } @@ -208,22 +186,20 @@ When publishing a community adapter: Choose the right adapter for your use case: -| Adapter | Best For | Authentication | Type Safety | Complexity | -|---------|----------|----------------|-------------|------------| -| **OpenAPI** | REST APIs with specs | Multi-provider | Auto-generated | Low | -| **Custom** | Any API/service | Fully customizable | Manual schemas | Medium-High | +| Adapter | Best For | Authentication | Type Safety | Complexity | +| ----------- | -------------------- | ------------------ | -------------- | ----------- | +| **OpenAPI** | REST APIs with specs | Multi-provider | Auto-generated | Low | +| **Custom** | Any API/service | Fully customizable | Manual schemas | Medium-High | ## Next Steps - + Learn how to use the OpenAPI adapter - See adapters in action with real examples - Get help and share your adapters @@ -232,18 +208,15 @@ Choose the right adapter for your use case: ## Resources - + Learn about the FrontMCP SDK and adapter interfaces - View the official adapters source code - Install the official adapters package - Contribute to FrontMCP adapters diff --git a/docs/draft/docs/getting-started/quickstart.mdx b/docs/draft/docs/getting-started/quickstart.mdx index 7bec9ad8..a6ec3ff1 100644 --- a/docs/draft/docs/getting-started/quickstart.mdx +++ b/docs/draft/docs/getting-started/quickstart.mdx @@ -41,14 +41,13 @@ yarn dev The CLI creates a complete project structure with: + - ✅ TypeScript configured - ✅ Sample server, app, and tool - ✅ Development scripts ready - ✅ Hot-reload enabled - - Your server is now running at `http://localhost:3000`! - +Your server is now running at `http://localhost:3000`! --- @@ -76,6 +75,7 @@ npx frontmcp init The `init` command: + - Adds FrontMCP scripts to `package.json` - Updates `tsconfig.json` with required settings - Creates a minimal server if none exists @@ -114,7 +114,8 @@ export default class Server {} ``` - The `@FrontMcp` decorator configures your server. It requires `info`, `apps`, and optional `http` and `logging` settings. + The `@FrontMcp` decorator configures your server. It requires `info`, `apps`, and optional `http` and `logging` + settings. ### App Definition @@ -154,6 +155,7 @@ export default class HelloApp {} } } ``` + @@ -169,11 +171,13 @@ export default class HelloApp {} return `Hello, ${input.name}!`; }); ``` + - Use class-based tools when you need access to providers, logging, or auth context. Use function-based tools for simple stateless operations. + Use class-based tools when you need access to providers, logging, or auth context. Use function-based tools for simple + stateless operations. --- @@ -216,7 +220,7 @@ npm run doctor ## Test Your Server - + ```bash npm run dev ``` @@ -226,9 +230,10 @@ npm run doctor [INFO] Server listening on http://localhost:3000 [INFO] Registered 1 tool: greet ``` + - + Open a new terminal and run: ```bash @@ -236,15 +241,15 @@ npm run doctor ``` This opens the MCP Inspector at `http://localhost:6274` - - - In the Inspector: - 1. Enter server URL: `http://localhost:3000` - 2. Click "Connect" - 3. You should see your `greet` tool listed + + + In the Inspector: 1. Enter server URL: `http://localhost:3000` 2. Click "Connect" 3. You should see your `greet` tool + listed + + 1. Select the `greet` tool 2. Enter input: `{ "name": "Ada" }` @@ -253,9 +258,7 @@ npm run doctor - - Congratulations! You've built and tested your first MCP server! 🎉 - +Congratulations! You've built and tested your first MCP server! 🎉 --- @@ -266,21 +269,25 @@ npm run doctor Learn how to create more powerful tools with validation, providers, and context - - Auto-generate tools from your REST API's OpenAPI spec - - - Improve performance with transparent caching - + + Auto-generate tools from your REST API's OpenAPI spec + - - Secure your server with OAuth (local or remote) - - - Add cross-cutting features like logging, metrics, and rate limiting - + + Improve performance with transparent caching + + + + + Secure your server with OAuth (local or remote) + + + + + Add cross-cutting features like logging, metrics, and rate limiting + Build and deploy your server to production @@ -291,14 +298,14 @@ npm run doctor ## Common Commands Reference -| Command | Description | -|---------|-------------| -| `npx frontmcp create ` | Create a new FrontMCP project | -| `npx frontmcp init` | Add FrontMCP to existing project | -| `npm run dev` | Start with hot-reload | -| `npm run build` | Compile to production | -| `npm run inspect` | Open MCP Inspector | -| `npm run doctor` | Verify setup | +| Command | Description | +| ---------------------------- | -------------------------------- | +| `npx frontmcp create ` | Create a new FrontMCP project | +| `npx frontmcp init` | Add FrontMCP to existing project | +| `npm run dev` | Start with hot-reload | +| `npm run build` | Compile to production | +| `npm run inspect` | Open MCP Inspector | +| `npm run doctor` | Verify setup | --- @@ -315,6 +322,7 @@ npm run doctor ```bash npm run doctor # Verify configuration ``` + @@ -332,6 +340,7 @@ npm run doctor } ``` 3. Ensure `import 'reflect-metadata'` at top of `main.ts` + @@ -342,6 +351,7 @@ npm run doctor # Manual type check npx tsc --noEmit ``` + @@ -355,6 +365,7 @@ npm run doctor # Test server directly curl http://localhost:3000/health ``` + @@ -413,6 +424,7 @@ export default class GreetTool extends ToolContext { ``` This example demonstrates: + - ✅ Input validation with Zod - ✅ Default values - ✅ Optional fields @@ -423,5 +435,6 @@ This example demonstrates: --- - FrontMCP speaks **MCP Streamable HTTP**. Any MCP-capable client (Claude Desktop, custom agents, etc.) can connect and call your tools! + FrontMCP speaks **MCP Streamable HTTP**. Any MCP-capable client (Claude Desktop, custom agents, etc.) can connect and + call your tools! diff --git a/docs/draft/docs/getting-started/welcome.mdx b/docs/draft/docs/getting-started/welcome.mdx index dc83de7b..717d1f40 100644 --- a/docs/draft/docs/getting-started/welcome.mdx +++ b/docs/draft/docs/getting-started/welcome.mdx @@ -65,4 +65,4 @@ export default class Server {} validation out of the box—no custom wiring needed. -**Build something real**: jump to the [Quickstart](/getting-started/quickstart) or set up your workspace in [Installation](/getting-started/installation). +**Build something real**: jump to the [Quickstart](/docs/getting-started/quickstart) or set up your workspace in [Installation](/docs/getting-started/installation). diff --git a/docs/draft/docs/guides/add-openapi-adapter.mdx b/docs/draft/docs/guides/add-openapi-adapter.mdx index 7de24305..98100a94 100644 --- a/docs/draft/docs/guides/add-openapi-adapter.mdx +++ b/docs/draft/docs/guides/add-openapi-adapter.mdx @@ -10,20 +10,22 @@ The OpenAPI Adapter automatically converts REST API endpoints defined in an Open ## What You'll Build By the end of this guide, you'll have: + - ✅ An app that automatically generates tools from an OpenAPI spec - ✅ Type-safe input validation for all API endpoints - ✅ Authentication configured for API requests - ✅ Tools that inherit all your app-level plugins and providers - The OpenAPI Adapter is perfect for quickly exposing REST APIs to AI agents without writing custom tool code for each endpoint. + The OpenAPI Adapter is perfect for quickly exposing REST APIs to AI agents without writing custom tool code for each + endpoint. --- ## Prerequisites -- A FrontMCP project initialized ([see Installation](/getting-started/installation)) +- A FrontMCP project initialized ([see Installation](/docs/getting-started/installation)) - An OpenAPI 3.x specification (URL or local file) - Basic understanding of REST APIs @@ -145,22 +147,21 @@ export default class Server {} ``` The server will load the OpenAPI spec and generate tools for each operation. - - - Check the console output for messages like: - ``` - [INFO] Generated 15 tools from expense-api - [INFO] Server listening on http://localhost:3000 - ``` + + Check the console output for messages like: ``` [INFO] Generated 15 tools from expense-api [INFO] Server listening on + http://localhost:3000 ``` + + ```bash npm run inspect ``` Open the MCP Inspector and you'll see all generated tools from your API spec! + @@ -175,13 +176,12 @@ Each OpenAPI operation becomes a tool with: Tools are named using the pattern: `{adapter-name}:{method}_{path}` For example: + - `GET /users` → `expense-api:get_users` - `POST /expenses` → `expense-api:post_expenses` - `GET /expenses/{id}` → `expense-api:get_expenses_id` - - If your OpenAPI spec includes `operationId`, that will be used instead of the generated name. - +If your OpenAPI spec includes `operationId`, that will be used instead of the generated name. ### Input Schema @@ -233,11 +233,9 @@ OpenapiAdapter.init({ url: 'https://api.example.com/openapi.json', generateOptions: { // Only include operations starting with /invoices or /customers - filterFn: (op) => - op.path.startsWith('/invoices') || - op.path.startsWith('/customers'), + filterFn: (op) => op.path.startsWith('/invoices') || op.path.startsWith('/customers'), }, -}) +}); ``` ### User-Based Authentication @@ -256,7 +254,7 @@ OpenapiAdapter.init({ } return headers; }, -}) +}); ``` ### Multi-Tenant Setup @@ -282,7 +280,7 @@ OpenapiAdapter.init({ tenantId: authInfo.user?.tenantId, }; }, -}) +}); ``` --- @@ -295,17 +293,14 @@ OpenapiAdapter.init({ - Each operation in the spec becomes an MCP tool with: - - Automatic input schema (path params, query params, headers, body) - - Type-safe validation using Zod - - Description from the operation summary + Each operation in the spec becomes an MCP tool with: - Automatic input schema (path params, query params, headers, + body) - Type-safe validation using Zod - Description from the operation summary + - Generated tools are registered with your app and inherit: - - App-level plugins (caching, logging, etc.) - - App-level providers - - App-level authentication + Generated tools are registered with your app and inherit: - App-level plugins (caching, logging, etc.) - App-level + providers - App-level authentication @@ -343,6 +338,7 @@ OpenapiAdapter.init({ ], }) ``` + @@ -360,6 +356,7 @@ OpenapiAdapter.init({ ], }) ``` + @@ -384,6 +381,7 @@ OpenapiAdapter.init({ ``` Now all generated tools can use caching! + @@ -402,6 +400,7 @@ OpenapiAdapter.init({ - Verify the URL is accessible - Convert OpenAPI 2.0 to 3.x using [Swagger Editor](https://editor.swagger.io/) - Check your filter configuration + @@ -413,6 +412,7 @@ OpenapiAdapter.init({ - Verify `additionalHeaders` or `headersMapper` configuration - Check that `authInfo.token` contains the expected value - Test the API directly with curl/Postman first + @@ -431,6 +431,7 @@ OpenapiAdapter.init({ }, } ``` + @@ -443,13 +444,13 @@ OpenapiAdapter.init({ Explore all configuration options and advanced features - - Learn how to configure authentication for your APIs - + + Learn how to configure authentication for your APIs + - - Add caching, logging, and other features to generated tools - + + Add caching, logging, and other features to generated tools + See a complete example using the OpenAPI adapter diff --git a/docs/draft/docs/guides/caching-and-cache-miss.mdx b/docs/draft/docs/guides/caching-and-cache-miss.mdx index cbc8d9c7..ae82cab7 100644 --- a/docs/draft/docs/guides/caching-and-cache-miss.mdx +++ b/docs/draft/docs/guides/caching-and-cache-miss.mdx @@ -10,6 +10,7 @@ The Cache Plugin provides transparent response caching for tools, dramatically i ## What You'll Learn By the end of this guide, you'll know how to: + - ✅ Enable caching for specific tools - ✅ Configure TTL (time-to-live) per tool - ✅ Use sliding windows to keep hot data cached @@ -17,7 +18,8 @@ By the end of this guide, you'll know how to: - ✅ Handle cache misses and invalidation - Caching is perfect for tools that make expensive computations, database queries, or third-party API calls with deterministic outputs. + Caching is perfect for tools that make expensive computations, database queries, or third-party API calls with + deterministic outputs. --- @@ -50,7 +52,9 @@ import CachePlugin from '@frontmcp/plugins/cache'; id: 'my-app', name: 'My App', plugins: [CachePlugin], // Default: memory store, 1-day TTL - tools: [/* your tools */], + tools: [ + /* your tools */ + ], }) export default class MyApp {} ``` @@ -68,7 +72,9 @@ import CachePlugin from '@frontmcp/plugins/cache'; defaultTTL: 300, // 5 minutes }), ], - tools: [/* your tools */], + tools: [ + /* your tools */ + ], }) export default class MyApp {} ``` @@ -91,7 +97,9 @@ import CachePlugin from '@frontmcp/plugins/cache'; }, }), ], - tools: [/* your tools */], + tools: [ + /* your tools */ + ], }) export default class MyApp {} ``` @@ -123,6 +131,7 @@ Caching is **opt-in** per tool. Add the `cache` field to your tool metadata: } } ``` + @@ -140,6 +149,7 @@ Caching is **opt-in** per tool. Add the `cache` field to your tool metadata: return await database.getUserProfile(input.userId); }); ``` + @@ -205,15 +215,13 @@ Caching is **opt-in** per tool. Add the `cache` field to your tool metadata: ``` The first call executes the tool normally (cache miss). - - - The second call returns instantly from cache! Check your logs for: - ``` - [DEBUG] Cache hit for get-user-profile - ``` + + The second call returns instantly from cache! Check your logs for: ``` [DEBUG] Cache hit for get-user-profile ``` + + Wait for the TTL to expire, then call again. The cache will miss and the tool will execute. @@ -230,26 +238,24 @@ Caching is **opt-in** per tool. Add the `cache` field to your tool metadata: - Validated input (e.g., `{ userId: "user-123" }`) Same input = Same cache key - - - The plugin checks the cache store for the key: - - **Cache Hit**: Return cached result immediately, skip execution - - **Cache Miss**: Allow tool to execute normally - - If the tool executed, the plugin stores the result in the cache with the configured TTL - + + The plugin checks the cache store for the key: - **Cache Hit**: Return cached result immediately, skip execution - + **Cache Miss**: Allow tool to execute normally + + + + If the tool executed, the plugin stores the result in the cache with the configured TTL + If `slideWindow: true`, each cache read refreshes the TTL, keeping popular data cached longer - - The cache operates at the **hook level**, so it works transparently without modifying your tool code. - +The cache operates at the **hook level**, so it works transparently without modifying your tool code. --- @@ -260,27 +266,31 @@ Caching is **opt-in** per tool. Add the `cache` field to your tool metadata: Enable caching for this tool - - `true` - Use plugin's default TTL - - `{ ttl, slideWindow }` - Custom configuration +- `true` - Use plugin's default TTL +- `{ ttl, slideWindow }` - Custom configuration + Time-to-live in seconds. Overrides plugin's `defaultTTL`. **Examples:** + - `60` - 1 minute - `300` - 5 minutes - `3600` - 1 hour - `86400` - 1 day + When `true`, reading from cache refreshes the TTL - **Use cases:** + - Trending/popular data - Frequently accessed reports - User dashboards + --- @@ -305,6 +315,7 @@ Caching is **opt-in** per tool. Add the `cache` field to your tool metadata: } } ``` + @@ -328,6 +339,7 @@ Caching is **opt-in** per tool. Add the `cache` field to your tool metadata: } } ``` + @@ -348,6 +360,7 @@ Caching is **opt-in** per tool. Add the `cache` field to your tool metadata: } } ``` + @@ -373,6 +386,7 @@ Caching is **opt-in** per tool. Add the `cache` field to your tool metadata: ``` Each tenant's data is cached separately! + @@ -387,22 +401,20 @@ Caching is **opt-in** per tool. Add the `cache` field to your tool metadata: Perfect for local development and testing - - When running one server instance - + + When running one server instance + - - Data loss on restart is acceptable - + + Data loss on restart is acceptable + No external dependencies needed - - Memory cache resets when the server restarts. Not shared across multiple instances. - +Memory cache resets when the server restarts. Not shared across multiple instances. ### When to Use Redis @@ -411,22 +423,20 @@ Caching is **opt-in** per tool. Add the `cache` field to your tool metadata: Recommended for production deployments - - Cache shared across multiple server instances - + + Cache shared across multiple server instances + - - Cache survives server restarts - + + Cache survives server restarts + Redis handles memory limits gracefully - - Redis provides persistence, sharing, and better memory management for production use. - +Redis provides persistence, sharing, and better memory management for production use. --- @@ -446,6 +456,7 @@ Caching is **opt-in** per tool. Add the `cache` field to your tool metadata: level: LogLevel.DEBUG, // See cache hit/miss logs } ``` + @@ -457,6 +468,7 @@ Caching is **opt-in** per tool. Add the `cache` field to your tool metadata: ttl: 60, // Shorter TTL = fresher data } ``` + @@ -469,6 +481,7 @@ Caching is **opt-in** per tool. Add the `cache` field to your tool metadata: config: { host: 'localhost', port: 6379 }, }) ``` + @@ -481,6 +494,7 @@ Caching is **opt-in** per tool. Add the `cache` field to your tool metadata: // No cache field - don't cache this! }) ``` + @@ -502,6 +516,7 @@ Caching is **opt-in** per tool. Add the `cache` field to your tool metadata: - Tools that return current time/date - Tools with random output - Tools with side effects (mutations) + @@ -513,6 +528,7 @@ Caching is **opt-in** per tool. Add the `cache` field to your tool metadata: | User profiles | 5-15 minutes | | Reports | 30 minutes - 1 hour | | Static content | Hours to days | + @@ -531,6 +547,7 @@ Caching is **opt-in** per tool. Add the `cache` field to your tool metadata: reportId: z.string(), } ``` + @@ -552,6 +569,7 @@ Caching is **opt-in** per tool. Add the `cache` field to your tool metadata: }, }) ``` + @@ -566,6 +584,7 @@ Caching is **opt-in** per tool. Add the `cache` field to your tool metadata: Look for: - High miss rates (TTL too short? Tool not deterministic?) - Memory growth (TTL too long?) + @@ -652,12 +671,7 @@ class GenerateReportTool extends ToolContext { }, }), ], - tools: [ - GetStockPriceTool, - GetUserTool, - GetTrendingTool, - GenerateReportTool, - ], + tools: [GetStockPriceTool, GetUserTool, GetTrendingTool, GenerateReportTool], }) class AnalyticsApp {} diff --git a/docs/draft/docs/guides/customize-flow-stages.mdx b/docs/draft/docs/guides/customize-flow-stages.mdx index e3096ec1..86474f32 100644 --- a/docs/draft/docs/guides/customize-flow-stages.mdx +++ b/docs/draft/docs/guides/customize-flow-stages.mdx @@ -21,7 +21,6 @@ Hooks allow you to intercept and modify tool execution at specific lifecycle sta Higher priority runs first for Will/Stage; Did runs in reverse order - Shared state object passed through all stages, allowing data flow between hooks @@ -51,6 +50,7 @@ Hooks allow you to intercept and modify tool execution at specific lifecycle sta } } ``` + @@ -76,6 +76,7 @@ Hooks allow you to intercept and modify tool execution at specific lifecycle sta } } ``` + @@ -121,9 +122,11 @@ Hooks allow you to intercept and modify tool execution at specific lifecycle sta await this.validateSchema(toolContext.input, tool.inputSchema); } ``` + + --- ## Available Flows @@ -239,10 +242,7 @@ import { Plugin, ToolHook, FlowCtxOf } from '@frontmcp/sdk'; }) export default class ResiliencePlugin { @ToolHook.Around('execute') - async withRetry( - ctx: FlowCtxOf<'tools:call-tool'>, - next: () => Promise - ) { + async withRetry(ctx: FlowCtxOf<'tools:call-tool'>, next: () => Promise) { const { tool, toolContext } = ctx.state; if (!tool || !toolContext) return await next(); @@ -454,6 +454,7 @@ class MyPlugin { const hooks = hookRegistry.getFlowHooks('tools:call-tool'); // Returns: HookEntry[] - all hooks for this flow ``` + @@ -485,6 +486,7 @@ class MyPlugin { - **Tool-specific hooks**: When a tool registers hooks that should only apply to its own execution - **Multi-tenant isolation**: Filter hooks by tenant ID to ensure proper isolation - **Plugin scoping**: Get hooks that belong to a specific plugin + @@ -497,6 +499,7 @@ class MyPlugin { ); // Returns: HookEntry[] - all hooks for the 'execute' stage ``` + @@ -506,6 +509,7 @@ class MyPlugin { const classHooks = hookRegistry.getClsHooks(MyPlugin); // Returns: HookEntry[] - all hooks defined on MyPlugin class ``` + @@ -528,21 +532,19 @@ export default class ToolIsolationPlugin { const toolOwnerId = toolContext.tool?.metadata.owner?.id; // Get only hooks relevant to this specific tool - const relevantHooks = hookRegistry.getFlowHooksForOwner( - 'tools:call-tool', - toolOwnerId - ); + const relevantHooks = hookRegistry.getFlowHooksForOwner('tools:call-tool', toolOwnerId); ctx.logger.info(`Found ${relevantHooks.length} hooks for this tool`, { toolOwnerId, - hooks: relevantHooks.map(h => h.metadata.stage), + hooks: relevantHooks.map((h) => h.metadata.stage), }); } } ``` - The Hook Registry API is an advanced feature primarily intended for framework developers and complex plugin authors. Most users should use the decorator-based approach (`@ToolHook`, `@HttpHook`, etc.) instead. + The Hook Registry API is an advanced feature primarily intended for framework developers and complex plugin authors. + Most users should use the decorator-based approach (`@ToolHook`, `@HttpHook`, etc.) instead. --- @@ -570,6 +572,7 @@ export default class ToolIsolationPlugin { plugins: [MyHooksPlugin], }) ``` + @@ -578,6 +581,7 @@ export default class ToolIsolationPlugin { - **Logging/Metrics**: Low priority (1-20) This ensures validation runs before transformation, and logging captures everything. + @@ -607,6 +611,7 @@ export default class ToolIsolationPlugin { this.logger.info(`Execution took ${duration}ms`); } ``` + @@ -626,9 +631,9 @@ export default class ToolIsolationPlugin { Learn more about FrontMCP plugins - - Understand tool lifecycle and execution - + + Understand tool lifecycle and execution + See hooks in action with the Cache Plugin diff --git a/docs/draft/docs/plugins/cache-plugin.mdx b/docs/draft/docs/plugins/cache-plugin.mdx index 0c65449f..c0678f1c 100644 --- a/docs/draft/docs/plugins/cache-plugin.mdx +++ b/docs/draft/docs/plugins/cache-plugin.mdx @@ -12,7 +12,6 @@ The Cache Plugin provides transparent response caching for tools based on their Return cached results instantly without re-executing expensive operations - Minimize API calls, database queries, and computational overhead @@ -38,14 +37,9 @@ npm install @frontmcp/plugins The plugin hashes the tool's validated input and checks the cache store + If a cached result exists, it's returned immediately, bypassing tool execution entirely - - If a cached result exists, it's returned immediately, bypassing tool execution entirely - - - - The tool executes normally, and the result is stored with the configured TTL - + The tool executes normally, and the result is stored with the configured TTL When enabled, each cache read refreshes the TTL to keep hot entries alive longer @@ -53,7 +47,8 @@ npm install @frontmcp/plugins - Cache entries are keyed using a **deterministic hash** of the tool's validated input. The same input always produces the same cache key. + Cache entries are keyed using a **deterministic hash** of the tool's validated input. The same input always produces + the same cache key. --- @@ -70,7 +65,9 @@ import CachePlugin from '@frontmcp/plugins/cache'; id: 'my-app', name: 'My App', plugins: [CachePlugin], // Default: memory store, 1-day TTL - tools: [/* your tools */], + tools: [ + /* your tools */ + ], }) export default class MyApp {} ``` @@ -128,12 +125,10 @@ Best for: Single-instance deployments, development, non-critical caching CachePlugin.init({ type: 'memory', defaultTTL: 300, // 5 minutes (default: 86400 = 1 day) -}) +}); ``` - - Memory cache resets when the process restarts. Not shared across multiple instances. - +Memory cache resets when the process restarts. Not shared across multiple instances. ### Redis (Recommended for Production) @@ -151,7 +146,7 @@ CachePlugin.init({ password: process.env.REDIS_PASSWORD, // optional db: 0, // optional }, -}) +}); ``` ```ts Reuse existing Redis client @@ -166,14 +161,12 @@ CachePlugin.init({ type: 'redis-client', defaultTTL: 900, // 15 minutes client: redis, -}) +}); ``` - - Redis enables cache sharing across multiple server instances and persists cache across restarts. - +Redis enables cache sharing across multiple server instances and persists cache across restarts. --- @@ -251,8 +244,9 @@ Configure caching behavior per tool in the `@Tool` or `tool()` metadata: Enable caching for this tool - - `true` - Use plugin defaults - - `object` - Custom configuration +- `true` - Use plugin defaults +- `object` - Custom configuration + @@ -342,18 +336,15 @@ Use short TTLs for frequently changing data: - Have side effects (mutations, API calls that change state) - - - **Short TTLs (5-60s)**: Real-time data, frequently changing content - - **Medium TTLs (5-30min)**: User dashboards, reports, analytics - - **Long TTLs (hours-days)**: Static content, configuration, reference data - + + - **Short TTLs (5-60s)**: Real-time data, frequently changing content - **Medium TTLs (5-30min)**: User dashboards, + reports, analytics - **Long TTLs (hours-days)**: Static content, configuration, reference data + - - Redis provides: - - Cache persistence across restarts - - Sharing across multiple server instances - - Better memory management with eviction policies - + + Redis provides: - Cache persistence across restarts - Sharing across multiple server instances - Better memory + management with eviction policies + Always include tenant IDs, user IDs, or other scoping fields in your tool inputs: @@ -364,6 +355,7 @@ Use short TTLs for frequently changing data: // Bad: no scoping, shared across tenants { reportId: "r-456" } ``` + @@ -381,14 +373,14 @@ Use short TTLs for frequently changing data: ## Cache Behavior Reference -| Behavior | Description | -|----------|-------------| +| Behavior | Description | +| ------------------ | -------------------------------------------------------------------- | | **Key Derivation** | Deterministic hash from validated input. Same input = same cache key | -| **Cache Hits** | Bypasses tool execution entirely, returns cached result instantly | -| **Default TTL** | 86400 seconds (1 day) if not specified | -| **Sliding Window** | Extends TTL on reads when enabled | -| **Store Choice** | Memory is node-local; Redis enables multi-instance sharing | -| **Invalidation** | Automatic after TTL expires, or manually by restarting (memory) | +| **Cache Hits** | Bypasses tool execution entirely, returns cached result instantly | +| **Default TTL** | 86400 seconds (1 day) if not specified | +| **Sliding Window** | Extends TTL on reads when enabled | +| **Store Choice** | Memory is node-local; Redis enables multi-instance sharing | +| **Invalidation** | Automatic after TTL expires, or manually by restarting (memory) | --- @@ -405,6 +397,7 @@ Use short TTLs for frequently changing data: - Verify `cache` field is set in tool metadata - Check Redis connection if using Redis backend - Ensure input structure is consistent + @@ -416,6 +409,7 @@ Use short TTLs for frequently changing data: - Reduce TTL for the tool - Consider input-based cache busting (include timestamp or version in input) - Restart server to clear memory cache (or flush Redis) + @@ -424,6 +418,7 @@ Use short TTLs for frequently changing data: **Solution:** - Switch to Redis backend for multi-instance deployments + @@ -472,10 +467,7 @@ class GenerateMonthlyReportTool extends ToolContext { this.logger.info('Generating report', input); // Expensive operation: aggregate data, generate charts, etc. - const report = await this.database.generateReport( - input.tenantId, - input.month - ); + const report = await this.database.generateReport(input.tenantId, input.month); return report; } @@ -496,10 +488,7 @@ class GenerateMonthlyReportTool extends ToolContext { }) class GetTrendingProductsTool extends ToolContext { async execute(input: { category: string; limit: number }) { - return await this.analytics.getTrendingProducts( - input.category, - input.limit - ); + return await this.analytics.getTrendingProducts(input.category, input.limit); } } @@ -528,13 +517,13 @@ export default class Server {} View the cache plugin source code - - See caching in action with real examples - + + See caching in action with real examples + - - Learn more about FrontMCP plugins - + + Learn more about FrontMCP plugins + Official Redis documentation diff --git a/docs/draft/docs/style.css b/docs/draft/docs/style.css new file mode 100644 index 00000000..4c49059e --- /dev/null +++ b/docs/draft/docs/style.css @@ -0,0 +1,7 @@ +.content-area.max-w-xl { + max-width: 46rem; +} + +.content-area.max-w-3xl { + max-width: 52rem; +} diff --git a/docs/docs/adapters/openapi-adapter.mdx b/docs/draft/docs/v/0.1/adapters/openapi-adapter.mdx similarity index 100% rename from docs/docs/adapters/openapi-adapter.mdx rename to docs/draft/docs/v/0.1/adapters/openapi-adapter.mdx diff --git a/docs/docs/deployment/local-dev-server.mdx b/docs/draft/docs/v/0.1/deployment/local-dev-server.mdx similarity index 100% rename from docs/docs/deployment/local-dev-server.mdx rename to docs/draft/docs/v/0.1/deployment/local-dev-server.mdx diff --git a/docs/docs/deployment/production-build.mdx b/docs/draft/docs/v/0.1/deployment/production-build.mdx similarity index 100% rename from docs/docs/deployment/production-build.mdx rename to docs/draft/docs/v/0.1/deployment/production-build.mdx diff --git a/docs/docs/v/0.1/getting-started/installation.mdx b/docs/draft/docs/v/0.1/getting-started/installation.mdx similarity index 100% rename from docs/docs/v/0.1/getting-started/installation.mdx rename to docs/draft/docs/v/0.1/getting-started/installation.mdx diff --git a/docs/docs/v/0.2/getting-started/quickstart.mdx b/docs/draft/docs/v/0.1/getting-started/quickstart.mdx similarity index 97% rename from docs/docs/v/0.2/getting-started/quickstart.mdx rename to docs/draft/docs/v/0.1/getting-started/quickstart.mdx index 1c3176f7..dd078826 100644 --- a/docs/docs/v/0.2/getting-started/quickstart.mdx +++ b/docs/draft/docs/v/0.1/getting-started/quickstart.mdx @@ -6,7 +6,7 @@ icon: rocket-launch Welcome! This guide will help you quickly set up FrontMCP and run your first MCP server. -If you haven't already installed FrontMCP, follow the [installation instructions](/getting-started/installation). +If you haven't already installed FrontMCP, follow the [installation instructions](./installation). ## 1) Create your FrontMCP server diff --git a/docs/docs/v/0.1/getting-started/welcome.mdx b/docs/draft/docs/v/0.1/getting-started/welcome.mdx similarity index 92% rename from docs/docs/v/0.1/getting-started/welcome.mdx rename to docs/draft/docs/v/0.1/getting-started/welcome.mdx index 5e585132..456beb88 100644 --- a/docs/docs/v/0.1/getting-started/welcome.mdx +++ b/docs/draft/docs/v/0.1/getting-started/welcome.mdx @@ -61,4 +61,4 @@ export default class Server {} validation out of the box—no custom wiring needed. -**Build something real**: jump to the [Quickstart](/getting-started/quickstart) or set up your workspace in [Installation](/getting-started/installation). +**Build something real**: jump to the [Quickstart](./quickstart) or set up your workspace in [Installation](./installation). diff --git a/docs/docs/servers/apps.mdx b/docs/draft/docs/v/0.1/servers/apps.mdx similarity index 100% rename from docs/docs/servers/apps.mdx rename to docs/draft/docs/v/0.1/servers/apps.mdx diff --git a/docs/docs/servers/authentication/local.mdx b/docs/draft/docs/v/0.1/servers/authentication/local.mdx similarity index 100% rename from docs/docs/servers/authentication/local.mdx rename to docs/draft/docs/v/0.1/servers/authentication/local.mdx diff --git a/docs/docs/v/0.1/servers/authentication/overview.mdx b/docs/draft/docs/v/0.1/servers/authentication/overview.mdx similarity index 100% rename from docs/docs/v/0.1/servers/authentication/overview.mdx rename to docs/draft/docs/v/0.1/servers/authentication/overview.mdx diff --git a/docs/docs/servers/authentication/remote-proxy.mdx b/docs/draft/docs/v/0.1/servers/authentication/remote-proxy.mdx similarity index 100% rename from docs/docs/servers/authentication/remote-proxy.mdx rename to docs/draft/docs/v/0.1/servers/authentication/remote-proxy.mdx diff --git a/docs/docs/servers/authentication/remote.mdx b/docs/draft/docs/v/0.1/servers/authentication/remote.mdx similarity index 100% rename from docs/docs/servers/authentication/remote.mdx rename to docs/draft/docs/v/0.1/servers/authentication/remote.mdx diff --git a/docs/docs/servers/authentication/token.mdx b/docs/draft/docs/v/0.1/servers/authentication/token.mdx similarity index 100% rename from docs/docs/servers/authentication/token.mdx rename to docs/draft/docs/v/0.1/servers/authentication/token.mdx diff --git a/docs/docs/servers/extensibility/adapters.mdx b/docs/draft/docs/v/0.1/servers/extensibility/adapters.mdx similarity index 100% rename from docs/docs/servers/extensibility/adapters.mdx rename to docs/draft/docs/v/0.1/servers/extensibility/adapters.mdx diff --git a/docs/docs/servers/extensibility/logging.mdx b/docs/draft/docs/v/0.1/servers/extensibility/logging.mdx similarity index 100% rename from docs/docs/servers/extensibility/logging.mdx rename to docs/draft/docs/v/0.1/servers/extensibility/logging.mdx diff --git a/docs/docs/servers/extensibility/plugins.mdx b/docs/draft/docs/v/0.1/servers/extensibility/plugins.mdx similarity index 100% rename from docs/docs/servers/extensibility/plugins.mdx rename to docs/draft/docs/v/0.1/servers/extensibility/plugins.mdx diff --git a/docs/docs/servers/extensibility/providers.mdx b/docs/draft/docs/v/0.1/servers/extensibility/providers.mdx similarity index 100% rename from docs/docs/servers/extensibility/providers.mdx rename to docs/draft/docs/v/0.1/servers/extensibility/providers.mdx diff --git a/docs/docs/servers/prompts.mdx b/docs/draft/docs/v/0.1/servers/prompts.mdx similarity index 100% rename from docs/docs/servers/prompts.mdx rename to docs/draft/docs/v/0.1/servers/prompts.mdx diff --git a/docs/docs/servers/resources.mdx b/docs/draft/docs/v/0.1/servers/resources.mdx similarity index 100% rename from docs/docs/servers/resources.mdx rename to docs/draft/docs/v/0.1/servers/resources.mdx diff --git a/docs/docs/v/0.1/servers/server.mdx b/docs/draft/docs/v/0.1/servers/server.mdx similarity index 100% rename from docs/docs/v/0.1/servers/server.mdx rename to docs/draft/docs/v/0.1/servers/server.mdx diff --git a/docs/docs/servers/tools.mdx b/docs/draft/docs/v/0.1/servers/tools.mdx similarity index 100% rename from docs/docs/servers/tools.mdx rename to docs/draft/docs/v/0.1/servers/tools.mdx diff --git a/docs/docs/v/0.1/adapters/openapi-adapter.mdx b/docs/draft/docs/v/0.2/adapters/openapi-adapter.mdx similarity index 100% rename from docs/docs/v/0.1/adapters/openapi-adapter.mdx rename to docs/draft/docs/v/0.2/adapters/openapi-adapter.mdx diff --git a/docs/docs/v/0.1/deployment/local-dev-server.mdx b/docs/draft/docs/v/0.2/deployment/local-dev-server.mdx similarity index 100% rename from docs/docs/v/0.1/deployment/local-dev-server.mdx rename to docs/draft/docs/v/0.2/deployment/local-dev-server.mdx diff --git a/docs/docs/v/0.1/deployment/production-build.mdx b/docs/draft/docs/v/0.2/deployment/production-build.mdx similarity index 100% rename from docs/docs/v/0.1/deployment/production-build.mdx rename to docs/draft/docs/v/0.2/deployment/production-build.mdx diff --git a/docs/docs/v/0.2/getting-started/installation.mdx b/docs/draft/docs/v/0.2/getting-started/installation.mdx similarity index 100% rename from docs/docs/v/0.2/getting-started/installation.mdx rename to docs/draft/docs/v/0.2/getting-started/installation.mdx diff --git a/docs/docs/v/0.1/getting-started/quickstart.mdx b/docs/draft/docs/v/0.2/getting-started/quickstart.mdx similarity index 97% rename from docs/docs/v/0.1/getting-started/quickstart.mdx rename to docs/draft/docs/v/0.2/getting-started/quickstart.mdx index 1c3176f7..dd078826 100644 --- a/docs/docs/v/0.1/getting-started/quickstart.mdx +++ b/docs/draft/docs/v/0.2/getting-started/quickstart.mdx @@ -6,7 +6,7 @@ icon: rocket-launch Welcome! This guide will help you quickly set up FrontMCP and run your first MCP server. -If you haven't already installed FrontMCP, follow the [installation instructions](/getting-started/installation). +If you haven't already installed FrontMCP, follow the [installation instructions](./installation). ## 1) Create your FrontMCP server diff --git a/docs/docs/v/0.2/getting-started/welcome.mdx b/docs/draft/docs/v/0.2/getting-started/welcome.mdx similarity index 92% rename from docs/docs/v/0.2/getting-started/welcome.mdx rename to docs/draft/docs/v/0.2/getting-started/welcome.mdx index 87d318f5..c17c8a57 100644 --- a/docs/docs/v/0.2/getting-started/welcome.mdx +++ b/docs/draft/docs/v/0.2/getting-started/welcome.mdx @@ -61,4 +61,4 @@ export default class Server {} validation out of the box—no custom wiring needed. -**Build something real**: jump to the [Quickstart](/getting-started/quickstart) or set up your workspace in [Installation](/getting-started/installation). +**Build something real**: jump to the [Quickstart](./quickstart) or set up your workspace in [Installation](./installation). diff --git a/docs/docs/guides/add-openapi-adapter.mdx b/docs/draft/docs/v/0.2/guides/add-openapi-adapter.mdx similarity index 100% rename from docs/docs/guides/add-openapi-adapter.mdx rename to docs/draft/docs/v/0.2/guides/add-openapi-adapter.mdx diff --git a/docs/docs/guides/caching-and-cache-miss.mdx b/docs/draft/docs/v/0.2/guides/caching-and-cache-miss.mdx similarity index 100% rename from docs/docs/guides/caching-and-cache-miss.mdx rename to docs/draft/docs/v/0.2/guides/caching-and-cache-miss.mdx diff --git a/docs/docs/guides/customize-flow-stages.mdx b/docs/draft/docs/v/0.2/guides/customize-flow-stages.mdx similarity index 100% rename from docs/docs/guides/customize-flow-stages.mdx rename to docs/draft/docs/v/0.2/guides/customize-flow-stages.mdx diff --git a/docs/docs/v/0.1/servers/apps.mdx b/docs/draft/docs/v/0.2/servers/apps.mdx similarity index 100% rename from docs/docs/v/0.1/servers/apps.mdx rename to docs/draft/docs/v/0.2/servers/apps.mdx diff --git a/docs/docs/v/0.1/servers/authentication/local.mdx b/docs/draft/docs/v/0.2/servers/authentication/local.mdx similarity index 100% rename from docs/docs/v/0.1/servers/authentication/local.mdx rename to docs/draft/docs/v/0.2/servers/authentication/local.mdx diff --git a/docs/docs/v/0.2/servers/authentication/overview.mdx b/docs/draft/docs/v/0.2/servers/authentication/overview.mdx similarity index 100% rename from docs/docs/v/0.2/servers/authentication/overview.mdx rename to docs/draft/docs/v/0.2/servers/authentication/overview.mdx diff --git a/docs/docs/v/0.1/servers/authentication/remote-proxy.mdx b/docs/draft/docs/v/0.2/servers/authentication/remote-proxy.mdx similarity index 100% rename from docs/docs/v/0.1/servers/authentication/remote-proxy.mdx rename to docs/draft/docs/v/0.2/servers/authentication/remote-proxy.mdx diff --git a/docs/docs/v/0.1/servers/authentication/remote.mdx b/docs/draft/docs/v/0.2/servers/authentication/remote.mdx similarity index 100% rename from docs/docs/v/0.1/servers/authentication/remote.mdx rename to docs/draft/docs/v/0.2/servers/authentication/remote.mdx diff --git a/docs/docs/v/0.1/servers/authentication/token.mdx b/docs/draft/docs/v/0.2/servers/authentication/token.mdx similarity index 100% rename from docs/docs/v/0.1/servers/authentication/token.mdx rename to docs/draft/docs/v/0.2/servers/authentication/token.mdx diff --git a/docs/docs/v/0.1/servers/extensibility/adapters.mdx b/docs/draft/docs/v/0.2/servers/extensibility/adapters.mdx similarity index 100% rename from docs/docs/v/0.1/servers/extensibility/adapters.mdx rename to docs/draft/docs/v/0.2/servers/extensibility/adapters.mdx diff --git a/docs/docs/v/0.1/servers/extensibility/logging.mdx b/docs/draft/docs/v/0.2/servers/extensibility/logging.mdx similarity index 100% rename from docs/docs/v/0.1/servers/extensibility/logging.mdx rename to docs/draft/docs/v/0.2/servers/extensibility/logging.mdx diff --git a/docs/docs/v/0.1/servers/extensibility/plugins.mdx b/docs/draft/docs/v/0.2/servers/extensibility/plugins.mdx similarity index 100% rename from docs/docs/v/0.1/servers/extensibility/plugins.mdx rename to docs/draft/docs/v/0.2/servers/extensibility/plugins.mdx diff --git a/docs/docs/v/0.1/servers/extensibility/providers.mdx b/docs/draft/docs/v/0.2/servers/extensibility/providers.mdx similarity index 100% rename from docs/docs/v/0.1/servers/extensibility/providers.mdx rename to docs/draft/docs/v/0.2/servers/extensibility/providers.mdx diff --git a/docs/docs/v/0.1/servers/prompts.mdx b/docs/draft/docs/v/0.2/servers/prompts.mdx similarity index 100% rename from docs/docs/v/0.1/servers/prompts.mdx rename to docs/draft/docs/v/0.2/servers/prompts.mdx diff --git a/docs/docs/v/0.1/servers/resources.mdx b/docs/draft/docs/v/0.2/servers/resources.mdx similarity index 100% rename from docs/docs/v/0.1/servers/resources.mdx rename to docs/draft/docs/v/0.2/servers/resources.mdx diff --git a/docs/docs/v/0.2/servers/server.mdx b/docs/draft/docs/v/0.2/servers/server.mdx similarity index 100% rename from docs/docs/v/0.2/servers/server.mdx rename to docs/draft/docs/v/0.2/servers/server.mdx diff --git a/docs/docs/v/0.1/servers/tools.mdx b/docs/draft/docs/v/0.2/servers/tools.mdx similarity index 100% rename from docs/docs/v/0.1/servers/tools.mdx rename to docs/draft/docs/v/0.2/servers/tools.mdx diff --git a/docs/draft/docs/v/0.3/adapters/openapi-adapter.mdx b/docs/draft/docs/v/0.3/adapters/openapi-adapter.mdx new file mode 100644 index 00000000..21451dc9 --- /dev/null +++ b/docs/draft/docs/v/0.3/adapters/openapi-adapter.mdx @@ -0,0 +1,177 @@ +--- +title: OpenAPI Adapter +slug: adapters/openapi-adapter +sidebarTitle: OpenAPI +description: Generate MCP tools directly from an OpenAPI spec and call them with strong validation. +icon: puzzle-piece +--- + +The OpenAPI Adapter turns an OpenAPI 3.x specification into ready-to-use MCP tools. It is powered by [`mcp-from-openapi`](https://www.npmjs.com/package/mcp-from-openapi), so every generated tool ships with a request mapper, automatic parameter conflict resolution, and validated schemas. + +## Why use it + +- **Zero boilerplate** — convert REST APIs to MCP tools by registering a single adapter. +- **Accurate schemas** — input and output schemas are derived from the spec and compiled to Zod for runtime validation. +- **Multi-auth aware** — map different security schemes to different auth providers with `authProviderMapper` or a `securityResolver`. +- **Safe by default** — the adapter validates your security configuration, prints a risk score, and refuses to expose tools that cannot be authenticated. +- **Mapper visibility** — every tool exposes how validated inputs become path/query/header/body values, making debugging and customization easy. + +## Quick start + +```ts +import { App } from '@frontmcp/sdk'; +import { OpenapiAdapter } from '@frontmcp/adapters'; + +@App({ + id: 'my-api', + name: 'My API MCP Server', + adapters: [ + OpenapiAdapter.init({ + name: 'backend:api', + baseUrl: process.env.API_BASE_URL!, + url: process.env.OPENAPI_SPEC_URL!, + }), + ], +}) +export default class MyApiApp {} +``` + +```ts +import { App } from '@frontmcp/sdk'; +import { OpenapiAdapter } from '@frontmcp/adapters'; + +@App({ + id: 'my-api', + name: 'My API MCP Server', + adapters: [ + OpenapiAdapter.init({ + name: 'backend:api', + baseUrl: process.env.API_BASE_URL!, + url: process.env.OPENAPI_SPEC_URL!, + headersMapper: (authInfo, headers) => { + if (authInfo.token) headers.set('authorization', `Bearer ${authInfo.token}`); + if (authInfo.user?.tenantId) headers.set('x-tenant-id', authInfo.user.tenantId); + return headers; + }, + }), + ], +}) +export default class MyApiApp {} +``` + +## Required options + +- `name` — unique identifier for the adapter (used when disambiguating tool ids). +- `baseUrl` — root URL used when the adapter builds HTTP requests. +- One of `url` (file path or remote URL) or `spec` (an `OpenAPIV3.Document`). + +## Authentication strategies + +The adapter can resolve authentication per tool. Pick the strategy that matches your API. + +### Static headers (server-to-server) + +```ts +OpenapiAdapter.init({ + name: 'billing', + baseUrl: 'https://api.example.com', + url: 'https://api.example.com/openapi.json', + additionalHeaders: { + 'x-api-key': process.env.BILLING_API_KEY!, + }, +}); +``` + +### `authProviderMapper` (recommended for multi-provider OAuth) + +Map each security scheme in your spec to the correct token on `authInfo`. + +```ts +OpenapiAdapter.init({ + name: 'integrations', + baseUrl: 'https://api.example.com', + url: 'https://api.example.com/openapi.json', + authProviderMapper: { + GitHubAuth: (authInfo) => authInfo.user?.githubToken, + SlackAuth: (authInfo) => authInfo.user?.slackToken, + ApiKeyAuth: (authInfo) => authInfo.user?.apiKey, + }, +}); +``` + +### Custom `securityResolver` + +Run arbitrary logic whenever a tool needs credentials. + +```ts +OpenapiAdapter.init({ + name: 'multi-auth', + baseUrl: 'https://api.example.com', + url: 'https://api.example.com/openapi.json', + securityResolver: (tool, authInfo) => { + if (tool.name.startsWith('github_')) { + return { jwt: authInfo.user?.githubToken }; + } + if (tool.name.startsWith('google_')) { + return { jwt: authInfo.user?.googleToken }; + } + return { jwt: authInfo.token }; + }, +}); +``` + +### Include credentials in tool inputs (development only) + +Set `generateOptions.includeSecurityInInput = true` to expose security fields in the tool schema. This is useful while exploring a new API but is flagged as **high risk** and should not be shipped to production. + +## Load and generate options + +Every `loadOptions` and `generateOptions` value is forwarded to [`mcp-from-openapi`](https://www.npmjs.com/package/mcp-from-openapi): + +- `loadOptions` — control validation (`validate`), `$ref` resolution (`dereference`), custom headers, `timeout`, and `followRedirects` when fetching the spec. +- `generateOptions` — filter/rename operations (`filterFn`, `defaultInclude`, `excludeOperationIds`), set `preferredStatusCodes`, keep or drop deprecated endpoints, include multiple responses, surface examples, or customize the naming strategy. +- `includeSecurityInInput`, `includeDeprecated`, `includeAllResponses`, and `includeExamples` map directly to the generator so you decide how much of the spec becomes part of each tool. + +## Understand the mapper + +Each generated tool exposes a `mapper` so you can see how validated inputs become an HTTP request. Conflicting parameter names are automatically renamed. + +```ts +import { OpenAPIToolGenerator } from 'mcp-from-openapi'; + +const generator = await OpenAPIToolGenerator.fromURL('https://api.example.com/openapi.json'); +const [createExpense] = await generator.generateTools(); + +console.log(createExpense.mapper); +// [ +// { inputKey: 'pathId', type: 'path', key: 'id' }, +// { inputKey: 'bodyId', type: 'body', key: 'id' }, +// ... +// ] +``` + +Use the mapper to plug in custom logging, redaction, or request mutation logic via `headersMapper` and `bodyMapper`. + +## Validation & logging + +During `fetch()` the adapter validates your security configuration against the spec and prints: + +- A **risk score** (`low`, `medium`, `high`) so you can catch unsafe configurations early. +- Warnings for missing `authProviderMapper` entries or when credentials would flow through tool inputs. +- A fatal error when a required security scheme cannot be satisfied (the adapter refuses to expose tools until it is fixed). + +Resolve these warnings before exposing the adapter publicly. + +## Tips + +- Combine adapters with plugins (cache, logging, authorization) for consistent cross-cutting behavior. +- Attach multiple OpenAPI adapters to one app; just set a unique `name` value for each instance. +- Use `inputSchemaMapper` or `bodyMapper` to hide secrets from the tool interface while still injecting them into requests. +- Keep `includeSecurityInInput` disabled in production so credentials never transit through the client. + +## Links + +- Demo app: `apps/demo/src/apps/expenses/index.ts` +- Spec used by the demo: https://frontmcp-test.proxy.beeceptor.com/openapi.json +- Generator library: https://www.npmjs.com/package/mcp-from-openapi +- Source code: `libs/adapters/src/openapi` diff --git a/docs/docs/v/0.2/deployment/local-dev-server.mdx b/docs/draft/docs/v/0.3/deployment/local-dev-server.mdx similarity index 100% rename from docs/docs/v/0.2/deployment/local-dev-server.mdx rename to docs/draft/docs/v/0.3/deployment/local-dev-server.mdx diff --git a/docs/docs/v/0.2/deployment/production-build.mdx b/docs/draft/docs/v/0.3/deployment/production-build.mdx similarity index 100% rename from docs/docs/v/0.2/deployment/production-build.mdx rename to docs/draft/docs/v/0.3/deployment/production-build.mdx diff --git a/docs/draft/docs/v/0.3/getting-started/installation.mdx b/docs/draft/docs/v/0.3/getting-started/installation.mdx new file mode 100644 index 00000000..a69d780d --- /dev/null +++ b/docs/draft/docs/v/0.3/getting-started/installation.mdx @@ -0,0 +1,117 @@ +--- +title: Installation +icon: arrow-down-to-line +--- + +## Prerequisites + +- **Node.js ≥ 22** +- **npm ≥ 10** (pnpm / yarn work too) +- TypeScript project (the init command can set this up) + +--- + +## Option A — Create a new project + +Use the built-in project generator. It **requires** a project name and will create a new folder under your current directory. + + + ```bash universal + npx frontmcp create my-app + ``` + + ```bash pnpm + pnpm dlx frontmcp create my-app + ``` + + ```bash yarn + yarn dlx frontmcp create my-app + ``` + + + +This will: + +- scaffold a FrontMCP project in `./my-app` +- configure `tsconfig.json` for decorators and modern ESM +- generate a `package.json` with helpful scripts +- install required dev dependencies (e.g. TypeScript, tsx, zod, reflect-metadata) + +--- + +## Option B — Add to an existing project + +Install the CLI and Node types (FrontMCP bundles compatible `@frontmcp/sdk` internally—no separate install needed). + + + ```bash npm + npm i -D frontmcp @types/node@^20 + ``` + + ```bash pnpm + pnpm add -D frontmcp @types/node@^20 + ``` + ```bash yarn + yarn add -D frontmcp @types/node@^20 + ``` + + + +Then initialize FrontMCP in your project root: + +```bash +npx frontmcp init +``` + +`init` will: + +- add/update **scripts** in your `package.json` +- ensure your **tsconfig.json** includes required compiler options +- verify a sensible project layout + +--- + +## Package scripts + +After `create` or `init`, you’ll have these scripts: + +```json +{ + "scripts": { + "dev": "frontmcp dev", + "build": "frontmcp build", + "inspect": "frontmcp inspector", + "doctor": "frontmcp doctor" + } +} +``` + +**What they do** + +- `frontmcp dev` — run your server in watch mode (tsx) +- `frontmcp build` — compile your entry with TypeScript (outputs to `./dist` by default) +- `frontmcp inspector` — launch the MCP Inspector (`npx @modelcontextprotocol/inspector`) +- `frontmcp doctor` — validate Node/npm versions, tsconfig, and project setup + +--- + +## Verify your setup + +Run: + +```bash +npm run doctor +``` + + + If anything is missing or misconfigured (Node/npm versions, `tsconfig.json`, scripts), **doctor** will tell you + exactly what to fix. + + +--- + +## Next steps + +- Start developing: `npm run dev` +- Build for distribution: `npm run build` +- Explore tools and messages live: `npm run inspect` diff --git a/docs/docs/getting-started/quickstart.mdx b/docs/draft/docs/v/0.3/getting-started/quickstart.mdx similarity index 95% rename from docs/docs/getting-started/quickstart.mdx rename to docs/draft/docs/v/0.3/getting-started/quickstart.mdx index e7a7147e..02d819c8 100644 --- a/docs/docs/getting-started/quickstart.mdx +++ b/docs/draft/docs/v/0.3/getting-started/quickstart.mdx @@ -1,12 +1,11 @@ --- title: Quickstart -slug: getting-started/quickstart icon: rocket-launch --- Welcome! This guide will help you quickly set up FrontMCP and run your first MCP server. -If you haven't already installed FrontMCP, follow the [installation instructions](/getting-started/installation). +If you haven't already installed FrontMCP, follow the [installation instructions](./installation). ## 1) Create your FrontMCP server diff --git a/docs/docs/getting-started/welcome.mdx b/docs/draft/docs/v/0.3/getting-started/welcome.mdx similarity index 92% rename from docs/docs/getting-started/welcome.mdx rename to docs/draft/docs/v/0.3/getting-started/welcome.mdx index dc83de7b..6694ffb6 100644 --- a/docs/docs/getting-started/welcome.mdx +++ b/docs/draft/docs/v/0.3/getting-started/welcome.mdx @@ -65,4 +65,4 @@ export default class Server {} validation out of the box—no custom wiring needed. -**Build something real**: jump to the [Quickstart](/getting-started/quickstart) or set up your workspace in [Installation](/getting-started/installation). +**Build something real**: jump to the [Quickstart](./quickstart) or set up your workspace in [Installation](./installation). diff --git a/docs/draft/docs/v/0.3/guides/add-openapi-adapter.mdx b/docs/draft/docs/v/0.3/guides/add-openapi-adapter.mdx new file mode 100644 index 00000000..4903d4a1 --- /dev/null +++ b/docs/draft/docs/v/0.3/guides/add-openapi-adapter.mdx @@ -0,0 +1,131 @@ +--- +title: Add OpenAPI Adapter +slug: guides/add-openapi-adapter +--- + +The OpenAPI adapter is powered by [`mcp-from-openapi`](https://www.npmjs.com/package/mcp-from-openapi) and can turn any OpenAPI 3.x document into ready-to-run MCP tools. Follow the steps below to add it to your server. + +## 1. Install the adapter + +```bash +npm install @frontmcp/adapters +``` + +(If you scaffolded your project with `frontmcp create`, it is already part of the workspace.) + +## 2. Initialize the adapter inside an app + +```ts +import { App } from '@frontmcp/sdk'; +import { OpenapiAdapter } from '@frontmcp/adapters'; + +@App({ + id: 'expense', + name: 'Expense MCP app', + adapters: [ + OpenapiAdapter.init({ + name: 'backend:api', + url: 'https://frontmcp-test.proxy.beeceptor.com/openapi.json', + baseUrl: 'https://frontmcp-test.proxy.beeceptor.com', + }), + ], +}) +export default class ExpenseMcpApp {} +``` + +Set `name` to something unique per adapter and always provide `baseUrl` so the adapter can build absolute request URLs. + +## 3. Map authentication + +Start with static headers for server-to-server APIs: + +```ts +OpenapiAdapter.init({ + name: 'billing', + baseUrl: 'https://api.example.com', + url: 'https://api.example.com/openapi.json', + additionalHeaders: { + 'x-api-key': process.env.BILLING_API_KEY!, + }, +}); +``` + +For user-scoped APIs, prefer `authProviderMapper` or a custom `securityResolver` so each security scheme pulls the right token from `authInfo`: + +```ts +OpenapiAdapter.init({ + name: 'integrations', + baseUrl: 'https://api.example.com', + url: 'https://api.example.com/openapi.json', + authProviderMapper: { + GitHubAuth: (authInfo) => authInfo.user?.githubToken, + SlackAuth: (authInfo) => authInfo.user?.slackToken, + }, +}); +``` + +```ts +OpenapiAdapter.init({ + name: 'multi-auth', + baseUrl: 'https://api.example.com', + url: 'https://api.example.com/openapi.json', + securityResolver: (tool, authInfo) => { + if (tool.name.startsWith('github_')) return { jwt: authInfo.user?.githubToken }; + if (tool.name.startsWith('google_')) return { jwt: authInfo.user?.googleToken }; + return { jwt: authInfo.token }; + }, +}); +``` + +Avoid leaking credentials through tool inputs; the adapter will flag that configuration as high risk. + +## 4. Tune load & generate options + +You can pass any [`mcp-from-openapi` option](https://www.npmjs.com/package/mcp-from-openapi) via `loadOptions` or `generateOptions`. + +```ts +OpenapiAdapter.init({ + name: 'backend:api', + baseUrl: 'https://api.example.com', + url: 'https://api.example.com/openapi.json', + loadOptions: { + validate: true, + dereference: true, + headers: { Authorization: `Bearer ${process.env.SPEC_TOKEN}` }, + timeout: 8000, + followRedirects: true, + }, + generateOptions: { + defaultInclude: true, + excludeOperationIds: ['deprecatedThing'], + filterFn: (op) => op.path.startsWith('/invoices'), + preferredStatusCodes: [200, 201, 202], + includeExamples: true, + }, +}); +``` + +Use these flags to exclude endpoints, rename tools, or include additional response metadata. + +## 5. Watch the mapper & security logs + +Run `frontmcp dev` and look for the adapter banner. It prints: + +- A security **risk score** (`low`, `medium`, `high`). +- Warnings when a required security scheme cannot be satisfied. +- The number of generated tools. + +Fix warnings before promoting the server. + +## 6. Validate before shipping + +- Keep `generateOptions.includeSecurityInInput` set to `false` in production so credentials never appear in tool inputs. +- If your API requires multiple auth providers, supply an `authProviderMapper` entry for each security scheme. +- When you filter operations, make sure you still expose the operations referenced by your client prompts. + +## Common gotchas + +- Always set `baseUrl`; otherwise the adapter cannot build outbound URLs. +- `filterFn` runs before `excludeOperationIds`. Use the combination to include a subset by default and explicitly drop legacy operations. +- The adapter will throw if a required security scheme lacks a mapping. This is intentional—fix the mapping instead of ignoring the warning. +- `includeSecurityInInput` is for local debugging only. It is logged as **high risk** and should not be deployed. diff --git a/docs/docs/v/0.2/guides/caching-and-cache-miss.mdx b/docs/draft/docs/v/0.3/guides/caching-and-cache-miss.mdx similarity index 100% rename from docs/docs/v/0.2/guides/caching-and-cache-miss.mdx rename to docs/draft/docs/v/0.3/guides/caching-and-cache-miss.mdx diff --git a/docs/docs/v/0.2/guides/customize-flow-stages.mdx b/docs/draft/docs/v/0.3/guides/customize-flow-stages.mdx similarity index 100% rename from docs/docs/v/0.2/guides/customize-flow-stages.mdx rename to docs/draft/docs/v/0.3/guides/customize-flow-stages.mdx diff --git a/docs/docs/v/0.2/servers/apps.mdx b/docs/draft/docs/v/0.3/servers/apps.mdx similarity index 100% rename from docs/docs/v/0.2/servers/apps.mdx rename to docs/draft/docs/v/0.3/servers/apps.mdx diff --git a/docs/docs/v/0.2/servers/authentication/local.mdx b/docs/draft/docs/v/0.3/servers/authentication/local.mdx similarity index 100% rename from docs/docs/v/0.2/servers/authentication/local.mdx rename to docs/draft/docs/v/0.3/servers/authentication/local.mdx diff --git a/docs/docs/servers/authentication/overview.mdx b/docs/draft/docs/v/0.3/servers/authentication/overview.mdx similarity index 100% rename from docs/docs/servers/authentication/overview.mdx rename to docs/draft/docs/v/0.3/servers/authentication/overview.mdx diff --git a/docs/docs/v/0.2/servers/authentication/remote-proxy.mdx b/docs/draft/docs/v/0.3/servers/authentication/remote-proxy.mdx similarity index 100% rename from docs/docs/v/0.2/servers/authentication/remote-proxy.mdx rename to docs/draft/docs/v/0.3/servers/authentication/remote-proxy.mdx diff --git a/docs/docs/v/0.2/servers/authentication/remote.mdx b/docs/draft/docs/v/0.3/servers/authentication/remote.mdx similarity index 100% rename from docs/docs/v/0.2/servers/authentication/remote.mdx rename to docs/draft/docs/v/0.3/servers/authentication/remote.mdx diff --git a/docs/docs/v/0.2/servers/authentication/token.mdx b/docs/draft/docs/v/0.3/servers/authentication/token.mdx similarity index 100% rename from docs/docs/v/0.2/servers/authentication/token.mdx rename to docs/draft/docs/v/0.3/servers/authentication/token.mdx diff --git a/docs/docs/v/0.2/servers/extensibility/adapters.mdx b/docs/draft/docs/v/0.3/servers/extensibility/adapters.mdx similarity index 100% rename from docs/docs/v/0.2/servers/extensibility/adapters.mdx rename to docs/draft/docs/v/0.3/servers/extensibility/adapters.mdx diff --git a/docs/docs/v/0.2/servers/extensibility/logging.mdx b/docs/draft/docs/v/0.3/servers/extensibility/logging.mdx similarity index 100% rename from docs/docs/v/0.2/servers/extensibility/logging.mdx rename to docs/draft/docs/v/0.3/servers/extensibility/logging.mdx diff --git a/docs/docs/v/0.2/servers/extensibility/plugins.mdx b/docs/draft/docs/v/0.3/servers/extensibility/plugins.mdx similarity index 100% rename from docs/docs/v/0.2/servers/extensibility/plugins.mdx rename to docs/draft/docs/v/0.3/servers/extensibility/plugins.mdx diff --git a/docs/docs/v/0.2/servers/extensibility/providers.mdx b/docs/draft/docs/v/0.3/servers/extensibility/providers.mdx similarity index 100% rename from docs/docs/v/0.2/servers/extensibility/providers.mdx rename to docs/draft/docs/v/0.3/servers/extensibility/providers.mdx diff --git a/docs/docs/v/0.2/servers/prompts.mdx b/docs/draft/docs/v/0.3/servers/prompts.mdx similarity index 100% rename from docs/docs/v/0.2/servers/prompts.mdx rename to docs/draft/docs/v/0.3/servers/prompts.mdx diff --git a/docs/docs/v/0.2/servers/resources.mdx b/docs/draft/docs/v/0.3/servers/resources.mdx similarity index 100% rename from docs/docs/v/0.2/servers/resources.mdx rename to docs/draft/docs/v/0.3/servers/resources.mdx diff --git a/docs/docs/servers/server.mdx b/docs/draft/docs/v/0.3/servers/server.mdx similarity index 100% rename from docs/docs/servers/server.mdx rename to docs/draft/docs/v/0.3/servers/server.mdx diff --git a/docs/draft/docs/v/0.3/servers/tools.mdx b/docs/draft/docs/v/0.3/servers/tools.mdx new file mode 100644 index 00000000..a04fae49 --- /dev/null +++ b/docs/draft/docs/v/0.3/servers/tools.mdx @@ -0,0 +1,114 @@ +--- +title: Tools +slug: servers/tools +icon: wrench +--- + +Tools are **typed actions** your server can execute. They’re described with Zod schemas and exposed via MCP. Implement as a class with `@Tool({...})` or as a function via `tool()`. + +## Minimal tool (class) + +```ts +import { Tool } from '@frontmcp/sdk'; +import { z } from 'zod'; + +@Tool({ + name: 'greet', + description: 'Greets a user by name', + inputSchema: { name: z.string() }, +}) +export default class GreetTool { + async execute({ name }: { name: string }) { + return `Hello, ${name}!`; + } +} +``` + +Register it on an app: + +```ts +@App({ id: 'hello', name: 'Hello', tools: [GreetTool] }) +export default class HelloApp {} +``` + +## Inline tool (function builder) + +```ts +import { tool } from '@frontmcp/sdk'; +import { z } from 'zod'; + +export const Add = tool({ + name: 'add', + description: 'Add two numbers', + inputSchema: { a: z.number(), b: z.number() }, +})((input) => input.a + input.b); +``` + +Add to app: + +```ts +@App({ name: 'Calc', tools: [Add] }) +class CalcApp {} +``` + +--- + +## Tool metadata + +```ts +@Tool({ + id?: string, + name: string, + description?: string, + inputSchema: { [key: string]: z.ZodTypeAny } | z.ZodObject, + rawInputSchema?: JSONSchema7, + outputSchema?: 'string' | 'number' | 'boolean' | 'date' | 'image' | 'audio' | 'resource' | 'resource_link' | z.ZodTypeAny | readonly ('string' | 'number' | 'boolean' | 'date' | 'image' | 'audio' | 'resource' | 'resource_link' | z.ZodTypeAny)[], + tags?: string[], + annotations?: { + title?: string, + readOnlyHint?: boolean, + destructiveHint?: boolean, + idempotentHint?: boolean, + openWorldHint?: boolean, + }, + hideFromDiscovery?: boolean, // default false +}) +``` + +**Notes** + +- `annotations` hint model & UI behavior (read-only, idempotent, etc.). +- `hideFromDiscovery` keeps a tool callable but off `tool/list`. +- Tools can attach **per-tool hooks** (see _Advanced → Hooks_). +- `rawInputSchema` lets you share a JSON Schema version of the input (handy for inspector tooling) while still passing a raw Zod shape to `inputSchema`. + +### Input & output schema shapes + +`inputSchema` can be a full `z.object({...})` or a raw shape (`{ name: z.string() }`). The SDK wraps raw shapes in an object internally, so you can keep declarations terse. + +`outputSchema` now accepts: + +- Literal primitives (`'string'`, `'number'`, `'boolean'`, `'date'`) when you return scalars. +- `'image'`, `'audio'`, `'resource'`, or `'resource_link'` when you emit MCP resource descriptors. +- Any Zod schema (objects, unions, arrays, discriminated unions, etc.). +- An array of the above to build tuple-like content (each entry becomes a separate structured content item). + +```ts +@Tool({ + name: 'add', + description: 'Add two numbers and echo the math', + inputSchema: { a: z.number(), b: z.number() }, + outputSchema: ['string', 'number'], +}) +export default class AddTool { + async execute({ a, b }: { a: number; b: number }) { + const result = a + b; + return [`${a} + ${b} = ${result}`, result]; + } +} +``` + +## Return values + +- Return primitives, structured objects, or tuple-like arrays. When `outputSchema` is provided (literal or Zod), the SDK validates the response and propagates the right metadata to clients. +- Errors are surfaced via MCP error responses; you can also throw typed errors inside executors. diff --git a/docs/snippets/card.jsx b/docs/draft/snippets/card.jsx similarity index 100% rename from docs/snippets/card.jsx rename to docs/draft/snippets/card.jsx diff --git a/docs/draft/updates.mdx b/docs/draft/updates.mdx new file mode 100644 index 00000000..020c98b9 --- /dev/null +++ b/docs/draft/updates.mdx @@ -0,0 +1,17 @@ +--- +title: 'Updates' +slug: 'updates' +icon: 'sparkles' +mode: 'center' +--- + + + + + + + diff --git a/docs/live/assets/banners/frontmcp-vs-standardmcp.dark.png b/docs/live/assets/banners/frontmcp-vs-standardmcp.dark.png new file mode 100644 index 00000000..75f9d4e2 Binary files /dev/null and b/docs/live/assets/banners/frontmcp-vs-standardmcp.dark.png differ diff --git a/docs/live/assets/banners/frontmcp-vs-standardmcp.light.png b/docs/live/assets/banners/frontmcp-vs-standardmcp.light.png new file mode 100644 index 00000000..cb23305f Binary files /dev/null and b/docs/live/assets/banners/frontmcp-vs-standardmcp.light.png differ diff --git a/docs/live/assets/banners/mcp-run-out-of-socket.svg b/docs/live/assets/banners/mcp-run-out-of-socket.svg new file mode 100644 index 00000000..ac5701c9 --- /dev/null +++ b/docs/live/assets/banners/mcp-run-out-of-socket.svg @@ -0,0 +1,1801 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/live/assets/banners/openapi-mcp-security.png b/docs/live/assets/banners/openapi-mcp-security.png new file mode 100644 index 00000000..d5b5c0e5 Binary files /dev/null and b/docs/live/assets/banners/openapi-mcp-security.png differ diff --git a/docs/live/assets/logo/banner.dark.png b/docs/live/assets/logo/banner.dark.png new file mode 100644 index 00000000..f41161aa Binary files /dev/null and b/docs/live/assets/logo/banner.dark.png differ diff --git a/docs/live/assets/logo/banner.light.png b/docs/live/assets/logo/banner.light.png new file mode 100644 index 00000000..454d34cc Binary files /dev/null and b/docs/live/assets/logo/banner.light.png differ diff --git a/docs/live/assets/logo/favicon-32x32.png b/docs/live/assets/logo/favicon-32x32.png new file mode 100644 index 00000000..64b60d7e Binary files /dev/null and b/docs/live/assets/logo/favicon-32x32.png differ diff --git a/docs/live/assets/logo/favicon.ico b/docs/live/assets/logo/favicon.ico new file mode 100644 index 00000000..e2e9a56a Binary files /dev/null and b/docs/live/assets/logo/favicon.ico differ diff --git a/docs/live/assets/logo/frontmcp.dark.png b/docs/live/assets/logo/frontmcp.dark.png new file mode 100644 index 00000000..9724541b Binary files /dev/null and b/docs/live/assets/logo/frontmcp.dark.png differ diff --git a/docs/live/assets/logo/frontmcp.dark.svg b/docs/live/assets/logo/frontmcp.dark.svg new file mode 100644 index 00000000..419bc288 --- /dev/null +++ b/docs/live/assets/logo/frontmcp.dark.svg @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/docs/live/assets/logo/frontmcp.light.png b/docs/live/assets/logo/frontmcp.light.png new file mode 100644 index 00000000..53068cfe Binary files /dev/null and b/docs/live/assets/logo/frontmcp.light.png differ diff --git a/docs/live/assets/logo/frontmcp.light.svg b/docs/live/assets/logo/frontmcp.light.svg new file mode 100644 index 00000000..261d9e50 --- /dev/null +++ b/docs/live/assets/logo/frontmcp.light.svg @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/docs/live/assets/logo/logo.dark.png b/docs/live/assets/logo/logo.dark.png new file mode 100644 index 00000000..0839903b Binary files /dev/null and b/docs/live/assets/logo/logo.dark.png differ diff --git a/docs/live/assets/logo/logo.dark.svg b/docs/live/assets/logo/logo.dark.svg new file mode 100644 index 00000000..5a65684d --- /dev/null +++ b/docs/live/assets/logo/logo.dark.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/docs/live/assets/logo/logo.light.png b/docs/live/assets/logo/logo.light.png new file mode 100644 index 00000000..410a1384 Binary files /dev/null and b/docs/live/assets/logo/logo.light.png differ diff --git a/docs/live/assets/logo/logo.light.svg b/docs/live/assets/logo/logo.light.svg new file mode 100644 index 00000000..a60cdaa4 --- /dev/null +++ b/docs/live/assets/logo/logo.light.svg @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/docs/live/blog/11-2025/introducing-frontmcp.mdx b/docs/live/blog/11-2025/introducing-frontmcp.mdx new file mode 100644 index 00000000..2771e2cd --- /dev/null +++ b/docs/live/blog/11-2025/introducing-frontmcp.mdx @@ -0,0 +1,321 @@ +--- +title: Introducing FrontMCP +description: The TypeScript-first framework for building production-grade MCP servers with decorators, DI, and Streamable HTTP. +mode: center +--- + + + + +## Why another MCP framework? + +If you’ve tried wiring up the Model Context Protocol (MCP) by hand, you already know the drill: + +- Define JSON schemas manually +- Hand-roll HTTP endpoints +- Keep auth, sessions, logging and transports consistent across tools +- Copy-paste boilerplate just to expose “one more” operation ([GitHub][1]) + +That’s fine for a weekend experiment, but painful for a real agentic backend. + +**FrontMCP** exists to make this boring part disappear. + +FrontMCP is a **TypeScript-first framework for MCP**: you describe servers, apps, tools, resources and prompts with decorators, and the framework handles protocol details, transport, dependency injection, sessions/auth, and execution flow for you. ([FrontMCP][2]) + + + If you’re new to MCP: it’s an open specification for how LLM clients talk to tools and data sources in a consistent + way. FrontMCP gives you a batteries-included way to implement those servers in TypeScript. + + +--- + +## FrontMCP in one sentence + +> **FrontMCP is the TypeScript way to build MCP servers with decorators, DI, and Streamable HTTP.** ([FrontMCP][2]) + +You write clean, typed code; FrontMCP takes care of: + +- Protocol & transport (MCP Streamable HTTP) +- Sessions & streaming +- Auth & security (remote and local OAuth) +- Logging, hooks, and extensibility via adapters and plugins ([FrontMCP][2]) + +Already sold? You can skip ahead to the Quickstart docs and have a server running in a few minutes. + +--- + +## The pillars of FrontMCP + + + + Use decorators and zod schemas to describe tools and apps, with strong typing from inputs to + responses. FrontMCP stays out of your way and lets the TypeScript type system do the heavy lifting. + + + + FrontMCP speaks MCP Streamable HTTP out of the box, including sessions and streaming responses, so you can plug + it + into any MCP-capable client without custom plumbing. + + + + Configure remote OAuth with your existing IdP, or use built-in local OAuth. Combine that with scoped execution + and + hooks for logging, rate limits, and policy checks. + + + + Generate tools from OpenAPI, enable transparent caching, wire custom logging transports, and more—without + turning + your codebase into a tangle of middleware. + + + + +--- + +## Core concepts (in 5 steps) + +At the heart of FrontMCP there are a few concepts you’ll see everywhere in the docs. + + + + A Server is your decorated entry point, defined with @FrontMcp. + It describes server info (name, version), which apps are available, HTTP settings, logging, session + configuration, and optional auth & providers. + + + + An App is a logical bundle of tools and related pieces, declared with @App. + You group tools, resources, prompts, adapters and plugins into apps so you can split behavior cleanly—per + product, per tenant, or per domain. + + + + A Tool is an active unit of work. You describe it with @Tool (class tools) + or tool()(handler) (function tools), and attach typed input/output schemas + using zod. + + + + Hooks give you cross-cutting behavior—auth checks, logging, rate limiting, request + transforms—while providers are dependency-injected singletons for things like config, DB, Redis, or KMS. + You control scopes per app, per session, or per request. + + + + Adapters generate tools/resources/prompts from external definitions (like OpenAPI), + and plugins layer on cross-cutting behavior such as caching or tracing—without polluting your business + logic. + + + + +--- + +## A tiny FrontMCP server + +Let’s look at a small, but realistic, FrontMCP server. It exposes a single tool that greets a user by name, grouped into a simple app. + +```ts title="src/main.ts" +import 'reflect-metadata'; +import { App, FrontMcp, Tool } from '@frontmcp/sdk'; +import { z } from 'zod'; + +@Tool({ + name: 'greet', + description: 'Greets a user by name', + inputSchema: z.object({ name: z.string() }), +}) +class GreetTool { + async execute({ name }: { name: string }) { + return `Hello, ${name}!`; + } +} + +@App({ + id: 'hello', + name: 'Hello App', + tools: [GreetTool], +}) +class HelloApp {} + +@FrontMcp({ + info: { + name: 'Hello MCP', + version: '0.1.0', + }, + apps: [HelloApp], +}) +export default class HelloServer {} +``` + +With less than a screenful of code you’ve just: + +- Defined a strongly typed tool +- Grouped it into an app +- Exposed a fully MCP-compatible server entrypoint with a clean contract ([FrontMCP][2]) + +--- + +## Installation in under a minute + +You can either scaffold a new project or add FrontMCP to an existing TypeScript codebase. + +### Option A — Create a new project + +```bash title="Create a new FrontMCP project" +npx frontmcp create my-app +``` + +This will: + +- Scaffold a new project under `./my-app` +- Configure `tsconfig.json` correctly for decorators and modern ESM +- Generate a `package.json` with useful scripts +- Install dev dependencies like TypeScript, `tsx`, `zod`, and `reflect-metadata` for you ([FrontMCP][3]) + +### Option B — Add to an existing project + +```bash title="Install the CLI and Node types" +npm i -D frontmcp @types/node@^20 +``` + +Then initialize FrontMCP in your project root: + +```bash title="Initialize FrontMCP" +npx frontmcp init +``` + +The `init` step updates your scripts, checks your `tsconfig.json`, and validates that your layout fits FrontMCP’s expectations. ([FrontMCP][3]) + +### Scripts you’ll see + +After using `create` or `init`, your `package.json` will include scripts like: + +```json title="package.json (scripts)" +{ + "scripts": { + "dev": "frontmcp dev", + "build": "frontmcp build", + "inspect": "frontmcp inspector", + "doctor": "frontmcp doctor" + } +} +``` + +- `frontmcp dev` – run your server in watch mode +- `frontmcp build` – compile your entry to `dist` +- `frontmcp inspector` – launch the MCP Inspector to explore tools live +- `frontmcp doctor` – validate Node/npm versions, tsconfig, and project setup ([FrontMCP][3]) + + + Once things look healthy, run npm run dev and connect any MCP-capable client to start calling your tools. + + +--- + +## From “Hello MCP” to production + +FrontMCP is designed so that the same primitives you use for a toy project scale all the way to hardened, production workloads. + +Some of the features you get out of the box: + +- **Sessions & Transport** + Choose between stateful and stateless session modes and configure how transport IDs are issued (`uuid` or `jwt`), depending on whether you run a single node or a distributed cluster. ([GitHub][1]) + +- **Authentication** + Use **remote OAuth** to integrate with an external IdP (like your B2B identity provider), or **local OAuth** for projects that want to keep everything inside the MCP server. Either way, you can scope auth per server or per app. ([GitHub][1]) + +- **Logging transports** + Ship logs to console, JSONL, or custom HTTP sinks by defining + + @LogTransport providers and wiring them into your server config. ([GitHub][1]) + +- **Adapters & plugins** + Use the OpenAPI adapter to turn existing REST APIs into MCP tools, and plug in caching, tracing, or policy engines as plugins—no need to wrap every tool manually. ([FrontMCP][4]) + +All of this is accessible from TypeScript, with type inference helping you stay honest as your server grows. + +--- + +## FrontMCP in the AgentFront ecosystem + +This is the **first post in the AgentFront blog**, and FrontMCP is a big part of why. + +We’re betting on a future where: + +- MCP is the default protocol for LLM tools and data access +- TypeScript is the default language for web-scale infrastructure +- Agentic systems are composed of many small, well-typed servers, not one giant monolith + +FrontMCP is our open-source foundation for that world: + +- 📦 **Source code**: [`agentfront/frontmcp` on GitHub](https://github.com/agentfront/frontmcp) ([GitHub][1]) +- 📚 **Docs**: [`/docs`](/docs/getting-started/welcome) for the latest 0.3 series ([FrontMCP][2]) + +We’ll be using this blog to share patterns, real-world architectures, and deep dives into features like adapters, auth, and deployment. + +--- + +## Where to go next + + + + Get the high-level overview of FrontMCP’s concepts and how it fits into MCP. + + + + Go step-by-step from empty folder to a running MCP server with a real tool. + + + + + + + Explore example apps and tools that showcase patterns you can reuse in your own servers. + + + + FrontMCP is open source (Apache-2.0). Issues, PRs, and feedback are very welcome. + + + + +--- + +This is just the beginning. In upcoming posts, we’ll dig into: + +- Designing tool interfaces your LLMs actually use +- Using adapters to wrap existing APIs +- Building secure multi-tenant MCP servers with remote OAuth +- Observability patterns with custom logging transports and hooks + +If you build something cool with FrontMCP, we’d love to hear about it—and maybe feature it here. + +[1]: https://github.com/agentfront/frontmcp 'GitHub - agentfront/frontmcp: FrontMCP Framework' +[2]: /docs/getting-started/welcome 'Welcome to FrontMCP - FrontMCP' +[3]: /docs/getting-started/installation 'Installation - FrontMCP' +[4]: /docs/getting-started/quickstart 'Quickstart - FrontMCP' diff --git a/docs/live/blog/11-2025/mcp-run-out-of-socket.mdx b/docs/live/blog/11-2025/mcp-run-out-of-socket.mdx new file mode 100644 index 00000000..675a8224 --- /dev/null +++ b/docs/live/blog/11-2025/mcp-run-out-of-socket.mdx @@ -0,0 +1,492 @@ +--- +title: Stop Spawning MCP Servers for Every Agent +description: One server, many agents. How FrontMCP makes multi-agent, multi-app MCP servers the default—with sessions, scoping, auth, DI, and decorators. +mode: center +--- + +One server, many agents + +# One Server, Many Agents + +Turn your MCP server into a shared, multi-app service—without transport gymnastics + +**TL;DR**: The MCP spec and official SDKs give you transports and sessions, but most examples wire a **single connection** to a **single server instance**. That’s fine until you want _several IDEs, bots, and dashboards_ to share **one** server—and to host **multiple apps** behind clean auth and scopes. **FrontMCP** makes that the default with **sessions, transport identity, per-app scoping, DI, decorators, and authentication** out of the box. + + + This is not a “stdio vs SSE” post. The issue is the connection model and defaults most examples + encourage—regardless of transport. Streamable HTTP + sessions is the spec-aligned path for multi-client servers; + FrontMCP productizes it and layers scoping, auth, and DI on top. + + +--- + +## How the “one client, one server” pattern sneaks into production + +If you follow the happy path: + +1. You open the official @modelcontextprotocol/sdk docs. +2. You copy the example that spins up a server. +3. You attach a transport (stdio, HTTP, SSE, or Streamable HTTP). +4. You connect an MCP client, see your tools, call them, and ship a demo. + +So far, life is good: + +- One IDE ↔ one MCP client ↔ one server. +- Logs are readable. +- JSON-RPC messages look clean. +- You can restart everything with a single command. + +The problems show up the moment you try to do something slightly more realistic: + +- A second IDE instance for another developer. +- A background worker using the same tools. +- A dashboard that introspects tools and runs them on behalf of users. +- Maybe even multiple agents orchestrating workflows together. + +You quickly notice patterns like: + +- **N agents = N server processes** + Each agent spins its own server, often as a child process or a separate container. +- **Zombie servers** + When a client crashes, the server it launched may keep running. +- **Scattered logs and state** + Caches, warmups, and logs live in different processes. +- **Awkward auth stories** + Access tokens and OAuth flows are tied to a single client/server pair. + +None of these are protocol problems. They’re **shape-of-server** problems: + +> You accidentally treated your MCP server as a client-owned subprocess instead of a shared, long-lived service. + +--- + +## Why this isn’t really about stdio vs HTTP vs SSE + +It’s tempting to blame the transport: + +- “Maybe stdio is the problem.” +- “Maybe SSE is the problem.” +- “Maybe I should just switch to Streamable HTTP and everything will be fine.” + +Transport matters, but the deeper issue is independent of the wire: + +- If your code assumes **one transport = one connection = one client**, +- Then it doesn’t matter whether that transport is stdio, SSE, or Streamable HTTP, +- You’ll still end up multiplying servers for every serious client. + +The MCP spec’s Streamable HTTP transport is explicitly designed to support **multiple clients + sessions**. The challenge is that most starter servers don’t implement a **multi-client server model**; they implement a **single-client demo**. + +That’s where you start writing glue: + +- a homegrown session store, +- hand-managed transport IDs, +- per-app routing, +- DIY auth integration, +- and a bunch of conditionals sprinkled across tools and handlers. + +You can do it—but you’re building a framework on evenings and weekends. + +--- + +## The real problem you’re hitting + +When you move beyond a single client, you suddenly need to solve: + +- **Session IDs & transport identity** + So requests don’t collide across clients and nodes, and you can safely resume or route streams. +- **Per-app scoping** + So multiple products/tenants can share one host without stepping on each other. +- **Auth surfaces** + Remote IdP or local issuer, wired consistently and scoped correctly to apps and tools. +- **Lifecycle management** + Sessions that can be created, resumed, and ended in a way that survives proxies, restarts, and load balancers. +- **DI & hooks** + So you can inject config, secrets, caches, tenants, and logging **without** duplicating boilerplate in every tool. + +All of that is infrastructure work. Necessary, but not what makes your product interesting. + +What you actually want is: + +- “Here are my tools and apps, written like normal TypeScript services.” +- “Give me a multi-agent, multi-app MCP server that Just Works.” + +--- + +## What we actually want from an MCP server + +If you strip away the noise, most teams want their MCP server to behave like a modern web backend: + +1. **Multi-client by design** + + - Many IDEs, agents, and services share one server. + - Sessions keep each client isolated and resumable. + - Transport identity is stable across reconnections and nodes. + +2. **Multi-app composition** + + - Group tools into apps by product/domain/tenant. + - Sometimes expose a unified toolbox. + - Sometimes isolate apps with separate auth and base paths. + - Sometimes expose an app as a standalone MCP server. + +3. **Auth-aware tool surface** + + - The tools a client sees are based on _who is logged in_, not just what’s deployed. + - OAuth scopes and identities matter. + +4. **Typed code with DI** + + - Tools look like clean TypeScript handlers. + - Inputs/outputs are validated with Zod. + - Dependencies (DB, caches, external APIs) are injected via DI. + +5. **Adapters for existing systems** + - Already have REST APIs? Use OpenAPI, not Ctrl+C/Ctrl+V into new @Tool classes. + +The MCP spec gives you the foundations (JSON-RPC, transports, sessions). +**FrontMCP** takes those foundations and bakes them into a framework that assumes multi-client, multi-app from day one. + +--- + +## What FrontMCP flips to “on by default” + +FrontMCP is a TypeScript-first framework that treats MCP servers like proper services, not disposable processes. + + + + Start a Streamable HTTP host that accepts many clients concurrently. Sessions and transport IDs are handled for + you, so multiple IDEs, copilots, and workers can share one server safely. + + + + Host several apps under one server or isolate them via splitByApp. Each app gets clean base paths + and can define its own auth. + + + + Use remote OAuth to front an external IdP, or local OAuth with a built-in issuer. Scope auth at + the server or per app. + + + + Describe servers, apps, tools, resources, and prompts with decorators. Inject providers with GLOBAL, + SESSION, or REQUEST scope. Add hooks for logging, rates, and policy without + boilerplate. + + + + +--- + +## Standard MCP vs FrontMCP (visually) + +Sometimes it’s easier to just see it: + +Before vs. after +Before vs. after + +- On the left: the “one client, one server” pattern duplicated per agent. +- On the right: one FrontMCP server hosting many apps and many agents in a spec-aligned way. + +--- + +## Show me the code (multi-agent, multi-app, scoped & authenticated) + +Let’s look at what a multi-client, multi-app server looks like in FrontMCP. + +We’ll define: + +- A server with Streamable HTTP. +- Two apps: `billing` and `docs`. +- Per-app auth for `billing`. + +```ts title="src/server.ts" +import 'reflect-metadata'; +import { FrontMcp, LogLevel } from '@frontmcp/sdk'; +import BillingApp from './billing.app'; +import DocsApp from './docs.app'; + +@FrontMcp({ + info: { name: 'FrontMCP Demo', version: '1.0.0' }, + + // Host multiple apps; split makes base paths & auth per-app: + splitByApp: true, + apps: [BillingApp, DocsApp], + + // Streamable HTTP host (multi-client capable) + http: { + port: 3001, + entryPath: '', // align with your .well-known PRM resourcePath, can be /mcp to prevent conflicts + }, + + logging: { level: LogLevel.INFO }, +}) +export default class Server {} +``` + +```ts title="src/billing.app.ts" +import { App } from '@frontmcp/sdk'; +import CreateInvoice from './tools/create-invoice.tool'; + +@App({ + id: 'billing', + name: 'Billing', + tools: [CreateInvoice], + // Per-app auth surface: + auth: { + type: 'remote', + name: 'corp-idp', + baseUrl: 'https://auth.example.com', + }, +}) +export default class BillingApp {} +``` + +```ts title="src/tools/create-invoice.tool.ts" +import { Tool, ToolContext } from '@frontmcp/sdk'; +import { z } from 'zod'; + +@Tool({ + name: 'create_invoice', + description: 'Create an invoice for a customer', + inputSchema: { customerId: z.string(), amount: z.number().positive() }, +}) +export default class CreateInvoice extends ToolContext { + async execute({ customerId, amount }) { + // DI-provided services are available from the execution context if needed + return { status: 'ok', invoiceId: `inv_${customerId}_${Date.now()}`, amount }; + } +} +``` + +From here, any number of MCP-capable clients can: + +- Connect over Streamable HTTP. +- Create their own sessions. +- Call `billing.create_invoice` (if authorized). +- Share the same app code and server instance. + +No more “one agent, one server” as an architectural constraint. + +--- + +## Composition modes: one server, many ways to slice apps + +The most underrated part of FrontMCP is how you can expose the same apps in different shapes—without changing the app code. + +### `splitByApp: false` — unified tool surface per user + +When `splitByApp` is **false** (the default): + +- All your applications are combined under **one MCP server**. +- A connected client can **list all tools** the **logged-in OAuth user** is allowed to see, regardless of which app defines them. +- Auth still applies, but the visible tools and resources are filtered by the user’s identity and scopes. +- If two apps define tools with the same name, FrontMCP automatically **prefixes the tool name with the app id** + —for example: `billing.create_invoice` vs `ops.create_invoice`. + +This mode is perfect when you want: + +- A single, large toolbox in the IDE. +- Agents that can orchestrate across many domains (billing + docs + ops). +- One “MCP endpoint” for the whole suite, with per-user filtering. + +Under the hood, your apps are still cleanly separated. The unified tool surface is a **view**, not a monolith. + +### `splitByApp: true` — per-app isolation + +When `splitByApp` is **true**: + +- Each app gets its own **base path** and can define its own **auth**. +- You can run multiple products/tenants under one server but with clearly separated surfaces. +- Clients can connect directly to the specific app they care about, while you still deploy just one server process. + +This is ideal when: + +- Apps have distinct consumers and security boundaries. +- You want a “microservices-like” feel, but still prefer one deployment unit. +- You’re onboarding teams gradually and want a clear line between their responsibilities. + +### Standalone apps under `/{appId}` + +Some apps need to serve double duty: + +- They live inside a grouped server (for internal or cross-app usage). +- They also need to behave as **standalone MCP servers** for specific clients. + +FrontMCP supports apps that are **standalone and excluded from the grouped surface**: + +- The app is served under the prefix `/{appId}` as if it were its own MCP server. +- From the client’s perspective, `/{appId}` is a standalone MCP endpoint with its own discovery and tools. +- From your perspective, it’s still the same app class and the same deployment. + +This lets you: + +- Expose a clean `/{appId}` endpoint for partners while keeping internal tools under a unified host. +- Gradually carve out apps from a single MCP server into more “independent” endpoints without duplicating code. + +One deployment, one codebase, multiple MCP “faces” tuned to who’s calling you. + +--- + +## Plugging in existing REST APIs with OpenAPIAdapter + +A common reaction to any MCP framework is: + +> “This is cool, but I already have a REST API. Do I really have to rewrite everything as @Tool classes?” + +With FrontMCP, the answer is **no**. + +### Connect your REST API with OpenAPIAdapter + +The **OpenAPI adapter** lets you generate tools directly from an OpenAPI spec: + +1. Provide an OpenAPI document (URL or file). +2. Specify the base URL for requests. +3. Optionally plug in middleware for auth, tenancy, or logging. +4. Get a bundle of MCP tools that call your REST endpoints. + +Here’s a sketch: + +```ts title="src/openapi.app.ts" +import { App } from '@frontmcp/sdk'; +import { createOpenApiAdapter } from '@frontmcp/openapi-adapter'; + +const openApi = await createOpenApiAdapter({ + id: 'my-rest-api', + name: 'My REST API', + schemaUrl: 'https://api.example.com/openapi.json', + baseUrl: 'https://api.example.com', + requestMiddleware: async (req, ctx) => { + const token = await ctx.providers.auth.getApiToken(); + req.headers['Authorization'] = `Bearer ${token}`; + return req; + }, +}); + +@App({ + id: 'rest', + name: 'REST-backed tools', + tools: openApi.tools, +}) +export default class RestApp {} +``` + +You can find the full details in the docs: +👉 **[OpenAPI Adapter docs](/docs/adapters/openapi-adapter)** (based on the guide at `https://docs.agentfront.dev/0.3/adapters/openapi-adapter`). + +This is the fastest path to: + +- Making your existing API “MCP-native.” +- Letting agents and IDEs call it via tools. +- Avoiding a full parallel tool implementation. + +--- + +## Migration playbook (keep your tools; change your host) + +If you already have MCP servers or REST APIs, here’s a practical way to move into FrontMCP without a rewrite: + + + + Wrap existing handlers with @Tool or tool()(handler) and group them under{' '} + @App. Or plug in the OpenAPI adapter to generate tools from your REST API. + + + Start with splitByApp: false for a unified tool surface, then introduce + splitByApp: true or standalone apps under + /{'{'}appId{'}'} + where isolation is needed. + + + Decide on remote vs local OAuth. Make your tool surface depend on the logged-in user and scopes, not + just the server process. + + + Keep entryPath in sync with your .well-known PRM resourcePath. Test behind + your real proxy/load balancer to validate streaming and keep-alives. + + + Add DI providers and hooks for logging, metrics, and tracing so you can see which apps and tools your agents + actually use. + + + + + Already running locally? Use frontmcp dev to boot the host and attach multiple MCP clients at once. + Multi-agent scenarios stop being hypothetical when you can open two clients and see them share one server. + + +--- + +## FAQs + +**Is this about “legacy SSE vs stdio”?** +No. The issue is the single-connection default common in examples across transports. Streamable HTTP + sessions already solves multi-client semantics; FrontMCP makes that the default and adds scoping, auth, and DI. + +**Can one server safely host multiple apps for multiple agents?** +Yes. With splitByApp: false, you get a unified tool surface per user (with automatic name-conflict prefixing). With splitByApp: true, each app gets isolated paths and optional per-app auth, while sharing the same underlying server. Standalone apps under /{'{'}appId{'}'} give you separate MCP endpoints when you need them. + +**Can I connect my current REST API without writing tools?** +Yes. Use the OpenAPIAdapter to generate tools automatically from your existing REST API. Point FrontMCP at your OpenAPI definition and let the adapter create MCP tools for you—no manual tool classes required. +See: OpenAPI Adapter docs (based on this guide). + +--- + +## Where to go next + + + + Get the high-level overview of FrontMCP’s concepts and how it fits into MCP. + + + Go step-by-step from empty folder to a running MCP server with a real tool. + + + + + + Explore example apps and tools that showcase patterns you can reuse in your own servers. + + + FrontMCP is open source (Apache-2.0). Issues, PRs, and feedback are very welcome. + + diff --git a/docs/live/blog/11-2025/openapi-mcp-security-nightmare.mdx b/docs/live/blog/11-2025/openapi-mcp-security-nightmare.mdx new file mode 100644 index 00000000..e349cbe1 --- /dev/null +++ b/docs/live/blog/11-2025/openapi-mcp-security-nightmare.mdx @@ -0,0 +1,575 @@ +--- +title: The OpenAPI-to-MCP Security Nightmare You Didn't Know You Had +sidebarTitle: The OpenAPI-to-MCP Security Nightmare +description: Why most OpenAPI-to-MCP tools are leaking your credentials, mixing headers between requests, and routing your customer tokens through someone else's cloud—and how to fix it. +mode: center +--- + + + +## The problem nobody's talking about + +You have an OpenAPI spec. You want to expose it as MCP tools. You search npm, find a library that promises "OpenAPI to MCP in 5 minutes," paste your configuration, and boom—you're done. + +**Except you just created a security nightmare.** + +Here's what's happening behind the scenes in most OpenAPI-to-MCP libraries: + +1. **Your JWT tokens are exposed** in tool input schemas, visible to every MCP client +2. **Headers are mixed** between requests, leaking credentials across different API calls +3. **Your traffic routes through someone else's cloud**, sending customer tokens outside your infrastructure +4. **No validation** of security configurations, leaving you vulnerable without warnings + +Let me show you exactly what I mean. + +--- + +## Exhibit A: The credential exposure problem + +Most OpenAPI-to-MCP libraries take the "easy" approach: if your OpenAPI spec has security requirements, they add those fields to the tool's input schema. + +```ts +// What most libraries generate +{ + name: "createUser", + inputSchema: { + type: "object", + properties: { + name: { type: "string" }, + email: { type: "string" }, + authorization: { type: "string" }, // ⚠️ EXPOSED! + apiKey: { type: "string" } // ⚠️ EXPOSED! + } + } +} +``` + +**Why is this bad?** + +1. **Logging disasters**: Your MCP client logs every tool call. Now you're logging JWTs, API keys, and OAuth tokens. +2. **Client-side exposure**: AI agents see these fields. Some might cache or transmit them insecurely. +3. **Developer confusion**: Your API consumers think they need to manually provide auth for every call. +4. **Compliance violations**: GDPR, SOC2, and PCI-DSS don't care that "the library made you do it." + +```ts +// What gets logged in your MCP client +{ + "tool": "createUser", + "arguments": { + "name": "John Doe", + "email": "john@example.com", + "authorization": "Bearer eyJhbGciOiJIUzI1NiIs..." // 🚨 LEAKED + } +} +``` + +Every call. Every log entry. Every error trace. Your JWTs, sitting in plain text. + +--- + +## Exhibit B: The header mixing catastrophe + +Some libraries try to be "clever" and manage authentication for you. But they don't isolate requests properly: + +```ts +// Pseudocode of what happens in many libraries +const globalHeaders = new Headers(); + +function callAPI(tool, input) { + // Set auth for this user + globalHeaders.set('authorization', input.token); + + // Make request + return fetch(tool.url, { headers: globalHeaders }); + + // ⚠️ Header never cleared! + // Next request to a different API uses the same headers! +} +``` + +**The result?** + +- User A calls GitHub API → sets `authorization: Bearer github_token_123` +- User B calls Slack API → **still has User A's GitHub token in headers** +- User B's Slack request now has both their Slack token AND User A's GitHub token + +This isn't theoretical. I've seen this exact bug in production systems processing millions of requests. + +```ts +// What actually gets sent to Slack +POST https://api.slack.com/messages +Headers: + authorization: Bearer github_token_123 // ⚠️ Wrong API! + x-slack-token: Bearer slack_token_456 // ✅ Correct + +// Slack rejects it. GitHub token leaks to Slack's logs. +``` + +--- + +## Exhibit C: The infrastructure control problem + +Here's the architectural decision that catches teams off-guard: + +**Self-hosted libraries** that convert OpenAPI to MCP run in your infrastructure. Your data stays in your VPC. You control the execution environment. + +**Cloud-based converters** route your API traffic through external infrastructure. Your requests, responses, and authentication tokens pass through someone else's systems. + +``` +Self-Hosted (Your Infrastructure): + MCP Client → Your Server → Your APIs ✅ + +Cloud-Based (External Infrastructure): + MCP Client → Their Cloud → Your APIs ⚠️ +``` + +**The critical questions:** + +1. **Where does authentication happen?** In your infrastructure or theirs? +2. **Who has access to tokens?** Just you, or the service provider too? +3. **What's their security posture?** SOC2? ISO 27001? GDPR-compliant? +4. **What's in their logs?** Are customer tokens being logged externally? +5. **Can you audit traffic?** Do you have visibility into what's being proxied? + +**When cloud routing becomes a compliance problem:** + +- **Regulated industries** (banking, healthcare): Customer tokens can't leave your infrastructure +- **Data sovereignty**: EU customer data routing through non-EU servers +- **Enterprise contracts**: "All customer data must remain within our VPC" +- **Audit requirements**: Need complete visibility into where tokens traveled + + + **Important distinction**: Enterprise-grade identity platforms (like Frontegg's AgentLink) are purpose-built for + secure token management with SOC2/ISO compliance, audit trails, and enterprise SLAs. Generic OpenAPI-to-MCP converter + tools typically aren't. + + +**Bottom line**: If you're building internal tools or need maximum control, self-hosted is the safest choice. If you use a cloud service, ensure it's a compliant identity platform, not just a conversion utility. + +--- + +## Exhibit D: The "it works on my machine" security + +Here's another pattern I see constantly: + +```ts +// Developer's local setup +OpenapiToMCP({ + spec: './api.yaml', + auth: { + apiKey: process.env.DEV_API_KEY, // ✅ Works locally + }, +}); +``` + +Looks fine, right? Developer tests it, it works, they ship it. + +**Production disaster:** + +```ts +// Production reality +OpenapiToMCP({ + spec: 'https://api.prod.com/openapi.yaml', + auth: { + apiKey: process.env.DEV_API_KEY, // ⚠️ Wrong key! + }, +}); +``` + +Problems: + +1. **No validation**: Library doesn't check if auth config matches spec requirements +2. **Silent failures**: Requests fail with generic 401s, no hint why +3. **Mixed environments**: Dev keys in prod, prod keys in dev +4. **Multi-provider chaos**: GitHub token used for Slack API, no errors, just failures + +Most libraries give you **zero** visibility into security configuration correctness. + +--- + +## The actual cost + +Let me make this concrete with a real-world scenario: + +**Company**: SaaS platform with 50,000 customers +**APIs**: GitHub, Slack, Stripe, internal APIs +**MCP Tools**: 200+ endpoints exposed via OpenAPI specs + +**What happened:** + +1. Used popular OpenAPI-to-MCP library +2. Didn't realize it exposed auth in input schemas +3. MCP client logs included customer OAuth tokens for 6 months +4. Security audit discovered it during SOC2 compliance review + +**The damage:** + +- **$500K+ cost**: Forensic analysis, customer notification, legal fees +- **3 weeks downtime**: Full security review, credential rotation +- **Lost customers**: 12 enterprise customers left immediately +- **Regulatory fines**: GDPR violations for EU customers +- **Reputation damage**: Security blog posts, HN discussion, vendor trust loss + +All because they chose the wrong OpenAPI-to-MCP library. + +--- + +## How FrontMCP solves this + +FrontMCP's OpenAPI adapter was built from day one with security as the foundation, not an afterthought. + +### 1. Authentication never exposed to clients + +```ts +import { OpenapiAdapter } from '@frontmcp/adapters'; + +OpenapiAdapter.init({ + name: 'my-api', + spec: myOpenApiSpec, + baseUrl: 'https://api.example.com', + + // ✅ Auth provider mapper - LOW RISK + authProviderMapper: { + GitHubAuth: (authInfo) => authInfo.user?.githubToken, + SlackAuth: (authInfo) => authInfo.user?.slackToken, + }, + + // ✅ Auth resolved from server-side context + // ✅ Never exposed in tool input schemas + // ✅ Never logged by MCP clients +}); +``` + +**Generated tool:** + +```ts +{ + name: "createUser", + inputSchema: { + type: "object", + properties: { + name: { type: "string" }, + email: { type: "string" } + // ✅ No auth fields! + } + } +} +``` + +Auth is resolved server-side from `authInfo` context. Clients never see it. + +### 2. Request isolation + multi-provider authentication + +Each request gets fresh headers with the correct auth provider—no global state, no mixing: + +```ts +OpenapiAdapter.init({ + name: 'multi-api', + spec: combinedSpec, + baseUrl: 'https://api.example.com', + + authProviderMapper: { + GitHubAuth: (authInfo) => authInfo.user?.githubToken, + SlackAuth: (authInfo) => authInfo.user?.slackToken, + StripeAuth: (authInfo) => authInfo.user?.stripeKey, + }, +}); + +// Each tool uses the right provider automatically: +// github_createRepo() → GitHubAuth → authInfo.user.githubToken +// slack_postMessage() → SlackAuth → authInfo.user.slackToken +// stripe_createCharge() → StripeAuth → authInfo.user.stripeKey +``` + +FrontMCP validates configuration at startup and creates isolated headers per-request—no cross-contamination possible. + +### 3. Security validation with risk scoring + +FrontMCP validates your security configuration and warns you: + +```ts +const validation = validateSecurityConfiguration(tools, options); + +console.log(validation); +// { +// valid: true, +// securityRiskScore: 'low', +// missingMappings: [], +// warnings: [ +// 'INFO: Using authProviderMapper - LOW security risk', +// 'Authentication resolved from user context' +// ] +// } +``` + +**Risk levels:** + +| Risk | Configuration | What it means | +| ------------- | ---------------------------------------- | ---------------------------------------------------- | +| **LOW** ✅ | `authProviderMapper`, `securityResolver` | Auth from context, not exposed | +| **MEDIUM** ⚠️ | `staticAuth`, `additionalHeaders` | Static credentials (acceptable for server-to-server) | +| **HIGH** 🚨 | `includeSecurityInInput: true` | Auth exposed to clients (NOT RECOMMENDED) | + +**Missing mappings detected:** + +```ts +// ❌ This fails at startup +OpenapiAdapter.init({ + spec: multiProviderSpec, // Has GitHubAuth, SlackAuth, StripeAuth + authProviderMapper: { + GitHubAuth: (authInfo) => authInfo.user?.githubToken, + // Missing SlackAuth and StripeAuth! + }, +}); + +// Error: Missing auth provider mappings for security schemes: SlackAuth, StripeAuth +// Solutions: +// 1. Add authProviderMapper: { 'SlackAuth': (authInfo) => authInfo.user?.slackToken } +// 2. Add securityResolver: (tool, authInfo) => ({ jwt: authInfo.token }) +// 3. Add staticAuth: { jwt: process.env.API_TOKEN } +``` + +You know **immediately** if your security config is wrong. No silent failures in production. + +### 5. Runs on YOUR infrastructure + +```ts +// FrontMCP runs entirely in your Node.js process +import { FrontMcp, App } from '@frontmcp/sdk'; +import { OpenapiAdapter } from '@frontmcp/adapters'; + +@FrontMcp({ + id: 'my-server', + apps: [MyApp], + http: { port: 3000 }, +}) +class MyServer {} + +// ✅ Your infrastructure +// ✅ Your VPC +// ✅ Your logs +// ✅ Your compliance boundary +// ✅ Your control +``` + +**No external dependencies. No cloud routing. No data leaving your infrastructure.** + +### 6. Headers and body mapping for tenant isolation + +```ts +OpenapiAdapter.init({ + name: 'saas-api', + spec: apiSpec, + baseUrl: 'https://api.example.com', + + headersMapper: (authInfo, headers) => { + // ✅ Add tenant ID from user context + if (authInfo.user?.tenantId) { + headers.set('x-tenant-id', authInfo.user.tenantId); + } + + // ✅ Add user authorization + if (authInfo.token) { + headers.set('authorization', `Bearer ${authInfo.token}`); + } + + return headers; + }, + + bodyMapper: (authInfo, body) => { + // ✅ Inject user context into all mutations + return { + ...body, + tenantId: authInfo.user?.tenantId, + userId: authInfo.user?.id, + }; + }, +}); +``` + +**Result:** + +- Tenant isolation guaranteed +- User context injected server-side +- Hidden from MCP clients +- Impossible to forge or bypass + +--- + +## Core security principles + +FrontMCP's OpenAPI adapter is built on five security principles: + +1. **Least Exposure** — Auth never exposed to MCP clients +2. **Isolation** — Fresh headers per request, no global state +3. **Validation** — Config validated at startup, not runtime +4. **Transparency** — Risk levels explicit (LOW/MEDIUM/HIGH) +5. **Control** — Runs in your infrastructure, no external routing + +--- + +## Migration guide: From insecure to secure + +If you're using another OpenAPI-to-MCP library, here's how to migrate: + +### Step 1: Audit your current setup + +```bash +# Search for exposed credentials +grep -r "authorization.*type.*string" ./generated-tools/ +grep -r "apiKey.*type.*string" ./generated-tools/ + +# Check logs for leaked tokens +grep -r "Bearer " ./logs/ +grep -r "api.*key" ./logs/ +``` + +### Step 2: Install FrontMCP + +```bash +npm install @frontmcp/sdk @frontmcp/adapters +``` + +### Step 3: Replace your adapter + + + +```ts Before (insecure) +import { openapiToMCP } from 'insecure-lib'; + +const tools = await openapiToMCP({ + spec: './api.yaml', + auth: { + apiKey: process.env.API_KEY, + }, +}); +``` + +```ts After (secure) +import { OpenapiAdapter } from '@frontmcp/adapters'; + +const adapter = OpenapiAdapter.init({ + name: 'my-api', + spec: require('./api.yaml'), + baseUrl: process.env.API_BASE_URL!, + authProviderMapper: { + BearerAuth: (authInfo) => authInfo.token, + ApiKeyAuth: (authInfo) => authInfo.user?.apiKey, + }, +}); +``` + + + +### Step 4: Rotate credentials + +```bash +# After migration, rotate all exposed credentials +# - Revoke old API keys +# - Regenerate OAuth tokens +# - Update environment variables +# - Clear logs with exposed credentials +``` + +### Step 5: Validate security + +```ts +import { validateSecurityConfiguration } from '@frontmcp/adapters/openapi'; + +const validation = validateSecurityConfiguration(tools, options); + +console.log(`Risk Score: ${validation.securityRiskScore}`); +console.log(`Valid: ${validation.valid}`); +console.log(`Warnings: ${validation.warnings.join('\n')}`); +``` + +--- + +## The comprehensive test suite + +We take security seriously. FrontMCP's OpenAPI adapter has **70 comprehensive tests** covering: + +- ✅ All authentication strategies +- ✅ Request isolation +- ✅ Multi-provider scenarios +- ✅ Security validation +- ✅ Missing mappings detection +- ✅ Headers and body mapping +- ✅ Error handling +- ✅ Edge cases + +```bash +npm test + +# Test Suites: 6 passed, 6 total +# Tests: 70 passed, 70 total +``` + +Every security feature is tested. Every edge case is covered. + +[View the test suite →](https://github.com/agentfront/frontmcp/tree/main/libs/adapters/src/openapi/__tests__) + +--- + +## Conclusion: Security isn't optional + +If you're using OpenAPI-to-MCP tools in production, ask yourself: + +1. ❓ Are my JWT tokens exposed in tool input schemas? +2. ❓ Are headers being mixed between different API requests? +3. ❓ Is my traffic routing through someone else's cloud? +4. ❓ Is my security configuration validated at startup? +5. ❓ Do I have visibility into security risk levels? + +If you answered "I don't know" to any of these, **you have a problem.** + +FrontMCP's OpenAPI adapter gives you: + +- ✅ **Zero credential exposure** — Auth resolved from context, never exposed +- ✅ **Request isolation** — Fresh headers per request, no mixing +- ✅ **Multi-provider support** — Map each security scheme to the right auth provider +- ✅ **Validation at startup** — Know immediately if config is wrong +- ✅ **Risk scoring** — Understand your security posture +- ✅ **Your infrastructure** — No external dependencies, no cloud routing + +**Security isn't optional. Choose the right tool.** + +--- + +## Get started with FrontMCP + + + + Comprehensive guide to the OpenAPI adapter with security best practices + + + + Get your secure MCP server running in 5 minutes + + + + Review the security implementation yourself + + + + Discuss security patterns with other developers + + + +--- + +
+

+ Don't let your OpenAPI-to-MCP integration become a security nightmare. +

+

Choose FrontMCP. Choose security.

+
diff --git a/docs/live/blog/external-links.mdx b/docs/live/blog/external-links.mdx new file mode 100644 index 00000000..6309c01b --- /dev/null +++ b/docs/live/blog/external-links.mdx @@ -0,0 +1,32 @@ +--- +title: Quick Links +description: External links to FrontMCP resources. +icon: link +sidebarTitle: Quick Links +mode: center +--- + + + FrontMCP official documentation site + + + FrontMCP source code on GitHub + + + Latest updates and news about FrontMCP and the ecosystem + + + Changelog for FrontMCP releases + + + Convert OpenAPI specifications to MCP tools with security validation + + + Convert JSON Schema to Zod schemas for type-safe validation + diff --git a/docs/live/blog/index.mdx b/docs/live/blog/index.mdx new file mode 100644 index 00000000..cdffe687 --- /dev/null +++ b/docs/live/blog/index.mdx @@ -0,0 +1,67 @@ +--- +title: Blog +description: Updates, features and explore the world of MCP and FrontMCP framework. +slug: /blog +icon: rss +sidebarTitle: Recent Posts +mode: frame +--- + +import { BlogCard } from '/snippets/card.jsx'; + +
+ +

Blog

+ +
+ Explore the world of MCP and FrontMCP framework. +
+ + + Why most OpenAPI-to-MCP tools are leaking your credentials, mixing headers between requests, and routing your customer + tokens through someone else's cloud—and how FrontMCP fixes it with authentication never exposed to clients, request + isolation guarantees, and security validation out of the box. + + + + Most SDK examples wire one transport to one client. That collapses when many IDEs, bots, and dashboards need to share + one server. FrontMCP flips the default: sessions, transport identity, per‑app scoping, auth, DI, and decorators—so a + single server can safely serve many agents across multiple apps. + + + + The TypeScript-first framework for building production-grade MCP servers with decorators, DI, and + Streamable HTTP. If you’ve tried wiring up the Model Context Protocol (MCP) by hand, you already know the drill + or define JSON schemas manually,... + + +
diff --git a/docs/live/blog/style.css b/docs/live/blog/style.css new file mode 100644 index 00000000..dbe5715c --- /dev/null +++ b/docs/live/blog/style.css @@ -0,0 +1,80 @@ +.blog-card { + width: 1000px; + max-width: 100%; +} + +.blog-card > .card { + /* flex is now set via Tailwind; just control width */ + width: 100%; + max-width: 100%; +} + +.blog-card [data-component-part='card-image'] { + width: 320px; + margin: 16px 0 16px 16px; + border-radius: 16px; + background-color: #f1f1f1; + overflow: hidden; +} + +.blog-full-image { + border-radius: 16px; + background-color: #f1f1f1; +} + +/* light/dark switching */ +.blog-full-image.dark-img { + display: none; +} + +.blog-full-image.light-img { + display: block; +} + +.dark .blog-full-image.dark-img { + display: block; +} + +.dark .blog-full-image.light-img { + display: none; +} + +.dark .blog-card [data-component-part='card-image'] { + background-color: #1e1e1e; +} + +.dark .blog-full-image { + background-color: #1e1e1e; +} + +.blog-card [data-component-part='card-content-container'] { + padding-top: 0.75rem; + padding-bottom: 0.75rem; +} + +.blog-card [data-component-part='card-title'] { + font-size: 24px; + font-weight: bold; + margin-bottom: 16px; +} + +.blog-card [data-component-part='card-content'] { + font-size: 14px; +} + +@media screen and (max-width: 768px) { + /* flex direction handled by Tailwind: flex-col md:flex-row */ + .blog-card [data-component-part='card-image'] { + width: calc(100% - 8px); + height: 200px; + margin: 4px 4px 0 4px; + } + + .blog-card [data-component-part='card-title'] { + font-size: 18px; + } + + .blog-card [data-component-part='card-content'] { + font-size: 12px; + } +} diff --git a/docs/docs.json b/docs/live/docs.json similarity index 70% rename from docs/docs.json rename to docs/live/docs.json index 7dd4e6e6..4af876a1 100644 --- a/docs/docs.json +++ b/docs/live/docs.json @@ -23,6 +23,16 @@ "interaction": { "drilldown": true }, + "seo": { + "indexing": "navigable", + "metatags": { + "canonical": "https://agentfront.dev", + "og:image": "/assets/logo/banner.dark.png", + "charset": "UTF-8", + "viewport": "width=device-width, initial-scale=1.0", + "keywords": "mcp, dependencyinjection, sdk, adapters, plugins, agentfront, frontmcp, framework, typescript, json-schema, zod, zod-v3, schemavalidation, openapi, apivalidation, typesafety, runtimevalidation" + } + }, "navbar": { "links": [ { @@ -54,7 +64,7 @@ "icon": "book", "versions": [ { - "version": "v0.3 (latest)", + "version": "v0.4 (latest)", "default": true, "groups": [ { @@ -97,7 +107,6 @@ "icon": "toolbox", "pages": [ "docs/servers/extensibility/providers", - "docs/servers/extensibility/adapters", "docs/servers/extensibility/plugins", "docs/servers/extensibility/logging" ] @@ -105,8 +114,12 @@ ] }, { - "group": "Deployment", - "pages": ["docs/deployment/local-dev-server", "docs/deployment/production-build"] + "group": "Adapters", + "pages": ["docs/adapters/overview", "docs/adapters/openapi-adapter"] + }, + { + "group": "Plugins", + "pages": ["docs/plugins/cache-plugin"] }, { "group": "Guides", @@ -116,9 +129,77 @@ "docs/guides/customize-flow-stages" ] }, + { + "group": "Deployment", + "pages": ["docs/deployment/local-dev-server", "docs/deployment/production-build"] + } + ] + }, + { + "version": "v0.3", + "groups": [ + { + "tag": "version 0.3", + "group": "Get Started", + "pages": [ + "docs/v/0.3/getting-started/welcome", + "docs/v/0.3/getting-started/installation", + "docs/v/0.3/getting-started/quickstart", + "updates" + ] + }, + { + "group": "Servers", + "pages": [ + "docs/v/0.3/servers/server", + { + "group": "Core Components", + "icon": "toolbox", + "pages": [ + "docs/v/0.3/servers/apps", + "docs/v/0.3/servers/tools", + "docs/v/0.3/servers/resources", + "docs/v/0.3/servers/prompts" + ] + }, + { + "group": "Authentication", + "icon": "shield-check", + "pages": [ + "docs/v/0.3/servers/authentication/overview", + "docs/v/0.3/servers/authentication/token", + "docs/v/0.3/servers/authentication/remote", + "docs/v/0.3/servers/authentication/remote-proxy", + "docs/v/0.3/servers/authentication/local" + ] + }, + { + "group": "Extensibility Components", + "icon": "toolbox", + "pages": [ + "docs/v/0.3/servers/extensibility/providers", + "docs/v/0.3/servers/extensibility/adapters", + "docs/v/0.3/servers/extensibility/plugins", + "docs/v/0.3/servers/extensibility/logging" + ] + } + ] + }, + { + "group": "Deployment", + "pages": ["docs/v/0.3/deployment/local-dev-server", "docs/v/0.3/deployment/production-build"] + }, + { + "group": "Guides", + "pages": [ + "docs/v/0.3/guides/add-openapi-adapter", + "docs/v/0.3/guides/caching-and-cache-miss", + "docs/v/0.3/guides/customize-flow-stages" + ] + }, { "group": "Adapters", - "pages": ["docs/adapters/openapi-adapter"] + "pages": ["docs/v/0.3/adapters/openapi-adapter"] } ] }, @@ -254,7 +335,11 @@ }, { "group": "November 2025", - "pages": ["blog/11-2025/mcp-run-out-of-socket", "blog/11-2025/introducing-frontmcp"] + "pages": [ + "blog/11-2025/openapi-mcp-security-nightmare", + "blog/11-2025/mcp-run-out-of-socket", + "blog/11-2025/introducing-frontmcp" + ] } ] } diff --git a/docs/live/docs/adapters/openapi-adapter.mdx b/docs/live/docs/adapters/openapi-adapter.mdx new file mode 100644 index 00000000..8f5aaa0c --- /dev/null +++ b/docs/live/docs/adapters/openapi-adapter.mdx @@ -0,0 +1,644 @@ +--- +title: OpenAPI Adapter +slug: adapters/openapi-adapter +sidebarTitle: OpenAPI +description: Generate MCP tools directly from an OpenAPI spec and call them with strong validation. +icon: puzzle-piece +--- + +The OpenAPI Adapter automatically converts OpenAPI 3.x specifications into fully-functional MCP tools. Each API operation becomes a callable tool with built-in validation, authentication, and type safety. + +## Why use it + +- **Zero boilerplate** — Turn REST APIs into MCP tools without writing glue code +- **Type-safe** — Automatic Zod schema generation from OpenAPI specs +- **Multi-auth support** — Built-in support for multiple authentication providers +- **Production-ready** — Comprehensive security validation and error handling +- **Flexible** — Filter operations, customize schemas, and inject custom logic + +## Installation + +```bash +npm install @frontmcp/adapters +``` + +## Quick start + + + +```ts Basic usage +import { App } from '@frontmcp/sdk'; +import { OpenapiAdapter } from '@frontmcp/adapters'; + +@App({ + id: 'my-api', + name: 'My API MCP Server', + adapters: [ + OpenapiAdapter.init({ + name: 'backend:api', + baseUrl: process.env.API_BASE_URL!, + url: process.env.OPENAPI_SPEC_URL!, + }), + ], +}) +export default class MyApiApp {} +``` + +```ts With authentication +import { App } from '@frontmcp/sdk'; +import { OpenapiAdapter } from '@frontmcp/adapters'; + +@App({ + id: 'my-api', + name: 'My API MCP Server', + adapters: [ + OpenapiAdapter.init({ + name: 'backend:api', + baseUrl: process.env.API_BASE_URL!, + url: process.env.OPENAPI_SPEC_URL!, + additionalHeaders: { + 'x-api-key': process.env.API_KEY!, + }, + }), + ], +}) +export default class MyApiApp {} +``` + + + +## Configuration + +### Required Options + + + Unique identifier for this adapter instance. Used to prefix tool names when multiple adapters are present. + + + + Base URL for API requests (e.g., `https://api.example.com/v1`). + + + + In-memory OpenAPI specification object. Use either `spec` or `url`, not both. + + + + URL or file path to the OpenAPI specification. Can be a local file path or remote URL. Use either `spec` or `url`, not + both. + + +### Optional Configuration + + + Static headers applied to every request. Useful for API keys or static authentication tokens. + + + + Function to dynamically set headers based on authenticated user context. Headers set here are hidden from MCP clients. + + + + Function to transform or augment the request body before sending. Useful for adding tenant IDs or user-specific data. + + + + Options for loading the OpenAPI specification (headers, timeout, etc.). See + [mcp-from-openapi](https://www.npmjs.com/package/mcp-from-openapi) for details. + + + + Options for tool generation. See [Advanced Features](#advanced-features) for details. + + +## Authentication + +The OpenAPI adapter provides multiple authentication strategies with different security risk levels. Choose the approach that best fits your use case. + +### Strategy 1: Static Headers (Medium Risk) + +Best for: Server-to-server APIs with static credentials. + +```ts +OpenapiAdapter.init({ + name: 'my-api', + url: 'https://api.example.com/openapi.json', + baseUrl: 'https://api.example.com', + additionalHeaders: { + 'x-api-key': process.env.API_KEY!, + authorization: `Bearer ${process.env.API_TOKEN}`, + }, +}); +``` + +Store credentials in environment variables or secrets manager, never hardcode them. + +### Strategy 2: Auth Provider Mapper (Low Risk) ⭐ Recommended + +Best for: Multi-provider authentication (GitHub, Slack, Google, etc.). + +This approach maps OpenAPI security scheme names to authentication extractors. Each security scheme can use a different auth provider from the authenticated user context. + +```ts +OpenapiAdapter.init({ + name: 'multi-auth-api', + url: 'https://api.example.com/openapi.json', + baseUrl: 'https://api.example.com', + authProviderMapper: { + // Map security scheme 'GitHubAuth' to GitHub token from user context + GitHubAuth: (authInfo) => authInfo.user?.githubToken, + + // Map security scheme 'SlackAuth' to Slack token from user context + SlackAuth: (authInfo) => authInfo.user?.slackToken, + + // Map security scheme 'ApiKeyAuth' to API key from user context + ApiKeyAuth: (authInfo) => authInfo.user?.apiKey, + }, +}); +``` + +**How it works:** + +1. Extracts security scheme names from OpenAPI spec (e.g., `GitHubAuth`, `SlackAuth`) +2. For each tool, looks up the required security scheme +3. Calls the corresponding extractor function to get the token from `authInfo` +4. Applies the token to the request + + + **Security Risk: LOW** — Authentication is resolved from authenticated user context, not exposed to MCP clients. + + +### Strategy 3: Custom Security Resolver (Low Risk) + +Best for: Complex authentication logic or custom security requirements. + +```ts +OpenapiAdapter.init({ + name: 'custom-auth-api', + url: 'https://api.example.com/openapi.json', + baseUrl: 'https://api.example.com', + securityResolver: (tool, authInfo) => { + // Use GitHub token for GitHub API tools + if (tool.name.startsWith('github_')) { + return { jwt: authInfo.user?.githubToken }; + } + + // Use Google token for Google API tools + if (tool.name.startsWith('google_')) { + return { jwt: authInfo.user?.googleToken }; + } + + // Use API key for admin tools + if (tool.name.startsWith('admin_')) { + return { apiKey: authInfo.user?.adminApiKey }; + } + + // Default to main JWT token + return { jwt: authInfo.token }; + }, +}); +``` + +**Security Risk: LOW** — Full control over authentication resolution from user context. + +### Strategy 4: Static Auth (Medium Risk) + +Best for: Server-to-server APIs where credentials don't change per user. + +```ts +OpenapiAdapter.init({ + name: 'backend-api', + url: 'https://api.example.com/openapi.json', + baseUrl: 'https://api.example.com', + staticAuth: { + jwt: process.env.API_JWT_TOKEN, + apiKey: process.env.API_KEY, + }, +}); +``` + +**Security Risk: MEDIUM** — Store credentials securely in environment variables or secrets manager. + +### Strategy 5: Dynamic Headers & Body Mapping (Low Risk) + +Best for: Adding user-specific data (tenant IDs, user IDs) to requests. + +```ts +OpenapiAdapter.init({ + name: 'tenant-api', + url: 'https://api.example.com/openapi.json', + baseUrl: 'https://api.example.com', + headersMapper: (authInfo, headers) => { + // Add authorization header + if (authInfo.token) { + headers.set('authorization', `Bearer ${authInfo.token}`); + } + + // Add tenant ID from user context + if (authInfo.user?.tenantId) { + headers.set('x-tenant-id', authInfo.user.tenantId); + } + + return headers; + }, + bodyMapper: (authInfo, body) => { + // Add user ID to all request bodies + return { + ...body, + createdBy: authInfo.user?.id, + tenantId: authInfo.user?.tenantId, + }; + }, +}); +``` + +**Security Risk: LOW** — User-specific data is injected server-side, hidden from MCP clients. + +### Default Behavior (Medium Risk) + +If no authentication configuration is provided, the adapter uses `authInfo.token` for all Bearer auth schemes. + +```ts +OpenapiAdapter.init({ + name: 'simple-api', + url: 'https://api.example.com/openapi.json', + baseUrl: 'https://api.example.com', + // No auth config - will use authInfo.token by default +}); +``` + + + **Security Risk: MEDIUM** — Only works for single Bearer auth. For multiple auth providers, use `authProviderMapper` + or `securityResolver`. + + +## Advanced Features + +### Filtering Operations + +Control which API operations become MCP tools. + + + +```ts Filter by path +OpenapiAdapter.init({ + name: 'billing-api', + url: 'https://api.example.com/openapi.json', + baseUrl: 'https://api.example.com', + generateOptions: { + filterFn: (op) => op.path.startsWith('/invoices') || op.path.startsWith('/customers'), + }, +}); +``` + +```ts Exclude specific operations +OpenapiAdapter.init({ + name: 'my-api', + url: 'https://api.example.com/openapi.json', + baseUrl: 'https://api.example.com', + generateOptions: { + excludeOperationIds: ['deprecatedEndpoint', 'internalOnly'], + }, +}); +``` + +```ts Include only specific operations +OpenapiAdapter.init({ + name: 'my-api', + url: 'https://api.example.com/openapi.json', + baseUrl: 'https://api.example.com', + generateOptions: { + defaultInclude: false, + filterFn: (op) => ['getUser', 'createUser', 'updateUser'].includes(op.operationId), + }, +}); +``` + + + +### Input Schema Transformation + +Customize the input schema for generated tools. + +```ts +OpenapiAdapter.init({ + name: 'my-api', + url: 'https://api.example.com/openapi.json', + baseUrl: 'https://api.example.com', + generateOptions: { + inputSchemaMapper: (schema) => { + // Remove sensitive fields from the input schema + if (schema.properties?.password) { + delete schema.properties.password; + } + + // Add custom fields + schema.properties.customField = { + type: 'string', + description: 'Custom field added by mapper', + }; + + return schema; + }, + }, +}); +``` + +### Load Options + +Configure how the OpenAPI spec is loaded. + +```ts +OpenapiAdapter.init({ + name: 'my-api', + url: 'https://api.example.com/openapi.json', + baseUrl: 'https://api.example.com', + loadOptions: { + headers: { + authorization: `Bearer ${process.env.SPEC_ACCESS_TOKEN}`, + }, + timeout: 10000, // 10 seconds + }, +}); +``` + +## How It Works + +### Request Processing + +1. **Path Parameters** — Interpolated into URL template (e.g., `/users/{id}` → `/users/123`) +2. **Query Parameters** — Validated and appended to URL +3. **Headers** — Merged from `additionalHeaders`, `headersMapper`, and security config +4. **Request Body** — Validated and transformed by `bodyMapper` (for POST/PUT/PATCH) +5. **Authentication** — Applied via selected strategy (auth provider mapper, security resolver, etc.) + +### Response Processing + +- **JSON responses** — Automatically parsed to objects +- **Text responses** — Returned as plain text +- **Error responses** — Thrown as errors with status code and message + +## Complete Examples + +### Multi-Provider OAuth Application + +```ts +import { App } from '@frontmcp/sdk'; +import { OpenapiAdapter } from '@frontmcp/adapters'; + +@App({ + id: 'multi-provider-app', + name: 'Multi-Provider Integration', + adapters: [ + // GitHub API + OpenapiAdapter.init({ + name: 'github', + url: 'https://api.github.com/openapi.json', + baseUrl: 'https://api.github.com', + authProviderMapper: { + GitHubAuth: (authInfo) => authInfo.user?.githubToken, + }, + }), + + // Slack API + OpenapiAdapter.init({ + name: 'slack', + url: 'https://api.slack.com/openapi.json', + baseUrl: 'https://api.slack.com', + authProviderMapper: { + SlackAuth: (authInfo) => authInfo.user?.slackToken, + }, + }), + ], +}) +export default class MultiProviderApp {} +``` + +### Multi-Tenant SaaS Application + +```ts +import { App } from '@frontmcp/sdk'; +import { OpenapiAdapter } from '@frontmcp/adapters'; + +@App({ + id: 'saas-app', + name: 'SaaS Platform', + adapters: [ + OpenapiAdapter.init({ + name: 'backend:api', + spec: require('./openapi.json'), + baseUrl: process.env.API_BASE_URL!, + headersMapper: (authInfo, headers) => { + // Add tenant ID to all requests + if (authInfo.user?.tenantId) { + headers.set('x-tenant-id', authInfo.user.tenantId); + } + + // Add user authorization + if (authInfo.token) { + headers.set('authorization', `Bearer ${authInfo.token}`); + } + + return headers; + }, + bodyMapper: (authInfo, body) => { + // Add user context to all mutations + return { + ...body, + tenantId: authInfo.user?.tenantId, + userId: authInfo.user?.id, + timestamp: new Date().toISOString(), + }; + }, + }), + ], +}) +export default class SaasApp {} +``` + +### Expense Management (From Demo) + +```ts +import { App } from '@frontmcp/sdk'; +import { OpenapiAdapter } from '@frontmcp/adapters'; + +@App({ + id: 'expense', + name: 'Expense MCP app', + adapters: [ + OpenapiAdapter.init({ + name: 'backend:api', + url: process.env.OPENAPI_SPEC_URL!, + baseUrl: process.env.API_BASE_URL!, + headersMapper: (authInfo, headers) => { + const token = authInfo.token; + if (token) { + headers.set('authorization', `Bearer ${token}`); + } + return headers; + }, + }), + ], +}) +export default class ExpenseMcpApp {} +``` + +## Security Best Practices + + + + For multi-provider authentication, use `authProviderMapper` to map each security scheme to the correct auth provider. This provides **LOW security risk**. + + + + Always store credentials in environment variables or secrets manager. Never commit credentials to source control. + + + + Setting `generateOptions.includeSecurityInInput: true` exposes auth fields to MCP clients (**HIGH risk**). Only use + for development/testing. + + + + Always validate that `authInfo.user` contains the expected fields before extracting tokens. Handle missing tokens gracefully. + + + +## Security Risk Levels + +The adapter automatically validates your security configuration and assigns a risk score: + +| Risk Level | Configuration | Description | +| ------------- | ---------------------------------------------- | ------------------------------------------------------- | +| **LOW** ✅ | `authProviderMapper` or `securityResolver` | Auth resolved from user context, not exposed to clients | +| **MEDIUM** ⚠️ | `staticAuth`, `additionalHeaders`, or default | Static credentials or default behavior | +| **HIGH** 🚨 | `generateOptions.includeSecurityInInput: true` | Auth fields exposed to MCP clients (not recommended) | + +## Troubleshooting + + + + **Cause:** Your OpenAPI spec defines security schemes that aren't mapped in `authProviderMapper`. + + **Solution:** Add all required security schemes to `authProviderMapper`: + + ```ts + authProviderMapper: { + 'GitHubAuth': (authInfo) => authInfo.user?.githubToken, + 'SlackAuth': (authInfo) => authInfo.user?.slackToken, + // Add all security schemes from your spec + } + ``` + + + + + **Cause:** The tool requires authentication but no auth configuration was provided. + + **Solution:** Choose one of the authentication strategies: + + 1. Add `authProviderMapper` (recommended for multi-provider) + 2. Add `securityResolver` (for custom logic) + 3. Add `staticAuth` (for server-to-server) + 4. Add `additionalHeaders` (for static API keys) + + + + + **Cause:** Tools may be filtered out by `filterFn` or `excludeOperationIds`. + + **Solution:** Check your filter configuration or remove filters to include all operations. + + + + + **Cause:** Authentication token is missing or invalid. + + **Solution:** + 1. Verify `authInfo.user` contains the expected token fields + 2. Check that token extraction returns a valid value + 3. Verify the token is not expired + 4. Check API logs for specific auth errors + + + + + **Cause:** OpenAPI spec may not be typed correctly. + + **Solution:** Cast the spec to `OpenAPIV3.Document`: + + ```ts + import { OpenAPIV3 } from 'openapi-types'; + + const spec = require('./openapi.json') as OpenAPIV3.Document; + ``` + + + + +## API Reference + +### OpenapiAdapter.init(options) + +Creates a new OpenAPI adapter instance. + +**Parameters:** + +- `options: OpenApiAdapterOptions` — Adapter configuration + +**Returns:** Adapter instance ready for use in `@App({ adapters: [...] })` + +### Security Types + +```ts +// Security context passed to mcp-from-openapi +interface SecurityContext { + jwt?: string; + apiKey?: string; + basic?: { username: string; password: string }; + oauth2Token?: string; + apiKeys?: Record; + customHeaders?: Record; +} + +// Auth info from FrontMCP +interface AuthInfo { + token?: string; + user?: { + id?: string; + email?: string; + [key: string]: any; // Custom user fields + }; +} +``` + +## Performance Tips + + + The adapter uses lazy loading — the OpenAPI spec is only loaded and tools are only generated on first use, not during + initialization. + + +Combine with app-level plugins (caching, logging, metrics) to enhance all generated tools automatically. + +Use `filterFn` to generate only the tools you need, reducing initialization time and memory usage. + +## Links & Resources + + + + See the expense management demo app using the OpenAPI adapter + + + + OpenAPI spec used in the demo application + + + + Underlying library for OpenAPI to MCP conversion + + + + View the adapter source code + + diff --git a/docs/live/docs/adapters/overview.mdx b/docs/live/docs/adapters/overview.mdx new file mode 100644 index 00000000..c3b8139d --- /dev/null +++ b/docs/live/docs/adapters/overview.mdx @@ -0,0 +1,223 @@ +--- +title: Adapters Overview +slug: adapters/overview +sidebarTitle: Overview +description: Transform external APIs into MCP tools with FrontMCP adapters +icon: circle-nodes +--- + +Adapters are powerful components that automatically convert external APIs and services into MCP tools. Instead of writing custom tools manually, adapters generate them from specifications like OpenAPI, GraphQL schemas, or database schemas. + +## Why Use Adapters? + + + + Generate hundreds of tools from a single specification without writing code for each endpoint. + + + Automatic schema validation using Zod ensures type-safe inputs and outputs. + + + All tools follow the same patterns for authentication, error handling, and validation. + + + Update your spec and regenerate tools automatically — no manual updates needed. + + + +## Official Adapters + +These adapters are maintained by the FrontMCP team and included in `@frontmcp/adapters`. + + + + Generate MCP tools from OpenAPI 3.x specifications. Perfect for REST APIs with comprehensive documentation. + + **Features:** + - Multi-provider authentication support + - Path, query, header, and body parameter handling + - Request/response validation + - Filter operations by path or operation ID + - Custom headers and body mapping + + **Best for:** REST APIs, microservices, third-party integrations + + + + +More official adapters are coming soon! GraphQL, gRPC, and database adapters are in development. + +## Community Adapters + +Community adapters are created and maintained by the FrontMCP community. While not officially supported, they extend FrontMCP's capabilities to new APIs and services. + + + Community adapters are not maintained by the FrontMCP team. Please review the code and security practices before using + them in production. + + +### How to Find Community Adapters + + + + Search for packages tagged with `frontmcp-adapter` or `mcp-adapter`: + ```bash + npm search frontmcp-adapter + ``` + + + Browse repositories tagged with [`frontmcp-adapter`](https://github.com/topics/frontmcp-adapter) on GitHub. + + + Visit the [FrontMCP Discussions](https://github.com/agentfront/frontmcp/discussions) to discover and share adapters. + + + +### Featured Community Adapters + + + This section is for community-contributed adapters. If you've created an adapter, submit a PR to add it here! + + + + + Created an adapter? Share it with the community! Submit a PR to add your adapter to this page. + + + + Use the official adapter structure as a template for building your own adapters. + + + +## Creating Custom Adapters + +You can create custom adapters to integrate any API or service with FrontMCP. Adapters are TypeScript classes that implement the adapter interface. + +### Basic Adapter Structure + +```ts +import { BaseAdapter } from '@frontmcp/sdk'; + +export class MyCustomAdapter extends BaseAdapter { + async fetch(ctx: ToolContext) { + // Initialize your adapter + // Generate tools from your API/service + // Return array of MCP tools + + return [ + { + name: 'myTool', + description: 'Description of the tool', + inputSchema: { + type: 'object', + properties: { + param1: { type: 'string' }, + }, + }, + execute: async (input) => { + // Execute the tool + return { result: 'success' }; + }, + }, + ]; + } + + static init(options: MyAdapterOptions) { + return new MyCustomAdapter(options); + } +} +``` + +### Adapter Best Practices + + + + - Never expose credentials in tool inputs + - Use `authInfo` from context for authentication + - Validate and sanitize all inputs + - Document security risk levels + - Implement proper error handling + + + - Generate Zod schemas from specifications - Validate inputs at runtime - Provide TypeScript types for all options - + Use strict type checking + + + - Use lazy loading for specifications - Cache generated tools when possible - Implement connection pooling - Handle + rate limiting gracefully + + + - Provide clear documentation - Include working examples - Support common authentication patterns - Add helpful error + messages - Write comprehensive tests + + + - Follow the official adapter structure + - Version your adapter properly + - Document breaking changes + - Provide migration guides + - Keep dependencies up to date + + + +### Publishing Your Adapter + +When publishing a community adapter: + +1. **Package Name:** Use the pattern `@yourscope/frontmcp-adapter-name` or `frontmcp-adapter-name` +2. **Keywords:** Include `frontmcp`, `frontmcp-adapter`, `mcp`, `adapter` +3. **README:** Include installation, usage, examples, and security considerations +4. **License:** Use a permissive license (MIT, Apache 2.0, etc.) +5. **Tests:** Include comprehensive test coverage +6. **TypeScript:** Provide TypeScript types and declarations + +```json package.json +{ + "name": "@yourscope/frontmcp-adapter-myapi", + "version": "1.0.0", + "description": "FrontMCP adapter for MyAPI", + "keywords": ["frontmcp", "frontmcp-adapter", "mcp", "adapter", "myapi"], + "peerDependencies": { + "@frontmcp/sdk": "^0.3.0" + } +} +``` + +## Adapter Comparison + +Choose the right adapter for your use case: + +| Adapter | Best For | Authentication | Type Safety | Complexity | +| ----------- | -------------------- | ------------------ | -------------- | ----------- | +| **OpenAPI** | REST APIs with specs | Multi-provider | Auto-generated | Low | +| **Custom** | Any API/service | Fully customizable | Manual schemas | Medium-High | + +## Next Steps + + + + Learn how to use the OpenAPI adapter + + + See adapters in action with real examples + + + Get help and share your adapters + + + +## Resources + + + + Learn about the FrontMCP SDK and adapter interfaces + + + View the official adapters source code + + + Install the official adapters package + + + Contribute to FrontMCP adapters + + diff --git a/docs/live/docs/deployment/local-dev-server.mdx b/docs/live/docs/deployment/local-dev-server.mdx new file mode 100644 index 00000000..06ebefaf --- /dev/null +++ b/docs/live/docs/deployment/local-dev-server.mdx @@ -0,0 +1,169 @@ +--- +title: Local Dev Server +slug: deployment/local-dev-server +icon: computer +--- + +Run your FrontMCP server locally with hot-reload and verify it with the **FrontMCP Inspector** (zero setup). + +## Prerequisites + +- **Node.js ≥ 22** +- **npm ≥ 10** (pnpm/yarn also supported) + +--- + +## Quick start + +### Option A — New project + +Creates a folder and scaffolds everything for you. + + + ```bash universal + npx frontmcp create my-app + ``` + + + +### Option B — Existing project + +Install and initialize in your current repo: + +```bash universal npm i -D frontmcp @types/node@^20 npx frontmcp init ``` + +`init` adds the required scripts and updates **tsconfig.json** automatically. + +--- + +## Package scripts + +After `create` or `init`, your `package.json` will include: + +```json +{ + "scripts": { + "dev": "frontmcp dev", + "build": "frontmcp build", + "inspect": "frontmcp inspector", + "doctor": "frontmcp doctor" + } +} +``` + +- `frontmcp dev` — run in watch mode with type-checks +- `frontmcp build` — compile to `./dist` (override with `--out-dir`) +- `frontmcp inspector` — launches **@modelcontextprotocol/inspector** with zero setup +- `frontmcp doctor` — verifies Node/npm versions and project configuration + +--- + +## Recommended tsconfig + +`init` writes this for you, but if you prefer to manage it manually: + +```json title="tsconfig.json" +{ + "compilerOptions": { + "target": "es2021", + "module": "esnext", + "lib": ["es2021"], + "moduleResolution": "bundler", + "rootDir": "src", + "outDir": "dist", + "strict": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "sourceMap": true + }, + "include": ["src/**/*.ts"], + "exclude": ["**/*.test.ts", "**/__tests__/**"] +} +``` + +--- + +## Minimal app + +> You can import from `@frontmcp/sdk` directly; no extra install needed. + +**src/main.ts** + +```ts +import 'reflect-metadata'; +import { FrontMcp, LogLevel } from '@frontmcp/sdk'; +import HelloApp from './hello.app'; + +@FrontMcp({ + info: { name: 'Hello MCP', version: '0.1.0' }, + apps: [HelloApp], + http: { port: Number(process.env.PORT) || 3000 }, + logging: { level: LogLevel.Info }, +}) +export default class Server {} +``` + +**src/hello.app.ts** + +```ts +import { App } from '@frontmcp/sdk'; +import Greet from './tools/greet.tool'; + +@App({ id: 'hello', name: 'Hello', tools: [Greet] }) +export default class HelloApp {} +``` + +**src/tools/greet.tool.ts** + +```ts +import { tool } from '@frontmcp/sdk'; +import { z } from 'zod'; + +export default tool({ + name: 'greet', + description: 'Greets a user by name', + // New style: pass Zod fields directly (no z.object wrapper) + inputSchema: { name: z.string() }, +})(({ name }) => `Hello, ${name}!`); +``` + +--- + +## Run the dev server + +```bash universal npm run dev ``` + +Your server will start (default `http://localhost:3000`). The console will print the MCP endpoint. + +--- + +## Inspect locally (zero setup) + +Launch the **FrontMCP Inspector** to exercise tools and messages in a friendly UI: + +```bash universal npm run inspect ``` + +- This runs `npx @modelcontextprotocol/inspector` behind the scenes. +- Point it at your local server URL printed by `dev` (e.g., `http://localhost:3000`). +- Try calling `greet` and watch responses stream back in real time. + +--- + +## Troubleshooting + +- **Check configuration** + +```bash +npm run doctor +``` + +Ensures Node/npm versions, entry detection, and `tsconfig.json` are correct. + +- **Entry detection** + The CLI looks for `package.json.main`; if missing, it falls back to `src/main.ts`. If no entry is found, it will tell you how to create one. + +- **Type errors in dev** + The `dev` command performs async type-checks while watching your files, so you’ll see issues immediately without stopping the server. diff --git a/docs/live/docs/deployment/production-build.mdx b/docs/live/docs/deployment/production-build.mdx new file mode 100644 index 00000000..54ef020e --- /dev/null +++ b/docs/live/docs/deployment/production-build.mdx @@ -0,0 +1,73 @@ +--- +title: Production Build +slug: deployment/production-build +icon: building +--- + +Build a compact Node artifact and run it behind a process manager / reverse proxy. + +## Build + + + + ```bash npm + npm run build + ``` + + ```bash yarn + yarn build + ``` + + ```bash pnpm + pnpm build + ``` + + + +This compiles TypeScript to `dist/` using `tsconfig.build.json`. + +## Start + + + + ```bash npm + NODE_ENV=production PORT=8080 npm start + ``` + + ```bash yarn + NODE_ENV=production PORT=8080 yarn start + ``` + + ```bash pnpm + NODE_ENV=production PORT=8080 pnpm start + ``` + + + +## Recommended runtime setup + +- Use a **process manager** (PM2, systemd) for restarts and logs. +- Put a **reverse proxy** (NGINX, Traefik, Caddy) in front for TLS and path routing. +- Pin matching versions of all `@frontmcp/*` packages. + +### Example NGINX snippet + +```nginx +server { + listen 443 ssl; + server_name mcp.example.com; + + location /mcp/ { + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto https; + proxy_pass http://127.0.0.1:8080/mcp/; + } +} +``` + +## Troubleshooting + +- **Version mismatch** at boot → align all `@frontmcp/*` versions and reinstall. +- **No decorators** working → ensure `experimentalDecorators` + `emitDecoratorMetadata` and `import 'reflect-metadata'` at the top of `main.ts`. +- **Port conflicts** → set `http.port` in `@FrontMcp` or use `PORT` env. diff --git a/docs/docs/getting-started/installation.mdx b/docs/live/docs/getting-started/installation.mdx similarity index 100% rename from docs/docs/getting-started/installation.mdx rename to docs/live/docs/getting-started/installation.mdx diff --git a/docs/live/docs/getting-started/quickstart.mdx b/docs/live/docs/getting-started/quickstart.mdx new file mode 100644 index 00000000..dd8938d9 --- /dev/null +++ b/docs/live/docs/getting-started/quickstart.mdx @@ -0,0 +1,435 @@ +--- +title: Quickstart +slug: getting-started/quickstart +icon: rocket-launch +description: Build your first MCP server with FrontMCP in under 5 minutes +--- + +Build and run your first MCP server with FrontMCP. This quickstart gets you from zero to a working server in under 5 minutes. + +## Prerequisites + +- **Node.js ≥ 22** +- **npm ≥ 10** (or pnpm/yarn) + +--- + +## Option 1: Quick Start (Recommended) + +Create a new project with the FrontMCP CLI: + + + +```bash npm +npx frontmcp create my-mcp-server +cd my-mcp-server +npm run dev +``` + +```bash pnpm +pnpm dlx frontmcp create my-mcp-server +cd my-mcp-server +pnpm dev +``` + +```bash yarn +npx frontmcp create my-mcp-server +cd my-mcp-server +yarn dev +``` + + + +The CLI creates a complete project structure with: + +- ✅ TypeScript configured +- ✅ Sample server, app, and tool +- ✅ Development scripts ready +- ✅ Hot-reload enabled + +Your server is now running at `http://localhost:3000`! + +--- + +## Option 2: Add to Existing Project + +If you already have a Node.js project, install FrontMCP: + + + +```bash npm +npm install -D frontmcp @types/node@^20 +npx frontmcp init +``` + +```bash pnpm +pnpm add -D frontmcp @types/node@^20 +pnpm dlx frontmcp init +``` + +```bash yarn +yarn add -D frontmcp @types/node@^20 +npx frontmcp init +``` + + + +The `init` command: + +- Adds FrontMCP scripts to `package.json` +- Updates `tsconfig.json` with required settings +- Creates a minimal server if none exists + +--- + +## Project Structure + +After creating your project, you'll have: + +``` +my-mcp-server/ +├── src/ +│ ├── main.ts # Server entry point +│ ├── hello.app.ts # App definition +│ └── tools/ +│ └── greet.tool.ts # Sample tool +├── package.json +└── tsconfig.json +``` + +### Server Configuration + +```ts src/main.ts +import 'reflect-metadata'; +import { FrontMcp, LogLevel } from '@frontmcp/sdk'; +import HelloApp from './hello.app'; + +@FrontMcp({ + info: { name: 'Hello MCP', version: '0.1.0' }, + apps: [HelloApp], + http: { port: 3000 }, + logging: { level: LogLevel.INFO }, +}) +export default class Server {} +``` + + + The `@FrontMcp` decorator configures your server. It requires `info`, `apps`, and optional `http` and `logging` + settings. + + +### App Definition + +```ts src/hello.app.ts +import { App } from '@frontmcp/sdk'; +import GreetTool from './tools/greet.tool'; + +@App({ + id: 'hello', + name: 'Hello App', + tools: [GreetTool], +}) +export default class HelloApp {} +``` + + + Apps organize related tools, plugins, and providers. Each app can have its own authentication and configuration. + + +### Your First Tool + + + + ```ts src/tools/greet.tool.ts + import { Tool, ToolContext } from '@frontmcp/sdk'; + import { z } from 'zod'; + + @Tool({ + name: 'greet', + description: 'Greets a user by name', + inputSchema: { name: z.string() }, + }) + export default class GreetTool extends ToolContext { + async execute(input: { name: string }) { + return `Hello, ${input.name}!`; + } + } + ``` + + + + + ```ts src/tools/greet.tool.ts + import { tool } from '@frontmcp/sdk'; + import { z } from 'zod'; + + export default tool({ + name: 'greet', + description: 'Greets a user by name', + inputSchema: { name: z.string() }, + })(async (input) => { + return `Hello, ${input.name}!`; + }); + ``` + + + + + + Use class-based tools when you need access to providers, logging, or auth context. Use function-based tools for simple + stateless operations. + + +--- + +## Available Commands + +Your project includes these scripts: + + + +```bash Development +npm run dev +# Starts server with hot-reload and type-checking +``` + +```bash Build +npm run build +# Compiles TypeScript to ./dist +``` + +```bash Production +npm run start +# Runs the compiled server +``` + +```bash Inspector +npm run inspect +# Opens the MCP Inspector UI +``` + +```bash Doctor +npm run doctor +# Verifies Node/npm versions and config +``` + + + +--- + +## Test Your Server + + + + ```bash + npm run dev + ``` + + You should see: + ``` + [INFO] Server listening on http://localhost:3000 + [INFO] Registered 1 tool: greet + ``` + + + + + Open a new terminal and run: + + ```bash + npm run inspect + ``` + + This opens the MCP Inspector at `http://localhost:6274` + + + + + In the Inspector: 1. Enter server URL: `http://localhost:3000` 2. Click "Connect" 3. You should see your `greet` tool + listed + + + + 1. Select the `greet` tool + 2. Enter input: `{ "name": "Ada" }` + 3. Click "Call Tool" + 4. You should see: `"Hello, Ada!"` + + + +Congratulations! You've built and tested your first MCP server! 🎉 + +--- + +## What's Next? + + + + Learn how to create more powerful tools with validation, providers, and context + + + + Auto-generate tools from your REST API's OpenAPI spec + + + + Improve performance with transparent caching + + + + Secure your server with OAuth (local or remote) + + + + Add cross-cutting features like logging, metrics, and rate limiting + + + + Build and deploy your server to production + + + +--- + +## Common Commands Reference + +| Command | Description | +| ---------------------------- | -------------------------------- | +| `npx frontmcp create ` | Create a new FrontMCP project | +| `npx frontmcp init` | Add FrontMCP to existing project | +| `npm run dev` | Start with hot-reload | +| `npm run build` | Compile to production | +| `npm run inspect` | Open MCP Inspector | +| `npm run doctor` | Verify setup | + +--- + +## Troubleshooting + + + + **Check:** + 1. Node.js version ≥ 22: `node --version` + 2. Port 3000 is available + 3. No TypeScript errors: Check console output + + **Fix:** + ```bash + npm run doctor # Verify configuration + ``` + + + + + **Possible causes:** + - Tool not imported in app + - Decorator metadata not enabled + + **Fix:** + 1. Verify `import GreetTool from './tools/greet.tool'` + 2. Check `tsconfig.json` has: + ```json + { + "experimentalDecorators": true, + "emitDecoratorMetadata": true + } + ``` + 3. Ensure `import 'reflect-metadata'` at top of `main.ts` + + + + + **Solution:** + The `dev` command performs async type-checks. Fix TypeScript errors shown in the console. + + ```bash + # Manual type check + npx tsc --noEmit + ``` + + + + + **Check:** + 1. Server is running: `http://localhost:3000` should be accessible + 2. Correct URL in Inspector: `http://localhost:3000` (not `https`) + 3. No CORS issues: Both server and inspector on localhost + + **Debug:** + ```bash + # Test server directly + curl http://localhost:3000/health + ``` + + + + +--- + +## Example: Extended Greeting Tool + +Here's a more advanced version with multiple features: + +```ts +import { Tool, ToolContext } from '@frontmcp/sdk'; +import { z } from 'zod'; + +@Tool({ + name: 'greet', + description: 'Greets a user with optional formality level', + inputSchema: { + name: z.string().min(1, 'Name is required'), + formality: z.enum(['casual', 'formal', 'enthusiastic']).default('casual'), + timeOfDay: z.enum(['morning', 'afternoon', 'evening']).optional(), + }, +}) +export default class GreetTool extends ToolContext { + async execute(input: { + name: string; + formality: 'casual' | 'formal' | 'enthusiastic'; + timeOfDay?: 'morning' | 'afternoon' | 'evening'; + }) { + // Access logger + this.logger.info('Greeting user', { name: input.name }); + + // Access auth info + const userId = this.authInfo.user?.id; + + // Build greeting based on formality + let greeting = ''; + switch (input.formality) { + case 'formal': + greeting = `Good ${input.timeOfDay || 'day'}, ${input.name}.`; + break; + case 'enthusiastic': + greeting = `Hey ${input.name}! Great to see you! 🎉`; + break; + case 'casual': + default: + greeting = `Hello, ${input.name}!`; + } + + return { + message: greeting, + timestamp: new Date().toISOString(), + userId, + }; + } +} +``` + +This example demonstrates: + +- ✅ Input validation with Zod +- ✅ Default values +- ✅ Optional fields +- ✅ Accessing logger +- ✅ Accessing auth context +- ✅ Structured output + +--- + + + FrontMCP speaks **MCP Streamable HTTP**. Any MCP-capable client (Claude Desktop, custom agents, etc.) can connect and + call your tools! + diff --git a/docs/live/docs/getting-started/welcome.mdx b/docs/live/docs/getting-started/welcome.mdx new file mode 100644 index 00000000..717d1f40 --- /dev/null +++ b/docs/live/docs/getting-started/welcome.mdx @@ -0,0 +1,68 @@ +--- +title: Welcome to FrontMCP +sidebarTitle: Welcome to FrontMCP +slug: getting-started/welcome +icon: hand-wave +description: The TypeScript way to build MCP servers with decorators, DI, and Streamable HTTP. +--- + +**FrontMCP is the TypeScript-first framework for MCP.** You write clean, typed code; FrontMCP handles the protocol, transport, and execution flow. + +```ts +import { FrontMcp, App, Tool, ToolContext } from '@frontmcp/sdk'; +import { z } from 'zod'; + +@Tool({ + name: 'add', + description: 'Add two numbers', + inputSchema: { a: z.number(), b: z.number() }, + outputSchema: { result: z.number() }, +}) +export default class AddTool extends ToolContext { + async execute(input: { a: number; b: number }) { + return { + result: input.a + input.b, + }; + } +} + +@App({ + id: 'calc', + name: 'Calculator', + tools: [AddTool], +}) +class CalcApp {} + +@FrontMcp({ + info: { name: 'Demo 🚀', version: '0.1.0' }, + apps: [CalcApp], + auth: { + type: 'remote', + name: 'my-oauth', + baseUrl: 'https://oauth.example.com', + }, +}) +export default class Server {} +``` + +### Why FrontMCP? + +- 🧑‍💻 **TypeScript-native DX** — decorators, Zod, and strong typing end-to-end +- 🧠 **Scoped invoker + DI** — secure, composable execution with hooks +- 🧩 **Adapters & Plugins** — extend your server without boilerplate +- 🔌 **Spec-aligned transport** — Streamable HTTP for modern MCP clients + +### Core concepts + +- **Server** — entry point via `@FrontMcp({...})` +- **App** — a logical bundle of tools via `@App({...})` +- **Tool** — typed units of work via `@Tool({...})` +- **Hooks** — cross-cutting behaviors (auth, logging, rate limits) +- **Adapters/Plugins** — load tools dynamically; enrich behavior + + + FrontMCP follows MCP protocol principles and **secured transport requirements**. You get sessions, streaming, and + validation out of the box—no custom wiring needed. + + +**Build something real**: jump to the [Quickstart](/docs/getting-started/quickstart) or set up your workspace in [Installation](/docs/getting-started/installation). diff --git a/docs/live/docs/guides/add-openapi-adapter.mdx b/docs/live/docs/guides/add-openapi-adapter.mdx new file mode 100644 index 00000000..fc2a2aae --- /dev/null +++ b/docs/live/docs/guides/add-openapi-adapter.mdx @@ -0,0 +1,528 @@ +--- +title: Add OpenAPI Adapter +slug: guides/add-openapi-adapter +description: Generate MCP tools automatically from an OpenAPI specification +icon: file-code +--- + +The OpenAPI Adapter automatically converts REST API endpoints defined in an OpenAPI 3.x specification into fully-functional MCP tools. This guide walks you through adding the adapter to your app. + +## What You'll Build + +By the end of this guide, you'll have: + +- ✅ An app that automatically generates tools from an OpenAPI spec +- ✅ Type-safe input validation for all API endpoints +- ✅ Authentication configured for API requests +- ✅ Tools that inherit all your app-level plugins and providers + + + The OpenAPI Adapter is perfect for quickly exposing REST APIs to AI agents without writing custom tool code for each + endpoint. + + +--- + +## Prerequisites + +- A FrontMCP project initialized ([see Installation](/docs/getting-started/installation)) +- An OpenAPI 3.x specification (URL or local file) +- Basic understanding of REST APIs + +--- + +## Step 1: Install the Adapter + + + +```bash npm +npm install @frontmcp/adapters +``` + +```bash pnpm +pnpm add @frontmcp/adapters +``` + +```bash yarn +yarn add @frontmcp/adapters +``` + + + +--- + +## Step 2: Add Adapter to Your App + +Create or update your app to include the OpenAPI adapter: + + + +```ts Basic usage +import { App } from '@frontmcp/sdk'; +import { OpenapiAdapter } from '@frontmcp/adapters'; + +@App({ + id: 'expense', + name: 'Expense MCP App', + adapters: [ + OpenapiAdapter.init({ + name: 'expense-api', + baseUrl: 'https://api.example.com', + url: 'https://api.example.com/openapi.json', + }), + ], +}) +export default class ExpenseApp {} +``` + +```ts Local spec file +import { App } from '@frontmcp/sdk'; +import { OpenapiAdapter } from '@frontmcp/adapters'; +import spec from './openapi.json'; + +@App({ + id: 'expense', + name: 'Expense MCP App', + adapters: [ + OpenapiAdapter.init({ + name: 'expense-api', + baseUrl: 'https://api.example.com', + spec: spec as any, // Use local spec instead of URL + }), + ], +}) +export default class ExpenseApp {} +``` + +```ts With authentication +import { App } from '@frontmcp/sdk'; +import { OpenapiAdapter } from '@frontmcp/adapters'; + +@App({ + id: 'expense', + name: 'Expense MCP App', + adapters: [ + OpenapiAdapter.init({ + name: 'expense-api', + baseUrl: 'https://api.example.com', + url: 'https://api.example.com/openapi.json', + additionalHeaders: { + 'x-api-key': process.env.API_KEY!, + }, + }), + ], +}) +export default class ExpenseApp {} +``` + + + +--- + +## Step 3: Configure Your Server + +Add your app to the FrontMCP server: + +```ts src/main.ts +import { FrontMcp, LogLevel } from '@frontmcp/sdk'; +import ExpenseApp from './apps/expense.app'; + +@FrontMcp({ + info: { name: 'Expense Server', version: '1.0.0' }, + apps: [ExpenseApp], + http: { port: 3000 }, + logging: { level: LogLevel.INFO }, +}) +export default class Server {} +``` + +--- + +## Step 4: Run and Test + + + + ```bash + npm run dev + ``` + + The server will load the OpenAPI spec and generate tools for each operation. + + + + + Check the console output for messages like: ``` [INFO] Generated 15 tools from expense-api [INFO] Server listening on + http://localhost:3000 ``` + + + + ```bash + npm run inspect + ``` + + Open the MCP Inspector and you'll see all generated tools from your API spec! + + + + +--- + +## Understanding Generated Tools + +Each OpenAPI operation becomes a tool with: + +### Tool Naming + +Tools are named using the pattern: `{adapter-name}:{method}_{path}` + +For example: + +- `GET /users` → `expense-api:get_users` +- `POST /expenses` → `expense-api:post_expenses` +- `GET /expenses/{id}` → `expense-api:get_expenses_id` + +If your OpenAPI spec includes `operationId`, that will be used instead of the generated name. + +### Input Schema + +The adapter automatically converts OpenAPI parameters to Zod schemas: + +```yaml OpenAPI spec +paths: + /expenses: + post: + parameters: + - name: category + in: query + schema: + type: string + requestBody: + content: + application/json: + schema: + type: object + properties: + amount: + type: number + description: + type: string +``` + +Becomes a tool with this input: + +```ts +{ + category: string, // from query parameter + amount: number, // from request body + description: string // from request body +} +``` + +--- + +## Advanced Configuration + +### Filter Operations + +Only include specific endpoints: + +```ts +OpenapiAdapter.init({ + name: 'billing-api', + baseUrl: 'https://api.example.com', + url: 'https://api.example.com/openapi.json', + generateOptions: { + // Only include operations starting with /invoices or /customers + filterFn: (op) => op.path.startsWith('/invoices') || op.path.startsWith('/customers'), + }, +}); +``` + +### User-Based Authentication + +Use authenticated user context for API requests: + +```ts +OpenapiAdapter.init({ + name: 'user-api', + baseUrl: 'https://api.example.com', + url: 'https://api.example.com/openapi.json', + headersMapper: (authInfo, headers) => { + // Add user's token to API requests + if (authInfo.token) { + headers.set('authorization', `Bearer ${authInfo.token}`); + } + return headers; + }, +}); +``` + +### Multi-Tenant Setup + +Include tenant ID from user context: + +```ts +OpenapiAdapter.init({ + name: 'tenant-api', + baseUrl: 'https://api.example.com', + url: 'https://api.example.com/openapi.json', + headersMapper: (authInfo, headers) => { + // Add tenant ID header + if (authInfo.user?.tenantId) { + headers.set('x-tenant-id', authInfo.user.tenantId); + } + return headers; + }, + bodyMapper: (authInfo, body) => { + // Add tenant ID to all request bodies + return { + ...body, + tenantId: authInfo.user?.tenantId, + }; + }, +}); +``` + +--- + +## How It Works + + + + The adapter fetches and parses the OpenAPI specification from the URL or uses the provided spec object + + + + Each operation in the spec becomes an MCP tool with: - Automatic input schema (path params, query params, headers, + body) - Type-safe validation using Zod - Description from the operation summary + + + + Generated tools are registered with your app and inherit: - App-level plugins (caching, logging, etc.) - App-level + providers - App-level authentication + + + + When a tool is called: + 1. Input is validated against the schema + 2. Headers/body are mapped (if configured) + 3. HTTP request is made to the API + 4. Response is parsed and returned + + + +--- + +## Common Patterns + + + + Add multiple adapters to one app: + + ```ts + @App({ + id: 'integrations', + name: 'Third-Party Integrations', + adapters: [ + OpenapiAdapter.init({ + name: 'github', + baseUrl: 'https://api.github.com', + url: 'https://api.github.com/openapi.json', + }), + OpenapiAdapter.init({ + name: 'slack', + baseUrl: 'https://api.slack.com', + url: 'https://api.slack.com/openapi.json', + }), + ], + }) + ``` + + + + + Mix generated and hand-written tools: + + ```ts + import CustomTool from './tools/custom.tool'; + + @App({ + id: 'hybrid', + name: 'Hybrid App', + tools: [CustomTool], // Hand-written + adapters: [ + OpenapiAdapter.init({...}), // Auto-generated + ], + }) + ``` + + + + + Generated tools inherit app plugins: + + ```ts + import CachePlugin from '@frontmcp/plugins/cache'; + + @App({ + id: 'cached-api', + name: 'Cached API', + plugins: [ + CachePlugin.init({ + type: 'redis', + defaultTTL: 300, + }), + ], + adapters: [ + OpenapiAdapter.init({...}), + ], + }) + ``` + + Now all generated tools can use caching! + + + + +--- + +## Troubleshooting + + + + **Possible causes:** + - Invalid OpenAPI spec URL + - Spec is OpenAPI 2.0 (only 3.x supported) + - All operations filtered out by `filterFn` + + **Solutions:** + - Verify the URL is accessible + - Convert OpenAPI 2.0 to 3.x using [Swagger Editor](https://editor.swagger.io/) + - Check your filter configuration + + + + + **Possible causes:** + - Missing or invalid API credentials + - Headers not properly mapped + + **Solutions:** + - Verify `additionalHeaders` or `headersMapper` configuration + - Check that `authInfo.token` contains the expected value + - Test the API directly with curl/Postman first + + + + + **Possible cause:** + - OpenAPI spec has complex parameter definitions + + **Solution:** + - Use `inputSchemaMapper` to transform the schema: + + ```ts + generateOptions: { + inputSchemaMapper: (schema) => { + // Remove or modify fields + delete schema.properties.internalField; + return schema; + }, + } + ``` + + + + +--- + +## What's Next? + + + + Explore all configuration options and advanced features + + + + Learn how to configure authentication for your APIs + + + + Add caching, logging, and other features to generated tools + + + + See a complete example using the OpenAPI adapter + + + +--- + +## Complete Example + +Here's a full working example with authentication and caching: + +```ts +import { FrontMcp, App, LogLevel } from '@frontmcp/sdk'; +import { OpenapiAdapter } from '@frontmcp/adapters'; +import CachePlugin from '@frontmcp/plugins/cache'; + +@App({ + id: 'expense', + name: 'Expense Management', + plugins: [ + CachePlugin.init({ + type: 'redis', + defaultTTL: 300, // 5 minutes + config: { + host: 'localhost', + port: 6379, + }, + }), + ], + adapters: [ + OpenapiAdapter.init({ + name: 'expense-api', + baseUrl: process.env.API_BASE_URL!, + url: process.env.OPENAPI_SPEC_URL!, + headersMapper: (authInfo, headers) => { + // Add user's JWT token + if (authInfo.token) { + headers.set('authorization', `Bearer ${authInfo.token}`); + } + // Add tenant ID + if (authInfo.user?.tenantId) { + headers.set('x-tenant-id', authInfo.user.tenantId); + } + return headers; + }, + bodyMapper: (authInfo, body) => { + // Add user context to mutations + return { + ...body, + createdBy: authInfo.user?.id, + tenantId: authInfo.user?.tenantId, + }; + }, + generateOptions: { + // Only expose expense-related endpoints + filterFn: (op) => op.path.startsWith('/expenses'), + }, + }), + ], +}) +class ExpenseApp {} + +@FrontMcp({ + info: { name: 'Expense Server', version: '1.0.0' }, + apps: [ExpenseApp], + http: { port: 3000 }, + logging: { level: LogLevel.INFO }, + auth: { + type: 'remote', + name: 'auth-provider', + baseUrl: process.env.AUTH_BASE_URL!, + }, +}) +export default class Server {} +``` diff --git a/docs/live/docs/guides/caching-and-cache-miss.mdx b/docs/live/docs/guides/caching-and-cache-miss.mdx new file mode 100644 index 00000000..67b339a2 --- /dev/null +++ b/docs/live/docs/guides/caching-and-cache-miss.mdx @@ -0,0 +1,702 @@ +--- +title: Caching & Cache Miss +slug: guides/caching-and-cache-miss +description: Add transparent response caching to tools with configurable TTL and storage options +icon: database +--- + +The Cache Plugin provides transparent response caching for tools, dramatically improving performance by avoiding redundant computations and API calls. This guide shows you how to add caching to your FrontMCP tools. + +## What You'll Learn + +By the end of this guide, you'll know how to: + +- ✅ Enable caching for specific tools +- ✅ Configure TTL (time-to-live) per tool +- ✅ Use sliding windows to keep hot data cached +- ✅ Switch between memory and Redis storage +- ✅ Handle cache misses and invalidation + + + Caching is perfect for tools that make expensive computations, database queries, or third-party API calls with + deterministic outputs. + + +--- + +## Prerequisites + +- A FrontMCP project with at least one app and tool +- Understanding of tool execution flow +- (Optional) Redis server for production caching + +--- + +## Step 1: Install the Cache Plugin + +```bash +npm install @frontmcp/plugins +``` + +--- + +## Step 2: Add Plugin to Your App + + + +```ts Simple (in-memory) +import { App } from '@frontmcp/sdk'; +import CachePlugin from '@frontmcp/plugins/cache'; + +@App({ + id: 'my-app', + name: 'My App', + plugins: [CachePlugin], // Default: memory store, 1-day TTL + tools: [ + /* your tools */ + ], +}) +export default class MyApp {} +``` + +```ts Custom TTL (in-memory) +import { App } from '@frontmcp/sdk'; +import CachePlugin from '@frontmcp/plugins/cache'; + +@App({ + id: 'my-app', + name: 'My App', + plugins: [ + CachePlugin.init({ + type: 'memory', + defaultTTL: 300, // 5 minutes + }), + ], + tools: [ + /* your tools */ + ], +}) +export default class MyApp {} +``` + +```ts Redis (production) +import { App } from '@frontmcp/sdk'; +import CachePlugin from '@frontmcp/plugins/cache'; + +@App({ + id: 'my-app', + name: 'My App', + plugins: [ + CachePlugin.init({ + type: 'redis', + defaultTTL: 600, // 10 minutes + config: { + host: 'localhost', + port: 6379, + password: process.env.REDIS_PASSWORD, + }, + }), + ], + tools: [ + /* your tools */ + ], +}) +export default class MyApp {} +``` + + + +--- + +## Step 3: Enable Caching on Tools + +Caching is **opt-in** per tool. Add the `cache` field to your tool metadata: + + + + ```ts + import { Tool, ToolContext } from '@frontmcp/sdk'; + import { z } from 'zod'; + + @Tool({ + name: 'get-user-profile', + description: 'Fetch user profile from database', + inputSchema: { userId: z.string() }, + cache: true, // Enable caching with plugin defaults + }) + export default class GetUserProfileTool extends ToolContext { + async execute(input: { userId: string }) { + // Expensive database query + return await this.database.getUserProfile(input.userId); + } + } + ``` + + + + + ```ts + import { tool } from '@frontmcp/sdk'; + import { z } from 'zod'; + + export const GetUserProfile = tool({ + name: 'get-user-profile', + description: 'Fetch user profile from database', + inputSchema: { userId: z.string() }, + cache: true, // Enable caching with plugin defaults + })(async (input) => { + // Expensive database query + return await database.getUserProfile(input.userId); + }); + ``` + + + + + ```ts + @Tool({ + name: 'get-report', + description: 'Generate monthly report', + inputSchema: { + month: z.string(), + year: z.number(), + }, + cache: { + ttl: 1800, // Cache for 30 minutes + }, + }) + export default class GetReportTool extends ToolContext { + async execute(input: { month: string; year: number }) { + // Expensive report generation + return await this.reportService.generate(input.month, input.year); + } + } + ``` + + + + ```ts + @Tool({ + name: 'get-popular-items', + description: 'Get trending items', + inputSchema: { category: z.string() }, + cache: { + ttl: 120, // 2 minutes + slideWindow: true, // Refresh TTL on each read + }, + }) + export default class GetPopularItemsTool extends ToolContext { + async execute(input: { category: string }) { + // Hot data that's frequently accessed + return await this.analytics.getPopularItems(input.category); + } + } + ``` + + + +--- + +## Step 4: Test Cache Behavior + + + + ```bash + npm run dev + ``` + + + + Use the MCP Inspector or a client to call your cached tool twice with the same input: + + ```json + // First call + { "userId": "user-123" } + ``` + + The first call executes the tool normally (cache miss). + + + + + The second call returns instantly from cache! Check your logs for: ``` [DEBUG] Cache hit for get-user-profile ``` + + + + Wait for the TTL to expire, then call again. The cache will miss and the tool will execute. + + + +--- + +## How Caching Works + + + + When a tool is called, the plugin creates a deterministic hash from: + - Tool name (e.g., `get-user-profile`) + - Validated input (e.g., `{ userId: "user-123" }`) + + Same input = Same cache key + + + + + The plugin checks the cache store for the key: - **Cache Hit**: Return cached result immediately, skip execution - + **Cache Miss**: Allow tool to execute normally + + + + If the tool executed, the plugin stores the result in the cache with the configured TTL + + + + If `slideWindow: true`, each cache read refreshes the TTL, keeping popular data cached longer + + + +The cache operates at the **hook level**, so it works transparently without modifying your tool code. + +--- + +## Configuration Options + +### Tool-Level Cache Options + + + Enable caching for this tool + +- `true` - Use plugin's default TTL +- `{ ttl, slideWindow }` - Custom configuration + + + + + Time-to-live in seconds. Overrides plugin's `defaultTTL`. + +**Examples:** + +- `60` - 1 minute +- `300` - 5 minutes +- `3600` - 1 hour +- `86400` - 1 day + + + + + When `true`, reading from cache refreshes the TTL + **Use cases:** + +- Trending/popular data +- Frequently accessed reports +- User dashboards + + + +--- + +## Common Patterns + + + + For data that changes frequently: + + ```ts + @Tool({ + name: 'get-stock-price', + inputSchema: { symbol: z.string() }, + cache: { + ttl: 5, // Only 5 seconds + }, + }) + class GetStockPriceTool extends ToolContext { + async execute(input: { symbol: string }) { + return await this.marketData.getPrice(input.symbol); + } + } + ``` + + + + + For computationally expensive operations: + + ```ts + @Tool({ + name: 'generate-annual-report', + inputSchema: { + year: z.number(), + department: z.string(), + }, + cache: { + ttl: 86400, // 24 hours + }, + }) + class GenerateAnnualReportTool extends ToolContext { + async execute(input) { + // Very expensive computation + return await this.reports.generateAnnual(input.year, input.department); + } + } + ``` + + + + + For frequently accessed data: + + ```ts + @Tool({ + name: 'get-user-dashboard', + inputSchema: { userId: z.string() }, + cache: { + ttl: 300, // 5 minutes + slideWindow: true, // Keep hot dashboards cached + }, + }) + class GetUserDashboardTool extends ToolContext { + async execute(input: { userId: string }) { + return await this.dashboard.generate(input.userId); + } + } + ``` + + + + + Include tenant ID in input for automatic isolation: + + ```ts + @Tool({ + name: 'get-tenant-data', + inputSchema: { + tenantId: z.string(), // Automatically part of cache key + dataType: z.string(), + }, + cache: { ttl: 600 }, + }) + class GetTenantDataTool extends ToolContext { + async execute(input) { + return await this.tenantService.getData( + input.tenantId, + input.dataType + ); + } + } + ``` + + Each tenant's data is cached separately! + + + + +--- + +## Memory vs Redis + +### When to Use Memory Cache + + + + Perfect for local development and testing + + + + When running one server instance + + + + Data loss on restart is acceptable + + + + No external dependencies needed + + + +Memory cache resets when the server restarts. Not shared across multiple instances. + +### When to Use Redis + + + + Recommended for production deployments + + + + Cache shared across multiple server instances + + + + Cache survives server restarts + + + + Redis handles memory limits gracefully + + + +Redis provides persistence, sharing, and better memory management for production use. + +--- + +## Troubleshooting + + + + **Checklist:** + 1. Tool has `cache: true` or `cache: { ... }` in metadata + 2. Plugin is registered in app's `plugins` array + 3. Redis is running (if using Redis backend) + 4. No errors in server logs + + **Debug:** + ```ts + logging: { + level: LogLevel.DEBUG, // See cache hit/miss logs + } + ``` + + + + + **Problem:** Cache TTL is too long for your data freshness requirements. + + **Solution:** Reduce the TTL: + ```ts + cache: { + ttl: 60, // Shorter TTL = fresher data + } + ``` + + + + + **Problem:** Using memory cache with multiple server instances. + + **Solution:** Switch to Redis: + ```ts + CachePlugin.init({ + type: 'redis', + config: { host: 'localhost', port: 6379 }, + }) + ``` + + + + + **Problem:** Tool output varies even with same input (e.g., returns current timestamp). + + **Solution:** Don't cache non-deterministic tools: + ```ts + @Tool({ + name: 'get-current-time', + // No cache field - don't cache this! + }) + ``` + + + + +--- + +## Best Practices + + + + Cache tools where the same input produces the same output: + + ✅ **Good candidates:** + - Database queries by ID + - API calls with stable responses + - Report generation + - Static data lookup + + ❌ **Bad candidates:** + - Tools that return current time/date + - Tools with random output + - Tools with side effects (mutations) + + + + + Match TTL to data change frequency: + + | Data Type | Suggested TTL | + |-----------|---------------| + | Real-time prices | 5-10 seconds | + | User profiles | 5-15 minutes | + | Reports | 30 minutes - 1 hour | + | Static content | Hours to days | + + + + + Always include tenant/user IDs in inputs: + + ```ts + // Good: Automatic tenant isolation + inputSchema: { + tenantId: z.string(), + userId: z.string(), + reportId: z.string(), + } + + // Bad: Shared across tenants + inputSchema: { + reportId: z.string(), + } + ``` + + + + + Redis provides: + - Persistence across restarts + - Sharing across instances + - Better memory management + - Monitoring and debugging tools + + ```ts + // Production config + CachePlugin.init({ + type: 'redis', + defaultTTL: 600, + config: { + host: process.env.REDIS_HOST, + port: parseInt(process.env.REDIS_PORT || '6379'), + password: process.env.REDIS_PASSWORD, + }, + }) + ``` + + + + + Enable debug logging to see cache hits/misses: + + ```ts + logging: { + level: LogLevel.DEBUG, + } + ``` + + Look for: + - High miss rates (TTL too short? Tool not deterministic?) + - Memory growth (TTL too long?) + + + + +--- + +## Complete Example + +Here's a full example with multiple tools using different caching strategies: + +```ts +import { FrontMcp, App, Tool, ToolContext } from '@frontmcp/sdk'; +import CachePlugin from '@frontmcp/plugins/cache'; +import { z } from 'zod'; + +// Real-time data: short TTL +@Tool({ + name: 'get-stock-price', + inputSchema: { symbol: z.string() }, + cache: { ttl: 10 }, // 10 seconds +}) +class GetStockPriceTool extends ToolContext { + async execute(input: { symbol: string }) { + return await this.marketData.getPrice(input.symbol); + } +} + +// User data: medium TTL +@Tool({ + name: 'get-user', + inputSchema: { + tenantId: z.string(), + userId: z.string(), + }, + cache: { ttl: 300 }, // 5 minutes +}) +class GetUserTool extends ToolContext { + async execute(input) { + return await this.database.getUser(input.tenantId, input.userId); + } +} + +// Popular content: sliding window +@Tool({ + name: 'get-trending', + inputSchema: { category: z.string() }, + cache: { + ttl: 120, // 2 minutes + slideWindow: true, // Keep hot data cached + }, +}) +class GetTrendingTool extends ToolContext { + async execute(input: { category: string }) { + return await this.analytics.getTrending(input.category); + } +} + +// Expensive reports: long TTL +@Tool({ + name: 'generate-report', + inputSchema: { + tenantId: z.string(), + month: z.string(), + }, + cache: { ttl: 3600 }, // 1 hour +}) +class GenerateReportTool extends ToolContext { + async execute(input) { + // Very expensive operation + return await this.reports.generate(input.tenantId, input.month); + } +} + +@App({ + id: 'analytics', + name: 'Analytics App', + plugins: [ + CachePlugin.init({ + type: 'redis', + defaultTTL: 600, // 10 minutes default + config: { + host: process.env.REDIS_HOST || 'localhost', + port: parseInt(process.env.REDIS_PORT || '6379'), + password: process.env.REDIS_PASSWORD, + }, + }), + ], + tools: [GetStockPriceTool, GetUserTool, GetTrendingTool, GenerateReportTool], +}) +class AnalyticsApp {} + +@FrontMcp({ + info: { name: 'Analytics Server', version: '1.0.0' }, + apps: [AnalyticsApp], + http: { port: 3000 }, +}) +export default class Server {} +``` + +--- + +## What's Next? + + + + Full Cache Plugin reference documentation + + + + Learn how the cache plugin uses hooks internally + + + + Create your own plugins with custom behavior + + diff --git a/docs/live/docs/guides/customize-flow-stages.mdx b/docs/live/docs/guides/customize-flow-stages.mdx new file mode 100644 index 00000000..e440b8b3 --- /dev/null +++ b/docs/live/docs/guides/customize-flow-stages.mdx @@ -0,0 +1,641 @@ +--- +title: Customize Flow Stages +slug: guides/customize-flow-stages +description: Hook into tool execution lifecycle to enforce policy, add telemetry, or transform inputs/outputs +icon: diagram-project +--- + +Hooks allow you to intercept and modify tool execution at specific lifecycle stages. Use hooks to add cross-cutting concerns like validation, logging, auditing, rate limiting, and data transformation without modifying individual tools. + +## Core Concepts + + + + Named execution pipelines with defined stages (e.g., `tools:call-tool`, `http:request`) + + + + **Will** (before), **Did** (after), **Around** (wrap), **Stage** (replace) + + + + Higher priority runs first for Will/Stage; Did runs in reverse order + + + Shared state object passed through all stages, allowing data flow between hooks + + + +--- + +## Hook Types + + + + Runs **before** a stage executes. Use for: + - Input validation + - Pre-processing + - Short-circuiting execution + - Setting up context + + ```ts + @ToolHook.Will('execute', { priority: 100 }) + async validateInput(ctx: FlowCtxOf<'tools:call-tool'>) { + const { toolContext } = ctx.state; + if (!toolContext) return; + + // Validate required fields + if (!toolContext.input.amount || toolContext.input.amount <= 0) { + throw new Error('Amount must be greater than 0'); + } + } + ``` + + + + + Runs **after** a stage completes. Use for: + - Post-processing + - Logging/auditing + - Output transformation + - Cleanup + + ```ts + @ToolHook.Did('execute', { priority: 100 }) + async redactSensitiveData(ctx: FlowCtxOf<'tools:call-tool'>) { + const { toolContext } = ctx.state; + if (!toolContext?.output) return; + + // Redact sensitive fields + if (typeof toolContext.output === 'object') { + toolContext.output = { + ...toolContext.output, + ssn: '***-**-****', + creditCard: '****-****-****-****', + }; + } + } + ``` + + + + + Wraps execution, running code before and after. Use for: + - Timing/profiling + - Try-catch error handling + - Resource management + - Transactions + + ```ts + @ToolHook.Around('execute') + async measureExecutionTime( + ctx: FlowCtxOf<'tools:call-tool'>, + next: () => Promise + ) { + const start = Date.now(); + const { toolContext } = ctx.state; + + try { + await next(); // Execute the stage + } finally { + const duration = Date.now() - start; + this.logger.info(`Tool ${toolContext?.toolName} took ${duration}ms`); + } + } + ``` + + + + + Replaces the default stage implementation entirely. Use for: + - Complete custom logic + - Alternative implementations + - Advanced control flow + + ```ts + @ToolHook.Stage('validate', { priority: 500 }) + async customValidation(ctx: FlowCtxOf<'tools:call-tool'>) { + // Custom validation logic + const { tool, toolContext } = ctx.state; + if (!tool || !toolContext) return; + + // Your custom validation + await this.validateSchema(toolContext.input, tool.inputSchema); + } + ``` + + + + +--- + +## Available Flows + +FrontMCP provides pre-defined hooks for common flows: + + + +```ts Tool Execution +import { ToolHook } from '@frontmcp/sdk'; + +class MyPlugin { + @ToolHook.Will('execute') + async beforeToolExecution(ctx: FlowCtxOf<'tools:call-tool'>) { + // Hook into tool execution + } + + @ToolHook.Did('execute') + async afterToolExecution(ctx: FlowCtxOf<'tools:call-tool'>) { + // Post-process tool results + } +} +``` + +```ts HTTP Requests +import { HttpHook } from '@frontmcp/sdk'; + +class MyPlugin { + @HttpHook.Will('execute') + async beforeHttpRequest(ctx: FlowCtxOf<'http:request'>) { + // Intercept HTTP requests + } +} +``` + +```ts List Tools +import { ListToolsHook } from '@frontmcp/sdk'; + +class MyPlugin { + @ListToolsHook.Did('execute') + async afterListTools(ctx: FlowCtxOf<'tools:list-tools'>) { + // Filter or transform tool list + } +} +``` + + + + + You can also create custom flows using `FlowHooksOf('custom-flow-name')` for application-specific pipelines. + + +--- + +## Complete Examples + +### Example 1: Request Validation & Auditing + +```ts +import { Plugin, ToolHook, FlowCtxOf } from '@frontmcp/sdk'; + +@Plugin({ + name: 'audit-plugin', + description: 'Validates requests and logs all tool executions', +}) +export default class AuditPlugin { + @ToolHook.Will('execute', { priority: 100 }) + async validateRequest(ctx: FlowCtxOf<'tools:call-tool'>) { + const { toolContext, tool } = ctx.state; + if (!toolContext || !tool) return; + + // Enforce tenant isolation + const tenantId = toolContext.authInfo.user?.tenantId; + if (!tenantId && tool.metadata.requiresTenant) { + throw new Error('Tenant ID required for this tool'); + } + + // Rate limiting check + await this.checkRateLimit(tenantId, tool.fullName); + } + + @ToolHook.Did('execute', { priority: 100 }) + async auditExecution(ctx: FlowCtxOf<'tools:call-tool'>) { + const { toolContext, tool } = ctx.state; + if (!toolContext || !tool) return; + + // Log execution + await this.auditLog.record({ + toolName: tool.fullName, + userId: toolContext.authInfo.user?.id, + tenantId: toolContext.authInfo.user?.tenantId, + input: toolContext.input, + output: toolContext.output, + timestamp: new Date().toISOString(), + duration: ctx.metrics?.duration, + }); + } + + private async checkRateLimit(tenantId: string, toolName: string) { + // Rate limiting logic + } +} +``` + +### Example 2: Error Handling & Retries + +```ts +import { Plugin, ToolHook, FlowCtxOf } from '@frontmcp/sdk'; + +@Plugin({ + name: 'resilience-plugin', + description: 'Adds retry logic and error handling to tools', +}) +export default class ResiliencePlugin { + @ToolHook.Around('execute') + async withRetry(ctx: FlowCtxOf<'tools:call-tool'>, next: () => Promise) { + const { tool, toolContext } = ctx.state; + if (!tool || !toolContext) return await next(); + + const maxRetries = tool.metadata.retries ?? 0; + let lastError: Error | undefined; + + for (let attempt = 0; attempt <= maxRetries; attempt++) { + try { + if (attempt > 0) { + this.logger.warn(`Retry attempt ${attempt} for ${tool.fullName}`); + await this.delay(attempt * 1000); // Exponential backoff + } + + await next(); + return; // Success + } catch (error) { + lastError = error as Error; + + // Don't retry on validation errors + if (error.message.includes('validation')) { + throw error; + } + + if (attempt === maxRetries) { + throw lastError; + } + } + } + } + + @ToolHook.Did('execute', { priority: -100 }) + async handleError(ctx: FlowCtxOf<'tools:call-tool'>) { + const { toolContext } = ctx.state; + if (!toolContext) return; + + // Transform errors into user-friendly messages + if (ctx.error) { + this.logger.error('Tool execution failed', { + tool: toolContext.toolName, + error: ctx.error.message, + }); + + // Could transform or wrap the error here + } + } + + private delay(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); + } +} +``` + +### Example 3: Data Transformation Pipeline + +```ts +import { Plugin, ToolHook, FlowCtxOf } from '@frontmcp/sdk'; + +@Plugin({ + name: 'transform-plugin', + description: 'Transforms inputs and outputs', +}) +export default class TransformPlugin { + @ToolHook.Will('execute', { priority: 50 }) + async normalizeInput(ctx: FlowCtxOf<'tools:call-tool'>) { + const { toolContext } = ctx.state; + if (!toolContext) return; + + // Normalize string inputs + if (typeof toolContext.input === 'object') { + for (const [key, value] of Object.entries(toolContext.input)) { + if (typeof value === 'string') { + toolContext.input[key] = value.trim().toLowerCase(); + } + } + } + } + + @ToolHook.Did('execute', { priority: 50 }) + async enrichOutput(ctx: FlowCtxOf<'tools:call-tool'>) { + const { toolContext, tool } = ctx.state; + if (!toolContext?.output || typeof toolContext.output !== 'object') { + return; + } + + // Add metadata to all responses + toolContext.output = { + ...toolContext.output, + _metadata: { + toolName: tool?.fullName, + executedAt: new Date().toISOString(), + version: tool?.metadata.version || '1.0.0', + }, + }; + } +} +``` + +--- + +## Priority System + + + Priority determines execution order. Higher priority runs **first** for Will/Stage hooks, and **last** for Did hooks. + + +```ts +class PriorityExample { + @ToolHook.Will('execute', { priority: 100 }) + async firstValidation() { + // Runs first + } + + @ToolHook.Will('execute', { priority: 50 }) + async secondValidation() { + // Runs second + } + + @ToolHook.Did('execute', { priority: 100 }) + async firstCleanup() { + // Runs last (Did reverses order) + } + + @ToolHook.Did('execute', { priority: 50 }) + async secondCleanup() { + // Runs first + } +} +``` + +--- + +## Flow Context + +The flow context (`FlowCtxOf<'flow-name'>`) contains: + +- `state` - Shared state between hooks (tool, toolContext, request, response, etc.) +- `error` - Any error that occurred during execution +- `metrics` - Timing and performance data +- `logger` - Logger instance for this flow + + + +```ts Accessing State +@ToolHook.Will('execute') +async logToolInfo(ctx: FlowCtxOf<'tools:call-tool'>) { + const { tool, toolContext } = ctx.state; + + ctx.logger.info('Executing tool', { + name: tool?.fullName, + input: toolContext?.input, + }); +} +``` + +```ts Modifying State +@ToolHook.Will('execute') +async enrichContext(ctx: FlowCtxOf<'tools:call-tool'>) { + const { toolContext } = ctx.state; + if (!toolContext) return; + + // Add user info to context + ctx.state.userEmail = toolContext.authInfo.user?.email; + ctx.state.requestId = generateRequestId(); +} +``` + +```ts Accessing Modified State +@ToolHook.Did('execute') +async useEnrichedContext(ctx: FlowCtxOf<'tools:call-tool'>) { + // Access data from earlier hooks + const requestId = ctx.state.requestId; + const userEmail = ctx.state.userEmail; + + this.logger.info('Request completed', { requestId, userEmail }); +} +``` + + + +--- + +## Hook Registry API + +For advanced use cases, you can programmatically access and manage hooks using the Hook Registry API. This is useful when building custom flow orchestration or when you need to dynamically query which hooks are registered. + +### Accessing the Hook Registry + +```ts +import { FlowCtxOf } from '@frontmcp/sdk'; + +class MyPlugin { + @ToolHook.Will('execute') + async checkRegisteredHooks(ctx: FlowCtxOf<'tools:call-tool'>) { + const hookRegistry = ctx.scope.providers.getHooksRegistry(); + + // Now you can use registry methods + const hooks = hookRegistry.getFlowHooks('tools:call-tool'); + } +} +``` + +### Registry Methods + + + + Retrieves all hooks registered for a specific flow. + + ```ts + const hooks = hookRegistry.getFlowHooks('tools:call-tool'); + // Returns: HookEntry[] - all hooks for this flow + ``` + + + + + Retrieves hooks for a specific flow, optionally filtered by owner ID. This is particularly useful for multi-tenant scenarios or when you need to isolate hooks by context. + + **Parameters:** + - `flow`: The flow name (e.g., `'tools:call-tool'`) + - `ownerId` (optional): The owner ID to filter by + + **Behavior:** + - If no `ownerId` is provided, returns all hooks for the flow + - If `ownerId` is provided, returns: + - Hooks belonging to the specified owner + - Global hooks (hooks with no owner) + + ```ts + // Get all hooks for a flow + const allHooks = hookRegistry.getFlowHooksForOwner('tools:call-tool'); + + // Get hooks for a specific owner (e.g., a specific tool or plugin) + const ownerHooks = hookRegistry.getFlowHooksForOwner( + 'tools:call-tool', + 'my-plugin-id' + ); + // Returns only hooks owned by 'my-plugin-id' + global hooks + ``` + + **Use Cases:** + - **Tool-specific hooks**: When a tool registers hooks that should only apply to its own execution + - **Multi-tenant isolation**: Filter hooks by tenant ID to ensure proper isolation + - **Plugin scoping**: Get hooks that belong to a specific plugin + + + + + Retrieves hooks for a specific flow and stage combination. + + ```ts + const executeHooks = hookRegistry.getFlowStageHooks( + 'tools:call-tool', + 'execute' + ); + // Returns: HookEntry[] - all hooks for the 'execute' stage + ``` + + + + + Retrieves hooks defined on a specific class. + + ```ts + const classHooks = hookRegistry.getClsHooks(MyPlugin); + // Returns: HookEntry[] - all hooks defined on MyPlugin class + ``` + + + + +### Example: Owner-Scoped Hook Filtering + +```ts +import { Plugin, ToolHook, FlowCtxOf } from '@frontmcp/sdk'; + +@Plugin({ + name: 'tool-isolation-plugin', + description: 'Demonstrates owner-scoped hook retrieval', +}) +export default class ToolIsolationPlugin { + @ToolHook.Will('execute') + async filterHooksByOwner(ctx: FlowCtxOf<'tools:call-tool'>) { + const { toolContext } = ctx.state; + if (!toolContext) return; + + const hookRegistry = ctx.scope.providers.getHooksRegistry(); + const toolOwnerId = toolContext.tool?.metadata.owner?.id; + + // Get only hooks relevant to this specific tool + const relevantHooks = hookRegistry.getFlowHooksForOwner('tools:call-tool', toolOwnerId); + + ctx.logger.info(`Found ${relevantHooks.length} hooks for this tool`, { + toolOwnerId, + hooks: relevantHooks.map((h) => h.metadata.stage), + }); + } +} +``` + + + The Hook Registry API is an advanced feature primarily intended for framework developers and complex plugin authors. + Most users should use the decorator-based approach (`@ToolHook`, `@HttpHook`, etc.) instead. + + +--- + +## Best Practices + + + + Package related hooks into plugins to reuse across multiple apps: + + ```ts + @Plugin({ + name: 'my-hooks', + description: 'Reusable hook collection', + }) + export default class MyHooksPlugin { + @ToolHook.Will('execute') + async myValidation(ctx) { + // Validation logic + } + } + + // Use in app + @App({ + plugins: [MyHooksPlugin], + }) + ``` + + + + + - **Validation**: High priority (90-100) + - **Transformation**: Medium priority (40-60) + - **Logging/Metrics**: Low priority (1-20) + + This ensures validation runs before transformation, and logging captures everything. + + + + + ```ts + @ToolHook.Will('execute') + async safeValidation(ctx) { + try { + await this.validate(ctx.state.toolContext?.input); + } catch (error) { + // Transform validation errors + throw new ValidationError(`Invalid input: ${error.message}`); + } + } + ``` + + + + ```ts + @ToolHook.Will('execute') + async startTimer(ctx) { + ctx.state.startTime = Date.now(); + } + + @ToolHook.Did('execute') + async endTimer(ctx) { + const duration = Date.now() - ctx.state.startTime; + this.logger.info(`Execution took ${duration}ms`); + } + ``` + + + + + Hooks run for every tool execution. Keep them fast: + - Use caching for expensive operations + - Delegate heavy work to background jobs + - Consider async/non-blocking operations + + + +--- + +## Links & Resources + + + + Learn more about FrontMCP plugins + + + + Understand tool lifecycle and execution + + + + See hooks in action with the Cache Plugin + + diff --git a/docs/live/docs/plugins/cache-plugin.mdx b/docs/live/docs/plugins/cache-plugin.mdx new file mode 100644 index 00000000..c0678f1c --- /dev/null +++ b/docs/live/docs/plugins/cache-plugin.mdx @@ -0,0 +1,531 @@ +--- +title: Cache Plugin +description: Transparently cache tool responses to reduce redundant computation and improve response times. +icon: database +--- + +The Cache Plugin provides transparent response caching for tools based on their input payloads, reducing redundant computation and improving response time. + +## Why Use Caching? + + + + Return cached results instantly without re-executing expensive operations + + + Minimize API calls, database queries, and computational overhead + + + + Lower infrastructure costs by reducing redundant processing + + + + Improve perceived performance with instant responses for repeated queries + + + +## Installation + +```bash +npm install @frontmcp/plugins +``` + +## How It Works + + + + The plugin hashes the tool's validated input and checks the cache store + + If a cached result exists, it's returned immediately, bypassing tool execution entirely + + The tool executes normally, and the result is stored with the configured TTL + + + When enabled, each cache read refreshes the TTL to keep hot entries alive longer + + + + + Cache entries are keyed using a **deterministic hash** of the tool's validated input. The same input always produces + the same cache key. + + +--- + +## Quick Start + +### Basic Setup (In-Memory) + +```ts +import { App } from '@frontmcp/sdk'; +import CachePlugin from '@frontmcp/plugins/cache'; + +@App({ + id: 'my-app', + name: 'My App', + plugins: [CachePlugin], // Default: memory store, 1-day TTL + tools: [ + /* your tools */ + ], +}) +export default class MyApp {} +``` + +### Enable Caching on Tools + +Caching is **opt-in** per tool. Add the `cache` field to your tool metadata: + + + +```ts Class-based tool +import { Tool, ToolContext } from '@frontmcp/sdk'; +import { z } from 'zod'; + +@Tool({ + name: 'get-user', + description: 'Get user by ID', + inputSchema: { id: z.string() }, + cache: true, // Enable caching with defaults +}) +export default class GetUserTool extends ToolContext { + async execute(input: { id: string }) { + // Expensive operation (e.g., database query) + return await this.database.getUser(input.id); + } +} +``` + +```ts Function-based tool +import { tool } from '@frontmcp/sdk'; +import { z } from 'zod'; + +export const GetUser = tool({ + name: 'get-user', + description: 'Get user by ID', + inputSchema: { id: z.string() }, + cache: true, // Enable caching with defaults +})(async (input) => { + // Expensive operation + return await database.getUser(input.id); +}); +``` + + + +--- + +## Storage Options + +### In-Memory (Default) + +Best for: Single-instance deployments, development, non-critical caching + +```ts +CachePlugin.init({ + type: 'memory', + defaultTTL: 300, // 5 minutes (default: 86400 = 1 day) +}); +``` + +Memory cache resets when the process restarts. Not shared across multiple instances. + +### Redis (Recommended for Production) + +Best for: Multi-instance deployments, persistent caching, production environments + + + +```ts Redis connection config +CachePlugin.init({ + type: 'redis', + defaultTTL: 600, // 10 minutes + config: { + host: '127.0.0.1', + port: 6379, + password: process.env.REDIS_PASSWORD, // optional + db: 0, // optional + }, +}); +``` + +```ts Reuse existing Redis client +import { Redis } from 'ioredis'; + +const redis = new Redis({ + host: 'redis.example.com', + port: 6379, +}); + +CachePlugin.init({ + type: 'redis-client', + defaultTTL: 900, // 15 minutes + client: redis, +}); +``` + + + +Redis enables cache sharing across multiple server instances and persists cache across restarts. + +--- + +## Configuration Options + +### Plugin-Level Configuration + +Configure default behavior when registering the plugin: + + + Cache store backend to use + + + + Default time-to-live in seconds (applies to all cached tools unless overridden) + + + + Redis connection configuration (required when `type: 'redis'`) + + + + Redis server hostname + + + Redis server port + + + Redis authentication password (optional) + + + Redis database number (optional, default: 0) + + + + + + Existing ioredis client instance (required when `type: 'redis-client'`) + + +### Tool-Level Configuration + +Configure caching behavior per tool in the `@Tool` or `tool()` metadata: + + + +```ts Simple (use defaults) +@Tool({ + name: 'get-report', + cache: true, // Uses plugin's defaultTTL +}) +``` + +```ts Custom TTL +@Tool({ + name: 'get-report', + cache: { + ttl: 60, // Cache for 1 minute + }, +}) +``` + +```ts With sliding window +@Tool({ + name: 'get-popular-items', + cache: { + ttl: 300, // 5 minutes + slideWindow: true, // Refresh TTL on each read + }, +}) +``` + + + + + Enable caching for this tool + +- `true` - Use plugin defaults +- `object` - Custom configuration + + + + + Time-to-live in seconds for this tool's cache entries (overrides plugin default) + + + + When `true`, reading from cache refreshes the TTL, keeping frequently accessed entries alive longer + + +--- + +## Advanced Usage + +### Multi-Tenant Caching + +Include tenant or user identifiers in your tool inputs to ensure cache isolation: + +```ts +@Tool({ + name: 'get-tenant-data', + inputSchema: { + tenantId: z.string(), + dataType: z.string(), + }, + cache: { ttl: 600 }, +}) +export default class GetTenantDataTool extends ToolContext { + async execute(input: { tenantId: string; dataType: string }) { + // Cache key includes tenantId automatically via input hash + return await this.fetchTenantData(input.tenantId, input.dataType); + } +} +``` + + + The cache key is derived from the **entire** input object, so including tenant/user IDs ensures proper isolation. + + +### Session-Scoped Caching + +For user-specific data, include session or user identifiers: + +```ts +export const GetUserDashboard = tool({ + name: 'get-user-dashboard', + inputSchema: { + userId: z.string(), + dateRange: z.object({ + start: z.string(), + end: z.string(), + }), + }, + cache: { + ttl: 120, // 2 minutes + slideWindow: true, + }, +})(async (input, ctx) => { + // Cache key includes userId and dateRange + return await generateDashboard(input.userId, input.dateRange); +}); +``` + +### Time-Based Invalidation + +Use short TTLs for frequently changing data: + +```ts +@Tool({ + name: 'get-live-stock-price', + inputSchema: { symbol: z.string() }, + cache: { + ttl: 5, // Only cache for 5 seconds + }, +}) +``` + +--- + +## Best Practices + + + + Cache tools whose outputs depend **solely** on their inputs. Don't cache tools that: + - Return random data + - Depend on external time-sensitive state + - Have side effects (mutations, API calls that change state) + + + + - **Short TTLs (5-60s)**: Real-time data, frequently changing content - **Medium TTLs (5-30min)**: User dashboards, + reports, analytics - **Long TTLs (hours-days)**: Static content, configuration, reference data + + + + Redis provides: - Cache persistence across restarts - Sharing across multiple server instances - Better memory + management with eviction policies + + + + Always include tenant IDs, user IDs, or other scoping fields in your tool inputs: + ```ts + // Good: includes tenantId for isolation + { tenantId: "t-123", reportId: "r-456" } + + // Bad: no scoping, shared across tenants + { reportId: "r-456" } + ``` + + + + + Enable `slideWindow` for frequently accessed data to keep it cached longer: + ```ts + cache: { + ttl: 300, + slideWindow: true, // Popular items stay cached + } + ``` + + + +--- + +## Cache Behavior Reference + +| Behavior | Description | +| ------------------ | -------------------------------------------------------------------- | +| **Key Derivation** | Deterministic hash from validated input. Same input = same cache key | +| **Cache Hits** | Bypasses tool execution entirely, returns cached result instantly | +| **Default TTL** | 86400 seconds (1 day) if not specified | +| **Sliding Window** | Extends TTL on reads when enabled | +| **Store Choice** | Memory is node-local; Redis enables multi-instance sharing | +| **Invalidation** | Automatic after TTL expires, or manually by restarting (memory) | + +--- + +## Troubleshooting + + + + **Possible causes:** + - Tool missing `cache: true` in metadata + - Cache store offline or misconfigured + - Input varies slightly (whitespace, order of fields) + + **Solutions:** + - Verify `cache` field is set in tool metadata + - Check Redis connection if using Redis backend + - Ensure input structure is consistent + + + + + **Possible causes:** + - TTL too long for data freshness requirements + - Data changed but cache not invalidated + + **Solutions:** + - Reduce TTL for the tool + - Consider input-based cache busting (include timestamp or version in input) + - Restart server to clear memory cache (or flush Redis) + + + + + **Possible cause:** + - Using memory cache with multiple server instances + + **Solution:** + - Switch to Redis backend for multi-instance deployments + + + + + **Solution:** + - Currently, manual invalidation requires custom implementation + - For memory: restart the server + - For Redis: use Redis CLI to delete keys manually + - Consider shorter TTLs or input-based versioning instead + + + +--- + +## Complete Example + +```ts +import { FrontMcp, App, Tool, ToolContext } from '@frontmcp/sdk'; +import CachePlugin from '@frontmcp/plugins/cache'; +import { z } from 'zod'; + +// Configure Redis cache +const cachePlugin = CachePlugin.init({ + type: 'redis', + defaultTTL: 600, // 10 minutes default + config: { + host: process.env.REDIS_HOST || 'localhost', + port: parseInt(process.env.REDIS_PORT || '6379'), + password: process.env.REDIS_PASSWORD, + }, +}); + +// Expensive report generation tool +@Tool({ + name: 'generate-monthly-report', + description: 'Generate monthly sales report for a tenant', + inputSchema: { + tenantId: z.string(), + month: z.string(), // "2025-01" + }, + cache: { + ttl: 1800, // 30 minutes (reports don't change often) + }, +}) +class GenerateMonthlyReportTool extends ToolContext { + async execute(input: { tenantId: string; month: string }) { + this.logger.info('Generating report', input); + + // Expensive operation: aggregate data, generate charts, etc. + const report = await this.database.generateReport(input.tenantId, input.month); + + return report; + } +} + +// Hot data with sliding window +@Tool({ + name: 'get-trending-products', + description: 'Get current trending products', + inputSchema: { + category: z.string(), + limit: z.number().default(10), + }, + cache: { + ttl: 120, // 2 minutes + slideWindow: true, // Keep popular queries cached + }, +}) +class GetTrendingProductsTool extends ToolContext { + async execute(input: { category: string; limit: number }) { + return await this.analytics.getTrendingProducts(input.category, input.limit); + } +} + +@App({ + id: 'analytics', + name: 'Analytics App', + plugins: [cachePlugin], + tools: [GenerateMonthlyReportTool, GetTrendingProductsTool], +}) +export default class AnalyticsApp {} + +@FrontMcp({ + info: { name: 'Analytics Server', version: '1.0.0' }, + apps: [AnalyticsApp], + http: { port: 3000 }, +}) +export default class Server {} +``` + +--- + +## Links & Resources + + + + View the cache plugin source code + + + + See caching in action with real examples + + + + Learn more about FrontMCP plugins + + + + Official Redis documentation + + diff --git a/docs/live/docs/plugins/codecall-plugin.mdx b/docs/live/docs/plugins/codecall-plugin.mdx new file mode 100644 index 00000000..e933e57d --- /dev/null +++ b/docs/live/docs/plugins/codecall-plugin.mdx @@ -0,0 +1,662 @@ +--- +title: CodeCall Plugin +description: Hide large toolsets behind a small, code-first meta-API using CodeCall Plugin. +--- + +Instead of listing every tool in `list_tools`, CodeCall: + +- Hides most tools. +- Exposes three main meta-tools: +- `codecall.search` +- `codecall.describe` +- `codecall.execute` +- And optionally, a direct-call helper: +- `codecall.invoke` (no code, no VM) + +The model then: + +1. Searches for relevant tools. +2. Describes selected tools (schemas). +3. Writes a JavaScript execution plan. +4. Executes that plan in a sandbox via `codecall.execute`, which calls your real tools. + +This lets you: + +- Expose **dozens or hundreds of tools** (including OpenAPI-generated ones). +- Avoid blowing up the context window. +- Let the LLM build richer workflows, joins, and filters in code. +- Work across **multiple apps** in the same FrontMCP server (e.g. `user` app + `billing` app). + +--- + +## When to use CodeCall + +Use CodeCall if: + +- You have **many tools** (inline + adapters). +- You don’t want to dump them all into `list_tools`. +- You want the LLM to: +- Combine multiple tools. +- Filter and post-process results. +- Build logic your API doesn’t support directly. +- You run **multiple apps** (e.g. `user`, `billing`) and want the model to: +- Search tools within a specific app. +- Or search across all apps, depending on the task. + +--- + +## Installation + +```bash +npm install @frontmcp/plugins +# or +yarn add @frontmcp/plugins +# or +pnpm add @frontmcp/plugins +``` + +> CodeCall uses [`vm2`](https://github.com/patriksimek/vm2) to run LLM-authored JavaScript in a sandbox. + +--- + +## Quick start + +### 1. Add CodeCallPlugin to your app + +```ts +import { App, Tool } from '@frontmcp/sdk'; +import CodeCallPlugin from '@frontmcp/plugins/codecall'; + +@Tool({ + name: 'health:ping', + description: 'Simple health check', + codecall: { + visibleInListTools: true, // keep visible as a normal tool + enabledInCodeCall: true, // also available inside CodeCall plans + }, +}) +class HealthPingTool { + async run() { + return { status: 'ok' }; + } +} + +@App({ + id: 'example', + name: 'Example App', + tools: [HealthPingTool], + plugins: [ + CodeCallPlugin.init({ + // Recommended default: many tools, all accessible via CodeCall + mode: 'codecall_only', + + topK: 8, + maxDefinitions: 8, + + directCalls: { + enabled: true, + // optional allowlist, otherwise defaults to tools enabled for CodeCall + allowedTools: ['users:getById', 'billing:getInvoice'], + }, + + vm: { + preset: 'secure', // CSP-like preset + timeoutMs: 4000, + allowLoops: false, + disabledBuiltins: ['eval', 'Function'], + disabledGlobals: ['require', 'process', 'fetch', 'setTimeout', 'setInterval'], + allowConsole: true, + }, + }), + ], +}) +export default class ExampleApp {} +``` + +From the model’s perspective, `list_tools` now shows: + +- `codecall.search` +- `codecall.describe` +- `codecall.execute` +- (optionally) `codecall.invoke` +- plus any tools with `codecall.visibleInListTools: true`. + +Everything else is reachable only via CodeCall. + +--- + +## Modes + +CodeCall offers three **modes** that control which tools are: + +- Visible in `list_tools`. +- Usable via `codecall.search/describe/execute`. + +### `mode: 'codecall_only'` (default) + +**Best when:** + +- You have **many tools**. +- You want to expose **all of them** via CodeCall. +- You **don’t** want to list them all. + +**Behavior:** + +- Hide all tools from `list_tools` by default. +- Include all tools in CodeCall search/execute by default. +- Per-tool metadata can: + + - Keep a tool visible in `list_tools`. + - Disable a tool from CodeCall if needed. + +--- + +### `mode: 'codecall_opt_in'` + +**Best when:** + +- You have a large toolbox, but only a **subset** should be usable via CodeCall. +- You don’t want every tool to be part of the “code surface”. + +**Behavior:** + +- Hide all tools from `list_tools` by default. +- Only tools with `codecall.enabledInCodeCall: true` are available via CodeCall. +- `codecall.visibleInListTools` controls whether a tool appears in `list_tools`. + +--- + +### `mode: 'metadata_driven'` + +**Best when:** + +- You have **up to ~10 tools**. +- You want to use **both** classic tool calls and CodeCall on the same tools. +- You’re happy to configure everything per-tool. + +**Behavior:** + +- CodeCall doesn’t assume anything by default. +- Only tools with: + + - `codecall.enabledInCodeCall: true` are used by CodeCall. + - `codecall.visibleInListTools: true` appear in `list_tools`. + +--- + +## Tool metadata + +CodeCall adds a `codecall` section to `@Tool` options: + +```ts +@Tool({ + name: 'users:list', + description: 'List users with pagination', + codecall: { + enabledInCodeCall: true, // usable from codecall.execute + visibleInListTools: false, // hidden from classic list_tools + }, +}) +class ListUsersTool { + // ... +} +``` + +Fields: + +- `codecall.visibleInListTools?: boolean` + + - `true` → keep visible in `list_tools`. + - `false` / omitted → plugin may hide it depending on `mode`. + +- `codecall.enabledInCodeCall?: boolean` + + - Controls whether the tool is indexed and callable from CodeCall. + - Defaults depend on `mode`: + + - `codecall_only`: default **true** + - `codecall_opt_in`: default **false** + - `metadata_driven`: default **false** + +--- + +## Multi-app search + +If your FrontMCP server has **multiple apps** (e.g. `user` app and `billing` app), CodeCall supports app-aware search: + +- `codecall.search` accepts an optional `filter` object with `appIds`. +- The model can: + + - Search tools in just the `user` app. + - Just the `billing` app. + - Or across both. + +Conceptually: + +```jsonc +// Search only in "user" app +{ + "tool": "codecall.search", + "input": { + "query": "get current user profile", + "filter": { "appIds": ["user"] } + } +} + +// Search across "user" AND "billing" apps +{ + "tool": "codecall.search", + "input": { + "query": "show unpaid invoices for the current user", + "filter": { "appIds": ["user", "billing"] } + } +} +``` + +The LLM can decide per task whether it needs data from a single app or to join data across apps. + +--- + +## VM security presets + +CodeCall runs code in a `vm2` sandbox with **CSP-style presets**. + +```ts +vm: { + preset?: 'locked_down' | 'secure' | 'balanced' | 'experimental'; + timeoutMs?: number; + allowLoops?: boolean; + disabledBuiltins?: string[]; + disabledGlobals?: string[]; + allowConsole?: boolean; +} +``` + +Suggested usage: + +- `locked_down` – strictest; highly sensitive environments. +- `secure` – **default**; safe for untrusted LLM code. +- `balanced` – more permissive; internal/trusted models. +- `experimental` – dev-only; very relaxed, not for production. + +You can override any fields on top of the preset. + +--- + +## Meta-tools + +CodeCall exposes **three primary** tools + **one optional**: + +1. `codecall.search` +2. `codecall.describe` +3. `codecall.execute` +4. `codecall.invoke` (optional, no code/VM) + +### `codecall.search` + +Find relevant tools by natural language. + +**Input (conceptual):** + +```json +{ + "query": "get all users who logged in today", + "topK": 8, + "filter": { + "appIds": ["user"], + "tags": ["users", "sessions"] + } +} +``` + +**Output:** + +```jsonc +{ + "tools": [ + { + "name": "users:list", + "description": "List users with pagination", + "appId": "user", + "source": "inline", + "score": 0.92 + } + ] +} +``` + +Usage pattern: + +1. Call `codecall.search` with your high-level goal. +2. Optionally scope by `appIds` if you want specific apps. +3. Pick tool names from the results. +4. Call `codecall.describe` on those tools. + +--- + +### `codecall.describe` + +Get input/output schemas for selected tools. + +**Input:** + +```json +{ + "tools": ["users:list", "billing:listInvoices"], + "max": 8 +} +``` + +**Output (simplified):** + +```jsonc +{ + "tools": [ + { + "name": "users:list", + "description": "List users with pagination", + "inputSchema": { + /* JSON schema-like */ + }, + "outputSchema": { + /* optional JSON schema-like */ + }, + "examples": [ + { + "input": { "limit": 10 }, + "output": { + "items": [ + /* ... */ + ] + } + } + ] + } + ] +} +``` + +The model uses these schemas to build a valid JS plan for `codecall.execute`. + +--- + +### `codecall.execute` + +Run a **JavaScript execution plan** that calls tools from inside a sandbox. + +**Input (conceptual):** + +```json +{ + "script": "/* JavaScript code */", + "allowedTools": ["users:list", "billing:listInvoices"], + "context": { + "tenantId": "t-123", + "userId": "u-456" + } +} +``` + +Inside the VM, the script has access to: + +```js +await callTool('users:list', { limit: 100 }); +const meta = getTool('users:list'); // name, description, schemas + +codecallContext.tenantId; // read-only context + +console.log('debug info'); // if allowConsole === true + +mcpLog('info', 'Loaded users', { count: 100 }); +mcpNotify('step_started', { step: 'load_users' }); +``` + +> `console`, `mcpLog`, and `mcpNotify` are available only if enabled in config. + +**Output (high level):** + +CodeCall normalizes results into a shape with `status`: + +- `ok` – script returned successfully. +- `syntax_error` – JS parse error. +- `illegal_access` – attempted to use a blocked builtin/global. +- `runtime_error` – bug in the JS plan (script-level error). +- `tool_error` – error thrown by a specific tool call (tool-level error). +- `timeout` – script exceeded the configured time. + +This allows the LLM and the orchestrator to distinguish: + +- “My code is wrong” vs +- “This specific tool call failed with input X”. + +--- + +### `codecall.invoke` (direct call, no code) + +For simple cases, you might want the model to call a tool **without** writing a +JavaScript plan and spinning up the VM. + +When `directCalls.enabled === true`, CodeCall exposes an extra meta-tool: + +- e.g. `codecall.invoke` + +**Input (conceptual):** + +```json +{ + "tool": "users:getById", + "input": { "id": "123" } +} +``` + +**Behavior:** + +- No VM is created. +- No user-written JS is executed. +- The call behaves just like a normal MCP tool call: + + - PII plugin still runs. + - Auth, rate limiting, logging, etc. still run. + +- On success: you get back the tool result. +- On error: + + - If the tool doesn’t exist or isn’t allowed, you get a clear “tool not found / not allowed” error. + - If the tool throws, the error includes: + + - the tool name + - the input that was used + - optional error code/details + +This is ideal when: + +- The model just needs a **single tool action**. +- There’s no need for multi-step orchestration or cross-tool joins. +- You still want all the benefits of CodeCall’s tooling (modes, PII plugins, etc.) + without paying the VM cost for simple cases. + +--- + +## Logging & notifications from plans + +CodeCall supports two types of visibility into what a plan is doing: + +1. **Automatic notifications** for tool calls: + +- Every `callTool()` can emit: + +- A “tool call started” event (with tool name + input). +- A “tool call finished” event (with tool name + input + status). +- These can be surfaced by your transport as live updates (e.g. SSE). + +2. **Script-driven logging/notifications**: + +- CodeCall can expose FrontMCP builtin logging/notification methods into the VM. +- This lets the LLM decide **what** to notify about and **with which parameters**. + +Example inside a plan: + +```js +async function main() { + mcpNotify('step_started', { step: 'sync_billing' }); + + const invoices = await callTool('billing:listInvoices', { status: 'unpaid' }); + + mcpLog('info', 'Loaded unpaid invoices', { count: invoices.items.length }); + + mcpNotify('step_completed', { + step: 'sync_billing', + count: invoices.items.length, + }); + + return invoices.items; +} + +return main(); +``` + +If you don’t want models to emit custom notifications, you can disable these globals in the plugin configuration. + +--- + +## PII & privacy + +CodeCall runs **on top of** your existing FrontMCP tool pipeline – it does _not_ bypass it. + +When a plan calls `callTool('some:tool', input)` inside `codecall.execute`: + +- The request still goes through: + + - Any PII plugins (scrubbing/masking input and output). + - Auth plugins. + - Logging / audit. + - Rate limiting. + +- The response that CodeCall sees is exactly what a normal tool call would see + after those plugins have run. + +This means: + +- CodeCall cannot leak more data than a normal tool call. +- PII redaction logic remains in your **PII plugin**, not inside CodeCall. +- The results you get from a CodeCall plan are equivalent (in terms of privacy) + to calling the same tools one-by-one outside of CodeCall. + +--- + +## Tool discovery, caching & notifications + +CodeCall works with both **stateful** and **stateless** clients. + +### With tool list change notifications + +If your transport supports: + +- A **transport session ID**, and +- **Tool list change notifications**, + +then the recommended pattern is: + +1. The client calls `codecall.search` and `codecall.describe` for a given session. +2. It caches tool definitions (names + schemas) in memory, keyed by session. +3. When tools relevant to that session change (added/updated/removed), the server sends a **“tool list changed”** notification. +4. The client refreshes only when notified, instead of re-describing tools on every round-trip. + +### Without notifications + +If the transport does **not** support notifications: + +- The simplest pattern is: + +> Before using a tool inside `codecall.execute`, call `codecall.describe` for that tool in the same turn. + +- Treat search/describe as ephemeral and re-run them as needed to avoid stale schemas. + +--- + +## Permissions & OAuth (future integration) + +Some tools may: + +- Be **listed and discoverable** to the user. +- But require **additional permissions or a separate OAuth flow** before they can be executed. + +In these cases: + +- The tool itself should return a structured “authorization required” result (e.g. including: + + - A URL to start an OAuth flow. + - A reason / scope description. + - The tool name. + +- CodeCall does **not** handle OAuth directly — it simply returns what the tool returned. +- A dedicated **authorization/permissions plugin** can be added later to: + + - Detect this pattern in tool responses. + - Coordinate running the OAuth flow. + - Retry execution after permissions are granted. + +This keeps a clear separation: + +- CodeCall: code orchestration + sandboxing. +- Auth plugin: identity/permissions orchestration. + +--- + +## Example: query-like behavior in code + +Even if your REST API doesn’t support this query directly, CodeCall can: + +> “Return all user IDs whose first name starts with `me` and logged in today.” + +```js +// Example script for `codecall.execute` + +async function main() { + // 1. Get users from a coarse API + const page = await callTool('users:list', { limit: 500 }); + + // 2. Filter in JS + const today = new Date().toISOString().slice(0, 10); + + const matching = page.items.filter((user) => { + const firstName = (user.firstName || '').toLowerCase(); + const lastLogin = (user.lastLogin || '').slice(0, 10); + return firstName.startsWith('me') && lastLogin === today; + }); + + // 3. Return just IDs + return matching.map((u) => u.id); +} + +return main(); +``` + +The LLM can generate this plan after: + +1. `codecall.search` → finds `users:list` (maybe from the `user` app). +2. `codecall.describe` → reads its schema. +3. `codecall.execute` → runs the plan. + +--- + +## Summary + +- Use **CodeCall** when you want to: + + - Hide large toolsets. + - Expose a small, code-first meta-API. + - Let the LLM orchestrate tools across one or more apps in JavaScript. + +- Choose a **mode** based on your tool count and control preferences: + + - `codecall_only` – many tools, expose all via CodeCall. + - `codecall_opt_in` – many tools, only some via CodeCall. + - `metadata_driven` – few tools, mix classic tool calls + CodeCall. + +- Use **VM presets** to balance power and safety. +- Use **direct calls (`codecall.invoke`)** for simple, single-tool actions without running a VM. +- Rely on your existing **PII and lifecycle plugins** for privacy and policy enforcement — CodeCall reuses the same pipeline. +- If your transport supports **tool list change notifications**, cache described tools per session and refresh on change; if not, re-describe before execution. + +This gives you a scalable, safe, and flexible way to expose your entire tool universe to LLMs without overwhelming them. diff --git a/docs/live/docs/servers/apps.mdx b/docs/live/docs/servers/apps.mdx new file mode 100644 index 00000000..ceb5ed19 --- /dev/null +++ b/docs/live/docs/servers/apps.mdx @@ -0,0 +1,106 @@ +--- +title: Apps +slug: servers/apps +icon: cube +--- + +Apps are how you **group capabilities** in FrontMCP. Each app can contribute **tools**, **resources**, **prompts**, plus **providers**, **adapters**, and **plugins**. Apps may run _locally_ (in this process) or be _remote_ (URL/worker). + +## Minimal local app + +```ts +import { App } from '@frontmcp/sdk'; + +@App({ + id: 'hello', + name: 'Hello App', + tools: [], +}) +export default class HelloApp {} +``` + +Add it to your server: + +```ts +@FrontMcp({ info: { name: 'Demo', version: '0.1.0' }, apps: [HelloApp] }) +export default class Server {} +``` + +--- + +## Local app options + +```ts +@App({ + id?: string, + name: string, + description?: string, + providers?: ProviderType[], + authProviders?: AuthProviderType[], + plugins?: PluginType[], + adapters?: AdapterType[], + tools?: ToolType[], + resources?: ResourceType[], + prompts?: PromptType[], + auth?: AuthOptions, // app‑default auth (overrides server auth) + standalone?: 'includeInParent' | boolean, // isolate scope / expose separately +}) +``` + +**Scoping & auth** + +- If the server uses **`splitByApp: true`**, each app is isolated and **must** configure its own auth (server-level `auth` is disallowed). +- `standalone: true` makes the app expose its own scope/entry; `'includeInParent'` lists it under the parent while keeping isolation. + +**Dependency resolution** + +- Providers resolve **tool → app → server**. +- Plugins/adapters attach at app scope; generated items inherit the app’s policies and provenance. + +--- + +## Remote apps + +Define an app that proxies to a **worker file** or a **URL**: + +```ts +@App({ + name: 'Remote CRM', + urlType: 'url', // 'url' | 'worker' + url: 'https://crm.example.com/mcp', + auth: { type: 'remote', name: 'crm-idp', baseUrl: 'https://idp.example.com' }, + standalone: true, +}) +export default class CrmApp {} +``` + +**Fields** + +```ts +{ + id?: string; + name: string; + description?: string; + urlType: 'worker' | 'url'; + url: string; + auth?: AuthOptions; + standalone?: 'includeInParent' | boolean; +} +``` + +--- + +## Example: app with adapter + providers + +```ts +@App({ + id: 'billing', + name: 'Billing', + providers: [DbProvider, CacheProvider], + adapters: [ + // e.g. OpenAPI adapter that generates tools/resources from a spec + BillingOpenApiAdapter, + ], +}) +export default class BillingApp {} +``` diff --git a/docs/live/docs/servers/authentication/local.mdx b/docs/live/docs/servers/authentication/local.mdx new file mode 100644 index 00000000..de2e31a7 --- /dev/null +++ b/docs/live/docs/servers/authentication/local.mdx @@ -0,0 +1,49 @@ +--- +title: Local OAuth +slug: servers/authentication/local +icon: window +--- + +FrontMCP ships a **built‑in OAuth provider** for first‑party scenarios for development. + +### Configuration + +```ts +auth: { + type: 'local', + id: 'local', + name: 'Local Auth', + scopes?: string[], + grantTypes?: ('authorization_code' | 'refresh_token')[], + allowAnonymous?: boolean, // default true + consent?: boolean, + jwks?: JSONWebKeySet, // inline JWKS (optional) + signKey?: JWK | Uint8Array // private key (optional; auto‑generated if omitted) +} +``` + +### Example (per app, split‑by‑app server) + +```ts +@FrontMcp({ + info: { name: 'Workspace', version: '1.0.0' }, + auth: { type: 'local' }, + apps: [DocsApp, MailApp], + splitByApp: true, +}) +export default class Server {} + +@App({ + name: 'Docs', +}) +export default class DocsApp {} +``` + +When using `splitByApp: true`, define auth per app; server‑level `auth` is not allowed. + +--- + +## Keys & JWKS + +- Omit `signKey` to auto‑generate keys (exposed via the local JWKS endpoint). +- Provide `jwks` or `signKey` to pin keys for stable environments. diff --git a/docs/live/docs/servers/authentication/overview.mdx b/docs/live/docs/servers/authentication/overview.mdx new file mode 100644 index 00000000..8330cdcb --- /dev/null +++ b/docs/live/docs/servers/authentication/overview.mdx @@ -0,0 +1,85 @@ +--- +title: Authentication +sidebarTitle: Overview +slug: servers/authentication/overview +icon: users-between-lines +--- + +FrontMCP supports **Remote OAuth** (connect to an external IdP) and a built‑in **Local OAuth** provider. You can configure auth **at the server** or **per app**: + +- **Server‑level auth**: a default provider for all apps (only when `splitByApp: false`). +- **App‑level auth**: each app defines its own provider; required when `splitByApp: true`. + + + With splitByApp: true, FrontMCP mounts each app at its own base path (for example /billing) + and reuses that scope for OAuth issuers and the /message SSE endpoint so clients automatically target the + right app. + + +Auth drives **session creation**, **token propagation**, and optional **consent** for tools/resources/prompts. + + + If an external provider **doesn’t support Dynamic Client Registration (DCR)**, FrontMCP can front it with a **local + OAuth proxy** that registers/holds the client on your behalf. See *Remote OAuth → Proxy*. + + +--- + +## Where to configure auth + +**Server (multi‑app, shared auth):** + +```ts +@FrontMcp({ + info: { name: 'Expense', version: '1.0.0' }, + apps: [BillingApp, AnalyticsApp], + auth: { type: 'remote', name: 'frontegg', baseUrl: 'https://idp.example.com' }, +}) +export default class Server {} +``` + +**Per app (isolated scopes):** + +```ts +@App({ name: 'Billing', auth: { type: 'local', id: 'local', name: 'Local Auth' } }) +export default class BillingApp {} + +@FrontMcp({ info: { name: 'Suite', version: '1.0.0' }, apps: [BillingApp, AnalyticsApp], splitByApp: true }) +export default class Server {} +``` + +--- + +## Consent & scopes + +Enable consent to let users select the **tools/resources/prompts** they grant, producing a **scoped token**. + +```ts +auth: { + type: 'remote', + name: 'frontegg', + baseUrl: 'https://idp.example.com', + consent: true +} +``` + +You can still send classic OAuth scopes via `scopes: string[]`. + +--- + +## Sessions & transport + +Auth integrates with server sessions (see _Server Overview → Sessions_): + +- `session.sessionMode`: `stateful` keeps tokens server‑side (recommended for nested providers); `stateless` embeds in JWT (simpler). +- `session.transportIdMode`: `uuid` (per‑node) or `jwt` (signed transport IDs for distributed setups). + + + Use **stateful** sessions when working with short‑lived upstream tokens—you’ll get refresh without round‑trips. + + +--- + +## Discovery & well‑known + +Remote providers typically self‑describe via `/.well-known/oauth-authorization-server` and `/.well-known/jwks.json`. You may override endpoints and JWKS inline when needed. diff --git a/docs/live/docs/servers/authentication/remote-proxy.mdx b/docs/live/docs/servers/authentication/remote-proxy.mdx new file mode 100644 index 00000000..67194067 --- /dev/null +++ b/docs/live/docs/servers/authentication/remote-proxy.mdx @@ -0,0 +1,40 @@ +--- +title: Remote OAuth — Proxy +slug: servers/authentication/remote-proxy +icon: share +--- + +Some IdPs don’t support **Dynamic Client Registration (DCR)**. FrontMCP can front them with a **local OAuth proxy** that: + +1. Exposes OAuth endpoints locally under your server/app scope. +2. Registers a client using your provided `clientId` (or derives one via function). +3. Issues/validates tokens while exchanging with the upstream IdP. + +### Configure + +```ts +@FrontMcp({ + info: { name: 'Suite', version: '1.0.0' }, + apps: [SalesApp], + auth: { + type: 'remote', + name: 'legacy-idp', + baseUrl: 'https://legacy-idp.example.com', + dcrEnabled: false, + clientId: 'my-preprovisioned-client', + consent: true, + }, +}) +export default class Server {} +``` + + + At runtime, the local proxy maintains client registration and keys while delegating user authentication to the + upstream IdP. + + +--- + +## Endpoint overrides + +If your IdP requires custom endpoints or static keys, set `authEndpoint`, `tokenEndpoint`, `registrationEndpoint`, `userInfoEndpoint`, `jwks`, or `jwksUri` directly. diff --git a/docs/live/docs/servers/authentication/remote.mdx b/docs/live/docs/servers/authentication/remote.mdx new file mode 100644 index 00000000..efef6a50 --- /dev/null +++ b/docs/live/docs/servers/authentication/remote.mdx @@ -0,0 +1,61 @@ +--- +title: Remote OAuth +slug: servers/authentication/remote +icon: user-shield +--- + +Use a remote identity provider (IdP) like Frontegg, Auth0, Azure Entra, etc. + +### Configuration + +```ts +auth: { + type: 'remote', + name: 'frontegg', + baseUrl: 'https://auth.example.com', + dcrEnabled?: boolean, + clientId?: string | ((clientInfo: { clientId: string }) => string), + mode?: 'orchestrated' | 'transparent', + allowAnonymous?: boolean, + consent?: boolean, + scopes?: string[], + grantTypes?: ('authorization_code' | 'refresh_token')[], + authEndpoint?: string, + tokenEndpoint?: string, + registrationEndpoint?: string, + userInfoEndpoint?: string, + jwks?: JSONWebKeySet, + jwksUri?: string, +} +``` + +### Example (server‑level) + +```ts +@FrontMcp({ + info: { name: 'Expense MCP', version: '1.0.0' }, + apps: [ExpenseApp], + auth: { type: 'remote', name: 'frontegg', baseUrl: 'https://auth.example.com', consent: true }, +}) +export default class Server {} +``` + +### Example (per app) + +```ts +@App({ + name: 'CRM', + auth: { type: 'remote', name: 'crm', baseUrl: 'https://idp.example.com', scopes: ['openid', 'email'] }, + standalone: true, +}) +export default class CrmApp {} +``` + +Use `standalone: true` to expose the app’s auth surface under its own scope/entry. + +--- + +## DCR vs non‑DCR + +- **`dcrEnabled: true`** → FrontMCP registers the client dynamically at the IdP. +- **`dcrEnabled: false`** → supply `clientId` and use a **local OAuth proxy** to handle registration/storage. See _Remote OAuth → Proxy_. diff --git a/docs/live/docs/servers/authentication/token.mdx b/docs/live/docs/servers/authentication/token.mdx new file mode 100644 index 00000000..344381d4 --- /dev/null +++ b/docs/live/docs/servers/authentication/token.mdx @@ -0,0 +1,40 @@ +--- +title: Token & Session +slug: servers/authentication/token +icon: key +--- + +Authorization determines **what** a token may do. + +## OAuth scopes + +Provide standard scopes to external IdPs: + +```ts +auth: { + type: 'remote', + name: 'frontegg', + baseUrl: 'https://idp.example.com', + scopes: ['openid','profile','email'] +} +``` + +## Tool/resource/prompt consent + +Set `consent: true` to display a **post‑login consent** listing your registered **tools/resources/prompts**. The issued access token includes the selected grants. + +## Modes (Remote OAuth) + +Use the `mode` field to reflect deployment topology: + +- `transparent` (default): your server acts as a regular confidential client. +- `orchestrated`: gateway coordinates multiple apps/providers under one umbrella token (used in advanced multi‑app setups). + +When `splitByApp: true`, configure auth **per app**; server‑level `auth` is disallowed. + +--- + +## Token lifetimes & sessions + +- **Stateful sessions**: tokens are encrypted server‑side; clients hold a lightweight reference. Smooth refresh. +- **Stateless sessions**: tokens ride inside JWT; simple but no silent refresh of upstream tokens. diff --git a/docs/live/docs/servers/extensibility/adapters.mdx b/docs/live/docs/servers/extensibility/adapters.mdx new file mode 100644 index 00000000..682d62b2 --- /dev/null +++ b/docs/live/docs/servers/extensibility/adapters.mdx @@ -0,0 +1,46 @@ +--- +title: Adapters Transformers +sidebarTitle: Adapters +slug: servers/extensibility/adapters +icon: plug +--- + +**Adapters** convert **external definitions or systems** into FrontMCP components (tools, resources, prompts). Common examples: + +- **OpenAPI** → generate typed **tools** from REST endpoints +- **File system / blobs** → expose documents as **resources** +- **Custom backends** → synthesize prompts/resources from proprietary schemas + +## Define an adapter + +```ts +import { Adapter } from '@frontmcp/sdk'; + +@Adapter({ + name: 'Billing OpenAPI Loader', + description: 'Generates tools/resources from the Billing API spec', +}) +export default class BillingOpenApiAdapter { + // implement adapter-specific logic to register generated tools/resources/prompts +} +``` + +Register the adapter on an app: + +```ts +@App({ + name: 'Billing', + adapters: [BillingOpenApiAdapter], +}) +export default class BillingApp {} +``` + +### Behavior + +- Generated items inherit the **app’s plugins, providers, and policies** and are tagged with provenance. +- Adapters can contribute **tools**, **resources**, and **prompts** simultaneously. + + + Keep adapters **pure**: read a spec or source, generate components, and let app-level plugins handle cross-cutting + concerns like auth, rate limiting, or caching. + diff --git a/docs/live/docs/servers/extensibility/logging.mdx b/docs/live/docs/servers/extensibility/logging.mdx new file mode 100644 index 00000000..88169522 --- /dev/null +++ b/docs/live/docs/servers/extensibility/logging.mdx @@ -0,0 +1,191 @@ +--- +title: Logging Transports +sidebarTitle: Logging +slug: servers/extensibility/logging +icon: receipt +description: Create and register custom log transports for FrontMCP. +--- + +FrontMCP logging is extensible. In addition to the default console logger, you can register one or more custom transports via the server config. + +## Register a transport + +```ts +@FrontMcp({ + info: { name: 'Demo', version: '0.1.0' }, + apps: [App], + logging: { + level: LogLevel.Info, + enableConsole: true, // set false to disable the built‑in console transport + transports: [StructuredJsonTransport, HttpBatchTransport], + }, +}) +export default class Server {} +``` + +## Transport contract + +A transport is a class decorated with `@LogTransport({...})` that implements `LogTransportInterface`. + +```ts +export interface LogRecord { + level: LogLevel; + levelName: string; + message: string; + args: unknown[]; + timestamp: Date; + prefix: string; +} + +export type LogFn = (msg?: any, ...args: any[]) => void; + +export abstract class LogTransportInterface { + abstract log(rec: LogRecord): void; +} +``` + +The framework filters by the configured `logging.level` before calling transports. + +> Transports should never throw. Handle errors internally and keep I/O non‑blocking (use buffering/batching for remote sinks). + +## Built‑in: Console + +The default console transport formats messages with ANSI (when TTY) and falls back to plain text otherwise. + +```ts +@LogTransport({ + name: 'ConsoleLogger', + description: 'Default console logger', +}) +export class ConsoleLogTransportInstance extends LogTransportInterface { + log(rec: LogRecord): void { + const fn = this.bind(rec.level, rec.prefix); + fn(String(rec.message), ...rec.args); + } + // ...see source for details +} +``` + +Disable it by setting `enableConsole: false` in your server config. + +## Example: Structured JSON (JSONL) + +Emit machine‑readable logs (one JSON per line). Useful for file shipping agents or centralized logging. + +```ts +import { LogTransport, LogTransportInterface, LogRecord } from '@frontmcp/sdk'; + +@LogTransport({ + name: 'StructuredJsonTransport', + description: 'Writes JSONL log records to stdout', +}) +export class StructuredJsonTransport extends LogTransportInterface { + log(rec: LogRecord): void { + try { + const payload = { + ts: rec.timestamp.toISOString(), + level: rec.levelName, // e.g. INFO + levelValue: rec.level, // numeric + prefix: rec.prefix || undefined, + msg: stringify(rec.message), + args: rec.args?.map(stringify), + }; + // Avoid console formatting; write raw line + process.stdout.write(JSON.stringify(payload) + '\n'); + } catch (err) { + // Never throw from a transport + } + } +} + +function stringify(x: unknown) { + if (x instanceof Error) { + return { name: x.name, message: x.message, stack: x.stack }; + } + try { + return typeof x === 'string' ? x : JSON.parse(JSON.stringify(x)); + } catch { + return String(x); + } +} +``` + +Register it: + +```ts +logging: { level: LogLevel.Info, enableConsole: false, transports: [StructuredJsonTransport] } +``` + +## Example: HTTP batch transport (non‑blocking) + +Buffer records in memory and POST them in batches. Implements basic retry with backoff. + +```ts +import { LogTransport, LogTransportInterface, LogRecord } from '@frontmcp/sdk'; + +@LogTransport({ + name: 'HttpBatchTransport', + description: 'POST logs in batches', +}) +export class HttpBatchTransport extends LogTransportInterface { + private queue: any[] = []; + private timer: NodeJS.Timeout | null = null; + private flushing = false; + private readonly maxBatch = 50; + private readonly flushMs = 1000; + + constructor(private endpoint = process.env.LOG_ENDPOINT || 'https://logs.example.com/ingest') { + super(); + } + + log(rec: LogRecord): void { + this.queue.push({ + ts: rec.timestamp.toISOString(), + lvl: rec.levelName, + pfx: rec.prefix || undefined, + msg: String(rec.message), + args: safeArgs(rec.args), + }); + if (this.queue.length >= this.maxBatch) this.flush(); + else if (!this.timer) this.timer = setTimeout(() => this.flush(), this.flushMs); + } + + private async flush() { + // TODO: Implement batch flush logic + // - Extract batch from queue + // - POST to this.endpoint + // - Handle errors and implement retry with backoff + // - Reset flushing flag and timer + } +} + +function safeArgs(a: unknown[]) { + return (a || []).map((x) => (x instanceof Error ? { name: x.name, message: x.message, stack: x.stack } : x)); +} +``` + +Notes: + +- Keep batches small and time‑bounded; avoid blocking the event loop. +- On process exit, you may add a `beforeExit`/`SIGTERM` handler to flush synchronously. + +## Prefixes and levels + +- `logging.prefix` adds a static scope tag to all records (e.g., app name or environment). +- Transports receive `rec.level` and `rec.levelName`; the framework already filtered below‑level logs. + +```ts +logging: { + level: LogLevel.Warn, + prefix: 'billing‑edge', + transports: [StructuredJsonTransport] +} +``` + +## Best practices + +- Never throw from `log()`; swallow and self‑heal. +- Avoid heavy sync I/O; prefer buffering and async flush. +- Redact sensitive fields before emit (tokens, PII). +- Serialize `Error` objects explicitly (name, message, stack). +- Apply backpressure: if the sink is down, drop or sample rather than blocking the server. diff --git a/docs/live/docs/servers/extensibility/plugins.mdx b/docs/live/docs/servers/extensibility/plugins.mdx new file mode 100644 index 00000000..b8c480c7 --- /dev/null +++ b/docs/live/docs/servers/extensibility/plugins.mdx @@ -0,0 +1,56 @@ +--- +title: Plugin Extensions +sidebarTitle: Plugins +slug: servers/extensibility/plugins +icon: puzzle-piece +--- + +**Plugins** add cross-cutting behavior and can also contribute components. Typical uses: auth/session helpers, PII filtering, tracing, logging, caching, error policy, rate-limits. + +## Define a plugin + +```ts +import { Plugin } from '@frontmcp/sdk'; + +@Plugin({ + name: 'Cache Plugin', + description: 'Adds transparent response caching for tools/resources', + providers: [CacheProvider], // plugin-scoped providers + exports: [CacheProvider], // re-export to host app + adapters: [SpecNormalizerAdapter], // optionally attach adapters + tools: [WarmCacheTool], // and tools/resources/prompts if desired + resources: [], + prompts: [], +}) +export default class CachePlugin {} +``` + +Attach a plugin at app scope: + +```ts +@App({ + name: 'Billing', + plugins: [CachePlugin, ObservabilityPlugin], +}) +export default class BillingApp {} +``` + +### What plugins can do + +- Register **providers** (and **export** them to the host app) +- Contribute **adapters**, **tools**, **resources**, **prompts** +- Participate in lifecycle via **hooks** (see _Advanced → Hooks_) + +### Composition + +Plugins compose **depth-first** at the app level. Later plugins can depend on providers exported by earlier ones. + + + Put organization-wide concerns (auth, audit, tracing) in plugins so all generated and inline components inherit the + behavior without boilerplate. + + +## Built-in plugins + +- [Cache Plugin](/docs/plugins/cache-plugin) — Transparent per-tool caching with memory or Redis stores, TTL controls, and cache-warming helpers. +- [CodeCall Plugin](/docs/plugins/codecall-plugin) — Hide large toolsets behind `codecall.search/describe/execute` meta-tools and sandboxed plans so models can orchestrate multiple tools without blowing up `list_tools`. diff --git a/docs/live/docs/servers/extensibility/providers.mdx b/docs/live/docs/servers/extensibility/providers.mdx new file mode 100644 index 00000000..ab9c197e --- /dev/null +++ b/docs/live/docs/servers/extensibility/providers.mdx @@ -0,0 +1,72 @@ +--- +title: Context Providers +sidebarTitle: Providers +slug: servers/extensibility/providers +icon: boxes +--- + +**Providers** are dependency-injected singletons (or scoped singletons) that your apps, tools, adapters, and plugins can use — e.g., config, DB pools, Redis clients, KMS, HTTP clients. + +They’re declared with `@Provider()` and registered at **server** or **app** scope. Resolution is hierarchical: **tool → app → server**. + +## Define a provider + +```ts +import { Provider, ProviderScope } from '@frontmcp/sdk'; + +@Provider({ + name: 'DbProvider', + description: 'Postgres connection pool', + scope: ProviderScope.GLOBAL, // GLOBAL | SESSION | REQUEST +}) +export class DbProvider { + /* create pool, expose query() etc. */ +} +``` + +### Scopes + +- **GLOBAL** (default): one instance per process/worker. Ideal for clients, pools, caches. +- **SESSION**: one instance per authenticated session. Use for per-user credentials or token-bound clients. +- **REQUEST**: one instance per inbound request. Use sparingly (e.g., per-request trace/state). + +## Register providers + +**Server-level** providers (available to all apps): + +```ts +@FrontMcp({ + info: { name: 'Suite', version: '1.0.0' }, + apps: [BillingApp, AnalyticsApp], + providers: [DbProvider, CacheProvider], +}) +export default class Server {} +``` + +**App-level** providers (override or add on top of server-level): + +```ts +@App({ + name: 'Billing', + providers: [BillingConfigProvider], +}) +export default class BillingApp {} +``` + + + You can register **class**, **value**, or **factory** providers. Factories are useful for async initialization or + composing other providers. + + +## Using providers from tools/plugins + +FrontMCP resolves providers for your executors and hooks. Keep your tool logic pure; read side-effects (DB, queues, secrets) via providers. + +- Prefer **GLOBAL** for shared clients. +- Use **SESSION** for user-bound clients (e.g., per-user API token). +- Reserve **REQUEST** for ephemeral state. + + + Provider injection/consumption follows your runtime’s DI rules. In general: register providers at the minimal scope + and let the framework resolve them for tools and hooks at execution time. + diff --git a/docs/live/docs/servers/prompts.mdx b/docs/live/docs/servers/prompts.mdx new file mode 100644 index 00000000..e7b792e0 --- /dev/null +++ b/docs/live/docs/servers/prompts.mdx @@ -0,0 +1,52 @@ +--- +title: Prompts +slug: servers/prompts +icon: message-lines +--- + +Prompts are **reusable prompt templates** with typed arguments. They return an MCP `GetPromptResult` for clients to render or send as messages to a model. + +## Minimal prompt + +```ts +import { Prompt } from '@frontmcp/sdk'; + +@Prompt({ + name: 'Summarize', + title: 'Summarize Text', + description: 'Create a concise summary', + arguments: [{ name: 'text', description: 'Input text', required: true }], +}) +export default class SummarizePrompt { + async execute(args: { text: string }) { + return { + description: 'Summarize the provided text', + messages: [ + { role: 'system', content: 'You are a concise assistant.' }, + { role: 'user', content: args.text }, + ], + }; + } +} +``` + +## Prompt metadata + +```ts +@Prompt({ + name: string, + title?: string, + description?: string, + arguments?: Array<{ + name: string; + description?: string; + required?: boolean; + }>, + icons?: Icon[], +}) +``` + +**Notes** + +- Prompts are discoverable and can be parameterized by clients. +- Use prompts to standardize instructions for common tasks across tools/apps. diff --git a/docs/live/docs/servers/resources.mdx b/docs/live/docs/servers/resources.mdx new file mode 100644 index 00000000..46501a21 --- /dev/null +++ b/docs/live/docs/servers/resources.mdx @@ -0,0 +1,72 @@ +--- +title: Resources +slug: servers/resources +icon: book-open +--- + +Resources provide **readable data** to load into the model’s context. Define fixed `Resource`s (a concrete `uri`) or `ResourceTemplate`s (an RFC 6570 `uriTemplate`). + +## Minimal resource + +```ts +import { Resource } from '@frontmcp/sdk'; + +@Resource({ + name: 'docs-home', + title: 'Docs Home', + uri: 'https://example.com/docs', + mimeType: 'text/html', +}) +export default class DocsHome { + async execute(uri: string) { + return { contents: [{ uri, text: 'Welcome to the docs!' }] }; + } +} +``` + +## Minimal resource template + +```ts +import { ResourceTemplate } from '@frontmcp/sdk'; + +@ResourceTemplate({ + name: 'repo-readme', + title: 'GitHub README', + uriTemplate: 'https://raw.githubusercontent.com/{owner}/{repo}/main/README.md', + mimeType: 'text/markdown', +}) +export default class RepoReadme {} +``` + +--- + +## Resource metadata + +```ts +@Resource({ + name: string, + title?: string, + uri: string, + description?: string, + mimeType?: string, + icons?: Icon[], +}) +``` + +## Resource template metadata + +```ts +@ResourceTemplate({ + name: string, + title?: string, + uriTemplate: string, // RFC 6570 + description?: string, + mimeType?: string, + icons?: Icon[], +}) +``` + +**Tips** + +- Use resources to preload long-form text, schemas, or docs the model should see before calling tools. +- Prefer templates when the shape is stable but the identifiers vary. diff --git a/docs/live/docs/servers/server.mdx b/docs/live/docs/servers/server.mdx new file mode 100644 index 00000000..17b16e4b --- /dev/null +++ b/docs/live/docs/servers/server.mdx @@ -0,0 +1,225 @@ +--- +title: The FrontMCP Server +sidebarTitle: Overview +description: The core FrontMCP server — start with the minimum and scale up with HTTP, sessions, logging, providers, and authentication. +icon: server +--- + +FrontMCP servers are defined with a single decorator, `@FrontMcp({ ... })`. This page shows the **minimal config** and then every **top-level option** you can use. Deep dives live in the pages listed under _Servers_. + +## Minimal server + +```ts +import { FrontMcp } from '@frontmcp/sdk'; +import MyApp from './my.app'; + +@FrontMcp({ + info: { name: 'My Server', version: '0.1.0' }, + apps: [MyApp], +}) +export default class Server {} +``` + +**Required:** + +- `info.name` (string) +- `info.version` (string) +- `apps` (at least one app) + +Everything else is optional with sensible defaults. + +--- + +## Full configuration (at a glance) + +```ts +@FrontMcp({ + /** Required */ + info: { + name: 'Expense MCP Server', + version: '1.0.0', + title?: 'Human title', + websiteUrl?: 'https://example.com', + icons?: Icon[], // MCP Icon[] + }, + apps: [/* App classes */], + + /** Optional */ + serve?: true, // default true (auto-boot) + splitByApp?: false, // app composition mode + providers?: [/* provider classes/factories/values */], + + http?: { + port?: 3001, // default 3001 + entryPath?: '', // MUST match PRM resourcePath in .well-known + hostFactory?: /* custom host */, + }, + + session?: { + sessionMode?: 'stateless' | 'stateful' | ((issuer) => ...), // default 'stateless' + transportIdMode?: 'uuid' | 'jwt' | ((issuer) => ...), // default 'uuid' + }, + + logging?: { + level?: LogLevel.Info, // Debug | VERBOSE | Info | Warn | Error | Off + enableConsole?: true, // default true + prefix?: string, + transports?: [/* custom log transports */], + }, + + /** Server-level default auth (omit if splitByApp: true) */ + auth?: ( + | { type: 'remote', name: string, baseUrl: string, ... } + | { type: 'local', id: string, name: string, ... } + ), +}) +``` + +--- + +## Composition mode + +FrontMCP can host **many apps**. Choose how they’re exposed: + +- **Multi-App (default)**: `splitByApp: false` + One server scope. You _may_ configure **server-level `auth`** and all apps inherit it (apps can still override with app-level auth). + +- **Split-By-App**: `splitByApp: true` + Each app is isolated under its own scope/base path (for example `/billing`). Streamable HTTP, the `/message` SSE endpoint, and OAuth issuers reuse that scope automatically. **Server-level `auth` is disallowed**; configure auth per app. (See _Authentication → Overview_.) + +If you’re offering multiple products or tenants, `splitByApp: true` gives clean separation and per-app auth. + +--- + +## HTTP transport + +```ts +http: { + port?: number; // default 3001 + entryPath?: string; // default ''; MUST match the PRM resourcePath in .well-known + hostFactory?: FrontMcpServer | ((cfg) => FrontMcpServer); +} +``` + +- **Port**: listening port for Streamable HTTP. +- **entryPath**: your MCP JSON-RPC entry (`''` or `'/mcp'`). Must align with discovery. +- **hostFactory**: advanced — provide/construct a custom host implementation. +- **Split-by-app scopes**: when `splitByApp` is enabled, clients hit `/` (for example `/mcp/billing`) and subscribe via `//message`; FrontMCP handles the prefixing. + +--- + +## Sessions & Transport IDs + +```ts +session: { + sessionMode?: 'stateless' | 'stateful' | ((issuer) => SessionMode); + transportIdMode?: 'uuid' | 'jwt' | ((issuer) => TransportIdMode); +} +``` + +- **sessionMode** (default **`'stateless'`**): + +- `'stateless'` → session data is carried in a signed/encrypted JWT. Simple, client-portable; no token refresh of nested providers. +- `'stateful'` → server-side store (e.g., Redis). Minimal JWTs, safer for nested tokens, supports refresh. + +- **transportIdMode** (default **`'uuid'`**): + +- `'uuid'` → per-node, strict transport identity. +- `'jwt'` → signed transport IDs for distributed setups; ties into session verification. + +You can supply functions for `sessionMode` / `transportIdMode` to decide per issuer. + +--- + +## Logging + +```ts +logging: { + level?: LogLevel; // default Info + enableConsole?: boolean; // default true + prefix?: string; + transports?: LogTransportType[]; // custom sinks via @FrontMcpLogTransport +} +``` + +Use custom log transports for shipping logs to external systems; console remains on by default. + +--- + +## Global providers + +```ts +providers: [ + /* Provider classes/factories/values */ +]; +``` + +Define DI-style singletons available to all apps (and their tools/plugins). Scopes are supported at the provider level (`GLOBAL`, `SESSION`, `REQUEST`). + +--- + +## Authentication (server level) + +Server-level `auth` sets the **default** auth for all apps (unless `splitByApp: true`, where auth must be per-app). + +### Remote OAuth (encapsulated external IdP) + +```ts +auth: { + type: 'remote', + name: 'frontegg', + baseUrl: 'https://auth.example.com', + dcrEnabled?: boolean, + clientId?: string | ((info) => string), // for non-DCR via local proxy + mode?: 'orchestrated' | 'transparent', + allowAnonymous?: boolean, + consent?: boolean, + scopes?: string[], + grantTypes?: ['authorization_code','refresh_token'], + authEndpoint?: string, + tokenEndpoint?: string, + registrationEndpoint?: string, + userInfoEndpoint?: string, + jwks?: JSONWebKeySet, + jwksUri?: string, +} +``` + +### Local OAuth (built-in AS) + +```ts +auth: { + type: 'local', + id: 'local', + name: 'Local Auth', + scopes?: string[], + grantTypes?: ['authorization_code','refresh_token'], + allowAnonymous?: boolean, // default true + consent?: boolean, // show tool/resource/prompt consent + jwks?: JSONWebKeySet, // inline keys (optional) + signKey?: JWK | Uint8Array // private key (optional; auto-gen if omitted) +} +``` + + + Apps can also define their own `auth` (and mark themselves `standalone`) to expose an isolated auth surface — useful + when mixing public and private apps under one server. + + +--- + +## Bootstrapping & discovery + +- **Version safety**: on boot, FrontMCP checks that all `@frontmcp/*` packages are aligned and throws a clear “version mismatch” error otherwise. + +If you disable `serve`, you’re responsible for calling the core bootstrap yourself. + +--- + +## Common starting points + +- **Single app, default everything**: minimal sample above. +- **Multiple apps, shared auth**: omit `splitByApp`, set server-level `auth`. +- **Isolated apps with per-app auth**: set `splitByApp: true`, configure `auth` in each app. + +Next up: learn how to structure **Apps**, **Tools**, **Resources**, and **Prompts** in the _Core Components_ section. diff --git a/docs/live/docs/servers/tools.mdx b/docs/live/docs/servers/tools.mdx new file mode 100644 index 00000000..c7330dda --- /dev/null +++ b/docs/live/docs/servers/tools.mdx @@ -0,0 +1,146 @@ +--- +title: Tools +slug: servers/tools +icon: wrench +--- + +Tools are **typed actions** your server can execute. They’re described with Zod schemas and exposed via MCP. Implement as a class with `@Tool({...})` or as a function via `tool()`. + +## Minimal tool (class) + +```ts +import { Tool } from '@frontmcp/sdk'; +import { z } from 'zod'; + +@Tool({ + name: 'greet', + description: 'Greets a user by name', + inputSchema: { name: z.string() }, +}) +export default class GreetTool { + async execute({ name }: { name: string }) { + return `Hello, ${name}!`; + } +} +``` + +Register it on an app: + +```ts +@App({ id: 'hello', name: 'Hello', tools: [GreetTool] }) +export default class HelloApp {} +``` + +## Inline tool (function builder) + +```ts +import { tool } from '@frontmcp/sdk'; +import { z } from 'zod'; + +export const Add = tool({ + name: 'add', + description: 'Add two numbers', + inputSchema: { a: z.number(), b: z.number() }, +})((input) => input.a + input.b); +``` + +Add to app: + +```ts +@App({ name: 'Calc', tools: [Add] }) +class CalcApp {} +``` + +--- + +## Tool metadata + +```ts +@Tool({ + id?: string, + name: string, + description?: string, + inputSchema: { [key: string]: z.ZodTypeAny } | z.ZodObject, + rawInputSchema?: JSONSchema7, + outputSchema?: + | 'string' + | 'number' + | 'boolean' + | 'date' + | 'image' + | 'audio' + | 'resource' + | 'resource_link' + | z.ZodTypeAny + | readonly ( + | 'string' + | 'number' + | 'boolean' + | 'date' + | 'image' + | 'audio' + | 'resource' + | 'resource_link' + | z.ZodTypeAny + )[], + tags?: string[], + annotations?: { + title?: string, + readOnlyHint?: boolean, + destructiveHint?: boolean, + idempotentHint?: boolean, + openWorldHint?: boolean, + }, + hideFromDiscovery?: boolean, // default false +}) +``` + +**Notes** + +- `annotations` hint model & UI behavior (read-only, idempotent, etc.). +- `hideFromDiscovery` keeps a tool callable but off `tool/list`. +- Tools can attach **per-tool hooks** (see _Advanced → Hooks_). +- `rawInputSchema` lets you share a JSON Schema version of the input (handy for the Inspector or adapters) while still passing a raw Zod shape to `inputSchema`. + +### Input & output schema shapes + +`inputSchema` can be a full `z.object({...})` or just the raw shape (`{ name: z.string() }`). The SDK wraps raw shapes into an object internally, so you can keep declarations terse and still get proper validation. + +Set `rawInputSchema` when you also want to expose the JSON Schema equivalent of your input — the OpenAPI adapter and Inspector will reuse it without needing to reverse-engineer your Zod types. + +`outputSchema` accepts: + +- Literal primitives (`'string'`, `'number'`, `'boolean'`, `'date'`) when you return scalars. +- `'image'`, `'audio'`, `'resource'`, or `'resource_link'` when you emit MCP resource descriptors. +- Any Zod schema (objects, unions, arrays, discriminated unions, etc.). +- An array of the above to build tuple-like responses; each entry becomes a separate structured content item in the MCP response. + +```ts +@Tool({ + name: 'add', + description: 'Add two numbers and echo the math', + inputSchema: { a: z.number(), b: z.number() }, + rawInputSchema: { + type: 'object', + properties: { + a: { type: 'number' }, + b: { type: 'number' }, + }, + required: ['a', 'b'], + }, + outputSchema: ['string', 'number'], +}) +export default class AddTool { + async execute({ a, b }: { a: number; b: number }) { + const result = a + b; + return [`${a} + ${b} = ${result}`, result]; + } +} +``` + +--- + +## Return values + +- Return primitives, structured objects, tuples, or MCP resources. When `outputSchema` is provided (literal, array, or Zod), the SDK validates the response and propagates the right metadata to clients. +- Errors are surfaced via MCP error responses; you can also throw typed errors inside executors. diff --git a/docs/docs/v/0.2/adapters/openapi-adapter.mdx b/docs/live/docs/v/0.1/adapters/openapi-adapter.mdx similarity index 100% rename from docs/docs/v/0.2/adapters/openapi-adapter.mdx rename to docs/live/docs/v/0.1/adapters/openapi-adapter.mdx diff --git a/docs/live/docs/v/0.1/deployment/local-dev-server.mdx b/docs/live/docs/v/0.1/deployment/local-dev-server.mdx new file mode 100644 index 00000000..06ebefaf --- /dev/null +++ b/docs/live/docs/v/0.1/deployment/local-dev-server.mdx @@ -0,0 +1,169 @@ +--- +title: Local Dev Server +slug: deployment/local-dev-server +icon: computer +--- + +Run your FrontMCP server locally with hot-reload and verify it with the **FrontMCP Inspector** (zero setup). + +## Prerequisites + +- **Node.js ≥ 22** +- **npm ≥ 10** (pnpm/yarn also supported) + +--- + +## Quick start + +### Option A — New project + +Creates a folder and scaffolds everything for you. + + + ```bash universal + npx frontmcp create my-app + ``` + + + +### Option B — Existing project + +Install and initialize in your current repo: + +```bash universal npm i -D frontmcp @types/node@^20 npx frontmcp init ``` + +`init` adds the required scripts and updates **tsconfig.json** automatically. + +--- + +## Package scripts + +After `create` or `init`, your `package.json` will include: + +```json +{ + "scripts": { + "dev": "frontmcp dev", + "build": "frontmcp build", + "inspect": "frontmcp inspector", + "doctor": "frontmcp doctor" + } +} +``` + +- `frontmcp dev` — run in watch mode with type-checks +- `frontmcp build` — compile to `./dist` (override with `--out-dir`) +- `frontmcp inspector` — launches **@modelcontextprotocol/inspector** with zero setup +- `frontmcp doctor` — verifies Node/npm versions and project configuration + +--- + +## Recommended tsconfig + +`init` writes this for you, but if you prefer to manage it manually: + +```json title="tsconfig.json" +{ + "compilerOptions": { + "target": "es2021", + "module": "esnext", + "lib": ["es2021"], + "moduleResolution": "bundler", + "rootDir": "src", + "outDir": "dist", + "strict": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "sourceMap": true + }, + "include": ["src/**/*.ts"], + "exclude": ["**/*.test.ts", "**/__tests__/**"] +} +``` + +--- + +## Minimal app + +> You can import from `@frontmcp/sdk` directly; no extra install needed. + +**src/main.ts** + +```ts +import 'reflect-metadata'; +import { FrontMcp, LogLevel } from '@frontmcp/sdk'; +import HelloApp from './hello.app'; + +@FrontMcp({ + info: { name: 'Hello MCP', version: '0.1.0' }, + apps: [HelloApp], + http: { port: Number(process.env.PORT) || 3000 }, + logging: { level: LogLevel.Info }, +}) +export default class Server {} +``` + +**src/hello.app.ts** + +```ts +import { App } from '@frontmcp/sdk'; +import Greet from './tools/greet.tool'; + +@App({ id: 'hello', name: 'Hello', tools: [Greet] }) +export default class HelloApp {} +``` + +**src/tools/greet.tool.ts** + +```ts +import { tool } from '@frontmcp/sdk'; +import { z } from 'zod'; + +export default tool({ + name: 'greet', + description: 'Greets a user by name', + // New style: pass Zod fields directly (no z.object wrapper) + inputSchema: { name: z.string() }, +})(({ name }) => `Hello, ${name}!`); +``` + +--- + +## Run the dev server + +```bash universal npm run dev ``` + +Your server will start (default `http://localhost:3000`). The console will print the MCP endpoint. + +--- + +## Inspect locally (zero setup) + +Launch the **FrontMCP Inspector** to exercise tools and messages in a friendly UI: + +```bash universal npm run inspect ``` + +- This runs `npx @modelcontextprotocol/inspector` behind the scenes. +- Point it at your local server URL printed by `dev` (e.g., `http://localhost:3000`). +- Try calling `greet` and watch responses stream back in real time. + +--- + +## Troubleshooting + +- **Check configuration** + +```bash +npm run doctor +``` + +Ensures Node/npm versions, entry detection, and `tsconfig.json` are correct. + +- **Entry detection** + The CLI looks for `package.json.main`; if missing, it falls back to `src/main.ts`. If no entry is found, it will tell you how to create one. + +- **Type errors in dev** + The `dev` command performs async type-checks while watching your files, so you’ll see issues immediately without stopping the server. diff --git a/docs/live/docs/v/0.1/deployment/production-build.mdx b/docs/live/docs/v/0.1/deployment/production-build.mdx new file mode 100644 index 00000000..54ef020e --- /dev/null +++ b/docs/live/docs/v/0.1/deployment/production-build.mdx @@ -0,0 +1,73 @@ +--- +title: Production Build +slug: deployment/production-build +icon: building +--- + +Build a compact Node artifact and run it behind a process manager / reverse proxy. + +## Build + + + + ```bash npm + npm run build + ``` + + ```bash yarn + yarn build + ``` + + ```bash pnpm + pnpm build + ``` + + + +This compiles TypeScript to `dist/` using `tsconfig.build.json`. + +## Start + + + + ```bash npm + NODE_ENV=production PORT=8080 npm start + ``` + + ```bash yarn + NODE_ENV=production PORT=8080 yarn start + ``` + + ```bash pnpm + NODE_ENV=production PORT=8080 pnpm start + ``` + + + +## Recommended runtime setup + +- Use a **process manager** (PM2, systemd) for restarts and logs. +- Put a **reverse proxy** (NGINX, Traefik, Caddy) in front for TLS and path routing. +- Pin matching versions of all `@frontmcp/*` packages. + +### Example NGINX snippet + +```nginx +server { + listen 443 ssl; + server_name mcp.example.com; + + location /mcp/ { + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto https; + proxy_pass http://127.0.0.1:8080/mcp/; + } +} +``` + +## Troubleshooting + +- **Version mismatch** at boot → align all `@frontmcp/*` versions and reinstall. +- **No decorators** working → ensure `experimentalDecorators` + `emitDecoratorMetadata` and `import 'reflect-metadata'` at the top of `main.ts`. +- **Port conflicts** → set `http.port` in `@FrontMcp` or use `PORT` env. diff --git a/docs/live/docs/v/0.1/getting-started/installation.mdx b/docs/live/docs/v/0.1/getting-started/installation.mdx new file mode 100644 index 00000000..3a0980a3 --- /dev/null +++ b/docs/live/docs/v/0.1/getting-started/installation.mdx @@ -0,0 +1,109 @@ +--- +title: Installation +slug: getting-started/installation +icon: arrow-down-to-line +--- + +## Prerequisites + +- **Node.js ≥ 22** +- **npm ≥ 10** (pnpm / yarn work too) +- TypeScript project (the init command can set this up) + +--- + +## Option A — Create a new project + +Use the built-in project generator. It **requires** a project name and will create a new folder under your current directory. + + +```bash universal +npx frontmcp create my-app +``` + +```bash pnpm +pnpm dlx frontmcp create my-app +``` + +```bash yarn +yarn dlx frontmcp create my-app +``` + + + +This will: + +- scaffold a FrontMCP project in `./my-app` +- configure `tsconfig.json` for decorators and modern ESM +- generate a `package.json` with helpful scripts +- install required dev dependencies (e.g. TypeScript, tsx, zod, reflect-metadata) + +--- + +## Option B — Add to an existing project + +Install the CLI and Node types (FrontMCP bundles compatible `@frontmcp/sdk` and `@frontmcp/core` internally—no separate install needed). + + + ```bash npm npm i -D frontmcp @types/node@^20 ``` ```bash pnpm pnpm add -D frontmcp @types/node@^20 ``` ```bash yarn + yarn add -D frontmcp @types/node@^20 ``` + + +Then initialize FrontMCP in your project root: + +```bash +npx frontmcp init +``` + +`init` will: + +- add/update **scripts** in your `package.json` +- ensure your **tsconfig.json** includes required compiler options +- verify a sensible project layout + +--- + +## Package scripts + +After `create` or `init`, you’ll have these scripts: + +```json +{ + "scripts": { + "dev": "frontmcp dev", + "build": "frontmcp build", + "inspect": "frontmcp inspector", + "doctor": "frontmcp doctor" + } +} +``` + +**What they do** + +- `frontmcp dev` — run your server in watch mode (tsx) +- `frontmcp build` — compile your entry with TypeScript (outputs to `./dist` by default) +- `frontmcp inspector` — launch the MCP Inspector (`npx @modelcontextprotocol/inspector`) +- `frontmcp doctor` — validate Node/npm versions, tsconfig, and project setup + +--- + +## Verify your setup + +Run: + +```bash +npm run doctor +``` + + + If anything is missing or misconfigured (Node/npm versions, `tsconfig.json`, scripts), **doctor** will tell you + exactly what to fix. + + +--- + +## Next steps + +- Start developing: `npm run dev` +- Build for distribution: `npm run build` +- Explore tools and messages live: `npm run inspect` diff --git a/docs/live/docs/v/0.1/getting-started/quickstart.mdx b/docs/live/docs/v/0.1/getting-started/quickstart.mdx new file mode 100644 index 00000000..dd078826 --- /dev/null +++ b/docs/live/docs/v/0.1/getting-started/quickstart.mdx @@ -0,0 +1,91 @@ +--- +title: Quickstart +slug: getting-started/quickstart +icon: rocket-launch +--- + +Welcome! This guide will help you quickly set up FrontMCP and run your first MCP server. + +If you haven't already installed FrontMCP, follow the [installation instructions](./installation). + +## 1) Create your FrontMCP server + +```ts +// src/main.ts +import { FrontMcp, LogLevel } from '@frontmcp/sdk'; +import HelloApp from './hello.app'; + +@FrontMcp({ + info: { name: 'Hello MCP', version: '0.1.0' }, + apps: [HelloApp], + http: { port: 3000 }, + logging: { level: LogLevel.INFO }, +}) +export default class HelloServer {} +``` + +## 2) Define an app + +```ts +// src/hello.app.ts +import { App } from '@frontmcp/sdk'; +import GreetTool from './tools/greet.tool'; + +@App({ + id: 'hello', + name: 'Hello App', + tools: [GreetTool], +}) +export default class HelloApp {} +``` + +## 3) Add your first tool + +```ts +// src/tools/greet.tool.ts +import { tool } from '@frontmcp/sdk'; +import { z } from 'zod'; + +export default tool({ + name: 'greet', + description: 'Greets a user by name', + inputSchema: { name: z.string() }, +})(async ({ name }) => { + return { + result: `Hello, ${name}!`, + }; +}); +``` + +## 4) Run it + +Add scripts: + +```json +{ + "scripts": { + "dev": "tsx src/main.ts", + "build": "tsc -p tsconfig.build.json", + "start": "node dist/apps/hello/main.js" + } +} +``` + +Run: + +```bash +npm run dev +# Server listening on http://localhost:3000 +``` + + + FrontMCP speaks **MCP Streamable HTTP**. You can connect any MCP-capable client and call the `greet` tool with + `{"name": "Ada"}`. + + +## What’s next + +- Add auth (local or remote OAuth) +- Use **adapters** to load tools from OpenAPI +- Enable **plugins** like transparent caching +- Wire **hooks** for logging, rate limits, or request transforms diff --git a/docs/live/docs/v/0.1/getting-started/welcome.mdx b/docs/live/docs/v/0.1/getting-started/welcome.mdx new file mode 100644 index 00000000..456beb88 --- /dev/null +++ b/docs/live/docs/v/0.1/getting-started/welcome.mdx @@ -0,0 +1,64 @@ +--- +title: Welcome to FrontMCP v0.1 +sidebarTitle: Welcome to FrontMCP +slug: getting-started/welcome +icon: hand-wave +description: The TypeScript way to build MCP servers with decorators, DI, and Streamable HTTP. +--- + +**FrontMCP is the TypeScript-first framework for MCP.** You write clean, typed code; FrontMCP handles the protocol, transport, and execution flow. + +```ts +import { FrontMcp, App, tool } from '@frontmcp/sdk'; +import { z } from 'zod'; + +const AddTool = tool({ + name: 'add', + description: 'Add two numbers', + inputSchema: { a: z.number(), b: z.number() }, +})(async (input, ctx) => { + return { + result: input.a + input.b, + }; +}); + +@App({ + id: 'calc', + name: 'Calculator', + tools: [AddTool], +}) +class CalcApp {} + +@FrontMcp({ + info: { name: 'Demo 🚀', version: '0.1.0' }, + apps: [CalcApp], + auth: { + type: 'remote', + name: 'my-oauth', + baseUrl: 'https://oauth.example.com', + }, +}) +export default class Server {} +``` + +### Why FrontMCP? + +- 🧑‍💻 **TypeScript-native DX** — decorators, Zod, and strong typing end-to-end +- 🧠 **Scoped invoker + DI** — secure, composable execution with hooks +- 🧩 **Adapters & Plugins** — extend your server without boilerplate +- 🔌 **Spec-aligned transport** — Streamable HTTP for modern MCP clients + +### Core concepts + +- **Server** — entry point via `@FrontMcp({...})` +- **App** — a logical bundle of tools via `@App({...})` +- **Tool** — typed units of work via `@Tool({...})` +- **Hooks** — cross-cutting behaviors (auth, logging, rate limits) +- **Adapters/Plugins** — load tools dynamically; enrich behavior + + + FrontMCP follows MCP protocol principles and **secured transport requirements**. You get sessions, streaming, and + validation out of the box—no custom wiring needed. + + +**Build something real**: jump to the [Quickstart](./quickstart) or set up your workspace in [Installation](./installation). diff --git a/docs/live/docs/v/0.1/servers/apps.mdx b/docs/live/docs/v/0.1/servers/apps.mdx new file mode 100644 index 00000000..ceb5ed19 --- /dev/null +++ b/docs/live/docs/v/0.1/servers/apps.mdx @@ -0,0 +1,106 @@ +--- +title: Apps +slug: servers/apps +icon: cube +--- + +Apps are how you **group capabilities** in FrontMCP. Each app can contribute **tools**, **resources**, **prompts**, plus **providers**, **adapters**, and **plugins**. Apps may run _locally_ (in this process) or be _remote_ (URL/worker). + +## Minimal local app + +```ts +import { App } from '@frontmcp/sdk'; + +@App({ + id: 'hello', + name: 'Hello App', + tools: [], +}) +export default class HelloApp {} +``` + +Add it to your server: + +```ts +@FrontMcp({ info: { name: 'Demo', version: '0.1.0' }, apps: [HelloApp] }) +export default class Server {} +``` + +--- + +## Local app options + +```ts +@App({ + id?: string, + name: string, + description?: string, + providers?: ProviderType[], + authProviders?: AuthProviderType[], + plugins?: PluginType[], + adapters?: AdapterType[], + tools?: ToolType[], + resources?: ResourceType[], + prompts?: PromptType[], + auth?: AuthOptions, // app‑default auth (overrides server auth) + standalone?: 'includeInParent' | boolean, // isolate scope / expose separately +}) +``` + +**Scoping & auth** + +- If the server uses **`splitByApp: true`**, each app is isolated and **must** configure its own auth (server-level `auth` is disallowed). +- `standalone: true` makes the app expose its own scope/entry; `'includeInParent'` lists it under the parent while keeping isolation. + +**Dependency resolution** + +- Providers resolve **tool → app → server**. +- Plugins/adapters attach at app scope; generated items inherit the app’s policies and provenance. + +--- + +## Remote apps + +Define an app that proxies to a **worker file** or a **URL**: + +```ts +@App({ + name: 'Remote CRM', + urlType: 'url', // 'url' | 'worker' + url: 'https://crm.example.com/mcp', + auth: { type: 'remote', name: 'crm-idp', baseUrl: 'https://idp.example.com' }, + standalone: true, +}) +export default class CrmApp {} +``` + +**Fields** + +```ts +{ + id?: string; + name: string; + description?: string; + urlType: 'worker' | 'url'; + url: string; + auth?: AuthOptions; + standalone?: 'includeInParent' | boolean; +} +``` + +--- + +## Example: app with adapter + providers + +```ts +@App({ + id: 'billing', + name: 'Billing', + providers: [DbProvider, CacheProvider], + adapters: [ + // e.g. OpenAPI adapter that generates tools/resources from a spec + BillingOpenApiAdapter, + ], +}) +export default class BillingApp {} +``` diff --git a/docs/live/docs/v/0.1/servers/authentication/local.mdx b/docs/live/docs/v/0.1/servers/authentication/local.mdx new file mode 100644 index 00000000..de2e31a7 --- /dev/null +++ b/docs/live/docs/v/0.1/servers/authentication/local.mdx @@ -0,0 +1,49 @@ +--- +title: Local OAuth +slug: servers/authentication/local +icon: window +--- + +FrontMCP ships a **built‑in OAuth provider** for first‑party scenarios for development. + +### Configuration + +```ts +auth: { + type: 'local', + id: 'local', + name: 'Local Auth', + scopes?: string[], + grantTypes?: ('authorization_code' | 'refresh_token')[], + allowAnonymous?: boolean, // default true + consent?: boolean, + jwks?: JSONWebKeySet, // inline JWKS (optional) + signKey?: JWK | Uint8Array // private key (optional; auto‑generated if omitted) +} +``` + +### Example (per app, split‑by‑app server) + +```ts +@FrontMcp({ + info: { name: 'Workspace', version: '1.0.0' }, + auth: { type: 'local' }, + apps: [DocsApp, MailApp], + splitByApp: true, +}) +export default class Server {} + +@App({ + name: 'Docs', +}) +export default class DocsApp {} +``` + +When using `splitByApp: true`, define auth per app; server‑level `auth` is not allowed. + +--- + +## Keys & JWKS + +- Omit `signKey` to auto‑generate keys (exposed via the local JWKS endpoint). +- Provide `jwks` or `signKey` to pin keys for stable environments. diff --git a/docs/live/docs/v/0.1/servers/authentication/overview.mdx b/docs/live/docs/v/0.1/servers/authentication/overview.mdx new file mode 100644 index 00000000..349c09d4 --- /dev/null +++ b/docs/live/docs/v/0.1/servers/authentication/overview.mdx @@ -0,0 +1,79 @@ +--- +title: Authentication +sidebarTitle: Overview +slug: servers/authentication/overview +icon: users-between-lines +--- + +FrontMCP supports **Remote OAuth** (connect to an external IdP) and a built‑in **Local OAuth** provider. You can configure auth **at the server** or **per app**: + +- **Server‑level auth**: a default provider for all apps (only when `splitByApp: false`). +- **App‑level auth**: each app defines its own provider; required when `splitByApp: true`. + +Auth drives **session creation**, **token propagation**, and optional **consent** for tools/resources/prompts. + + + If an external provider **doesn’t support Dynamic Client Registration (DCR)**, FrontMCP can front it with a **local + OAuth proxy** that registers/holds the client on your behalf. See *Remote OAuth → Proxy*. + + +--- + +## Where to configure auth + +**Server (multi‑app, shared auth):** + +```ts +@FrontMcp({ + info: { name: 'Expense', version: '1.0.0' }, + apps: [BillingApp, AnalyticsApp], + auth: { type: 'remote', name: 'frontegg', baseUrl: 'https://idp.example.com' }, +}) +export default class Server {} +``` + +**Per app (isolated scopes):** + +```ts +@App({ name: 'Billing', auth: { type: 'local', id: 'local', name: 'Local Auth' } }) +export default class BillingApp {} + +@FrontMcp({ info: { name: 'Suite', version: '1.0.0' }, apps: [BillingApp, AnalyticsApp], splitByApp: true }) +export default class Server {} +``` + +--- + +## Consent & scopes + +Enable consent to let users select the **tools/resources/prompts** they grant, producing a **scoped token**. + +```ts +auth: { + type: 'remote', + name: 'frontegg', + baseUrl: 'https://idp.example.com', + consent: true +} +``` + +You can still send classic OAuth scopes via `scopes: string[]`. + +--- + +## Sessions & transport + +Auth integrates with server sessions (see _Server Overview → Sessions_): + +- `session.sessionMode`: `stateful` keeps tokens server‑side (recommended for nested providers); `stateless` embeds in JWT (simpler). +- `session.transportIdMode`: `uuid` (per‑node) or `jwt` (signed transport IDs for distributed setups). + + + Use **stateful** sessions when working with short‑lived upstream tokens—you’ll get refresh without round‑trips. + + +--- + +## Discovery & well‑known + +Remote providers typically self‑describe via `/.well-known/oauth-authorization-server` and `/.well-known/jwks.json`. You may override endpoints and JWKS inline when needed. diff --git a/docs/live/docs/v/0.1/servers/authentication/remote-proxy.mdx b/docs/live/docs/v/0.1/servers/authentication/remote-proxy.mdx new file mode 100644 index 00000000..67194067 --- /dev/null +++ b/docs/live/docs/v/0.1/servers/authentication/remote-proxy.mdx @@ -0,0 +1,40 @@ +--- +title: Remote OAuth — Proxy +slug: servers/authentication/remote-proxy +icon: share +--- + +Some IdPs don’t support **Dynamic Client Registration (DCR)**. FrontMCP can front them with a **local OAuth proxy** that: + +1. Exposes OAuth endpoints locally under your server/app scope. +2. Registers a client using your provided `clientId` (or derives one via function). +3. Issues/validates tokens while exchanging with the upstream IdP. + +### Configure + +```ts +@FrontMcp({ + info: { name: 'Suite', version: '1.0.0' }, + apps: [SalesApp], + auth: { + type: 'remote', + name: 'legacy-idp', + baseUrl: 'https://legacy-idp.example.com', + dcrEnabled: false, + clientId: 'my-preprovisioned-client', + consent: true, + }, +}) +export default class Server {} +``` + + + At runtime, the local proxy maintains client registration and keys while delegating user authentication to the + upstream IdP. + + +--- + +## Endpoint overrides + +If your IdP requires custom endpoints or static keys, set `authEndpoint`, `tokenEndpoint`, `registrationEndpoint`, `userInfoEndpoint`, `jwks`, or `jwksUri` directly. diff --git a/docs/live/docs/v/0.1/servers/authentication/remote.mdx b/docs/live/docs/v/0.1/servers/authentication/remote.mdx new file mode 100644 index 00000000..efef6a50 --- /dev/null +++ b/docs/live/docs/v/0.1/servers/authentication/remote.mdx @@ -0,0 +1,61 @@ +--- +title: Remote OAuth +slug: servers/authentication/remote +icon: user-shield +--- + +Use a remote identity provider (IdP) like Frontegg, Auth0, Azure Entra, etc. + +### Configuration + +```ts +auth: { + type: 'remote', + name: 'frontegg', + baseUrl: 'https://auth.example.com', + dcrEnabled?: boolean, + clientId?: string | ((clientInfo: { clientId: string }) => string), + mode?: 'orchestrated' | 'transparent', + allowAnonymous?: boolean, + consent?: boolean, + scopes?: string[], + grantTypes?: ('authorization_code' | 'refresh_token')[], + authEndpoint?: string, + tokenEndpoint?: string, + registrationEndpoint?: string, + userInfoEndpoint?: string, + jwks?: JSONWebKeySet, + jwksUri?: string, +} +``` + +### Example (server‑level) + +```ts +@FrontMcp({ + info: { name: 'Expense MCP', version: '1.0.0' }, + apps: [ExpenseApp], + auth: { type: 'remote', name: 'frontegg', baseUrl: 'https://auth.example.com', consent: true }, +}) +export default class Server {} +``` + +### Example (per app) + +```ts +@App({ + name: 'CRM', + auth: { type: 'remote', name: 'crm', baseUrl: 'https://idp.example.com', scopes: ['openid', 'email'] }, + standalone: true, +}) +export default class CrmApp {} +``` + +Use `standalone: true` to expose the app’s auth surface under its own scope/entry. + +--- + +## DCR vs non‑DCR + +- **`dcrEnabled: true`** → FrontMCP registers the client dynamically at the IdP. +- **`dcrEnabled: false`** → supply `clientId` and use a **local OAuth proxy** to handle registration/storage. See _Remote OAuth → Proxy_. diff --git a/docs/live/docs/v/0.1/servers/authentication/token.mdx b/docs/live/docs/v/0.1/servers/authentication/token.mdx new file mode 100644 index 00000000..344381d4 --- /dev/null +++ b/docs/live/docs/v/0.1/servers/authentication/token.mdx @@ -0,0 +1,40 @@ +--- +title: Token & Session +slug: servers/authentication/token +icon: key +--- + +Authorization determines **what** a token may do. + +## OAuth scopes + +Provide standard scopes to external IdPs: + +```ts +auth: { + type: 'remote', + name: 'frontegg', + baseUrl: 'https://idp.example.com', + scopes: ['openid','profile','email'] +} +``` + +## Tool/resource/prompt consent + +Set `consent: true` to display a **post‑login consent** listing your registered **tools/resources/prompts**. The issued access token includes the selected grants. + +## Modes (Remote OAuth) + +Use the `mode` field to reflect deployment topology: + +- `transparent` (default): your server acts as a regular confidential client. +- `orchestrated`: gateway coordinates multiple apps/providers under one umbrella token (used in advanced multi‑app setups). + +When `splitByApp: true`, configure auth **per app**; server‑level `auth` is disallowed. + +--- + +## Token lifetimes & sessions + +- **Stateful sessions**: tokens are encrypted server‑side; clients hold a lightweight reference. Smooth refresh. +- **Stateless sessions**: tokens ride inside JWT; simple but no silent refresh of upstream tokens. diff --git a/docs/live/docs/v/0.1/servers/extensibility/adapters.mdx b/docs/live/docs/v/0.1/servers/extensibility/adapters.mdx new file mode 100644 index 00000000..682d62b2 --- /dev/null +++ b/docs/live/docs/v/0.1/servers/extensibility/adapters.mdx @@ -0,0 +1,46 @@ +--- +title: Adapters Transformers +sidebarTitle: Adapters +slug: servers/extensibility/adapters +icon: plug +--- + +**Adapters** convert **external definitions or systems** into FrontMCP components (tools, resources, prompts). Common examples: + +- **OpenAPI** → generate typed **tools** from REST endpoints +- **File system / blobs** → expose documents as **resources** +- **Custom backends** → synthesize prompts/resources from proprietary schemas + +## Define an adapter + +```ts +import { Adapter } from '@frontmcp/sdk'; + +@Adapter({ + name: 'Billing OpenAPI Loader', + description: 'Generates tools/resources from the Billing API spec', +}) +export default class BillingOpenApiAdapter { + // implement adapter-specific logic to register generated tools/resources/prompts +} +``` + +Register the adapter on an app: + +```ts +@App({ + name: 'Billing', + adapters: [BillingOpenApiAdapter], +}) +export default class BillingApp {} +``` + +### Behavior + +- Generated items inherit the **app’s plugins, providers, and policies** and are tagged with provenance. +- Adapters can contribute **tools**, **resources**, and **prompts** simultaneously. + + + Keep adapters **pure**: read a spec or source, generate components, and let app-level plugins handle cross-cutting + concerns like auth, rate limiting, or caching. + diff --git a/docs/live/docs/v/0.1/servers/extensibility/logging.mdx b/docs/live/docs/v/0.1/servers/extensibility/logging.mdx new file mode 100644 index 00000000..88169522 --- /dev/null +++ b/docs/live/docs/v/0.1/servers/extensibility/logging.mdx @@ -0,0 +1,191 @@ +--- +title: Logging Transports +sidebarTitle: Logging +slug: servers/extensibility/logging +icon: receipt +description: Create and register custom log transports for FrontMCP. +--- + +FrontMCP logging is extensible. In addition to the default console logger, you can register one or more custom transports via the server config. + +## Register a transport + +```ts +@FrontMcp({ + info: { name: 'Demo', version: '0.1.0' }, + apps: [App], + logging: { + level: LogLevel.Info, + enableConsole: true, // set false to disable the built‑in console transport + transports: [StructuredJsonTransport, HttpBatchTransport], + }, +}) +export default class Server {} +``` + +## Transport contract + +A transport is a class decorated with `@LogTransport({...})` that implements `LogTransportInterface`. + +```ts +export interface LogRecord { + level: LogLevel; + levelName: string; + message: string; + args: unknown[]; + timestamp: Date; + prefix: string; +} + +export type LogFn = (msg?: any, ...args: any[]) => void; + +export abstract class LogTransportInterface { + abstract log(rec: LogRecord): void; +} +``` + +The framework filters by the configured `logging.level` before calling transports. + +> Transports should never throw. Handle errors internally and keep I/O non‑blocking (use buffering/batching for remote sinks). + +## Built‑in: Console + +The default console transport formats messages with ANSI (when TTY) and falls back to plain text otherwise. + +```ts +@LogTransport({ + name: 'ConsoleLogger', + description: 'Default console logger', +}) +export class ConsoleLogTransportInstance extends LogTransportInterface { + log(rec: LogRecord): void { + const fn = this.bind(rec.level, rec.prefix); + fn(String(rec.message), ...rec.args); + } + // ...see source for details +} +``` + +Disable it by setting `enableConsole: false` in your server config. + +## Example: Structured JSON (JSONL) + +Emit machine‑readable logs (one JSON per line). Useful for file shipping agents or centralized logging. + +```ts +import { LogTransport, LogTransportInterface, LogRecord } from '@frontmcp/sdk'; + +@LogTransport({ + name: 'StructuredJsonTransport', + description: 'Writes JSONL log records to stdout', +}) +export class StructuredJsonTransport extends LogTransportInterface { + log(rec: LogRecord): void { + try { + const payload = { + ts: rec.timestamp.toISOString(), + level: rec.levelName, // e.g. INFO + levelValue: rec.level, // numeric + prefix: rec.prefix || undefined, + msg: stringify(rec.message), + args: rec.args?.map(stringify), + }; + // Avoid console formatting; write raw line + process.stdout.write(JSON.stringify(payload) + '\n'); + } catch (err) { + // Never throw from a transport + } + } +} + +function stringify(x: unknown) { + if (x instanceof Error) { + return { name: x.name, message: x.message, stack: x.stack }; + } + try { + return typeof x === 'string' ? x : JSON.parse(JSON.stringify(x)); + } catch { + return String(x); + } +} +``` + +Register it: + +```ts +logging: { level: LogLevel.Info, enableConsole: false, transports: [StructuredJsonTransport] } +``` + +## Example: HTTP batch transport (non‑blocking) + +Buffer records in memory and POST them in batches. Implements basic retry with backoff. + +```ts +import { LogTransport, LogTransportInterface, LogRecord } from '@frontmcp/sdk'; + +@LogTransport({ + name: 'HttpBatchTransport', + description: 'POST logs in batches', +}) +export class HttpBatchTransport extends LogTransportInterface { + private queue: any[] = []; + private timer: NodeJS.Timeout | null = null; + private flushing = false; + private readonly maxBatch = 50; + private readonly flushMs = 1000; + + constructor(private endpoint = process.env.LOG_ENDPOINT || 'https://logs.example.com/ingest') { + super(); + } + + log(rec: LogRecord): void { + this.queue.push({ + ts: rec.timestamp.toISOString(), + lvl: rec.levelName, + pfx: rec.prefix || undefined, + msg: String(rec.message), + args: safeArgs(rec.args), + }); + if (this.queue.length >= this.maxBatch) this.flush(); + else if (!this.timer) this.timer = setTimeout(() => this.flush(), this.flushMs); + } + + private async flush() { + // TODO: Implement batch flush logic + // - Extract batch from queue + // - POST to this.endpoint + // - Handle errors and implement retry with backoff + // - Reset flushing flag and timer + } +} + +function safeArgs(a: unknown[]) { + return (a || []).map((x) => (x instanceof Error ? { name: x.name, message: x.message, stack: x.stack } : x)); +} +``` + +Notes: + +- Keep batches small and time‑bounded; avoid blocking the event loop. +- On process exit, you may add a `beforeExit`/`SIGTERM` handler to flush synchronously. + +## Prefixes and levels + +- `logging.prefix` adds a static scope tag to all records (e.g., app name or environment). +- Transports receive `rec.level` and `rec.levelName`; the framework already filtered below‑level logs. + +```ts +logging: { + level: LogLevel.Warn, + prefix: 'billing‑edge', + transports: [StructuredJsonTransport] +} +``` + +## Best practices + +- Never throw from `log()`; swallow and self‑heal. +- Avoid heavy sync I/O; prefer buffering and async flush. +- Redact sensitive fields before emit (tokens, PII). +- Serialize `Error` objects explicitly (name, message, stack). +- Apply backpressure: if the sink is down, drop or sample rather than blocking the server. diff --git a/docs/live/docs/v/0.1/servers/extensibility/plugins.mdx b/docs/live/docs/v/0.1/servers/extensibility/plugins.mdx new file mode 100644 index 00000000..6c9854cb --- /dev/null +++ b/docs/live/docs/v/0.1/servers/extensibility/plugins.mdx @@ -0,0 +1,51 @@ +--- +title: Plugin Extensions +sidebarTitle: Plugins +slug: servers/extensibility/plugins +icon: puzzle-piece +--- + +**Plugins** add cross-cutting behavior and can also contribute components. Typical uses: auth/session helpers, PII filtering, tracing, logging, caching, error policy, rate-limits. + +## Define a plugin + +```ts +import { Plugin } from '@frontmcp/sdk'; + +@Plugin({ + name: 'Cache Plugin', + description: 'Adds transparent response caching for tools/resources', + providers: [CacheProvider], // plugin-scoped providers + exports: [CacheProvider], // re-export to host app + adapters: [SpecNormalizerAdapter], // optionally attach adapters + tools: [WarmCacheTool], // and tools/resources/prompts if desired + resources: [], + prompts: [], +}) +export default class CachePlugin {} +``` + +Attach a plugin at app scope: + +```ts +@App({ + name: 'Billing', + plugins: [CachePlugin, ObservabilityPlugin], +}) +export default class BillingApp {} +``` + +### What plugins can do + +- Register **providers** (and **export** them to the host app) +- Contribute **adapters**, **tools**, **resources**, **prompts** +- Participate in lifecycle via **hooks** (see _Advanced → Hooks_) + +### Composition + +Plugins compose **depth-first** at the app level. Later plugins can depend on providers exported by earlier ones. + + + Put organization-wide concerns (auth, audit, tracing) in plugins so all generated and inline components inherit the + behavior without boilerplate. + diff --git a/docs/live/docs/v/0.1/servers/extensibility/providers.mdx b/docs/live/docs/v/0.1/servers/extensibility/providers.mdx new file mode 100644 index 00000000..ab9c197e --- /dev/null +++ b/docs/live/docs/v/0.1/servers/extensibility/providers.mdx @@ -0,0 +1,72 @@ +--- +title: Context Providers +sidebarTitle: Providers +slug: servers/extensibility/providers +icon: boxes +--- + +**Providers** are dependency-injected singletons (or scoped singletons) that your apps, tools, adapters, and plugins can use — e.g., config, DB pools, Redis clients, KMS, HTTP clients. + +They’re declared with `@Provider()` and registered at **server** or **app** scope. Resolution is hierarchical: **tool → app → server**. + +## Define a provider + +```ts +import { Provider, ProviderScope } from '@frontmcp/sdk'; + +@Provider({ + name: 'DbProvider', + description: 'Postgres connection pool', + scope: ProviderScope.GLOBAL, // GLOBAL | SESSION | REQUEST +}) +export class DbProvider { + /* create pool, expose query() etc. */ +} +``` + +### Scopes + +- **GLOBAL** (default): one instance per process/worker. Ideal for clients, pools, caches. +- **SESSION**: one instance per authenticated session. Use for per-user credentials or token-bound clients. +- **REQUEST**: one instance per inbound request. Use sparingly (e.g., per-request trace/state). + +## Register providers + +**Server-level** providers (available to all apps): + +```ts +@FrontMcp({ + info: { name: 'Suite', version: '1.0.0' }, + apps: [BillingApp, AnalyticsApp], + providers: [DbProvider, CacheProvider], +}) +export default class Server {} +``` + +**App-level** providers (override or add on top of server-level): + +```ts +@App({ + name: 'Billing', + providers: [BillingConfigProvider], +}) +export default class BillingApp {} +``` + + + You can register **class**, **value**, or **factory** providers. Factories are useful for async initialization or + composing other providers. + + +## Using providers from tools/plugins + +FrontMCP resolves providers for your executors and hooks. Keep your tool logic pure; read side-effects (DB, queues, secrets) via providers. + +- Prefer **GLOBAL** for shared clients. +- Use **SESSION** for user-bound clients (e.g., per-user API token). +- Reserve **REQUEST** for ephemeral state. + + + Provider injection/consumption follows your runtime’s DI rules. In general: register providers at the minimal scope + and let the framework resolve them for tools and hooks at execution time. + diff --git a/docs/live/docs/v/0.1/servers/prompts.mdx b/docs/live/docs/v/0.1/servers/prompts.mdx new file mode 100644 index 00000000..e7b792e0 --- /dev/null +++ b/docs/live/docs/v/0.1/servers/prompts.mdx @@ -0,0 +1,52 @@ +--- +title: Prompts +slug: servers/prompts +icon: message-lines +--- + +Prompts are **reusable prompt templates** with typed arguments. They return an MCP `GetPromptResult` for clients to render or send as messages to a model. + +## Minimal prompt + +```ts +import { Prompt } from '@frontmcp/sdk'; + +@Prompt({ + name: 'Summarize', + title: 'Summarize Text', + description: 'Create a concise summary', + arguments: [{ name: 'text', description: 'Input text', required: true }], +}) +export default class SummarizePrompt { + async execute(args: { text: string }) { + return { + description: 'Summarize the provided text', + messages: [ + { role: 'system', content: 'You are a concise assistant.' }, + { role: 'user', content: args.text }, + ], + }; + } +} +``` + +## Prompt metadata + +```ts +@Prompt({ + name: string, + title?: string, + description?: string, + arguments?: Array<{ + name: string; + description?: string; + required?: boolean; + }>, + icons?: Icon[], +}) +``` + +**Notes** + +- Prompts are discoverable and can be parameterized by clients. +- Use prompts to standardize instructions for common tasks across tools/apps. diff --git a/docs/live/docs/v/0.1/servers/resources.mdx b/docs/live/docs/v/0.1/servers/resources.mdx new file mode 100644 index 00000000..46501a21 --- /dev/null +++ b/docs/live/docs/v/0.1/servers/resources.mdx @@ -0,0 +1,72 @@ +--- +title: Resources +slug: servers/resources +icon: book-open +--- + +Resources provide **readable data** to load into the model’s context. Define fixed `Resource`s (a concrete `uri`) or `ResourceTemplate`s (an RFC 6570 `uriTemplate`). + +## Minimal resource + +```ts +import { Resource } from '@frontmcp/sdk'; + +@Resource({ + name: 'docs-home', + title: 'Docs Home', + uri: 'https://example.com/docs', + mimeType: 'text/html', +}) +export default class DocsHome { + async execute(uri: string) { + return { contents: [{ uri, text: 'Welcome to the docs!' }] }; + } +} +``` + +## Minimal resource template + +```ts +import { ResourceTemplate } from '@frontmcp/sdk'; + +@ResourceTemplate({ + name: 'repo-readme', + title: 'GitHub README', + uriTemplate: 'https://raw.githubusercontent.com/{owner}/{repo}/main/README.md', + mimeType: 'text/markdown', +}) +export default class RepoReadme {} +``` + +--- + +## Resource metadata + +```ts +@Resource({ + name: string, + title?: string, + uri: string, + description?: string, + mimeType?: string, + icons?: Icon[], +}) +``` + +## Resource template metadata + +```ts +@ResourceTemplate({ + name: string, + title?: string, + uriTemplate: string, // RFC 6570 + description?: string, + mimeType?: string, + icons?: Icon[], +}) +``` + +**Tips** + +- Use resources to preload long-form text, schemas, or docs the model should see before calling tools. +- Prefer templates when the shape is stable but the identifiers vary. diff --git a/docs/live/docs/v/0.1/servers/server.mdx b/docs/live/docs/v/0.1/servers/server.mdx new file mode 100644 index 00000000..77ff9638 --- /dev/null +++ b/docs/live/docs/v/0.1/servers/server.mdx @@ -0,0 +1,225 @@ +--- +title: The FrontMCP Server +sidebarTitle: Overview +description: The core FrontMCP server — start with the minimum and scale up with HTTP, sessions, logging, providers, and authentication. +icon: server +--- + +FrontMCP servers are defined with a single decorator, `@FrontMcp({ ... })`. This page shows the **minimal config** and then every **top-level option** you can use. Deep dives live in the pages listed under _Servers_. + +## Minimal server + +```ts +import { FrontMcp } from '@frontmcp/sdk'; +import MyApp from './my.app'; + +@FrontMcp({ + info: { name: 'My Server', version: '0.1.0' }, + apps: [MyApp], +}) +export default class Server {} +``` + +**Required:** + +- `info.name` (string) +- `info.version` (string) +- `apps` (at least one app) + +Everything else is optional with sensible defaults. + +--- + +## Full configuration (at a glance) + +```ts +@FrontMcp({ + /** Required */ + info: { + name: 'Expense MCP Server', + version: '1.0.0', + title?: 'Human title', + websiteUrl?: 'https://example.com', + icons?: Icon[], // MCP Icon[] + }, + apps: [/* App classes */], + + /** Optional */ + serve?: true, // default true (auto-boot) + splitByApp?: false, // app composition mode + providers?: [/* provider classes/factories/values */], + + http?: { + port?: 3001, // default 3001 + entryPath?: '', // MUST match PRM resourcePath in .well-known + hostFactory?: /* custom host */, + }, + + session?: { + sessionMode?: 'stateless' | 'stateful' | ((issuer) => ...), // default 'stateless' + transportIdMode?: 'uuid' | 'jwt' | ((issuer) => ...), // default 'uuid' + }, + + logging?: { + level?: LogLevel.Info, // Debug | VERBOSE | Info | Warn | Error | Off + enableConsole?: true, // default true + prefix?: string, + transports?: [/* custom log transports */], + }, + + /** Server-level default auth (omit if splitByApp: true) */ + auth?: ( + | { type: 'remote', name: string, baseUrl: string, ... } + | { type: 'local', id: string, name: string, ... } + ), +}) +``` + +--- + +## Composition mode + +FrontMCP can host **many apps**. Choose how they’re exposed: + +- **Multi-App (default)**: `splitByApp: false` + One server scope. You _may_ configure **server-level `auth`** and all apps inherit it (apps can still override with app-level auth). + +- **Split-By-App**: `splitByApp: true` + Each app is isolated under its own scope/base path. **Server-level `auth` is disallowed**; configure auth per app. (See _Authentication → Overview_.) + +If you’re offering multiple products or tenants, `splitByApp: true` gives clean separation and per-app auth. + +--- + +## HTTP transport + +```ts +http: { + port?: number; // default 3001 + entryPath?: string; // default ''; MUST match the PRM resourcePath in .well-known + hostFactory?: FrontMcpServer | ((cfg) => FrontMcpServer); +} +``` + +- **Port**: listening port for Streamable HTTP. +- **entryPath**: your MCP JSON-RPC entry (`''` or `'/mcp'`). Must align with discovery. +- **hostFactory**: advanced — provide/construct a custom host implementation. + +--- + +## Sessions & Transport IDs + +```ts +session: { + sessionMode?: 'stateless' | 'stateful' | ((issuer) => SessionMode); + transportIdMode?: 'uuid' | 'jwt' | ((issuer) => TransportIdMode); +} +``` + +- **sessionMode** (default **`'stateless'`**): + +- `'stateless'` → session data is carried in a signed/encrypted JWT. Simple, client-portable; no token refresh of nested providers. +- `'stateful'` → server-side store (e.g., Redis). Minimal JWTs, safer for nested tokens, supports refresh. + +- **transportIdMode** (default **`'uuid'`**): + +- `'uuid'` → per-node, strict transport identity. +- `'jwt'` → signed transport IDs for distributed setups; ties into session verification. + +You can supply functions for `sessionMode` / `transportIdMode` to decide per issuer. + +--- + +## Logging + +```ts +logging: { + level?: LogLevel; // default Info + enableConsole?: boolean; // default true + prefix?: string; + transports?: LogTransportType[]; // custom sinks via @FrontMcpLogTransport +} +``` + +Use custom log transports for shipping logs to external systems; console remains on by default. + +--- + +## Global providers + +```ts +providers: [ + /* Provider classes/factories/values */ +]; +``` + +Define DI-style singletons available to all apps (and their tools/plugins). Scopes are supported at the provider level (`GLOBAL`, `SESSION`, `REQUEST`). + +--- + +## Authentication (server level) + +Server-level `auth` sets the **default** auth for all apps (unless `splitByApp: true`, where auth must be per-app). + +### Remote OAuth (encapsulated external IdP) + +```ts +auth: { + type: 'remote', + name: 'frontegg', + baseUrl: 'https://auth.example.com', + dcrEnabled?: boolean, + clientId?: string | ((info) => string), // for non-DCR via local proxy + mode?: 'orchestrated' | 'transparent', + allowAnonymous?: boolean, + consent?: boolean, + scopes?: string[], + grantTypes?: ['authorization_code','refresh_token'], + authEndpoint?: string, + tokenEndpoint?: string, + registrationEndpoint?: string, + userInfoEndpoint?: string, + jwks?: JSONWebKeySet, + jwksUri?: string, +} +``` + +### Local OAuth (built-in AS) + +```ts +auth: { + type: 'local', + id: 'local', + name: 'Local Auth', + scopes?: string[], + grantTypes?: ['authorization_code','refresh_token'], + allowAnonymous?: boolean, // default true + consent?: boolean, // show tool/resource/prompt consent + jwks?: JSONWebKeySet, // inline keys (optional) + signKey?: JWK | Uint8Array // private key (optional; auto-gen if omitted) +} +``` + + + Apps can also define their own `auth` (and mark themselves `standalone`) to expose an isolated auth surface — useful + when mixing public and private apps under one server. + + +--- + +## Bootstrapping & discovery + +- **`serve`** (default **true**): when true, importing the decorated class **boots the server** via `@frontmcp/core`. +- **Version safety**: on boot, FrontMCP checks that all `@frontmcp/*` packages are aligned and throws a clear “version mismatch” error otherwise. + +If you disable `serve`, you’re responsible for calling the core bootstrap yourself. + +--- + +## Common starting points + +- **Single app, default everything**: minimal sample above. +- **Multiple apps, shared auth**: omit `splitByApp`, set server-level `auth`. +- **Isolated apps with per-app auth**: set `splitByApp: true`, configure `auth` in each app. + +Next up: learn how to structure **Apps**, **Tools**, **Resources**, and **Prompts** in the _Core Components_ section. diff --git a/docs/docs/v/0.2/servers/tools.mdx b/docs/live/docs/v/0.1/servers/tools.mdx similarity index 100% rename from docs/docs/v/0.2/servers/tools.mdx rename to docs/live/docs/v/0.1/servers/tools.mdx diff --git a/docs/live/docs/v/0.2/adapters/openapi-adapter.mdx b/docs/live/docs/v/0.2/adapters/openapi-adapter.mdx new file mode 100644 index 00000000..26975691 --- /dev/null +++ b/docs/live/docs/v/0.2/adapters/openapi-adapter.mdx @@ -0,0 +1,121 @@ +--- +title: OpenAPI Adapter +slug: adapters/openapi-adapter +sidebarTitle: OpenAPI +description: Generate MCP tools directly from an OpenAPI spec and call them with strong validation. +icon: puzzle-piece +--- + +The OpenAPI Adapter turns an OpenAPI 3 spec into ready-to-use MCP tools. Each operation in your spec becomes a tool with a Zod-validated input schema and automatic request/response handling. + +## Why use it + +- Zero boilerplate to expose REST APIs as tools +- Runtime validation with Zod (derived from operation params/body) +- Handles path/query/header params, bodies, and JSON parsing +- Simple hooks to inject auth headers or shape the body + +## Quick start + +```ts +import { App } from '@frontmcp/sdk'; +import { OpenapiAdapter } from '@frontmcp/adapters'; + +@App({ + id: 'expense', + name: 'Expense MCP app', + adapters: [ + OpenapiAdapter.init({ + name: 'backend:api', + // Provide either 'url' (string) or 'spec' (OpenAPIV3.Document) + url: process.env.OPENAPI_SPEC_URL!, + baseUrl: process.env.API_BASE_URL!, + }), + ], +}) +export default class ExpenseMcpApp {} +``` + +This mirrors the example in `apps/demo/src/apps/expenses/index.ts`. + +## Options + +Required + +- `name: string` — Identifier for this adapter instance; disambiguates tools when multiple adapters are present. +- `baseUrl: string` — Base URL for requests (e.g., `process.env.API_BASE_URL`). +- One of: + - `url: string` — Path or URL to the OpenAPI document (local file or remote). + - `spec: OpenAPIV3.Document` — An in-memory OpenAPI document. + +Common optional fields + +- `additionalHeaders?: Record` — Static headers applied to every request. +- `headersMapper?: (authInfo: AuthInfo, headers: Headers) => Headers` — Map authenticated session data to headers (e.g., `Authorization`, tenant IDs). +- `bodyMapper?: (authInfo: AuthInfo, body: any) => any` — Transform/augment the request body before sending. +- `inputSchemaMapper?: (inputSchema: any) => any` — Transform the generated input schema (hide/fill fields, etc.). +- OpenAPI tool generation controls (passed to `openapi-mcp-generator`): +- `filterFn?: (op) => boolean` +- `defaultInclude?: boolean` +- `excludeOperationIds?: string[]` + +## Authentication examples + +Static API key header + +```ts +OpenapiAdapter.init({ + name: 'my-api', + url: 'https://api.example.com/openapi.json', + baseUrl: 'https://api.example.com', + additionalHeaders: { 'x-api-key': process.env.MY_API_KEY! }, +}); +``` + +Derive headers from the authenticated context + +```ts +OpenapiAdapter.init({ + name: 'my-api', + url: 'https://api.example.com/openapi.json', + baseUrl: 'https://api.example.com', + headersMapper: (authInfo, headers) => { + if (authInfo.token) headers.set('authorization', `Bearer ${authInfo.token}`); + return headers; + }, +}); +``` + +## Filtering which operations become tools + +Only generate tools for specific operations using `filterFn` or exclude by id with `excludeOperationIds`. + +```ts +OpenapiAdapter.init({ + name: 'billing', + url: 'https://my.api/openapi.json', + baseUrl: 'https://my.api', + filterFn: (op) => op.path.startsWith('/invoices') || op.operationId === 'getCustomer', + excludeOperationIds: ['deprecatedEndpoint'], +}); +``` + +## How requests and responses are handled + +- Path params are interpolated into the template (e.g., `/users/{id}`) +- Query params and headers are taken from validated inputs +- For non-GET/HEAD/OPTIONS methods, a request body is sent when defined +- Responses with `content-type: application/json` are parsed to objects; otherwise raw text is returned + +## Tips + +- Combine with app-level plugins (logging, caching, metrics) to enhance generated tools. +- You can attach multiple OpenAPI adapters to one app; set unique `name` values to avoid tool id conflicts. +- Use `inputSchemaMapper` to hide sensitive fields from the tool interface while still supplying them server-side. + +## Links + +- Demo app: `apps/demo/src/apps/expenses/index.ts` +- Spec used by the demo: https://frontmcp-test.proxy.beeceptor.com/openapi.json +- Generator library: https://www.npmjs.com/package/openapi-mcp-generator +- Source code: `libs/adapters/src/openapi` diff --git a/docs/live/docs/v/0.2/deployment/local-dev-server.mdx b/docs/live/docs/v/0.2/deployment/local-dev-server.mdx new file mode 100644 index 00000000..06ebefaf --- /dev/null +++ b/docs/live/docs/v/0.2/deployment/local-dev-server.mdx @@ -0,0 +1,169 @@ +--- +title: Local Dev Server +slug: deployment/local-dev-server +icon: computer +--- + +Run your FrontMCP server locally with hot-reload and verify it with the **FrontMCP Inspector** (zero setup). + +## Prerequisites + +- **Node.js ≥ 22** +- **npm ≥ 10** (pnpm/yarn also supported) + +--- + +## Quick start + +### Option A — New project + +Creates a folder and scaffolds everything for you. + + + ```bash universal + npx frontmcp create my-app + ``` + + + +### Option B — Existing project + +Install and initialize in your current repo: + +```bash universal npm i -D frontmcp @types/node@^20 npx frontmcp init ``` + +`init` adds the required scripts and updates **tsconfig.json** automatically. + +--- + +## Package scripts + +After `create` or `init`, your `package.json` will include: + +```json +{ + "scripts": { + "dev": "frontmcp dev", + "build": "frontmcp build", + "inspect": "frontmcp inspector", + "doctor": "frontmcp doctor" + } +} +``` + +- `frontmcp dev` — run in watch mode with type-checks +- `frontmcp build` — compile to `./dist` (override with `--out-dir`) +- `frontmcp inspector` — launches **@modelcontextprotocol/inspector** with zero setup +- `frontmcp doctor` — verifies Node/npm versions and project configuration + +--- + +## Recommended tsconfig + +`init` writes this for you, but if you prefer to manage it manually: + +```json title="tsconfig.json" +{ + "compilerOptions": { + "target": "es2021", + "module": "esnext", + "lib": ["es2021"], + "moduleResolution": "bundler", + "rootDir": "src", + "outDir": "dist", + "strict": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "sourceMap": true + }, + "include": ["src/**/*.ts"], + "exclude": ["**/*.test.ts", "**/__tests__/**"] +} +``` + +--- + +## Minimal app + +> You can import from `@frontmcp/sdk` directly; no extra install needed. + +**src/main.ts** + +```ts +import 'reflect-metadata'; +import { FrontMcp, LogLevel } from '@frontmcp/sdk'; +import HelloApp from './hello.app'; + +@FrontMcp({ + info: { name: 'Hello MCP', version: '0.1.0' }, + apps: [HelloApp], + http: { port: Number(process.env.PORT) || 3000 }, + logging: { level: LogLevel.Info }, +}) +export default class Server {} +``` + +**src/hello.app.ts** + +```ts +import { App } from '@frontmcp/sdk'; +import Greet from './tools/greet.tool'; + +@App({ id: 'hello', name: 'Hello', tools: [Greet] }) +export default class HelloApp {} +``` + +**src/tools/greet.tool.ts** + +```ts +import { tool } from '@frontmcp/sdk'; +import { z } from 'zod'; + +export default tool({ + name: 'greet', + description: 'Greets a user by name', + // New style: pass Zod fields directly (no z.object wrapper) + inputSchema: { name: z.string() }, +})(({ name }) => `Hello, ${name}!`); +``` + +--- + +## Run the dev server + +```bash universal npm run dev ``` + +Your server will start (default `http://localhost:3000`). The console will print the MCP endpoint. + +--- + +## Inspect locally (zero setup) + +Launch the **FrontMCP Inspector** to exercise tools and messages in a friendly UI: + +```bash universal npm run inspect ``` + +- This runs `npx @modelcontextprotocol/inspector` behind the scenes. +- Point it at your local server URL printed by `dev` (e.g., `http://localhost:3000`). +- Try calling `greet` and watch responses stream back in real time. + +--- + +## Troubleshooting + +- **Check configuration** + +```bash +npm run doctor +``` + +Ensures Node/npm versions, entry detection, and `tsconfig.json` are correct. + +- **Entry detection** + The CLI looks for `package.json.main`; if missing, it falls back to `src/main.ts`. If no entry is found, it will tell you how to create one. + +- **Type errors in dev** + The `dev` command performs async type-checks while watching your files, so you’ll see issues immediately without stopping the server. diff --git a/docs/live/docs/v/0.2/deployment/production-build.mdx b/docs/live/docs/v/0.2/deployment/production-build.mdx new file mode 100644 index 00000000..54ef020e --- /dev/null +++ b/docs/live/docs/v/0.2/deployment/production-build.mdx @@ -0,0 +1,73 @@ +--- +title: Production Build +slug: deployment/production-build +icon: building +--- + +Build a compact Node artifact and run it behind a process manager / reverse proxy. + +## Build + + + + ```bash npm + npm run build + ``` + + ```bash yarn + yarn build + ``` + + ```bash pnpm + pnpm build + ``` + + + +This compiles TypeScript to `dist/` using `tsconfig.build.json`. + +## Start + + + + ```bash npm + NODE_ENV=production PORT=8080 npm start + ``` + + ```bash yarn + NODE_ENV=production PORT=8080 yarn start + ``` + + ```bash pnpm + NODE_ENV=production PORT=8080 pnpm start + ``` + + + +## Recommended runtime setup + +- Use a **process manager** (PM2, systemd) for restarts and logs. +- Put a **reverse proxy** (NGINX, Traefik, Caddy) in front for TLS and path routing. +- Pin matching versions of all `@frontmcp/*` packages. + +### Example NGINX snippet + +```nginx +server { + listen 443 ssl; + server_name mcp.example.com; + + location /mcp/ { + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto https; + proxy_pass http://127.0.0.1:8080/mcp/; + } +} +``` + +## Troubleshooting + +- **Version mismatch** at boot → align all `@frontmcp/*` versions and reinstall. +- **No decorators** working → ensure `experimentalDecorators` + `emitDecoratorMetadata` and `import 'reflect-metadata'` at the top of `main.ts`. +- **Port conflicts** → set `http.port` in `@FrontMcp` or use `PORT` env. diff --git a/docs/live/docs/v/0.2/getting-started/installation.mdx b/docs/live/docs/v/0.2/getting-started/installation.mdx new file mode 100644 index 00000000..3a0980a3 --- /dev/null +++ b/docs/live/docs/v/0.2/getting-started/installation.mdx @@ -0,0 +1,109 @@ +--- +title: Installation +slug: getting-started/installation +icon: arrow-down-to-line +--- + +## Prerequisites + +- **Node.js ≥ 22** +- **npm ≥ 10** (pnpm / yarn work too) +- TypeScript project (the init command can set this up) + +--- + +## Option A — Create a new project + +Use the built-in project generator. It **requires** a project name and will create a new folder under your current directory. + + +```bash universal +npx frontmcp create my-app +``` + +```bash pnpm +pnpm dlx frontmcp create my-app +``` + +```bash yarn +yarn dlx frontmcp create my-app +``` + + + +This will: + +- scaffold a FrontMCP project in `./my-app` +- configure `tsconfig.json` for decorators and modern ESM +- generate a `package.json` with helpful scripts +- install required dev dependencies (e.g. TypeScript, tsx, zod, reflect-metadata) + +--- + +## Option B — Add to an existing project + +Install the CLI and Node types (FrontMCP bundles compatible `@frontmcp/sdk` and `@frontmcp/core` internally—no separate install needed). + + + ```bash npm npm i -D frontmcp @types/node@^20 ``` ```bash pnpm pnpm add -D frontmcp @types/node@^20 ``` ```bash yarn + yarn add -D frontmcp @types/node@^20 ``` + + +Then initialize FrontMCP in your project root: + +```bash +npx frontmcp init +``` + +`init` will: + +- add/update **scripts** in your `package.json` +- ensure your **tsconfig.json** includes required compiler options +- verify a sensible project layout + +--- + +## Package scripts + +After `create` or `init`, you’ll have these scripts: + +```json +{ + "scripts": { + "dev": "frontmcp dev", + "build": "frontmcp build", + "inspect": "frontmcp inspector", + "doctor": "frontmcp doctor" + } +} +``` + +**What they do** + +- `frontmcp dev` — run your server in watch mode (tsx) +- `frontmcp build` — compile your entry with TypeScript (outputs to `./dist` by default) +- `frontmcp inspector` — launch the MCP Inspector (`npx @modelcontextprotocol/inspector`) +- `frontmcp doctor` — validate Node/npm versions, tsconfig, and project setup + +--- + +## Verify your setup + +Run: + +```bash +npm run doctor +``` + + + If anything is missing or misconfigured (Node/npm versions, `tsconfig.json`, scripts), **doctor** will tell you + exactly what to fix. + + +--- + +## Next steps + +- Start developing: `npm run dev` +- Build for distribution: `npm run build` +- Explore tools and messages live: `npm run inspect` diff --git a/docs/live/docs/v/0.2/getting-started/quickstart.mdx b/docs/live/docs/v/0.2/getting-started/quickstart.mdx new file mode 100644 index 00000000..dd078826 --- /dev/null +++ b/docs/live/docs/v/0.2/getting-started/quickstart.mdx @@ -0,0 +1,91 @@ +--- +title: Quickstart +slug: getting-started/quickstart +icon: rocket-launch +--- + +Welcome! This guide will help you quickly set up FrontMCP and run your first MCP server. + +If you haven't already installed FrontMCP, follow the [installation instructions](./installation). + +## 1) Create your FrontMCP server + +```ts +// src/main.ts +import { FrontMcp, LogLevel } from '@frontmcp/sdk'; +import HelloApp from './hello.app'; + +@FrontMcp({ + info: { name: 'Hello MCP', version: '0.1.0' }, + apps: [HelloApp], + http: { port: 3000 }, + logging: { level: LogLevel.INFO }, +}) +export default class HelloServer {} +``` + +## 2) Define an app + +```ts +// src/hello.app.ts +import { App } from '@frontmcp/sdk'; +import GreetTool from './tools/greet.tool'; + +@App({ + id: 'hello', + name: 'Hello App', + tools: [GreetTool], +}) +export default class HelloApp {} +``` + +## 3) Add your first tool + +```ts +// src/tools/greet.tool.ts +import { tool } from '@frontmcp/sdk'; +import { z } from 'zod'; + +export default tool({ + name: 'greet', + description: 'Greets a user by name', + inputSchema: { name: z.string() }, +})(async ({ name }) => { + return { + result: `Hello, ${name}!`, + }; +}); +``` + +## 4) Run it + +Add scripts: + +```json +{ + "scripts": { + "dev": "tsx src/main.ts", + "build": "tsc -p tsconfig.build.json", + "start": "node dist/apps/hello/main.js" + } +} +``` + +Run: + +```bash +npm run dev +# Server listening on http://localhost:3000 +``` + + + FrontMCP speaks **MCP Streamable HTTP**. You can connect any MCP-capable client and call the `greet` tool with + `{"name": "Ada"}`. + + +## What’s next + +- Add auth (local or remote OAuth) +- Use **adapters** to load tools from OpenAPI +- Enable **plugins** like transparent caching +- Wire **hooks** for logging, rate limits, or request transforms diff --git a/docs/live/docs/v/0.2/getting-started/welcome.mdx b/docs/live/docs/v/0.2/getting-started/welcome.mdx new file mode 100644 index 00000000..c17c8a57 --- /dev/null +++ b/docs/live/docs/v/0.2/getting-started/welcome.mdx @@ -0,0 +1,64 @@ +--- +title: Welcome to FrontMCP v0.2 +sidebarTitle: Welcome to FrontMCP +slug: getting-started/welcome +icon: hand-wave +description: The TypeScript way to build MCP servers with decorators, DI, and Streamable HTTP. +--- + +**FrontMCP is the TypeScript-first framework for MCP.** You write clean, typed code; FrontMCP handles the protocol, transport, and execution flow. + +```ts +import { FrontMcp, App, tool } from '@frontmcp/sdk'; +import { z } from 'zod'; + +const AddTool = tool({ + name: 'add', + description: 'Add two numbers', + inputSchema: { a: z.number(), b: z.number() }, +})(async (input, ctx) => { + return { + result: input.a + input.b, + }; +}); + +@App({ + id: 'calc', + name: 'Calculator', + tools: [AddTool], +}) +class CalcApp {} + +@FrontMcp({ + info: { name: 'Demo 🚀', version: '0.1.0' }, + apps: [CalcApp], + auth: { + type: 'remote', + name: 'my-oauth', + baseUrl: 'https://oauth.example.com', + }, +}) +export default class Server {} +``` + +### Why FrontMCP? + +- 🧑‍💻 **TypeScript-native DX** — decorators, Zod, and strong typing end-to-end +- 🧠 **Scoped invoker + DI** — secure, composable execution with hooks +- 🧩 **Adapters & Plugins** — extend your server without boilerplate +- 🔌 **Spec-aligned transport** — Streamable HTTP for modern MCP clients + +### Core concepts + +- **Server** — entry point via `@FrontMcp({...})` +- **App** — a logical bundle of tools via `@App({...})` +- **Tool** — typed units of work via `@Tool({...})` +- **Hooks** — cross-cutting behaviors (auth, logging, rate limits) +- **Adapters/Plugins** — load tools dynamically; enrich behavior + + + FrontMCP follows MCP protocol principles and **secured transport requirements**. You get sessions, streaming, and + validation out of the box—no custom wiring needed. + + +**Build something real**: jump to the [Quickstart](./quickstart) or set up your workspace in [Installation](./installation). diff --git a/docs/docs/v/0.2/guides/add-openapi-adapter.mdx b/docs/live/docs/v/0.2/guides/add-openapi-adapter.mdx similarity index 100% rename from docs/docs/v/0.2/guides/add-openapi-adapter.mdx rename to docs/live/docs/v/0.2/guides/add-openapi-adapter.mdx diff --git a/docs/live/docs/v/0.2/guides/caching-and-cache-miss.mdx b/docs/live/docs/v/0.2/guides/caching-and-cache-miss.mdx new file mode 100644 index 00000000..0247e3e0 --- /dev/null +++ b/docs/live/docs/v/0.2/guides/caching-and-cache-miss.mdx @@ -0,0 +1,42 @@ +--- +title: Caching & Cache Miss +slug: guides/caching-and-cache-miss +--- + +Use the Cache plugin to cache tool results and control TTL/refresh behavior. + +Overview + +- Opt in per tool via metadata (e.g., `cache: true` or `{ ttl, slideWindow }`). +- Configure the store at app level: in‑memory or Redis (client or config). +- Cache keys include tool id and validated input; you can extend via hooks. + +Set up (pseudocode) + +``` +import CachePlugin from '@frontmcp/plugins/cache' + +@App({ + plugins: [CachePlugin], // or CachePlugin.init({ defaultTTL: 300 }) +}) +class MyApp {} +``` + +Opt in per tool (pseudocode) + +``` +export const GetReport = tool({ + id: 'get-report', + inputSchema: { id: z.string() }, + cache: { ttl: 600, slideWindow: true }, +})(async (input, session) => { + // expensive computation or remote call + return await fetchReport(input.id); +}); +``` + +Notes + +- Memory store is per‑process; prefer Redis in production. +- `slideWindow` updates the TTL on access; without it, TTL is absolute. +- Hooks can decorate the key or short‑circuit on policy. diff --git a/docs/live/docs/v/0.2/guides/customize-flow-stages.mdx b/docs/live/docs/v/0.2/guides/customize-flow-stages.mdx new file mode 100644 index 00000000..83eb98e9 --- /dev/null +++ b/docs/live/docs/v/0.2/guides/customize-flow-stages.mdx @@ -0,0 +1,36 @@ +--- +title: Customize Flow Stages +slug: guides/customize-flow-stages +--- + +Hook into lifecycle stages to enforce policy, add telemetry, or transform IO. + +Concepts + +- Flows are named pipelines with stages; hooks run at specific stages. +- Kinds: Will (before), Stage (in-place), Did (after), Around (wrap). +- Priority orders execution; higher runs earlier for Will/Stage; Did runs in reverse. + +Example (pseudocode) + +``` + const Tool = FlowHooksOf('tool.execute'); + + class MyHooks { + @Tool.Will('validate', { priority: 100 }) + requireFields(ctx) { + // if (!ctx.input.amount) throw new Error('amount required'); + } + + @Tool.Did('execute') + redact(ctx) { + // ctx.result = redactSensitive(ctx.result); + } + } +``` + +Tips + +- Contribute hooks from a Plugin to reuse across apps. +- Use context to pass data between stages in the same flow. +- See servers/flows and servers/hooks for more details. diff --git a/docs/live/docs/v/0.2/servers/apps.mdx b/docs/live/docs/v/0.2/servers/apps.mdx new file mode 100644 index 00000000..ceb5ed19 --- /dev/null +++ b/docs/live/docs/v/0.2/servers/apps.mdx @@ -0,0 +1,106 @@ +--- +title: Apps +slug: servers/apps +icon: cube +--- + +Apps are how you **group capabilities** in FrontMCP. Each app can contribute **tools**, **resources**, **prompts**, plus **providers**, **adapters**, and **plugins**. Apps may run _locally_ (in this process) or be _remote_ (URL/worker). + +## Minimal local app + +```ts +import { App } from '@frontmcp/sdk'; + +@App({ + id: 'hello', + name: 'Hello App', + tools: [], +}) +export default class HelloApp {} +``` + +Add it to your server: + +```ts +@FrontMcp({ info: { name: 'Demo', version: '0.1.0' }, apps: [HelloApp] }) +export default class Server {} +``` + +--- + +## Local app options + +```ts +@App({ + id?: string, + name: string, + description?: string, + providers?: ProviderType[], + authProviders?: AuthProviderType[], + plugins?: PluginType[], + adapters?: AdapterType[], + tools?: ToolType[], + resources?: ResourceType[], + prompts?: PromptType[], + auth?: AuthOptions, // app‑default auth (overrides server auth) + standalone?: 'includeInParent' | boolean, // isolate scope / expose separately +}) +``` + +**Scoping & auth** + +- If the server uses **`splitByApp: true`**, each app is isolated and **must** configure its own auth (server-level `auth` is disallowed). +- `standalone: true` makes the app expose its own scope/entry; `'includeInParent'` lists it under the parent while keeping isolation. + +**Dependency resolution** + +- Providers resolve **tool → app → server**. +- Plugins/adapters attach at app scope; generated items inherit the app’s policies and provenance. + +--- + +## Remote apps + +Define an app that proxies to a **worker file** or a **URL**: + +```ts +@App({ + name: 'Remote CRM', + urlType: 'url', // 'url' | 'worker' + url: 'https://crm.example.com/mcp', + auth: { type: 'remote', name: 'crm-idp', baseUrl: 'https://idp.example.com' }, + standalone: true, +}) +export default class CrmApp {} +``` + +**Fields** + +```ts +{ + id?: string; + name: string; + description?: string; + urlType: 'worker' | 'url'; + url: string; + auth?: AuthOptions; + standalone?: 'includeInParent' | boolean; +} +``` + +--- + +## Example: app with adapter + providers + +```ts +@App({ + id: 'billing', + name: 'Billing', + providers: [DbProvider, CacheProvider], + adapters: [ + // e.g. OpenAPI adapter that generates tools/resources from a spec + BillingOpenApiAdapter, + ], +}) +export default class BillingApp {} +``` diff --git a/docs/live/docs/v/0.2/servers/authentication/local.mdx b/docs/live/docs/v/0.2/servers/authentication/local.mdx new file mode 100644 index 00000000..de2e31a7 --- /dev/null +++ b/docs/live/docs/v/0.2/servers/authentication/local.mdx @@ -0,0 +1,49 @@ +--- +title: Local OAuth +slug: servers/authentication/local +icon: window +--- + +FrontMCP ships a **built‑in OAuth provider** for first‑party scenarios for development. + +### Configuration + +```ts +auth: { + type: 'local', + id: 'local', + name: 'Local Auth', + scopes?: string[], + grantTypes?: ('authorization_code' | 'refresh_token')[], + allowAnonymous?: boolean, // default true + consent?: boolean, + jwks?: JSONWebKeySet, // inline JWKS (optional) + signKey?: JWK | Uint8Array // private key (optional; auto‑generated if omitted) +} +``` + +### Example (per app, split‑by‑app server) + +```ts +@FrontMcp({ + info: { name: 'Workspace', version: '1.0.0' }, + auth: { type: 'local' }, + apps: [DocsApp, MailApp], + splitByApp: true, +}) +export default class Server {} + +@App({ + name: 'Docs', +}) +export default class DocsApp {} +``` + +When using `splitByApp: true`, define auth per app; server‑level `auth` is not allowed. + +--- + +## Keys & JWKS + +- Omit `signKey` to auto‑generate keys (exposed via the local JWKS endpoint). +- Provide `jwks` or `signKey` to pin keys for stable environments. diff --git a/docs/live/docs/v/0.2/servers/authentication/overview.mdx b/docs/live/docs/v/0.2/servers/authentication/overview.mdx new file mode 100644 index 00000000..349c09d4 --- /dev/null +++ b/docs/live/docs/v/0.2/servers/authentication/overview.mdx @@ -0,0 +1,79 @@ +--- +title: Authentication +sidebarTitle: Overview +slug: servers/authentication/overview +icon: users-between-lines +--- + +FrontMCP supports **Remote OAuth** (connect to an external IdP) and a built‑in **Local OAuth** provider. You can configure auth **at the server** or **per app**: + +- **Server‑level auth**: a default provider for all apps (only when `splitByApp: false`). +- **App‑level auth**: each app defines its own provider; required when `splitByApp: true`. + +Auth drives **session creation**, **token propagation**, and optional **consent** for tools/resources/prompts. + + + If an external provider **doesn’t support Dynamic Client Registration (DCR)**, FrontMCP can front it with a **local + OAuth proxy** that registers/holds the client on your behalf. See *Remote OAuth → Proxy*. + + +--- + +## Where to configure auth + +**Server (multi‑app, shared auth):** + +```ts +@FrontMcp({ + info: { name: 'Expense', version: '1.0.0' }, + apps: [BillingApp, AnalyticsApp], + auth: { type: 'remote', name: 'frontegg', baseUrl: 'https://idp.example.com' }, +}) +export default class Server {} +``` + +**Per app (isolated scopes):** + +```ts +@App({ name: 'Billing', auth: { type: 'local', id: 'local', name: 'Local Auth' } }) +export default class BillingApp {} + +@FrontMcp({ info: { name: 'Suite', version: '1.0.0' }, apps: [BillingApp, AnalyticsApp], splitByApp: true }) +export default class Server {} +``` + +--- + +## Consent & scopes + +Enable consent to let users select the **tools/resources/prompts** they grant, producing a **scoped token**. + +```ts +auth: { + type: 'remote', + name: 'frontegg', + baseUrl: 'https://idp.example.com', + consent: true +} +``` + +You can still send classic OAuth scopes via `scopes: string[]`. + +--- + +## Sessions & transport + +Auth integrates with server sessions (see _Server Overview → Sessions_): + +- `session.sessionMode`: `stateful` keeps tokens server‑side (recommended for nested providers); `stateless` embeds in JWT (simpler). +- `session.transportIdMode`: `uuid` (per‑node) or `jwt` (signed transport IDs for distributed setups). + + + Use **stateful** sessions when working with short‑lived upstream tokens—you’ll get refresh without round‑trips. + + +--- + +## Discovery & well‑known + +Remote providers typically self‑describe via `/.well-known/oauth-authorization-server` and `/.well-known/jwks.json`. You may override endpoints and JWKS inline when needed. diff --git a/docs/live/docs/v/0.2/servers/authentication/remote-proxy.mdx b/docs/live/docs/v/0.2/servers/authentication/remote-proxy.mdx new file mode 100644 index 00000000..67194067 --- /dev/null +++ b/docs/live/docs/v/0.2/servers/authentication/remote-proxy.mdx @@ -0,0 +1,40 @@ +--- +title: Remote OAuth — Proxy +slug: servers/authentication/remote-proxy +icon: share +--- + +Some IdPs don’t support **Dynamic Client Registration (DCR)**. FrontMCP can front them with a **local OAuth proxy** that: + +1. Exposes OAuth endpoints locally under your server/app scope. +2. Registers a client using your provided `clientId` (or derives one via function). +3. Issues/validates tokens while exchanging with the upstream IdP. + +### Configure + +```ts +@FrontMcp({ + info: { name: 'Suite', version: '1.0.0' }, + apps: [SalesApp], + auth: { + type: 'remote', + name: 'legacy-idp', + baseUrl: 'https://legacy-idp.example.com', + dcrEnabled: false, + clientId: 'my-preprovisioned-client', + consent: true, + }, +}) +export default class Server {} +``` + + + At runtime, the local proxy maintains client registration and keys while delegating user authentication to the + upstream IdP. + + +--- + +## Endpoint overrides + +If your IdP requires custom endpoints or static keys, set `authEndpoint`, `tokenEndpoint`, `registrationEndpoint`, `userInfoEndpoint`, `jwks`, or `jwksUri` directly. diff --git a/docs/live/docs/v/0.2/servers/authentication/remote.mdx b/docs/live/docs/v/0.2/servers/authentication/remote.mdx new file mode 100644 index 00000000..efef6a50 --- /dev/null +++ b/docs/live/docs/v/0.2/servers/authentication/remote.mdx @@ -0,0 +1,61 @@ +--- +title: Remote OAuth +slug: servers/authentication/remote +icon: user-shield +--- + +Use a remote identity provider (IdP) like Frontegg, Auth0, Azure Entra, etc. + +### Configuration + +```ts +auth: { + type: 'remote', + name: 'frontegg', + baseUrl: 'https://auth.example.com', + dcrEnabled?: boolean, + clientId?: string | ((clientInfo: { clientId: string }) => string), + mode?: 'orchestrated' | 'transparent', + allowAnonymous?: boolean, + consent?: boolean, + scopes?: string[], + grantTypes?: ('authorization_code' | 'refresh_token')[], + authEndpoint?: string, + tokenEndpoint?: string, + registrationEndpoint?: string, + userInfoEndpoint?: string, + jwks?: JSONWebKeySet, + jwksUri?: string, +} +``` + +### Example (server‑level) + +```ts +@FrontMcp({ + info: { name: 'Expense MCP', version: '1.0.0' }, + apps: [ExpenseApp], + auth: { type: 'remote', name: 'frontegg', baseUrl: 'https://auth.example.com', consent: true }, +}) +export default class Server {} +``` + +### Example (per app) + +```ts +@App({ + name: 'CRM', + auth: { type: 'remote', name: 'crm', baseUrl: 'https://idp.example.com', scopes: ['openid', 'email'] }, + standalone: true, +}) +export default class CrmApp {} +``` + +Use `standalone: true` to expose the app’s auth surface under its own scope/entry. + +--- + +## DCR vs non‑DCR + +- **`dcrEnabled: true`** → FrontMCP registers the client dynamically at the IdP. +- **`dcrEnabled: false`** → supply `clientId` and use a **local OAuth proxy** to handle registration/storage. See _Remote OAuth → Proxy_. diff --git a/docs/live/docs/v/0.2/servers/authentication/token.mdx b/docs/live/docs/v/0.2/servers/authentication/token.mdx new file mode 100644 index 00000000..344381d4 --- /dev/null +++ b/docs/live/docs/v/0.2/servers/authentication/token.mdx @@ -0,0 +1,40 @@ +--- +title: Token & Session +slug: servers/authentication/token +icon: key +--- + +Authorization determines **what** a token may do. + +## OAuth scopes + +Provide standard scopes to external IdPs: + +```ts +auth: { + type: 'remote', + name: 'frontegg', + baseUrl: 'https://idp.example.com', + scopes: ['openid','profile','email'] +} +``` + +## Tool/resource/prompt consent + +Set `consent: true` to display a **post‑login consent** listing your registered **tools/resources/prompts**. The issued access token includes the selected grants. + +## Modes (Remote OAuth) + +Use the `mode` field to reflect deployment topology: + +- `transparent` (default): your server acts as a regular confidential client. +- `orchestrated`: gateway coordinates multiple apps/providers under one umbrella token (used in advanced multi‑app setups). + +When `splitByApp: true`, configure auth **per app**; server‑level `auth` is disallowed. + +--- + +## Token lifetimes & sessions + +- **Stateful sessions**: tokens are encrypted server‑side; clients hold a lightweight reference. Smooth refresh. +- **Stateless sessions**: tokens ride inside JWT; simple but no silent refresh of upstream tokens. diff --git a/docs/live/docs/v/0.2/servers/extensibility/adapters.mdx b/docs/live/docs/v/0.2/servers/extensibility/adapters.mdx new file mode 100644 index 00000000..682d62b2 --- /dev/null +++ b/docs/live/docs/v/0.2/servers/extensibility/adapters.mdx @@ -0,0 +1,46 @@ +--- +title: Adapters Transformers +sidebarTitle: Adapters +slug: servers/extensibility/adapters +icon: plug +--- + +**Adapters** convert **external definitions or systems** into FrontMCP components (tools, resources, prompts). Common examples: + +- **OpenAPI** → generate typed **tools** from REST endpoints +- **File system / blobs** → expose documents as **resources** +- **Custom backends** → synthesize prompts/resources from proprietary schemas + +## Define an adapter + +```ts +import { Adapter } from '@frontmcp/sdk'; + +@Adapter({ + name: 'Billing OpenAPI Loader', + description: 'Generates tools/resources from the Billing API spec', +}) +export default class BillingOpenApiAdapter { + // implement adapter-specific logic to register generated tools/resources/prompts +} +``` + +Register the adapter on an app: + +```ts +@App({ + name: 'Billing', + adapters: [BillingOpenApiAdapter], +}) +export default class BillingApp {} +``` + +### Behavior + +- Generated items inherit the **app’s plugins, providers, and policies** and are tagged with provenance. +- Adapters can contribute **tools**, **resources**, and **prompts** simultaneously. + + + Keep adapters **pure**: read a spec or source, generate components, and let app-level plugins handle cross-cutting + concerns like auth, rate limiting, or caching. + diff --git a/docs/live/docs/v/0.2/servers/extensibility/logging.mdx b/docs/live/docs/v/0.2/servers/extensibility/logging.mdx new file mode 100644 index 00000000..88169522 --- /dev/null +++ b/docs/live/docs/v/0.2/servers/extensibility/logging.mdx @@ -0,0 +1,191 @@ +--- +title: Logging Transports +sidebarTitle: Logging +slug: servers/extensibility/logging +icon: receipt +description: Create and register custom log transports for FrontMCP. +--- + +FrontMCP logging is extensible. In addition to the default console logger, you can register one or more custom transports via the server config. + +## Register a transport + +```ts +@FrontMcp({ + info: { name: 'Demo', version: '0.1.0' }, + apps: [App], + logging: { + level: LogLevel.Info, + enableConsole: true, // set false to disable the built‑in console transport + transports: [StructuredJsonTransport, HttpBatchTransport], + }, +}) +export default class Server {} +``` + +## Transport contract + +A transport is a class decorated with `@LogTransport({...})` that implements `LogTransportInterface`. + +```ts +export interface LogRecord { + level: LogLevel; + levelName: string; + message: string; + args: unknown[]; + timestamp: Date; + prefix: string; +} + +export type LogFn = (msg?: any, ...args: any[]) => void; + +export abstract class LogTransportInterface { + abstract log(rec: LogRecord): void; +} +``` + +The framework filters by the configured `logging.level` before calling transports. + +> Transports should never throw. Handle errors internally and keep I/O non‑blocking (use buffering/batching for remote sinks). + +## Built‑in: Console + +The default console transport formats messages with ANSI (when TTY) and falls back to plain text otherwise. + +```ts +@LogTransport({ + name: 'ConsoleLogger', + description: 'Default console logger', +}) +export class ConsoleLogTransportInstance extends LogTransportInterface { + log(rec: LogRecord): void { + const fn = this.bind(rec.level, rec.prefix); + fn(String(rec.message), ...rec.args); + } + // ...see source for details +} +``` + +Disable it by setting `enableConsole: false` in your server config. + +## Example: Structured JSON (JSONL) + +Emit machine‑readable logs (one JSON per line). Useful for file shipping agents or centralized logging. + +```ts +import { LogTransport, LogTransportInterface, LogRecord } from '@frontmcp/sdk'; + +@LogTransport({ + name: 'StructuredJsonTransport', + description: 'Writes JSONL log records to stdout', +}) +export class StructuredJsonTransport extends LogTransportInterface { + log(rec: LogRecord): void { + try { + const payload = { + ts: rec.timestamp.toISOString(), + level: rec.levelName, // e.g. INFO + levelValue: rec.level, // numeric + prefix: rec.prefix || undefined, + msg: stringify(rec.message), + args: rec.args?.map(stringify), + }; + // Avoid console formatting; write raw line + process.stdout.write(JSON.stringify(payload) + '\n'); + } catch (err) { + // Never throw from a transport + } + } +} + +function stringify(x: unknown) { + if (x instanceof Error) { + return { name: x.name, message: x.message, stack: x.stack }; + } + try { + return typeof x === 'string' ? x : JSON.parse(JSON.stringify(x)); + } catch { + return String(x); + } +} +``` + +Register it: + +```ts +logging: { level: LogLevel.Info, enableConsole: false, transports: [StructuredJsonTransport] } +``` + +## Example: HTTP batch transport (non‑blocking) + +Buffer records in memory and POST them in batches. Implements basic retry with backoff. + +```ts +import { LogTransport, LogTransportInterface, LogRecord } from '@frontmcp/sdk'; + +@LogTransport({ + name: 'HttpBatchTransport', + description: 'POST logs in batches', +}) +export class HttpBatchTransport extends LogTransportInterface { + private queue: any[] = []; + private timer: NodeJS.Timeout | null = null; + private flushing = false; + private readonly maxBatch = 50; + private readonly flushMs = 1000; + + constructor(private endpoint = process.env.LOG_ENDPOINT || 'https://logs.example.com/ingest') { + super(); + } + + log(rec: LogRecord): void { + this.queue.push({ + ts: rec.timestamp.toISOString(), + lvl: rec.levelName, + pfx: rec.prefix || undefined, + msg: String(rec.message), + args: safeArgs(rec.args), + }); + if (this.queue.length >= this.maxBatch) this.flush(); + else if (!this.timer) this.timer = setTimeout(() => this.flush(), this.flushMs); + } + + private async flush() { + // TODO: Implement batch flush logic + // - Extract batch from queue + // - POST to this.endpoint + // - Handle errors and implement retry with backoff + // - Reset flushing flag and timer + } +} + +function safeArgs(a: unknown[]) { + return (a || []).map((x) => (x instanceof Error ? { name: x.name, message: x.message, stack: x.stack } : x)); +} +``` + +Notes: + +- Keep batches small and time‑bounded; avoid blocking the event loop. +- On process exit, you may add a `beforeExit`/`SIGTERM` handler to flush synchronously. + +## Prefixes and levels + +- `logging.prefix` adds a static scope tag to all records (e.g., app name or environment). +- Transports receive `rec.level` and `rec.levelName`; the framework already filtered below‑level logs. + +```ts +logging: { + level: LogLevel.Warn, + prefix: 'billing‑edge', + transports: [StructuredJsonTransport] +} +``` + +## Best practices + +- Never throw from `log()`; swallow and self‑heal. +- Avoid heavy sync I/O; prefer buffering and async flush. +- Redact sensitive fields before emit (tokens, PII). +- Serialize `Error` objects explicitly (name, message, stack). +- Apply backpressure: if the sink is down, drop or sample rather than blocking the server. diff --git a/docs/live/docs/v/0.2/servers/extensibility/plugins.mdx b/docs/live/docs/v/0.2/servers/extensibility/plugins.mdx new file mode 100644 index 00000000..6c9854cb --- /dev/null +++ b/docs/live/docs/v/0.2/servers/extensibility/plugins.mdx @@ -0,0 +1,51 @@ +--- +title: Plugin Extensions +sidebarTitle: Plugins +slug: servers/extensibility/plugins +icon: puzzle-piece +--- + +**Plugins** add cross-cutting behavior and can also contribute components. Typical uses: auth/session helpers, PII filtering, tracing, logging, caching, error policy, rate-limits. + +## Define a plugin + +```ts +import { Plugin } from '@frontmcp/sdk'; + +@Plugin({ + name: 'Cache Plugin', + description: 'Adds transparent response caching for tools/resources', + providers: [CacheProvider], // plugin-scoped providers + exports: [CacheProvider], // re-export to host app + adapters: [SpecNormalizerAdapter], // optionally attach adapters + tools: [WarmCacheTool], // and tools/resources/prompts if desired + resources: [], + prompts: [], +}) +export default class CachePlugin {} +``` + +Attach a plugin at app scope: + +```ts +@App({ + name: 'Billing', + plugins: [CachePlugin, ObservabilityPlugin], +}) +export default class BillingApp {} +``` + +### What plugins can do + +- Register **providers** (and **export** them to the host app) +- Contribute **adapters**, **tools**, **resources**, **prompts** +- Participate in lifecycle via **hooks** (see _Advanced → Hooks_) + +### Composition + +Plugins compose **depth-first** at the app level. Later plugins can depend on providers exported by earlier ones. + + + Put organization-wide concerns (auth, audit, tracing) in plugins so all generated and inline components inherit the + behavior without boilerplate. + diff --git a/docs/live/docs/v/0.2/servers/extensibility/providers.mdx b/docs/live/docs/v/0.2/servers/extensibility/providers.mdx new file mode 100644 index 00000000..ab9c197e --- /dev/null +++ b/docs/live/docs/v/0.2/servers/extensibility/providers.mdx @@ -0,0 +1,72 @@ +--- +title: Context Providers +sidebarTitle: Providers +slug: servers/extensibility/providers +icon: boxes +--- + +**Providers** are dependency-injected singletons (or scoped singletons) that your apps, tools, adapters, and plugins can use — e.g., config, DB pools, Redis clients, KMS, HTTP clients. + +They’re declared with `@Provider()` and registered at **server** or **app** scope. Resolution is hierarchical: **tool → app → server**. + +## Define a provider + +```ts +import { Provider, ProviderScope } from '@frontmcp/sdk'; + +@Provider({ + name: 'DbProvider', + description: 'Postgres connection pool', + scope: ProviderScope.GLOBAL, // GLOBAL | SESSION | REQUEST +}) +export class DbProvider { + /* create pool, expose query() etc. */ +} +``` + +### Scopes + +- **GLOBAL** (default): one instance per process/worker. Ideal for clients, pools, caches. +- **SESSION**: one instance per authenticated session. Use for per-user credentials or token-bound clients. +- **REQUEST**: one instance per inbound request. Use sparingly (e.g., per-request trace/state). + +## Register providers + +**Server-level** providers (available to all apps): + +```ts +@FrontMcp({ + info: { name: 'Suite', version: '1.0.0' }, + apps: [BillingApp, AnalyticsApp], + providers: [DbProvider, CacheProvider], +}) +export default class Server {} +``` + +**App-level** providers (override or add on top of server-level): + +```ts +@App({ + name: 'Billing', + providers: [BillingConfigProvider], +}) +export default class BillingApp {} +``` + + + You can register **class**, **value**, or **factory** providers. Factories are useful for async initialization or + composing other providers. + + +## Using providers from tools/plugins + +FrontMCP resolves providers for your executors and hooks. Keep your tool logic pure; read side-effects (DB, queues, secrets) via providers. + +- Prefer **GLOBAL** for shared clients. +- Use **SESSION** for user-bound clients (e.g., per-user API token). +- Reserve **REQUEST** for ephemeral state. + + + Provider injection/consumption follows your runtime’s DI rules. In general: register providers at the minimal scope + and let the framework resolve them for tools and hooks at execution time. + diff --git a/docs/live/docs/v/0.2/servers/prompts.mdx b/docs/live/docs/v/0.2/servers/prompts.mdx new file mode 100644 index 00000000..e7b792e0 --- /dev/null +++ b/docs/live/docs/v/0.2/servers/prompts.mdx @@ -0,0 +1,52 @@ +--- +title: Prompts +slug: servers/prompts +icon: message-lines +--- + +Prompts are **reusable prompt templates** with typed arguments. They return an MCP `GetPromptResult` for clients to render or send as messages to a model. + +## Minimal prompt + +```ts +import { Prompt } from '@frontmcp/sdk'; + +@Prompt({ + name: 'Summarize', + title: 'Summarize Text', + description: 'Create a concise summary', + arguments: [{ name: 'text', description: 'Input text', required: true }], +}) +export default class SummarizePrompt { + async execute(args: { text: string }) { + return { + description: 'Summarize the provided text', + messages: [ + { role: 'system', content: 'You are a concise assistant.' }, + { role: 'user', content: args.text }, + ], + }; + } +} +``` + +## Prompt metadata + +```ts +@Prompt({ + name: string, + title?: string, + description?: string, + arguments?: Array<{ + name: string; + description?: string; + required?: boolean; + }>, + icons?: Icon[], +}) +``` + +**Notes** + +- Prompts are discoverable and can be parameterized by clients. +- Use prompts to standardize instructions for common tasks across tools/apps. diff --git a/docs/live/docs/v/0.2/servers/resources.mdx b/docs/live/docs/v/0.2/servers/resources.mdx new file mode 100644 index 00000000..46501a21 --- /dev/null +++ b/docs/live/docs/v/0.2/servers/resources.mdx @@ -0,0 +1,72 @@ +--- +title: Resources +slug: servers/resources +icon: book-open +--- + +Resources provide **readable data** to load into the model’s context. Define fixed `Resource`s (a concrete `uri`) or `ResourceTemplate`s (an RFC 6570 `uriTemplate`). + +## Minimal resource + +```ts +import { Resource } from '@frontmcp/sdk'; + +@Resource({ + name: 'docs-home', + title: 'Docs Home', + uri: 'https://example.com/docs', + mimeType: 'text/html', +}) +export default class DocsHome { + async execute(uri: string) { + return { contents: [{ uri, text: 'Welcome to the docs!' }] }; + } +} +``` + +## Minimal resource template + +```ts +import { ResourceTemplate } from '@frontmcp/sdk'; + +@ResourceTemplate({ + name: 'repo-readme', + title: 'GitHub README', + uriTemplate: 'https://raw.githubusercontent.com/{owner}/{repo}/main/README.md', + mimeType: 'text/markdown', +}) +export default class RepoReadme {} +``` + +--- + +## Resource metadata + +```ts +@Resource({ + name: string, + title?: string, + uri: string, + description?: string, + mimeType?: string, + icons?: Icon[], +}) +``` + +## Resource template metadata + +```ts +@ResourceTemplate({ + name: string, + title?: string, + uriTemplate: string, // RFC 6570 + description?: string, + mimeType?: string, + icons?: Icon[], +}) +``` + +**Tips** + +- Use resources to preload long-form text, schemas, or docs the model should see before calling tools. +- Prefer templates when the shape is stable but the identifiers vary. diff --git a/docs/live/docs/v/0.2/servers/server.mdx b/docs/live/docs/v/0.2/servers/server.mdx new file mode 100644 index 00000000..77ff9638 --- /dev/null +++ b/docs/live/docs/v/0.2/servers/server.mdx @@ -0,0 +1,225 @@ +--- +title: The FrontMCP Server +sidebarTitle: Overview +description: The core FrontMCP server — start with the minimum and scale up with HTTP, sessions, logging, providers, and authentication. +icon: server +--- + +FrontMCP servers are defined with a single decorator, `@FrontMcp({ ... })`. This page shows the **minimal config** and then every **top-level option** you can use. Deep dives live in the pages listed under _Servers_. + +## Minimal server + +```ts +import { FrontMcp } from '@frontmcp/sdk'; +import MyApp from './my.app'; + +@FrontMcp({ + info: { name: 'My Server', version: '0.1.0' }, + apps: [MyApp], +}) +export default class Server {} +``` + +**Required:** + +- `info.name` (string) +- `info.version` (string) +- `apps` (at least one app) + +Everything else is optional with sensible defaults. + +--- + +## Full configuration (at a glance) + +```ts +@FrontMcp({ + /** Required */ + info: { + name: 'Expense MCP Server', + version: '1.0.0', + title?: 'Human title', + websiteUrl?: 'https://example.com', + icons?: Icon[], // MCP Icon[] + }, + apps: [/* App classes */], + + /** Optional */ + serve?: true, // default true (auto-boot) + splitByApp?: false, // app composition mode + providers?: [/* provider classes/factories/values */], + + http?: { + port?: 3001, // default 3001 + entryPath?: '', // MUST match PRM resourcePath in .well-known + hostFactory?: /* custom host */, + }, + + session?: { + sessionMode?: 'stateless' | 'stateful' | ((issuer) => ...), // default 'stateless' + transportIdMode?: 'uuid' | 'jwt' | ((issuer) => ...), // default 'uuid' + }, + + logging?: { + level?: LogLevel.Info, // Debug | VERBOSE | Info | Warn | Error | Off + enableConsole?: true, // default true + prefix?: string, + transports?: [/* custom log transports */], + }, + + /** Server-level default auth (omit if splitByApp: true) */ + auth?: ( + | { type: 'remote', name: string, baseUrl: string, ... } + | { type: 'local', id: string, name: string, ... } + ), +}) +``` + +--- + +## Composition mode + +FrontMCP can host **many apps**. Choose how they’re exposed: + +- **Multi-App (default)**: `splitByApp: false` + One server scope. You _may_ configure **server-level `auth`** and all apps inherit it (apps can still override with app-level auth). + +- **Split-By-App**: `splitByApp: true` + Each app is isolated under its own scope/base path. **Server-level `auth` is disallowed**; configure auth per app. (See _Authentication → Overview_.) + +If you’re offering multiple products or tenants, `splitByApp: true` gives clean separation and per-app auth. + +--- + +## HTTP transport + +```ts +http: { + port?: number; // default 3001 + entryPath?: string; // default ''; MUST match the PRM resourcePath in .well-known + hostFactory?: FrontMcpServer | ((cfg) => FrontMcpServer); +} +``` + +- **Port**: listening port for Streamable HTTP. +- **entryPath**: your MCP JSON-RPC entry (`''` or `'/mcp'`). Must align with discovery. +- **hostFactory**: advanced — provide/construct a custom host implementation. + +--- + +## Sessions & Transport IDs + +```ts +session: { + sessionMode?: 'stateless' | 'stateful' | ((issuer) => SessionMode); + transportIdMode?: 'uuid' | 'jwt' | ((issuer) => TransportIdMode); +} +``` + +- **sessionMode** (default **`'stateless'`**): + +- `'stateless'` → session data is carried in a signed/encrypted JWT. Simple, client-portable; no token refresh of nested providers. +- `'stateful'` → server-side store (e.g., Redis). Minimal JWTs, safer for nested tokens, supports refresh. + +- **transportIdMode** (default **`'uuid'`**): + +- `'uuid'` → per-node, strict transport identity. +- `'jwt'` → signed transport IDs for distributed setups; ties into session verification. + +You can supply functions for `sessionMode` / `transportIdMode` to decide per issuer. + +--- + +## Logging + +```ts +logging: { + level?: LogLevel; // default Info + enableConsole?: boolean; // default true + prefix?: string; + transports?: LogTransportType[]; // custom sinks via @FrontMcpLogTransport +} +``` + +Use custom log transports for shipping logs to external systems; console remains on by default. + +--- + +## Global providers + +```ts +providers: [ + /* Provider classes/factories/values */ +]; +``` + +Define DI-style singletons available to all apps (and their tools/plugins). Scopes are supported at the provider level (`GLOBAL`, `SESSION`, `REQUEST`). + +--- + +## Authentication (server level) + +Server-level `auth` sets the **default** auth for all apps (unless `splitByApp: true`, where auth must be per-app). + +### Remote OAuth (encapsulated external IdP) + +```ts +auth: { + type: 'remote', + name: 'frontegg', + baseUrl: 'https://auth.example.com', + dcrEnabled?: boolean, + clientId?: string | ((info) => string), // for non-DCR via local proxy + mode?: 'orchestrated' | 'transparent', + allowAnonymous?: boolean, + consent?: boolean, + scopes?: string[], + grantTypes?: ['authorization_code','refresh_token'], + authEndpoint?: string, + tokenEndpoint?: string, + registrationEndpoint?: string, + userInfoEndpoint?: string, + jwks?: JSONWebKeySet, + jwksUri?: string, +} +``` + +### Local OAuth (built-in AS) + +```ts +auth: { + type: 'local', + id: 'local', + name: 'Local Auth', + scopes?: string[], + grantTypes?: ['authorization_code','refresh_token'], + allowAnonymous?: boolean, // default true + consent?: boolean, // show tool/resource/prompt consent + jwks?: JSONWebKeySet, // inline keys (optional) + signKey?: JWK | Uint8Array // private key (optional; auto-gen if omitted) +} +``` + + + Apps can also define their own `auth` (and mark themselves `standalone`) to expose an isolated auth surface — useful + when mixing public and private apps under one server. + + +--- + +## Bootstrapping & discovery + +- **`serve`** (default **true**): when true, importing the decorated class **boots the server** via `@frontmcp/core`. +- **Version safety**: on boot, FrontMCP checks that all `@frontmcp/*` packages are aligned and throws a clear “version mismatch” error otherwise. + +If you disable `serve`, you’re responsible for calling the core bootstrap yourself. + +--- + +## Common starting points + +- **Single app, default everything**: minimal sample above. +- **Multiple apps, shared auth**: omit `splitByApp`, set server-level `auth`. +- **Isolated apps with per-app auth**: set `splitByApp: true`, configure `auth` in each app. + +Next up: learn how to structure **Apps**, **Tools**, **Resources**, and **Prompts** in the _Core Components_ section. diff --git a/docs/live/docs/v/0.2/servers/tools.mdx b/docs/live/docs/v/0.2/servers/tools.mdx new file mode 100644 index 00000000..36d96598 --- /dev/null +++ b/docs/live/docs/v/0.2/servers/tools.mdx @@ -0,0 +1,89 @@ +--- +title: Tools +slug: servers/tools +icon: wrench +--- + +Tools are **typed actions** your server can execute. They’re described with Zod schemas and exposed via MCP. Implement as a class with `@Tool({...})` or as a function via `tool()`. + +## Minimal tool (class) + +```ts +import { Tool } from '@frontmcp/sdk'; +import { z } from 'zod'; + +@Tool({ + name: 'greet', + description: 'Greets a user by name', + inputSchema: { name: z.string() }, +}) +export default class GreetTool { + async execute({ name }: { name: string }) { + return `Hello, ${name}!`; + } +} +``` + +Register it on an app: + +```ts +@App({ id: 'hello', name: 'Hello', tools: [GreetTool] }) +export default class HelloApp {} +``` + +## Inline tool (function builder) + +```ts +import { tool } from '@frontmcp/sdk'; +import { z } from 'zod'; + +export const Add = tool({ + name: 'add', + description: 'Add two numbers', + inputSchema: { a: z.number(), b: z.number() }, +})((input) => input.a + input.b); +``` + +Add to app: + +```ts +@App({ name: 'Calc', tools: [Add] }) +class CalcApp {} +``` + +--- + +## Tool metadata + +```ts +@Tool({ + id?: string, + name: string, + description?: string, + inputSchema: ZodSchema, + rawInputSchema?: JSONSchema7, + outputSchema?: ZodSchema, + tags?: string[], + annotations?: { + title?: string, + readOnlyHint?: boolean, + destructiveHint?: boolean, + idempotentHint?: boolean, + openWorldHint?: boolean, + }, + hideFromDiscovery?: boolean, // default false +}) +``` + +**Notes** + +- `annotations` hint model & UI behavior (read-only, idempotent, etc.). +- `hideFromDiscovery` keeps a tool callable but off `tool/list`. +- Tools can attach **per-tool hooks** (see _Advanced → Hooks_). + +--- + +## Return values + +- Return any serializable type; if you provide `outputSchema`, it will validate. +- Errors are surfaced via MCP error responses; you can also throw typed errors inside executors. diff --git a/docs/live/docs/v/0.3/adapters/openapi-adapter.mdx b/docs/live/docs/v/0.3/adapters/openapi-adapter.mdx new file mode 100644 index 00000000..21451dc9 --- /dev/null +++ b/docs/live/docs/v/0.3/adapters/openapi-adapter.mdx @@ -0,0 +1,177 @@ +--- +title: OpenAPI Adapter +slug: adapters/openapi-adapter +sidebarTitle: OpenAPI +description: Generate MCP tools directly from an OpenAPI spec and call them with strong validation. +icon: puzzle-piece +--- + +The OpenAPI Adapter turns an OpenAPI 3.x specification into ready-to-use MCP tools. It is powered by [`mcp-from-openapi`](https://www.npmjs.com/package/mcp-from-openapi), so every generated tool ships with a request mapper, automatic parameter conflict resolution, and validated schemas. + +## Why use it + +- **Zero boilerplate** — convert REST APIs to MCP tools by registering a single adapter. +- **Accurate schemas** — input and output schemas are derived from the spec and compiled to Zod for runtime validation. +- **Multi-auth aware** — map different security schemes to different auth providers with `authProviderMapper` or a `securityResolver`. +- **Safe by default** — the adapter validates your security configuration, prints a risk score, and refuses to expose tools that cannot be authenticated. +- **Mapper visibility** — every tool exposes how validated inputs become path/query/header/body values, making debugging and customization easy. + +## Quick start + +```ts +import { App } from '@frontmcp/sdk'; +import { OpenapiAdapter } from '@frontmcp/adapters'; + +@App({ + id: 'my-api', + name: 'My API MCP Server', + adapters: [ + OpenapiAdapter.init({ + name: 'backend:api', + baseUrl: process.env.API_BASE_URL!, + url: process.env.OPENAPI_SPEC_URL!, + }), + ], +}) +export default class MyApiApp {} +``` + +```ts +import { App } from '@frontmcp/sdk'; +import { OpenapiAdapter } from '@frontmcp/adapters'; + +@App({ + id: 'my-api', + name: 'My API MCP Server', + adapters: [ + OpenapiAdapter.init({ + name: 'backend:api', + baseUrl: process.env.API_BASE_URL!, + url: process.env.OPENAPI_SPEC_URL!, + headersMapper: (authInfo, headers) => { + if (authInfo.token) headers.set('authorization', `Bearer ${authInfo.token}`); + if (authInfo.user?.tenantId) headers.set('x-tenant-id', authInfo.user.tenantId); + return headers; + }, + }), + ], +}) +export default class MyApiApp {} +``` + +## Required options + +- `name` — unique identifier for the adapter (used when disambiguating tool ids). +- `baseUrl` — root URL used when the adapter builds HTTP requests. +- One of `url` (file path or remote URL) or `spec` (an `OpenAPIV3.Document`). + +## Authentication strategies + +The adapter can resolve authentication per tool. Pick the strategy that matches your API. + +### Static headers (server-to-server) + +```ts +OpenapiAdapter.init({ + name: 'billing', + baseUrl: 'https://api.example.com', + url: 'https://api.example.com/openapi.json', + additionalHeaders: { + 'x-api-key': process.env.BILLING_API_KEY!, + }, +}); +``` + +### `authProviderMapper` (recommended for multi-provider OAuth) + +Map each security scheme in your spec to the correct token on `authInfo`. + +```ts +OpenapiAdapter.init({ + name: 'integrations', + baseUrl: 'https://api.example.com', + url: 'https://api.example.com/openapi.json', + authProviderMapper: { + GitHubAuth: (authInfo) => authInfo.user?.githubToken, + SlackAuth: (authInfo) => authInfo.user?.slackToken, + ApiKeyAuth: (authInfo) => authInfo.user?.apiKey, + }, +}); +``` + +### Custom `securityResolver` + +Run arbitrary logic whenever a tool needs credentials. + +```ts +OpenapiAdapter.init({ + name: 'multi-auth', + baseUrl: 'https://api.example.com', + url: 'https://api.example.com/openapi.json', + securityResolver: (tool, authInfo) => { + if (tool.name.startsWith('github_')) { + return { jwt: authInfo.user?.githubToken }; + } + if (tool.name.startsWith('google_')) { + return { jwt: authInfo.user?.googleToken }; + } + return { jwt: authInfo.token }; + }, +}); +``` + +### Include credentials in tool inputs (development only) + +Set `generateOptions.includeSecurityInInput = true` to expose security fields in the tool schema. This is useful while exploring a new API but is flagged as **high risk** and should not be shipped to production. + +## Load and generate options + +Every `loadOptions` and `generateOptions` value is forwarded to [`mcp-from-openapi`](https://www.npmjs.com/package/mcp-from-openapi): + +- `loadOptions` — control validation (`validate`), `$ref` resolution (`dereference`), custom headers, `timeout`, and `followRedirects` when fetching the spec. +- `generateOptions` — filter/rename operations (`filterFn`, `defaultInclude`, `excludeOperationIds`), set `preferredStatusCodes`, keep or drop deprecated endpoints, include multiple responses, surface examples, or customize the naming strategy. +- `includeSecurityInInput`, `includeDeprecated`, `includeAllResponses`, and `includeExamples` map directly to the generator so you decide how much of the spec becomes part of each tool. + +## Understand the mapper + +Each generated tool exposes a `mapper` so you can see how validated inputs become an HTTP request. Conflicting parameter names are automatically renamed. + +```ts +import { OpenAPIToolGenerator } from 'mcp-from-openapi'; + +const generator = await OpenAPIToolGenerator.fromURL('https://api.example.com/openapi.json'); +const [createExpense] = await generator.generateTools(); + +console.log(createExpense.mapper); +// [ +// { inputKey: 'pathId', type: 'path', key: 'id' }, +// { inputKey: 'bodyId', type: 'body', key: 'id' }, +// ... +// ] +``` + +Use the mapper to plug in custom logging, redaction, or request mutation logic via `headersMapper` and `bodyMapper`. + +## Validation & logging + +During `fetch()` the adapter validates your security configuration against the spec and prints: + +- A **risk score** (`low`, `medium`, `high`) so you can catch unsafe configurations early. +- Warnings for missing `authProviderMapper` entries or when credentials would flow through tool inputs. +- A fatal error when a required security scheme cannot be satisfied (the adapter refuses to expose tools until it is fixed). + +Resolve these warnings before exposing the adapter publicly. + +## Tips + +- Combine adapters with plugins (cache, logging, authorization) for consistent cross-cutting behavior. +- Attach multiple OpenAPI adapters to one app; just set a unique `name` value for each instance. +- Use `inputSchemaMapper` or `bodyMapper` to hide secrets from the tool interface while still injecting them into requests. +- Keep `includeSecurityInInput` disabled in production so credentials never transit through the client. + +## Links + +- Demo app: `apps/demo/src/apps/expenses/index.ts` +- Spec used by the demo: https://frontmcp-test.proxy.beeceptor.com/openapi.json +- Generator library: https://www.npmjs.com/package/mcp-from-openapi +- Source code: `libs/adapters/src/openapi` diff --git a/docs/live/docs/v/0.3/deployment/local-dev-server.mdx b/docs/live/docs/v/0.3/deployment/local-dev-server.mdx new file mode 100644 index 00000000..06ebefaf --- /dev/null +++ b/docs/live/docs/v/0.3/deployment/local-dev-server.mdx @@ -0,0 +1,169 @@ +--- +title: Local Dev Server +slug: deployment/local-dev-server +icon: computer +--- + +Run your FrontMCP server locally with hot-reload and verify it with the **FrontMCP Inspector** (zero setup). + +## Prerequisites + +- **Node.js ≥ 22** +- **npm ≥ 10** (pnpm/yarn also supported) + +--- + +## Quick start + +### Option A — New project + +Creates a folder and scaffolds everything for you. + + + ```bash universal + npx frontmcp create my-app + ``` + + + +### Option B — Existing project + +Install and initialize in your current repo: + +```bash universal npm i -D frontmcp @types/node@^20 npx frontmcp init ``` + +`init` adds the required scripts and updates **tsconfig.json** automatically. + +--- + +## Package scripts + +After `create` or `init`, your `package.json` will include: + +```json +{ + "scripts": { + "dev": "frontmcp dev", + "build": "frontmcp build", + "inspect": "frontmcp inspector", + "doctor": "frontmcp doctor" + } +} +``` + +- `frontmcp dev` — run in watch mode with type-checks +- `frontmcp build` — compile to `./dist` (override with `--out-dir`) +- `frontmcp inspector` — launches **@modelcontextprotocol/inspector** with zero setup +- `frontmcp doctor` — verifies Node/npm versions and project configuration + +--- + +## Recommended tsconfig + +`init` writes this for you, but if you prefer to manage it manually: + +```json title="tsconfig.json" +{ + "compilerOptions": { + "target": "es2021", + "module": "esnext", + "lib": ["es2021"], + "moduleResolution": "bundler", + "rootDir": "src", + "outDir": "dist", + "strict": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "sourceMap": true + }, + "include": ["src/**/*.ts"], + "exclude": ["**/*.test.ts", "**/__tests__/**"] +} +``` + +--- + +## Minimal app + +> You can import from `@frontmcp/sdk` directly; no extra install needed. + +**src/main.ts** + +```ts +import 'reflect-metadata'; +import { FrontMcp, LogLevel } from '@frontmcp/sdk'; +import HelloApp from './hello.app'; + +@FrontMcp({ + info: { name: 'Hello MCP', version: '0.1.0' }, + apps: [HelloApp], + http: { port: Number(process.env.PORT) || 3000 }, + logging: { level: LogLevel.Info }, +}) +export default class Server {} +``` + +**src/hello.app.ts** + +```ts +import { App } from '@frontmcp/sdk'; +import Greet from './tools/greet.tool'; + +@App({ id: 'hello', name: 'Hello', tools: [Greet] }) +export default class HelloApp {} +``` + +**src/tools/greet.tool.ts** + +```ts +import { tool } from '@frontmcp/sdk'; +import { z } from 'zod'; + +export default tool({ + name: 'greet', + description: 'Greets a user by name', + // New style: pass Zod fields directly (no z.object wrapper) + inputSchema: { name: z.string() }, +})(({ name }) => `Hello, ${name}!`); +``` + +--- + +## Run the dev server + +```bash universal npm run dev ``` + +Your server will start (default `http://localhost:3000`). The console will print the MCP endpoint. + +--- + +## Inspect locally (zero setup) + +Launch the **FrontMCP Inspector** to exercise tools and messages in a friendly UI: + +```bash universal npm run inspect ``` + +- This runs `npx @modelcontextprotocol/inspector` behind the scenes. +- Point it at your local server URL printed by `dev` (e.g., `http://localhost:3000`). +- Try calling `greet` and watch responses stream back in real time. + +--- + +## Troubleshooting + +- **Check configuration** + +```bash +npm run doctor +``` + +Ensures Node/npm versions, entry detection, and `tsconfig.json` are correct. + +- **Entry detection** + The CLI looks for `package.json.main`; if missing, it falls back to `src/main.ts`. If no entry is found, it will tell you how to create one. + +- **Type errors in dev** + The `dev` command performs async type-checks while watching your files, so you’ll see issues immediately without stopping the server. diff --git a/docs/live/docs/v/0.3/deployment/production-build.mdx b/docs/live/docs/v/0.3/deployment/production-build.mdx new file mode 100644 index 00000000..54ef020e --- /dev/null +++ b/docs/live/docs/v/0.3/deployment/production-build.mdx @@ -0,0 +1,73 @@ +--- +title: Production Build +slug: deployment/production-build +icon: building +--- + +Build a compact Node artifact and run it behind a process manager / reverse proxy. + +## Build + + + + ```bash npm + npm run build + ``` + + ```bash yarn + yarn build + ``` + + ```bash pnpm + pnpm build + ``` + + + +This compiles TypeScript to `dist/` using `tsconfig.build.json`. + +## Start + + + + ```bash npm + NODE_ENV=production PORT=8080 npm start + ``` + + ```bash yarn + NODE_ENV=production PORT=8080 yarn start + ``` + + ```bash pnpm + NODE_ENV=production PORT=8080 pnpm start + ``` + + + +## Recommended runtime setup + +- Use a **process manager** (PM2, systemd) for restarts and logs. +- Put a **reverse proxy** (NGINX, Traefik, Caddy) in front for TLS and path routing. +- Pin matching versions of all `@frontmcp/*` packages. + +### Example NGINX snippet + +```nginx +server { + listen 443 ssl; + server_name mcp.example.com; + + location /mcp/ { + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto https; + proxy_pass http://127.0.0.1:8080/mcp/; + } +} +``` + +## Troubleshooting + +- **Version mismatch** at boot → align all `@frontmcp/*` versions and reinstall. +- **No decorators** working → ensure `experimentalDecorators` + `emitDecoratorMetadata` and `import 'reflect-metadata'` at the top of `main.ts`. +- **Port conflicts** → set `http.port` in `@FrontMcp` or use `PORT` env. diff --git a/docs/live/docs/v/0.3/getting-started/installation.mdx b/docs/live/docs/v/0.3/getting-started/installation.mdx new file mode 100644 index 00000000..a69d780d --- /dev/null +++ b/docs/live/docs/v/0.3/getting-started/installation.mdx @@ -0,0 +1,117 @@ +--- +title: Installation +icon: arrow-down-to-line +--- + +## Prerequisites + +- **Node.js ≥ 22** +- **npm ≥ 10** (pnpm / yarn work too) +- TypeScript project (the init command can set this up) + +--- + +## Option A — Create a new project + +Use the built-in project generator. It **requires** a project name and will create a new folder under your current directory. + + + ```bash universal + npx frontmcp create my-app + ``` + + ```bash pnpm + pnpm dlx frontmcp create my-app + ``` + + ```bash yarn + yarn dlx frontmcp create my-app + ``` + + + +This will: + +- scaffold a FrontMCP project in `./my-app` +- configure `tsconfig.json` for decorators and modern ESM +- generate a `package.json` with helpful scripts +- install required dev dependencies (e.g. TypeScript, tsx, zod, reflect-metadata) + +--- + +## Option B — Add to an existing project + +Install the CLI and Node types (FrontMCP bundles compatible `@frontmcp/sdk` internally—no separate install needed). + + + ```bash npm + npm i -D frontmcp @types/node@^20 + ``` + + ```bash pnpm + pnpm add -D frontmcp @types/node@^20 + ``` + ```bash yarn + yarn add -D frontmcp @types/node@^20 + ``` + + + +Then initialize FrontMCP in your project root: + +```bash +npx frontmcp init +``` + +`init` will: + +- add/update **scripts** in your `package.json` +- ensure your **tsconfig.json** includes required compiler options +- verify a sensible project layout + +--- + +## Package scripts + +After `create` or `init`, you’ll have these scripts: + +```json +{ + "scripts": { + "dev": "frontmcp dev", + "build": "frontmcp build", + "inspect": "frontmcp inspector", + "doctor": "frontmcp doctor" + } +} +``` + +**What they do** + +- `frontmcp dev` — run your server in watch mode (tsx) +- `frontmcp build` — compile your entry with TypeScript (outputs to `./dist` by default) +- `frontmcp inspector` — launch the MCP Inspector (`npx @modelcontextprotocol/inspector`) +- `frontmcp doctor` — validate Node/npm versions, tsconfig, and project setup + +--- + +## Verify your setup + +Run: + +```bash +npm run doctor +``` + + + If anything is missing or misconfigured (Node/npm versions, `tsconfig.json`, scripts), **doctor** will tell you + exactly what to fix. + + +--- + +## Next steps + +- Start developing: `npm run dev` +- Build for distribution: `npm run build` +- Explore tools and messages live: `npm run inspect` diff --git a/docs/live/docs/v/0.3/getting-started/quickstart.mdx b/docs/live/docs/v/0.3/getting-started/quickstart.mdx new file mode 100644 index 00000000..02d819c8 --- /dev/null +++ b/docs/live/docs/v/0.3/getting-started/quickstart.mdx @@ -0,0 +1,91 @@ +--- +title: Quickstart +icon: rocket-launch +--- + +Welcome! This guide will help you quickly set up FrontMCP and run your first MCP server. + +If you haven't already installed FrontMCP, follow the [installation instructions](./installation). + +## 1) Create your FrontMCP server + +```ts +// src/main.ts +import { FrontMcp, LogLevel } from '@frontmcp/sdk'; +import HelloApp from './hello.app'; + +@FrontMcp({ + info: { name: 'Hello MCP', version: '0.1.0' }, + apps: [HelloApp], + http: { port: 3000 }, + logging: { level: LogLevel.INFO }, +}) +export default class HelloServer {} +``` + +## 2) Define an app + +```ts +// src/hello.app.ts +import { App } from '@frontmcp/sdk'; +import GreetTool from './tools/greet.tool'; + +@App({ + id: 'hello', + name: 'Hello App', + tools: [GreetTool], +}) +export default class HelloApp {} +``` + +## 3) Add your first tool + +```ts +// src/tools/greet.tool.ts +import { Tool } from '@frontmcp/sdk'; +import { z } from 'zod'; + +@Tool({ + name: 'greet', + description: 'Greets a user by name', + inputSchema: { name: z.string() }, +}) +export default class GreetTool { + async execute({ name }: { name: string }) { + return `Hello, ${name}!`; + } +} +``` + +## 4) Run it + +Add scripts: + +```json +{ + "scripts": { + "dev": "tsx src/main.ts", + "build": "tsc -p tsconfig.build.json", + "start": "node dist/apps/hello/main.js" + } +} +``` + +Run: + +```bash +npm run dev +# Server listening on http://localhost:3000 +``` + + + FrontMCP speaks **MCP Streamable HTTP**. You can connect any MCP-capable client and call the `greet` tool with + `{"name": "Ada"}`. + + +## What’s next + +- Add auth (local or remote OAuth) +- Use **adapters** to load tools from OpenAPI +- Enable **plugins** like transparent caching +- Wire **hooks** for logging, rate limits, or request transforms diff --git a/docs/live/docs/v/0.3/getting-started/welcome.mdx b/docs/live/docs/v/0.3/getting-started/welcome.mdx new file mode 100644 index 00000000..6694ffb6 --- /dev/null +++ b/docs/live/docs/v/0.3/getting-started/welcome.mdx @@ -0,0 +1,68 @@ +--- +title: Welcome to FrontMCP +sidebarTitle: Welcome to FrontMCP +slug: getting-started/welcome +icon: hand-wave +description: The TypeScript way to build MCP servers with decorators, DI, and Streamable HTTP. +--- + +**FrontMCP is the TypeScript-first framework for MCP.** You write clean, typed code; FrontMCP handles the protocol, transport, and execution flow. + +```ts +import { FrontMcp, App, Tool, ToolContext } from '@frontmcp/sdk'; +import { z } from 'zod'; + +@Tool({ + name: 'add', + description: 'Add two numbers', + inputSchema: { a: z.number(), b: z.number() }, + outputSchema: { result: z.number() }, +}) +export default class AddTool extends ToolContext { + async execute(input: { a: number; b: number }) { + return { + result: input.a + input.b, + }; + } +} + +@App({ + id: 'calc', + name: 'Calculator', + tools: [AddTool], +}) +class CalcApp {} + +@FrontMcp({ + info: { name: 'Demo 🚀', version: '0.1.0' }, + apps: [CalcApp], + auth: { + type: 'remote', + name: 'my-oauth', + baseUrl: 'https://oauth.example.com', + }, +}) +export default class Server {} +``` + +### Why FrontMCP? + +- 🧑‍💻 **TypeScript-native DX** — decorators, Zod, and strong typing end-to-end +- 🧠 **Scoped invoker + DI** — secure, composable execution with hooks +- 🧩 **Adapters & Plugins** — extend your server without boilerplate +- 🔌 **Spec-aligned transport** — Streamable HTTP for modern MCP clients + +### Core concepts + +- **Server** — entry point via `@FrontMcp({...})` +- **App** — a logical bundle of tools via `@App({...})` +- **Tool** — typed units of work via `@Tool({...})` +- **Hooks** — cross-cutting behaviors (auth, logging, rate limits) +- **Adapters/Plugins** — load tools dynamically; enrich behavior + + + FrontMCP follows MCP protocol principles and **secured transport requirements**. You get sessions, streaming, and + validation out of the box—no custom wiring needed. + + +**Build something real**: jump to the [Quickstart](./quickstart) or set up your workspace in [Installation](./installation). diff --git a/docs/live/docs/v/0.3/guides/add-openapi-adapter.mdx b/docs/live/docs/v/0.3/guides/add-openapi-adapter.mdx new file mode 100644 index 00000000..4903d4a1 --- /dev/null +++ b/docs/live/docs/v/0.3/guides/add-openapi-adapter.mdx @@ -0,0 +1,131 @@ +--- +title: Add OpenAPI Adapter +slug: guides/add-openapi-adapter +--- + +The OpenAPI adapter is powered by [`mcp-from-openapi`](https://www.npmjs.com/package/mcp-from-openapi) and can turn any OpenAPI 3.x document into ready-to-run MCP tools. Follow the steps below to add it to your server. + +## 1. Install the adapter + +```bash +npm install @frontmcp/adapters +``` + +(If you scaffolded your project with `frontmcp create`, it is already part of the workspace.) + +## 2. Initialize the adapter inside an app + +```ts +import { App } from '@frontmcp/sdk'; +import { OpenapiAdapter } from '@frontmcp/adapters'; + +@App({ + id: 'expense', + name: 'Expense MCP app', + adapters: [ + OpenapiAdapter.init({ + name: 'backend:api', + url: 'https://frontmcp-test.proxy.beeceptor.com/openapi.json', + baseUrl: 'https://frontmcp-test.proxy.beeceptor.com', + }), + ], +}) +export default class ExpenseMcpApp {} +``` + +Set `name` to something unique per adapter and always provide `baseUrl` so the adapter can build absolute request URLs. + +## 3. Map authentication + +Start with static headers for server-to-server APIs: + +```ts +OpenapiAdapter.init({ + name: 'billing', + baseUrl: 'https://api.example.com', + url: 'https://api.example.com/openapi.json', + additionalHeaders: { + 'x-api-key': process.env.BILLING_API_KEY!, + }, +}); +``` + +For user-scoped APIs, prefer `authProviderMapper` or a custom `securityResolver` so each security scheme pulls the right token from `authInfo`: + +```ts +OpenapiAdapter.init({ + name: 'integrations', + baseUrl: 'https://api.example.com', + url: 'https://api.example.com/openapi.json', + authProviderMapper: { + GitHubAuth: (authInfo) => authInfo.user?.githubToken, + SlackAuth: (authInfo) => authInfo.user?.slackToken, + }, +}); +``` + +```ts +OpenapiAdapter.init({ + name: 'multi-auth', + baseUrl: 'https://api.example.com', + url: 'https://api.example.com/openapi.json', + securityResolver: (tool, authInfo) => { + if (tool.name.startsWith('github_')) return { jwt: authInfo.user?.githubToken }; + if (tool.name.startsWith('google_')) return { jwt: authInfo.user?.googleToken }; + return { jwt: authInfo.token }; + }, +}); +``` + +Avoid leaking credentials through tool inputs; the adapter will flag that configuration as high risk. + +## 4. Tune load & generate options + +You can pass any [`mcp-from-openapi` option](https://www.npmjs.com/package/mcp-from-openapi) via `loadOptions` or `generateOptions`. + +```ts +OpenapiAdapter.init({ + name: 'backend:api', + baseUrl: 'https://api.example.com', + url: 'https://api.example.com/openapi.json', + loadOptions: { + validate: true, + dereference: true, + headers: { Authorization: `Bearer ${process.env.SPEC_TOKEN}` }, + timeout: 8000, + followRedirects: true, + }, + generateOptions: { + defaultInclude: true, + excludeOperationIds: ['deprecatedThing'], + filterFn: (op) => op.path.startsWith('/invoices'), + preferredStatusCodes: [200, 201, 202], + includeExamples: true, + }, +}); +``` + +Use these flags to exclude endpoints, rename tools, or include additional response metadata. + +## 5. Watch the mapper & security logs + +Run `frontmcp dev` and look for the adapter banner. It prints: + +- A security **risk score** (`low`, `medium`, `high`). +- Warnings when a required security scheme cannot be satisfied. +- The number of generated tools. + +Fix warnings before promoting the server. + +## 6. Validate before shipping + +- Keep `generateOptions.includeSecurityInInput` set to `false` in production so credentials never appear in tool inputs. +- If your API requires multiple auth providers, supply an `authProviderMapper` entry for each security scheme. +- When you filter operations, make sure you still expose the operations referenced by your client prompts. + +## Common gotchas + +- Always set `baseUrl`; otherwise the adapter cannot build outbound URLs. +- `filterFn` runs before `excludeOperationIds`. Use the combination to include a subset by default and explicitly drop legacy operations. +- The adapter will throw if a required security scheme lacks a mapping. This is intentional—fix the mapping instead of ignoring the warning. +- `includeSecurityInInput` is for local debugging only. It is logged as **high risk** and should not be deployed. diff --git a/docs/live/docs/v/0.3/guides/caching-and-cache-miss.mdx b/docs/live/docs/v/0.3/guides/caching-and-cache-miss.mdx new file mode 100644 index 00000000..0247e3e0 --- /dev/null +++ b/docs/live/docs/v/0.3/guides/caching-and-cache-miss.mdx @@ -0,0 +1,42 @@ +--- +title: Caching & Cache Miss +slug: guides/caching-and-cache-miss +--- + +Use the Cache plugin to cache tool results and control TTL/refresh behavior. + +Overview + +- Opt in per tool via metadata (e.g., `cache: true` or `{ ttl, slideWindow }`). +- Configure the store at app level: in‑memory or Redis (client or config). +- Cache keys include tool id and validated input; you can extend via hooks. + +Set up (pseudocode) + +``` +import CachePlugin from '@frontmcp/plugins/cache' + +@App({ + plugins: [CachePlugin], // or CachePlugin.init({ defaultTTL: 300 }) +}) +class MyApp {} +``` + +Opt in per tool (pseudocode) + +``` +export const GetReport = tool({ + id: 'get-report', + inputSchema: { id: z.string() }, + cache: { ttl: 600, slideWindow: true }, +})(async (input, session) => { + // expensive computation or remote call + return await fetchReport(input.id); +}); +``` + +Notes + +- Memory store is per‑process; prefer Redis in production. +- `slideWindow` updates the TTL on access; without it, TTL is absolute. +- Hooks can decorate the key or short‑circuit on policy. diff --git a/docs/live/docs/v/0.3/guides/customize-flow-stages.mdx b/docs/live/docs/v/0.3/guides/customize-flow-stages.mdx new file mode 100644 index 00000000..83eb98e9 --- /dev/null +++ b/docs/live/docs/v/0.3/guides/customize-flow-stages.mdx @@ -0,0 +1,36 @@ +--- +title: Customize Flow Stages +slug: guides/customize-flow-stages +--- + +Hook into lifecycle stages to enforce policy, add telemetry, or transform IO. + +Concepts + +- Flows are named pipelines with stages; hooks run at specific stages. +- Kinds: Will (before), Stage (in-place), Did (after), Around (wrap). +- Priority orders execution; higher runs earlier for Will/Stage; Did runs in reverse. + +Example (pseudocode) + +``` + const Tool = FlowHooksOf('tool.execute'); + + class MyHooks { + @Tool.Will('validate', { priority: 100 }) + requireFields(ctx) { + // if (!ctx.input.amount) throw new Error('amount required'); + } + + @Tool.Did('execute') + redact(ctx) { + // ctx.result = redactSensitive(ctx.result); + } + } +``` + +Tips + +- Contribute hooks from a Plugin to reuse across apps. +- Use context to pass data between stages in the same flow. +- See servers/flows and servers/hooks for more details. diff --git a/docs/live/docs/v/0.3/servers/apps.mdx b/docs/live/docs/v/0.3/servers/apps.mdx new file mode 100644 index 00000000..ceb5ed19 --- /dev/null +++ b/docs/live/docs/v/0.3/servers/apps.mdx @@ -0,0 +1,106 @@ +--- +title: Apps +slug: servers/apps +icon: cube +--- + +Apps are how you **group capabilities** in FrontMCP. Each app can contribute **tools**, **resources**, **prompts**, plus **providers**, **adapters**, and **plugins**. Apps may run _locally_ (in this process) or be _remote_ (URL/worker). + +## Minimal local app + +```ts +import { App } from '@frontmcp/sdk'; + +@App({ + id: 'hello', + name: 'Hello App', + tools: [], +}) +export default class HelloApp {} +``` + +Add it to your server: + +```ts +@FrontMcp({ info: { name: 'Demo', version: '0.1.0' }, apps: [HelloApp] }) +export default class Server {} +``` + +--- + +## Local app options + +```ts +@App({ + id?: string, + name: string, + description?: string, + providers?: ProviderType[], + authProviders?: AuthProviderType[], + plugins?: PluginType[], + adapters?: AdapterType[], + tools?: ToolType[], + resources?: ResourceType[], + prompts?: PromptType[], + auth?: AuthOptions, // app‑default auth (overrides server auth) + standalone?: 'includeInParent' | boolean, // isolate scope / expose separately +}) +``` + +**Scoping & auth** + +- If the server uses **`splitByApp: true`**, each app is isolated and **must** configure its own auth (server-level `auth` is disallowed). +- `standalone: true` makes the app expose its own scope/entry; `'includeInParent'` lists it under the parent while keeping isolation. + +**Dependency resolution** + +- Providers resolve **tool → app → server**. +- Plugins/adapters attach at app scope; generated items inherit the app’s policies and provenance. + +--- + +## Remote apps + +Define an app that proxies to a **worker file** or a **URL**: + +```ts +@App({ + name: 'Remote CRM', + urlType: 'url', // 'url' | 'worker' + url: 'https://crm.example.com/mcp', + auth: { type: 'remote', name: 'crm-idp', baseUrl: 'https://idp.example.com' }, + standalone: true, +}) +export default class CrmApp {} +``` + +**Fields** + +```ts +{ + id?: string; + name: string; + description?: string; + urlType: 'worker' | 'url'; + url: string; + auth?: AuthOptions; + standalone?: 'includeInParent' | boolean; +} +``` + +--- + +## Example: app with adapter + providers + +```ts +@App({ + id: 'billing', + name: 'Billing', + providers: [DbProvider, CacheProvider], + adapters: [ + // e.g. OpenAPI adapter that generates tools/resources from a spec + BillingOpenApiAdapter, + ], +}) +export default class BillingApp {} +``` diff --git a/docs/live/docs/v/0.3/servers/authentication/local.mdx b/docs/live/docs/v/0.3/servers/authentication/local.mdx new file mode 100644 index 00000000..de2e31a7 --- /dev/null +++ b/docs/live/docs/v/0.3/servers/authentication/local.mdx @@ -0,0 +1,49 @@ +--- +title: Local OAuth +slug: servers/authentication/local +icon: window +--- + +FrontMCP ships a **built‑in OAuth provider** for first‑party scenarios for development. + +### Configuration + +```ts +auth: { + type: 'local', + id: 'local', + name: 'Local Auth', + scopes?: string[], + grantTypes?: ('authorization_code' | 'refresh_token')[], + allowAnonymous?: boolean, // default true + consent?: boolean, + jwks?: JSONWebKeySet, // inline JWKS (optional) + signKey?: JWK | Uint8Array // private key (optional; auto‑generated if omitted) +} +``` + +### Example (per app, split‑by‑app server) + +```ts +@FrontMcp({ + info: { name: 'Workspace', version: '1.0.0' }, + auth: { type: 'local' }, + apps: [DocsApp, MailApp], + splitByApp: true, +}) +export default class Server {} + +@App({ + name: 'Docs', +}) +export default class DocsApp {} +``` + +When using `splitByApp: true`, define auth per app; server‑level `auth` is not allowed. + +--- + +## Keys & JWKS + +- Omit `signKey` to auto‑generate keys (exposed via the local JWKS endpoint). +- Provide `jwks` or `signKey` to pin keys for stable environments. diff --git a/docs/live/docs/v/0.3/servers/authentication/overview.mdx b/docs/live/docs/v/0.3/servers/authentication/overview.mdx new file mode 100644 index 00000000..8330cdcb --- /dev/null +++ b/docs/live/docs/v/0.3/servers/authentication/overview.mdx @@ -0,0 +1,85 @@ +--- +title: Authentication +sidebarTitle: Overview +slug: servers/authentication/overview +icon: users-between-lines +--- + +FrontMCP supports **Remote OAuth** (connect to an external IdP) and a built‑in **Local OAuth** provider. You can configure auth **at the server** or **per app**: + +- **Server‑level auth**: a default provider for all apps (only when `splitByApp: false`). +- **App‑level auth**: each app defines its own provider; required when `splitByApp: true`. + + + With splitByApp: true, FrontMCP mounts each app at its own base path (for example /billing) + and reuses that scope for OAuth issuers and the /message SSE endpoint so clients automatically target the + right app. + + +Auth drives **session creation**, **token propagation**, and optional **consent** for tools/resources/prompts. + + + If an external provider **doesn’t support Dynamic Client Registration (DCR)**, FrontMCP can front it with a **local + OAuth proxy** that registers/holds the client on your behalf. See *Remote OAuth → Proxy*. + + +--- + +## Where to configure auth + +**Server (multi‑app, shared auth):** + +```ts +@FrontMcp({ + info: { name: 'Expense', version: '1.0.0' }, + apps: [BillingApp, AnalyticsApp], + auth: { type: 'remote', name: 'frontegg', baseUrl: 'https://idp.example.com' }, +}) +export default class Server {} +``` + +**Per app (isolated scopes):** + +```ts +@App({ name: 'Billing', auth: { type: 'local', id: 'local', name: 'Local Auth' } }) +export default class BillingApp {} + +@FrontMcp({ info: { name: 'Suite', version: '1.0.0' }, apps: [BillingApp, AnalyticsApp], splitByApp: true }) +export default class Server {} +``` + +--- + +## Consent & scopes + +Enable consent to let users select the **tools/resources/prompts** they grant, producing a **scoped token**. + +```ts +auth: { + type: 'remote', + name: 'frontegg', + baseUrl: 'https://idp.example.com', + consent: true +} +``` + +You can still send classic OAuth scopes via `scopes: string[]`. + +--- + +## Sessions & transport + +Auth integrates with server sessions (see _Server Overview → Sessions_): + +- `session.sessionMode`: `stateful` keeps tokens server‑side (recommended for nested providers); `stateless` embeds in JWT (simpler). +- `session.transportIdMode`: `uuid` (per‑node) or `jwt` (signed transport IDs for distributed setups). + + + Use **stateful** sessions when working with short‑lived upstream tokens—you’ll get refresh without round‑trips. + + +--- + +## Discovery & well‑known + +Remote providers typically self‑describe via `/.well-known/oauth-authorization-server` and `/.well-known/jwks.json`. You may override endpoints and JWKS inline when needed. diff --git a/docs/live/docs/v/0.3/servers/authentication/remote-proxy.mdx b/docs/live/docs/v/0.3/servers/authentication/remote-proxy.mdx new file mode 100644 index 00000000..67194067 --- /dev/null +++ b/docs/live/docs/v/0.3/servers/authentication/remote-proxy.mdx @@ -0,0 +1,40 @@ +--- +title: Remote OAuth — Proxy +slug: servers/authentication/remote-proxy +icon: share +--- + +Some IdPs don’t support **Dynamic Client Registration (DCR)**. FrontMCP can front them with a **local OAuth proxy** that: + +1. Exposes OAuth endpoints locally under your server/app scope. +2. Registers a client using your provided `clientId` (or derives one via function). +3. Issues/validates tokens while exchanging with the upstream IdP. + +### Configure + +```ts +@FrontMcp({ + info: { name: 'Suite', version: '1.0.0' }, + apps: [SalesApp], + auth: { + type: 'remote', + name: 'legacy-idp', + baseUrl: 'https://legacy-idp.example.com', + dcrEnabled: false, + clientId: 'my-preprovisioned-client', + consent: true, + }, +}) +export default class Server {} +``` + + + At runtime, the local proxy maintains client registration and keys while delegating user authentication to the + upstream IdP. + + +--- + +## Endpoint overrides + +If your IdP requires custom endpoints or static keys, set `authEndpoint`, `tokenEndpoint`, `registrationEndpoint`, `userInfoEndpoint`, `jwks`, or `jwksUri` directly. diff --git a/docs/live/docs/v/0.3/servers/authentication/remote.mdx b/docs/live/docs/v/0.3/servers/authentication/remote.mdx new file mode 100644 index 00000000..efef6a50 --- /dev/null +++ b/docs/live/docs/v/0.3/servers/authentication/remote.mdx @@ -0,0 +1,61 @@ +--- +title: Remote OAuth +slug: servers/authentication/remote +icon: user-shield +--- + +Use a remote identity provider (IdP) like Frontegg, Auth0, Azure Entra, etc. + +### Configuration + +```ts +auth: { + type: 'remote', + name: 'frontegg', + baseUrl: 'https://auth.example.com', + dcrEnabled?: boolean, + clientId?: string | ((clientInfo: { clientId: string }) => string), + mode?: 'orchestrated' | 'transparent', + allowAnonymous?: boolean, + consent?: boolean, + scopes?: string[], + grantTypes?: ('authorization_code' | 'refresh_token')[], + authEndpoint?: string, + tokenEndpoint?: string, + registrationEndpoint?: string, + userInfoEndpoint?: string, + jwks?: JSONWebKeySet, + jwksUri?: string, +} +``` + +### Example (server‑level) + +```ts +@FrontMcp({ + info: { name: 'Expense MCP', version: '1.0.0' }, + apps: [ExpenseApp], + auth: { type: 'remote', name: 'frontegg', baseUrl: 'https://auth.example.com', consent: true }, +}) +export default class Server {} +``` + +### Example (per app) + +```ts +@App({ + name: 'CRM', + auth: { type: 'remote', name: 'crm', baseUrl: 'https://idp.example.com', scopes: ['openid', 'email'] }, + standalone: true, +}) +export default class CrmApp {} +``` + +Use `standalone: true` to expose the app’s auth surface under its own scope/entry. + +--- + +## DCR vs non‑DCR + +- **`dcrEnabled: true`** → FrontMCP registers the client dynamically at the IdP. +- **`dcrEnabled: false`** → supply `clientId` and use a **local OAuth proxy** to handle registration/storage. See _Remote OAuth → Proxy_. diff --git a/docs/live/docs/v/0.3/servers/authentication/token.mdx b/docs/live/docs/v/0.3/servers/authentication/token.mdx new file mode 100644 index 00000000..344381d4 --- /dev/null +++ b/docs/live/docs/v/0.3/servers/authentication/token.mdx @@ -0,0 +1,40 @@ +--- +title: Token & Session +slug: servers/authentication/token +icon: key +--- + +Authorization determines **what** a token may do. + +## OAuth scopes + +Provide standard scopes to external IdPs: + +```ts +auth: { + type: 'remote', + name: 'frontegg', + baseUrl: 'https://idp.example.com', + scopes: ['openid','profile','email'] +} +``` + +## Tool/resource/prompt consent + +Set `consent: true` to display a **post‑login consent** listing your registered **tools/resources/prompts**. The issued access token includes the selected grants. + +## Modes (Remote OAuth) + +Use the `mode` field to reflect deployment topology: + +- `transparent` (default): your server acts as a regular confidential client. +- `orchestrated`: gateway coordinates multiple apps/providers under one umbrella token (used in advanced multi‑app setups). + +When `splitByApp: true`, configure auth **per app**; server‑level `auth` is disallowed. + +--- + +## Token lifetimes & sessions + +- **Stateful sessions**: tokens are encrypted server‑side; clients hold a lightweight reference. Smooth refresh. +- **Stateless sessions**: tokens ride inside JWT; simple but no silent refresh of upstream tokens. diff --git a/docs/live/docs/v/0.3/servers/extensibility/adapters.mdx b/docs/live/docs/v/0.3/servers/extensibility/adapters.mdx new file mode 100644 index 00000000..682d62b2 --- /dev/null +++ b/docs/live/docs/v/0.3/servers/extensibility/adapters.mdx @@ -0,0 +1,46 @@ +--- +title: Adapters Transformers +sidebarTitle: Adapters +slug: servers/extensibility/adapters +icon: plug +--- + +**Adapters** convert **external definitions or systems** into FrontMCP components (tools, resources, prompts). Common examples: + +- **OpenAPI** → generate typed **tools** from REST endpoints +- **File system / blobs** → expose documents as **resources** +- **Custom backends** → synthesize prompts/resources from proprietary schemas + +## Define an adapter + +```ts +import { Adapter } from '@frontmcp/sdk'; + +@Adapter({ + name: 'Billing OpenAPI Loader', + description: 'Generates tools/resources from the Billing API spec', +}) +export default class BillingOpenApiAdapter { + // implement adapter-specific logic to register generated tools/resources/prompts +} +``` + +Register the adapter on an app: + +```ts +@App({ + name: 'Billing', + adapters: [BillingOpenApiAdapter], +}) +export default class BillingApp {} +``` + +### Behavior + +- Generated items inherit the **app’s plugins, providers, and policies** and are tagged with provenance. +- Adapters can contribute **tools**, **resources**, and **prompts** simultaneously. + + + Keep adapters **pure**: read a spec or source, generate components, and let app-level plugins handle cross-cutting + concerns like auth, rate limiting, or caching. + diff --git a/docs/live/docs/v/0.3/servers/extensibility/logging.mdx b/docs/live/docs/v/0.3/servers/extensibility/logging.mdx new file mode 100644 index 00000000..88169522 --- /dev/null +++ b/docs/live/docs/v/0.3/servers/extensibility/logging.mdx @@ -0,0 +1,191 @@ +--- +title: Logging Transports +sidebarTitle: Logging +slug: servers/extensibility/logging +icon: receipt +description: Create and register custom log transports for FrontMCP. +--- + +FrontMCP logging is extensible. In addition to the default console logger, you can register one or more custom transports via the server config. + +## Register a transport + +```ts +@FrontMcp({ + info: { name: 'Demo', version: '0.1.0' }, + apps: [App], + logging: { + level: LogLevel.Info, + enableConsole: true, // set false to disable the built‑in console transport + transports: [StructuredJsonTransport, HttpBatchTransport], + }, +}) +export default class Server {} +``` + +## Transport contract + +A transport is a class decorated with `@LogTransport({...})` that implements `LogTransportInterface`. + +```ts +export interface LogRecord { + level: LogLevel; + levelName: string; + message: string; + args: unknown[]; + timestamp: Date; + prefix: string; +} + +export type LogFn = (msg?: any, ...args: any[]) => void; + +export abstract class LogTransportInterface { + abstract log(rec: LogRecord): void; +} +``` + +The framework filters by the configured `logging.level` before calling transports. + +> Transports should never throw. Handle errors internally and keep I/O non‑blocking (use buffering/batching for remote sinks). + +## Built‑in: Console + +The default console transport formats messages with ANSI (when TTY) and falls back to plain text otherwise. + +```ts +@LogTransport({ + name: 'ConsoleLogger', + description: 'Default console logger', +}) +export class ConsoleLogTransportInstance extends LogTransportInterface { + log(rec: LogRecord): void { + const fn = this.bind(rec.level, rec.prefix); + fn(String(rec.message), ...rec.args); + } + // ...see source for details +} +``` + +Disable it by setting `enableConsole: false` in your server config. + +## Example: Structured JSON (JSONL) + +Emit machine‑readable logs (one JSON per line). Useful for file shipping agents or centralized logging. + +```ts +import { LogTransport, LogTransportInterface, LogRecord } from '@frontmcp/sdk'; + +@LogTransport({ + name: 'StructuredJsonTransport', + description: 'Writes JSONL log records to stdout', +}) +export class StructuredJsonTransport extends LogTransportInterface { + log(rec: LogRecord): void { + try { + const payload = { + ts: rec.timestamp.toISOString(), + level: rec.levelName, // e.g. INFO + levelValue: rec.level, // numeric + prefix: rec.prefix || undefined, + msg: stringify(rec.message), + args: rec.args?.map(stringify), + }; + // Avoid console formatting; write raw line + process.stdout.write(JSON.stringify(payload) + '\n'); + } catch (err) { + // Never throw from a transport + } + } +} + +function stringify(x: unknown) { + if (x instanceof Error) { + return { name: x.name, message: x.message, stack: x.stack }; + } + try { + return typeof x === 'string' ? x : JSON.parse(JSON.stringify(x)); + } catch { + return String(x); + } +} +``` + +Register it: + +```ts +logging: { level: LogLevel.Info, enableConsole: false, transports: [StructuredJsonTransport] } +``` + +## Example: HTTP batch transport (non‑blocking) + +Buffer records in memory and POST them in batches. Implements basic retry with backoff. + +```ts +import { LogTransport, LogTransportInterface, LogRecord } from '@frontmcp/sdk'; + +@LogTransport({ + name: 'HttpBatchTransport', + description: 'POST logs in batches', +}) +export class HttpBatchTransport extends LogTransportInterface { + private queue: any[] = []; + private timer: NodeJS.Timeout | null = null; + private flushing = false; + private readonly maxBatch = 50; + private readonly flushMs = 1000; + + constructor(private endpoint = process.env.LOG_ENDPOINT || 'https://logs.example.com/ingest') { + super(); + } + + log(rec: LogRecord): void { + this.queue.push({ + ts: rec.timestamp.toISOString(), + lvl: rec.levelName, + pfx: rec.prefix || undefined, + msg: String(rec.message), + args: safeArgs(rec.args), + }); + if (this.queue.length >= this.maxBatch) this.flush(); + else if (!this.timer) this.timer = setTimeout(() => this.flush(), this.flushMs); + } + + private async flush() { + // TODO: Implement batch flush logic + // - Extract batch from queue + // - POST to this.endpoint + // - Handle errors and implement retry with backoff + // - Reset flushing flag and timer + } +} + +function safeArgs(a: unknown[]) { + return (a || []).map((x) => (x instanceof Error ? { name: x.name, message: x.message, stack: x.stack } : x)); +} +``` + +Notes: + +- Keep batches small and time‑bounded; avoid blocking the event loop. +- On process exit, you may add a `beforeExit`/`SIGTERM` handler to flush synchronously. + +## Prefixes and levels + +- `logging.prefix` adds a static scope tag to all records (e.g., app name or environment). +- Transports receive `rec.level` and `rec.levelName`; the framework already filtered below‑level logs. + +```ts +logging: { + level: LogLevel.Warn, + prefix: 'billing‑edge', + transports: [StructuredJsonTransport] +} +``` + +## Best practices + +- Never throw from `log()`; swallow and self‑heal. +- Avoid heavy sync I/O; prefer buffering and async flush. +- Redact sensitive fields before emit (tokens, PII). +- Serialize `Error` objects explicitly (name, message, stack). +- Apply backpressure: if the sink is down, drop or sample rather than blocking the server. diff --git a/docs/live/docs/v/0.3/servers/extensibility/plugins.mdx b/docs/live/docs/v/0.3/servers/extensibility/plugins.mdx new file mode 100644 index 00000000..6c9854cb --- /dev/null +++ b/docs/live/docs/v/0.3/servers/extensibility/plugins.mdx @@ -0,0 +1,51 @@ +--- +title: Plugin Extensions +sidebarTitle: Plugins +slug: servers/extensibility/plugins +icon: puzzle-piece +--- + +**Plugins** add cross-cutting behavior and can also contribute components. Typical uses: auth/session helpers, PII filtering, tracing, logging, caching, error policy, rate-limits. + +## Define a plugin + +```ts +import { Plugin } from '@frontmcp/sdk'; + +@Plugin({ + name: 'Cache Plugin', + description: 'Adds transparent response caching for tools/resources', + providers: [CacheProvider], // plugin-scoped providers + exports: [CacheProvider], // re-export to host app + adapters: [SpecNormalizerAdapter], // optionally attach adapters + tools: [WarmCacheTool], // and tools/resources/prompts if desired + resources: [], + prompts: [], +}) +export default class CachePlugin {} +``` + +Attach a plugin at app scope: + +```ts +@App({ + name: 'Billing', + plugins: [CachePlugin, ObservabilityPlugin], +}) +export default class BillingApp {} +``` + +### What plugins can do + +- Register **providers** (and **export** them to the host app) +- Contribute **adapters**, **tools**, **resources**, **prompts** +- Participate in lifecycle via **hooks** (see _Advanced → Hooks_) + +### Composition + +Plugins compose **depth-first** at the app level. Later plugins can depend on providers exported by earlier ones. + + + Put organization-wide concerns (auth, audit, tracing) in plugins so all generated and inline components inherit the + behavior without boilerplate. + diff --git a/docs/live/docs/v/0.3/servers/extensibility/providers.mdx b/docs/live/docs/v/0.3/servers/extensibility/providers.mdx new file mode 100644 index 00000000..ab9c197e --- /dev/null +++ b/docs/live/docs/v/0.3/servers/extensibility/providers.mdx @@ -0,0 +1,72 @@ +--- +title: Context Providers +sidebarTitle: Providers +slug: servers/extensibility/providers +icon: boxes +--- + +**Providers** are dependency-injected singletons (or scoped singletons) that your apps, tools, adapters, and plugins can use — e.g., config, DB pools, Redis clients, KMS, HTTP clients. + +They’re declared with `@Provider()` and registered at **server** or **app** scope. Resolution is hierarchical: **tool → app → server**. + +## Define a provider + +```ts +import { Provider, ProviderScope } from '@frontmcp/sdk'; + +@Provider({ + name: 'DbProvider', + description: 'Postgres connection pool', + scope: ProviderScope.GLOBAL, // GLOBAL | SESSION | REQUEST +}) +export class DbProvider { + /* create pool, expose query() etc. */ +} +``` + +### Scopes + +- **GLOBAL** (default): one instance per process/worker. Ideal for clients, pools, caches. +- **SESSION**: one instance per authenticated session. Use for per-user credentials or token-bound clients. +- **REQUEST**: one instance per inbound request. Use sparingly (e.g., per-request trace/state). + +## Register providers + +**Server-level** providers (available to all apps): + +```ts +@FrontMcp({ + info: { name: 'Suite', version: '1.0.0' }, + apps: [BillingApp, AnalyticsApp], + providers: [DbProvider, CacheProvider], +}) +export default class Server {} +``` + +**App-level** providers (override or add on top of server-level): + +```ts +@App({ + name: 'Billing', + providers: [BillingConfigProvider], +}) +export default class BillingApp {} +``` + + + You can register **class**, **value**, or **factory** providers. Factories are useful for async initialization or + composing other providers. + + +## Using providers from tools/plugins + +FrontMCP resolves providers for your executors and hooks. Keep your tool logic pure; read side-effects (DB, queues, secrets) via providers. + +- Prefer **GLOBAL** for shared clients. +- Use **SESSION** for user-bound clients (e.g., per-user API token). +- Reserve **REQUEST** for ephemeral state. + + + Provider injection/consumption follows your runtime’s DI rules. In general: register providers at the minimal scope + and let the framework resolve them for tools and hooks at execution time. + diff --git a/docs/live/docs/v/0.3/servers/prompts.mdx b/docs/live/docs/v/0.3/servers/prompts.mdx new file mode 100644 index 00000000..e7b792e0 --- /dev/null +++ b/docs/live/docs/v/0.3/servers/prompts.mdx @@ -0,0 +1,52 @@ +--- +title: Prompts +slug: servers/prompts +icon: message-lines +--- + +Prompts are **reusable prompt templates** with typed arguments. They return an MCP `GetPromptResult` for clients to render or send as messages to a model. + +## Minimal prompt + +```ts +import { Prompt } from '@frontmcp/sdk'; + +@Prompt({ + name: 'Summarize', + title: 'Summarize Text', + description: 'Create a concise summary', + arguments: [{ name: 'text', description: 'Input text', required: true }], +}) +export default class SummarizePrompt { + async execute(args: { text: string }) { + return { + description: 'Summarize the provided text', + messages: [ + { role: 'system', content: 'You are a concise assistant.' }, + { role: 'user', content: args.text }, + ], + }; + } +} +``` + +## Prompt metadata + +```ts +@Prompt({ + name: string, + title?: string, + description?: string, + arguments?: Array<{ + name: string; + description?: string; + required?: boolean; + }>, + icons?: Icon[], +}) +``` + +**Notes** + +- Prompts are discoverable and can be parameterized by clients. +- Use prompts to standardize instructions for common tasks across tools/apps. diff --git a/docs/live/docs/v/0.3/servers/resources.mdx b/docs/live/docs/v/0.3/servers/resources.mdx new file mode 100644 index 00000000..46501a21 --- /dev/null +++ b/docs/live/docs/v/0.3/servers/resources.mdx @@ -0,0 +1,72 @@ +--- +title: Resources +slug: servers/resources +icon: book-open +--- + +Resources provide **readable data** to load into the model’s context. Define fixed `Resource`s (a concrete `uri`) or `ResourceTemplate`s (an RFC 6570 `uriTemplate`). + +## Minimal resource + +```ts +import { Resource } from '@frontmcp/sdk'; + +@Resource({ + name: 'docs-home', + title: 'Docs Home', + uri: 'https://example.com/docs', + mimeType: 'text/html', +}) +export default class DocsHome { + async execute(uri: string) { + return { contents: [{ uri, text: 'Welcome to the docs!' }] }; + } +} +``` + +## Minimal resource template + +```ts +import { ResourceTemplate } from '@frontmcp/sdk'; + +@ResourceTemplate({ + name: 'repo-readme', + title: 'GitHub README', + uriTemplate: 'https://raw.githubusercontent.com/{owner}/{repo}/main/README.md', + mimeType: 'text/markdown', +}) +export default class RepoReadme {} +``` + +--- + +## Resource metadata + +```ts +@Resource({ + name: string, + title?: string, + uri: string, + description?: string, + mimeType?: string, + icons?: Icon[], +}) +``` + +## Resource template metadata + +```ts +@ResourceTemplate({ + name: string, + title?: string, + uriTemplate: string, // RFC 6570 + description?: string, + mimeType?: string, + icons?: Icon[], +}) +``` + +**Tips** + +- Use resources to preload long-form text, schemas, or docs the model should see before calling tools. +- Prefer templates when the shape is stable but the identifiers vary. diff --git a/docs/live/docs/v/0.3/servers/server.mdx b/docs/live/docs/v/0.3/servers/server.mdx new file mode 100644 index 00000000..17b16e4b --- /dev/null +++ b/docs/live/docs/v/0.3/servers/server.mdx @@ -0,0 +1,225 @@ +--- +title: The FrontMCP Server +sidebarTitle: Overview +description: The core FrontMCP server — start with the minimum and scale up with HTTP, sessions, logging, providers, and authentication. +icon: server +--- + +FrontMCP servers are defined with a single decorator, `@FrontMcp({ ... })`. This page shows the **minimal config** and then every **top-level option** you can use. Deep dives live in the pages listed under _Servers_. + +## Minimal server + +```ts +import { FrontMcp } from '@frontmcp/sdk'; +import MyApp from './my.app'; + +@FrontMcp({ + info: { name: 'My Server', version: '0.1.0' }, + apps: [MyApp], +}) +export default class Server {} +``` + +**Required:** + +- `info.name` (string) +- `info.version` (string) +- `apps` (at least one app) + +Everything else is optional with sensible defaults. + +--- + +## Full configuration (at a glance) + +```ts +@FrontMcp({ + /** Required */ + info: { + name: 'Expense MCP Server', + version: '1.0.0', + title?: 'Human title', + websiteUrl?: 'https://example.com', + icons?: Icon[], // MCP Icon[] + }, + apps: [/* App classes */], + + /** Optional */ + serve?: true, // default true (auto-boot) + splitByApp?: false, // app composition mode + providers?: [/* provider classes/factories/values */], + + http?: { + port?: 3001, // default 3001 + entryPath?: '', // MUST match PRM resourcePath in .well-known + hostFactory?: /* custom host */, + }, + + session?: { + sessionMode?: 'stateless' | 'stateful' | ((issuer) => ...), // default 'stateless' + transportIdMode?: 'uuid' | 'jwt' | ((issuer) => ...), // default 'uuid' + }, + + logging?: { + level?: LogLevel.Info, // Debug | VERBOSE | Info | Warn | Error | Off + enableConsole?: true, // default true + prefix?: string, + transports?: [/* custom log transports */], + }, + + /** Server-level default auth (omit if splitByApp: true) */ + auth?: ( + | { type: 'remote', name: string, baseUrl: string, ... } + | { type: 'local', id: string, name: string, ... } + ), +}) +``` + +--- + +## Composition mode + +FrontMCP can host **many apps**. Choose how they’re exposed: + +- **Multi-App (default)**: `splitByApp: false` + One server scope. You _may_ configure **server-level `auth`** and all apps inherit it (apps can still override with app-level auth). + +- **Split-By-App**: `splitByApp: true` + Each app is isolated under its own scope/base path (for example `/billing`). Streamable HTTP, the `/message` SSE endpoint, and OAuth issuers reuse that scope automatically. **Server-level `auth` is disallowed**; configure auth per app. (See _Authentication → Overview_.) + +If you’re offering multiple products or tenants, `splitByApp: true` gives clean separation and per-app auth. + +--- + +## HTTP transport + +```ts +http: { + port?: number; // default 3001 + entryPath?: string; // default ''; MUST match the PRM resourcePath in .well-known + hostFactory?: FrontMcpServer | ((cfg) => FrontMcpServer); +} +``` + +- **Port**: listening port for Streamable HTTP. +- **entryPath**: your MCP JSON-RPC entry (`''` or `'/mcp'`). Must align with discovery. +- **hostFactory**: advanced — provide/construct a custom host implementation. +- **Split-by-app scopes**: when `splitByApp` is enabled, clients hit `/` (for example `/mcp/billing`) and subscribe via `//message`; FrontMCP handles the prefixing. + +--- + +## Sessions & Transport IDs + +```ts +session: { + sessionMode?: 'stateless' | 'stateful' | ((issuer) => SessionMode); + transportIdMode?: 'uuid' | 'jwt' | ((issuer) => TransportIdMode); +} +``` + +- **sessionMode** (default **`'stateless'`**): + +- `'stateless'` → session data is carried in a signed/encrypted JWT. Simple, client-portable; no token refresh of nested providers. +- `'stateful'` → server-side store (e.g., Redis). Minimal JWTs, safer for nested tokens, supports refresh. + +- **transportIdMode** (default **`'uuid'`**): + +- `'uuid'` → per-node, strict transport identity. +- `'jwt'` → signed transport IDs for distributed setups; ties into session verification. + +You can supply functions for `sessionMode` / `transportIdMode` to decide per issuer. + +--- + +## Logging + +```ts +logging: { + level?: LogLevel; // default Info + enableConsole?: boolean; // default true + prefix?: string; + transports?: LogTransportType[]; // custom sinks via @FrontMcpLogTransport +} +``` + +Use custom log transports for shipping logs to external systems; console remains on by default. + +--- + +## Global providers + +```ts +providers: [ + /* Provider classes/factories/values */ +]; +``` + +Define DI-style singletons available to all apps (and their tools/plugins). Scopes are supported at the provider level (`GLOBAL`, `SESSION`, `REQUEST`). + +--- + +## Authentication (server level) + +Server-level `auth` sets the **default** auth for all apps (unless `splitByApp: true`, where auth must be per-app). + +### Remote OAuth (encapsulated external IdP) + +```ts +auth: { + type: 'remote', + name: 'frontegg', + baseUrl: 'https://auth.example.com', + dcrEnabled?: boolean, + clientId?: string | ((info) => string), // for non-DCR via local proxy + mode?: 'orchestrated' | 'transparent', + allowAnonymous?: boolean, + consent?: boolean, + scopes?: string[], + grantTypes?: ['authorization_code','refresh_token'], + authEndpoint?: string, + tokenEndpoint?: string, + registrationEndpoint?: string, + userInfoEndpoint?: string, + jwks?: JSONWebKeySet, + jwksUri?: string, +} +``` + +### Local OAuth (built-in AS) + +```ts +auth: { + type: 'local', + id: 'local', + name: 'Local Auth', + scopes?: string[], + grantTypes?: ['authorization_code','refresh_token'], + allowAnonymous?: boolean, // default true + consent?: boolean, // show tool/resource/prompt consent + jwks?: JSONWebKeySet, // inline keys (optional) + signKey?: JWK | Uint8Array // private key (optional; auto-gen if omitted) +} +``` + + + Apps can also define their own `auth` (and mark themselves `standalone`) to expose an isolated auth surface — useful + when mixing public and private apps under one server. + + +--- + +## Bootstrapping & discovery + +- **Version safety**: on boot, FrontMCP checks that all `@frontmcp/*` packages are aligned and throws a clear “version mismatch” error otherwise. + +If you disable `serve`, you’re responsible for calling the core bootstrap yourself. + +--- + +## Common starting points + +- **Single app, default everything**: minimal sample above. +- **Multiple apps, shared auth**: omit `splitByApp`, set server-level `auth`. +- **Isolated apps with per-app auth**: set `splitByApp: true`, configure `auth` in each app. + +Next up: learn how to structure **Apps**, **Tools**, **Resources**, and **Prompts** in the _Core Components_ section. diff --git a/docs/live/docs/v/0.3/servers/tools.mdx b/docs/live/docs/v/0.3/servers/tools.mdx new file mode 100644 index 00000000..a04fae49 --- /dev/null +++ b/docs/live/docs/v/0.3/servers/tools.mdx @@ -0,0 +1,114 @@ +--- +title: Tools +slug: servers/tools +icon: wrench +--- + +Tools are **typed actions** your server can execute. They’re described with Zod schemas and exposed via MCP. Implement as a class with `@Tool({...})` or as a function via `tool()`. + +## Minimal tool (class) + +```ts +import { Tool } from '@frontmcp/sdk'; +import { z } from 'zod'; + +@Tool({ + name: 'greet', + description: 'Greets a user by name', + inputSchema: { name: z.string() }, +}) +export default class GreetTool { + async execute({ name }: { name: string }) { + return `Hello, ${name}!`; + } +} +``` + +Register it on an app: + +```ts +@App({ id: 'hello', name: 'Hello', tools: [GreetTool] }) +export default class HelloApp {} +``` + +## Inline tool (function builder) + +```ts +import { tool } from '@frontmcp/sdk'; +import { z } from 'zod'; + +export const Add = tool({ + name: 'add', + description: 'Add two numbers', + inputSchema: { a: z.number(), b: z.number() }, +})((input) => input.a + input.b); +``` + +Add to app: + +```ts +@App({ name: 'Calc', tools: [Add] }) +class CalcApp {} +``` + +--- + +## Tool metadata + +```ts +@Tool({ + id?: string, + name: string, + description?: string, + inputSchema: { [key: string]: z.ZodTypeAny } | z.ZodObject, + rawInputSchema?: JSONSchema7, + outputSchema?: 'string' | 'number' | 'boolean' | 'date' | 'image' | 'audio' | 'resource' | 'resource_link' | z.ZodTypeAny | readonly ('string' | 'number' | 'boolean' | 'date' | 'image' | 'audio' | 'resource' | 'resource_link' | z.ZodTypeAny)[], + tags?: string[], + annotations?: { + title?: string, + readOnlyHint?: boolean, + destructiveHint?: boolean, + idempotentHint?: boolean, + openWorldHint?: boolean, + }, + hideFromDiscovery?: boolean, // default false +}) +``` + +**Notes** + +- `annotations` hint model & UI behavior (read-only, idempotent, etc.). +- `hideFromDiscovery` keeps a tool callable but off `tool/list`. +- Tools can attach **per-tool hooks** (see _Advanced → Hooks_). +- `rawInputSchema` lets you share a JSON Schema version of the input (handy for inspector tooling) while still passing a raw Zod shape to `inputSchema`. + +### Input & output schema shapes + +`inputSchema` can be a full `z.object({...})` or a raw shape (`{ name: z.string() }`). The SDK wraps raw shapes in an object internally, so you can keep declarations terse. + +`outputSchema` now accepts: + +- Literal primitives (`'string'`, `'number'`, `'boolean'`, `'date'`) when you return scalars. +- `'image'`, `'audio'`, `'resource'`, or `'resource_link'` when you emit MCP resource descriptors. +- Any Zod schema (objects, unions, arrays, discriminated unions, etc.). +- An array of the above to build tuple-like content (each entry becomes a separate structured content item). + +```ts +@Tool({ + name: 'add', + description: 'Add two numbers and echo the math', + inputSchema: { a: z.number(), b: z.number() }, + outputSchema: ['string', 'number'], +}) +export default class AddTool { + async execute({ a, b }: { a: number; b: number }) { + const result = a + b; + return [`${a} + ${b} = ${result}`, result]; + } +} +``` + +## Return values + +- Return primitives, structured objects, or tuple-like arrays. When `outputSchema` is provided (literal or Zod), the SDK validates the response and propagates the right metadata to clients. +- Errors are surfaced via MCP error responses; you can also throw typed errors inside executors. diff --git a/docs/live/snippets/card.jsx b/docs/live/snippets/card.jsx new file mode 100644 index 00000000..92601fd3 --- /dev/null +++ b/docs/live/snippets/card.jsx @@ -0,0 +1,105 @@ +// BlogCard.tsx + +export const BlogCard = ({ + author, + date, + title, + link, + img, // light image + imgDark, // optional dark image + time, + draft, + children, +}) => { + if (draft) { + return null; + } + return ( +
+ + {/* IMAGE (left on desktop, full-width on mobile) */} +
+ {imgDark ? ( + <> + {/* light mode image */} + {title} + {/* dark mode image */} + {title} + + ) : ( + // single image if you don't pass imgDark + {title} + )} +
+ + {/* CONTENT */} +
+ ); +}; diff --git a/docs/live/style.css b/docs/live/style.css new file mode 100644 index 00000000..a3d08689 --- /dev/null +++ b/docs/live/style.css @@ -0,0 +1,11 @@ +#content-area.max-w-xl { + max-width: 46rem; +} + +#content-area.max-w-3xl { + max-width: 52rem; +} + +[data-component-part='update-content'] [data-component-part='card-title'] { + margin-bottom: 1rem; +} diff --git a/docs/updates.mdx b/docs/live/updates.mdx similarity index 62% rename from docs/updates.mdx rename to docs/live/updates.mdx index 29299833..b22be37c 100644 --- a/docs/updates.mdx +++ b/docs/live/updates.mdx @@ -5,6 +5,52 @@ icon: 'sparkles' mode: 'center' --- + + + 🚀 **Generator-native adapters** – The OpenAPI adapter now rides on the standalone `mcp-from-openapi` engine so every tool inherits conflict-free params, request mappers, and per-scheme auth scoring. + + 🧩 **Tool metadata upgrades** – Declare literal primitives, tuple arrays, `resource` / `resource_link` payloads, and a `rawInputSchema` so clients see structured responses without wrapper objects. + + 🛡️ **Typed MCP errors** – Throw `PublicMcpError` / `InternalMcpError` variants and let the new handler stamp traceable IDs, FlowControl-aware stop semantics, and safe production messaging automatically. + + 📚 **Maintainer runbooks** – Fresh docs explain how draft/live Mintlify trees sync, when to edit each folder, and how independent libraries ship alongside synchronized packages. + + + + + + 🔒 **Regex hardening** – Default-on quantifier guards, pattern timeouts, and `validatePattern` helpers keep untrusted specs from triggering ReDoS in your adapters. + + 🧠 **Schema utilities** – Helpers like `jsonSchemaObjectToZodRawShape` and tuple/prefix handling reproduce complex shapes without custom parsing code. + + 🛠️ **Configurable security profiles** – Tune `setSecurityConfig` once to balance trusted internal specs and public uploads. + + + + + + + ⚙️ **Request mappers** – Every generated tool ships its own mapper so you always know where each input lands (path, query, header, or body). + + 🌐 **Flexible loaders** – Load specs from URLs, files, JSON, or YAML strings before generating tools with consistent metadata. + + 🧾 **Typed metadata** – Capture auth schemes, preferred servers, and deprecation info so adapters like FrontMCP can reason about each operation automatically. + + + + ", "homepage": "https://docs.agentfront.dev", @@ -32,7 +32,7 @@ } }, "dependencies": { - "@frontmcp/sdk": "^0.3.1", + "@frontmcp/sdk": "^0.4.0", "@types/json-schema": "^7.0.15", "zod": "^3.25.76", "openapi-types": "^12.1.3", diff --git a/libs/cli/package.json b/libs/cli/package.json index 3667e9f3..4f00fa05 100644 --- a/libs/cli/package.json +++ b/libs/cli/package.json @@ -1,6 +1,6 @@ { "name": "frontmcp", - "version": "0.3.1", + "version": "0.4.0", "description": "FrontMCP command line interface", "author": "AgentFront ", "homepage": "https://docs.agentfront.dev", @@ -30,9 +30,9 @@ "prepare": "npm run build" }, "dependencies": { - "@frontmcp/sdk": "^0.3.1", - "@frontmcp/plugins": "^0.3.1", - "@frontmcp/adapters": "^0.3.1" + "@frontmcp/sdk": "^0.4.0", + "@frontmcp/plugins": "^0.4.0", + "@frontmcp/adapters": "^0.4.0" }, "devDependencies": { "typescript": "^5.5.3", diff --git a/libs/json-schema-to-zod-v3/CHANGELOG.md b/libs/json-schema-to-zod-v3/CHANGELOG.md new file mode 100644 index 00000000..6f83a1e7 --- /dev/null +++ b/libs/json-schema-to-zod-v3/CHANGELOG.md @@ -0,0 +1,32 @@ +# Changelog + +## [1.0.0] - 2025-11-21 + +### Features + +- Production-ready converter from JSON Schema (Draft 7+) to Zod v3 schemas +- Full TypeScript support with proper type inference +- Support for all common JSON Schema types (string, number, boolean, object, array, null) +- Advanced schema features: + - String formats and patterns (email, uri, uuid, date-time, etc.) + - Numeric constraints (minimum, maximum, multipleOf) + - Array constraints (minItems, maxItems, uniqueItems) + - Object properties with required/optional fields + - Nested schemas and complex compositions +- Schema composition support (allOf, anyOf, oneOf, not) +- Reference resolution ($ref) for schema reusability +- Enum and const value handling +- Default value support +- Description and metadata preservation +- Validation of input schemas +- Comprehensive error handling + +### Documentation + +- Complete API documentation +- Usage examples and code samples +- TypeScript integration guide +- Migration notes from other schema converters + +This is the first official release of `json-schema-to-zod-v3`, extracted from the FrontMCP framework to be published as +a standalone, reusable library for the community. diff --git a/libs/json-schema-to-zod-v3/package.json b/libs/json-schema-to-zod-v3/package.json index 0a6934f5..d558566f 100644 --- a/libs/json-schema-to-zod-v3/package.json +++ b/libs/json-schema-to-zod-v3/package.json @@ -27,7 +27,6 @@ "url": "https://github.com/agentfront/frontmcp/issues" }, "homepage": "https://github.com/agentfront/frontmcp/blob/main/libs/json-schema-to-zod-v3/README.md", - "main": "./dist/src/index.js", "types": "./dist/src/index.d.ts", "exports": { @@ -39,7 +38,6 @@ "default": "./dist/src/index.js" } }, - "peerDependencies": { "zod": "^3.0.0" }, diff --git a/libs/json-schema-to-zod-v3/project.json b/libs/json-schema-to-zod-v3/project.json index 760ea6da..76ae527e 100644 --- a/libs/json-schema-to-zod-v3/project.json +++ b/libs/json-schema-to-zod-v3/project.json @@ -12,7 +12,7 @@ "outputPath": "libs/json-schema-to-zod-v3/dist", "main": "libs/json-schema-to-zod-v3/src/index.ts", "tsConfig": "libs/json-schema-to-zod-v3/tsconfig.lib.json", - "assets": ["libs/json-schema-to-zod-v3/*.md", "LICENSE"] + "assets": ["libs/json-schema-to-zod-v3/README.md", "libs/json-schema-to-zod-v3/CHANGELOG.md", "LICENSE"] } }, "build": { diff --git a/libs/mcp-from-openapi/CHANGELOG.md b/libs/mcp-from-openapi/CHANGELOG.md new file mode 100644 index 00000000..4d67596c --- /dev/null +++ b/libs/mcp-from-openapi/CHANGELOG.md @@ -0,0 +1,45 @@ +# Changelog + +## [1.0.0] - 2025-11-21 + +### Features + +- Production-ready library for converting OpenAPI specifications into MCP tool definitions +- OpenAPI 3.0+ and Swagger 2.0 support +- Comprehensive operation parsing: + - RESTful endpoint detection (GET, POST, PUT, PATCH, DELETE, etc.) + - Path parameter extraction and validation + - Query parameter handling + - Request body schema conversion + - Response schema parsing +- Advanced OpenAPI features: + - Reference resolution ($ref) across the entire specification + - Security scheme detection and configuration + - Parameter conflict resolution + - Operation naming controls and customization +- Request mapper generation: + - Automatic parameter mapping from MCP tool inputs to HTTP requests + - Type-safe parameter handling + - Support for different parameter locations (path, query, header, cookie) + - Request body transformation +- Tool metadata generation: + - Descriptive tool names from operation IDs + - Documentation from OpenAPI descriptions + - Schema validation rules +- Multiple input formats: + - JSON OpenAPI specifications + - YAML OpenAPI specifications + - Inline specification objects + - URL references to remote specifications +- Type-safe TypeScript implementation +- Node.js 18+ compatibility + +### Documentation + +- Complete API reference +- OpenAPI conversion examples +- Integration guides for MCP servers +- Best practices for tool generation + +This is the first official release of `mcp-from-openapi`, extracted from the FrontMCP framework to be published as a +standalone, reusable library for the community. diff --git a/libs/mcp-from-openapi/project.json b/libs/mcp-from-openapi/project.json index 45dff4c6..2bb1a7e5 100644 --- a/libs/mcp-from-openapi/project.json +++ b/libs/mcp-from-openapi/project.json @@ -12,7 +12,7 @@ "outputPath": "libs/mcp-from-openapi/dist", "main": "libs/mcp-from-openapi/src/index.ts", "tsConfig": "libs/mcp-from-openapi/tsconfig.lib.json", - "assets": ["libs/mcp-from-openapi/*.md", "LICENSE"] + "assets": ["libs/mcp-from-openapi/README.md", "libs/mcp-from-openapi/CHANGELOG.md", "LICENSE"] } }, "build": { diff --git a/libs/plugins/package.json b/libs/plugins/package.json index 9fd86dae..e9343a7e 100644 --- a/libs/plugins/package.json +++ b/libs/plugins/package.json @@ -1,6 +1,6 @@ { "name": "@frontmcp/plugins", - "version": "0.3.1", + "version": "0.4.0", "description": "FrontMCP plugins to extend the SDK", "author": "AgentFront ", "homepage": "https://docs.agentfront.dev", @@ -32,7 +32,7 @@ } }, "dependencies": { - "ioredis":"^5.8.0", - "@frontmcp/sdk": "^0.3.1" + "ioredis": "^5.8.0", + "@frontmcp/sdk": "^0.4.0" } } diff --git a/libs/sdk/package.json b/libs/sdk/package.json index 62c10bc9..8d8f3ca0 100644 --- a/libs/sdk/package.json +++ b/libs/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@frontmcp/sdk", - "version": "0.3.1", + "version": "0.4.0", "description": "FrontMCP SDK", "author": "AgentFront ", "homepage": "https://docs.agentfront.dev", diff --git a/package.json b/package.json index 3bf64e04..54987944 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,12 @@ { "name": "@frontmcp/source", - "version": "0.3.1", + "version": "0.4.0", "license": "Apache-2.0", "scripts": { "dev": "nx serve demo", "build": "nx run-many -t build", - "docs:local": "bash scripts/dev-docs.sh", + "docs:live": "cd docs/live && mint dev", + "docs:draft": "cd docs/draft && mint dev", "prepare": "husky" }, "lint-staged": { @@ -17,7 +18,6 @@ "axios": "^1.6.0", "ioredis": "^5.8.0", "jose": "^6.1.0", - "json-schema-to-zod-v3": "1.0.0", "openapi-mcp-generator": "^3.2.0", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.0", diff --git a/scripts/apply-doc-patches.mjs b/scripts/apply-doc-patches.mjs index a2e943c5..7b140d2b 100644 --- a/scripts/apply-doc-patches.mjs +++ b/scripts/apply-doc-patches.mjs @@ -19,9 +19,32 @@ function die(msg) { function isAllowedPath(p) { const norm = p.replaceAll("\\", "/"); + + // CRITICAL: Reject blog files explicitly (must NOT be modified by Codex) + if (/^docs\/(live|draft)\/blog\//.test(norm)) { + return false; + } + + // CRITICAL: Reject archived versioned docs explicitly (read-only) + if (/^docs\/live\/docs\/v\//.test(norm)) { + return false; + } + return ( - /^docs\/.+\.(md|mdx)$/.test(norm) || - norm === "docs/docs.json" || + // Live docs (production) - specific to docs/ subdirectory only + /^docs\/live\/docs\/.+\.(md|mdx)$/.test(norm) || + norm === "docs/live/docs.json" || + norm === "docs/live/updates.mdx" || + // Draft docs (next release) - specific to docs/ subdirectory only + /^docs\/draft\/docs\/.+\.(md|mdx)$/.test(norm) || + norm === "docs/draft/docs.json" || + norm === "docs/draft/updates.mdx" || + // Assets and snippets (both live and draft) + /^docs\/live\/assets\/.+/.test(norm) || + /^docs\/live\/snippets\/.+/.test(norm) || + /^docs\/draft\/assets\/.+/.test(norm) || + /^docs\/draft\/snippets\/.+/.test(norm) || + // Other allowed files norm === "CHANGELOG.md" || norm === "README.md" || /^libs\/.+\/README\.md$/.test(norm) diff --git a/scripts/archive-and-publish-docs.mjs b/scripts/archive-and-publish-docs.mjs new file mode 100755 index 00000000..2be35ba5 --- /dev/null +++ b/scripts/archive-and-publish-docs.mjs @@ -0,0 +1,355 @@ +#!/usr/bin/env node +import fs from "node:fs/promises"; +import path from "node:path"; + +/** + * Archive current docs and publish draft docs to live + * + * This script: + * 1. Archives /docs/live/docs/* to /docs/live/docs/v/{previousMinor}/* (excluding v/ folder) + * 2. Updates /docs/live/docs.json to add archived version + * 3. Moves content from /docs/draft to /docs/live (replacing, not merging): + * - docs/draft/docs → docs/live/docs (excluding v/ folder) + * - docs/draft/blog → docs/live/blog + * - docs/draft/assets → docs/live/assets + * - docs/draft/snippets → docs/live/snippets + * + * Note: updates.mdx is NOT handled by this script. It is updated by Codex + * in the codex-mintlify-docs workflow which intelligently merges the draft + * update into the live updates. + * + * Usage: node scripts/archive-and-publish-docs.mjs + * Example: node scripts/archive-and-publish-docs.mjs 0.3 0.4 + */ + +const [, , previousMinor, newMinor] = process.argv; + +if (!previousMinor || !newMinor) { + console.error("Usage: node scripts/archive-and-publish-docs.mjs "); + console.error("Example: node scripts/archive-and-publish-docs.mjs 0.3 0.4"); + process.exit(1); +} + +// Validate version format (x.y) +if (!/^\d+\.\d+$/.test(previousMinor) || !/^\d+\.\d+$/.test(newMinor)) { + console.error(`Invalid version format. Must be x.y (e.g., 0.3)`); + process.exit(1); +} + +const LIVE_ROOT = path.join(process.cwd(), "docs/live"); +const DRAFT_ROOT = path.join(process.cwd(), "docs/draft"); + +/** + * Recursively copy directory + */ +async function copyDir(src, dest) { + await fs.mkdir(dest, { recursive: true }); + const entries = await fs.readdir(src, { withFileTypes: true }); + + for (const entry of entries) { + const srcPath = path.join(src, entry.name); + const destPath = path.join(dest, entry.name); + + if (entry.isDirectory()) { + await copyDir(srcPath, destPath); + } else { + await fs.copyFile(srcPath, destPath); + } + } +} + +/** + * Recursively remove directory + */ +async function removeDir(dir) { + try { + await fs.rm(dir, { recursive: true, force: true }); + } catch (error) { + // Ignore if doesn't exist + if (error.code !== 'ENOENT') { + throw error; + } + } +} + +/** + * Check if directory exists + */ +async function dirExists(dir) { + try { + const stat = await fs.stat(dir); + return stat.isDirectory(); + } catch { + return false; + } +} + +/** + * Get all entries in a directory (excluding specific items) + */ +async function getDirEntries(dir, exclude = []) { + try { + const entries = await fs.readdir(dir, { withFileTypes: true }); + return entries.filter(entry => !exclude.includes(entry.name)); + } catch (error) { + if (error.code === 'ENOENT') { + return []; + } + throw error; + } +} + +/** + * Step 1: Archive current docs to v/{previousMinor} + */ +async function archiveCurrentDocs() { + console.log(`\n📦 Archiving current docs to v/${previousMinor}...\n`); + + const liveDocsDir = path.join(LIVE_ROOT, "docs"); + const archiveDir = path.join(LIVE_ROOT, "docs/v", previousMinor); + + // Check if live docs exist + if (!await dirExists(liveDocsDir)) { + console.log("⚠️ No live docs found to archive"); + return; + } + + // Create archive directory + await fs.mkdir(archiveDir, { recursive: true }); + + // Get all entries in live docs except the 'v' folder + const entries = await getDirEntries(liveDocsDir, ['v']); + + let archivedCount = 0; + for (const entry of entries) { + const srcPath = path.join(liveDocsDir, entry.name); + const destPath = path.join(archiveDir, entry.name); + + if (entry.isDirectory()) { + await copyDir(srcPath, destPath); + console.log(` ✓ Archived directory: ${entry.name}`); + } else { + await fs.copyFile(srcPath, destPath); + console.log(` ✓ Archived file: ${entry.name}`); + } + archivedCount++; + } + + console.log(`\n✅ Archived ${archivedCount} items to v/${previousMinor}\n`); +} + +/** + * Step 2: Update docs.json to add archived version + */ +async function updateDocsJson() { + console.log(`📝 Updating docs.json...\n`); + + const docsJsonPath = path.join(LIVE_ROOT, "docs.json"); + + try { + const content = await fs.readFile(docsJsonPath, "utf8"); + const docs = JSON.parse(content); + + // Find Documentation dropdown + const docDropdown = docs.navigation?.dropdowns?.find(d => d.dropdown === 'Documentation'); + + if (!docDropdown || !docDropdown.versions) { + console.log("⚠️ No Documentation dropdown with versions found in docs.json"); + return; + } + + // Find current latest version + const latestIndex = docDropdown.versions.findIndex(v => v.default === true); + + if (latestIndex === -1) { + console.log("⚠️ No default version found in docs.json"); + return; + } + + const oldLatest = docDropdown.versions[latestIndex]; + + // Create archived version from old latest + const archivedVersion = { + version: `v${previousMinor}`, + default: false, + groups: JSON.parse(JSON.stringify(oldLatest.groups)) + }; + + // Update paths in archived version to point to v/{previousMinor} + function updatePathsToArchive(groups) { + for (const group of groups) { + if (group.tag === 'latest') { + group.tag = `version ${previousMinor}`; + } + + if (group.pages) { + group.pages = group.pages.map(page => { + if (typeof page === 'string') { + // Don't update 'updates' path or paths already in v/ + if (page === 'updates' || page.startsWith('docs/v/')) { + return page; + } + // Update docs/ paths to docs/v/{previousMinor}/ + if (page.startsWith('docs/')) { + return page.replace('docs/', `docs/v/${previousMinor}/`); + } + return page; + } else if (page.pages) { + // Recursively update nested pages + updatePathsToArchive([page]); + return page; + } + return page; + }); + } + } + } + + updatePathsToArchive(archivedVersion.groups); + + // Update old latest to new version + oldLatest.version = `v${newMinor} (latest)`; + oldLatest.default = true; + + // Restore paths in new latest to point to current docs (not archived) + function restorePathsToCurrent(groups) { + for (const group of groups) { + if (group.tag === `version ${previousMinor}`) { + group.tag = 'latest'; + } + + if (group.pages) { + group.pages = group.pages.map(page => { + if (typeof page === 'string') { + // Update archived paths back to current + if (page.startsWith(`docs/v/${previousMinor}/`)) { + return page.replace(`docs/v/${previousMinor}/`, 'docs/'); + } + return page; + } else if (page.pages) { + restorePathsToCurrent([page]); + return page; + } + return page; + }); + } + } + } + + restorePathsToCurrent(oldLatest.groups); + + // Add archived version after the latest + docDropdown.versions.splice(latestIndex + 1, 0, archivedVersion); + + // Write updated docs.json + await fs.writeFile(docsJsonPath, JSON.stringify(docs, null, 2) + "\n", "utf8"); + + console.log(` ✓ Added v${previousMinor} to versions`); + console.log(` ✓ Updated latest to v${newMinor}`); + console.log(`\n✅ docs.json updated successfully\n`); + + } catch (error) { + console.error(`❌ Error updating docs.json:`, error.message); + throw error; + } +} + +/** + * Step 3: Move content from draft to live (replace mode) + */ +async function publishDraftToLive() { + console.log(`🚀 Publishing draft to live...\n`); + + const itemsToMove = ['docs', 'blog', 'assets', 'snippets']; + let movedCount = 0; + + for (const item of itemsToMove) { + const draftPath = path.join(DRAFT_ROOT, item); + const livePath = path.join(LIVE_ROOT, item); + + if (!await dirExists(draftPath)) { + console.log(` ⊘ Skipping ${item} (not found in draft)`); + continue; + } + + // Remove existing live item (except v/ folder in docs) + if (item === 'docs') { + // For docs, preserve the v/ folder and only remove other content + const entries = await getDirEntries(livePath, ['v']); + for (const entry of entries) { + await removeDir(path.join(livePath, entry.name)); + } + } else { + // For other items, remove completely + await removeDir(livePath); + } + + // Copy from draft to live + if (item === 'docs') { + // For docs, copy everything except v/ folder + const entries = await getDirEntries(draftPath, ['v']); + await fs.mkdir(livePath, { recursive: true }); + + for (const entry of entries) { + const srcPath = path.join(draftPath, entry.name); + const destPath = path.join(livePath, entry.name); + + if (entry.isDirectory()) { + await copyDir(srcPath, destPath); + } else { + await fs.copyFile(srcPath, destPath); + } + } + } else { + await copyDir(draftPath, livePath); + } + + console.log(` ✓ Published ${item}`); + movedCount++; + } + + console.log(`\n✅ Published ${movedCount} items from draft to live\n`); +} + +/** + * Note: updates.mdx is NOT handled by this script + * It is updated by Codex in the codex-mintlify-docs workflow + * which intelligently merges the draft update into the live updates + */ + +/** + * Main execution + */ +async function main() { + console.log(`\n${'='.repeat(60)}`); + console.log(` Archive and Publish Docs`); + console.log(` Previous: v${previousMinor} → New: v${newMinor}`); + console.log(`${'='.repeat(60)}\n`); + + try { + // Step 1: Archive current docs + await archiveCurrentDocs(); + + // Step 2: Update docs.json + await updateDocsJson(); + + // Step 3: Publish draft to live + await publishDraftToLive(); + + // Note: Step 4 (updates.mdx) is handled by Codex in codex-mintlify-docs workflow + + console.log(`${'='.repeat(60)}`); + console.log(` ✅ All steps completed successfully!`); + console.log(` Note: updates.mdx will be updated by Codex workflow`); + console.log(`${'='.repeat(60)}\n`); + + } catch (error) { + console.error(`\n❌ Fatal error:`, error); + process.exit(1); + } +} + +main().catch((error) => { + console.error("Fatal error:", error); + process.exit(1); +}); diff --git a/scripts/dev-docs.sh b/scripts/dev-docs.sh deleted file mode 100755 index d2f5f896..00000000 --- a/scripts/dev-docs.sh +++ /dev/null @@ -1,94 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# Colors for output -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -RED='\033[0;31m' -NC='\033[0m' # No Color - -# Get the script directory and project root -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -DOCS_DIR="$PROJECT_ROOT/docs" - -# File names (will be used relative to DOCS_DIR) -PROD_DOCS="docs.json" -DRAFT_DOCS="docs.draft.json" -BACKUP_DOCS="docs.backup.json" - -# Cleanup function to restore backup -cleanup() { - local exit_code=$? - echo "" - echo -e "${YELLOW}Cleaning up...${NC}" - - # Navigate to docs directory for cleanup - cd "$DOCS_DIR" - - if [ -f "$BACKUP_DOCS" ]; then - echo -e "${GREEN}Restoring original docs.json from backup${NC}" - mv "$BACKUP_DOCS" "$PROD_DOCS" - echo -e "${GREEN}✓ Backup restored successfully${NC}" - else - echo -e "${YELLOW}⚠ No backup found to restore${NC}" - fi - - exit $exit_code -} - -# Register cleanup function to run on exit -trap cleanup EXIT INT TERM - -# Main script -main() { - echo -e "${GREEN}Starting Mintlify dev server with draft docs...${NC}" - echo "" - - # Check if docs directory exists - if [ ! -d "$DOCS_DIR" ]; then - echo -e "${RED}Error: docs directory not found at $DOCS_DIR${NC}" - exit 1 - fi - - # Navigate to docs directory - cd "$DOCS_DIR" - echo -e "${GREEN}Working directory: $DOCS_DIR${NC}" - echo "" - - # Check if draft docs exist - if [ ! -f "$DRAFT_DOCS" ]; then - echo -e "${RED}Error: $DRAFT_DOCS not found in docs directory${NC}" - echo "Please ensure docs.draft.json exists before running this script." - exit 1 - fi - - # Check if production docs exist - if [ ! -f "$PROD_DOCS" ]; then - echo -e "${RED}Error: $PROD_DOCS not found in docs directory${NC}" - echo "Please ensure docs.json exists before running this script." - exit 1 - fi - - # Backup production docs - echo -e "${YELLOW}Creating backup of docs.json...${NC}" - cp "$PROD_DOCS" "$BACKUP_DOCS" - echo -e "${GREEN}✓ Backup created: $BACKUP_DOCS${NC}" - - # Replace production docs with draft - echo -e "${YELLOW}Replacing docs.json with draft version...${NC}" - cp "$DRAFT_DOCS" "$PROD_DOCS" - echo -e "${GREEN}✓ docs.json replaced with draft version${NC}" - echo "" - - # Run mintlify dev - echo -e "${GREEN}Starting Mintlify dev server...${NC}" - echo -e "${YELLOW}Press Ctrl+C to stop the server and restore backup${NC}" - echo "" - - # Run mintlify dev (this will block until user stops it) - npx mintlify dev -} - -# Run main function -main diff --git a/scripts/remove-blog-drafts.mjs b/scripts/remove-blog-drafts.mjs index 45af6481..ea115b0e 100755 --- a/scripts/remove-blog-drafts.mjs +++ b/scripts/remove-blog-drafts.mjs @@ -3,134 +3,120 @@ import fs from "node:fs/promises"; import path from "node:path"; /** - * Remove draft attributes from BlogCard components - * This script finds all BlogCard components with draft attributes and removes them + * Remove draft attributes from BlogCard components in docs + * + * This script scans all .mdx and .md files in docs/ and removes + * the 'draft' prop from BlogCard components to make them visible in production. + * * Usage: node scripts/remove-blog-drafts.mjs */ -const DOCS_DIR = path.join(process.cwd(), "docs"); +const DOCS_ROOT = path.join(process.cwd(), "docs"); -async function findBlogFiles(dir = DOCS_DIR) { - const files = []; - - async function walk(currentDir) { - const entries = await fs.readdir(currentDir, { withFileTypes: true }); +/** + * Recursively find all .mdx and .md files + */ +async function findDocFiles(dir, files = []) { + const entries = await fs.readdir(dir, { withFileTypes: true }); - for (const entry of entries) { - const fullPath = path.join(currentDir, entry.name); + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); - if (entry.isDirectory() && entry.name !== "node_modules" && entry.name !== "dist") { - await walk(fullPath); - } else if (entry.isFile() && entry.name.endsWith(".mdx")) { - files.push(fullPath); - } + if (entry.isDirectory()) { + await findDocFiles(fullPath, files); + } else if (entry.isFile() && (entry.name.endsWith('.mdx') || entry.name.endsWith('.md'))) { + files.push(fullPath); } } - await walk(dir); return files; } -async function removeDraftFromFile(filePath) { +/** + * Remove draft attributes from BlogCard components in content + */ +function removeDraftAttributes(content) { + // Pattern to match BlogCard with draft prop + // Handles: draft={true}, draft={false}, draft="true", draft="false", draft + const draftPattern = /(]*?)\s+draft(?:=(?:\{(?:true|false)\}|"(?:true|false)"))?/g; + + return content.replace(draftPattern, '$1'); +} + +/** + * Process a single file + */ +async function processFile(filePath) { try { const content = await fs.readFile(filePath, "utf8"); - // Check if file contains BlogCard with draft - if (!content.includes("BlogCard") || !content.includes("draft")) { - return { modified: false }; + // Check if file contains draft attributes + if (!content.includes('draft')) { + return { processed: false, changed: false }; } - let modified = false; - let newContent = content; - - // Pattern 1: Remove standalone "draft" attribute (on its own line or inline) - // Matches: draft, draft={true}, or draft={false} - // Handles cases with various whitespace configurations - const patterns = [ - // Pattern for draft on its own line with any whitespace - /^(\s*)(draft\s*(?:=\s*\{?\s*(?:true|false)\s*\}?)?\s*)\n/gm, - // Pattern for draft inline (with space before and after) - /\s+(draft\s*(?:=\s*\{?\s*(?:true|false)\s*\}?)?\s*)\s+/g, - // Pattern for draft at the end (before closing tag or next prop) - /\s+(draft\s*(?:=\s*\{?\s*(?:true|false)\s*\}?)?\s*)(\s*(?:>|\w))/g, - ]; - - for (const pattern of patterns) { - const before = newContent; - if (pattern.source.includes('^')) { - // For line-based patterns, remove the entire line - newContent = newContent.replace(pattern, ''); - } else if (pattern.source.includes('(\\s*(?:>|\\w))')) { - // For patterns at the end, keep the following character - newContent = newContent.replace(pattern, '$2'); - } else { - // For inline patterns, keep single space - newContent = newContent.replace(pattern, ' '); - } - - if (before !== newContent) { - modified = true; - } - } + const updatedContent = removeDraftAttributes(content); - if (modified) { - await fs.writeFile(filePath, newContent, "utf8"); - return { modified: true, file: path.relative(DOCS_DIR, filePath) }; + if (updatedContent !== content) { + await fs.writeFile(filePath, updatedContent, "utf8"); + return { processed: true, changed: true }; } - return { modified: false }; + return { processed: true, changed: false }; + } catch (error) { console.error(`Error processing ${filePath}:`, error.message); - return { modified: false, error: error.message }; + return { processed: false, changed: false, error: error.message }; } } +/** + * Main execution + */ async function main() { - console.log("🔍 Searching for BlogCard components with draft attributes...\n"); - - const files = await findBlogFiles(); - console.log(`Found ${files.length} MDX files to check\n`); + console.log("\n" + "=".repeat(60)); + console.log(" Remove Draft Attributes from Blog Cards"); + console.log("=".repeat(60) + "\n"); - const results = { - modified: [], - unchanged: [], - errors: [], - }; + try { + // Find all doc files + console.log("🔍 Scanning for .mdx and .md files...\n"); + const files = await findDocFiles(DOCS_ROOT); + console.log(`Found ${files.length} files to scan\n`); + + if (files.length === 0) { + console.log("No files found to process"); + return; + } - for (const file of files) { - const result = await removeDraftFromFile(file); + // Process each file + console.log("🔧 Processing files...\n"); + const results = await Promise.all(files.map(processFile)); + + const changed = results.filter(r => r.changed).length; + const processed = results.filter(r => r.processed).length; + const errors = results.filter(r => r.error).length; + + console.log("\n" + "=".repeat(60)); + console.log(` Results:`); + console.log(` - Files scanned: ${files.length}`); + console.log(` - Files processed: ${processed}`); + console.log(` - Files changed: ${changed}`); + if (errors > 0) { + console.log(` - Errors: ${errors}`); + } + console.log("=".repeat(60) + "\n"); - if (result.error) { - results.errors.push({ file, error: result.error }); - } else if (result.modified) { - results.modified.push(result.file); - console.log(`✓ Removed draft from: ${result.file}`); + if (changed > 0) { + console.log("✅ Draft attributes removed successfully\n"); } else { - results.unchanged.push(path.relative(DOCS_DIR, file)); + console.log("ℹ️ No draft attributes found to remove\n"); } - } - - console.log("\n" + "=".repeat(50)); - console.log("Summary:"); - console.log("=".repeat(50)); - console.log(`✓ Modified: ${results.modified.length}`); - console.log(`- Unchanged: ${results.unchanged.length}`); - console.log(`✗ Errors: ${results.errors.length}`); - - if (results.modified.length > 0) { - console.log("\nModified files:"); - results.modified.forEach(file => console.log(` - ${file}`)); - } - if (results.errors.length > 0) { - console.log("\nErrors:"); - results.errors.forEach(({ file, error }) => console.log(` - ${file}: ${error}`)); + } catch (error) { + console.error("\n❌ Fatal error:", error); + process.exit(1); } - - console.log("\n✅ Done!"); } -main().catch((error) => { - console.error("Fatal error:", error); - process.exit(1); -}); +main(); diff --git a/yarn.lock b/yarn.lock index a03a4fe0..99d4fb30 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2371,6 +2371,11 @@ unist-util-visit "^4.1.1" uuid "^11.1.0" +"@mintlify/prettier-config@^1.0.6": + version "1.0.6" + resolved "https://registry.yarnpkg.com/@mintlify/prettier-config/-/prettier-config-1.0.6.tgz#0fa2d43bb3a40edf1f0689edb8cfbaed0f3177b2" + integrity sha512-LGjeQ3cYr6kaO1xKosJWweQISfOI58eSqw47h7aN3L/ZJcm4goqR6I9iBcgkdwnZLjErB5G1rQ+OEByJdqqYEA== + "@mintlify/previewing@4.0.789": version "4.0.789" resolved "https://registry.yarnpkg.com/@mintlify/previewing/-/previewing-4.0.789.tgz#d2de0e44c6a8d3ac12713d55ed6aca730456b66a"