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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/frontmcp/deployment/local-dev-server.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ npm run docker:down

# Rebuild the Docker image
npm run docker:build

# Rebuild only the app service (selective rebuild — skips Redis)
docker compose -f ci/docker-compose.yml up --build app
```

---
Expand Down
7 changes: 6 additions & 1 deletion docs/frontmcp/getting-started/cli-reference.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Commands for building, testing, and debugging your FrontMCP server.
| `build --exec --cli` | Build CLI executable with subcommands per tool |
| `test` | Run E2E tests with auto-injected Jest configuration |
| `init` | Create or fix a tsconfig.json suitable for FrontMCP |
| `doctor` | Check Node/npm versions and tsconfig requirements |
| `doctor` | Check Node/npm versions and tsconfig requirements. Use `--fix` to auto-repair (installs missing deps, creates app directories) |
| `inspector` | Launch MCP Inspector (`npx @modelcontextprotocol/inspector`) |
| `create [name]` | Scaffold a new FrontMCP project (interactive if name omitted) |
| `socket <entry>` | Start Unix socket daemon for local MCP server |
Expand Down Expand Up @@ -142,6 +142,11 @@ See [ESM Packages](/frontmcp/servers/esm-packages) for full documentation on ESM
| `--no-cicd` | Disable GitHub Actions CI/CD |
| `--nx` | Scaffold an Nx monorepo workspace |

<Info>
The `create` command automatically initializes a git repository and creates an initial commit after scaffolding.
If `git` is not installed, this step is silently skipped.
</Info>

### Socket Options

| Option | Description |
Expand Down
1 change: 1 addition & 0 deletions docs/frontmcp/getting-started/quickstart.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ The CLI creates a complete project structure with:
- ✅ Hot-reload enabled
- ✅ Deployment configuration for your target platform
- ✅ GitHub Actions CI/CD (optional)
- ✅ Git repository initialized with initial commit

<Check>Your server is now running at `http://localhost:3000`!</Check>

Expand Down
2 changes: 1 addition & 1 deletion docs/frontmcp/nx-plugin/generators/workspace.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ npx frontmcp create my-platform --nx
| `name` | `string` | — | **Required.** The name of the workspace |
| `packageManager` | `npm` \| `yarn` \| `pnpm` | `npm` | Package manager to use |
| `skipInstall` | `boolean` | `false` | Skip package installation |
| `skipGit` | `boolean` | `false` | Skip git initialization |
| `skipGit` | `boolean` | `false` | Skip git initialization. When `false`, runs `git init` and creates an initial commit. |
| `createSampleApp` | `boolean` | `true` | Create a sample demo application |

