Skip to content

Conversation

@frontegg-david
Copy link
Contributor

@frontegg-david frontegg-david commented Jan 22, 2026

Summary by CodeRabbit

  • New Features

    • Full "Skills" system: discover, search, load, session management, tool authorization, in-memory and external storage, and plugin integration.
  • Tests

    • Extensive new unit and E2E test suites validating skills, providers, sessions, flows, guards/hooks, and tool authorization.
  • Documentation

    • Comprehensive Skills and plugin docs added and integrated into navigation.
  • Chores

    • CI: dynamic E2E discovery, matrix-driven runs, conditional coverage collection; reduced demo apps' log verbosity.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 22, 2026

📝 Walkthrough

Walkthrough

Adds a Skills subsystem (metadata, decorators, records, registry, providers, flows, sessions, guards), an e2e-skills app with many tests, CI workflow changes for dynamic E2E discovery and per-project coverage, multiple demo app logging/test tweaks, and various docs and helper additions.

Changes

Cohort / File(s) Summary
CI / GitHub Actions
\.github/workflows/push.yml
Adds discover-e2e job outputting an E2E matrix, RUN_COVERAGE flag, splits unit test steps, matrix-driven e2e-tests, per-project coverage artifacts, and updates coverage download/merge/upload flow.
E2E skills app
apps/e2e/demo-e2e-skills/*
New NX app: entry (src/main.ts), build/jest/tsconfig/webpack, skills, plugin (devops), tools, many e2e test suites and project config.
Demo apps — logging & test config
apps/e2e/*/src/main.ts, apps/e2e/*/e2e/*.e2e.test.ts, apps/e2e/*/project.json
Lowers FrontMcp logging Verbose→Warn across many demos; adds project to Playwright/Jest e2e configs; target rename/port constant tweaks.
Documentation
docs/draft/**, docs/live/**, docs/draft/docs.json
Adds comprehensive Skills docs, updates plugin/app docs to list skills metadata and contributing-skills guidance.
SDK — metadata, decorators, tokens, records & schemas
libs/sdk/src/common/metadata/*, .../decorators/skill.decorator.ts, .../tokens/*, .../records/*, .../schemas/*
Adds SkillMetadata schema, annotated skill schema, tokens, records, decorator and inline-skill helpers, and related utilities.
SDK — public interfaces & entries
libs/sdk/src/common/interfaces/skill.interface.ts, libs/sdk/src/common/entries/*.ts, libs/sdk/src/common/metadata/*
Adds SkillContent/SkillContext types, SkillEntry abstraction; extends App/Scope metadata to accept skills?: SkillType[].
SDK — skill core (registry, instance, utils, events)
libs/sdk/src/skill/*
New SkillInstance, SkillRegistry surface, normalization/hashing/formatting utilities, SkillEmitter/events, skill records and core logic.
SDK — providers & storage factory
libs/sdk/src/skill/providers/*, libs/sdk/src/skill/skill-storage.factory.ts, libs/sdk/package.json
Adds MemorySkillProvider (TF‑IDF search), ExternalSkillProviderBase with sync state, storage factory helpers; adds vectoriadb peerDependency.
SDK — flows, tools, hooks & guards
libs/sdk/src/skill/flows/*, libs/sdk/src/skill/hooks/*, libs/sdk/src/skill/guards/*
Adds SearchSkillsFlow and LoadSkillFlow, skill-tool guard hook, ToolAuthorizationGuard and integration.
SDK — sessions & session store
libs/sdk/src/skill/session/*
Adds SkillSessionManager, session types/serialization, MemorySkillSessionStore and factory, session APIs for activation/authorization/tracking.
SDK — errors & validation
libs/sdk/src/skill/errors/*, libs/sdk/src/errors/elicitation.error.ts
Adds ToolNotAllowedError, ToolApprovalRequiredError, SkillValidationError, ElicitationSubscriptionError and re-exports.
Runtime integration
libs/sdk/src/app/instances/*, libs/sdk/src/scope/scope.instance.ts, libs/sdk/src/plugin/plugin.registry.ts, libs/sdk/src/front-mcp/front-mcp.ts, libs/sdk/src/index.ts
Wires SkillRegistry into AppLocal/Remote/Scope/Plugin lifecycles (EmptySkillRegistry for remote), registers flows/tools/hooks, injects stdio sessionId into handlers, and re-exports skill API.
Elicitation helpers & tests
libs/sdk/src/elicitation/helpers/fallback.helper.ts, .../__tests__/*
Adds canDeliverNotifications, sendFallbackNotification, handleWaitingFallback and unit tests.
Tests — large additions
libs/sdk/src/skill/__tests__/*, libs/sdk/src/plugin/__tests__/*, apps/e2e/demo-e2e-skills/e2e/*
Extensive new unit/integration/e2e tests covering registry, providers, session manager, flows, guards, hooks, plugin interactions and skill behaviors.
Misc & small changes
libs/auth/jest.config.cts, various e2e tests
Minor jest comment updates, test config tweaks (maxWorkers), and other small refactors.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client/LLM
    participant Scope as Scope/Server
    participant SearchFlow as SearchSkillsFlow
    participant Registry as SkillRegistry
    participant Provider as SkillProvider
    participant Validator as SkillToolValidator

    Client->>Scope: skills/search(query)
    Scope->>SearchFlow: execute(input)
    SearchFlow->>Registry: search(options)
    Registry->>Provider: fetch/index query
    Provider-->>Registry: results (skills + scores)
    Registry-->>SearchFlow: mapped results
    SearchFlow->>Validator: validateToolAvailability(results)
    Validator-->>SearchFlow: available/missing tools
    SearchFlow-->>Scope: SearchSkillsResult
    Scope-->>Client: response
Loading
sequenceDiagram
    participant Client as Client/LLM
    participant Scope as Scope/Server
    participant LoadFlow as LoadSkillFlow
    participant Registry as SkillRegistry
    participant Provider as SkillProvider
    participant Session as SkillSessionManager
    participant Guard as ToolAuthorizationGuard

    Client->>Scope: skills/load(skillId, activate?)
    Scope->>LoadFlow: execute(input)
    LoadFlow->>Registry: load(skillId)
    Registry->>Provider: load content
    Provider-->>Registry: SkillContent
    alt activateSession
        LoadFlow->>Session: activateSkill(skillId, content, policy?)
        Session-->>LoadFlow: session metadata
    end
    LoadFlow-->>Scope: LoadSkillResult (skill + tools + session?)
    Scope-->>Client: response

    Client->>Scope: tools:call(toolName)
    Scope->>Guard: check(toolName)
    Guard->>Session: getActiveSession()
    Session-->>Guard: SkillSessionState
    Guard-->>Scope: allowed? / error
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

"🐰
I hop through code with a curious nose,
I plant new Skills where the search-light grows,
Sessions hum and tools line up on cue,
Guards nod politely, docs show what to do,
Tests munch carrots — bright, precise, and new!"

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: Add support for skills' is highly related to the changeset's main objective of introducing comprehensive skill functionality throughout the SDK.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 65.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 20

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
docs/live/docs/extensibility/plugins.mdx (1)

17-24: Revert edits in docs/live/.
Per repo rules, live docs are generated and should not be edited directly. Please revert these changes and keep the update in docs/draft so automation can regenerate live docs.

As per coding guidelines, ...

Also applies to: 242-296

docs/live/docs/servers/apps.mdx (1)

52-85: Update the draft app docs instead of live output.
Changes under docs/live/docs/** should be made in docs/draft/docs/** and regenerated. Please move this update to the draft app docs and revert the live edit. As per coding guidelines, avoid manual edits to live docs.

🤖 Fix all issues with AI agents
In @.github/workflows/push.yml:
- Around line 123-152: The discover-e2e job lacks explicit permissions; add a
minimal permissions block under the discover-e2e job (job name "discover-e2e")
such as permissions: contents: read to follow least-privilege principles —
insert this directly beneath the job declaration so the job only has repository
read access it needs; optionally mirror or add equivalent permissions at
workflow level or to other jobs for consistency.

In `@docs/live/docs/servers/skills.mdx`:
- Around line 1-5: Revert the live MDX change for the document with title
"Skills" and slug "servers/skills" in the generated live docs, and instead apply
your edits to the draft source (the draft copy of the Skills doc); after
updating the draft, run the documentation regeneration pipeline so the live docs
are rebuilt from draft sources and the live file is no longer modified directly.

In `@libs/auth/jest.config.cts`:
- Around line 12-19: Update the global coverageThreshold values in
jest.config.cts (the coverageThreshold -> global block: statements, branches,
functions, lines) to 95 instead of lowering them; if there are known gaps that
must be excluded, add explicit, justified exclusions via
coveragePathIgnorePatterns or per-file/per-directory coverageThreshold overrides
and create a follow-up ticket documenting those exclusions and remediation
steps; also add a TODO comment referencing the ticket ID so reviewers can trace
the justification.

In `@libs/sdk/src/common/metadata/skill.metadata.ts`:
- Around line 305-333: The instruction URL branch in
skillInstructionSourceSchema currently uses z.string().url(), which doesn't
enforce RFC 3986 MCP URI rules; import isValidMcpUri from '@frontmcp/utils' and
replace the z.object({ url: z.string().url() }).strict() entry with a refinement
using z.string().refine(isValidMcpUri, { message: 'Invalid MCP URI' }) (keeping
.strict()), so skillInstructionSourceSchema (and thus skillMetadataSchema)
validates MCP URIs the same way as resource.metadata.ts and app.metadata.ts.
- Around line 385-424: In normalizeToolRef replace the three plain Error throws
with new PublicMcpError(MCP_ERROR_CODES.INVALID_PARAMS, ...) so invalid tool
class and invalid reference format are surfaced as parameter validation errors;
import { PublicMcpError, MCP_ERROR_CODES } from 'libs/sdk/src/errors', and use
descriptive messages in each throw (where getToolNameFromClass returns falsy and
in the final invalid reference fallback) — update the throws inside the function
(the branches using getToolNameFromClass and the final JSON.stringify(ref)
throw) to use PublicMcpError instead of Error.

In `@libs/sdk/src/common/tokens/skill.tokens.ts`:
- Around line 1-4: The TypeScript error is caused by a missing import for
ExtendFrontMcpSkillMetadata referenced in the satisfies clause; update the top
imports in skill.tokens.ts to include ExtendFrontMcpSkillMetadata (alongside
tokenFactory, RawMetadataShape, and SkillMetadata), importing it from the module
that exports it (e.g., ../metadata), so the satisfies check compiles correctly
when using tokenFactory and the SkillMetadata/RawMetadataShape types.

In `@libs/sdk/src/direct/__tests__/direct-server.spec.ts`:
- Around line 18-33: The tests currently cast the mock scope to any when
constructing DirectMcpServerImpl; instead define createMockScope to return a
properly typed partial scope (e.g., const createMockScope = (): Pick<Scope,
'runFlowForOutput' | 'transportService'> => ({ runFlowForOutput: jest.fn(),
transportService: { destroy: jest.fn().mockResolvedValue(undefined) } });) and
remove all as any casts, then pass the result directly into new
DirectMcpServerImpl(mockScope) so runFlowForOutput and transportService are
correctly typed for the tests.

In `@libs/sdk/src/elicitation/helpers/fallback.helper.ts`:
- Around line 103-177: The subscribeFallbackResult promise can resolve before
the timeout/callback, leaving no unsubscribe reference and causing a
subscription leak; change handleWaitingFallback to capture the unsubscribe
function into a local variable (e.g., let unsubscribeFn: (() => Promise<void>) |
null = null) when the .then callback runs, and call unsubscribeFn() from every
resolution path (timeout handler, fallback callback branch where resolved is
set, and the subscription .catch branch) after clearing timeoutHandle; also
ensure unsubscribe errors are caught/ignored and that unsubscribe is awaited or
its promise is handled to guarantee cleanup.

In `@libs/sdk/src/front-mcp/front-mcp.ts`:
- Around line 251-271: The wrapper for MCP handlers currently clobbers any
existing authInfo; update the wrappedHandler created around handler.handler
(from createMcpHandlers/handlers) to merge existing ctx.authInfo with the new
sessionId instead of overwriting it (e.g., const enrichedCtx = { ...ctx,
authInfo: { ...ctx.authInfo, sessionId } }); also remove the gratuitous "any"
annotations on the wrappedHandler parameters and use the proper handler/request
context types expected by mcpServer.setRequestHandler so TypeScript
type-checking is preserved.

In `@libs/sdk/src/index.ts`:
- Around line 61-98: The SDK index now exports developer-facing Skill APIs that
lack documentation; update the docs to add an API reference section documenting
SkillRegistry, SkillInstance, createSkillInstance, SkillEmitter,
MemorySkillProvider, SkillToolValidator and the utility functions
normalizeSkill, isSkillRecord, and formatSkillForLLM, describing their purpose,
signatures, returned types (e.g., SkillRegistryInterface, IndexedSkill,
SkillLoadResult, ToolValidationResult), and example usage for
creating/instantiating skills, registering/listening with the registry, using
the memory provider, and validating tools so integrators can use the
programmatic SDK surface.

In `@libs/sdk/src/skill/__tests__/skill-validator.spec.ts`:
- Around line 26-43: The mock returned by createMockToolRegistry should include
the missing getToolsForListing method to satisfy the ToolRegistryInterface
contract; update createMockToolRegistry to add a getToolsForListing stub
(returning the same list shape as getTools or an empty array) so the object
matches ToolRegistryInterface without relying on the as unknown as cast, and
keep other jest.fn() stubs intact; reference createMockToolRegistry and
ToolRegistryInterface and ensure the added getToolsForListing signature aligns
with how SkillToolValidator would expect it.

In `@libs/sdk/src/skill/flows/__tests__/search-skills.flow.spec.ts`:
- Line 14: The test file contains an unused import of SearchSkillsFlow; remove
the unused import statement for SearchSkillsFlow (import SearchSkillsFlow from
'../search-skills.flow') to silence the lint/test warning, or alternatively add
tests that instantiate/exercise the SearchSkillsFlow class (e.g., call its
constructor or relevant methods) so the import is actually used; update the test
file accordingly to either delete the import or reference the SearchSkillsFlow
symbol.

In `@libs/sdk/src/skill/flows/load-skill.flow.ts`:
- Around line 78-86: The stateSchema currently uses z.any() for loadResult and
activationResult which violates the no-any guideline; replace those with proper
Zod schemas (e.g., define skillLoadResultSchema and skillActivationResultSchema
matching SkillLoadResult and SkillActivationResult and use them in stateSchema)
or, if you cannot fully model the types yet, change to z.unknown().optional()
for the loadResult and activationResult fields (instead of z.any()) and keep the
existing type assertions removed; update references in stateSchema and ensure
output still uses outputSchema.

In `@libs/sdk/src/skill/guards/tool-authorization.guard.ts`:
- Around line 95-119: The approval branch must not fall through to the generic
denial path: when result.requiresApproval is true and opts.onApprovalRequired is
absent, explicitly handle it by throwing ToolApprovalRequiredError(toolName,
result.skillId) if opts.throwOnDenied is true, otherwise return result as-is
(preserving requiresApproval). Also when opts.onApprovalRequired returns
approved, update the returned object to set allowed: true, reason:
'dynamically_approved' AND clear requiresApproval (set to false) before
returning, and ensure the sessionManager.approveToolForSession/tool call
remains. This avoids reaching the ToolNotAllowedError(result, allowedTools)
branch for approval-required cases.

In `@libs/sdk/src/skill/index.ts`:
- Around line 37-40: Add API-level documentation for the exported registry
surface: document SkillRegistry, SkillRegistryInterface, IndexedSkill, and
SkillRegistryOptions in the docs/draft/docs site (e.g., extend
docs/draft/docs/servers/skills.mdx or add a new docs page). Describe each
exported type/class signature at a high level, list required/optional properties
and methods on SkillRegistryInterface, show typical usage patterns (implementing
a custom registry, registering/unregistering skills, indexing behavior), and
include example snippets and use-case notes for extending SkillRegistry to
ensure SDK consumers can implement or extend registries.

In `@libs/sdk/src/skill/providers/external-skill.provider.ts`:
- Around line 336-346: In syncSkills, ensure the provider is initialized before
using persisted state and replace the generic Error with a typed SDK error: if
this.mode !== 'persistent' throw new SdkError(...) (or the project’s
SDK-specific error class) instead of new Error; then before computing prevState,
if this.syncState is undefined call await this.initialize() and then set
prevState = this.syncState ?? createEmptySyncState() so persisted sync state is
respected (keep references to syncSkills, initialize, syncState,
createEmptySyncState and mode).

In `@libs/sdk/src/skill/providers/memory-skill.provider.ts`:
- Around line 495-512: In skillToMetadata, the current tools mapping drops the
tool.required flag when t.purpose is undefined; update the mapping in the tools
property so each mapped entry always preserves t.required (e.g., include
required: t.required when present) while still using the compact { name, purpose
} shape when purpose exists — ensure the transformed items for tools in
skillToMetadata keep required semantics so downstream code relying on required
continues to work.

In `@libs/sdk/src/skill/README.md`:
- Around line 28-41: The README.md has fenced code blocks lacking language
identifiers (MD040); edit the README.md blocks that render the ASCII diagrams
(the "SkillRegistry (Main Facade)" diagram, the "Session State" block, and the
"libs/sdk/src/ skill tree" block) and add a language identifier such as "text"
(e.g., replace ``` with ```text) so each fenced block is tagged; locate these
blocks by the diagram content or headings (SkillRegistry, Session State, and the
libs/sdk/src/ tree) and update them consistently throughout the file.

In `@libs/sdk/src/skill/session/skill-session-store.interface.ts`:
- Around line 52-125: MemorySkillSessionStore is leaking mutable references
because activeSkills and the nested Sets inside each ActiveSkill are
stored/returned by reference; update save, get, update, and listActive to
deep-clone activeSkills and every nested Set. Specifically, in save and when
persisting from update ensure you copy session.activeSkills as a new array and
for each skill clone its properties and convert
allowedTools/requiredTools/approvedTools/deniedTools into new Set instances
before storing; in get and listActive return deep clones the same way (new
array, new skill objects, new Sets) so consumers cannot mutate the store; in
update also detect updates.activeSkills and replace with a deep-cloned version
before Object.assign into the stored session.

In `@libs/sdk/src/skill/skill-storage.factory.ts`:
- Around line 219-227: The switch branch handling providerType === 'external' in
createSkillStorageProvider currently throws a generic Error when
externalProvider is missing; replace that with the SDK's typed error (e.g.,
SdkError or McpError) populated with the appropriate MCP error code and a clear
message. Update the throw inside the 'external' case so it constructs and throws
the SDK-specific error class (including the same descriptive text and the MCP
code), and keep the existing logger?.error call; reference
createSkillStorageProvider, providerType 'external', externalProvider, and
ExternalSkillProviderBase when implementing the fix.
🧹 Nitpick comments (39)
libs/cli/src/__tests__/env.spec.ts (1)

292-302: Consider a more explicit singular form assertion.

The assertion on line 301 works but is subtle. A clearer approach would be to match the exact expected output format or use a regex to ensure variable appears without an s suffix.

♻️ Suggested improvement
     it('should use singular "variable" for count of 1', () => {
       (fs.existsSync as jest.Mock).mockImplementation((p: string) => {
         return p.endsWith('.env');
       });
       (fs.readFileSync as jest.Mock).mockReturnValue('SINGLE=value');

       loadDevEnv('/test/cwd');

-      expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('loaded 1 environment variable'));
-      expect(consoleSpy).not.toHaveBeenCalledWith(expect.stringContaining('variables'));
+      expect(consoleSpy).toHaveBeenCalledWith(
+        expect.stringMatching(/loaded 1 environment variable(?!s)/)
+      );
     });
libs/adapters/src/openapi/__tests__/openapi-branch-coverage.spec.ts (4)

51-61: Consider using proper type guards instead of any with non-null assertions.

The helper functions use any types and a non-null assertion on line 59. Per coding guidelines, avoid any without strong justification and use proper error handling instead of non-null assertions.

♻️ Suggested improvement
-// eslint-disable-next-line `@typescript-eslint/no-explicit-any`
-function getToolMeta(tool: any): any {
-  return tool[FrontMcpToolTokens.metadata] || {};
-}
-
-// eslint-disable-next-line `@typescript-eslint/no-explicit-any`
-function getFirstTool(result: { tools?: any[] }): any {
-  expect(result.tools).toBeDefined();
-  expect(result.tools!.length).toBeGreaterThan(0);
-  return result.tools![0];
-}
+function getToolMeta(tool: Record<string | symbol, unknown>): Record<string, unknown> {
+  return (tool[FrontMcpToolTokens.metadata] as Record<string, unknown>) || {};
+}
+
+function getFirstTool(result: { tools?: unknown[] }): unknown {
+  expect(result.tools).toBeDefined();
+  expect(result.tools?.length).toBeGreaterThan(0);
+  return result.tools?.[0];
+}

108-111: Weak assertion - consider verifying actual logger calls.

The assertion expect(debugSpy).toBeDefined() is always true since debugSpy is defined on line 96. The comment indicates the debug method should have been called, but this isn't actually verified.

♻️ Suggested fix
-    // The console logger's verbose/debug methods should have been called
-    // Verify the spy was set up correctly
-    expect(debugSpy).toBeDefined();
+    // Verify the console logger's debug method was called during fetch
+    expect(debugSpy).toHaveBeenCalled();

440-445: Missing assertion for documented expected behavior.

The comment states "Debug log should NOT be called when schemas are unchanged" but there's no assertion to verify this. This reduces the value of the test for detecting regressions.

♻️ Suggested fix
     const result = await adapter.fetch();
 
     expect(result.tools).toHaveLength(1);
-    // Debug log should NOT be called when schemas are unchanged
+    // Debug log should NOT be called when schemas are unchanged
+    expect(mockLogger.debug).not.toHaveBeenCalledWith(expect.stringContaining('Applied schema transforms'));
   });

608-654: Consider asserting that transform functions are actually invoked.

The test sets up mock functions (globalTransform, perToolTransform, filterFn) but only asserts result.tools.toHaveLength(1). Verifying the mocks were called would provide stronger coverage guarantees.

♻️ Suggested enhancement
     const result = await adapter.fetch();
 
     expect(result.tools).toHaveLength(1);
+    // Verify transforms were invoked
+    expect(filterFn).toHaveBeenCalled();
+    expect(globalTransform).toHaveBeenCalled();
+    expect(perToolTransform).toHaveBeenCalled();
   });
libs/sdk/src/skill/__tests__/skill-validator.spec.ts (1)

11-43: Consider reusing the shared registry mock to avoid drift.

There’s already a mock helper at libs/sdk/src/__test-utils__/mocks/tool-registry.mock.ts. Reusing it (and extending it for hidden tools) will keep tests aligned with canonical mock behavior and reduce duplication.

libs/sdk/jest.config.ts (1)

19-28: Coverage thresholds are well below the 95% guideline - consider tracking this debt.

The pragmatic approach of setting low thresholds for incremental improvement is reasonable given existing coverage gaps. The TODO comment is helpful for documenting intent.

Consider creating a tracking issue to monitor progress toward the 95% target. This would help ensure the thresholds are incrementally raised as tests are added.

Would you like me to help draft an issue to track the SDK coverage improvement effort?

apps/e2e/demo-e2e-skills/src/apps/skills/tools/github-add-comment.tool.ts (2)

4-15: Harden tool schemas with z.object().strict() and minimal constraints.
This aligns with SDK tool patterns and prevents unknown fields from slipping into E2E flows.

♻️ Proposed refactor
-const inputSchema = {
-  prNumber: z.number().describe('The pull request number'),
-  comment: z.string().describe('The comment text to add'),
-};
+const inputSchema = z
+  .object({
+    prNumber: z.number().int().positive().describe('The pull request number'),
+    comment: z.string().min(1).describe('The comment text to add'),
+  })
+  .strict();
 
-const outputSchema = {
-  commentId: z.number(),
-  success: z.boolean(),
-};
+const outputSchema = z
+  .object({
+    commentId: z.number().int().nonnegative(),
+    success: z.boolean(),
+  })
+  .strict();
 
-type Input = z.infer<z.ZodObject<typeof inputSchema>>;
-type Output = z.infer<z.ZodObject<typeof outputSchema>>;
+type Input = z.infer<typeof inputSchema>;
+type Output = z.infer<typeof outputSchema>;

25-31: Prefer deterministic IDs in E2E mocks to avoid flaky runs.
Math.random() makes outputs non-reproducible.

♻️ Proposed refactor
-    return {
-      commentId: Math.floor(Math.random() * 10000),
-      success: true,
-    };
+    return {
+      commentId: input.prNumber * 1000 + input.comment.length,
+      success: true,
+    };
apps/e2e/demo-e2e-skills/src/apps/skills/tools/github-get-pr.tool.ts (1)

4-19: Wrap schemas in z.object().strict() and tighten prNumber validation.
This keeps schema behavior consistent with other SDK tool patterns.

♻️ Proposed refactor
-const inputSchema = {
-  prNumber: z.number().describe('The pull request number'),
-};
+const inputSchema = z
+  .object({
+    prNumber: z.number().int().positive().describe('The pull request number'),
+  })
+  .strict();
 
-const outputSchema = {
-  pr: z.object({
-    number: z.number(),
-    title: z.string(),
-    author: z.string(),
-    status: z.enum(['open', 'closed', 'merged']),
-    files: z.array(z.string()),
-  }),
-};
+const outputSchema = z
+  .object({
+    pr: z
+      .object({
+        number: z.number(),
+        title: z.string(),
+        author: z.string(),
+        status: z.enum(['open', 'closed', 'merged']),
+        files: z.array(z.string()),
+      })
+      .strict(),
+  })
+  .strict();
 
-type Input = z.infer<z.ZodObject<typeof inputSchema>>;
-type Output = z.infer<z.ZodObject<typeof outputSchema>>;
+type Input = z.infer<typeof inputSchema>;
+type Output = z.infer<typeof outputSchema>;
apps/e2e/demo-e2e-skills/src/apps/skills/tools/slack-notify.tool.ts (2)

4-16: Wrap schemas in z.object().strict() and enforce non-empty inputs.
Optionally, consider an ISO timestamp validator (z.iso.datetime() in Zod v4) if you want strict timestamp formatting.

♻️ Proposed refactor
-const inputSchema = {
-  channel: z.string().describe('The Slack channel to send the message to'),
-  message: z.string().describe('The message text to send'),
-};
+const inputSchema = z
+  .object({
+    channel: z.string().min(1).describe('The Slack channel to send the message to'),
+    message: z.string().min(1).describe('The message text to send'),
+  })
+  .strict();
 
-const outputSchema = {
-  messageId: z.string(),
-  success: z.boolean(),
-  timestamp: z.string(),
-};
+const outputSchema = z
+  .object({
+    messageId: z.string(),
+    success: z.boolean(),
+    timestamp: z.string(),
+  })
+  .strict();
 
-type Input = z.infer<z.ZodObject<typeof inputSchema>>;
-type Output = z.infer<z.ZodObject<typeof outputSchema>>;
+type Input = z.infer<typeof inputSchema>;
+type Output = z.infer<typeof outputSchema>;

26-32: Optional: make mock outputs deterministic for stable E2E runs.

♻️ Proposed refactor
-    return {
-      messageId: `msg-${Math.floor(Math.random() * 10000)}`,
-      success: true,
-      timestamp: new Date().toISOString(),
-    };
+    const stableId = `msg-${input.channel}-${input.message.length}`;
+    return {
+      messageId: stableId,
+      success: true,
+      timestamp: new Date(0).toISOString(),
+    };
apps/e2e/demo-e2e-skills/src/apps/skills/plugins/devops-plugin.ts (1)

8-22: Prefer z.object().strict() and basic validation on tool schemas.
Also consider a stricter timestamp validator (e.g., z.iso.datetime() in Zod v4) if you want format enforcement.

♻️ Proposed refactor
-const deployInputSchema = {
-  environment: z.enum(['staging', 'production']).describe('Deployment environment'),
-  version: z.string().describe('Version to deploy'),
-};
+const deployInputSchema = z
+  .object({
+    environment: z.enum(['staging', 'production']).describe('Deployment environment'),
+    version: z.string().min(1).describe('Version to deploy'),
+  })
+  .strict();
 
-const deployOutputSchema = {
-  success: z.boolean(),
-  environment: z.enum(['staging', 'production']),
-  version: z.string(),
-  timestamp: z.string(),
-};
+const deployOutputSchema = z
+  .object({
+    success: z.boolean(),
+    environment: z.enum(['staging', 'production']),
+    version: z.string(),
+    timestamp: z.string(),
+  })
+  .strict();
 
-type DeployInput = z.infer<z.ZodObject<typeof deployInputSchema>>;
-type DeployOutput = z.infer<z.ZodObject<typeof deployOutputSchema>>;
+type DeployInput = z.infer<typeof deployInputSchema>;
+type DeployOutput = z.infer<typeof deployOutputSchema>;
 
-const rollbackInputSchema = {
-  environment: z.enum(['staging', 'production']).describe('Environment to rollback'),
-  targetVersion: z.string().optional().describe('Target version to rollback to'),
-};
+const rollbackInputSchema = z
+  .object({
+    environment: z.enum(['staging', 'production']).describe('Environment to rollback'),
+    targetVersion: z.string().min(1).optional().describe('Target version to rollback to'),
+  })
+  .strict();
 
-const rollbackOutputSchema = {
-  success: z.boolean(),
-  environment: z.enum(['staging', 'production']),
-  rolledBackTo: z.string(),
-};
+const rollbackOutputSchema = z
+  .object({
+    success: z.boolean(),
+    environment: z.enum(['staging', 'production']),
+    rolledBackTo: z.string(),
+  })
+  .strict();
 
-type RollbackInput = z.infer<z.ZodObject<typeof rollbackInputSchema>>;
-type RollbackOutput = z.infer<z.ZodObject<typeof rollbackOutputSchema>>;
+type RollbackInput = z.infer<typeof rollbackInputSchema>;
+type RollbackOutput = z.infer<typeof rollbackOutputSchema>;

Also applies to: 41-54

apps/e2e/demo-e2e-skills/webpack.config.js (1)

4-12: Make mode/devtool environment‑aware.
Right now mode is always "development" and devtool always uses eval source maps. This pattern is consistent across all demo/e2e apps (except demo-e2e-standalone which already makes mode environment-aware). If these configs are used for production builds, you'll still emit dev bundles. Consider conditioning on NODE_ENV.

♻️ Suggested update
 const { NxAppWebpackPlugin } = require('@nx/webpack/app-plugin');
 const { join } = require('path');
+const isProd = process.env.NODE_ENV === 'production';

 module.exports = {
   output: {
     path: join(__dirname, '../../../dist/apps/e2e/demo-e2e-skills'),
     ...(process.env.NODE_ENV !== 'production' && {
       devtoolModuleFilenameTemplate: '[absolute-resource-path]',
     }),
   },
-  mode: 'development',
-  devtool: 'eval-cheap-module-source-map',
+  mode: isProd ? 'production' : 'development',
+  devtool: isProd ? 'source-map' : 'eval-cheap-module-source-map',
libs/sdk/src/skill/session/skill-session.manager.ts (4)

190-190: Fire-and-forget async calls to persistSession may silently lose data.

persistSession is an async method, but it's called without await in multiple locations (lines 190, 331, 528, 556, 580, 602). While errors are logged internally, callers have no way to know if persistence failed, which could lead to session state loss in distributed scenarios.

If this is intentional for performance (non-blocking persistence), consider documenting this behavior. Otherwise, consider awaiting these calls or using a background queue with retry logic.

Example: Await persistence in critical paths
     // Persist to store if available
-    this.persistSession(session);
+    await this.persistSession(session);

Also applies to: 331-331, 528-528, 556-556, 580-580, 602-602


411-423: Side effect during authorization check: session deactivation on expiry.

The checkToolAuthorization method deactivates the skill session when duration exceeds the limit (line 415). This mutation during what appears to be a "check" operation could be unexpected. Consider either:

  1. Renaming to indicate mutation (e.g., authorizeToolCall)
  2. Moving expiry handling to a separate method called explicitly
  3. Documenting this behavior clearly

145-148: Consider using a specific error class instead of generic Error.

The coding guidelines for libs/sdk/** recommend using specific error classes with MCP error codes instead of generic errors. Consider creating a SkillSessionError or similar for session-related errors.

Example with specific error class
+import { SkillSessionError } from '../errors/skill-session.error';
+
 // In activateSkill:
     if (!session) {
-      throw new Error('Cannot activate skill: not running within a session context');
+      throw new SkillSessionError('Cannot activate skill: not running within a session context', 'NO_SESSION_CONTEXT');
     }

506-510: Consistent use of generic Error across session methods.

Similar to activateSkill, methods approveToolForSession, denyToolForSession, and setPolicyMode throw generic Error instances. Consider using a specific error class for consistency with SDK guidelines.

Also applies to: 534-538, 595-599

libs/sdk/src/skill/__tests__/memory-skill.provider.spec.ts (1)

126-151: Type assertion on addMany call for skills with extended properties.

The addMany call uses as SkillContent[] to cast skills that include extra properties (tags, priority, hideFromDiscovery). This is acceptable for testing internal behavior, but consider whether these extended properties should be formally part of the SkillContent interface or a separate SkillMetadata type.

libs/sdk/src/skill/flows/index.ts (1)

1-1: Consider removing the redundant file path comment.

The // file: ... comment on line 1 is redundant since modern IDEs and tooling display file paths. This pattern isn't used elsewhere in the codebase.

Suggested change
-// file: libs/sdk/src/skill/flows/index.ts
-
 /**
  * Skill Flows
libs/sdk/src/skill/hooks/index.ts (1)

1-1: Consider removing the redundant file path comment.

Same as the flows barrel - this comment is redundant.

apps/e2e/demo-e2e-skills/e2e/search-skills.e2e.test.ts (1)

174-184: Non-null assertions used after toBeDefined() checks.

Per coding guidelines, avoid non-null assertions (!). While the expect(...).toBeDefined() provides a runtime check, you could refactor to avoid the ! operator:

Suggested approach
       expect(reviewPrSkill).toBeDefined();
-      expect(reviewPrSkill!.tools).toBeDefined();
-      expect(reviewPrSkill!.tools.length).toBeGreaterThan(0);
+      if (!reviewPrSkill) throw new Error('reviewPrSkill not found');
+      expect(reviewPrSkill.tools).toBeDefined();
+      expect(reviewPrSkill.tools.length).toBeGreaterThan(0);

       // Check tool availability
-      for (const tool of reviewPrSkill!.tools) {
+      for (const tool of reviewPrSkill.tools) {

Alternatively, since Jest's expect().toBeDefined() throws on failure, you could also use a type guard helper or simply accept that expect already guards the code path. Given this is E2E test code and the pattern is common, this is optional.

libs/sdk/src/skill/__tests__/tool-authorization.spec.ts (2)

1-1: Consider removing the redundant file path comment.

Consistent with other new files in this PR.


207-229: Consider adding explicit instanceof check for error hierarchy.

Per coding guidelines for test files: "Include instanceof checks in tests for error classes to verify proper error hierarchy." While toThrow(ToolNotAllowedError) implicitly checks instanceof, an explicit assertion would make the error hierarchy verification clearer:

Suggested addition
   it('should format error message for not_in_allowlist with skill name', () => {
     const error = new ToolNotAllowedError(
       {
         allowed: false,
         reason: 'not_in_allowlist',
         toolName: 'unauthorized_tool',
         skillId: 'test-skill',
         skillName: 'Test Skill',
       },
       ['tool_a', 'tool_b'],
     );

+    expect(error).toBeInstanceOf(ToolNotAllowedError);
+    expect(error).toBeInstanceOf(Error);
     expect(error.message).toContain('unauthorized_tool');
libs/sdk/src/skill/__tests__/skill.metadata.spec.ts (2)

18-143: Good coverage for schema validation, but consider adding test for toolValidation default.

The skillMetadataSchema tests comprehensively cover field validation and defaults, but the toolValidation field (which defaults to 'warn' per the schema at lines 318-332 of skill.metadata.ts) is not explicitly verified. Consider adding a check in the minimal metadata test:

expect(result.data.toolValidation).toBe('warn'); // default value

145-162: Consider expanding normalizeToolRef tests to cover class-based tool references.

The implementation supports four input types (string, function/tool class, ToolRefWithClass, and SkillToolRefWithName), but tests only cover string and SkillToolRefWithName cases. For comprehensive coverage, consider adding tests for class-based tool references if feasible within the test setup.

apps/e2e/demo-e2e-skills/e2e/load-skill.e2e.test.ts (2)

185-190: Consider using type narrowing instead of non-null assertion.

After expect(githubGetPr).toBeDefined(), the non-null assertion on line 188-189 works but doesn't provide compile-time safety. A conditional check would be more robust:

♻️ Suggested improvement
       const githubGetPr = content.skill.tools.find((t) => t.name === 'github_get_pr');
 
       expect(githubGetPr).toBeDefined();
-      expect(githubGetPr!.purpose).toBeDefined();
-      expect(githubGetPr!.purpose).toContain('Fetch PR details');
+      if (githubGetPr) {
+        expect(githubGetPr.purpose).toBeDefined();
+        expect(githubGetPr.purpose).toContain('Fetch PR details');
+      }

202-210: Same pattern: prefer conditional narrowing over non-null assertions.

♻️ Suggested improvement
       // Should have pr_url parameter
       const prUrlParam = content.skill.parameters!.find((p) => p.name === 'pr_url');
-      expect(prUrlParam).toBeDefined();
-      expect(prUrlParam!.required).toBe(true);
-      expect(prUrlParam!.type).toBe('string');
+      expect(prUrlParam).toBeDefined();
+      if (prUrlParam) {
+        expect(prUrlParam.required).toBe(true);
+        expect(prUrlParam.type).toBe('string');
+      }
libs/sdk/src/skill/flows/__tests__/search-skills.flow.spec.ts (1)

48-118: Consider testing actual SearchSkillsFlow methods rather than simulating logic.

The pagination and output format tests replicate the flow's internal logic rather than exercising the actual flow class. While this validates algorithmic correctness, it may diverge from the actual implementation. For more robust coverage, consider instantiating SearchSkillsFlow with mocked dependencies and testing its actual execute() or equivalent methods.

libs/sdk/src/plugin/__tests__/plugin-skills.spec.ts (1)

64-71: Avoid non-null assertions in tests.

Use an explicit guard after expect(...).toBeDefined() so strict typing doesn't rely on !; apply the same pattern to the other ! usages here. As per coding guidelines, please avoid non-null assertions in TS.

♻️ Example adjustment
-      const pluginSkillRegistry = skillRegistries.find(
-        (r) => (r as SkillRegistry).owner.kind === 'plugin' && (r as SkillRegistry).owner.id === 'test-plugin',
-      ) as SkillRegistry | undefined;
-      expect(pluginSkillRegistry).toBeDefined();
-      const skills = pluginSkillRegistry!.getSkills();
+      const pluginSkillRegistry = skillRegistries.find(
+        (r) => (r as SkillRegistry).owner.kind === 'plugin' && (r as SkillRegistry).owner.id === 'test-plugin',
+      ) as SkillRegistry | undefined;
+      expect(pluginSkillRegistry).toBeDefined();
+      if (!pluginSkillRegistry) {
+        throw new Error('Expected plugin skill registry to be defined');
+      }
+      const skills = pluginSkillRegistry.getSkills();

Also applies to: 94-98, 124-129

libs/sdk/src/skill/flows/search-skills.flow.ts (1)

48-57: Prefer z.unknown() over z.any() in state schema.

unknown avoids propagating any while keeping flexibility. As per coding guidelines, prefer unknown to any.

♻️ Suggested tweak
-  results: z.array(z.any()),
+  results: z.array(z.unknown()),
libs/sdk/src/app/instances/app.remote.instance.ts (1)

72-123: Align EmptySkillRegistry method signatures with SkillRegistryInterface.

Several methods omit parameters or narrow return types (e.g., Promise<[]>), which can drift from the interface and weaken typing. Consider using Parameters<>/ReturnType<> to keep signatures aligned (apply to the other methods too).

♻️ Example alignment
-  findByName(): SkillEntry | undefined {
+  findByName(
+    ..._args: Parameters<SkillRegistryInterface['findByName']>
+  ): ReturnType<SkillRegistryInterface['findByName']> {
     return undefined;
   }
-  async search(): Promise<[]> {
+  async search(
+    ..._args: Parameters<SkillRegistryInterface['search']>
+  ): ReturnType<SkillRegistryInterface['search']> {
     return [];
   }
libs/sdk/src/common/records/skill.record.ts (1)

28-56: Prefer interfaces for record shapes

These record definitions are object shapes; consider using interface for consistency with the SDK style guide, keeping the union type as-is. As per coding guidelines, please prefer interface for object shapes.

♻️ Suggested refactor
-export type SkillClassTokenRecord = {
+export interface SkillClassTokenRecord {
   kind: SkillKind.CLASS_TOKEN;
   provide: Type<SkillContext>;
   metadata: SkillMetadata;
-};
+}

-export type SkillValueRecord = {
+export interface SkillValueRecord {
   kind: SkillKind.VALUE;
   provide: symbol;
   metadata: SkillMetadata;
-};
+}

-export type SkillFileRecord = {
+export interface SkillFileRecord {
   kind: SkillKind.FILE;
   provide: symbol;
   metadata: SkillMetadata;
   /**
    * Path to the source file (for reloading/hot-reload support).
    */
   filePath: string;
