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
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,17 @@
* - 1 Job: process-data (input/output schemas)
*/
import 'reflect-metadata';
import { Tool, ToolContext, Resource, Prompt, Skill, Job, JobContext } from '@frontmcp/sdk';
import {
Tool,
ToolContext,
Resource,
ResourceContext,
Prompt,
PromptContext,
Skill,
Job,
JobContext,
} from '@frontmcp/sdk';
import { z } from 'zod';

// ═══════════════════════════════════════════════════════════════════
Expand Down Expand Up @@ -57,12 +67,12 @@ export class AddTool extends ToolContext {
mimeType: 'application/json',
description: 'Server status from decorated package',
})
export class StatusResource {
execute() {
export class StatusResource extends ResourceContext {
async execute(uri: string) {
return {
contents: [
{
uri: 'esm://status',
uri,
text: JSON.stringify({ status: 'ok', source: 'decorated-package' }),
},
],
Expand All @@ -79,8 +89,8 @@ export class StatusResource {
description: 'A greeting prompt template',
arguments: [{ name: 'name', description: 'Name to greet', required: true }],
})
export class GreetingPrompt {
execute(args: Record<string, string>) {
export class GreetingPrompt extends PromptContext {
async execute(args: Record<string, string>) {
return {
messages: [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* The ESM loader detects decorated classes automatically — no manifest needed.
*/
import 'reflect-metadata';
import { Prompt } from '@frontmcp/sdk';
import { Prompt, PromptContext } from '@frontmcp/sdk';

@Prompt({
name: 'summarize',
Expand All @@ -14,8 +14,8 @@ import { Prompt } from '@frontmcp/sdk';
{ name: 'style', description: 'Summary style (brief/detailed)', required: false },
],
})
export class SummarizePrompt {
execute(args: Record<string, string>) {
export class SummarizePrompt extends PromptContext {
async execute(args: Record<string, string>) {
const style = args?.['style'] ?? 'brief';
return {
messages: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,20 @@
* The ESM loader detects decorated classes automatically — no manifest needed.
*/
import 'reflect-metadata';
import { Resource } from '@frontmcp/sdk';
import { Resource, ResourceContext } from '@frontmcp/sdk';

@Resource({
name: 'config',
uri: 'esm://config',
mimeType: 'application/json',
description: 'Application configuration',
})
export class ConfigResource {
execute() {
export class ConfigResource extends ResourceContext {
async execute(uri: string) {
return {
contents: [
{
uri: 'esm://config',
uri,
text: JSON.stringify({ env: 'test', version: '1.0.0' }),
},
],
Expand All @@ -31,12 +31,12 @@ export class ConfigResource {
mimeType: 'application/json',
description: 'Health check endpoint',
})
export class HealthResource {
execute() {
export class HealthResource extends ResourceContext {
async execute(uri: string) {
return {
contents: [
{
uri: 'esm://health',
uri,
text: JSON.stringify({ healthy: true, uptime: 12345 }),
},
],
Expand Down
115 changes: 115 additions & 0 deletions libs/sdk/src/common/decorators/__tests__/agent-type-safety.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import 'reflect-metadata';
import { z } from 'zod';
import { Agent, AgentContext } from '../../';

// ════════════════════════════════════════════════════════════════
// Type-level tests: verified by `nx typecheck sdk`
//
// Each [ts-expect-error] MUST correspond to an actual type error.
// If the error disappears (regression), tsc will flag it as
// unused, failing the typecheck.
//
// Invalid decorator usages are wrapped in never-called functions
// to prevent Zod runtime validation errors while still being
// type-checked by TypeScript.
// ════════════════════════════════════════════════════════════════

// ── Valid: agent with inputSchema and llm config ────────────

@Agent({
name: 'valid-agent',
inputSchema: { topic: z.string() },
llm: { provider: 'openai', model: 'gpt-4', apiKey: 'test-key' },
})
class ValidAgent extends AgentContext {
override async execute(input: { topic: string }) {
return { result: input.topic };
}
}

// ── Valid: agent with guard configs ──────────────────────────

@Agent({
name: 'valid-agent-guards',
inputSchema: { topic: z.string() },
llm: { provider: 'openai', model: 'gpt-4', apiKey: 'test-key' },
concurrency: { maxConcurrent: 2 },
rateLimit: { maxRequests: 10, windowMs: 60_000 },
})
class ValidAgentWithGuards extends AgentContext {
override async execute(input: { topic: string }) {
return { result: input.topic };
}
}

// ── Invalid: concurrency typo ───────────────────────────────

function _testInvalidConcurrency() {
// @ts-expect-error - decorator fails: invalid concurrency property cascades to unresolvable signature
@Agent({
name: 'bad-concurrency-agent',
inputSchema: { topic: z.string() },
llm: { provider: 'openai', model: 'gpt-4', apiKey: 'test-key' },
concurrency: {
// @ts-expect-error - 'maxConcurrensst' does not exist on ConcurrencyConfigInput
maxConcurrensst: 5,
},
})
class BadConcurrencyAgent extends AgentContext {
override async execute(input: { topic: string }) {
return {};
}
}
void BadConcurrencyAgent;
}

// ── Invalid: wrong execute() param type ─────────────────────

function _testWrongParamType() {
// @ts-expect-error - execute() param { topic: number } does not match input schema { topic: string }
@Agent({
name: 'wrong-param-agent',
inputSchema: { topic: z.string() },
llm: { provider: 'openai', model: 'gpt-4', apiKey: 'test-key' },
})
class WrongParamAgent extends AgentContext {
override async execute(input: { topic: number }) {
return {};
}
}
void WrongParamAgent;
}

// ── Invalid: class not extending AgentContext ───────────────

function _testNotAgentContext() {
// @ts-expect-error - class must extend AgentContext
@Agent({
name: 'not-agent-context',
inputSchema: { topic: z.string() },
llm: { provider: 'openai', model: 'gpt-4', apiKey: 'test-key' },
})
class NotAgentContext {
async execute(input: { topic: string }) {
return {};
}
}
void NotAgentContext;
}

// Suppress unused variable/function warnings
void ValidAgent;
void ValidAgentWithGuards;
void _testInvalidConcurrency;
void _testWrongParamType;
void _testNotAgentContext;

// ════════════════════════════════════════════════════════════════
// Runtime placeholder (required by Jest)
// ════════════════════════════════════════════════════════════════

describe('Agent decorator type safety', () => {
it('type assertions are verified by tsc --noEmit (nx typecheck sdk)', () => {
expect(true).toBe(true);
});
});
95 changes: 95 additions & 0 deletions libs/sdk/src/common/decorators/__tests__/job-type-safety.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import 'reflect-metadata';
import { z } from 'zod';
import { Job, JobContext } from '../../';

// ════════════════════════════════════════════════════════════════
// Type-level tests: verified by `nx typecheck sdk`
//
// Each [ts-expect-error] MUST correspond to an actual type error.
// If the error disappears (regression), tsc will flag it as
// unused, failing the typecheck.
//
// Invalid decorator usages are wrapped in never-called functions
// to prevent Zod runtime validation errors while still being
// type-checked by TypeScript.
// ════════════════════════════════════════════════════════════════

// ── Valid: job with inputSchema + outputSchema ──────────────

@Job({
name: 'valid-job',
inputSchema: { data: z.string() },
outputSchema: { result: z.string() },
})
class ValidJob extends JobContext {
async execute(input: { data: string }) {
return { result: input.data };
}
}

// ── Invalid: wrong execute() param type ─────────────────────

function _testWrongParamType() {
// @ts-expect-error - execute() param { data: number } does not match input schema { data: string }
@Job({
name: 'wrong-param-job',
inputSchema: { data: z.string() },
outputSchema: { result: z.string() },
})
class WrongParamJob extends JobContext {
async execute(input: { data: number }) {
return { result: 'ok' };
}
}
void WrongParamJob;
}

// ── Invalid: wrong execute() return type ────────────────────

function _testWrongReturnType() {
// @ts-expect-error - execute() returns { result: string } but outputSchema expects { result: number }
@Job({
name: 'wrong-return-job',
inputSchema: { data: z.string() },
outputSchema: { result: z.number() },
})
class WrongReturnJob extends JobContext {
async execute(input: { data: string }) {
return { result: 'not-a-number' };
}
}
void WrongReturnJob;
}

// ── Invalid: class not extending JobContext ──────────────────

function _testNotJobContext() {
// @ts-expect-error - class must extend JobContext
@Job({
name: 'not-job-context',
inputSchema: { data: z.string() },
outputSchema: { result: z.string() },
})
class NotJobContext {
async execute(input: { data: string }) {
return { result: input.data };
}
}
void NotJobContext;
}

// Suppress unused variable/function warnings
void ValidJob;
void _testWrongParamType;
void _testWrongReturnType;
void _testNotJobContext;

// ════════════════════════════════════════════════════════════════
// Runtime placeholder (required by Jest)
// ════════════════════════════════════════════════════════════════

describe('Job decorator type safety', () => {
it('type assertions are verified by tsc --noEmit (nx typecheck sdk)', () => {
expect(true).toBe(true);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import 'reflect-metadata';
import { Prompt, PromptContext } from '../../';
import { GetPromptResult } from '@frontmcp/protocol';

// ════════════════════════════════════════════════════════════════
// Type-level tests: verified by `nx typecheck sdk`
//
// Each [ts-expect-error] MUST correspond to an actual type error.
// If the error disappears (regression), tsc will flag it as
// unused, failing the typecheck.
//
// Invalid decorator usages are wrapped in never-called functions
// to prevent runtime errors while still being type-checked.
// ════════════════════════════════════════════════════════════════

// ── Valid: Prompt extending PromptContext ────────────────────

@Prompt({ name: 'valid-prompt', arguments: [{ name: 'topic', required: true }] })
class ValidPrompt extends PromptContext {
async execute(args: Record<string, string>): Promise<GetPromptResult> {
return {
messages: [{ role: 'user', content: { type: 'text', text: args['topic'] ?? '' } }],
};
}
}

// ── Invalid: Prompt class not extending PromptContext ────────

function _testNotPromptContext() {
// @ts-expect-error - class must extend PromptContext
@Prompt({ name: 'not-prompt-ctx', arguments: [] })
class NotPromptContext {
async execute(args: Record<string, string>): Promise<GetPromptResult> {
return { messages: [{ role: 'user', content: { type: 'text', text: 'ok' } }] };
}
}
void NotPromptContext;
}

// Suppress unused variable/function warnings
void ValidPrompt;
void _testNotPromptContext;

// ════════════════════════════════════════════════════════════════
// Runtime placeholder (required by Jest)
// ════════════════════════════════════════════════════════════════

describe('Prompt decorator type safety', () => {
it('type assertions are verified by tsc --noEmit (nx typecheck sdk)', () => {
expect(true).toBe(true);
});
});
Loading
Loading