> ## Documentation Index
> Fetch the complete documentation index at: https://fentaris.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Tutorial

> Build a multi-server proxy step by step with the modern Fentaris app API.

This tutorial starts from a generated Fentaris project and turns it into a proxy for two upstream MCP servers. The app code keeps runtime endpoint settings in `fentaris.json` and declares upstream servers with `app.mcp(...)`.

## Quick Start

Use the optional agent skills when you want an agent to ask setup questions, choose upstream MCP servers, and validate the project after edits:

```bash theme={null} theme={"theme":{"light":"github-light","dark":"github-dark"}}
npx skills add Fentaris/fentaris-skills --skill '*'
```

Then restart or reload your agent and ask it to use the setup skill:

```txt theme={null} theme={"theme":{"light":"github-light","dark":"github-dark"}}
Use $fentaris-project-setup to help me create a Fentaris proxy for my team.
```

If you prefer to build the tutorial manually, create a project with the CLI:

```bash theme={null} theme={"theme":{"light":"github-light","dark":"github-dark"}}
npm i -g @fentaris/cli
fentaris init tutorial-proxy
cd tutorial-proxy
```

## Step 1: Configure the Runtime Endpoint

`fentaris init` writes `fentaris.json`. Keep endpoint settings there instead of hardcoding them in `src/index.ts`:

```json theme={null} theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "name": "tutorial-proxy",
  "packageManager": "pnpm",
  "entrypoint": "src/index.ts",
  "port": 4000,
  "host": "127.0.0.1",
  "path": "/mcp",
  "authDir": ".fentaris"
}
```

When `app.start()` runs without explicit options, `@fentaris/core` searches upward from the current working directory and reads the nearest `fentaris.json`. With the config above, the local endpoint is `http://localhost:4000/mcp`.

<Tip>
  Change `port` or `path` in `fentaris.json` when the local endpoint needs to move. Keep `src/index.ts` focused on proxy behavior.
</Tip>

## Step 2: Create the App Boundary

Open `src/index.ts` and start with an empty Fentaris app:

```ts theme={null} theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { Policy, fentaris, stdio, streamableHttp } from "@fentaris/core";

const app = fentaris({
  policy: Policy.allowAll(),
});
```

`fentaris()` creates the proxy application boundary. Project discovery, host, port, and path come from `fentaris.json`, while upstream MCP servers are attached with `app.mcp(...)`. `Policy.allowAll()` keeps this local tutorial focused on routing; replace it with an allow-list policy before sharing the endpoint.

## Step 3: Attach the Filesystem Server

Create a local directory that the filesystem MCP server can expose:

```bash theme={null} theme={"theme":{"light":"github-light","dark":"github-dark"}}
mkdir demo-files
echo "hello from Fentaris" > demo-files/readme.txt
```

Then register the upstream MCP server:

```ts theme={null} theme={"theme":{"light":"github-light","dark":"github-dark"}}
app.mcp("filesystem", {
  displayName: "Filesystem",
  transport: stdio({
    command: "npx",
    args: ["-y", "@modelcontextprotocol/server-filesystem", "./demo-files"],
  }),
});
```

`app.mcp("filesystem", ...)` gives the upstream server a stable Fentaris name. Fentaris uses that name when routing tool calls, applying middleware, logging events, and exposing proxied tool names such as `filesystem__read_file`.

## Step 4: Attach a Remote Server

Add the public MCP specification server as a second upstream:

```ts theme={null} theme={"theme":{"light":"github-light","dark":"github-dark"}}
app.mcp("specification", {
  displayName: "MCP Specification",
  transport: streamableHttp({
    url: "https://mcp.specification.website/mcp",
  }),
});
```

<Note>
  The specification server requires network access. The filesystem server remains local, so you can still start the proxy and test local routing if the remote server is unavailable.
</Note>

## Step 5: Add Global Middleware

Middleware runs after Fentaris builds request context and before the matching upstream MCP server receives the call:

```ts theme={null} theme={"theme":{"light":"github-light","dark":"github-dark"}}
app.use(async (ctx, next) => {
  ctx.log.info("tool-call", {
    server: ctx.server?.name,
    tool: ctx.tool?.name,
  });

  if (ctx.server?.name === "filesystem" && ctx.tool?.name === "delete_file") {
    return ctx.deny("Filesystem delete is disabled in this environment.");
  }

  return next();
});
```

Use middleware for runtime checks that depend on request data, tool arguments, tenant state, or an approval workflow. For static allow-list access, use policy rules.

## Step 6: Add a Server Hook

Server-scoped handles keep observability close to the upstream MCP server they describe:

```ts theme={null} theme={"theme":{"light":"github-light","dark":"github-dark"}}
app.mcp("specification").on("tool:success", async ({ ctx, durationMs }) => {
  ctx.log.info("specification.tool.success", {
    tool: ctx.tool?.name,
    durationMs,
  });
});
```

This hook only observes successful tool calls for the `specification` upstream MCP server. It does not affect filesystem calls and does not replace middleware or policy decisions.

## Step 7: Start the Proxy

Finish `src/index.ts` with `app.start()`:

```ts theme={null} theme={"theme":{"light":"github-light","dark":"github-dark"}}
await app.start();
```

The complete entrypoint should look like this:

```ts theme={null} theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { Policy, fentaris, stdio, streamableHttp } from "@fentaris/core";

const app = fentaris({
  policy: Policy.allowAll(),
});

app.mcp("filesystem", {
  displayName: "Filesystem",
  transport: stdio({
    command: "npx",
    args: ["-y", "@modelcontextprotocol/server-filesystem", "./demo-files"],
  }),
});

app.mcp("specification", {
  displayName: "MCP Specification",
  transport: streamableHttp({
    url: "https://mcp.specification.website/mcp",
  }),
});

app.use(async (ctx, next) => {
  ctx.log.info("tool-call", {
    server: ctx.server?.name,
    tool: ctx.tool?.name,
  });

  if (ctx.server?.name === "filesystem" && ctx.tool?.name === "delete_file") {
    return ctx.deny("Filesystem delete is disabled in this environment.");
  }

  return next();
});

app.mcp("specification").on("tool:success", async ({ ctx, durationMs }) => {
  ctx.log.info("specification.tool.success", {
    tool: ctx.tool?.name,
    durationMs,
  });
});

await app.start();
```

Run the proxy:

```bash theme={null} theme={"theme":{"light":"github-light","dark":"github-dark"}}
fentaris dev
```

## Step 8: Connect a Client

Point your MCP client to the endpoint from `fentaris.json`:

```bash theme={null} theme={"theme":{"light":"github-light","dark":"github-dark"}}
http://localhost:4000/mcp
```

Fentaris exposes one MCP endpoint and routes each request to the matching upstream MCP server. The client talks to Fentaris; Fentaris starts or connects to upstream MCP servers, applies policy and middleware, forwards allowed calls, and records hooks and logs.

## Validate the Project

Run static project checks before sharing the endpoint:

```bash theme={null} theme={"theme":{"light":"github-light","dark":"github-dark"}}
fentaris check --offline
```

When the proxy is running, verify the runtime endpoint:

```bash theme={null} theme={"theme":{"light":"github-light","dark":"github-dark"}}
fentaris doctor --runtime
```

<Warning>
  The tutorial is suitable for local development. Add authentication and allow-list policies before exposing a Fentaris endpoint outside your machine.
</Warning>

## What You Built

You now have:

* one Fentaris endpoint configured by `fentaris.json`
* two upstream MCP servers declared with `app.mcp(...)`
* global middleware for logging and request-time denial
* a server-scoped hook for remote upstream observability

## Related Documentation

* [Agent Skills](/getting-started/codex-skills)
* [Quickstart](/getting-started/quickstart)
* [Config file](/reference/config-file)
* [Middleware](/guides/middleware)
* [Hooks](/guides/hooks)
* [Proxy setup](/guides/proxy-setup)
