# Monorepo patterns

> How to lay out a FrontMCP Nx workspace with @frontmcp/nx: apps, shared libraries and server shells, composing several apps into one server, building it, tags and affected projects, and moving a standalone project in. Each pattern was run in a real workspace.

Source: https://frontmcp.dev/reference/nx/monorepo

A FrontMCP workspace has three kinds of project. **Apps** in `apps/` hold capabilities: tools, resources, prompts. **Libraries** in `libs/` hold code several apps share. **Server shells** in `servers/` decide which apps are served together, with which options, for which platform. Nx sees the imports between them, so it knows what to rebuild and retest when something changes. In 1.9.2 the generators make all three, each runs with `nx dev` and type-checks with `nx typecheck` as generated, and the CLI bundles code an app imports from outside its folder, a library through its alias or a shell's apps by relative path, with nothing to set up first.

```text
help-desk-platform/
  apps/tickets/        @App: the ticket tools        nx dev tickets
  apps/billing/        @App: invoices                nx dev billing
  libs/ticket-model/   shared types and helpers      imported by apps
  servers/gateway/     @FrontMcp: tickets + billing  nx dev server-gateway
```

---

## Reference

### The three layers

| Layer | Contains | Imports | Made by |
| --- | --- | --- | --- |
| `apps/<name>` | An `@App` and its tools, resources, prompts, skills, jobs; a `main.ts` that serves it alone. | Libraries. | [`nx g @frontmcp/nx:app`](https://frontmcp.dev/reference/nx/generators#app) |
| `libs/<name>` | Plain TypeScript: types, clients, helpers, shared tools or plugins. | Other libraries. | [`nx g @frontmcp/nx:lib`](https://frontmcp.dev/reference/nx/generators#lib) |
| `servers/<name>` | A `@FrontMcp` server listing apps, with the options and the deployment files of one environment. | Apps and libraries. | [`nx g @frontmcp/nx:server`](https://frontmcp.dev/reference/nx/generators#server) |

Nx finds these dependencies in the imports, and shows them in the project graph:

```bash
npx nx graph --file=graph.json
```

```text
server-gateway -> demo, tickets
tickets -> ticket-model
```

Because `nx.json` makes `build` depend on `^build`, `nx build server-gateway` builds `demo` and `tickets` first (`Successfully ran target build for project server-gateway and 2 tasks it depends on`). A library has no `build` of its own: each bundle that imports it compiles it in.

### What each pattern needs

| Pattern | `nx dev` | `nx build` | `nx typecheck` |
| --- | --- | --- | --- |
| An app that imports nothing outside its folder | Works. | Works. | Works. |
| A server shell that imports apps | Works. | Works: the apps build first, then the shell, into one bundle. | Works, checking the apps it imports too. |
| An app that imports a library through a `tsconfig.base.json` alias | Works. | Works. | Works. |

The executors run the CLI in the project's folder ([Executors](https://frontmcp.dev/reference/nx/executors#how-an-executor-runs)). `frontmcp dev` runs the entry with `tsx`, which reads path aliases from the project's own `tsconfig.json`, and that file extends `tsconfig.base.json`, where the `lib` generator put the alias. `frontmcp build` compiles the project with that `tsconfig.json`, mirrors the folders it imports from in the output, bundles all of it, and removes the mirrored folders afterwards. A bundle holds the libraries and apps it imports, so the built server runs without them, and `dist/node/` has only the bundle, the runner, the manifest and the installer. Before 1.8.7 all of this failed: dev couldn't find the alias without a root `tsconfig.json`, and builds stopped with `TS6059 … is not under 'rootDir'` unless the entry sat at the workspace root. In 1.8.7 an app that imported a library still needed a root `tsconfig.json` to build, and `nx typecheck` failed in every project.

#### Caveats

- In a workspace made with 1.8.7, `nx.json` lists the `@nx/js/typescript` plugin, which gives each library an inferred `build` that needs a root `tsconfig.json`. Remove it: [Upgrading a 1.8.7 workspace](https://frontmcp.dev/reference/nx#upgrading-a-187-workspace).
- The `lib` generator's default alias takes the npm scope of the workspace's root `package.json`, and is the bare name when it has none, as in a workspace from `frontmcp create --nx` (`ticket-model`). Pass `--importPath` to choose it, as below. Before 1.9.2 the default was `@frontmcp/<name>`, under FrontMCP's own npm scope.

---

## Usage

### Composing apps into one server

Apps stay independent, each with its own `main.ts` for `nx dev <app>`, and a server shell lists several. When two apps have a tool with the same name, FrontMCP keeps both and prefixes each with its app's id, as `<id>:<name>` ([Handling name clashes](https://frontmcp.dev/reference/server/apps#handling-name-clashes)). The `app` generator gives every app a `hello` tool, so a shell of two generated apps shows it at once:

```ts main.ts active
import { FrontMcp } from "@frontmcp/sdk";
import { DemoApp } from "./demo.app";
import { TicketsApp } from "./tickets.app";

// servers/gateway/src/main.ts
@FrontMcp({
  info: { name: "Gateway Server", version: "0.1.0" },
  apps: [DemoApp, TicketsApp],
})
export default class Server {}
```

```ts tickets.app.ts
import { App, Tool, ToolContext, z } from "@frontmcp/sdk";

// apps/tickets/src/tools/hello.tool.ts
@Tool({
  name: "hello",
  description: "Say hello to someone",
  inputSchema: { name: z.string().describe("Name to greet") },
  outputSchema: { message: z.string() },
})
class HelloTool extends ToolContext {
  async execute(input: { name: string }) {
    return { message: `Hello, ${input.name}! Tickets here.` };
  }
}

// apps/tickets/src/tickets.app.ts
@App({ id: "tickets", name: "Tickets", tools: [HelloTool] })
export class TicketsApp {}
```

```ts demo.app.ts
import { App, Tool, ToolContext, z } from "@frontmcp/sdk";

// apps/demo/src/tools/hello.tool.ts
@Tool({
  name: "hello",
  description: "Say hello to someone",
  inputSchema: { name: z.string().describe("Name to greet") },
  outputSchema: { message: z.string() },
})
class HelloTool extends ToolContext {
  async execute(input: { name: string }) {
    return { message: `Hello, ${input.name}!` };
  }
}

// apps/demo/src/demo.app.ts
@App({ id: "demo", name: "Demo", tools: [HelloTool] })
export class DemoApp {}
```

```ts gateway.test.ts
import { test, expect } from "@frontmcp/testing";

test("both apps' hello tools are listed, prefixed with their app's id", async ({ mcp }) => {
  const tools = await mcp.tools.list();
  expect(tools).toContainTool("demo:hello");
  expect(tools).toContainTool("tickets:hello");
});

test("each prefixed name reaches its own app", async ({ mcp }) => {
  expect((await mcp.tools.call("tickets:hello", { name: "Ada" })).json()).toEqual({ message: "Hello, Ada! Tickets here." });
  expect((await mcp.tools.call("demo:hello", { name: "Ada" })).json()).toEqual({ message: "Hello, Ada!" });
});
```

In the workspace, generate the shell, and run it with its `dev` target:

```bash
npx nx g @frontmcp/nx:server gateway --apps demo,tickets --skills none
npx nx dev server-gateway --port=4110
```

```text
Running: frontmcp dev --entry /Users/you/help-desk-platform/servers/gateway/src/main.ts --port 4110 (in /Users/you/help-desk-platform/servers/gateway)
…
MCP HTTP (Express) on 127.0.0.1:4110
[10:01:12 AM] Found 0 errors. Watching for file changes.
```

A client connected to `http://localhost:4110/` lists `demo:hello` and `tickets:hello`, as in the Playground. Give tools names that don't clash when you can: the prefix is only added when two apps collide, so adding an app can rename another app's tools.

### Building a composed server

The shell's own `main.ts` is the entry, and it imports the apps by relative path. `nx build server-gateway` bundles all of it:

```text
$ npx nx build server-gateway
 NX   Running target build for project server-gateway and 2 tasks it depends on:
Running: frontmcp build --entry …/apps/demo/src/main.ts --out-dir …/apps/demo/dist (in …/apps/demo)
Running: frontmcp build --entry …/apps/tickets/src/main.ts --out-dir …/apps/tickets/dist (in …/apps/tickets)
Running: frontmcp build --target node --entry …/servers/gateway/src/main.ts --out-dir …/servers/gateway/dist (in …/servers/gateway)
[build:exec] bundle created: dist/node/server-gateway.bundle.js (26.8 KB)

 NX   Successfully ran target build for project server-gateway and 2 tasks it depends on
```

`PORT=8080 servers/gateway/dist/node/server-gateway` serves both apps, with `demo:hello` and `tickets:hello` answering. Here `tickets` imports the library of the [next section](#sharing-code-in-a-library), which is compiled into both bundles. Before 1.8.7 this needed an entry at the workspace root that re-exported the shell's server.

### Sharing code in a library

```bash
npx nx g @frontmcp/nx:lib ticket-model --importPath @help-desk/ticket-model
```

It writes `libs/ticket-model/` and adds the alias to `tsconfig.base.json`:

```json tsconfig.base.json
{
  "compilerOptions": {
    "paths": { "@help-desk/ticket-model": ["./libs/ticket-model/src/index.ts"] }
  }
}
```

(Only `paths` is shown; the rest of the file stays.) Put code in it, and export it from `index.ts`, which the generator wrote to export its sample class by name:

```ts libs/ticket-model/src/ticket-model.ts
export const PRIORITIES = ['low', 'normal', 'urgent'] as const;
export type Priority = (typeof PRIORITIES)[number];

export function formatTicketId(n: number): string {
  return `T-${String(n).padStart(4, '0')}`;
}
```

```ts libs/ticket-model/src/index.ts
export * from './ticket-model';
```

Import it from an app:

```ts apps/tickets/src/tools/hello.tool.ts
import { Tool, ToolContext, z } from '@frontmcp/sdk';
import { formatTicketId } from '@help-desk/ticket-model';

@Tool({
  name: 'hello',
  description: 'Say hello to someone',
  inputSchema: { name: z.string().describe('Name to greet') },
  outputSchema: { message: z.string() },
})
export default class HelloTool extends ToolContext {
  async execute(input: { name: string }) {
    return { message: `Hello, ${input.name}! Your ticket is ${formatTicketId(7)}.` };
  }
}
```

`nx dev tickets` serves it as it is: the CLI runs in `apps/tickets/`, where `tsconfig.json` extends the base config that has the alias, and `frontmcp dev`'s type checker reports `Found 0 errors.` Calling the tool returns `{"message":"Hello, Ada! Your ticket is T-0007."}`, and the graph shows `tickets -> ticket-model`. A spec in the app can import the alias too: the generated `jest.config.cjs` maps every alias in `paths`. Update the generated `hello.tool.spec.ts` to the new message, or `nx test tickets` fails on it.

`nx build tickets` and `nx typecheck tickets` work too:

```text
$ npx nx build tickets
> nx run tickets:build
…
[build:exec] bundle created: dist/node/tickets.bundle.js (25.1 KB)

 NX   Successfully ran target build for project tickets
```

The library is compiled into the bundle, so the built server runs without it: `PORT=8080 apps/tickets/dist/node/tickets`. In 1.8.7 this build stopped first with `[@nx/js:typescript-sync]: Missing root "tsconfig.json"`, from a `build` that Nx's TypeScript plugin gave the library, until you added a root `tsconfig.json` ([Troubleshooting](#the-workspace-is-probably-out-of-sync-because-a-sync-generator-failed-to-run)).

### One server per environment

Each server shell is its own `@FrontMcp` server, so shells can serve different apps, with different options, from the same code:

```ts servers/staging/src/main.ts
import 'reflect-metadata';
import { FrontMcp } from '@frontmcp/sdk';
import { DemoApp } from '../../../apps/demo/src/demo.app';
import { TicketsApp } from '../../../apps/tickets/src/tickets.app';

@FrontMcp({
  info: { name: 'Help Desk (staging)', version: '0.1.0' },
  apps: [DemoApp, TicketsApp], // every app, to try them out
})
export default class Server {}
```

```ts servers/production/src/main.ts
import 'reflect-metadata';
import { FrontMcp } from '@frontmcp/sdk';
import { TicketsApp } from '../../../apps/tickets/src/tickets.app';

@FrontMcp({
  info: { name: 'Help Desk', version: '1.0.0' },
  apps: [TicketsApp], // only what's released
  // auth, redis, http… for this environment: see @FrontMcp
})
export default class Server {}
```

[`@FrontMcp`](https://frontmcp.dev/reference/sdk/frontmcp) has the options, like `auth`, `redis` and `http`, that differ between environments. Secrets and addresses belong in each environment's variables, not in the shell: [Configuration files](https://frontmcp.dev/reference/server/config-files#environment-variables). Each shell the generator makes has a `dev` target, so two run side by side with `nx dev server-staging --port=4110` and `nx dev server-production --port=4111`.

### Tagging projects and finding what changed

`--tags` on the generators writes tags into `project.json` (the default is `scope:apps` for apps, `scope:servers` for shells and `scope:libs` for libraries). Nx selects projects by tag:

```bash
npx nx g @frontmcp/nx:app billing --tags scope:billing,type:app
npx nx show projects --projects "tag:scope:billing"
npx nx show projects --projects "tag:scope:servers"
```

```text
["billing"]
["server-gateway"]
```

After a change to `apps/tickets/`, Nx lists the projects it affects, the app and every shell that imports it, and after a change to a library, the library as well:

```bash
npx nx show projects --affected --base=HEAD
```

```text
["tickets","server-gateway"]
["ticket-model","tickets","server-gateway"]
```

`npx nx affected -t build` builds only those. Nx can also enforce which tags may import which, with ESLint; the workspace `frontmcp create --nx` makes has no ESLint configuration, so that's yours to add.

### Moving a standalone project into a workspace

1. Create the workspace, next to the project: `npx frontmcp create help-desk-platform --nx` ([Nx plugin](https://frontmcp.dev/reference/nx#creating-a-workspace)).
2. Generate an app for it: `npx nx g @frontmcp/nx:app help-desk`.
3. Copy the code: `cp ../help-desk/src/tools/*.ts apps/help-desk/src/tools/`, and the same for resources and prompts. If you don't want the generated `hello` tool, delete `hello.tool.ts` and its test, `hello.tool.spec.ts`.
4. List them in the app, which the generator named `HelpDeskApp`:

   ```ts apps/help-desk/src/help-desk.app.ts
   import { App } from '@frontmcp/sdk';
   import AddTool from './tools/add.tool';

   @App({
     id: 'help-desk',
     name: 'HelpDesk',
     tools: [AddTool],
   })
   export class HelpDeskApp {}
   ```

   The generated `main.ts` serves `HelpDeskApp`. If your old `main.ts` set options (`auth`, `http`, `redis`…), copy them into it.
5. Copy the tests to `apps/help-desk/e2e/`, with a `port` of their own in `test.use`. The `test` target the generator wrote runs them: it finds `jest.config.cjs` in the app, which loads the matchers.
6. Check it:

   ```bash
   npx nx dev help-desk
   npx nx test help-desk
   npx nx build help-desk
   ```

   ```text
   Tests:       4 passed, 4 total
    NX   Successfully ran target test for project help-desk
   [build:exec] bundle created: dist/node/help-desk.bundle.js (24.4 KB)
    NX   Successfully ran target build for project help-desk
   ```

7. Move code other apps will need into `libs/` as [above](#sharing-code-in-a-library).

`frontmcp.config.ts` comes along: copy it into `apps/help-desk/`. The executors run the CLI there, so `dev`, `build`, `test` and `inspector` read it, and its `name`, `version`, `transport` and `env` apply to that app. A `.env` in the app's folder is read by `dev` too.

---

## Troubleshooting

### `The workspace is probably out of sync because a sync generator failed to run`

With `[@nx/js:typescript-sync]: Missing root "tsconfig.json"`, from `nx build` of an app that imports a library, in a workspace made with 1.8.7. Its `nx.json` lists the `@nx/js/typescript` plugin, which gives each library an inferred `build` that needs a root `tsconfig.json`. Remove the plugin from `nx.json`, as 1.9.2 workspaces don't have it ([Upgrading a 1.8.7 workspace](https://frontmcp.dev/reference/nx#upgrading-a-187-workspace)). Or keep it and add a root `tsconfig.json`, which can be nearly empty:

```json tsconfig.json
{
  "extends": "./tsconfig.base.json",
  "files": [],
  "references": []
}
```

### `Cannot find module '@help-desk/ticket-model'`

The alias isn't in `tsconfig.base.json`'s `paths`, or the app's `tsconfig.json` doesn't extend that file. Run the `lib` generator in this workspace, or add the alias.

### `error TS5069` from `nx typecheck`

A project made before 1.9, with no `typecheck` target of its own, in a workspace with Nx's TypeScript plugin, whose inferred one fails. Add the `typecheck` target a 1.9.2 project has ([Nx plugin](https://frontmcp.dev/reference/nx#seeing-what-an-app-runs)), or use `npx tsc -p apps/<app>/tsconfig.lib.json --noEmit`.

### A tool's name has its app's id in front, like `tickets:hello`

Two apps on the server have a tool with the same name. Rename one, or call the prefixed name. See [Handling name clashes](https://frontmcp.dev/reference/server/apps#handling-name-clashes).

### `nx affected` doesn't list a server shell

The shell doesn't import the changed app. Check the imports in `servers/<name>/src/main.ts`: the generator writes relative paths to each app's `src/<app>.app`.

### `Could not resolve "…/dist/node/main.js"`

`tsc` emitted no JavaScript, because the workspace's `tsconfig.base.json` sets `emitDeclarationOnly` and the app's `tsconfig.json`, from before 1.9, doesn't turn it off. See [Nx plugin](https://frontmcp.dev/reference/nx#adding-the-plugin-to-a-workspace-you-have).