-};
+}
apps/e2e/demo-e2e-skills/e2e/plugin-skills.e2e.test.ts (1)

161-166: Avoid non-null assertions in tests

Replace the ! assertions with explicit guards so the test fails with a clear message when a lookup unexpectedly returns undefined. As per coding guidelines, avoid non-null assertions in TS.

♻️ Suggested refactor
       const deploySkill = content.skills.find((s) => s.id === 'deploy-workflow');

       expect(deploySkill).toBeDefined();
-      expect(deploySkill!.source).toBe('local');
+      if (!deploySkill) {
+        throw new Error('Expected deploy-workflow to be present in search results');
+      }
+      expect(deploySkill.source).toBe('local');
       const deployTool = content.skill.tools.find((t) => t.name === 'deploy_application');
       const rollbackTool = content.skill.tools.find((t) => t.name === 'rollback_deployment');

       expect(deployTool).toBeDefined();
-      expect(deployTool!.purpose).toContain('Deploy the application');
+      if (!deployTool) {
+        throw new Error('Expected deploy_application tool to be present');
+      }
+      expect(deployTool.purpose).toContain('Deploy the application');

       expect(rollbackTool).toBeDefined();
-      expect(rollbackTool!.purpose).toContain('Rollback if needed');
+      if (!rollbackTool) {
+        throw new Error('Expected rollback_deployment tool to be present');
+      }
+      expect(rollbackTool.purpose).toContain('Rollback if needed');