## Generated Files
Expand Down
19 changes: 16 additions & 3 deletions docs/frontmcp/sdk-reference/decorators/agent.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,24 @@ export default class ResearchAgent extends AgentContext {
## Signature

```typescript
function Agent<InSchema, OutSchema>(
providedMetadata: AgentMetadata<InSchema, OutSchema>
): ClassDecorator
function Agent<I extends ZodRawShape, O extends OutputSchema>(
opts: AgentMetadataOptions<I, O>
): TypedClassDecorator
```

## Type Safety

The `@Agent` decorator validates at compile time that:

- The decorated class extends `AgentContext`
- When `inputSchema` is provided, the `execute()` parameter matches the schema type
- When `outputSchema` is provided, the `execute()` return type is compatible
- Invalid metadata options (e.g., typos in `concurrency`) produce specific compile-time errors

<Note>
Agents can use the default `execute()` from `AgentContext` (which runs the LLM agent loop) without overriding it. The type checker allows this pattern.
</Note>

## Configuration Options

### Required Properties
Expand Down
15 changes: 12 additions & 3 deletions docs/frontmcp/sdk-reference/decorators/job.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,20 @@ class SendEmailJob extends JobContext {
## Signature

```typescript
function Job<I extends ToolInputType, O extends ToolOutputType>(
opts: JobMetadata<I, O>
): ClassDecorator
function Job<I extends ZodRawShape, O extends OutputSchema>(
opts: JobMetadata<I, O> & { outputSchema: O }
): TypedClassDecorator
```

## Type Safety

The `@Job` decorator enforces at compile time:

- The decorated class must extend `JobContext`
- The `execute()` parameter must exactly match the `inputSchema` type
- The `execute()` return type must match the `outputSchema` type
- Both `inputSchema` and `outputSchema` are required for Jobs

## Configuration Options

### Required Properties
Expand Down
6 changes: 5 additions & 1 deletion docs/frontmcp/sdk-reference/decorators/prompt.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,13 @@ class ResearchPrompt extends PromptContext {
## Signature

```typescript
function Prompt(providedMetadata: PromptMetadata): ClassDecorator
function Prompt(opts: PromptMetadata): TypedClassDecorator
```

## Type Safety

The `@Prompt` decorator validates at compile time that the decorated class extends `PromptContext`. Using it on a plain class produces a descriptive compile error.

## Configuration Options

### Required Properties
Expand Down
8 changes: 7 additions & 1 deletion docs/frontmcp/sdk-reference/decorators/resource.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,15 @@ class AppConfigResource extends ResourceContext {
## Signature

```typescript
function Resource(providedMetadata: ResourceMetadata): ClassDecorator
function Resource(opts: ResourceMetadata): TypedClassDecorator
```

## Type Safety

The `@Resource` decorator validates at compile time that the decorated class extends `ResourceContext`. Using it on a plain class produces a descriptive compile error.

The same applies to `@ResourceTemplate`.

## Configuration Options

### Required Properties
Expand Down
23 changes: 21 additions & 2 deletions docs/frontmcp/sdk-reference/decorators/tool.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,28 @@ class GetWeatherTool extends ToolContext {
## Signature

```typescript
function Tool<I extends Shape, O extends OutputSchema>(
function Tool<I extends ZodRawShape, O extends OutputSchema>(
opts: ToolMetadataOptions<I, O>
): ClassDecorator
): TypedClassDecorator
```

## Type Safety

The `@Tool` decorator provides compile-time type checking:

- **Input validation**: The `execute()` parameter type must exactly match the `inputSchema`. Mismatches produce a descriptive error at compile time.
- **Output validation**: When `outputSchema` is provided, the `execute()` return type must be assignable to the inferred output type.
- **Context check**: The decorated class must extend `ToolContext`. Using `@Tool` on a plain class produces a compile error.
- **Invalid options**: Typos in decorator options (e.g., `concurrency: { maxConcurrensst: 5 }`) are caught at compile time with specific error messages, without losing autocomplete on other fields.

```typescript
// Compile error: parameter type { query: number } doesn't match inputSchema { query: string }
@Tool({ name: 'search', inputSchema: { query: z.string() } })
class BadTool extends ToolContext {
async execute(input: { query: number }) { // TS error on this class
return { result: input.query };
}
}
```

## Configuration Options
Expand Down
14 changes: 13 additions & 1 deletion libs/cli/src/commands/build/exec/__tests__/daemon-client.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,10 +150,22 @@ describe('generateDaemonClientSource', () => {
});
});

it('should include _isDaemon flag on client object', () => {
expect(source).toContain('_isDaemon: true');
});

it('should warn about notification limitations in daemon mode', () => {
expect(source).toContain('Notifications are not supported in daemon mode');
});

it('should warn about resource subscription limitations in daemon mode', () => {
expect(source).toContain('Resource subscriptions are not supported in daemon mode');
});

it('should be parseable as JavaScript', () => {
// Verify the generated source is valid JS by creating a function from it
expect(() => {

new Function(source);
}).not.toThrow();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1111,6 +1111,70 @@ describe('extractTemplateParams', () => {
});
});

describe('daemon config extraction', () => {
it('should use Reflect.getMetadata for class config resolution in daemon script', () => {
const source = generateCliEntry(makeOptions());
expect(source).toContain('Reflect.getMetadata("__frontmcp:config"');
});

it('should require reflect-metadata in daemon script', () => {
const source = generateCliEntry(makeOptions());
expect(source).toContain('require("reflect-metadata")');
});

it('should fall back to raw module if not a function', () => {
const source = generateCliEntry(makeOptions());
// The daemon script checks typeof raw === "function" before using Reflect
expect(source).toContain('typeof raw === "function"');
});
});

describe('doctor --fix app directory', () => {
it('should create app directory when --fix is used', () => {
const source = generateCliEntry(makeOptions({
nativeDeps: {},
}));
expect(source).toContain('fs.mkdirSync(appDir, { recursive: true })');
expect(source).toContain('[fixed] Created');
});

it('should only create directory when opts.fix is true', () => {
const source = generateCliEntry(makeOptions());
expect(source).toContain('if (opts.fix)');
expect(source).toContain('App directory not found');
});
});

describe('subscribe commands', () => {
it('should include setInterval for event loop keep-alive', () => {
const source = generateCliEntry(makeOptions());
expect(source).toContain('setInterval(function() {}, 2147483647)');
});

it('should define getSubscribeClient function', () => {
const source = generateCliEntry(makeOptions());
expect(source).toContain('async function getSubscribeClient()');
});

it('should use getSubscribeClient instead of getClient for subscribe commands', () => {
const source = generateCliEntry(makeOptions());
// Subscribe resource and notification should use getSubscribeClient
expect(source).toContain('await getSubscribeClient()');
});

it('should detect daemon mode via _isDaemon flag', () => {
const source = generateCliEntry(makeOptions());
expect(source).toContain('client._isDaemon');
});

it('should reconnect via in-process when daemon is detected', () => {
const source = generateCliEntry(makeOptions());
// When daemon detected, should clear cached client and use connect()
expect(source).toContain('_client = null');
expect(source).toContain("connect(configOrClass, { mode: 'cli' })");
});
});

describe('RESERVED_COMMANDS', () => {
it('should contain all expected reserved names', () => {
expect(RESERVED_COMMANDS.has('resource')).toBe(true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ function createDaemonClient(socketPath) {
}

return {
_isDaemon: true,
ping: function() {
return call('ping');
},
Expand Down
45 changes: 38 additions & 7 deletions libs/cli/src/commands/build/exec/cli-runtime/generate-cli-entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -833,14 +833,33 @@ workflowCmd
}

function generateSubscribeCommands(): string {
return `var subscribeCmd = program.command('subscribe').description('Subscribe to updates');
return `
// Subscribe commands need push support (onNotification/onResourceUpdated).
// Daemon HTTP cannot push, so we force in-process when daemon was used.
async function getSubscribeClient() {
var client = await getClient();
// If connected via daemon, the onNotification/onResourceUpdated are no-ops.
// Reconnect via in-process for push support.
if (client._isDaemon) {
_client = null; // clear cached daemon client
var mod = require(SERVER_BUNDLE);
var configOrClass = mod.default || mod;
var sdk = require('@frontmcp/sdk');
var connect = sdk.connect || sdk.direct.connect;
_client = await connect(configOrClass, { mode: 'cli' });
return _client;
}
return client;
}

var subscribeCmd = program.command('subscribe').description('Subscribe to updates');

subscribeCmd
.command('resource <uri>')
.description('Stream resource updates (Ctrl+C to stop)')
.action(async function(uri) {
try {
var client = await getClient();
var client = await getSubscribeClient();
await client.subscribeResource(uri);
var mode = program.opts().output || 'text';
console.log('Subscribed to resource: ' + uri);
Expand All @@ -853,7 +872,9 @@ subscribeCmd
try { await client.unsubscribeResource(uri); } catch (_) { /* ok */ }
process.exit(0);
});
// Keep process alive
// Keep process alive — setInterval creates an active event loop handle
// so Node.js won't exit even with InMemoryTransport (no persistent I/O)
setInterval(function() {}, 2147483647);
await new Promise(function() {});
} catch (err) {
console.error('Error:', err.message || err);
Expand All @@ -866,7 +887,7 @@ subscribeCmd
.description('Stream notifications (Ctrl+C to stop)')
.action(async function(name) {
try {
var client = await getClient();
var client = await getSubscribeClient();
var mode = program.opts().output || 'text';
console.log('Listening for notification: ' + name);
console.log('Waiting for events... (Ctrl+C to stop)\\n');
Expand All @@ -879,7 +900,9 @@ subscribeCmd
console.log('\\nStopping...');
process.exit(0);
});
// Keep process alive
// Keep process alive — setInterval creates an active event loop handle
// so Node.js won't exit even with InMemoryTransport (no persistent I/O)
setInterval(function() {}, 2147483647);
await new Promise(function() {});
} catch (err) {
console.error('Error:', err.message || err);
Expand Down Expand Up @@ -1097,6 +1120,10 @@ ${checks.join(',\n')}
} else {
console.log(' [!!] App directory not found: ' + appDir);
ok = false;
if (opts.fix) {
fs.mkdirSync(appDir, { recursive: true });
console.log(' [fixed] Created ' + appDir);
}
}

if (ok) console.log('\\nAll checks passed.');
Expand Down Expand Up @@ -1261,10 +1288,14 @@ daemonCmd
// Start the daemon using runUnixSocket via a small wrapper script
// Always use absolute path for the server bundle (SCRIPT_DIR resolves to __dirname at runtime)
var serverBundlePath = pathMod.join(SCRIPT_DIR, ${JSON.stringify(serverBundleFilename)});
var daemonScript = 'var mod = require(' + JSON.stringify(serverBundlePath) + ');' +
var daemonScript = 'require("reflect-metadata");' +
'var mod = require(' + JSON.stringify(serverBundlePath) + ');' +
'var sdk = require("@frontmcp/sdk");' +
'var FrontMcpInstance = sdk.FrontMcpInstance || sdk.default.FrontMcpInstance;' +
'var config = mod.default || mod;' +
'var raw = mod.default || mod;' +
// If the export is a @FrontMcp-decorated class, extract config via Reflect metadata
'var config = (typeof raw === "function" && typeof Reflect !== "undefined" && Reflect.getMetadata) ' +
' ? (Reflect.getMetadata("__frontmcp:config", raw) || raw) : raw;' +
'FrontMcpInstance.runUnixSocket(Object.assign({}, config, { socketPath: ' + JSON.stringify(socketPath) + ' }))' +
'.then(function() { console.log("Daemon listening on " + ' + JSON.stringify(socketPath) + '); })' +
'.catch(function(e) { console.error("Daemon failed:", e); process.exit(1); });';
Expand Down
Loading
Loading