@tigerdata/mcp-boilerplate 0.9.2 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,20 +2,17 @@
2
2
 
3
3
  This provides some common code for creating a [Model Context Protocol](https://modelcontextprotocol.io/introduction) (MCP) server in Node.js.
4
4
 
5
- ## Setup
5
+ ## Usage
6
6
 
7
- 1. Clone the repository:
7
+ ```bash
8
+ npm install @tigerdata/mcp-boilerplate
9
+ ```
8
10
 
9
- ```bash
10
- git clone <repository-url>
11
- cd mcp-boilerplate-node
12
- ```
11
+ See [tiger-skills-mcp-server](https://github.com/tigerdata/tiger-skills-mcp-server) for an example MCP server using this boilerplate.
13
12
 
14
- 2. Install dependencies:
13
+ ### Skills
15
14
 
16
- ```bash
17
- ./bun install
18
- ```
15
+ Add skills support to your MCP server by leveraging the skills submodule in `@tigerdata/mcp-boilerplate/skills`. See [src/skills/README.md](./skills/README.md) for details.
19
16
 
20
17
  ## Eslint Plugin
21
18
 
@@ -2,4 +2,4 @@
2
2
  * REST API alternative to MCP for direct use of the same tools.
3
3
  */
4
4
  import type { BaseApiFactory, RouterFactoryResult } from '../types.js';
5
- export declare const apiRouterFactory: <Context extends Record<string, unknown>>(context: Context, apiFactories: readonly BaseApiFactory<Context>[]) => RouterFactoryResult;
5
+ export declare const apiRouterFactory: <Context extends Record<string, unknown>>(context: Context, apiFactories: readonly BaseApiFactory<Context>[]) => Promise<RouterFactoryResult>;
package/dist/http/api.js CHANGED
@@ -4,11 +4,11 @@
4
4
  import bodyParser from 'body-parser';
5
5
  import { Router } from 'express';
6
6
  import { z } from 'zod';
7
- export const apiRouterFactory = (context, apiFactories) => {
7
+ export const apiRouterFactory = async (context, apiFactories) => {
8
8
  const router = Router();
9
9
  router.use(bodyParser.json());
10
10
  for (const factory of apiFactories) {
11
- const tool = factory(context, {});
11
+ const tool = await factory(context, {});
12
12
  if (!tool.method || !tool.route)
13
13
  continue;
14
14
  router[tool.method](tool.route, async (req, res) => {
@@ -1,8 +1,8 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import type { McpFeatureFlags, RouterFactoryResult } from '../types.js';
3
- export declare const mcpRouterFactory: <Context extends Record<string, unknown>>(context: Context, createServer: (context: Context, featureFlags: McpFeatureFlags) => {
3
+ export declare const mcpRouterFactory: <Context extends Record<string, unknown>>(context: Context, createServer: (context: Context, featureFlags: McpFeatureFlags) => Promise<{
4
4
  server: McpServer;
5
- }, { name, stateful, inspector, }?: {
5
+ }>, { name, stateful, inspector, }?: {
6
6
  name?: string;
7
7
  stateful?: boolean;
8
8
  inspector?: boolean;
package/dist/http/mcp.js CHANGED
@@ -33,7 +33,7 @@ export const mcpRouterFactory = (context, createServer, { name, stateful = true,
33
33
  });
34
34
  const handleStatelessRequest = async (req, res) => {
35
35
  const featureFlags = parseFeatureFlags(req);
36
- const { server } = createServer(context, featureFlags);
36
+ const { server } = await createServer(context, featureFlags);
37
37
  const transport = new StreamableHTTPServerTransport({
38
38
  sessionIdGenerator: undefined,
39
39
  });
@@ -98,7 +98,7 @@ export const mcpRouterFactory = (context, createServer, { name, stateful = true,
98
98
  }
99
99
  },
100
100
  });
101
- const { server } = createServer(context, featureFlags);
101
+ const { server } = await createServer(context, featureFlags);
102
102
  await server.connect(transport);
103
103
  }
104
104
  await transport.handleRequest(req, res, body);
@@ -201,7 +201,7 @@ export const mcpRouterFactory = (context, createServer, { name, stateful = true,
201
201
  ${inspector
202
202
  ? `
203
203
  <h3>Inspector</h3>
204
- <p>You can use the <a href="/inspector">MCP Inspector</a> for testing purposes.</p>`
204
+ <p>You can use the <a href="/inspector?server=${encodeURIComponent(fullUrl)}">MCP Inspector</a> for testing purposes.</p>`
205
205
  : ''}
206
206
  <h3>Claude Code</h3>
207
207
  <p>To connect to this MCP server using Claude Code, run the following command in your terminal:</p>
@@ -14,10 +14,10 @@ export declare const httpServerFactory: <Context extends Record<string, unknown>
14
14
  cleanupFn?: () => void | Promise<void>;
15
15
  stateful?: boolean;
16
16
  instructions?: string;
17
- }) => {
17
+ }) => Promise<{
18
18
  app: express.Express;
19
19
  server: Server;
20
20
  apiRouter: express.Router;
21
21
  mcpRouter: express.Router;
22
22
  registerCleanupFn: (fn: () => Promise<void>) => void;
23
- };
23
+ }>;
@@ -7,7 +7,7 @@ import { log } from './logger.js';
7
7
  import { mcpServerFactory } from './mcpServer.js';
8
8
  import { registerExitHandlers } from './registerExitHandlers.js';
9
9
  import { StatusError } from './StatusError.js';
10
- export const httpServerFactory = ({ name, version, context, apiFactories = [], promptFactories, resourceFactories, additionalSetup, cleanupFn, stateful = true, instructions, }) => {
10
+ export const httpServerFactory = async ({ name, version, context, apiFactories = [], promptFactories, resourceFactories, additionalSetup, cleanupFn, stateful = true, instructions, }) => {
11
11
  const cleanupFns = cleanupFn
12
12
  ? [cleanupFn]
13
13
  : [];
@@ -31,7 +31,7 @@ export const httpServerFactory = ({ name, version, context, apiFactories = [], p
31
31
  }), { name, stateful, inspector });
32
32
  cleanupFns.push(mcpCleanup);
33
33
  app.use('/mcp', mcpRouter);
34
- const [apiRouter, apiCleanup] = apiRouterFactory(context, apiFactories);
34
+ const [apiRouter, apiCleanup] = await apiRouterFactory(context, apiFactories);
35
35
  cleanupFns.push(apiCleanup);
36
36
  app.use('/api', apiRouter);
37
37
  // Error handler
@@ -54,7 +54,9 @@ export const httpServerFactory = ({ name, version, context, apiFactories = [], p
54
54
  import('@mcp-use/inspector')
55
55
  .then(({ mountInspector }) => {
56
56
  app.use(bodyParser.json());
57
- mountInspector(app, { autoConnectUrl: `http://localhost:${PORT}/mcp` });
57
+ mountInspector(app, {
58
+ autoConnectUrl: process.env.MCP_PUBLIC_URL ?? `http://localhost:${PORT}/mcp`,
59
+ });
58
60
  })
59
61
  .catch(log.error);
60
62
  }
@@ -17,6 +17,6 @@ export declare const mcpServerFactory: <Context extends Record<string, unknown>>
17
17
  additionalCapabilities?: ServerCapabilities;
18
18
  featureFlags?: McpFeatureFlags;
19
19
  instructions?: string;
20
- }) => {
20
+ }) => Promise<{
21
21
  server: McpServer;
22
- };
22
+ }>;
package/dist/mcpServer.js CHANGED
@@ -25,7 +25,7 @@ const shouldSkip = (item, enabledSets, disabledSets) => {
25
25
  }
26
26
  return false;
27
27
  };
28
- export const mcpServerFactory = ({ name, version = '1.0.0', context, apiFactories = [], promptFactories = [], resourceFactories = [], additionalSetup, additionalCapabilities = {}, featureFlags = {}, instructions, }) => {
28
+ export const mcpServerFactory = async ({ name, version = '1.0.0', context, apiFactories = [], promptFactories = [], resourceFactories = [], additionalSetup, additionalCapabilities = {}, featureFlags = {}, instructions, }) => {
29
29
  const enablePrompts = featureFlags.prompts !== false;
30
30
  const enableResources = featureFlags.resources !== false;
31
31
  const enableTools = featureFlags.tools !== false;
@@ -45,7 +45,7 @@ export const mcpServerFactory = ({ name, version = '1.0.0', context, apiFactorie
45
45
  });
46
46
  if (enableTools) {
47
47
  for (const factory of apiFactories) {
48
- const tool = factory(context, featureFlags);
48
+ const tool = await factory(context, featureFlags);
49
49
  if (shouldSkip(tool, [enabledTools, featureFlags.enabledTools], [disabledTools, featureFlags.disabledTools])) {
50
50
  continue;
51
51
  }
@@ -111,7 +111,7 @@ export const mcpServerFactory = ({ name, version = '1.0.0', context, apiFactorie
111
111
  }
112
112
  if (enablePrompts) {
113
113
  for (const factory of promptFactories) {
114
- const prompt = factory(context, featureFlags);
114
+ const prompt = await factory(context, featureFlags);
115
115
  if (shouldSkip(prompt, [enabledPrompts, featureFlags.enabledPrompts], [disabledPrompts, featureFlags.disabledPrompts])) {
116
116
  continue;
117
117
  }
@@ -139,7 +139,7 @@ export const mcpServerFactory = ({ name, version = '1.0.0', context, apiFactorie
139
139
  }
140
140
  if (enableResources) {
141
141
  for (const factory of resourceFactories) {
142
- const resource = factory(context, featureFlags);
142
+ const resource = await factory(context, featureFlags);
143
143
  if (shouldSkip(resource, [enabledResources, featureFlags.enabledResources], [disabledResources, featureFlags.disabledResources])) {
144
144
  continue;
145
145
  }
@@ -0,0 +1,5 @@
1
+ export * from './prompts.js';
2
+ export * from './resource.js';
3
+ export * from './tool.js';
4
+ export * from './types.js';
5
+ export * from './utils.js';
@@ -0,0 +1,5 @@
1
+ export * from './prompts.js';
2
+ export * from './resource.js';
3
+ export * from './tool.js';
4
+ export * from './types.js';
5
+ export * from './utils.js';
@@ -0,0 +1,8 @@
1
+ import type { Octokit } from '@octokit/rest';
2
+ import type { PromptFactory } from '../types.js';
3
+ import type { ServerContextWithOctokit } from './types.js';
4
+ interface Options {
5
+ octokit?: Octokit;
6
+ }
7
+ export declare const createSkillsPromptFactories: (options?: Options) => Promise<PromptFactory<ServerContextWithOctokit, Record<string, never>>[]>;
8
+ export {};
@@ -0,0 +1,31 @@
1
+ import { loadSkills, parseSkillsFlags, viewSkillContent } from './utils.js';
2
+ // Create a prompt for each skill from the main SKILL.md files.
3
+ export const createSkillsPromptFactories = async (options = {}) => {
4
+ const skills = await loadSkills(options);
5
+ return Array.from(skills.entries()).map(([name, skillData]) => ({ octokit }, { query }) => ({
6
+ name,
7
+ config: {
8
+ // Using the dash-separated name as the title to work around a problem in Claude Code
9
+ // See https://github.com/anthropics/claude-code/issues/7464
10
+ title: name,
11
+ description: skillData.description,
12
+ inputSchema: {}, // No arguments for static skills
13
+ },
14
+ fn: async () => {
15
+ const flags = parseSkillsFlags(query);
16
+ const content = await viewSkillContent({ octokit, flags, name });
17
+ return {
18
+ description: skillData.description || name,
19
+ messages: [
20
+ {
21
+ role: 'user',
22
+ content: {
23
+ type: 'text',
24
+ text: content,
25
+ },
26
+ },
27
+ ],
28
+ };
29
+ },
30
+ }));
31
+ };
@@ -0,0 +1,12 @@
1
+ import type { McpFeatureFlags, ResourceFactory } from '../types.js';
2
+ import type { ServerContextWithOctokit } from './types.js';
3
+ interface Options<Context extends ServerContextWithOctokit> {
4
+ appendSkillsListToDescription?: boolean;
5
+ description?: string;
6
+ disabled?: boolean | ((ctx: Context, flags: McpFeatureFlags) => boolean | Promise<boolean>);
7
+ name?: string;
8
+ title?: string;
9
+ uriScheme?: string;
10
+ }
11
+ export declare const createSkillsResourceFactory: <Context extends ServerContextWithOctokit>(options?: Options<Context>) => ResourceFactory<ServerContextWithOctokit>;
12
+ export {};
@@ -0,0 +1,47 @@
1
+ import { listSkills, loadSkills, parseSkillsFlags, skillsDescription, skillVisible, viewSkillContent, } from './utils.js';
2
+ export const createSkillsResourceFactory = (options = {}) => async (ctx, mcpFlags) => {
3
+ const { octokit } = ctx;
4
+ const { query } = mcpFlags;
5
+ const flags = parseSkillsFlags(query);
6
+ return {
7
+ type: 'templated',
8
+ name: options.name || 'skills',
9
+ disabled: typeof options.disabled === 'function'
10
+ ? await options.disabled(ctx, mcpFlags)
11
+ : options.disabled,
12
+ config: {
13
+ title: options.title || 'Skills',
14
+ description: `${options.description || skillsDescription}${options.appendSkillsListToDescription
15
+ ? `\n\n## Available Skills\n\n${await listSkills({ octokit, flags: parseSkillsFlags(query) })}`
16
+ : ''}`,
17
+ },
18
+ uriTemplate: `${options.uriScheme || 'skills'}://{name}{?path}`,
19
+ list: async () => {
20
+ const skills = await loadSkills({ octokit });
21
+ return {
22
+ resources: Array.from(skills.values())
23
+ .filter((s) => skillVisible(s.name, flags))
24
+ .map((skill) => ({
25
+ uri: `${options.uriScheme || 'skills'}://${skill.name}?path=SKILL.md`,
26
+ name: skill.name,
27
+ title: skill.name,
28
+ description: skill.description,
29
+ mimeType: 'text/markdown',
30
+ })),
31
+ };
32
+ },
33
+ read: async (uri, { name, path }) => {
34
+ if (Array.isArray(name) || Array.isArray(path) || !name) {
35
+ throw new Error('Invalid parameters');
36
+ }
37
+ return {
38
+ contents: [
39
+ {
40
+ uri: uri.href,
41
+ text: await viewSkillContent({ octokit, flags, name, path }),
42
+ },
43
+ ],
44
+ };
45
+ },
46
+ };
47
+ };
@@ -0,0 +1,13 @@
1
+ import type { ApiFactory, McpFeatureFlags } from '../types.js';
2
+ import { type ServerContextWithOctokit, zViewSkillInputSchema, zViewSkillOutputSchema } from './types.js';
3
+ interface Options<Context extends ServerContextWithOctokit> {
4
+ appendSkillsListToDescription?: boolean;
5
+ description?: string;
6
+ disabled?: boolean | ((ctx: Context, flags: McpFeatureFlags) => boolean | Promise<boolean>);
7
+ method?: 'get' | 'post';
8
+ name?: string;
9
+ route?: string;
10
+ title?: string;
11
+ }
12
+ export declare const createViewSkillToolFactory: <Context extends ServerContextWithOctokit>(options?: Options<Context>) => ApiFactory<ServerContextWithOctokit, typeof zViewSkillInputSchema, typeof zViewSkillOutputSchema>;
13
+ export {};
@@ -0,0 +1,37 @@
1
+ import { zViewSkillInputSchema, zViewSkillOutputSchema, } from './types.js';
2
+ import { listSkills, parseSkillsFlags, skillsDescription, viewSkillContent, } from './utils.js';
3
+ export const createViewSkillToolFactory = (options = {}) => async (ctx, mcpFlags) => {
4
+ const { octokit } = ctx;
5
+ const flags = parseSkillsFlags(mcpFlags.query);
6
+ return {
7
+ name: options.name || 'view',
8
+ disabled: typeof options.disabled === 'function'
9
+ ? await options.disabled(ctx, mcpFlags)
10
+ : options.disabled,
11
+ method: options.method || 'get',
12
+ route: options.route || '/view',
13
+ config: {
14
+ title: options.title || 'View Skill',
15
+ description: `${options.description || skillsDescription}${options.appendSkillsListToDescription
16
+ ? `\n\n## Available Skills\n\n${await listSkills({ octokit, flags })}`
17
+ : ''}`,
18
+ inputSchema: zViewSkillInputSchema,
19
+ outputSchema: zViewSkillOutputSchema,
20
+ },
21
+ fn: async ({ skill_name: name, path, }) => {
22
+ if (!name || name === '.') {
23
+ return {
24
+ content: await listSkills({ octokit, flags }),
25
+ };
26
+ }
27
+ return {
28
+ content: await viewSkillContent({
29
+ octokit,
30
+ flags,
31
+ name,
32
+ path,
33
+ }),
34
+ };
35
+ },
36
+ };
37
+ };
@@ -0,0 +1,363 @@
1
+ import type { Octokit } from '@octokit/rest';
2
+ import { z } from 'zod';
3
+ import type { InferSchema } from '../types.js';
4
+ export declare const zSkillType: z.ZodEnum<["local", "local_collection", "github", "github_collection"]>;
5
+ export type SkillType = z.infer<typeof zSkillType>;
6
+ export declare const zLocalSkillCfg: z.ZodObject<{
7
+ type: z.ZodLiteral<"local">;
8
+ path: z.ZodString;
9
+ }, "strip", z.ZodTypeAny, {
10
+ type: "local";
11
+ path: string;
12
+ }, {
13
+ type: "local";
14
+ path: string;
15
+ }>;
16
+ export type LocalSkillCfg = z.infer<typeof zLocalSkillCfg>;
17
+ declare const zCollectionFlagsCfg: z.ZodObject<{
18
+ enabled_skills: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
19
+ disabled_skills: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
20
+ ignored_paths: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
21
+ }, "strip", z.ZodTypeAny, {
22
+ enabled_skills?: string[] | undefined;
23
+ disabled_skills?: string[] | undefined;
24
+ ignored_paths?: string[] | undefined;
25
+ }, {
26
+ enabled_skills?: string[] | undefined;
27
+ disabled_skills?: string[] | undefined;
28
+ ignored_paths?: string[] | undefined;
29
+ }>;
30
+ export type CollectionFlagsCfg = z.infer<typeof zCollectionFlagsCfg>;
31
+ export interface CollectionFlags {
32
+ enabledSkills: Set<string> | null;
33
+ disabledSkills: Set<string> | null;
34
+ ignoredPaths: Set<string> | null;
35
+ }
36
+ export declare const zLocalCollectionSkillCfg: z.ZodObject<{
37
+ enabled_skills: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
38
+ disabled_skills: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
39
+ ignored_paths: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
40
+ } & {
41
+ type: z.ZodLiteral<"local_collection">;
42
+ path: z.ZodString;
43
+ }, "strip", z.ZodTypeAny, {
44
+ type: "local_collection";
45
+ path: string;
46
+ enabled_skills?: string[] | undefined;
47
+ disabled_skills?: string[] | undefined;
48
+ ignored_paths?: string[] | undefined;
49
+ }, {
50
+ type: "local_collection";
51
+ path: string;
52
+ enabled_skills?: string[] | undefined;
53
+ disabled_skills?: string[] | undefined;
54
+ ignored_paths?: string[] | undefined;
55
+ }>;
56
+ export type LocalCollectionSkillCfg = z.infer<typeof zLocalCollectionSkillCfg>;
57
+ export declare const zGitHubSkillCfg: z.ZodObject<{
58
+ type: z.ZodLiteral<"github">;
59
+ repo: z.ZodString;
60
+ path: z.ZodOptional<z.ZodString>;
61
+ }, "strip", z.ZodTypeAny, {
62
+ type: "github";
63
+ repo: string;
64
+ path?: string | undefined;
65
+ }, {
66
+ type: "github";
67
+ repo: string;
68
+ path?: string | undefined;
69
+ }>;
70
+ export type GitHubSkillCfg = z.infer<typeof zGitHubSkillCfg>;
71
+ export declare const zGitHubCollectionSkillCfg: z.ZodObject<{
72
+ enabled_skills: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
73
+ disabled_skills: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
74
+ ignored_paths: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
75
+ } & {
76
+ type: z.ZodLiteral<"github_collection">;
77
+ repo: z.ZodString;
78
+ path: z.ZodOptional<z.ZodString>;
79
+ }, "strip", z.ZodTypeAny, {
80
+ type: "github_collection";
81
+ repo: string;
82
+ path?: string | undefined;
83
+ enabled_skills?: string[] | undefined;
84
+ disabled_skills?: string[] | undefined;
85
+ ignored_paths?: string[] | undefined;
86
+ }, {
87
+ type: "github_collection";
88
+ repo: string;
89
+ path?: string | undefined;
90
+ enabled_skills?: string[] | undefined;
91
+ disabled_skills?: string[] | undefined;
92
+ ignored_paths?: string[] | undefined;
93
+ }>;
94
+ export type GitHubCollectionSkillCfg = z.infer<typeof zGitHubCollectionSkillCfg>;
95
+ export declare const zSkillCfg: z.ZodDiscriminatedUnion<"type", [z.ZodObject<{
96
+ type: z.ZodLiteral<"local">;
97
+ path: z.ZodString;
98
+ }, "strip", z.ZodTypeAny, {
99
+ type: "local";
100
+ path: string;
101
+ }, {
102
+ type: "local";
103
+ path: string;
104
+ }>, z.ZodObject<{
105
+ enabled_skills: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
106
+ disabled_skills: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
107
+ ignored_paths: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
108
+ } & {
109
+ type: z.ZodLiteral<"local_collection">;
110
+ path: z.ZodString;
111
+ }, "strip", z.ZodTypeAny, {
112
+ type: "local_collection";
113
+ path: string;
114
+ enabled_skills?: string[] | undefined;
115
+ disabled_skills?: string[] | undefined;
116
+ ignored_paths?: string[] | undefined;
117
+ }, {
118
+ type: "local_collection";
119
+ path: string;
120
+ enabled_skills?: string[] | undefined;
121
+ disabled_skills?: string[] | undefined;
122
+ ignored_paths?: string[] | undefined;
123
+ }>, z.ZodObject<{
124
+ type: z.ZodLiteral<"github">;
125
+ repo: z.ZodString;
126
+ path: z.ZodOptional<z.ZodString>;
127
+ }, "strip", z.ZodTypeAny, {
128
+ type: "github";
129
+ repo: string;
130
+ path?: string | undefined;
131
+ }, {
132
+ type: "github";
133
+ repo: string;
134
+ path?: string | undefined;
135
+ }>, z.ZodObject<{
136
+ enabled_skills: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
137
+ disabled_skills: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
138
+ ignored_paths: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
139
+ } & {
140
+ type: z.ZodLiteral<"github_collection">;
141
+ repo: z.ZodString;
142
+ path: z.ZodOptional<z.ZodString>;
143
+ }, "strip", z.ZodTypeAny, {
144
+ type: "github_collection";
145
+ repo: string;
146
+ path?: string | undefined;
147
+ enabled_skills?: string[] | undefined;
148
+ disabled_skills?: string[] | undefined;
149
+ ignored_paths?: string[] | undefined;
150
+ }, {
151
+ type: "github_collection";
152
+ repo: string;
153
+ path?: string | undefined;
154
+ enabled_skills?: string[] | undefined;
155
+ disabled_skills?: string[] | undefined;
156
+ ignored_paths?: string[] | undefined;
157
+ }>]>;
158
+ export type SkillCfg = z.infer<typeof zSkillCfg>;
159
+ export declare const zSkillCfgMap: z.ZodRecord<z.ZodString, z.ZodDiscriminatedUnion<"type", [z.ZodObject<{
160
+ type: z.ZodLiteral<"local">;
161
+ path: z.ZodString;
162
+ }, "strip", z.ZodTypeAny, {
163
+ type: "local";
164
+ path: string;
165
+ }, {
166
+ type: "local";
167
+ path: string;
168
+ }>, z.ZodObject<{
169
+ enabled_skills: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
170
+ disabled_skills: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
171
+ ignored_paths: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
172
+ } & {
173
+ type: z.ZodLiteral<"local_collection">;
174
+ path: z.ZodString;
175
+ }, "strip", z.ZodTypeAny, {
176
+ type: "local_collection";
177
+ path: string;
178
+ enabled_skills?: string[] | undefined;
179
+ disabled_skills?: string[] | undefined;
180
+ ignored_paths?: string[] | undefined;
181
+ }, {
182
+ type: "local_collection";
183
+ path: string;
184
+ enabled_skills?: string[] | undefined;
185
+ disabled_skills?: string[] | undefined;
186
+ ignored_paths?: string[] | undefined;
187
+ }>, z.ZodObject<{
188
+ type: z.ZodLiteral<"github">;
189
+ repo: z.ZodString;
190
+ path: z.ZodOptional<z.ZodString>;
191
+ }, "strip", z.ZodTypeAny, {
192
+ type: "github";
193
+ repo: string;
194
+ path?: string | undefined;
195
+ }, {
196
+ type: "github";
197
+ repo: string;
198
+ path?: string | undefined;
199
+ }>, z.ZodObject<{
200
+ enabled_skills: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
201
+ disabled_skills: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
202
+ ignored_paths: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
203
+ } & {
204
+ type: z.ZodLiteral<"github_collection">;
205
+ repo: z.ZodString;
206
+ path: z.ZodOptional<z.ZodString>;
207
+ }, "strip", z.ZodTypeAny, {
208
+ type: "github_collection";
209
+ repo: string;
210
+ path?: string | undefined;
211
+ enabled_skills?: string[] | undefined;
212
+ disabled_skills?: string[] | undefined;
213
+ ignored_paths?: string[] | undefined;
214
+ }, {
215
+ type: "github_collection";
216
+ repo: string;
217
+ path?: string | undefined;
218
+ enabled_skills?: string[] | undefined;
219
+ disabled_skills?: string[] | undefined;
220
+ ignored_paths?: string[] | undefined;
221
+ }>]>>;
222
+ export type SkillCfgMap = z.infer<typeof zSkillCfgMap>;
223
+ export declare const zSkillMatter: z.ZodObject<{
224
+ name: z.ZodString;
225
+ description: z.ZodString;
226
+ }, "strip", z.ZodTypeAny, {
227
+ description: string;
228
+ name: string;
229
+ }, {
230
+ description: string;
231
+ name: string;
232
+ }>;
233
+ export type SkillMatter = z.infer<typeof zSkillMatter>;
234
+ export declare const zLocalSkill: z.ZodObject<{
235
+ name: z.ZodString;
236
+ description: z.ZodString;
237
+ } & {
238
+ type: z.ZodLiteral<"local">;
239
+ path: z.ZodString;
240
+ }, "strip", z.ZodTypeAny, {
241
+ type: "local";
242
+ description: string;
243
+ name: string;
244
+ path: string;
245
+ }, {
246
+ type: "local";
247
+ description: string;
248
+ name: string;
249
+ path: string;
250
+ }>;
251
+ export type LocalSkill = z.infer<typeof zLocalSkill>;
252
+ export declare const zGitHubSkill: z.ZodObject<{
253
+ name: z.ZodString;
254
+ description: z.ZodString;
255
+ } & {
256
+ type: z.ZodLiteral<"github">;
257
+ repo: z.ZodString;
258
+ path: z.ZodOptional<z.ZodString>;
259
+ }, "strip", z.ZodTypeAny, {
260
+ type: "github";
261
+ description: string;
262
+ name: string;
263
+ repo: string;
264
+ path?: string | undefined;
265
+ }, {
266
+ type: "github";
267
+ description: string;
268
+ name: string;
269
+ repo: string;
270
+ path?: string | undefined;
271
+ }>;
272
+ export type GitHubSkill = z.infer<typeof zGitHubSkill>;
273
+ export declare const zSkill: z.ZodDiscriminatedUnion<"type", [z.ZodObject<{
274
+ name: z.ZodString;
275
+ description: z.ZodString;
276
+ } & {
277
+ type: z.ZodLiteral<"local">;
278
+ path: z.ZodString;
279
+ }, "strip", z.ZodTypeAny, {
280
+ type: "local";
281
+ description: string;
282
+ name: string;
283
+ path: string;
284
+ }, {
285
+ type: "local";
286
+ description: string;
287
+ name: string;
288
+ path: string;
289
+ }>, z.ZodObject<{
290
+ name: z.ZodString;
291
+ description: z.ZodString;
292
+ } & {
293
+ type: z.ZodLiteral<"github">;
294
+ repo: z.ZodString;
295
+ path: z.ZodOptional<z.ZodString>;
296
+ }, "strip", z.ZodTypeAny, {
297
+ type: "github";
298
+ description: string;
299
+ name: string;
300
+ repo: string;
301
+ path?: string | undefined;
302
+ }, {
303
+ type: "github";
304
+ description: string;
305
+ name: string;
306
+ repo: string;
307
+ path?: string | undefined;
308
+ }>]>;
309
+ export type Skill = z.infer<typeof zSkill>;
310
+ export declare const zSkillMap: z.ZodRecord<z.ZodString, z.ZodDiscriminatedUnion<"type", [z.ZodObject<{
311
+ name: z.ZodString;
312
+ description: z.ZodString;
313
+ } & {
314
+ type: z.ZodLiteral<"local">;
315
+ path: z.ZodString;
316
+ }, "strip", z.ZodTypeAny, {
317
+ type: "local";
318
+ description: string;
319
+ name: string;
320
+ path: string;
321
+ }, {
322
+ type: "local";
323
+ description: string;
324
+ name: string;
325
+ path: string;
326
+ }>, z.ZodObject<{
327
+ name: z.ZodString;
328
+ description: z.ZodString;
329
+ } & {
330
+ type: z.ZodLiteral<"github">;
331
+ repo: z.ZodString;
332
+ path: z.ZodOptional<z.ZodString>;
333
+ }, "strip", z.ZodTypeAny, {
334
+ type: "github";
335
+ description: string;
336
+ name: string;
337
+ repo: string;
338
+ path?: string | undefined;
339
+ }, {
340
+ type: "github";
341
+ description: string;
342
+ name: string;
343
+ repo: string;
344
+ path?: string | undefined;
345
+ }>]>>;
346
+ export type SkillMap = z.infer<typeof zSkillMap>;
347
+ export interface SkillsFlags {
348
+ enabledSkills?: Set<string> | null;
349
+ disabledSkills?: Set<string> | null;
350
+ }
351
+ export interface ServerContextWithOctokit extends Record<string, unknown> {
352
+ octokit?: Octokit | null;
353
+ }
354
+ export declare const zViewSkillInputSchema: {
355
+ readonly skill_name: z.ZodString;
356
+ readonly path: z.ZodString;
357
+ };
358
+ export type ViewSkillInputSchema = InferSchema<typeof zViewSkillInputSchema>;
359
+ export declare const zViewSkillOutputSchema: {
360
+ readonly content: z.ZodString;
361
+ };
362
+ export type ViewSkillOutputSchema = InferSchema<typeof zViewSkillOutputSchema>;
363
+ export {};
@@ -0,0 +1,58 @@
1
+ import { z } from 'zod';
2
+ export const zSkillType = z.enum([
3
+ 'local',
4
+ 'local_collection',
5
+ 'github',
6
+ 'github_collection',
7
+ ]);
8
+ export const zLocalSkillCfg = z.object({
9
+ type: z.literal('local'),
10
+ path: z.string(),
11
+ });
12
+ const zCollectionFlagsCfg = z.object({
13
+ enabled_skills: z.array(z.string()).optional(),
14
+ disabled_skills: z.array(z.string()).optional(),
15
+ ignored_paths: z.array(z.string()).optional(),
16
+ });
17
+ export const zLocalCollectionSkillCfg = zCollectionFlagsCfg.extend({
18
+ type: z.literal('local_collection'),
19
+ path: z.string(),
20
+ });
21
+ export const zGitHubSkillCfg = z.object({
22
+ type: z.literal('github'),
23
+ repo: z.string(),
24
+ path: z.string().optional(),
25
+ });
26
+ export const zGitHubCollectionSkillCfg = zCollectionFlagsCfg.extend({
27
+ type: z.literal('github_collection'),
28
+ repo: z.string(),
29
+ path: z.string().optional(),
30
+ });
31
+ export const zSkillCfg = z.discriminatedUnion('type', [
32
+ zLocalSkillCfg,
33
+ zLocalCollectionSkillCfg,
34
+ zGitHubSkillCfg,
35
+ zGitHubCollectionSkillCfg,
36
+ ]);
37
+ export const zSkillCfgMap = z.record(zSkillCfg);
38
+ export const zSkillMatter = z.object({
39
+ name: z.string().trim().min(1),
40
+ description: z.string(),
41
+ });
42
+ export const zLocalSkill = zSkillMatter.extend(zLocalSkillCfg.shape);
43
+ export const zGitHubSkill = zSkillMatter.extend(zGitHubSkillCfg.shape);
44
+ export const zSkill = z.discriminatedUnion('type', [zLocalSkill, zGitHubSkill]);
45
+ export const zSkillMap = z.record(zSkill);
46
+ export const zViewSkillInputSchema = {
47
+ skill_name: z
48
+ .string()
49
+ .describe('The name of the skill to browse, or `.` to list all available skills.'),
50
+ path: z.string().describe(`
51
+ A relative path to a file or directory within the skill to view.
52
+ If empty, will view the \`SKILL.md\` file by default.
53
+ Use \`.\` to list the root directory of the skill.
54
+ `.trim()),
55
+ };
56
+ export const zViewSkillOutputSchema = {
57
+ content: z.string().describe('The content of the file or directory listing.'),
58
+ };
@@ -0,0 +1,33 @@
1
+ import type { Octokit } from '@octokit/rest';
2
+ import { type McpFeatureFlags } from '@tigerdata/mcp-boilerplate';
3
+ import { type CollectionFlags, type CollectionFlagsCfg, type Skill, type SkillCfgMap, type SkillMatter, type SkillsFlags } from './types.js';
4
+ export declare const getSkillConfig: (configFilePath?: string) => Promise<SkillCfgMap>;
5
+ export declare const parseSkillFile: (fileContent: string) => Promise<{
6
+ matter: SkillMatter;
7
+ content: string;
8
+ }>;
9
+ export declare const loadSkills: ({ octokit, force, }?: {
10
+ octokit?: Octokit | null;
11
+ force?: boolean;
12
+ }) => Promise<Map<string, Skill>>;
13
+ export declare const resolveSkill: ({ name, octokit, flags, force, }: {
14
+ name: string;
15
+ octokit?: Octokit | null;
16
+ flags?: SkillsFlags;
17
+ force?: boolean;
18
+ }) => Promise<Skill | null>;
19
+ export declare const skillVisible: (name: string, flags: SkillsFlags) => boolean;
20
+ export declare const listSkills: ({ octokit, flags, force, }?: {
21
+ octokit?: Octokit | null;
22
+ flags?: SkillsFlags;
23
+ force?: boolean;
24
+ }) => Promise<string>;
25
+ export declare const viewSkillContent: ({ octokit, flags, name, path: passedPath, }: {
26
+ octokit?: Octokit | null;
27
+ flags?: SkillsFlags;
28
+ name: string;
29
+ path?: string;
30
+ }) => Promise<string>;
31
+ export declare const skillsDescription: string;
32
+ export declare const parseSkillsFlags: (query: McpFeatureFlags["query"]) => SkillsFlags;
33
+ export declare const parseCollectionFlags: (cfg: CollectionFlagsCfg) => CollectionFlags;
@@ -0,0 +1,404 @@
1
+ import { readdir, readFile, stat } from 'node:fs/promises';
2
+ import Path from 'node:path';
3
+ import { log, } from '@tigerdata/mcp-boilerplate';
4
+ import { encode } from '@toon-format/toon';
5
+ import matter from 'gray-matter';
6
+ import YAML from 'yaml';
7
+ import { zSkillCfgMap, zSkillMatter, } from './types.js';
8
+ const TTL = process.env.SKILLS_TTL
9
+ ? parseInt(process.env.SKILLS_TTL, 10)
10
+ : 5 * 60 * 1000;
11
+ let lastFetchCfg = 0;
12
+ let skillCfgMap = null;
13
+ export const getSkillConfig = async (configFilePath = process.env.SKILLS_FILE || './skills.yaml') => {
14
+ if (skillCfgMap && Date.now() - lastFetchCfg < TTL)
15
+ return skillCfgMap;
16
+ const data = await readFile(configFilePath, 'utf-8');
17
+ skillCfgMap = zSkillCfgMap.parse(YAML.parse(data));
18
+ lastFetchCfg = Date.now();
19
+ return skillCfgMap;
20
+ };
21
+ export const parseSkillFile = async (fileContent) => {
22
+ const { data, content } = matter(fileContent);
23
+ const skillMatter = zSkillMatter.parse(data);
24
+ if (!/^[a-zA-Z0-9-_]+$/.test(skillMatter.name)) {
25
+ const normalized = skillMatter.name
26
+ .toLowerCase()
27
+ .replace(/\s+/g, '-')
28
+ .replace(/[^a-z0-9-_]/g, '_')
29
+ .replace(/-[-_]+/g, '-')
30
+ .replace(/_[_-]+/g, '_')
31
+ .replace(/(^[-_]+)|([-_]+$)/g, '');
32
+ log.warn(`Skill name "${skillMatter.name}" contains invalid characters. Normalizing to "${normalized}".`);
33
+ skillMatter.name = normalized;
34
+ }
35
+ return {
36
+ matter: skillMatter,
37
+ content,
38
+ };
39
+ };
40
+ // skill name/path -> content
41
+ const skillContentCache = new Map();
42
+ let lastFetchSkillMap = 0;
43
+ let skillMap = null;
44
+ export const loadSkills = async ({ octokit, force = false, } = {}) => {
45
+ if (skillMap && !force && Date.now() - lastFetchSkillMap < TTL)
46
+ return skillMap;
47
+ skillMap = doLoadSkills(octokit).then((result) => {
48
+ lastFetchSkillMap = Date.now();
49
+ return result;
50
+ }, (err) => {
51
+ log.error('Failed to load skills, returning empty skill map', err);
52
+ skillMap = null;
53
+ return new Map();
54
+ });
55
+ return skillMap;
56
+ };
57
+ const doLoadSkills = async (octokit) => {
58
+ const skillCfgs = await getSkillConfig();
59
+ skillContentCache.clear();
60
+ const skills = new Map();
61
+ const alreadyExists = (name, path, description) => {
62
+ const existing = skills.get(name);
63
+ if (existing) {
64
+ log.warn(`Skill with name "${name}" already loaded from path "${existing.path}". Skipping duplicate at path "${path}".`, { existing, duplicate: { path, name, description } });
65
+ return true;
66
+ }
67
+ return false;
68
+ };
69
+ const shouldIgnorePath = (path, flags) => {
70
+ if (flags?.ignoredPaths?.has(path)) {
71
+ log.debug(`Ignoring path "${path}" in ignoredPaths`);
72
+ return true;
73
+ }
74
+ return false;
75
+ };
76
+ const shouldIgnoreSkill = (name, flags) => {
77
+ if (flags?.enabledSkills && !flags.enabledSkills.has(name)) {
78
+ log.debug(`Ignoring skill "${name}" not in enabledSkills`);
79
+ return true;
80
+ }
81
+ if (flags?.disabledSkills?.has(name)) {
82
+ log.debug(`Ignoring skill "${name}" in disabledSkills`);
83
+ return true;
84
+ }
85
+ return false;
86
+ };
87
+ const loadLocalPath = async (path, flags) => {
88
+ if (shouldIgnorePath(path, flags))
89
+ return;
90
+ const skillPath = `${path}/SKILL.md`;
91
+ try {
92
+ const fileContent = await readFile(skillPath, 'utf-8');
93
+ const { matter: { name, description }, content, } = await parseSkillFile(fileContent);
94
+ if (shouldIgnoreSkill(name, flags))
95
+ return;
96
+ if (alreadyExists(name, path, description))
97
+ return;
98
+ skills.set(name, {
99
+ type: 'local',
100
+ path,
101
+ name,
102
+ description,
103
+ });
104
+ skillContentCache.set(`${name}/SKILL.md`, content);
105
+ }
106
+ catch (err) {
107
+ log.error(`Failed to load skill at path: ${skillPath}`, err);
108
+ }
109
+ };
110
+ const loadGitHubPath = async (owner, repo, path, flags) => {
111
+ if (shouldIgnorePath(path, flags))
112
+ return;
113
+ const skillPath = `${path}/SKILL.md`;
114
+ if (!octokit) {
115
+ log.error(`Octokit instance is required to load GitHub skills: ${owner}/${repo}/${skillPath}`);
116
+ return;
117
+ }
118
+ try {
119
+ const skillFileResponse = await octokit.repos.getContent({
120
+ owner,
121
+ repo,
122
+ path: skillPath,
123
+ });
124
+ if (Array.isArray(skillFileResponse.data) ||
125
+ skillFileResponse.data.type !== 'file') {
126
+ log.error(`Expected SKILL.md to be a file`, null, {
127
+ owner,
128
+ repo,
129
+ path: skillPath,
130
+ });
131
+ return;
132
+ }
133
+ const fileContent = Buffer.from(skillFileResponse.data.content, 'base64').toString('utf-8');
134
+ const { matter: { name, description }, content, } = await parseSkillFile(fileContent);
135
+ if (shouldIgnoreSkill(name, flags))
136
+ return;
137
+ if (alreadyExists(name, path, description))
138
+ return;
139
+ skills.set(name, {
140
+ type: 'github',
141
+ repo: `${owner}/${repo}`,
142
+ path,
143
+ name,
144
+ description,
145
+ });
146
+ skillContentCache.set(`${name}/SKILL.md`, content);
147
+ }
148
+ catch (err) {
149
+ log.error(`Failed to load skill at GitHub path: ${owner}/${repo}/${skillPath}\n${err.message}`);
150
+ }
151
+ };
152
+ const promises = [];
153
+ await Promise.all(Object.entries(skillCfgs).map(async ([name, cfg]) => {
154
+ try {
155
+ switch (cfg.type) {
156
+ case 'local': {
157
+ promises.push(loadLocalPath(cfg.path));
158
+ break;
159
+ }
160
+ case 'local_collection': {
161
+ const dirEntries = await readdir(cfg.path, {
162
+ withFileTypes: true,
163
+ });
164
+ const flags = parseCollectionFlags(cfg);
165
+ for (const entry of dirEntries) {
166
+ if (entry.isFile())
167
+ continue;
168
+ if (!entry.isDirectory()) {
169
+ log.warn(`Skipping non-directory entry in local_collection`, {
170
+ path: `${cfg.path}/${entry.name}`,
171
+ });
172
+ continue;
173
+ }
174
+ promises.push(loadLocalPath(`${cfg.path}/${entry.name}`, flags));
175
+ }
176
+ break;
177
+ }
178
+ case 'github': {
179
+ const [owner, repo] = cfg.repo.split('/');
180
+ if (!owner || !repo) {
181
+ log.error(`Invalid GitHub repo format in skill config: ${cfg.repo}`, null, { name, repo: cfg.repo });
182
+ break;
183
+ }
184
+ promises.push(loadGitHubPath(owner, repo, cfg.path || '.'));
185
+ break;
186
+ }
187
+ case 'github_collection': {
188
+ const [owner, repo] = cfg.repo.split('/');
189
+ if (!owner || !repo) {
190
+ log.error(`Invalid GitHub repo format in skill config: ${cfg.repo}`, null, { name, repo: cfg.repo });
191
+ break;
192
+ }
193
+ if (!octokit) {
194
+ log.error(`Octokit instance is required to load GitHub collection skills: ${cfg.repo}`);
195
+ break;
196
+ }
197
+ const rootPath = cfg.path
198
+ ? cfg.path.replace(/(^\.?\/+)|(^\.$)|(\/\.$)/g, '')
199
+ : '';
200
+ const dirResponse = await octokit.repos.getContent({
201
+ owner,
202
+ repo,
203
+ path: rootPath,
204
+ });
205
+ if (!Array.isArray(dirResponse.data)) {
206
+ log.error(`Expected github_collection repo path to be a directory`, null, { name, owner, repo, path: cfg.path || '.' });
207
+ break;
208
+ }
209
+ const flags = parseCollectionFlags(cfg);
210
+ for (const entry of dirResponse.data) {
211
+ if (entry.type === 'file')
212
+ continue;
213
+ if (entry.type !== 'dir') {
214
+ log.warn(`Skipping non-directory entry in github_collection`, {
215
+ path: `${cfg.repo}/${entry.path}`,
216
+ type: entry.type,
217
+ });
218
+ continue;
219
+ }
220
+ promises.push(loadGitHubPath(owner, repo, entry.path, flags));
221
+ }
222
+ break;
223
+ }
224
+ default: {
225
+ // @ts-expect-error exhaustive check
226
+ throw new Error(`Unhandled skill config type: ${cfg.type}`);
227
+ }
228
+ }
229
+ }
230
+ catch (err) {
231
+ log.error(`Failed to load skill config "${name}"`, err);
232
+ }
233
+ }));
234
+ await Promise.all(promises);
235
+ // Sort skills by name
236
+ return new Map(Array.from(skills.entries()).sort((a, b) => a[0].localeCompare(b[0])));
237
+ };
238
+ export const resolveSkill = async ({ name, octokit, flags = {}, force = false, }) => {
239
+ if (!skillVisible(name, flags)) {
240
+ return null;
241
+ }
242
+ const skills = await loadSkills({ octokit, force });
243
+ return skills.get(name) || null;
244
+ };
245
+ export const skillVisible = (name, flags) => {
246
+ if (flags.enabledSkills && !flags.enabledSkills.has(name)) {
247
+ return false;
248
+ }
249
+ if (flags.disabledSkills?.has(name)) {
250
+ return false;
251
+ }
252
+ return true;
253
+ };
254
+ export const listSkills = async ({ octokit, flags = {}, force = false, } = {}) => {
255
+ const skills = await loadSkills({ octokit, force });
256
+ return `<available_skills>
257
+ ${encode([...skills.values()]
258
+ .filter((s) => skillVisible(s.name, flags))
259
+ .map((s) => ({
260
+ name: s.name,
261
+ description: s.description,
262
+ })), { delimiter: '\t' })}
263
+ </available_skills>`;
264
+ };
265
+ export const viewSkillContent = async ({ octokit, flags = {}, name, path: passedPath, }) => {
266
+ const skill = await resolveSkill({ octokit, flags, name });
267
+ if (!skill) {
268
+ throw new Error(`Skill not found: ${name}`);
269
+ }
270
+ const targetPath = passedPath || 'SKILL.md';
271
+ const cacheKey = `${name}/${normalizeSkillPath(targetPath)}`;
272
+ const cached = skillContentCache.get(cacheKey);
273
+ if (cached) {
274
+ return cached;
275
+ }
276
+ const content = await getSkillContent({ octokit, skill, path: targetPath });
277
+ skillContentCache.set(cacheKey, content);
278
+ return content;
279
+ };
280
+ const normalizeSkillPath = (path) => {
281
+ const normalizedPath = Path.posix
282
+ .normalize(
283
+ // treat \ as /
284
+ path.replace(/\\/g, '/'))
285
+ // strip leading ./ or /
286
+ .replace(/^(\.?\/)+/, '');
287
+ if (normalizedPath.split('/').some((s) => s === '..') ||
288
+ normalizedPath.includes('\0')) {
289
+ throw new Error(`Invalid path: ${path}`);
290
+ }
291
+ return normalizedPath;
292
+ };
293
+ const getSkillContent = async ({ skill, path: targetPath, octokit, }) => {
294
+ const normalizedPath = normalizeSkillPath(targetPath);
295
+ switch (skill.type) {
296
+ case 'local': {
297
+ const root = Path.resolve(skill.path);
298
+ const target = Path.resolve(Path.join(root, normalizedPath));
299
+ if (targetPath !== '.' && !target.startsWith(root)) {
300
+ throw new Error(`Invalid path: ${targetPath}`);
301
+ }
302
+ const s = await stat(target).catch(() => {
303
+ throw new Error(`Path not found: ${targetPath}`);
304
+ });
305
+ if (s.isDirectory()) {
306
+ const entries = await readdir(target, {
307
+ withFileTypes: true,
308
+ });
309
+ const listing = entries
310
+ .map((entry) => {
311
+ return `${entry.isDirectory() ? '📁' : '📄'} ${entry.name}`;
312
+ })
313
+ .join('\n');
314
+ return `Directory listing for ${skill.name}/${normalizedPath}:\n${listing}`;
315
+ }
316
+ else if (s.isFile()) {
317
+ return await readFile(target, 'utf-8');
318
+ }
319
+ else {
320
+ throw new Error(`Unsupported file type at path: ${target}`);
321
+ }
322
+ }
323
+ case 'github': {
324
+ const [owner, repo] = skill.repo.split('/');
325
+ if (!owner || !repo) {
326
+ throw new Error(`Invalid GitHub repo format in skill: ${skill.repo}`);
327
+ }
328
+ const path = `${skill.path || '.'}/${normalizedPath}`
329
+ .replace(/\/+/g, '/')
330
+ .replace(/(^\.?\/+)|(^\.$)|(\/\.$)/g, '');
331
+ if (!octokit) {
332
+ throw new Error(`Octokit instance is required to load GitHub skill content: ${owner}/${repo}/${path}`);
333
+ }
334
+ const response = await octokit.repos.getContent({
335
+ owner,
336
+ repo,
337
+ path,
338
+ });
339
+ if (Array.isArray(response.data)) {
340
+ // Directory listing
341
+ const listing = response.data
342
+ .map((entry) => {
343
+ return `${entry.type === 'dir' ? '📁' : '📄'} ${entry.name}`;
344
+ })
345
+ .join('\n');
346
+ return `Directory listing for ${skill.name}/${normalizedPath}:\n${listing}`;
347
+ }
348
+ if (response.data.type !== 'file') {
349
+ throw new Error(`Unsupported content type: ${response.data.type}`);
350
+ }
351
+ return Buffer.from(response.data.content, 'base64').toString('utf-8');
352
+ }
353
+ default: {
354
+ // @ts-expect-error exhaustive check
355
+ throw new Error(`Unhandled skill type: ${skill.type}`);
356
+ }
357
+ }
358
+ };
359
+ export const skillsDescription = `
360
+ # Skills
361
+
362
+ This tool provides access to domain-specific skills - structured knowledge and procedures for specialized tasks.
363
+
364
+ ## How to Use Skills
365
+
366
+ 1. **Discover**: If you have not been provided the list of skills, fetch them by invoking this tool with \`name: "."\`
367
+ 2. **Read**: Access a skill by reading its SKILL.md file: \`name: "skill-name", path: "SKILL.md"\`
368
+ 3. **Explore**: Navigate within the skill directory to find additional resources, examples, or templates.
369
+ The SKILL.md file and other documents may contain relative links to guide you.
370
+ You can list the content of directories by specifying the directory path, relative to the skill root.
371
+ 4. **Apply**: Follow the procedures and reference the knowledge in the skill to complete your task
372
+
373
+ ## Skill Structure
374
+
375
+ Each skill contains:
376
+ - **SKILL.md**: Main documentation (always start here)
377
+ - **REFERENCE.md**: Quick reference card (optional)
378
+ - Additional files: Examples, templates, scripts
379
+
380
+ ## When to Use Skills
381
+
382
+ - Read relevant skills proactively when you identify a task that matches a skill's domain
383
+ - Skills are meant to augment your knowledge, not replace your reasoning
384
+ - Apply skill guidance while adapting to the specific user request
385
+ - Skills use progressive disclosure - start with high-level guidance and drill down only as needed
386
+ - Skills may also refer to tools or resources external to the skill itself
387
+ - Use other tools to execute any code or scripts provided by the skill
388
+ `.trim();
389
+ const toSet = (flag) => flag
390
+ ? Array.isArray(flag)
391
+ ? new Set(flag)
392
+ : typeof flag === 'string'
393
+ ? new Set(flag.split(',').map((s) => s.trim()))
394
+ : null
395
+ : null;
396
+ export const parseSkillsFlags = (query) => ({
397
+ enabledSkills: toSet(query?.enabled_skills),
398
+ disabledSkills: toSet(query?.disabled_skills),
399
+ });
400
+ export const parseCollectionFlags = (cfg) => ({
401
+ enabledSkills: toSet(cfg.enabled_skills),
402
+ disabledSkills: toSet(cfg.disabled_skills),
403
+ ignoredPaths: toSet(cfg.ignored_paths),
404
+ });
package/dist/stdio.js CHANGED
@@ -6,7 +6,7 @@ export const stdioServerFactory = async ({ name, version, context, apiFactories,
6
6
  try {
7
7
  console.error('Starting default (STDIO) server...');
8
8
  const transport = new StdioServerTransport();
9
- const { server } = mcpServerFactory({
9
+ const { server } = await mcpServerFactory({
10
10
  name,
11
11
  version,
12
12
  context,
package/dist/types.d.ts CHANGED
@@ -18,7 +18,7 @@ export interface BaseApiDefinition {
18
18
  fn(args: Record<string, unknown>): Promise<Record<string, unknown>>;
19
19
  pickResult?(result: Record<string, unknown>): unknown;
20
20
  }
21
- export type BaseApiFactory<Context extends Record<string, unknown>> = (ctx: Context, featureFlags: McpFeatureFlags) => BaseApiDefinition;
21
+ export type BaseApiFactory<Context extends Record<string, unknown>> = (ctx: Context, featureFlags: McpFeatureFlags) => BaseApiDefinition | Promise<BaseApiDefinition>;
22
22
  export type BasePromptConfig = {
23
23
  title?: string;
24
24
  description?: string;
@@ -30,7 +30,7 @@ export interface BasePromptDefinition {
30
30
  disabled?: boolean;
31
31
  fn(args: Record<string, unknown>): Promise<GetPromptResult>;
32
32
  }
33
- export type BasePromptFactory<Context extends Record<string, unknown>> = (ctx: Context, featureFlags: McpFeatureFlags) => BasePromptDefinition;
33
+ export type BasePromptFactory<Context extends Record<string, unknown>> = (ctx: Context, featureFlags: McpFeatureFlags) => BasePromptDefinition | Promise<BasePromptDefinition>;
34
34
  export type ToolConfig<InputArgs extends ZodRawShape, OutputArgs extends ZodRawShape> = {
35
35
  title?: string;
36
36
  description?: string;
@@ -43,7 +43,7 @@ export interface ApiDefinition<InputArgs extends ZodRawShape, OutputArgs extends
43
43
  fn(args: z.objectOutputType<InputArgs, ZodTypeAny>): Promise<z.objectOutputType<OutputArgs, ZodTypeAny>>;
44
44
  pickResult?(result: z.objectOutputType<OutputArgs, ZodTypeAny>): SimplifiedOutputArgs;
45
45
  }
46
- export type ApiFactory<Context extends Record<string, unknown>, Input extends ZodRawShape, Output extends ZodRawShape, RestOutput = Output> = (ctx: Context, featureFlags: McpFeatureFlags) => ApiDefinition<Input, Output, RestOutput>;
46
+ export type ApiFactory<Context extends Record<string, unknown>, Input extends ZodRawShape, Output extends ZodRawShape, RestOutput = Output> = (ctx: Context, featureFlags: McpFeatureFlags) => ApiDefinition<Input, Output, RestOutput> | Promise<ApiDefinition<Input, Output, RestOutput>>;
47
47
  export type RouterFactoryResult = [Router, () => void | Promise<void>];
48
48
  export type PromptConfig<InputArgs extends ZodRawShape> = {
49
49
  title?: string;
@@ -76,7 +76,7 @@ export interface StaticResourceDefinition {
76
76
  read: ReadResourceCallback;
77
77
  }
78
78
  export type ResourceDefinition = TemplatedResourceDefinition | StaticResourceDefinition;
79
- export type ResourceFactory<Context extends Record<string, unknown>> = (ctx: Context, featureFlags: McpFeatureFlags) => ResourceDefinition;
79
+ export type ResourceFactory<Context extends Record<string, unknown>> = (ctx: Context, featureFlags: McpFeatureFlags) => ResourceDefinition | Promise<ResourceDefinition>;
80
80
  export interface ParsedQs {
81
81
  [key: string]: undefined | string | ParsedQs | (string | ParsedQs)[];
82
82
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tigerdata/mcp-boilerplate",
3
- "version": "0.9.2",
3
+ "version": "1.0.0",
4
4
  "description": "MCP boilerplate code for Node.js",
5
5
  "license": "Apache-2.0",
6
6
  "author": "TigerData",
@@ -24,6 +24,10 @@
24
24
  "./instrumentation": {
25
25
  "import": "./dist/instrumentation.js",
26
26
  "types": "./dist/instrumentation.d.ts"
27
+ },
28
+ "./skills": {
29
+ "import": "./dist/skills/index.js",
30
+ "types": "./dist/skills/index.d.ts"
27
31
  }
28
32
  },
29
33
  "files": [
@@ -36,7 +40,7 @@
36
40
  "lint": "./bun x @biomejs/biome check"
37
41
  },
38
42
  "dependencies": {
39
- "@mcp-use/inspector": "^0.13.2",
43
+ "@mcp-use/inspector": "^0.14.0",
40
44
  "@modelcontextprotocol/sdk": "^1.25.1",
41
45
  "@opentelemetry/api": "^1.9.0",
42
46
  "@opentelemetry/auto-instrumentations-node": "^0.67.3",
@@ -46,17 +50,21 @@
46
50
  "@opentelemetry/sdk-node": "^0.208.0",
47
51
  "@opentelemetry/sdk-trace-node": "^2.2.0",
48
52
  "@opentelemetry/semantic-conventions": "^1.38.0",
53
+ "@toon-format/toon": "^2.1.0",
49
54
  "express": "^5.2.1",
55
+ "gray-matter": "^4.0.3",
50
56
  "raw-body": "^3.0.2",
57
+ "yaml": "^2.8.2",
51
58
  "zod": "^3.23.8"
52
59
  },
53
60
  "devDependencies": {
54
- "@biomejs/biome": "^2.3.10",
61
+ "@biomejs/biome": "^2.3.11",
62
+ "@octokit/rest": "^22.0.1",
55
63
  "@types/bun": "^1.3.5",
56
64
  "@types/express": "^5.0.6",
57
65
  "@types/node": "^22.19.3",
58
- "@typescript-eslint/typescript-estree": "^8.50.0",
59
- "ai": "^5.0.115",
66
+ "@typescript-eslint/typescript-estree": "^8.52.0",
67
+ "ai": "^5.0.118",
60
68
  "eslint": "^9.39.2",
61
69
  "typescript": "^5.9.3"
62
70
  },