Also applies to: 219-226

apps/e2e/demo-e2e-skills/e2e/skill-session.e2e.test.ts (1)

15-56: Consider extracting shared interfaces to a common test utilities file.

The interfaces LoadSkillResult, SearchSkillsResult, GitHubPRResult, CommentResult, and SlackResult appear to be duplicated across multiple E2E test files (per the AI summary, similar definitions exist in load-skill.e2e.test.ts and other test files). Consider extracting these to a shared test utilities file to reduce duplication and ensure consistency.

libs/sdk/src/skill/flows/load-skill.flow.ts (1)

190-214: Consider adding type guard for scope.skillSession access.

The type assertion on line 192 works but could be fragile if the scope interface changes. Consider using a type guard or checking the property directly.

♻️ Optional improvement
     // Try to get skill session manager from scope
     // The manager is optional - it may not be configured
-    const scope = this.scope as { skillSession?: SkillSessionManager };
-    const sessionManager = scope.skillSession;
+    const sessionManager = 'skillSession' in this.scope 
+      ? (this.scope as { skillSession?: SkillSessionManager }).skillSession 
+      : undefined;

Alternatively, define a proper type extension for scopes that support skill sessions.

libs/sdk/src/common/decorators/skill.decorator.ts (1)

141-164: The final type assertion may be unsafe if metadata is partially set.

