diff --git a/.gitignore b/.gitignore index 22dab542a..320dac513 100644 --- a/.gitignore +++ b/.gitignore @@ -83,3 +83,4 @@ apps/*/src/**/*.d.ts.map # Performance test results (generated) perf-results/ /test-results/.last-run.json +/.claude/planning/ diff --git a/libs/sdk/src/__test-utils__/fixtures/plugin.fixtures.ts b/libs/sdk/src/__test-utils__/fixtures/plugin.fixtures.ts index ab36f5b11..bf71c8299 100644 --- a/libs/sdk/src/__test-utils__/fixtures/plugin.fixtures.ts +++ b/libs/sdk/src/__test-utils__/fixtures/plugin.fixtures.ts @@ -4,9 +4,7 @@ */ import { PluginMetadata } from '../../common/metadata'; -import { PluginInterface } from '../../common/interfaces'; import { createProviderMetadata } from './provider.fixtures'; -import { createToolMetadata } from './tool.fixtures'; /** * Creates a simple plugin metadata object @@ -61,7 +59,7 @@ export function createPluginWithNestedPlugins(): PluginMetadata { /** * Mock plugin class for testing */ -export class MockPluginClass implements PluginInterface { +export class MockPluginClass { static readonly metadata: PluginMetadata = { name: 'MockPlugin', description: 'A mock plugin', diff --git a/libs/sdk/src/__test-utils__/fixtures/provider.fixtures.ts b/libs/sdk/src/__test-utils__/fixtures/provider.fixtures.ts index 83e0c46e5..d5b33ec81 100644 --- a/libs/sdk/src/__test-utils__/fixtures/provider.fixtures.ts +++ b/libs/sdk/src/__test-utils__/fixtures/provider.fixtures.ts @@ -5,7 +5,6 @@ import 'reflect-metadata'; import { ProviderMetadata, ProviderScope } from '../../common/metadata'; -import { ProviderInterface } from '../../common/interfaces'; import { FrontMcpProviderTokens } from '../../common/tokens/provider.tokens'; /** @@ -21,7 +20,7 @@ function Injectable() { * Simple test service class */ @Injectable() -export class TestService implements ProviderInterface { +export class TestService { public readonly name = 'TestService'; constructor() {} @@ -35,7 +34,7 @@ export class TestService implements ProviderInterface { * Service that depends on another service */ @Injectable() -export class DependentService implements ProviderInterface { +export class DependentService { constructor(public readonly testService: TestService) {} callGreet(): string { @@ -47,7 +46,7 @@ export class DependentService implements ProviderInterface { * Service with async initialization */ @Injectable() -export class AsyncService implements ProviderInterface { +export class AsyncService { private initialized = false; async with(callback: (service: this) => Promise): Promise { diff --git a/libs/sdk/src/agent/agent.instance.ts b/libs/sdk/src/agent/agent.instance.ts index 5c39341ae..1f6c7c827 100644 --- a/libs/sdk/src/agent/agent.instance.ts +++ b/libs/sdk/src/agent/agent.instance.ts @@ -29,7 +29,7 @@ import { ToolInstance } from '../tool/tool.instance'; import { normalizeTool } from '../tool/tool.utils'; import ProviderRegistry from '../provider/provider.registry'; import HookRegistry from '../hooks/hook.registry'; -import { Scope } from '../scope'; +import { ScopeEntry } from '../common'; import { normalizeHooksFromCls } from '../hooks/hooks.utils'; import { createAdapter, CreateAdapterOptions, ConfigResolver } from './adapters'; import { ConfigService } from '../builtin/config'; @@ -92,7 +92,7 @@ export class AgentInstance< Out = AgentOutputOf<{ outputSchema: OutSchema }>, > extends AgentEntry { private readonly providers: ProviderRegistry; - readonly scope: Scope; + readonly scope: ScopeEntry; readonly hooks: HookRegistry; /** The LLM adapter for this agent */ @@ -119,7 +119,7 @@ export class AgentInstance< this.id = record.metadata.id ?? record.metadata.name; this.fullName = this.owner.id + ':' + this.name; this.scope = this.providers.getActiveScope(); - this.hooks = this.scope.providers.getHooksRegistry(); + this.hooks = this.scope.hooks; // inputSchema is always a ZodRawShape this.inputSchema = (record.metadata.inputSchema ?? {}) as InSchema; diff --git a/libs/sdk/src/agent/agent.scope.ts b/libs/sdk/src/agent/agent.scope.ts index e9d7de0ab..1cc99842e 100644 --- a/libs/sdk/src/agent/agent.scope.ts +++ b/libs/sdk/src/agent/agent.scope.ts @@ -66,7 +66,7 @@ export class AgentScope { readonly logger: FrontMcpLogger; readonly ready: Promise; - private readonly parentScope: Scope; + private readonly parentScope: ScopeEntry; private readonly agentOwner: EntryOwnerRef; // Agent's own registries (like an app) @@ -81,7 +81,7 @@ export class AgentScope { private agentFlows!: FlowRegistry; constructor( - parentScope: Scope, + parentScope: ScopeEntry, agentId: string, private readonly metadata: AgentMetadata, agentToken: Token, @@ -260,6 +260,14 @@ export class AgentScope { return this.parentScope.toolUI; } + get skills() { + return this.parentScope.skills; + } + + get scopeMetadata() { + return this.parentScope.metadata; + } + // ============================================================================ // Flow Execution // ============================================================================ @@ -345,6 +353,10 @@ class AgentScopeEntry { return this.agentScope.agents; } + get skills() { + return this.agentScope.skills; + } + get notifications() { return this.agentScope.notifications; } @@ -353,6 +365,30 @@ class AgentScopeEntry { return this.agentScope.toolUI; } + get transportService(): undefined { + return undefined; + } + + get rateLimitManager(): undefined { + return undefined; + } + + get elicitationStore(): undefined { + return undefined; + } + + get metadata() { + return this.agentScope.scopeMetadata; + } + + get record(): undefined { + return undefined; + } + + get ready() { + return this.agentScope.ready; + } + registryFlows(...flows: FlowType[]): Promise { return this.agentScope.registryFlows(...flows); } @@ -364,4 +400,12 @@ class AgentScopeEntry { ): Promise | undefined> { return this.agentScope.runFlow(name, input, deps); } + + runFlowForOutput( + name: Name, + input: FlowInputOf, + deps?: Map, + ): Promise> { + return this.agentScope.runFlowForOutput(name, input, deps); + } } diff --git a/libs/sdk/src/agent/flows/call-agent.flow.ts b/libs/sdk/src/agent/flows/call-agent.flow.ts index c2e2b82b6..7a8e2393e 100644 --- a/libs/sdk/src/agent/flows/call-agent.flow.ts +++ b/libs/sdk/src/agent/flows/call-agent.flow.ts @@ -13,7 +13,6 @@ import { AgentExecutionError, RateLimitError, } from '../../errors'; -import { Scope } from '../../scope'; import { ExecutionTimeoutError, ConcurrencyLimitError, withTimeout, type SemaphoreTicket } from '@frontmcp/guard'; // ============================================================================ @@ -134,14 +133,12 @@ export default class CallAgentFlow extends FlowBase { // Find the agent early to get its owner ID for hook filtering const { name: toolName } = params; - const scope = this.scope as Scope; - // Agent ID is the tool name (agents use standard tool names) const agentId = toolName; let agent: AgentEntry | undefined; - if (scope.agents) { - agent = scope.agents.findById(agentId) ?? scope.agents.findByName(agentId); + if (this.scope.agents) { + agent = this.scope.agents.findById(agentId) ?? this.scope.agents.findByName(agentId); } // Store agent owner ID in state for hook filtering @@ -170,8 +167,7 @@ export default class CallAgentFlow extends FlowBase { async findAgent() { this.logger.verbose('findAgent:start'); - const scope = this.scope as Scope; - const agents = scope.agents; + const agents = this.scope.agents; if (!agents) { this.logger.warn('findAgent: no agent registry available'); @@ -317,7 +313,7 @@ export default class CallAgentFlow extends FlowBase { async acquireQuota() { this.logger.verbose('acquireQuota:start'); - const manager = (this.scope as Scope).rateLimitManager; + const manager = this.scope.rateLimitManager; if (!manager) { this.state.agentContext?.mark('acquireQuota'); this.logger.verbose('acquireQuota:done (no rate limit manager)'); @@ -357,7 +353,7 @@ export default class CallAgentFlow extends FlowBase { async acquireSemaphore() { this.logger.verbose('acquireSemaphore:start'); - const manager = (this.scope as Scope).rateLimitManager; + const manager = this.scope.rateLimitManager; if (!manager) { this.state.agentContext?.mark('acquireSemaphore'); this.logger.verbose('acquireSemaphore:done (no rate limit manager)'); @@ -434,7 +430,7 @@ export default class CallAgentFlow extends FlowBase { const timeoutMs = agent.metadata.timeout?.executeMs ?? agent.metadata.execution?.timeout ?? - (this.scope as Scope).rateLimitManager?.config?.defaultTimeout?.executeMs; + this.scope.rateLimitManager?.config?.defaultTimeout?.executeMs; try { const doExecute = async () => { diff --git a/libs/sdk/src/app/instances/app.esm.instance.ts b/libs/sdk/src/app/instances/app.esm.instance.ts index e75929776..12c0f3cdf 100644 --- a/libs/sdk/src/app/instances/app.esm.instance.ts +++ b/libs/sdk/src/app/instances/app.esm.instance.ts @@ -11,11 +11,8 @@ import { AppEntry, AppRecord, PluginRegistryInterface, - PromptRegistryInterface, ProviderRegistryInterface, RemoteAppMetadata, - ResourceRegistryInterface, - ToolRegistryInterface, EntryOwnerRef, PluginEntry, AdapterEntry, @@ -264,15 +261,15 @@ export class AppEsmInstance extends AppEntry { return this._plugins; } - override get tools(): ToolRegistryInterface { + override get tools(): ToolRegistry { return this._tools; } - override get resources(): ResourceRegistryInterface { + override get resources(): ResourceRegistry { return this._resources; } - override get prompts(): PromptRegistryInterface { + override get prompts(): PromptRegistry { return this._prompts; } diff --git a/libs/sdk/src/app/instances/app.local.instance.ts b/libs/sdk/src/app/instances/app.local.instance.ts index 49b291d1d..20dc77778 100644 --- a/libs/sdk/src/app/instances/app.local.instance.ts +++ b/libs/sdk/src/app/instances/app.local.instance.ts @@ -115,15 +115,15 @@ export class AppLocalInstance extends AppEntry { return this.appPlugins; } - get tools(): Readonly { + get tools(): ToolRegistry { return this.appTools; } - get resources(): Readonly { + get resources(): ResourceRegistry { return this.appResources; } - get prompts(): Readonly { + get prompts(): PromptRegistry { return this.appPrompts; } diff --git a/libs/sdk/src/app/instances/app.remote.instance.ts b/libs/sdk/src/app/instances/app.remote.instance.ts index 2969b7ff2..0d265524e 100644 --- a/libs/sdk/src/app/instances/app.remote.instance.ts +++ b/libs/sdk/src/app/instances/app.remote.instance.ts @@ -11,12 +11,9 @@ import { AppEntry, AppRecord, PluginRegistryInterface, - PromptRegistryInterface, ProviderRegistryInterface, RemoteAppMetadata, RemoteAuthConfig, - ResourceRegistryInterface, - ToolRegistryInterface, EntryOwnerRef, PluginEntry, AdapterEntry, @@ -296,15 +293,15 @@ export class AppRemoteInstance extends AppEntry { return this._plugins; } - override get tools(): ToolRegistryInterface { + override get tools(): ToolRegistry { return this._tools; } - override get resources(): ResourceRegistryInterface { + override get resources(): ResourceRegistry { return this._resources; } - override get prompts(): PromptRegistryInterface { + override get prompts(): PromptRegistry { return this._prompts; } diff --git a/libs/sdk/src/auth/auth.registry.ts b/libs/sdk/src/auth/auth.registry.ts index c6976d219..74f47ed77 100644 --- a/libs/sdk/src/auth/auth.registry.ts +++ b/libs/sdk/src/auth/auth.registry.ts @@ -8,7 +8,6 @@ import { FrontMcpLogger, AuthProviderType, AuthProviderEntry, - AuthRegistryInterface, AuthProviderRecord, AuthProviderKind, EntryOwnerRef, @@ -39,10 +38,7 @@ const DEFAULT_AUTH_OPTIONS: AuthOptionsInput = { mode: 'public', }; -export class AuthRegistry - extends RegistryAbstract - implements AuthRegistryInterface -{ +export class AuthRegistry extends RegistryAbstract { private readonly primary?: FrontMcpAuth; private readonly parsedOptions: AuthOptions; private readonly logger: FrontMcpLogger; diff --git a/libs/sdk/src/auth/instances/instance.remote-primary-auth.ts b/libs/sdk/src/auth/instances/instance.remote-primary-auth.ts index 23556002d..8da0fcac7 100644 --- a/libs/sdk/src/auth/instances/instance.remote-primary-auth.ts +++ b/libs/sdk/src/auth/instances/instance.remote-primary-auth.ts @@ -6,7 +6,6 @@ import WellKnownPrmFlow from '../flows/well-known.prm.flow'; import WellKnownAsFlow from '../flows/well-known.oauth-authorization-server.flow'; import WellKnownJwksFlow from '../flows/well-known.jwks.flow'; import SessionVerifyFlow from '../flows/session.verify.flow'; -import { Scope } from '../../scope'; export class RemotePrimaryAuth extends FrontMcpAuth { override ready: Promise; @@ -49,7 +48,7 @@ export class RemotePrimaryAuth extends FrontMcpAuth { return Promise.resolve(); } - private async registerAuthFlows(scope: Scope) { + private async registerAuthFlows(scope: ScopeEntry) { await scope.registryFlows( WellKnownPrmFlow /** /.well-known/oauth-protected-resource */, WellKnownAsFlow /** /.well-known/oauth-authorization-server */, diff --git a/libs/sdk/src/common/entries/app.entry.ts b/libs/sdk/src/common/entries/app.entry.ts index c52e73544..efdb3c42b 100644 --- a/libs/sdk/src/common/entries/app.entry.ts +++ b/libs/sdk/src/common/entries/app.entry.ts @@ -1,18 +1,13 @@ import { BaseEntry } from './base.entry'; import { AppRecord } from '../records'; -import { - AdapterRegistryInterface, - AppInterface, - PluginRegistryInterface, - PromptRegistryInterface, - ProviderRegistryInterface, - ResourceRegistryInterface, - ToolRegistryInterface, -} from '../interfaces'; +import { AdapterRegistryInterface, PluginRegistryInterface, ProviderRegistryInterface } from '../interfaces'; import type { SkillRegistryInterface } from '../../skill/skill.registry'; import { AppMetadata } from '../metadata'; +import type ToolRegistry from '../../tool/tool.registry'; +import type ResourceRegistry from '../../resource/resource.registry'; +import type PromptRegistry from '../../prompt/prompt.registry'; -export abstract class AppEntry extends BaseEntry { +export abstract class AppEntry extends BaseEntry { readonly id: string; /** @@ -31,11 +26,11 @@ export abstract class AppEntry extends BaseEntry { +export abstract class PluginEntry extends BaseEntry { abstract get(token: Token): T; } diff --git a/libs/sdk/src/common/entries/provider.entry.ts b/libs/sdk/src/common/entries/provider.entry.ts index ce713ada6..34b2ba635 100644 --- a/libs/sdk/src/common/entries/provider.entry.ts +++ b/libs/sdk/src/common/entries/provider.entry.ts @@ -1,8 +1,7 @@ import { BaseEntry } from './base.entry'; import type { ProviderRecord } from '../records'; -import type { ProviderInterface } from '../interfaces'; import type { ProviderMetadata } from '../metadata'; -abstract class ProviderEntry extends BaseEntry {} +abstract class ProviderEntry extends BaseEntry {} export { ProviderEntry }; diff --git a/libs/sdk/src/common/entries/scope.entry.ts b/libs/sdk/src/common/entries/scope.entry.ts index d77fc3819..955bdd4cc 100644 --- a/libs/sdk/src/common/entries/scope.entry.ts +++ b/libs/sdk/src/common/entries/scope.entry.ts @@ -2,26 +2,30 @@ import { Token, Type } from '@frontmcp/di'; import { BaseEntry } from './base.entry'; import { ScopeRecord } from '../records'; import { - ScopeInterface, ProviderRegistryInterface, - AppRegistryInterface, - AuthRegistryInterface, FrontMcpAuth, FlowInputOf, FlowOutputOf, FlowType, FrontMcpLogger, - ToolRegistryInterface, - HookRegistryInterface, - ResourceRegistryInterface, - PromptRegistryInterface, } from '../interfaces'; import { FlowName, ScopeMetadata } from '../metadata'; import { normalizeEntryPrefix, normalizeScopeBase } from '../utils'; import type { NotificationService } from '../../notification'; import type { SkillRegistryInterface } from '../../skill/skill.registry'; +import type { ToolUIRegistry } from '../../tool/ui/ui-shared'; +import type { TransportService } from '../../transport/transport.registry'; +import type { ElicitationStore } from '../../elicitation/store/elicitation.store'; +import type { GuardManager } from '@frontmcp/guard'; +import type HookRegistry from '../../hooks/hook.registry'; +import type { AuthRegistry } from '../../auth/auth.registry'; +import type AppRegistry from '../../app/app.registry'; +import type ToolRegistry from '../../tool/tool.registry'; +import type ResourceRegistry from '../../resource/resource.registry'; +import type PromptRegistry from '../../prompt/prompt.registry'; +import type AgentRegistry from '../../agent/agent.registry'; -export abstract class ScopeEntry extends BaseEntry { +export abstract class ScopeEntry extends BaseEntry { abstract readonly id: string; abstract readonly entryPath: string; abstract readonly routeBase: string; @@ -35,24 +39,34 @@ export abstract class ScopeEntry extends BaseEntry; abstract runFlow( @@ -60,4 +74,10 @@ export abstract class ScopeEntry extends BaseEntry, additionalDeps?: Map, ): Promise | undefined>; + + abstract runFlowForOutput( + name: Name, + input: FlowInputOf, + additionalDeps?: Map, + ): Promise>; } diff --git a/libs/sdk/src/common/interfaces/app.interface.ts b/libs/sdk/src/common/interfaces/app.interface.ts index a0256d42f..07ead69fa 100644 --- a/libs/sdk/src/common/interfaces/app.interface.ts +++ b/libs/sdk/src/common/interfaces/app.interface.ts @@ -1,12 +1,6 @@ import { Type, ValueType } from '@frontmcp/di'; import { AppMetadata, RemoteAppMetadata } from '../metadata'; -/** Marker interface for FrontMCP application classes */ - -export interface AppInterface {} - export type AppValueType = ValueType & AppMetadata; -// Using 'any' default to allow broad compatibility with untyped app classes -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export type AppType = Type | AppValueType | RemoteAppMetadata; +export type AppType = Type | AppValueType | RemoteAppMetadata; diff --git a/libs/sdk/src/common/interfaces/internal/registry.interface.ts b/libs/sdk/src/common/interfaces/internal/registry.interface.ts index 45d7bb529..ddfd51aab 100644 --- a/libs/sdk/src/common/interfaces/internal/registry.interface.ts +++ b/libs/sdk/src/common/interfaces/internal/registry.interface.ts @@ -1,25 +1,15 @@ import { Token } from '@frontmcp/di'; -import { - ScopeEntry, - FlowEntry, - AuthProviderEntry, - AppEntry, - ProviderEntry, - PluginEntry, - AdapterEntry, - PromptEntry, - ResourceEntry, - ToolEntry, - LoggerEntry, - AgentEntry, - EntryOwnerRef, - HookEntry, -} from '../../entries'; -import { FrontMcpAuth } from './primary-auth-provider.interface'; +import { ScopeEntry, FlowEntry, ProviderEntry, PluginEntry, AdapterEntry, LoggerEntry } from '../../entries'; import { FlowName } from '../../metadata'; -import { FlowCtxOf, FlowInputOf, FlowStagesOf } from '../flow.interface'; -import { HookRecord } from '../../records'; -import { ToolChangeEvent } from '../../../tool/tool.events'; + +// Import concrete registry classes using `import type` to avoid circular deps +import type HookRegistryCls from '../../../hooks/hook.registry'; +import type { AuthRegistry as AuthRegistryCls } from '../../../auth/auth.registry'; +import type AppRegistryCls from '../../../app/app.registry'; +import type ToolRegistryCls from '../../../tool/tool.registry'; +import type ResourceRegistryCls from '../../../resource/resource.registry'; +import type PromptRegistryCls from '../../../prompt/prompt.registry'; +import type AgentRegistryCls from '../../../agent/agent.registry'; export interface ScopeRegistryInterface { getScopes(): ScopeEntry[]; @@ -29,49 +19,6 @@ export interface FlowRegistryInterface { getFlows(): FlowEntry[]; } -export interface HookRegistryInterface { - /** - * used to pull hooks registered by a class and related to that class only, - * like registering hooks on specific tool execution - * @param token - */ - getClsHooks(token: Token): HookEntry[]; - - /** - * Used to pull all hooks registered to specific flow by name, - * this is used to construct the flow graph and execute hooks in order - * @param flow - */ - getFlowHooks( - flow: Name, - ): HookEntry, Name, FlowStagesOf, FlowCtxOf>[]; - - /** - * Used to pull all hooks registered to specific flow and stage by name, - * this is used to construct the flow graph and execute hooks in order - * @param flow - * @param stage - */ - getFlowStageHooks( - flow: Name, - stage: FlowStagesOf | string, - ): HookEntry, Name, FlowStagesOf, FlowCtxOf>[]; - - /** - * Used to pull hooks for a specific flow, optionally filtered by owner ID. - * Returns all hooks if no ownerId is provided, or only hooks belonging to - * the specified owner or global hooks (no owner) if ownerId is provided. - * @param flow - * @param ownerId - */ - getFlowHooksForOwner( - flow: Name, - ownerId?: string, - ): HookEntry, Name, FlowStagesOf, FlowCtxOf>[]; - - registerHooks(embedded: boolean, ...records: HookRecord[]): Promise; -} - export interface ProviderViews { /** App-wide singletons, created at boot. Immutable from invoke's POV. */ global: ReadonlyMap; @@ -93,16 +40,6 @@ export interface ProviderRegistryInterface { buildViews(session: any): Promise; } -export interface AuthRegistryInterface { - getPrimary(): FrontMcpAuth; - - getAuthProviders(): AuthProviderEntry[]; -} - -export interface AppRegistryInterface { - getApps(): AppEntry[]; -} - export interface PluginRegistryInterface { getPlugins(): PluginEntry[]; getPluginNames(): string[]; @@ -112,80 +49,10 @@ export interface AdapterRegistryInterface { getAdapters(): AdapterEntry[]; } -export interface ToolRegistryInterface { - owner: EntryOwnerRef; - - // inline tools plus discovered by nested tool registries - getTools(includeHidden?: boolean): ToolEntry[]; - - // tools appropriate for MCP listing based on client elicitation support - getToolsForListing(supportsElicitation?: boolean): ToolEntry[]; - - // inline tools only - getInlineTools(): ToolEntry[]; - - // subscribe to tool change events - subscribe( - opts: { immediate?: boolean; filter?: (i: ToolEntry) => boolean }, - cb: (evt: ToolChangeEvent) => void, - ): () => void; -} - -export interface ResourceRegistryInterface { - // owner of this registry - owner: EntryOwnerRef; - - // inline resources plus discovered by nested resource registries - getResources(includeHidden?: boolean): ResourceEntry[]; - - // resource templates - getResourceTemplates(): ResourceEntry[]; - - // inline resources only - getInlineResources(): ResourceEntry[]; - - // find a resource by URI (exact match first, then template matching) - findResourceForUri(uri: string): { instance: ResourceEntry; params: Record } | undefined; -} - -export interface PromptRegistryInterface { - // owner reference for the registry - owner: EntryOwnerRef; - - // inline prompts plus discovered by nested prompt registries - getPrompts(includeHidden?: boolean): PromptEntry[]; - - // inline prompts only - getInlinePrompts(): PromptEntry[]; - - // find a prompt by name - findByName(name: string): PromptEntry | undefined; -} - export interface LoggerRegistryInterface { getLoggers(): LoggerEntry[]; } -export interface AgentRegistryInterface { - // owner reference for the registry - owner: EntryOwnerRef; - - // all agents (inline plus discovered from nested registries) - getAgents(includeHidden?: boolean): AgentEntry[]; - - // inline agents only - getInlineAgents(): AgentEntry[]; - - // find agent by ID - findById(id: string): AgentEntry | undefined; - - // find agent by name - findByName(name: string): AgentEntry | undefined; - - // get agents visible to a specific agent - getVisibleAgentsFor(agentId: string): AgentEntry[]; -} - export type GlobalRegistryKind = 'LoggerRegistry' | 'ScopeRegistry'; export type ScopedRegistryKind = 'AppRegistry' | 'AuthRegistry' | 'FlowRegistry' | 'HookRegistry'; @@ -213,16 +80,16 @@ export type RegistryType = { LoggerRegistry: LoggerRegistryInterface; ScopeRegistry: ScopeRegistryInterface; FlowRegistry: FlowRegistryInterface; - HookRegistry: HookRegistryInterface; - AppRegistry: AppRegistryInterface; - AuthRegistry: AuthRegistryInterface; + HookRegistry: HookRegistryCls; + AppRegistry: AppRegistryCls; + AuthRegistry: AuthRegistryCls; ProviderRegistry: ProviderRegistryInterface; PluginRegistry: PluginRegistryInterface; AdapterRegistry: AdapterRegistryInterface; - ToolRegistry: ToolRegistryInterface; - ResourceRegistry: ResourceRegistryInterface; - PromptRegistry: PromptRegistryInterface; - AgentRegistry: AgentRegistryInterface; + ToolRegistry: ToolRegistryCls; + ResourceRegistry: ResourceRegistryCls; + PromptRegistry: PromptRegistryCls; + AgentRegistry: AgentRegistryCls; SkillRegistry: SkillRegistryInterface; JobRegistry: JobRegistryInterface; WorkflowRegistry: WorkflowRegistryInterface; diff --git a/libs/sdk/src/common/interfaces/plugin.interface.ts b/libs/sdk/src/common/interfaces/plugin.interface.ts index 2dac31ae1..95e86a890 100644 --- a/libs/sdk/src/common/interfaces/plugin.interface.ts +++ b/libs/sdk/src/common/interfaces/plugin.interface.ts @@ -1,13 +1,11 @@ import { Type, Token, ValueType, ClassType, FactoryType } from '@frontmcp/di'; import { PluginMetadata } from '../metadata'; -export interface PluginInterface {} - export type PluginClassType = ClassType & PluginMetadata; export type PluginValueType = ValueType & PluginMetadata; export type PluginFactoryType = FactoryType & PluginMetadata; -export type PluginType = +export type PluginType = | Type | PluginClassType | PluginValueType diff --git a/libs/sdk/src/common/interfaces/provider.interface.ts b/libs/sdk/src/common/interfaces/provider.interface.ts index fb3d35e4c..042d9145a 100644 --- a/libs/sdk/src/common/interfaces/provider.interface.ts +++ b/libs/sdk/src/common/interfaces/provider.interface.ts @@ -1,8 +1,6 @@ import { Type, Token, ValueType, ClassType, FactoryType, ClassToken } from '@frontmcp/di'; import { ProviderMetadata } from '../metadata'; -export interface ProviderInterface {} - export type ProviderClassType = ClassType & ProviderMetadata; export type ProviderValueType = ValueType & ProviderMetadata; export type ProviderFactoryType = FactoryType< @@ -12,7 +10,7 @@ export type ProviderFactoryType = Type | ProviderClassType | ProviderValueType | ProviderFactoryType; diff --git a/libs/sdk/src/common/interfaces/scope.interface.ts b/libs/sdk/src/common/interfaces/scope.interface.ts index 4297dd78d..2af23bf9a 100644 --- a/libs/sdk/src/common/interfaces/scope.interface.ts +++ b/libs/sdk/src/common/interfaces/scope.interface.ts @@ -1,7 +1,5 @@ import { Type } from '@frontmcp/di'; -export interface ScopeInterface {} +export type ScopeType = Type; -export type ScopeType = Type; - -export { ScopeInterface as FrontMcpScopeInterface, ScopeType as FrontMcpScopeType }; +export { ScopeType as FrontMcpScopeType }; diff --git a/libs/sdk/src/elicitation/flows/elicitation-request.flow.ts b/libs/sdk/src/elicitation/flows/elicitation-request.flow.ts index 36cf90359..cfc58c0f6 100644 --- a/libs/sdk/src/elicitation/flows/elicitation-request.flow.ts +++ b/libs/sdk/src/elicitation/flows/elicitation-request.flow.ts @@ -10,11 +10,10 @@ import { Flow, FlowBase, FlowHooksOf, FlowPlan, FlowRunOptions } from '../../common'; import { z } from 'zod'; import { randomUUID } from '@frontmcp/utils'; -import { InvalidInputError } from '../../errors'; +import { InvalidInputError, ElicitationStoreNotInitializedError } from '../../errors'; import type { ElicitMode } from '../elicitation.types'; import { DEFAULT_ELICIT_TTL } from '../elicitation.types'; import type { PendingElicitRecord } from '../store'; -import type { Scope } from '../../scope'; const inputSchema = z.object({ /** Related request ID from the transport */ @@ -163,7 +162,10 @@ export default class ElicitationRequestFlow extends FlowBase { this.logger.verbose('storePendingRecord:start'); const { elicitId, sessionId, message, mode, expiresAt, requestedSchema } = this.state.required; - const scope = this.scope as Scope; + const store = this.scope.elicitationStore; + if (!store) { + throw new ElicitationStoreNotInitializedError(); + } const pendingRecord: PendingElicitRecord = { elicitId, @@ -175,7 +177,7 @@ export default class ElicitationRequestFlow extends FlowBase { requestedSchema, }; - await scope.elicitationStore.setPending(pendingRecord); + await store.setPending(pendingRecord); this.state.set('pendingRecord', pendingRecord); this.logger.verbose('storePendingRecord:done', { elicitId, sessionId }); diff --git a/libs/sdk/src/elicitation/flows/elicitation-result.flow.ts b/libs/sdk/src/elicitation/flows/elicitation-result.flow.ts index b58661050..7bfb057dc 100644 --- a/libs/sdk/src/elicitation/flows/elicitation-result.flow.ts +++ b/libs/sdk/src/elicitation/flows/elicitation-result.flow.ts @@ -11,8 +11,7 @@ import { Flow, FlowBase, FlowHooksOf, FlowPlan, FlowRunOptions } from '../../com import { z } from 'zod'; import type { ElicitResult, ElicitStatus } from '../elicitation.types'; import type { PendingElicitRecord } from '../store'; -import type { Scope } from '../../scope'; -import { InvalidInputError } from '../../errors'; +import { InvalidInputError, ElicitationStoreNotInitializedError } from '../../errors'; import { validateElicitationContent } from '../helpers'; const inputSchema = z.object({ @@ -110,9 +109,12 @@ export default class ElicitationResultFlow extends FlowBase { this.logger.verbose('lookupPending:start'); const { sessionId } = this.state.required; - const scope = this.scope as Scope; + const store = this.scope.elicitationStore; + if (!store) { + throw new ElicitationStoreNotInitializedError(); + } - const pendingRecord = await scope.elicitationStore.getPending(sessionId); + const pendingRecord = await store.getPending(sessionId); this.state.set('pendingRecord', pendingRecord ?? undefined); if (!pendingRecord) { @@ -191,16 +193,16 @@ export default class ElicitationResultFlow extends FlowBase { const { pendingRecord, elicitResult } = this.state; // sessionId is set in parseInput and is required by stateSchema const { sessionId } = this.state.required; - const scope = this.scope as Scope; + const store = this.scope.elicitationStore; - if (!pendingRecord || !elicitResult) { + if (!pendingRecord || !elicitResult || !store) { this.state.set('handled', false); this.logger.verbose('publishResult:skip (no pending or no result)'); return; } try { - await scope.elicitationStore.publishResult(pendingRecord.elicitId, sessionId, elicitResult); + await store.publishResult(pendingRecord.elicitId, sessionId, elicitResult); this.state.set('handled', true); this.logger.verbose('publishResult:done', { elicitId: pendingRecord.elicitId, diff --git a/libs/sdk/src/elicitation/helpers/fallback.helper.ts b/libs/sdk/src/elicitation/helpers/fallback.helper.ts index 1d885756e..d60d41f6b 100644 --- a/libs/sdk/src/elicitation/helpers/fallback.helper.ts +++ b/libs/sdk/src/elicitation/helpers/fallback.helper.ts @@ -11,7 +11,11 @@ import type { FrontMcpLogger } from '../../common'; import type { Scope } from '../../scope'; import type { FallbackExecutionResult } from '../elicitation.types'; import { DEFAULT_FALLBACK_WAIT_TTL } from '../elicitation.types'; -import { ElicitationFallbackRequired, ElicitationSubscriptionError } from '../../errors'; +import { + ElicitationFallbackRequired, + ElicitationSubscriptionError, + ElicitationStoreNotInitializedError, +} from '../../errors'; import type { CallToolResult } from '@frontmcp/protocol'; /** @@ -147,7 +151,14 @@ export async function handleWaitingFallback( }, ttl); // Subscribe to fallback results - scope.elicitationStore + const store = scope.elicitationStore; + if (!store) { + resolved = true; + clearTimeout(timeoutHandle); + reject(new ElicitationStoreNotInitializedError()); + return; + } + store .subscribeFallbackResult( error.elicitId, (result: FallbackExecutionResult) => { diff --git a/libs/sdk/src/elicitation/send-elicitation-result.tool.ts b/libs/sdk/src/elicitation/send-elicitation-result.tool.ts index 038399f70..e09453565 100644 --- a/libs/sdk/src/elicitation/send-elicitation-result.tool.ts +++ b/libs/sdk/src/elicitation/send-elicitation-result.tool.ts @@ -16,7 +16,6 @@ import { z } from 'zod'; import type { CallToolResult } from '@frontmcp/protocol'; import { Tool, ToolContext } from '../common'; import type { ElicitResult, ElicitStatus } from './elicitation.types'; -import type { Scope } from '../scope'; const inputSchema = { elicitId: z.string().describe('The elicitation ID from the pending request'), @@ -53,11 +52,10 @@ export class SendElicitationResultTool extends ToolContext { this.logger.info('sendElicitationResult: processing', { elicitId, action }); - // Cast scope to Scope type to access elicitationStore - const scope = this.scope as unknown as Scope; + const store = this.scope.elicitationStore; - // Guard: ensure scope and elicitationStore exist - if (!scope?.elicitationStore) { + // Guard: ensure elicitationStore is available + if (!store) { this.logger.error('sendElicitationResult: scope or elicitationStore not available'); return { content: [ @@ -71,7 +69,7 @@ export class SendElicitationResultTool extends ToolContext { } // Get pending fallback context - const pending = await scope.elicitationStore.getPendingFallback(elicitId); + const pending = await store.getPendingFallback(elicitId); if (!pending) { this.logger.warn('sendElicitationResult: no pending elicitation found', { elicitId }); return { @@ -87,7 +85,7 @@ export class SendElicitationResultTool extends ToolContext { // Check expiration if (Date.now() > pending.expiresAt) { - await scope.elicitationStore.deletePendingFallback(elicitId); + await store.deletePendingFallback(elicitId); this.logger.warn('sendElicitationResult: elicitation expired', { elicitId }); return { content: [ @@ -107,10 +105,10 @@ export class SendElicitationResultTool extends ToolContext { }; // Store resolved result for re-invocation (pass sessionId for encryption support) - await scope.elicitationStore.setResolvedResult(elicitId, elicitResult, pending.sessionId); + await store.setResolvedResult(elicitId, elicitResult, pending.sessionId); // Clean up pending fallback - await scope.elicitationStore.deletePendingFallback(elicitId); + await store.deletePendingFallback(elicitId); this.logger.info('sendElicitationResult: re-invoking original tool', { elicitId, @@ -127,7 +125,7 @@ export class SendElicitationResultTool extends ToolContext { // Re-invoke the original tool using the flow // The pre-resolved result is in the async context, so the tool's elicit() // will return it immediately instead of throwing ElicitationFallbackRequired - const toolResult = await scope.runFlowForOutput('tools:call-tool', { + const toolResult = await this.scope.runFlowForOutput('tools:call-tool', { request: { method: 'tools/call', params: { @@ -146,26 +144,26 @@ export class SendElicitationResultTool extends ToolContext { // Publish the result to notify any waiting requests (distributed mode) // This allows the original request on a different node to receive the result - await scope.elicitationStore.publishFallbackResult(elicitId, pending.sessionId, { + await store.publishFallbackResult(elicitId, pending.sessionId, { success: true, result: toolResult, }); // Clean up resolved result - await scope.elicitationStore.deleteResolvedResult(elicitId); + await store.deleteResolvedResult(elicitId); return toolResult; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); // Publish the error to notify any waiting requests (distributed mode) - await scope.elicitationStore.publishFallbackResult(elicitId, pending.sessionId, { + await store.publishFallbackResult(elicitId, pending.sessionId, { success: false, error: errorMessage, }); // Clean up resolved result on error - await scope.elicitationStore.deleteResolvedResult(elicitId); + await store.deleteResolvedResult(elicitId); this.logger.error('sendElicitationResult: original tool failed', { elicitId, diff --git a/libs/sdk/src/errors/index.ts b/libs/sdk/src/errors/index.ts index 01bc8a6df..b4e9346c7 100644 --- a/libs/sdk/src/errors/index.ts +++ b/libs/sdk/src/errors/index.ts @@ -165,6 +165,7 @@ export { TransportNotConnectedError, TransportAlreadyStartedError, UnsupportedContentTypeError, + TransportServiceNotAvailableError, } from './transport.errors'; // Export auth internal errors diff --git a/libs/sdk/src/errors/transport.errors.ts b/libs/sdk/src/errors/transport.errors.ts index 2174c41a5..117b66eac 100644 --- a/libs/sdk/src/errors/transport.errors.ts +++ b/libs/sdk/src/errors/transport.errors.ts @@ -72,3 +72,12 @@ export class UnsupportedContentTypeError extends PublicMcpError { this.contentType = contentType; } } + +/** + * Thrown when the transport service is required but not available on the scope. + */ +export class TransportServiceNotAvailableError extends InternalMcpError { + constructor() { + super('Transport service not available', 'TRANSPORT_SERVICE_NOT_AVAILABLE'); + } +} diff --git a/libs/sdk/src/flows/flow.instance.ts b/libs/sdk/src/flows/flow.instance.ts index ffbf079dd..d03b59499 100644 --- a/libs/sdk/src/flows/flow.instance.ts +++ b/libs/sdk/src/flows/flow.instance.ts @@ -15,6 +15,7 @@ import { HookEntry, HookMetadata, Reference, + ScopeEntry, ServerRequest, Token, Type, @@ -22,7 +23,6 @@ import { import ProviderRegistry from '../provider/provider.registry'; import { collectFlowHookMap, StageMap, cloneStageMap, mergeHookMetasIntoStageMap } from './flow.stages'; import { writeHttpResponse } from '../server/server.validation'; -import { Scope } from '../scope'; import HookRegistry from '../hooks/hook.registry'; import { FrontMcpContextStorage, FRONTMCP_CONTEXT } from '../context'; import { RequestContextNotAvailableError, InternalMcpError } from '../errors'; @@ -58,14 +58,14 @@ export class FlowInstance extends FlowEntry { private hooks: HookRegistry; private readonly logger: FrontMcpLogger; - constructor(scope: Scope, record: FlowRecord, deps: Set, globalProviders: ProviderRegistry) { + constructor(scope: ScopeEntry, record: FlowRecord, deps: Set, globalProviders: ProviderRegistry) { super(scope, record); this.deps = [...deps]; this.globalProviders = globalProviders; this.FlowClass = this.record.provide; this.ready = this.initialize(); this.plan = this.record.metadata.plan; - this.hooks = scope.providers.getHooksRegistry(); + this.hooks = scope.hooks; this.logger = scope.logger.child('FlowInstance'); } diff --git a/libs/sdk/src/hooks/hook.registry.ts b/libs/sdk/src/hooks/hook.registry.ts index c756ffe1b..cd798092b 100644 --- a/libs/sdk/src/hooks/hook.registry.ts +++ b/libs/sdk/src/hooks/hook.registry.ts @@ -7,7 +7,6 @@ import { FlowStagesOf, HookEntry, HookRecord, - HookRegistryInterface, HookType, ScopeEntry, Token, @@ -17,10 +16,7 @@ import ProviderRegistry from '../provider/provider.registry'; import { HookInstance } from './hook.instance'; import { UnsupportedHookOwnerKindError } from '../errors'; -export default class HookRegistry - extends RegistryAbstract - implements HookRegistryInterface -{ +export default class HookRegistry extends RegistryAbstract { scope: ScopeEntry; /** Historical records by class (kept if you still want access to raw records) */ diff --git a/libs/sdk/src/job/job.instance.ts b/libs/sdk/src/job/job.instance.ts index 563a16a33..8f7b0d0ba 100644 --- a/libs/sdk/src/job/job.instance.ts +++ b/libs/sdk/src/job/job.instance.ts @@ -6,7 +6,7 @@ import { ToolInputOf, ToolOutputOf } from '../common/decorators'; import ProviderRegistry from '../provider/provider.registry'; import { z } from 'zod'; import HookRegistry from '../hooks/hook.registry'; -import { Scope } from '../scope'; +import { ScopeEntry } from '../common'; import { normalizeHooksFromCls } from '../hooks/hooks.utils'; import { InvalidHookFlowError } from '../errors/mcp.error'; import { InvalidRegistryKindError, DynamicJobDirectExecutionError } from '../errors'; @@ -21,7 +21,7 @@ export class JobInstance< Out = ToolOutputOf<{ outputSchema: OutSchema }>, > extends JobEntry { private readonly _providers: ProviderRegistry; - readonly scope: Scope; + readonly scope: ScopeEntry; readonly hooks: HookRegistry; constructor(record: JobRecord, providers: ProviderRegistry, owner: EntryOwnerRef) { @@ -31,7 +31,7 @@ export class JobInstance< this.name = record.metadata.id || record.metadata.name; this.fullName = this.owner.id + ':' + this.name; this.scope = this._providers.getActiveScope(); - this.hooks = this.scope.providers.getHooksRegistry(); + this.hooks = this.scope.hooks; // inputSchema is always a ZodRawShape this.inputSchema = (record.metadata.inputSchema ?? {}) as InSchema; diff --git a/libs/sdk/src/plugin/__tests__/plugin.registry.spec.ts b/libs/sdk/src/plugin/__tests__/plugin.registry.spec.ts index 71cc708ff..e55b2919f 100644 --- a/libs/sdk/src/plugin/__tests__/plugin.registry.spec.ts +++ b/libs/sdk/src/plugin/__tests__/plugin.registry.spec.ts @@ -4,10 +4,10 @@ import 'reflect-metadata'; import PluginRegistry, { PluginScopeInfo } from '../plugin.registry'; -import { PluginInterface, FlowCtxOf } from '../../common/interfaces'; +import { FlowCtxOf } from '../../common/interfaces'; import { FrontMcpPlugin } from '../../common/decorators/plugin.decorator'; import { FlowHooksOf } from '../../common/decorators/hook.decorator'; -import { createClassProvider, createValueProvider } from '../../__test-utils__/fixtures/provider.fixtures'; +import { createClassProvider } from '../../__test-utils__/fixtures/provider.fixtures'; import { createProviderRegistryWithScope, createMockScope } from '../../__test-utils__/fixtures/scope.fixtures'; import { InvalidPluginScopeError } from '../../errors'; import { Scope } from '../../scope'; @@ -22,7 +22,7 @@ describe('PluginRegistry', () => { name: 'TestPlugin', description: 'A test plugin', }) - class TestPlugin implements PluginInterface { + class TestPlugin { constructor() {} } @@ -41,13 +41,13 @@ describe('PluginRegistry', () => { name: 'PluginA', description: 'First plugin', }) - class PluginA implements PluginInterface {} + class PluginA {} @FrontMcpPlugin({ name: 'PluginB', description: 'Second plugin', }) - class PluginB implements PluginInterface {} + class PluginB {} const providers = await createProviderRegistryWithScope(); @@ -63,7 +63,7 @@ describe('PluginRegistry', () => { it('should register a plugin using useClass', async () => { const PLUGIN_TOKEN = Symbol('PLUGIN_TOKEN'); - class TestPluginImpl implements PluginInterface { + class TestPluginImpl { constructor() {} } @@ -145,7 +145,7 @@ describe('PluginRegistry', () => { description: 'Plugin that provides services', providers: [], }) - class PluginWithProviders implements PluginInterface { + class PluginWithProviders { get: any; getService() { @@ -174,7 +174,7 @@ describe('PluginRegistry', () => { providers: [], exports: [], }) - class PluginWithExports implements PluginInterface {} + class PluginWithExports {} const providers = await createProviderRegistryWithScope(); @@ -193,7 +193,7 @@ describe('PluginRegistry', () => { name: 'DependentPlugin', description: 'Plugin with dependencies', }) - class DependentPlugin implements PluginInterface { + class DependentPlugin { constructor() {} } @@ -213,7 +213,7 @@ describe('PluginRegistry', () => { name: 'SimplePlugin', description: 'Plugin without dependencies', }) - class SimplePlugin implements PluginInterface { + class SimplePlugin { constructor() {} } @@ -264,14 +264,14 @@ describe('PluginRegistry', () => { name: 'NestedPlugin', description: 'A nested plugin', }) - class NestedPlugin implements PluginInterface {} + class NestedPlugin {} @FrontMcpPlugin({ name: 'ParentPlugin', description: 'Parent plugin with nested plugins', plugins: [NestedPlugin], }) - class ParentPlugin implements PluginInterface {} + class ParentPlugin {} const providers = await createProviderRegistryWithScope(); @@ -291,7 +291,7 @@ describe('PluginRegistry', () => { description: 'Plugin that provides tools', tools: [], }) - class PluginWithTools implements PluginInterface {} + class PluginWithTools {} const providers = await createProviderRegistryWithScope(); @@ -309,7 +309,7 @@ describe('PluginRegistry', () => { description: 'Plugin that provides resources', resources: [], }) - class PluginWithResources implements PluginInterface {} + class PluginWithResources {} const providers = await createProviderRegistryWithScope(); @@ -327,7 +327,7 @@ describe('PluginRegistry', () => { description: 'Plugin that provides prompts', prompts: [], }) - class PluginWithPrompts implements PluginInterface {} + class PluginWithPrompts {} const providers = await createProviderRegistryWithScope(); @@ -345,7 +345,7 @@ describe('PluginRegistry', () => { description: 'Plugin that provides adapters', adapters: [], }) - class PluginWithAdapters implements PluginInterface {} + class PluginWithAdapters {} const providers = await createProviderRegistryWithScope(); @@ -451,7 +451,7 @@ describe('PluginRegistry', () => { description: 'Plugin that uses get method', providers: [], }) - class PluginWithGet implements PluginInterface { + class PluginWithGet { get: any; } @@ -471,7 +471,7 @@ describe('PluginRegistry', () => { it('should handle plugins with dynamic providers', async () => { const DYNAMIC_TOKEN = Symbol('DYNAMIC'); - class DynamicPlugin implements PluginInterface { + class DynamicPlugin { get: any; } @@ -500,7 +500,7 @@ describe('PluginRegistry', () => { name: 'DefaultScopePlugin', description: 'Plugin without explicit scope', }) - class DefaultScopePlugin implements PluginInterface {} + class DefaultScopePlugin {} const providers = await createProviderRegistryWithScope(); const ownScope = providers.get(Scope); @@ -525,7 +525,7 @@ describe('PluginRegistry', () => { description: 'Plugin with app scope', scope: 'app', }) - class AppScopePlugin implements PluginInterface {} + class AppScopePlugin {} const providers = await createProviderRegistryWithScope(); const ownScope = providers.get(Scope); @@ -550,7 +550,7 @@ describe('PluginRegistry', () => { description: 'Plugin with server scope', scope: 'server', }) - class ServerScopePlugin implements PluginInterface {} + class ServerScopePlugin {} const providers = await createProviderRegistryWithScope(); const ownScope = providers.get(Scope); @@ -576,7 +576,7 @@ describe('PluginRegistry', () => { description: 'Plugin with server scope', scope: 'server', }) - class ServerScopePlugin implements PluginInterface {} + class ServerScopePlugin {} const providers = await createProviderRegistryWithScope(); const ownScope = providers.get(Scope); @@ -600,7 +600,7 @@ describe('PluginRegistry', () => { description: 'Plugin with app scope', scope: 'app', }) - class AppScopePlugin implements PluginInterface {} + class AppScopePlugin {} const providers = await createProviderRegistryWithScope(); const ownScope = providers.get(Scope); @@ -625,14 +625,14 @@ describe('PluginRegistry', () => { description: 'App scope plugin', scope: 'app', }) - class AppPlugin implements PluginInterface {} + class AppPlugin {} @FrontMcpPlugin({ name: 'ServerPlugin', description: 'Server scope plugin', scope: 'server', }) - class ServerPlugin implements PluginInterface {} + class ServerPlugin {} const providers = await createProviderRegistryWithScope(); const ownScope = providers.get(Scope); @@ -658,7 +658,7 @@ describe('PluginRegistry', () => { name: 'LegacyPlugin', description: 'Plugin without scopeInfo', }) - class LegacyPlugin implements PluginInterface {} + class LegacyPlugin {} const providers = await createProviderRegistryWithScope(); @@ -674,7 +674,7 @@ describe('PluginRegistry', () => { it('should validate server scope plugin with object-based registration', async () => { const PLUGIN_TOKEN = Symbol('SERVER_PLUGIN'); - class ServerPlugin implements PluginInterface { + class ServerPlugin { get: any; } @@ -711,7 +711,7 @@ describe('PluginRegistry', () => { name: 'DecoratedServerPlugin', scope: 'app', // Decorator says app }) - class DecoratedServerPlugin implements PluginInterface {} + class DecoratedServerPlugin {} const providers = await createProviderRegistryWithScope(); const ownScope = providers.get(Scope); @@ -747,7 +747,7 @@ describe('PluginRegistry', () => { name: 'DecoratedAppPlugin', scope: 'app', }) - class DecoratedAppPlugin implements PluginInterface {} + class DecoratedAppPlugin {} const providers = await createProviderRegistryWithScope(); const ownScope = providers.get(Scope); @@ -785,13 +785,13 @@ describe('PluginRegistry', () => { name: 'NestedServerPlugin', scope: 'server', }) - class NestedServerPlugin implements PluginInterface {} + class NestedServerPlugin {} @FrontMcpPlugin({ name: 'ParentPlugin', plugins: [NestedServerPlugin], }) - class ParentPlugin implements PluginInterface {} + class ParentPlugin {} const providers = await createProviderRegistryWithScope(); const ownScope = providers.get(Scope); @@ -814,13 +814,13 @@ describe('PluginRegistry', () => { name: 'NestedAppPlugin', scope: 'app', }) - class NestedAppPlugin implements PluginInterface {} + class NestedAppPlugin {} @FrontMcpPlugin({ name: 'ParentPlugin', plugins: [NestedAppPlugin], }) - class ParentPlugin implements PluginInterface {} + class ParentPlugin {} const providers = await createProviderRegistryWithScope(); const ownScope = providers.get(Scope); @@ -843,13 +843,13 @@ describe('PluginRegistry', () => { name: 'NestedServerPlugin', scope: 'server', }) - class NestedServerPlugin implements PluginInterface {} + class NestedServerPlugin {} @FrontMcpPlugin({ name: 'ParentPlugin', plugins: [NestedServerPlugin], }) - class ParentPlugin implements PluginInterface {} + class ParentPlugin {} const providers = await createProviderRegistryWithScope(); const ownScope = providers.get(Scope); @@ -873,19 +873,19 @@ describe('PluginRegistry', () => { name: 'DeeplyNestedServerPlugin', scope: 'server', }) - class DeeplyNestedServerPlugin implements PluginInterface {} + class DeeplyNestedServerPlugin {} @FrontMcpPlugin({ name: 'MiddlePlugin', plugins: [DeeplyNestedServerPlugin], }) - class MiddlePlugin implements PluginInterface {} + class MiddlePlugin {} @FrontMcpPlugin({ name: 'TopPlugin', plugins: [MiddlePlugin], }) - class TopPlugin implements PluginInterface {} + class TopPlugin {} const providers = await createProviderRegistryWithScope(); const ownScope = providers.get(Scope); @@ -910,7 +910,7 @@ describe('PluginRegistry', () => { name: 'AppScopePluginWithHooks', scope: 'app', }) - class AppScopePluginWithHooks implements PluginInterface { + class AppScopePluginWithHooks { @ToolHook.Will('execute') async beforeExecute(_ctx: FlowCtxOf<'tools:call-tool'>) { // App-scoped hook @@ -940,7 +940,7 @@ describe('PluginRegistry', () => { name: 'ServerScopePluginWithHooks', scope: 'server', }) - class ServerScopePluginWithHooks implements PluginInterface { + class ServerScopePluginWithHooks { @ToolHook.Will('execute') async beforeExecute(_ctx: FlowCtxOf<'tools:call-tool'>) { // Server-scoped hook @@ -970,7 +970,7 @@ describe('PluginRegistry', () => { name: 'ServerScopePluginNoParent', scope: 'server', }) - class ServerScopePluginNoParent implements PluginInterface { + class ServerScopePluginNoParent { @ToolHook.Will('execute') async beforeExecute(_ctx: FlowCtxOf<'tools:call-tool'>) { // Server-scoped hook with no parent @@ -1006,7 +1006,7 @@ describe('PluginRegistry', () => { name: 'DecoratorAppPlugin', scope: 'app', }) - class DecoratorAppPlugin implements PluginInterface { + class DecoratorAppPlugin { get: any; } @@ -1045,7 +1045,7 @@ describe('PluginRegistry', () => { name: 'DecoratorAppPlugin', scope: 'app', }) - class DecoratorAppPlugin implements PluginInterface { + class DecoratorAppPlugin { get: any; } @@ -1082,7 +1082,7 @@ describe('PluginRegistry', () => { const PLUGIN_TOKEN = Symbol('PLUGIN'); // No @FrontMcpPlugin decorator - class PlainPlugin implements PluginInterface { + class PlainPlugin { get: any; } diff --git a/libs/sdk/src/plugin/__tests__/plugin.utils.spec.ts b/libs/sdk/src/plugin/__tests__/plugin.utils.spec.ts index b96ffdc3e..9c8ec4a44 100644 --- a/libs/sdk/src/plugin/__tests__/plugin.utils.spec.ts +++ b/libs/sdk/src/plugin/__tests__/plugin.utils.spec.ts @@ -5,7 +5,7 @@ import 'reflect-metadata'; import { normalizePlugin, collectPluginMetadata, pluginDiscoveryDeps } from '../plugin.utils'; import { FrontMcpPlugin } from '../../common/decorators/plugin.decorator'; -import { PluginInterface } from '../../common/interfaces'; + import { PluginKind } from '../../common/records'; import { createValueProvider } from '../../__test-utils__/fixtures/provider.fixtures'; @@ -16,7 +16,7 @@ describe('Plugin Utils', () => { name: 'TestPlugin', description: 'A test plugin', }) - class TestPlugin implements PluginInterface {} + class TestPlugin {} const metadata = collectPluginMetadata(TestPlugin); @@ -39,7 +39,7 @@ describe('Plugin Utils', () => { description: 'Plugin with providers', providers: [], }) - class PluginWithProviders implements PluginInterface {} + class PluginWithProviders {} const metadata = collectPluginMetadata(PluginWithProviders); @@ -55,7 +55,7 @@ describe('Plugin Utils', () => { providers: [], exports: [], }) - class PluginWithExports implements PluginInterface {} + class PluginWithExports {} const metadata = collectPluginMetadata(PluginWithExports); @@ -72,7 +72,7 @@ describe('Plugin Utils', () => { resources: [], prompts: [], }) - class FullPlugin implements PluginInterface {} + class FullPlugin {} const metadata = collectPluginMetadata(FullPlugin); @@ -90,7 +90,7 @@ describe('Plugin Utils', () => { name: 'TestPlugin', description: 'A test plugin', }) - class TestPlugin implements PluginInterface {} + class TestPlugin {} const record = normalizePlugin(TestPlugin); @@ -114,7 +114,7 @@ describe('Plugin Utils', () => { it('should normalize a plugin object with useClass to CLASS kind', () => { const PLUGIN_TOKEN = Symbol('PLUGIN'); - class TestPluginImpl implements PluginInterface {} + class TestPluginImpl {} const plugin = { provide: PLUGIN_TOKEN, @@ -378,7 +378,7 @@ describe('Plugin Utils', () => { describe('CLASS Plugin Dependencies', () => { it('should return empty array for plugins without dependencies', () => { - class TestPlugin implements PluginInterface { + class TestPlugin { constructor() {} } @@ -395,7 +395,7 @@ describe('Plugin Utils', () => { }); it('should filter dependencies based on type', () => { - class TestPlugin implements PluginInterface { + class TestPlugin { constructor( public dep1: unknown, public primitive: string, @@ -425,7 +425,7 @@ describe('Plugin Utils', () => { name: 'TestPlugin', description: 'Test plugin', }) - class TestPlugin implements PluginInterface { + class TestPlugin { constructor() {} } @@ -441,7 +441,7 @@ describe('Plugin Utils', () => { name: 'TestPlugin', description: 'Test plugin', }) - class TestPlugin implements PluginInterface { + class TestPlugin { constructor( public dep1: unknown, public nullable: unknown, @@ -464,7 +464,7 @@ describe('Plugin Utils', () => { name: 'TestPlugin', description: 'Test plugin', }) - class TestPlugin implements PluginInterface { + class TestPlugin { constructor( public dep1: unknown, public stringDep: string, @@ -497,7 +497,7 @@ describe('Plugin Utils', () => { resources: [], prompts: [], }) - class ComplexPlugin implements PluginInterface {} + class ComplexPlugin {} const record = normalizePlugin(ComplexPlugin); @@ -518,7 +518,7 @@ describe('Plugin Utils', () => { name: 'DependentPlugin', description: 'Plugin with dependencies', }) - class DependentPlugin implements PluginInterface { + class DependentPlugin { constructor() {} } diff --git a/libs/sdk/src/plugin/plugin.registry.ts b/libs/sdk/src/plugin/plugin.registry.ts index 376155547..4fab7fdce 100644 --- a/libs/sdk/src/plugin/plugin.registry.ts +++ b/libs/sdk/src/plugin/plugin.registry.ts @@ -9,6 +9,7 @@ import { PluginRegistryInterface, PluginType, ProviderEntry, + ScopeEntry, } from '../common'; import { normalizePlugin, pluginDiscoveryDeps } from './plugin.utils'; import ProviderRegistry from '../provider/provider.registry'; @@ -19,7 +20,6 @@ import PromptRegistry from '../prompt/prompt.registry'; import SkillRegistry from '../skill/skill.registry'; import { normalizeProvider } from '../provider/provider.utils'; import { RegistryAbstract, RegistryBuildMapResult } from '../regsitry'; -import { Scope } from '../scope'; import { normalizeHooksFromCls } from '../hooks/hooks.utils'; import { InvalidPluginScopeError, RegistryDependencyNotRegisteredError, InvalidRegistryKindError } from '../errors'; import { installContextExtensions } from '../context'; @@ -32,9 +32,9 @@ import { FrontMcpLogger } from '../common'; */ export interface PluginScopeInfo { /** The scope where the plugin is defined (app's own scope) */ - ownScope: Scope; + ownScope: ScopeEntry; /** Parent scope for non-standalone apps (gateway scope) */ - parentScope?: Scope; + parentScope?: ScopeEntry; /** Whether the app is standalone (standalone: true) */ isStandaloneApp: boolean; } @@ -58,7 +58,7 @@ export default class PluginRegistry /** skills by token */ private readonly pSkills: Map = new Map(); - private readonly scope: Scope; + private readonly scope: ScopeEntry; private readonly scopeInfo?: PluginScopeInfo; private readonly owner?: EntryOwnerRef; private readonly logger?: FrontMcpLogger; @@ -225,7 +225,7 @@ export default class PluginRegistry // Determine which scope to use for hook registration: // - scope='app' (default): register hooks to own scope (app-level) // - scope='server': register hooks to parent scope (gateway-level) if available - let targetHookScope: Scope; + let targetHookScope: ScopeEntry; if (pluginScope === 'server' && this.scopeInfo?.parentScope) { targetHookScope = this.scopeInfo.parentScope; } else { diff --git a/libs/sdk/src/prompt/prompt.instance.ts b/libs/sdk/src/prompt/prompt.instance.ts index 82c8c85f2..1b8c31a97 100644 --- a/libs/sdk/src/prompt/prompt.instance.ts +++ b/libs/sdk/src/prompt/prompt.instance.ts @@ -15,7 +15,7 @@ import { } from '../common'; import ProviderRegistry from '../provider/provider.registry'; import HookRegistry from '../hooks/hook.registry'; -import { Scope } from '../scope'; +import { ScopeEntry } from '../common'; import { normalizeHooksFromCls } from '../hooks/hooks.utils'; import { buildParsedPromptResult } from './prompt.utils'; import { GetPromptResult } from '@frontmcp/protocol'; @@ -23,7 +23,7 @@ import { MissingPromptArgumentError, InvalidRegistryKindError } from '../errors' export class PromptInstance extends PromptEntry { private readonly _providers: ProviderRegistry; - readonly scope: Scope; + readonly scope: ScopeEntry; readonly hooks: HookRegistry; constructor(record: PromptRecord, providers: ProviderRegistry, owner: EntryOwnerRef) { @@ -33,7 +33,7 @@ export class PromptInstance extends PromptEntry { this.name = record.metadata.name; this.fullName = this.owner.id + ':' + this.name; this.scope = this._providers.getActiveScope(); - this.hooks = this.scope.providers.getHooksRegistry(); + this.hooks = this.scope.hooks; this.ready = this.initialize(); } diff --git a/libs/sdk/src/prompt/prompt.registry.ts b/libs/sdk/src/prompt/prompt.registry.ts index 945d367e1..46ee97472 100644 --- a/libs/sdk/src/prompt/prompt.registry.ts +++ b/libs/sdk/src/prompt/prompt.registry.ts @@ -1,15 +1,7 @@ // file: libs/sdk/src/prompt/prompt.registry.ts import { Token, tokenName, getMetadata } from '@frontmcp/di'; -import { - EntryLineage, - EntryOwnerRef, - PromptEntry, - PromptRecord, - PromptRegistryInterface, - PromptType, - AppEntry, -} from '../common'; +import { AppEntry, EntryLineage, EntryOwnerRef, PromptEntry, PromptRecord, PromptType, ScopeEntry } from '../common'; import { PromptChangeEvent, PromptEmitter } from './prompt.events'; import ProviderRegistry from '../provider/provider.registry'; import { ensureMaxLen, sepFor } from '@frontmcp/utils'; @@ -22,7 +14,6 @@ import { DEFAULT_PROMPT_EXPORT_OPTS, PromptExportOptions, IndexedPrompt } from ' import GetPromptFlow from './flows/get-prompt.flow'; import PromptsListFlow from './flows/prompts-list.flow'; import { ServerCapabilities } from '@frontmcp/protocol'; -import { Scope } from '../scope'; import { NameDisambiguationError, EntryValidationError, @@ -33,14 +24,11 @@ import { /** Maximum attempts for name disambiguation to prevent infinite loops */ const MAX_DISAMBIGUATE_ATTEMPTS = 10000; -export default class PromptRegistry - extends RegistryAbstract< - PromptInstance, // instances map holds PromptInstance - PromptRecord, - PromptType[] - > - implements PromptRegistryInterface -{ +export default class PromptRegistry extends RegistryAbstract< + PromptInstance, // instances map holds PromptInstance + PromptRecord, + PromptType[] +> { /** Who owns this registry (used for provenance). */ owner: EntryOwnerRef; @@ -190,7 +178,7 @@ export default class PromptRegistry * Remote apps expose prompts via proxy entries that forward execution to the remote server. * This also subscribes to updates from the remote app's registry for lazy-loaded prompts. */ - private adoptPromptsFromRemoteApp(app: AppEntry, scope: Scope): void { + private adoptPromptsFromRemoteApp(app: AppEntry, scope: ScopeEntry): void { const remoteRegistry = app.prompts as PromptRegistry; // Helper to adopt/re-adopt prompts from the remote app diff --git a/libs/sdk/src/provider/provider.registry.ts b/libs/sdk/src/provider/provider.registry.ts index 674191d29..13fc363ed 100644 --- a/libs/sdk/src/provider/provider.registry.ts +++ b/libs/sdk/src/provider/provider.registry.ts @@ -13,7 +13,6 @@ import { hasAsyncWith, } from '@frontmcp/di'; import { - ProviderInterface, ProviderType, ProviderRegistryInterface, ScopeEntry, @@ -38,7 +37,6 @@ import { import { RegistryAbstract, RegistryBuildMapResult } from '../regsitry'; import { ProviderViews } from './provider.types'; import { Scope } from '../scope'; -import HookRegistry from '../hooks/hook.registry'; import { validateSessionId } from '../context/frontmcp-context'; import { type DistributedEnabled, shouldCacheProviders } from '../common/types/options/transport'; @@ -575,7 +573,7 @@ export default class ProviderRegistry } getHooksRegistry() { - return this.getRegistries('HookRegistry')[0] as HookRegistry; + return this.getRegistries('HookRegistry')[0]; } // noinspection JSUnusedGlobalSymbols @@ -648,7 +646,7 @@ export default class ProviderRegistry mergeFromRegistry( providedBy: ProviderRegistry, exported: { - token: Token; + token: Token; def: ProviderRecord; /** Instance may be undefined for CONTEXT-scoped providers (built per-request) */ instance: ProviderEntry | undefined; @@ -669,7 +667,7 @@ export default class ProviderRegistry /** * Used by plugins to get the exported provider definitions. */ - getProviderInfo(token: Token) { + getProviderInfo(token: Token) { const def = this.defs.get(token); const instance = this.instances.get(token); if (!def || !instance) @@ -733,7 +731,7 @@ export default class ProviderRegistry return parent.getWithParents(token); } - getActiveScope(): Scope { + getActiveScope(): ScopeEntry { return this.getWithParents(Scope); } diff --git a/libs/sdk/src/resource/flows/read-resource.flow.ts b/libs/sdk/src/resource/flows/read-resource.flow.ts index e9782386c..9d645b9d5 100644 --- a/libs/sdk/src/resource/flows/read-resource.flow.ts +++ b/libs/sdk/src/resource/flows/read-resource.flow.ts @@ -12,7 +12,6 @@ import { ResourceReadError, } from '../../errors'; import { isUIResourceUri, handleUIResourceRead } from '../../tool/ui'; -import { Scope } from '../../scope'; import { FlowContextProviders } from '../../provider/flow-context-providers'; const inputSchema = z.object({ @@ -169,19 +168,21 @@ export default class ReadResourceFlow extends FlowBase { if (isUIResourceUri(uri)) { this.logger.info(`findResource: detected UI resource URI "${uri}"`); - // Get the ToolUIRegistry from the scope - const scope = this.scope as Scope; - // Get platform type: first check sessionIdPayload (detected from user-agent), // then fall back to notification service (detected from MCP clientInfo) const { sessionId, authInfo } = this.state; const platformType = authInfo?.sessionIdPayload?.platformType ?? - (sessionId ? scope.notifications.getPlatformType(sessionId) : undefined); + (sessionId ? this.scope.notifications.getPlatformType(sessionId) : undefined); this.logger.verbose(`findResource: platform type for session: ${platformType ?? 'unknown'}`); - const uiResult = handleUIResourceRead(uri, scope.toolUI, platformType); + if (!this.scope.toolUI) { + this.logger.verbose('findResource: toolUI not available, skipping UI resource handling'); + throw new ResourceNotFoundError(uri); + } + + const uiResult = handleUIResourceRead(uri, this.scope.toolUI, platformType); if (uiResult.handled) { if (uiResult.error) { diff --git a/libs/sdk/src/resource/resource.instance.ts b/libs/sdk/src/resource/resource.instance.ts index 4571cc270..fc10cf00c 100644 --- a/libs/sdk/src/resource/resource.instance.ts +++ b/libs/sdk/src/resource/resource.instance.ts @@ -18,7 +18,7 @@ import { } from '../common'; import ProviderRegistry from '../provider/provider.registry'; import HookRegistry from '../hooks/hook.registry'; -import { Scope } from '../scope'; +import { ScopeEntry } from '../common'; import { normalizeHooksFromCls } from '../hooks/hooks.utils'; import { matchUriTemplate, parseUriTemplate } from '@frontmcp/utils'; import { buildResourceContent as buildParsedResourceResult } from '../utils/content.utils'; @@ -30,7 +30,7 @@ export class ResourceInstance< Out = unknown, > extends ResourceEntry { private readonly providers: ProviderRegistry; - readonly scope: Scope; + readonly scope: ScopeEntry; readonly hooks: HookRegistry; /** Parsed URI template info for template resources */ @@ -43,7 +43,7 @@ export class ResourceInstance< this.name = record.metadata.name; this.fullName = this.owner.id + ':' + this.name; this.scope = this.providers.getActiveScope(); - this.hooks = this.scope.providers.getHooksRegistry(); + this.hooks = this.scope.hooks; // Determine if this is a template resource this.isTemplate = 'uriTemplate' in record.metadata; diff --git a/libs/sdk/src/resource/resource.registry.ts b/libs/sdk/src/resource/resource.registry.ts index 9cc719e37..2c7963c27 100644 --- a/libs/sdk/src/resource/resource.registry.ts +++ b/libs/sdk/src/resource/resource.registry.ts @@ -2,13 +2,14 @@ import { Token, tokenName, getMetadata } from '@frontmcp/di'; import { + AppEntry, EntryLineage, EntryOwnerRef, ResourceEntry, ResourceRecord, ResourceTemplateRecord, - ResourceRegistryInterface, ResourceType, + ScopeEntry, } from '../common'; import { ResourceChangeEvent, ResourceEmitter } from './resource.events'; import ProviderRegistry from '../provider/provider.registry'; @@ -23,8 +24,6 @@ import { } from './resource.utils'; import { RegistryAbstract, RegistryBuildMapResult } from '../regsitry'; import { ResourceInstance } from './resource.instance'; -import { Scope } from '../scope'; -import { AppEntry } from '../common'; import { DEFAULT_RESOURCE_EXPORT_OPTS, ResourceExportOptions, IndexedResource } from './resource.types'; import ReadResourceFlow from './flows/read-resource.flow'; import ResourcesListFlow from './flows/resources-list.flow'; @@ -34,14 +33,11 @@ import UnsubscribeResourceFlow from './flows/unsubscribe-resource.flow'; import type { ServerCapabilities } from '@frontmcp/protocol'; import { NameDisambiguationError, EntryValidationError } from '../errors'; -export default class ResourceRegistry - extends RegistryAbstract< - ResourceInstance, // instances map holds ResourceInstance - ResourceRecord | ResourceTemplateRecord, - ResourceType[] - > - implements ResourceRegistryInterface -{ +export default class ResourceRegistry extends RegistryAbstract< + ResourceInstance, // instances map holds ResourceInstance + ResourceRecord | ResourceTemplateRecord, + ResourceType[] +> { /** Who owns this registry (used for provenance). */ owner: EntryOwnerRef; @@ -197,7 +193,7 @@ export default class ResourceRegistry * Remote apps expose resources via proxy entries that forward execution to the remote server. * This also subscribes to updates from the remote app's registry for lazy-loaded resources. */ - private adoptResourcesFromRemoteApp(app: AppEntry, scope: Scope): void { + private adoptResourcesFromRemoteApp(app: AppEntry, scope: ScopeEntry): void { const remoteRegistry = app.resources as ResourceRegistry; // Helper to adopt/re-adopt resources from the remote app diff --git a/libs/sdk/src/scope/flows/http.request.flow.ts b/libs/sdk/src/scope/flows/http.request.flow.ts index 8d8792c2a..19d943aa8 100644 --- a/libs/sdk/src/scope/flows/http.request.flow.ts +++ b/libs/sdk/src/scope/flows/http.request.flow.ts @@ -23,7 +23,6 @@ import { z } from 'zod'; import { sessionVerifyOutputSchema } from '../../auth/flows/session.verify.flow'; import { randomUUID } from '@frontmcp/utils'; import { SessionVerificationFailedError } from '../../errors'; -import type { Scope } from '../scope.instance'; const plan = { pre: [ @@ -158,7 +157,7 @@ export default class HttpRequestFlow extends FlowBase { @Stage('acquireQuota') async acquireQuota() { - const manager = (this.scope as Scope).rateLimitManager; + const manager = this.scope.rateLimitManager; if (!manager?.config?.global) return; const context = this.tryGetContext(); @@ -526,12 +525,14 @@ export default class HttpRequestFlow extends FlowBase { // session persists, allowing recreation on other nodes in distributed mode. const authorization = request[ServerRequestTokens.auth] as Authorization | undefined; if (authorization?.token) { - const transportService = (this.scope as Scope).transportService; - for (const protocol of ['streamable-http', 'sse'] as const) { - try { - await transportService.destroyTransporter(protocol, authorization.token, sessionId); - } catch { - // Transport may already be evicted or not found — non-critical + const transportService = this.scope.transportService; + if (transportService) { + for (const protocol of ['streamable-http', 'sse'] as const) { + try { + await transportService.destroyTransporter(protocol, authorization.token, sessionId); + } catch { + // Transport may already be evicted or not found — non-critical + } } } } diff --git a/libs/sdk/src/scope/scope.instance.ts b/libs/sdk/src/scope/scope.instance.ts index fb151852a..0fafadfcc 100644 --- a/libs/sdk/src/scope/scope.instance.ts +++ b/libs/sdk/src/scope/scope.instance.ts @@ -8,7 +8,6 @@ import { FrontMcpAuth, FrontMcpLogger, FrontMcpServer, - HookRegistryInterface, ProviderScope, ScopeEntry, ScopeRecord, @@ -43,7 +42,6 @@ import CallAgentFlow from '../agent/flows/call-agent.flow'; import PluginRegistry, { PluginScopeInfo } from '../plugin/plugin.registry'; import { ElicitationStore, createElicitationStore } from '../elicitation'; import { ElicitationRequestFlow, ElicitationResultFlow } from '../elicitation/flows'; -import { ElicitationStoreNotInitializedError } from '../errors/elicitation.error'; import { SendElicitationResultTool } from '../elicitation/send-elicitation-result.tool'; import { normalizeTool } from '../tool/tool.utils'; import { ToolInstance } from '../tool/tool.instance'; @@ -597,7 +595,7 @@ export class Scope extends ScopeEntry { return this.scopeAuth.getPrimary(); } - get hooks(): HookRegistryInterface { + get hooks(): HookRegistry { return this.scopeHooks; } @@ -678,10 +676,7 @@ export class Scope extends ScopeEntry { * * @see createElicitationStore for factory implementation details */ - get elicitationStore(): ElicitationStore { - if (!this._elicitationStore) { - throw new ElicitationStoreNotInitializedError(); - } + get elicitationStore(): ElicitationStore | undefined { return this._elicitationStore; } diff --git a/libs/sdk/src/skill/__tests__/memory-skill.provider.spec.ts b/libs/sdk/src/skill/__tests__/memory-skill.provider.spec.ts index 4ba1afb43..6a22b9e13 100644 --- a/libs/sdk/src/skill/__tests__/memory-skill.provider.spec.ts +++ b/libs/sdk/src/skill/__tests__/memory-skill.provider.spec.ts @@ -7,7 +7,7 @@ import { MemorySkillProvider } from '../providers/memory-skill.provider'; import { SkillToolValidator } from '../skill-validator'; import { SkillContent } from '../../common/interfaces'; -import { ToolRegistryInterface } from '../../common/interfaces/internal'; +import type ToolRegistry from '../../tool/tool.registry'; import { ToolEntry } from '../../common'; // Helper to create test skills @@ -23,7 +23,7 @@ const createTestSkill = ( }); // Mock tool registry -const createMockToolRegistry = (tools: string[]): ToolRegistryInterface => +const createMockToolRegistry = (tools: string[]): ToolRegistry => ({ getTools: () => tools.map( @@ -41,7 +41,7 @@ const createMockToolRegistry = (tools: string[]): ToolRegistryInterface => getCapabilities: jest.fn(), getInlineTools: jest.fn(), owner: { kind: 'scope', id: 'test', ref: {} }, - }) as unknown as ToolRegistryInterface; + }) as unknown as ToolRegistry; describe('MemorySkillProvider', () => { let provider: MemorySkillProvider; diff --git a/libs/sdk/src/skill/__tests__/skill-http.utils.spec.ts b/libs/sdk/src/skill/__tests__/skill-http.utils.spec.ts index 463480f06..ea44d9537 100644 --- a/libs/sdk/src/skill/__tests__/skill-http.utils.spec.ts +++ b/libs/sdk/src/skill/__tests__/skill-http.utils.spec.ts @@ -62,7 +62,7 @@ function createMockSkillContent(overrides: Partial = {}): SkillCon }; } -// Mock ToolRegistryInterface for testing +// Mock ToolRegistry for testing function createMockToolRegistry( tools: Array<{ name: string; diff --git a/libs/sdk/src/skill/__tests__/skill-validator.spec.ts b/libs/sdk/src/skill/__tests__/skill-validator.spec.ts index cc715a782..7056e6790 100644 --- a/libs/sdk/src/skill/__tests__/skill-validator.spec.ts +++ b/libs/sdk/src/skill/__tests__/skill-validator.spec.ts @@ -5,7 +5,7 @@ */ import { SkillToolValidator, ToolValidationResult } from '../skill-validator'; -import { ToolRegistryInterface } from '../../common/interfaces/internal'; +import type ToolRegistry from '../../tool/tool.registry'; import { ToolEntry } from '../../common'; // Mock tool entries for testing @@ -23,7 +23,7 @@ const createMockTool = (name: string, hidden = false): ToolEntry => }) as unknown as ToolEntry; // Create a mock tool registry -const createMockToolRegistry = (visibleTools: string[], hiddenTools: string[] = []): ToolRegistryInterface => { +const createMockToolRegistry = (visibleTools: string[], hiddenTools: string[] = []): ToolRegistry => { const allToolList: ToolEntry[] = [ ...visibleTools.map((name) => createMockTool(name, false)), ...hiddenTools.map((name) => createMockTool(name, true)), @@ -41,7 +41,7 @@ const createMockToolRegistry = (visibleTools: string[], hiddenTools: string[] = getCapabilities: jest.fn(), getInlineTools: jest.fn(), owner: { kind: 'scope', id: 'test', ref: {} }, - } as unknown as ToolRegistryInterface; + } as unknown as ToolRegistry; }; describe('skill-validator', () => { diff --git a/libs/sdk/src/skill/flows/http/skills-api.flow.ts b/libs/sdk/src/skill/flows/http/skills-api.flow.ts index a4235be62..a3c1d6a19 100644 --- a/libs/sdk/src/skill/flows/http/skills-api.flow.ts +++ b/libs/sdk/src/skill/flows/http/skills-api.flow.ts @@ -18,8 +18,8 @@ import { FlowHooksOf, normalizeEntryPrefix, normalizeScopeBase, - ToolRegistryInterface, } from '../../../common'; +import type ToolRegistry from '../../../tool/tool.registry'; import { z } from 'zod'; import { skillToApiResponse, formatSkillForLLMWithSchemas } from '../../skill-http.utils'; import { formatSkillForLLM } from '../../skill.utils'; @@ -258,7 +258,7 @@ export default class SkillsApiFlow extends FlowBase { private async handleGetSkill( skillId: string, skillRegistry: SkillRegistryInterface, - toolRegistry: ToolRegistryInterface | null, + toolRegistry: ToolRegistry | null, ) { const loadResult = await skillRegistry.loadSkill(skillId); diff --git a/libs/sdk/src/skill/flows/load-skill.flow.ts b/libs/sdk/src/skill/flows/load-skill.flow.ts index 8fa9f63db..e105ff4d3 100644 --- a/libs/sdk/src/skill/flows/load-skill.flow.ts +++ b/libs/sdk/src/skill/flows/load-skill.flow.ts @@ -8,7 +8,6 @@ import { formatSkillForLLMWithSchemas } from '../skill-http.utils'; import type { SkillLoadResult } from '../skill-storage.interface'; import type { SkillSessionManager } from '../session/skill-session.manager'; import type { SkillPolicyMode, SkillActivationResult } from '../session/skill-session.types'; -import type { Scope } from '../../scope'; // Input schema matching MCP request format - supports multiple skill IDs const inputSchema = z.object({ @@ -273,7 +272,7 @@ export default class LoadSkillFlow extends FlowBase { return; } - const toolRegistry = (this.scope as Scope).tools; + const toolRegistry = this.scope.tools; const skillResults: z.infer[] = []; let totalTools = 0; let allToolsAvailable = true; diff --git a/libs/sdk/src/skill/skill-http.utils.ts b/libs/sdk/src/skill/skill-http.utils.ts index 79ebf1f96..674b07dab 100644 --- a/libs/sdk/src/skill/skill-http.utils.ts +++ b/libs/sdk/src/skill/skill-http.utils.ts @@ -9,7 +9,8 @@ * - /skills API - JSON responses */ -import type { SkillContent, SkillEntry, ToolRegistryInterface, ToolEntry } from '../common'; +import type { SkillContent, SkillEntry, ToolEntry } from '../common'; +import type ToolRegistry from '../tool/tool.registry'; import type { SkillVisibility, SkillResources } from '../common/metadata/skill.metadata'; import type { SkillRegistryInterface as SkillRegistryInterfaceType } from './skill.registry'; @@ -91,7 +92,7 @@ export function formatSkillsForLlmCompact(skills: SkillEntry[]): string { */ export async function formatSkillsForLlmFull( registry: SkillRegistryInterfaceType, - toolRegistry: ToolRegistryInterface, + toolRegistry: ToolRegistry, visibility: SkillVisibility = 'both', ): Promise { const skills = registry.getSkills(false); // Don't include hidden @@ -128,7 +129,7 @@ export function formatSkillForLLMWithSchemas( skill: SkillContent, availableTools: string[], missingTools: string[], - toolRegistry: ToolRegistryInterface, + toolRegistry: ToolRegistry, ): string { const parts: string[] = []; diff --git a/libs/sdk/src/skill/skill-storage.factory.ts b/libs/sdk/src/skill/skill-storage.factory.ts index 3b2abb8a5..341684d51 100644 --- a/libs/sdk/src/skill/skill-storage.factory.ts +++ b/libs/sdk/src/skill/skill-storage.factory.ts @@ -10,7 +10,7 @@ */ import type { FrontMcpLogger } from '../common'; -import type { ToolRegistryInterface } from '../common/interfaces/internal'; +import type ToolRegistry from '../tool/tool.registry'; import type { SkillStorageProvider, SkillStorageProviderType } from './skill-storage.interface'; import { SkillToolValidator } from './skill-validator'; import { MemorySkillProvider, MemorySkillProviderOptions } from './providers/memory-skill.provider'; @@ -127,7 +127,7 @@ export interface SkillStorageFactoryOptions { * Tool registry for validating tool references. * Required for tool validation in search results. */ - toolRegistry?: ToolRegistryInterface; + toolRegistry?: ToolRegistry; /** * Logger instance. @@ -291,7 +291,7 @@ export function createSkillStorageProvider( */ export function createMemorySkillProvider( options: { - toolRegistry?: ToolRegistryInterface; + toolRegistry?: ToolRegistry; defaultTopK?: number; defaultMinScore?: number; logger?: FrontMcpLogger; diff --git a/libs/sdk/src/skill/skill-validator.ts b/libs/sdk/src/skill/skill-validator.ts index 0beb5c99c..7749316ee 100644 --- a/libs/sdk/src/skill/skill-validator.ts +++ b/libs/sdk/src/skill/skill-validator.ts @@ -1,6 +1,6 @@ // file: libs/sdk/src/skill/skill-validator.ts -import { ToolRegistryInterface } from '../common/interfaces/internal'; +import type ToolRegistry from '../tool/tool.registry'; /** * Result of validating tool availability for a skill. @@ -34,9 +34,9 @@ export interface ToolValidationResult { * in the current scope. It categorizes tools as available, missing, or hidden. */ export class SkillToolValidator { - private readonly toolRegistry: ToolRegistryInterface; + private readonly toolRegistry: ToolRegistry; - constructor(toolRegistry: ToolRegistryInterface) { + constructor(toolRegistry: ToolRegistry) { this.toolRegistry = toolRegistry; } diff --git a/libs/sdk/src/skill/skill.instance.ts b/libs/sdk/src/skill/skill.instance.ts index d1c3535db..c1a89eee7 100644 --- a/libs/sdk/src/skill/skill.instance.ts +++ b/libs/sdk/src/skill/skill.instance.ts @@ -4,7 +4,7 @@ import { EntryOwnerRef, SkillEntry, SkillKind, SkillRecord, SkillToolRef, normal import { SkillContent } from '../common/interfaces'; import { SkillVisibility } from '../common/metadata/skill.metadata'; import ProviderRegistry from '../provider/provider.registry'; -import { Scope } from '../scope'; +import { ScopeEntry } from '../common'; import { loadInstructions, buildSkillContent } from './skill.utils'; /** @@ -33,7 +33,7 @@ export class SkillInstance extends SkillEntry { private readonly providersRef: ProviderRegistry; /** The scope this skill operates in */ - readonly scope: Scope; + readonly scope: ScopeEntry; /** Cached instructions (loaded lazily) */ private cachedInstructions?: string; diff --git a/libs/sdk/src/skill/skill.registry.ts b/libs/sdk/src/skill/skill.registry.ts index beb37b66d..d6d4cec0a 100644 --- a/libs/sdk/src/skill/skill.registry.ts +++ b/libs/sdk/src/skill/skill.registry.ts @@ -1,7 +1,7 @@ // file: libs/sdk/src/skill/skill.registry.ts import { Token, tokenName } from '@frontmcp/di'; -import { EntryLineage, EntryOwnerRef, SkillEntry, SkillType, SkillToolValidationMode } from '../common'; +import { EntryLineage, EntryOwnerRef, ScopeEntry, SkillEntry, SkillType, SkillToolValidationMode } from '../common'; import { SkillContent } from '../common/interfaces'; import { SkillChangeEvent, SkillEmitter } from './skill.events'; import { SkillInstance, createSkillInstance } from './skill.instance'; @@ -9,7 +9,6 @@ import { normalizeSkill, skillDiscoveryDeps } from './skill.utils'; import { SkillRecord } from '../common/records'; import ProviderRegistry from '../provider/provider.registry'; import { RegistryAbstract, RegistryBuildMapResult } from '../regsitry'; -import { Scope } from '../scope'; import { SkillStorageProvider, SkillSearchOptions, @@ -221,7 +220,7 @@ export default class SkillRegistry private toolValidator?: SkillToolValidator; /** The scope this registry operates in */ - readonly scope: Scope; + readonly scope: ScopeEntry; /** Registry-level options for validation behavior */ private readonly options: SkillRegistryOptions; diff --git a/libs/sdk/src/skill/tools/load-skills.tool.ts b/libs/sdk/src/skill/tools/load-skills.tool.ts index 69d28f1a0..2534ebd68 100644 --- a/libs/sdk/src/skill/tools/load-skills.tool.ts +++ b/libs/sdk/src/skill/tools/load-skills.tool.ts @@ -4,7 +4,7 @@ import { z } from 'zod'; import { Tool, ToolContext } from '../../common'; import { formatSkillForLLM, generateNextSteps } from '../skill.utils'; import { formatSkillForLLMWithSchemas } from '../skill-http.utils'; -import type { ToolRegistryInterface } from '../../common'; +import type ToolRegistry from '../../tool/tool.registry'; /** * Input schema for loadSkills tool. @@ -143,7 +143,7 @@ export class LoadSkillsTool extends ToolContext { async acquireQuota() { this.logger.verbose('acquireQuota:start'); - const manager = (this.scope as Scope).rateLimitManager; + const manager = this.scope.rateLimitManager; if (!manager) { this.state.toolContext?.mark('acquireQuota'); this.logger.verbose('acquireQuota:done (no rate limit manager)'); @@ -501,7 +502,7 @@ export default class CallToolFlow extends FlowBase { async acquireSemaphore() { this.logger.verbose('acquireSemaphore:start'); - const manager = (this.scope as Scope).rateLimitManager; + const manager = this.scope.rateLimitManager; if (!manager) { this.state.toolContext?.mark('acquireSemaphore'); this.logger.verbose('acquireSemaphore:done (no rate limit manager)'); @@ -573,7 +574,7 @@ export default class CallToolFlow extends FlowBase { const { tool } = this.state.required; const timeoutMs = - tool.metadata.timeout?.executeMs ?? (this.scope as Scope).rateLimitManager?.config?.defaultTimeout?.executeMs; + tool.metadata.timeout?.executeMs ?? this.scope.rateLimitManager?.config?.defaultTimeout?.executeMs; try { const doExecute = async () => { @@ -607,9 +608,12 @@ export default class CallToolFlow extends FlowBase { const authInfo = this.state.authInfo; const authInfoWithTransport = authInfo as (AuthInfo & TransportExtension) | undefined; const sessionId = authInfo?.sessionId ?? 'anonymous'; - const scope = this.scope as Scope; + const store = this.scope.elicitationStore; + if (!store) { + throw new InternalMcpError('Elicitation store not initialized'); + } - await scope.elicitationStore.setPendingFallback({ + await store.setPendingFallback({ elicitId: error.elicitId, sessionId, toolName: error.toolName, @@ -627,14 +631,14 @@ export default class CallToolFlow extends FlowBase { // 1. Transport is streamable-http (supports keeping connection open) // 2. Notifications can be delivered (session is registered in NotificationService) // Some LLMs don't support MCP notifications, so we need to fall back to fire-and-forget - if (transportType === 'streamable-http' && canDeliverNotifications(scope, sessionId)) { + if (transportType === 'streamable-http' && canDeliverNotifications(this.scope as Scope, sessionId)) { // Waiting mode: Send notification + wait for pub/sub result // This keeps the request open and returns the actual tool result // when sendElicitationResult is called this.logger.info('execute: using waiting fallback for streamable-http', { elicitId: error.elicitId, }); - const deps: FallbackHandlerDeps = { scope, sessionId, logger: this.logger }; + const deps: FallbackHandlerDeps = { scope: this.scope as Scope, sessionId, logger: this.logger }; const result = await handleWaitingFallback(deps, error); toolContext.output = result; this.logger.verbose('execute:done (elicitation waiting fallback)'); @@ -745,8 +749,7 @@ export default class CallToolFlow extends FlowBase { } try { - // Cast scope to Scope to access toolUI and notifications - const scope = this.scope as Scope; + const scope = this.scope; // Get session info for platform detection from authInfo (already in state from parseInput) const { authInfo } = this.state; @@ -786,6 +789,11 @@ export default class CallToolFlow extends FlowBase { let htmlContent: string | undefined; let uiMeta: Record = {}; + if (!scope.toolUI) { + this.logger.verbose('applyUI: toolUI not available, skipping UI rendering'); + return; + } + if (servingMode === 'static') { // For static mode: no additional rendering needed // Widget was already registered at server startup diff --git a/libs/sdk/src/tool/flows/tools-list.flow.ts b/libs/sdk/src/tool/flows/tools-list.flow.ts index 3e00136c4..9af5642d9 100644 --- a/libs/sdk/src/tool/flows/tools-list.flow.ts +++ b/libs/sdk/src/tool/flows/tools-list.flow.ts @@ -176,15 +176,12 @@ export default class ToolsListFlow extends FlowBase { const sessionId = authInfo.sessionId; - // Cast scope to access notifications service for platform detection - const scope = this.scope as Scope; - // Get platform type: first check sessionIdPayload (detected from user-agent), // then fall back to notification service (detected from MCP clientInfo), // finally default to 'unknown' const platformType: AIPlatformType = authInfo.sessionIdPayload?.platformType ?? - (sessionId ? scope.notifications?.getPlatformType(sessionId) : undefined) ?? + (sessionId ? this.scope.notifications?.getPlatformType(sessionId) : undefined) ?? 'unknown'; this.logger.verbose(`parseInput: detected platform=${platformType}`); @@ -340,9 +337,8 @@ export default class ToolsListFlow extends FlowBase { const allResolved = this.state.required.resolvedTools; const platformType = this.state.platformType ?? 'unknown'; - // Get pagination config from scope - const scope = this.scope as Scope; - const paginationConfig = scope.pagination?.tools; + // Get pagination config from scope metadata + const paginationConfig = this.scope.metadata.pagination?.tools; // Determine if pagination should apply const usePagination = this.shouldPaginate(allResolved.length, paginationConfig); diff --git a/libs/sdk/src/tool/tool.instance.ts b/libs/sdk/src/tool/tool.instance.ts index f1f30a4fa..8a1a5d185 100644 --- a/libs/sdk/src/tool/tool.instance.ts +++ b/libs/sdk/src/tool/tool.instance.ts @@ -20,7 +20,7 @@ import { import ProviderRegistry from '../provider/provider.registry'; import { z } from 'zod'; import HookRegistry from '../hooks/hook.registry'; -import { Scope } from '../scope'; +import { ScopeEntry } from '../common'; import { normalizeHooksFromCls } from '../hooks/hooks.utils'; import type { CallToolRequest } from '@frontmcp/protocol'; import { buildParsedToolResult } from './tool.utils'; @@ -45,7 +45,7 @@ export class ToolInstance< /** The provider registry this tool is bound to (captured at construction) */ private readonly _providers: ProviderRegistry; /** The scope this tool operates in (captured at construction from providers) */ - readonly scope: Scope; + readonly scope: ScopeEntry; /** The hook registry for this tool's scope (captured at construction) */ readonly hooks: HookRegistry; @@ -56,7 +56,7 @@ export class ToolInstance< this.name = record.metadata.id || record.metadata.name; this.fullName = this.owner.id + ':' + this.name; this.scope = this._providers.getActiveScope(); - this.hooks = this.scope.providers.getHooksRegistry(); + this.hooks = this.scope.hooks; // inputSchema is always a ZodRawShape this.inputSchema = (record.metadata.inputSchema ?? {}) as InSchema; diff --git a/libs/sdk/src/tool/tool.registry.ts b/libs/sdk/src/tool/tool.registry.ts index bdf236646..f0ce8b6b6 100644 --- a/libs/sdk/src/tool/tool.registry.ts +++ b/libs/sdk/src/tool/tool.registry.ts @@ -1,5 +1,5 @@ import { Token, tokenName, getMetadata } from '@frontmcp/di'; -import { EntryLineage, EntryOwnerRef, ToolEntry, ToolRecord, ToolRegistryInterface, ToolType } from '../common'; +import { AppEntry, EntryLineage, EntryOwnerRef, ScopeEntry, ToolEntry, ToolRecord, ToolType } from '../common'; import { ToolChangeEvent, ToolEmitter } from './tool.events'; import ProviderRegistry from '../provider/provider.registry'; import { ensureMaxLen, sepFor } from '@frontmcp/utils'; @@ -12,8 +12,6 @@ import { DEFAULT_EXPORT_OPTS, ExportNameOptions, IndexedTool } from './tool.type import ToolsListFlow from './flows/tools-list.flow'; import CallToolFlow from './flows/call-tool.flow'; import { ServerCapabilities } from '@frontmcp/protocol'; -import { Scope } from '../scope'; -import { AppEntry } from '../common'; import { isSendElicitationResultTool } from '../elicitation/send-elicitation-result.tool'; import { NameDisambiguationError, @@ -22,14 +20,11 @@ import { RegistryGraphEntryNotFoundError, } from '../errors'; -export default class ToolRegistry - extends RegistryAbstract< - ToolInstance, // IMPORTANT: instances map holds ToolInstance (not the interface) - ToolRecord, - ToolType[] - > - implements ToolRegistryInterface -{ +export default class ToolRegistry extends RegistryAbstract< + ToolInstance, // IMPORTANT: instances map holds ToolInstance (not the interface) + ToolRecord, + ToolType[] +> { /** Who owns this registry (used for provenance). Optional. */ owner: EntryOwnerRef; @@ -193,7 +188,7 @@ export default class ToolRegistry * Remote apps expose tools via proxy entries that forward execution to the remote server. * This also subscribes to updates from the remote app's registry for lazy-loaded tools. */ - private adoptToolsFromRemoteApp(app: AppEntry, scope: Scope): void { + private adoptToolsFromRemoteApp(app: AppEntry, scope: ScopeEntry): void { // Validate that app.tools has the expected interface before casting // Remote apps may have different registry implementations if (!app.tools || typeof app.tools.getTools !== 'function') { diff --git a/libs/sdk/src/transport/adapters/transport.local.adapter.ts b/libs/sdk/src/transport/adapters/transport.local.adapter.ts index fa6c5d0dd..759999b09 100644 --- a/libs/sdk/src/transport/adapters/transport.local.adapter.ts +++ b/libs/sdk/src/transport/adapters/transport.local.adapter.ts @@ -290,10 +290,22 @@ export abstract class LocalTransportAdapter { * Get the elicitation store for distributed elicitation support. * Uses Redis in distributed mode, in-memory for single-node. */ - protected get elicitStore(): ElicitationStore { + protected get elicitStore(): ElicitationStore | undefined { return this.scope.elicitationStore; } + /** + * Get the elicitation store, throwing if not initialized. + * Use in contexts where elicitation is required (sendElicitRequest, cancelPendingElicit). + */ + protected requireElicitStore(): ElicitationStore { + const store = this.elicitStore; + if (!store) { + throw new Error('Elicitation store not initialized'); + } + return store; + } + /** * Cancel any pending elicitation request. * Called before sending a new elicit to enforce single-elicit-per-session. @@ -315,9 +327,12 @@ export abstract class LocalTransportAdapter { // Publish cancel to store for distributed mode (non-atomic, intentional) // In distributed mode, another node may have already processed this elicitation const sessionId = this.key.sessionId; - const pending = await this.elicitStore.getPending(sessionId); - if (pending) { - await this.elicitStore.publishResult(pending.elicitId, sessionId, { status: 'cancel' }); + const store = this.elicitStore; + if (store) { + const pending = await store.getPending(sessionId); + if (pending) { + await store.publishResult(pending.elicitId, sessionId, { status: 'cancel' }); + } } this.pendingElicit = undefined; diff --git a/libs/sdk/src/transport/adapters/transport.sse.adapter.ts b/libs/sdk/src/transport/adapters/transport.sse.adapter.ts index 2dc94a1a5..811f6e2db 100644 --- a/libs/sdk/src/transport/adapters/transport.sse.adapter.ts +++ b/libs/sdk/src/transport/adapters/transport.sse.adapter.ts @@ -121,7 +121,8 @@ export class TransportSSEAdapter extends LocalTransportAdapter ? O : unknown>( elicitId, (result) => { @@ -203,7 +204,7 @@ export class TransportSSEAdapter extends LocalTransportAdapter ? O : unknown>( elicitId, (result) => { @@ -261,7 +262,7 @@ export class TransportStreamableHttpAdapter extends LocalTransportAdapter { session = createSessionId('legacy-sse', token, { userAgent: request.headers?.['user-agent'] as string | undefined, - platformDetectionConfig: (this.scope as Scope).metadata.transport?.platformDetection, + platformDetectionConfig: this.scope.metadata.transport?.platformDetection, skillsOnlyMode, }); } @@ -156,10 +156,9 @@ export default class HandleSseFlow extends FlowBase { @Stage('router') async router() { const { request } = this.rawInput; - const scope = this.scope as Scope; const requestPath = normalizeEntryPrefix(request.path); - const prefix = normalizeEntryPrefix(scope.entryPath); - const scopePath = normalizeScopeBase(scope.routeBase); + const prefix = normalizeEntryPrefix(this.scope.entryPath); + const scopePath = normalizeScopeBase(this.scope.routeBase); const basePath = `${prefix}${scopePath}`; if (requestPath === `${basePath}/sse`) { @@ -173,7 +172,10 @@ export default class HandleSseFlow extends FlowBase { filter: ({ state: { requestType } }) => requestType === 'initialize', }) async onInitialize() { - const transportService = (this.scope as Scope).transportService; + const transportService = this.scope.transportService; + if (!transportService) { + throw new TransportServiceNotAvailableError(); + } const { request, response } = this.rawInput; const { token, session } = this.state.required; @@ -199,7 +201,10 @@ export default class HandleSseFlow extends FlowBase { filter: ({ state: { requestType } }) => requestType === 'message', }) async onMessage() { - const transportService = (this.scope as Scope).transportService; + const transportService = this.scope.transportService; + if (!transportService) { + throw new TransportServiceNotAvailableError(); + } const logger = this.scopeLogger.child('handle:legacy-sse:onMessage'); const { request, response } = this.rawInput; diff --git a/libs/sdk/src/transport/flows/handle.stateless-http.flow.ts b/libs/sdk/src/transport/flows/handle.stateless-http.flow.ts index 506390e5f..907091575 100644 --- a/libs/sdk/src/transport/flows/handle.stateless-http.flow.ts +++ b/libs/sdk/src/transport/flows/handle.stateless-http.flow.ts @@ -12,7 +12,7 @@ import { } from '../../common'; import { z } from 'zod'; import { RequestSchema } from '@frontmcp/protocol'; -import { Scope } from '../../scope'; +import { TransportServiceNotAvailableError } from '../../errors'; export const plan = { pre: ['parseInput', 'router'], @@ -55,7 +55,7 @@ export default class HandleStatelessHttpFlow extends FlowBase { @Stage('parseInput') async parseInput() { const { request } = this.rawInput; - const logger = (this.scope as Scope).logger.child('HandleStatelessHttpFlow'); + const logger = this.scope.logger.child('HandleStatelessHttpFlow'); // Check if we have auth info const auth = request[ServerRequestTokens.auth] as Authorization | undefined; @@ -75,7 +75,7 @@ export default class HandleStatelessHttpFlow extends FlowBase { @Stage('router') async router() { const { request } = this.rawInput; - const logger = (this.scope as Scope).logger.child('HandleStatelessHttpFlow'); + const logger = this.scope.logger.child('HandleStatelessHttpFlow'); const body = request.body as { method?: string } | undefined; const method = body?.method; @@ -95,8 +95,11 @@ export default class HandleStatelessHttpFlow extends FlowBase { @Stage('handleRequest') async handleRequest() { - const transportService = (this.scope as Scope).transportService; - const logger = (this.scope as Scope).logger.child('HandleStatelessHttpFlow'); + const transportService = this.scope.transportService; + if (!transportService) { + throw new TransportServiceNotAvailableError(); + } + const logger = this.scope.logger.child('HandleStatelessHttpFlow'); const { request, response } = this.rawInput; const { token, isAuthenticated, requestType } = this.state; diff --git a/libs/sdk/src/transport/flows/handle.streamable-http.flow.ts b/libs/sdk/src/transport/flows/handle.streamable-http.flow.ts index 01dec2564..05fcce6c4 100644 --- a/libs/sdk/src/transport/flows/handle.streamable-http.flow.ts +++ b/libs/sdk/src/transport/flows/handle.streamable-http.flow.ts @@ -13,11 +13,10 @@ import { FlowControl, validateMcpSessionHeader, } from '../../common'; -import { InternalMcpError } from '../../errors'; +import { InternalMcpError, TransportServiceNotAvailableError } from '../../errors'; import { z } from 'zod'; import { ElicitResultSchema, RequestSchema, CallToolResultSchema } from '@frontmcp/protocol'; import type { StoredSession } from '@frontmcp/auth'; -import { Scope } from '../../scope'; import { createSessionId } from '../../auth/session/utils/session-id.utils'; import { detectSkillsOnlyMode } from '../../skill/skill-mode.utils'; import { createExtAppsMessageHandler, type ExtAppsJsonRpcRequest, type ExtAppsHostCapabilities } from '../../ext-apps'; @@ -246,7 +245,7 @@ export default class HandleStreamableHttpFlow extends FlowBase { return createSessionId('streamable-http', token, { userAgent: request.headers?.['user-agent'] as string | undefined, - platformDetectionConfig: (this.scope as Scope).metadata.transport?.platformDetection, + platformDetectionConfig: this.scope.metadata.transport?.platformDetection, skillsOnlyMode, }); }, @@ -302,8 +301,11 @@ export default class HandleStreamableHttpFlow extends FlowBase { filter: ({ state: { requestType } }) => requestType === 'initialize', }) async onInitialize() { - const transportService = (this.scope as Scope).transportService; - const logger = (this.scope as Scope).logger.child('handle:streamable-http:onInitialize'); + const transportService = this.scope.transportService; + if (!transportService) { + throw new TransportServiceNotAvailableError(); + } + const logger = this.scope.logger.child('handle:streamable-http:onInitialize'); const { request, response } = this.rawInput; const { token, session } = this.state.required; @@ -357,7 +359,10 @@ export default class HandleStreamableHttpFlow extends FlowBase { filter: ({ state: { requestType } }) => requestType === 'elicitResult', }) async onElicitResult() { - const transportService = (this.scope as Scope).transportService; + const transportService = this.scope.transportService; + if (!transportService) { + throw new TransportServiceNotAvailableError(); + } const logger = this.scopeLogger.child('handle:streamable-http:onElicitResult'); const { request, response } = this.rawInput; @@ -426,7 +431,10 @@ export default class HandleStreamableHttpFlow extends FlowBase { filter: ({ state: { requestType } }) => requestType === 'message', }) async onMessage() { - const transportService = (this.scope as Scope).transportService; + const transportService = this.scope.transportService; + if (!transportService) { + throw new TransportServiceNotAvailableError(); + } const logger = this.scopeLogger.child('handle:streamable-http:onMessage'); const { request, response } = this.rawInput; @@ -507,7 +515,10 @@ export default class HandleStreamableHttpFlow extends FlowBase { filter: ({ state: { requestType } }) => requestType === 'sseListener', }) async onSseListener() { - const transportService = (this.scope as Scope).transportService; + const transportService = this.scope.transportService; + if (!transportService) { + throw new TransportServiceNotAvailableError(); + } const logger = this.scopeLogger.child('handle:streamable-http:onSseListener'); const { request, response } = this.rawInput; @@ -557,7 +568,10 @@ export default class HandleStreamableHttpFlow extends FlowBase { filter: ({ state: { requestType } }) => requestType === 'extApps', }) async onExtApps() { - const transportService = (this.scope as Scope).transportService; + const transportService = this.scope.transportService; + if (!transportService) { + throw new TransportServiceNotAvailableError(); + } const logger = this.scopeLogger.child('handle:streamable-http:onExtApps'); const { request, response } = this.rawInput; @@ -618,10 +632,9 @@ export default class HandleStreamableHttpFlow extends FlowBase { } // 4. Create ExtAppsMessageHandler with session context - const scope = this.scope as Scope; // Get host capabilities from scope metadata, with defaults - const configuredCapabilities = scope.metadata.extApps?.hostCapabilities; + const configuredCapabilities = this.scope.metadata.extApps?.hostCapabilities; const hostCapabilities: ExtAppsHostCapabilities = { serverToolProxy: configuredCapabilities?.serverToolProxy ?? true, logging: configuredCapabilities?.logging ?? true, @@ -631,10 +644,10 @@ export default class HandleStreamableHttpFlow extends FlowBase { const handler = createExtAppsMessageHandler({ context: { sessionId: session.id, - logger: scope.logger, + logger: this.scope.logger, callTool: async (name, args) => { // Route through CallToolFlow with session's authInfo - const result = await scope.runFlow('tools:call-tool', { + const result = await this.scope.runFlow('tools:call-tool', { request: { method: 'tools/call', params: { name, arguments: args } }, ctx: { authInfo: { diff --git a/libs/sdk/src/workflow/workflow.instance.ts b/libs/sdk/src/workflow/workflow.instance.ts index 7b380776b..86739d335 100644 --- a/libs/sdk/src/workflow/workflow.instance.ts +++ b/libs/sdk/src/workflow/workflow.instance.ts @@ -3,7 +3,7 @@ import { WorkflowEntry } from '../common/entries/workflow.entry'; import { WorkflowMetadata } from '../common/metadata/workflow.metadata'; import { WorkflowRecord } from '../common/records/workflow.record'; import ProviderRegistry from '../provider/provider.registry'; -import { Scope } from '../scope'; +import { ScopeEntry } from '../common'; import HookRegistry from '../hooks/hook.registry'; /** @@ -11,7 +11,7 @@ import HookRegistry from '../hooks/hook.registry'; */ export class WorkflowInstance extends WorkflowEntry { private readonly _providers: ProviderRegistry; - readonly scope: Scope; + readonly scope: ScopeEntry; readonly hooks: HookRegistry; constructor(record: WorkflowRecord, providers: ProviderRegistry, owner: EntryOwnerRef) { @@ -21,7 +21,7 @@ export class WorkflowInstance extends WorkflowEntry { this.name = record.metadata.id || record.metadata.name; this.fullName = this.owner.id + ':' + this.name; this.scope = this._providers.getActiveScope(); - this.hooks = this.scope.providers.getHooksRegistry(); + this.hooks = this.scope.hooks; this.ready = this.initialize(); }