The return metadata as SkillMetadata on line 163 assumes all required fields were collected. While FrontMcpSkill validates via Zod schema before storing, if metadata were somehow corrupted or partially set, this could return an object missing required fields.

Consider validating the reconstructed metadata before returning, or document that this function is only safe to call on properly decorated classes.

♻️ Safer alternative with validation
   // Merge extended metadata
   const extended = Reflect.getMetadata(extendedSkillMetadata, target);
   if (extended) {
     Object.assign(metadata, extended);
   }

-  return metadata as SkillMetadata;
+  // Validate reconstructed metadata matches expected shape
+  const result = skillMetadataSchema.safeParse(metadata);
+  return result.success ? result.data as SkillMetadata : undefined;
 }
libs/sdk/src/common/interfaces/skill.interface.ts (1)

39-47: Prefer SkillToolRef[] for SkillContent.tools to avoid drift.

Using an inline object type duplicates the existing SkillToolRef contract and can diverge from normalization semantics over time.

♻️ Proposed refactor
-  tools: Array<{ name: string; purpose?: string; required?: boolean }>;
+  tools: SkillToolRef[];
libs/sdk/src/skill/index.ts (1)

119-120: Add @deprecated JSDoc to deprecated tool exports.

The comment won’t surface deprecation warnings to TS consumers; a JSDoc tag will.

♻️ Proposed change
 // Tools (deprecated - use flows instead)
+/** `@deprecated` Use SearchSkillsFlow / LoadSkillFlow instead. */
 export { SearchSkillsTool, LoadSkillTool, getSkillTools } from './tools';

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Fix all issues with AI agents
In `@libs/sdk/src/elicitation/helpers/fallback.helper.ts`:
- Around line 185-191: The rejection in the subscribeFallbackResult() catch
block incorrectly throws ElicitationTimeoutError; replace that with the original
subscription error (or a new ElicitationSubscriptionError if you prefer) so
callers can distinguish subscription failures from timeouts—locate the catch in
handleWaitingFallback where logger.error('handleWaitingFallback: failed to
subscribe', err) is called and change the reject(...) to either reject(err) or
reject(new ElicitationSubscriptionError(error.elicitId, err)) while keeping the
logging and timeout cleanup intact.

In `@libs/sdk/src/skill/providers/external-skill.provider.ts`:
- Around line 337-344: The syncSkills method in ExternalSkillProvider throws a
PublicMcpError with a custom code 'SKILL_SYNC_MODE_ERROR'; change that to use a
standard MCP error code (e.g., 'INVALID_PARAMS' or 'INVALID_REQUEST') so SDK
error handling is consistent — update the PublicMcpError instantiation in
syncSkills to use the chosen MCP code (keeping the message and HTTP status) to
replace the custom string.

In `@libs/sdk/src/skill/providers/memory-skill.provider.ts`:
- Around line 351-365: The update() path can leave orphaned vector docs when
skillId and skill.id diverge; before mutating the in-memory map and indexing,
normalize and validate the IDs: if skill.id !== skillId, set skill.id = skillId
(or throw) so the same id is used for skills.set(...) and indexSkill(...); also
ensure you remove any existing vector entries by checking/removing both skillId
and the original skill.id via vectorDB.hasDocument/removeDocument to avoid stale
docs (referencing update, add, indexSkill, vectorDB.hasDocument,
vectorDB.removeDocument, and skills.set).
- Around line 125-150: MemorySkillProvider is a newly exported public API but
lacks draft docs; add a new .mdx under docs/draft/docs describing the
MemorySkillProvider class, its options (MemorySkillProviderOptions including
defaultTopK, defaultMinScore, toolValidator), behavior (in-memory storage,
TFIDFVectoria usage), constructor example showing how to instantiate and
configure it, and notes on defaults and intended use; ensure the doc references
the class name MemorySkillProvider and related types
(MemorySkillProviderOptions, SkillContent, SkillDocumentMetadata, TFIDFVectoria)
so the public API is documented alongside other SDK providers.

In `@libs/sdk/src/skill/skill-storage.factory.ts`:
- Around line 221-229: In the case 'external' branch inside SkillStorageFactory
replace the custom error code 'SKILL_STORAGE_CONFIG_ERROR' with the MCP-standard
error code (e.g., INVALID_PARAMS) when throwing PublicMcpError for a missing
externalProvider; keep the existing message and status but swap the code
argument to the MCP constant (or string) used across the SDK so callers can
programmatically handle it (update any necessary import or reference to the MCP
error code constant alongside the existing PublicMcpError usage).

In `@libs/sdk/src/skill/skill.instance.ts`:
- Around line 171-187: The JSDoc for getContentSync is incorrect: it claims the
method "throws if instructions need loading" but the implementation returns
undefined when instructions are not a string; update the comment for
getContentSync (and mention SkillContent return behavior) to state that it
returns the cached SkillContent or builds from inline instructions, and
otherwise returns undefined rather than throwing; reference
metadata.instructions and buildSkillContent in the description so readers know
when undefined is returned.
♻️ Duplicate comments (4)
libs/auth/jest.config.cts (1)

12-22: Documentation for coverage gaps is a positive step; threshold concern already flagged.

The added comments explaining the coverage gaps and the TODO(FrontMCP-Auth-Coverage) tracking ticket are helpful for transparency and accountability. This documents existing technical debt rather than introducing it.

However, the underlying issue—coverage thresholds significantly below the 95% requirement—was already raised in a previous review. Please ensure the referenced ticket (FrontMCP-Auth-Coverage) is created and prioritized so these thresholds can be incrementally raised as tests are added. Based on learnings, the repo requires 95%+ coverage across all metrics.

libs/sdk/src/front-mcp/front-mcp.ts (1)

257-272: Remove any from wrapped MCP handlers.

The any annotations and cast defeat strict TS checks and violate the SDK guideline against any without strong justification. Consider typing the wrapper as the same signature as handler.handler, then pass it through without casting.

🔧 Suggested refinement
       // Wrap handler to inject auth context (same pattern as in-memory-server)
       const originalHandler = handler.handler;
-      // eslint-disable-next-line `@typescript-eslint/no-explicit-any`
-      const wrappedHandler = async (request: any, ctx: any) => {
+      const wrappedHandler: typeof handler.handler = async (request, ctx) => {
         // Inject auth info into context while preserving MCP SDK context properties
         // Merge with existing authInfo to avoid clobbering any existing properties
         const enrichedCtx = {
           ...ctx,
-          authInfo: { ...ctx?.authInfo, sessionId },
+          authInfo: { ...(ctx.authInfo ?? {}), sessionId },
         };
         return originalHandler(request, enrichedCtx);
       };
       // Cast required: MCP SDK's handler type expects specific context shape,
       // but our wrapped handlers preserve all context properties via pass-through
-      // eslint-disable-next-line `@typescript-eslint/no-explicit-any`
-      mcpServer.setRequestHandler(handler.requestSchema, wrappedHandler as any);
+      mcpServer.setRequestHandler(handler.requestSchema, wrappedHandler);

As per coding guidelines, please verify handler typing with:

#!/bin/bash
# Inspect handler signatures and setRequestHandler typing for a safe typed wrapper.
rg -nP --type=ts -C3 '\bsetRequestHandler\s*\(' libs/sdk/src
rg -nP --type=ts -C3 '\bhandler\s*:\s*' libs/sdk/src/transport/mcp-handlers
rg -nP --type=ts -C3 '\bRequestHandler\b|\bMcpHandler\b' libs/sdk/src/transport/mcp-handlers
libs/sdk/src/skill/session/skill-session-store.interface.ts (1)

56-101: activeSkills Map still not deep-cloned — shared references remain.

The past review comment about deep-cloning activeSkills and its nested Sets has not been addressed. The current implementation clones top-level Sets but shares the activeSkills Map by reference in save, get, update, and listActive. External code can mutate the stored state.

Additionally, in update (line 100), Object.assign(session, clonedUpdates) mutates the stored session directly rather than replacing it with a cloned copy, which can lead to partial state corruption if updates contains uncloned references.

libs/sdk/src/common/metadata/skill.metadata.ts (1)

392-434: Replace generic Error throws with SDK/MCP error types.

normalizeToolRef still throws plain Error, which makes it harder for callers to handle validation errors consistently. Please switch to PublicMcpError (or the SDK’s typed error) with an MCP error code (e.g., INVALID_PARAMS) in all three branches. As per coding guidelines, use MCP-coded SDK errors.

🔧 Suggested fix
+import { MCP_ERROR_CODES, PublicMcpError } from '../../errors';
 ...
-      throw new Error(
+      throw new PublicMcpError(
         `Invalid tool class '${ref.name ?? 'unknown'}'. ` +
           'Tool class must be decorated with `@Tool` and have a name property.',
+        MCP_ERROR_CODES.INVALID_PARAMS,
+        400,
       );
 ...
-      throw new Error(
+      throw new PublicMcpError(
         `Invalid tool class in reference. ` + 'Tool class must be decorated with `@Tool` and have a name property.',
+        MCP_ERROR_CODES.INVALID_PARAMS,
+        400,
       );
 ...
-  throw new Error(`Invalid tool reference: ${JSON.stringify(ref)}`);
+  throw new PublicMcpError(
+    `Invalid tool reference: ${JSON.stringify(ref)}`,
+    MCP_ERROR_CODES.INVALID_PARAMS,
+    400,
+  );
🧹 Nitpick comments (8)
libs/sdk/src/skill/skill.instance.ts (1)

111-114: Avoid type assertions to mutate objects with undeclared properties.

Adding properties via repeated type assertions circumvents type safety and makes the code fragile. Consider defining an extended interface for cached content that includes these metadata fields.

Suggested approach

Define an internal interface for cached content:

interface CachedSkillContent extends SkillContent {
  tags?: string[];
  priority?: number;
  hideFromDiscovery?: boolean;
}

Then change the cache field type and assignment:

-  /** Cached skill content (built lazily) */
-  private _cachedContent?: SkillContent;
+  /** Cached skill content (built lazily) */
+  private _cachedContent?: CachedSkillContent;
-    // Add additional metadata that's useful for search but not in SkillContent
-    // We store these as extra properties on the cached content
-    (this._cachedContent as SkillContent & { tags?: string[]; priority?: number; hideFromDiscovery?: boolean }).tags =
-      this._tags;
-    (this._cachedContent as SkillContent & { priority?: number }).priority = this._priority;
-    (this._cachedContent as SkillContent & { hideFromDiscovery?: boolean }).hideFromDiscovery = this._hidden;
+    // Add additional metadata that's useful for search but not in SkillContent
+    this._cachedContent.tags = this._tags;
+    this._cachedContent.priority = this._priority;
+    this._cachedContent.hideFromDiscovery = this._hidden;
libs/sdk/src/skill/session/skill-session-store.interface.ts (1)

127-140: Clarify cleanup logic — comment says "inactive" but checks startedAt age.

The comment on line 132 says "Clean up sessions that have been inactive" but the logic deletes sessions based on startedAt timestamp, regardless of activity. Consider updating the comment to reflect the actual behavior, or if the intent is to clean up based on inactivity, use a lastActivityAt timestamp instead.

📝 Suggested comment fix
   async cleanup(maxAge: number): Promise<number> {
     const now = Date.now();
     let cleaned = 0;

     for (const [sessionId, session] of this.sessions) {
-      // Clean up sessions that have been inactive
+      // Clean up sessions older than maxAge based on startedAt timestamp
       if (session.startedAt > 0 && now - session.startedAt > maxAge) {
         this.sessions.delete(sessionId);
         cleaned++;
       }
     }

     return cleaned;
   }
libs/sdk/src/skill/__tests__/skill-session.spec.ts (1)

509-614: Consider adding mutation isolation tests.

The store tests verify CRUD operations but don't test that retrieved sessions are properly isolated from the stored state. Adding a test that mutates a retrieved session and verifies the store remains unaffected would catch reference-sharing bugs.

🧪 Suggested test for mutation isolation
it('should isolate retrieved sessions from store mutations', async () => {
  const session = createSession('session-1');
  await store.save(session);

  const retrieved = await store.get('session-1');
  // Mutate the retrieved session
  retrieved!.allowedTools.add('mutated_tool');
  retrieved!.activeSkills.get('skill-1')!.allowedTools.add('another_mutated');

  // Verify store is unaffected
  const freshRetrieved = await store.get('session-1');
  expect(freshRetrieved!.allowedTools.has('mutated_tool')).toBe(false);
  expect(freshRetrieved!.activeSkills.get('skill-1')!.allowedTools.has('another_mutated')).toBe(false);
});
libs/sdk/src/skill/flows/load-skill.flow.ts (1)

158-162: Consider a more appropriate error type for missing skill registry.

InvalidInputError implies the client sent invalid data, but a missing skill registry is a server configuration issue. Consider using a more semantically appropriate error type (e.g., InternalError or a configuration-specific error).

♻️ Suggested improvement
+import { InternalError } from '../../errors';
...
     if (!skillRegistry) {
-      throw new InvalidInputError('Skills are not available in this scope');
+      throw new InternalError('Skill registry not configured');
     }

Per coding guidelines, use specific error classes with MCP error codes.

.github/workflows/push.yml (1)

248-266: Consider extracting coverage flag into a reusable approach.

The conditional coverage logic is duplicated between the initial run and retry steps. While functional, you could simplify by defining a computed command variable or using a composite action.

♻️ Optional: Use a shell variable to reduce duplication
      - name: Run E2E tests (${{ matrix.project }})
        id: test
        continue-on-error: true
        run: |
-          if [ "$RUN_COVERAGE" = "true" ]; then
-            npx nx run ${{ matrix.project }}:test --coverage
-          else
-            npx nx run ${{ matrix.project }}:test
-          fi
+          COVERAGE_FLAG=$([[ "$RUN_COVERAGE" = "true" ]] && echo "--coverage" || echo "")
+          npx nx run ${{ matrix.project }}:test $COVERAGE_FLAG

      # ... reset step ...

      - name: Retry E2E tests (${{ matrix.project }})
        if: steps.test.outcome == 'failure'
        run: |
-          if [ "$RUN_COVERAGE" = "true" ]; then
-            npx nx run ${{ matrix.project }}:test --coverage
-          else
-            npx nx run ${{ matrix.project }}:test
-          fi
+          COVERAGE_FLAG=$([[ "$RUN_COVERAGE" = "true" ]] && echo "--coverage" || echo "")
+          npx nx run ${{ matrix.project }}:test $COVERAGE_FLAG
libs/sdk/src/elicitation/helpers/fallback.helper.ts (1)

118-138: Consider typing the error response objects.

The error result objects (timeout at lines 128-136, failure at lines 159-167) match the CallToolResult structure but are untyped inline objects. For better type safety and consistency with MCP protocol responses, consider using or creating a typed helper.

💡 Example typed helper
// Could be added to elicitation.types.ts or a shared location
function createErrorResult(message: string): { content: Array<{ type: 'text'; text: string }>; isError: true } {
  return {
    content: [{ type: 'text', text: message }],
    isError: true,
  };
}

Also applies to: 155-168

libs/sdk/src/skill/providers/external-skill.provider.ts (1)

470-472: Return a deeper copy of sync state entries.

getSyncState() currently copies the Map but reuses entry objects, so callers can mutate internal state via object references. Consider cloning each entry value for better encapsulation.

♻️ Suggested tweak
-    return this.syncState ? { ...this.syncState, entries: new Map(this.syncState.entries) } : null;
+    return this.syncState
+      ? {
+          ...this.syncState,
+          entries: new Map(
+            [...this.syncState.entries].map(([key, value]) => [key, { ...value }]),
+          ),
+        }
+      : null;
libs/sdk/src/skill/providers/memory-skill.provider.ts (1)

524-546: Consider a typed metadata wrapper instead of casts.
The SkillContent & { tags?: ... } casts are a bit brittle; a small wrapper type for stored skills would avoid repeated assertions.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@apps/e2e/demo-e2e-transport-recreation/project.json`:
- Around line 36-42: The test target in the project configuration is missing the
runInBand option which other e2e projects use to force sequential same-process
Jest runs; update the "test" target options (the JSON object under "test" for
this project) to include "runInBand": true alongside the existing "jestConfig"
and "passWithNoTests" keys so Jest runs in-band like other e2e projects and
avoids concurrent port/resource conflicts.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@docs/draft/docs/servers/memory-skill-provider.mdx`:
- Around line 213-217: In the "See Also" section remove or fix the two broken
links (/servers/external-skill-providers and /servers/skill-search): either
create the missing docs pages or replace those link targets with valid existing
docs (e.g., other relevant pages in the docs set) and ensure the link text
remains meaningful; specifically edit the "See Also" block in
memory-skill-provider’s See Also section so it only contains valid URLs (or
remove the entries) while keeping "Skills Overview" intact.
♻️ Duplicate comments (1)
libs/sdk/src/skill/skill-storage.factory.ts (1)

220-230: HTTP status 500 is incorrect for invalid configuration.

The error for missing externalProvider uses HTTP status 500 (Internal Server Error), but this is a client configuration error that should use 400 (Bad Request). The past review suggested changing to 400.

🔧 Suggested fix
       throw new PublicMcpError(
         'External provider type requires externalProvider instance. ' +
           'Create a class extending ExternalSkillProviderBase and pass it via externalProvider option.',
         String(MCP_ERROR_CODES.INVALID_PARAMS),
-        500,
+        400,
       );
🧹 Nitpick comments (11)
libs/sdk/src/skill/skill.instance.ts (2)

95-100: Consider using path module for cross-platform path handling.

The path extraction using lastIndexOf('/') assumes forward slashes. While SDK paths are typically normalized, using path.dirname() would be more robust.

♻️ Suggested improvement
+import { dirname } from 'path';
+
 // For file-based skills, use the directory of the skill file
-const filePath = this.record.filePath;
-const lastSlash = filePath.lastIndexOf('/');
-basePath = lastSlash > 0 ? filePath.substring(0, lastSlash) : undefined;
+basePath = dirname(this.record.filePath) || undefined;

189-201: Consider caching inline-built content for performance.

When building content from inline instructions, the result isn't cached. This means repeated getContentSync() calls before load() will rebuild each time. If side-effect-free behavior is intentional, this is fine; otherwise, consider populating the cache.

♻️ Optional: Cache inline-built content
   // Only works with inline instructions
   if (typeof this.metadata.instructions === 'string') {
-    return buildSkillContent(this.metadata, this.metadata.instructions);
+    const content = buildSkillContent(this.metadata, this.metadata.instructions);
+    this._cachedContent = {
+      ...content,
+      tags: this._tags,
+      priority: this._priority,
+      hideFromDiscovery: this._hidden,
+    };
+    return this._cachedContent;
   }
libs/sdk/src/skill/providers/external-skill.provider.ts (1)

251-267: Consider validating tools against registry for external loads.

The load method marks all tools as "available" with an empty missingTools array. While the comment explains validation happens at registry level, this means callers cannot distinguish between "tools validated as available" vs "tools not validated at all."

Consider adding an optional toolValidator (like MemorySkillProvider) to provide consistent tool validation across providers.

libs/sdk/src/skill/skill-storage.factory.ts (1)

242-263: Unreachable catch block in vectordb case.

The try-catch block wraps code that doesn't throw any exceptions. The MemorySkillProvider constructor is synchronous and doesn't throw. This creates dead code that may confuse maintainers.

Consider simplifying since the VectorDB provider is not yet implemented:

♻️ Suggested simplification
     case 'vectordb': {
-      // Lazy require VectorDB provider to avoid bundling when not used
-      try {
-        // VectorDB provider will be implemented in Phase 7
-        // For now, fall back to memory provider with a warning
-        logger?.warn('[SkillStorageFactory] VectorDB provider not yet implemented, falling back to memory provider');
-        provider = new MemorySkillProvider({
-          ...memory,
-          toolValidator,
-        });
-        type = 'memory';
-      } catch (error) {
-        logger?.warn('[SkillStorageFactory] Failed to create VectorDB provider, falling back to memory', {
-          error: error instanceof Error ? error.message : String(error),
-        });
-        provider = new MemorySkillProvider({
-          ...memory,
-          toolValidator,
-        });
-        type = 'memory';
-      }
+      // VectorDB provider will be implemented in Phase 7
+      // For now, fall back to memory provider with a warning
+      logger?.warn('[SkillStorageFactory] VectorDB provider not yet implemented, falling back to memory provider');
+      provider = new MemorySkillProvider({
+        ...memory,
+        toolValidator,
+      });
+      type = 'memory';
       break;
     }
libs/sdk/src/skill/providers/memory-skill.provider.ts (3)

149-172: initialized flag is set but never checked.

The initialized flag is set to true in initialize() and false in dispose(), but it's never checked before operations. Consider either:

  1. Adding guards to methods that require initialization
  2. Removing the flag if initialization is not required
♻️ Optional: Add initialization guard
+  private ensureInitialized(): void {
+    if (!this.initialized) {
+      throw new Error('MemorySkillProvider not initialized. Call initialize() first.');
+    }
+  }
+
   async search(query: string, options: SkillSearchOptions = {}): Promise<SkillSearchResult[]> {
+    this.ensureInitialized();
     const {

422-435: Reindexing after every single add may impact performance.

The indexSkill method calls this.vectorDB.reindex() after adding a single document. For bulk operations, this is already optimized in addMany, but individual add and update calls will trigger full reindexing.

Consider documenting this behavior or deferring reindex for batch scenarios:

// Consider adding a flag or batch mode
async add(skill: SkillContent, skipReindex = false): Promise<void> {
  this.skills.set(skill.id, skill);
  this.indexSkillDocument(skill);
  if (!skipReindex) {
    this.vectorDB.reindex();
  }
}

This is noted in the documentation under "Performance Considerations" but may warrant a batch API for efficiency.


541-559: Type assertions to StoredSkillContent are safe but could be cleaner.

The helper methods cast SkillContent to StoredSkillContent to access extended properties. While this works, consider using a type guard or making the internal storage explicitly use StoredSkillContent:

♻️ Optional: Use explicit typing
-  private skills: Map<string, SkillContent> = new Map();
+  private skills: Map<string, StoredSkillContent> = new Map();

   // Then remove type assertions in helper methods
   private getSkillTags(skill: StoredSkillContent): string[] {
     return skill.tags ?? [];
   }

This would require updating method signatures but provides better type safety throughout the class.

libs/sdk/src/elicitation/helpers/fallback.helper.ts (1)

88-195: Consider tightening the return type to avoid unknown leakage.

handleWaitingFallback always resolves to a tool result shape, but Promise<unknown> forces downstream casts. If possible, align FallbackExecutionResult.result to CallToolResult and update this signature accordingly. As per coding guidelines, prefer strictly typed MCP protocol responses where feasible.

libs/sdk/src/skill/session/skill-session-store.interface.ts (2)

59-71: Use a top-level import instead of inline import() type references.

The import('./skill-session.types').ActiveSkillInfo syntax is repeated three times. Import ActiveSkillInfo at the top of the file alongside SkillSessionState for cleaner, more maintainable code.

♻️ Suggested refactor
-import type { SkillSessionState } from './skill-session.types';
+import type { SkillSessionState, ActiveSkillInfo } from './skill-session.types';

Then update the method signature:

   private cloneActiveSkills(
-    activeSkills: Map<string, import('./skill-session.types').ActiveSkillInfo>,
-  ): Map<string, import('./skill-session.types').ActiveSkillInfo> {
-    const cloned = new Map<string, import('./skill-session.types').ActiveSkillInfo>();
+    activeSkills: Map<string, ActiveSkillInfo>,
+  ): Map<string, ActiveSkillInfo> {
+    const cloned = new Map<string, ActiveSkillInfo>();

183-206: Factory function type options are inconsistent with the interface.

The interface declares type: 'memory' | 'redis' | 'custom' (line 45), but the factory only accepts 'memory' | 'redis'. Additionally, the default case silently returns a memory store, which could mask errors if new types are added in the future.

Consider either:

  1. Adding 'custom' support to the factory (e.g., accepting a custom store instance), or
  2. Removing 'custom' from the interface if it's not intended to be created via the factory.

For the default case, using a type-narrowing exhaustive check would surface future mismatches at compile time.

♻️ Suggested refactor with exhaustive check
   switch (type) {
     case 'memory':
       return new MemorySkillSessionStore();

     case 'redis':
       // Redis implementation would go here
       // For now, fall back to memory
       console.warn('Redis skill session store not implemented, using memory store');
       return new MemorySkillSessionStore();

-    default:
-      return new MemorySkillSessionStore();
+    default: {
+      const _exhaustive: never = type;
+      throw new Error(`Unknown skill session store type: ${_exhaustive}`);
+    }
   }
libs/sdk/src/skill/__tests__/skill-session.spec.ts (1)

615-637: Replace non-null assertions with proper guards.

The coding guidelines specify avoiding non-null assertions (!) in favor of proper error handling and type guards. In tests, use expect assertions or guards before accessing properties.

As per coding guidelines, consider this pattern:

♻️ Suggested refactor
   it('should isolate retrieved sessions from store mutations', async () => {
     const session = createSession('session-1');
     await store.save(session);

     // Retrieve the session and mutate it
     const retrieved = await store.get('session-1');
-    expect(retrieved).not.toBeNull();
+    expect(retrieved).not.toBeNull();
+    if (!retrieved) return; // Type guard for TS narrowing

     // Mutate the retrieved session's Sets and Maps
-    retrieved!.allowedTools.add('mutated_tool');
-    retrieved!.activeSkills.get('skill-1')!.allowedTools.add('another_mutated');
-    retrieved!.approvedTools.add('mutated_approved');
-    retrieved!.deniedTools.add('mutated_denied');
+    retrieved.allowedTools.add('mutated_tool');
+    const skill = retrieved.activeSkills.get('skill-1');
+    expect(skill).toBeDefined();
+    if (skill) skill.allowedTools.add('another_mutated');
+    retrieved.approvedTools.add('mutated_approved');
+    retrieved.deniedTools.add('mutated_denied');

     // Get a fresh copy from the store
     const freshRetrieved = await store.get('session-1');
+    expect(freshRetrieved).not.toBeNull();
+    if (!freshRetrieved) return;
+    const freshSkill = freshRetrieved.activeSkills.get('skill-1');
+    expect(freshSkill).toBeDefined();

     // Verify the fresh copy is not affected by mutations
-    expect(freshRetrieved!.allowedTools.has('mutated_tool')).toBe(false);
-    expect(freshRetrieved!.activeSkills.get('skill-1')!.allowedTools.has('another_mutated')).toBe(false);
-    expect(freshRetrieved!.approvedTools.has('mutated_approved')).toBe(false);
-    expect(freshRetrieved!.deniedTools.has('mutated_denied')).toBe(false);
+    expect(freshRetrieved.allowedTools.has('mutated_tool')).toBe(false);
+    expect(freshSkill?.allowedTools.has('another_mutated')).toBe(false);
+    expect(freshRetrieved.approvedTools.has('mutated_approved')).toBe(false);
+    expect(freshRetrieved.deniedTools.has('mutated_denied')).toBe(false);
   });

@frontegg-david frontegg-david merged commit d6a8621 into main Jan 24, 2026
36 checks passed
@frontegg-david frontegg-david deleted the add-support-for-skills branch January 24, 2026 22:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants