wm-create-mcp-server 0.1.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.
Files changed (47) hide show
  1. package/README.md +137 -0
  2. package/dist/index.js +390 -0
  3. package/dist/index.js.map +1 -0
  4. package/package.json +44 -0
  5. package/src/templates/full/Dockerfile +15 -0
  6. package/src/templates/full/README.md +76 -0
  7. package/src/templates/full/_dot_env.example +3 -0
  8. package/src/templates/full/_dot_gitignore +5 -0
  9. package/src/templates/full/_dot_vscode/extensions.json +7 -0
  10. package/src/templates/full/docker-compose.yml +8 -0
  11. package/src/templates/full/package.json +35 -0
  12. package/src/templates/full/src/auth/index.ts +24 -0
  13. package/src/templates/full/src/index.sse.ts +58 -0
  14. package/src/templates/full/src/index.ts +6 -0
  15. package/src/templates/full/src/lib/tool-builder.ts +45 -0
  16. package/src/templates/full/src/prompts/index.ts +41 -0
  17. package/src/templates/full/src/resources/index.ts +37 -0
  18. package/src/templates/full/src/server.ts +18 -0
  19. package/src/templates/full/src/tools/__tests__/echo.test.ts +14 -0
  20. package/src/templates/full/src/tools/__tests__/get-weather.test.ts +16 -0
  21. package/src/templates/full/src/tools/echo.ts +11 -0
  22. package/src/templates/full/src/tools/get-weather.ts +24 -0
  23. package/src/templates/full/src/tools/index.ts +9 -0
  24. package/src/templates/full/tsconfig.json +14 -0
  25. package/src/templates/full/vitest.config.ts +7 -0
  26. package/src/templates/minimal/README.md +41 -0
  27. package/src/templates/minimal/_dot_gitignore +5 -0
  28. package/src/templates/minimal/package.json +24 -0
  29. package/src/templates/minimal/src/index.ts +21 -0
  30. package/src/templates/minimal/tsconfig.json +12 -0
  31. package/src/templates/standard/.github/workflows/test.yml +20 -0
  32. package/src/templates/standard/README.md +83 -0
  33. package/src/templates/standard/_dot_env.example +3 -0
  34. package/src/templates/standard/_dot_gitignore +5 -0
  35. package/src/templates/standard/_dot_vscode/extensions.json +7 -0
  36. package/src/templates/standard/package.json +32 -0
  37. package/src/templates/standard/src/index.sse.ts +58 -0
  38. package/src/templates/standard/src/index.ts +6 -0
  39. package/src/templates/standard/src/lib/tool-builder.ts +45 -0
  40. package/src/templates/standard/src/server.ts +14 -0
  41. package/src/templates/standard/src/tools/__tests__/echo.test.ts +14 -0
  42. package/src/templates/standard/src/tools/__tests__/get-weather.test.ts +16 -0
  43. package/src/templates/standard/src/tools/echo.ts +11 -0
  44. package/src/templates/standard/src/tools/get-weather.ts +24 -0
  45. package/src/templates/standard/src/tools/index.ts +9 -0
  46. package/src/templates/standard/tsconfig.json +14 -0
  47. package/src/templates/standard/vitest.config.ts +7 -0
@@ -0,0 +1,76 @@
1
+ # {{PROJECT_NAME}}
2
+
3
+ An MCP server built with [create-mcp-server](https://github.com/workingmodel/wm-create-mcp-server).
4
+
5
+ ## Commands
6
+
7
+ ```bash
8
+ npm run dev # start server in watch mode
9
+ npm run inspector # build + open MCP Inspector UI
10
+ npm run test # run unit tests
11
+ npm run validate # typecheck + tests
12
+ npm run build # compile to dist/
13
+ npm run docker:build # build Docker image
14
+ npm run docker:run # start via docker compose
15
+ ```
16
+
17
+ ## Project structure
18
+
19
+ ```
20
+ src/
21
+ tools/
22
+ index.ts ← register all tools here
23
+ echo.ts ← example: echo tool
24
+ get-weather.ts ← example: weather tool (stub)
25
+ __tests__/ ← vitest unit tests per tool
26
+ resources/
27
+ index.ts ← MCP resource definitions
28
+ prompts/
29
+ index.ts ← MCP prompt templates
30
+ auth/
31
+ index.ts ← API key auth stub
32
+ lib/
33
+ tool-builder.ts ← defineTool() + registerTools()
34
+ server.ts ← MCP server setup
35
+ index.ts ← transport entry point
36
+ ```
37
+
38
+ ## Adding a tool
39
+
40
+ 1. Create `src/tools/my-tool.ts`:
41
+
42
+ ```ts
43
+ import { z } from "zod";
44
+ import { defineTool, text } from "../lib/tool-builder.js";
45
+
46
+ export const myTool = defineTool({
47
+ name: "my-tool",
48
+ description: "What this tool does",
49
+ input: z.object({
50
+ param: z.string().describe("Description of param"),
51
+ }),
52
+ handler: async ({ param }) => text(`Result: ${param}`),
53
+ });
54
+ ```
55
+
56
+ 2. Add to `src/tools/index.ts`:
57
+
58
+ ```ts
59
+ import { myTool } from "./my-tool.js";
60
+ export const tools = [...existingTools, myTool];
61
+ ```
62
+
63
+ ## Connecting to Claude Desktop
64
+
65
+ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
66
+
67
+ ```json
68
+ {
69
+ "mcpServers": {
70
+ "{{PROJECT_NAME}}": {
71
+ "command": "node",
72
+ "args": ["/absolute/path/to/{{PROJECT_NAME}}/dist/index.js"]
73
+ }
74
+ }
75
+ }
76
+ ```
@@ -0,0 +1,3 @@
1
+ # Copy this file to .env and fill in values
2
+ # Add your API keys and config here
3
+ # SOME_API_KEY=your_key_here
@@ -0,0 +1,5 @@
1
+ node_modules/
2
+ dist/
3
+ *.tsbuildinfo
4
+ .env
5
+ .DS_Store
@@ -0,0 +1,7 @@
1
+ {
2
+ "recommendations": [
3
+ "dbaeumer.vscode-eslint",
4
+ "esbenp.prettier-vscode",
5
+ "vitest.explorer"
6
+ ]
7
+ }
@@ -0,0 +1,8 @@
1
+ services:
2
+ mcp-server:
3
+ build: .
4
+ ports:
5
+ - "3000:3000"
6
+ env_file:
7
+ - .env
8
+ restart: unless-stopped
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "{{PROJECT_NAME}}",
3
+ "version": "{{VERSION}}",
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "tsx --watch src/index.ts",
7
+ "dev:sse": "tsx --watch src/index.sse.ts",
8
+ "build": "tsup src/index.ts src/index.sse.ts --format esm --out-dir dist",
9
+ "start": "node dist/index.js",
10
+ "start:sse": "node dist/index.sse.js",
11
+ "typecheck": "tsc --noEmit",
12
+ "test": "vitest run",
13
+ "test:watch": "vitest",
14
+ "inspector": "npm run build && npx @modelcontextprotocol/inspector node dist/index.js",
15
+ "validate": "npm run typecheck && npm run test",
16
+ "docker:build": "docker build -t {{PROJECT_NAME}} .",
17
+ "docker:run": "docker compose up"
18
+ },
19
+ "dependencies": {
20
+ "@modelcontextprotocol/sdk": "^1.0.0",
21
+ "zod": "^3.23.0",
22
+ "zod-to-json-schema": "^3.23.0"
23
+ },
24
+ "devDependencies": {
25
+ "@modelcontextprotocol/inspector": "^0.6.0",
26
+ "@types/node": "^20.0.0",
27
+ "tsup": "^8.0.0",
28
+ "tsx": "^4.0.0",
29
+ "typescript": "^5.4.0",
30
+ "vitest": "^1.6.0"
31
+ },
32
+ "engines": {
33
+ "node": ">=18.0.0"
34
+ }
35
+ }
@@ -0,0 +1,24 @@
1
+ // Auth stub — wire this to your actual auth layer.
2
+ // Currently validates a static API key from the environment.
3
+ // Replace with OAuth2, JWT, or your preferred auth mechanism.
4
+
5
+ const VALID_API_KEY = process.env.API_KEY;
6
+
7
+ export interface AuthResult {
8
+ ok: boolean;
9
+ reason?: string;
10
+ }
11
+
12
+ export function validateApiKey(providedKey: string | undefined): AuthResult {
13
+ if (!VALID_API_KEY) {
14
+ // No key configured → auth disabled (open access)
15
+ return { ok: true };
16
+ }
17
+ if (!providedKey) {
18
+ return { ok: false, reason: "Missing API key" };
19
+ }
20
+ if (providedKey !== VALID_API_KEY) {
21
+ return { ok: false, reason: "Invalid API key" };
22
+ }
23
+ return { ok: true };
24
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * SSE transport entry point — for remote HTTP clients (Cursor, Windsurf, web apps).
3
+ *
4
+ * Usage:
5
+ * npm run dev:sse # dev mode
6
+ * npm run start:sse # production
7
+ *
8
+ * Connect your MCP client to: http://localhost:PORT/sse
9
+ */
10
+ import { createServer as createHttpServer, IncomingMessage, ServerResponse } from "node:http";
11
+ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
12
+ import { createServer } from "./server.js";
13
+
14
+ const PORT = Number(process.env.PORT ?? 3000);
15
+
16
+ const mcpServer = createServer();
17
+
18
+ // Map sessionId → transport so POST /messages can route to the right connection
19
+ const transports = new Map<string, SSEServerTransport>();
20
+
21
+ const httpServer = createHttpServer(async (req: IncomingMessage, res: ServerResponse) => {
22
+ const url = new URL(req.url ?? "/", `http://localhost:${PORT}`);
23
+
24
+ // Client opens SSE stream
25
+ if (req.method === "GET" && url.pathname === "/sse") {
26
+ const transport = new SSEServerTransport("/messages", res);
27
+ transports.set(transport.sessionId, transport);
28
+
29
+ res.on("close", () => {
30
+ transports.delete(transport.sessionId);
31
+ });
32
+
33
+ await mcpServer.connect(transport);
34
+ return;
35
+ }
36
+
37
+ // Client posts a message to an established session
38
+ if (req.method === "POST" && url.pathname === "/messages") {
39
+ const sessionId = url.searchParams.get("sessionId") ?? "";
40
+ const transport = transports.get(sessionId);
41
+
42
+ if (!transport) {
43
+ res.writeHead(404, { "Content-Type": "text/plain" });
44
+ res.end("Session not found");
45
+ return;
46
+ }
47
+
48
+ await transport.handlePostMessage(req, res);
49
+ return;
50
+ }
51
+
52
+ res.writeHead(404, { "Content-Type": "text/plain" });
53
+ res.end("Not found");
54
+ });
55
+
56
+ httpServer.listen(PORT, () => {
57
+ console.log(`{{PROJECT_NAME}} MCP server (SSE) listening on http://localhost:${PORT}/sse`);
58
+ });
@@ -0,0 +1,6 @@
1
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2
+ import { createServer } from "./server.js";
3
+
4
+ const server = createServer();
5
+ const transport = new StdioServerTransport();
6
+ await server.connect(transport);
@@ -0,0 +1,45 @@
1
+ import { z } from "zod";
2
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+
4
+ export interface ToolResult {
5
+ content: Array<
6
+ | { type: "text"; text: string }
7
+ | { type: "image"; data: string; mimeType: string }
8
+ >;
9
+ isError?: boolean;
10
+ [key: string]: unknown;
11
+ }
12
+
13
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
14
+ export interface ToolDefinition<TInput extends z.ZodRawShape = any> {
15
+ name: string;
16
+ description: string;
17
+ input: z.ZodObject<TInput>;
18
+ handler: (input: z.infer<z.ZodObject<TInput>>) => Promise<ToolResult>;
19
+ }
20
+
21
+ export function defineTool<TInput extends z.ZodRawShape>(
22
+ definition: ToolDefinition<TInput>
23
+ ): ToolDefinition<TInput> {
24
+ return definition;
25
+ }
26
+
27
+ export function registerTools(server: McpServer, tools: ToolDefinition[]) {
28
+ for (const tool of tools) {
29
+ server.tool(
30
+ tool.name,
31
+ tool.description,
32
+ tool.input.shape,
33
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
34
+ async (input: any) => tool.handler(input)
35
+ );
36
+ }
37
+ }
38
+
39
+ export function text(content: string): ToolResult {
40
+ return { content: [{ type: "text", text: content }] };
41
+ }
42
+
43
+ export function error(message: string): ToolResult {
44
+ return { content: [{ type: "text", text: message }], isError: true };
45
+ }
@@ -0,0 +1,41 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+
4
+ export function registerPrompts(server: McpServer) {
5
+ server.prompt(
6
+ "summarize",
7
+ "Summarize a piece of text concisely",
8
+ { text: z.string().describe("The text to summarize") },
9
+ ({ text }) => ({
10
+ messages: [
11
+ {
12
+ role: "user",
13
+ content: {
14
+ type: "text",
15
+ text: `Please summarize the following text in 2-3 sentences:\n\n${text}`,
16
+ },
17
+ },
18
+ ],
19
+ })
20
+ );
21
+
22
+ server.prompt(
23
+ "improve-writing",
24
+ "Improve the clarity and style of a piece of writing",
25
+ {
26
+ text: z.string().describe("The text to improve"),
27
+ tone: z.enum(["professional", "casual", "concise"]).default("professional"),
28
+ },
29
+ ({ text, tone }) => ({
30
+ messages: [
31
+ {
32
+ role: "user",
33
+ content: {
34
+ type: "text",
35
+ text: `Rewrite the following text with a ${tone} tone, improving clarity and style:\n\n${text}`,
36
+ },
37
+ },
38
+ ],
39
+ })
40
+ );
41
+ }
@@ -0,0 +1,37 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+
4
+ export function registerResources(server: McpServer) {
5
+ // Static resource example
6
+ server.resource(
7
+ "config",
8
+ "mcp://{{PROJECT_NAME}}/config",
9
+ async (uri) => ({
10
+ contents: [
11
+ {
12
+ uri: uri.href,
13
+ mimeType: "application/json",
14
+ text: JSON.stringify({ name: "{{PROJECT_NAME}}", version: "{{VERSION}}" }, null, 2),
15
+ },
16
+ ],
17
+ })
18
+ );
19
+
20
+ // Dynamic resource example (parameterized URI template)
21
+ server.resource(
22
+ "item",
23
+ new ResourceTemplate("mcp://{{PROJECT_NAME}}/items/{id}", { list: undefined }),
24
+ async (uri) => {
25
+ const id = uri.pathname.split("/").pop();
26
+ return {
27
+ contents: [
28
+ {
29
+ uri: uri.href,
30
+ mimeType: "text/plain",
31
+ text: `Item ${id} content here. Replace with real data lookup.`,
32
+ },
33
+ ],
34
+ };
35
+ }
36
+ );
37
+ }
@@ -0,0 +1,18 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { registerTools } from "./lib/tool-builder.js";
3
+ import { tools } from "./tools/index.js";
4
+ import { registerResources } from "./resources/index.js";
5
+ import { registerPrompts } from "./prompts/index.js";
6
+
7
+ export function createServer(): McpServer {
8
+ const server = new McpServer({
9
+ name: "{{PROJECT_NAME}}",
10
+ version: "{{VERSION}}",
11
+ });
12
+
13
+ registerTools(server, tools);
14
+ registerResources(server);
15
+ registerPrompts(server);
16
+
17
+ return server;
18
+ }
@@ -0,0 +1,14 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { echoTool } from "../echo.js";
3
+
4
+ describe("echo tool", () => {
5
+ it("returns the input message", async () => {
6
+ const result = await echoTool.handler({ message: "hello world" });
7
+ expect(result.content[0]).toEqual({ type: "text", text: "hello world" });
8
+ });
9
+
10
+ it("handles empty string", async () => {
11
+ const result = await echoTool.handler({ message: "" });
12
+ expect(result.content[0]).toEqual({ type: "text", text: "" });
13
+ });
14
+ });
@@ -0,0 +1,16 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { getWeatherTool } from "../get-weather.js";
3
+
4
+ describe("get-weather tool", () => {
5
+ it("returns weather for a location in celsius", async () => {
6
+ const result = await getWeatherTool.handler({ location: "London", units: "celsius" });
7
+ expect(result.content[0].type).toBe("text");
8
+ expect((result.content[0] as { type: "text"; text: string }).text).toContain("London");
9
+ expect((result.content[0] as { type: "text"; text: string }).text).toContain("°C");
10
+ });
11
+
12
+ it("returns weather in fahrenheit", async () => {
13
+ const result = await getWeatherTool.handler({ location: "New York", units: "fahrenheit" });
14
+ expect((result.content[0] as { type: "text"; text: string }).text).toContain("°F");
15
+ });
16
+ });
@@ -0,0 +1,11 @@
1
+ import { z } from "zod";
2
+ import { defineTool, text } from "../lib/tool-builder.js";
3
+
4
+ export const echoTool = defineTool({
5
+ name: "echo",
6
+ description: "Returns the input message unchanged. Useful for testing your server connection.",
7
+ input: z.object({
8
+ message: z.string().describe("The message to echo back"),
9
+ }),
10
+ handler: async ({ message }) => text(message),
11
+ });
@@ -0,0 +1,24 @@
1
+ import { z } from "zod";
2
+ import { defineTool, text, error } from "../lib/tool-builder.js";
3
+
4
+ // Example tool showing a realistic pattern: validated input, error handling, typed output.
5
+ // Replace with your own logic or delete this file.
6
+ export const getWeatherTool = defineTool({
7
+ name: "get-weather",
8
+ description: "Get the current weather for a city. Returns temperature and conditions.",
9
+ input: z.object({
10
+ location: z.string().describe("City name, e.g. 'New York' or 'London, UK'"),
11
+ units: z
12
+ .enum(["celsius", "fahrenheit"])
13
+ .default("celsius")
14
+ .describe("Temperature unit"),
15
+ }),
16
+ handler: async ({ location, units }) => {
17
+ // TODO: Replace with a real weather API call
18
+ // e.g. const data = await fetch(`https://api.openweathermap.org/...`)
19
+
20
+ // Stub response for demonstration
21
+ const temp = units === "celsius" ? "22°C" : "72°F";
22
+ return text(`Weather in ${location}: ${temp}, partly cloudy.`);
23
+ },
24
+ });
@@ -0,0 +1,9 @@
1
+ import type { ToolDefinition } from "../lib/tool-builder.js";
2
+ import { echoTool } from "./echo.js";
3
+ import { getWeatherTool } from "./get-weather.js";
4
+
5
+ // Register all tools here. Import and add to this array.
6
+ export const tools: ToolDefinition[] = [
7
+ echoTool,
8
+ getWeatherTool,
9
+ ];
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "outDir": "dist",
10
+ "baseUrl": ".",
11
+ "paths": { "@/*": ["src/*"] }
12
+ },
13
+ "include": ["src"]
14
+ }
@@ -0,0 +1,7 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ include: ["src/**/__tests__/**/*.test.ts"],
6
+ },
7
+ });
@@ -0,0 +1,41 @@
1
+ # {{PROJECT_NAME}}
2
+
3
+ An MCP server built with [create-mcp-server](https://github.com/workingmodel/create-mcp-server).
4
+
5
+ ## Getting started
6
+
7
+ ```bash
8
+ npm run dev # start server in watch mode (stdio transport)
9
+ npm run build # compile to dist/
10
+ npm run start # run compiled server
11
+ ```
12
+
13
+ ## Adding tools
14
+
15
+ Edit `src/index.ts` and add a new `server.tool()` call:
16
+
17
+ ```ts
18
+ server.tool(
19
+ "my-tool",
20
+ "What this tool does",
21
+ { param: z.string() },
22
+ async ({ param }) => ({
23
+ content: [{ type: "text", text: `Result: ${param}` }],
24
+ })
25
+ );
26
+ ```
27
+
28
+ ## Connecting to Claude Desktop
29
+
30
+ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
31
+
32
+ ```json
33
+ {
34
+ "mcpServers": {
35
+ "{{PROJECT_NAME}}": {
36
+ "command": "node",
37
+ "args": ["/absolute/path/to/{{PROJECT_NAME}}/dist/index.js"]
38
+ }
39
+ }
40
+ }
41
+ ```
@@ -0,0 +1,5 @@
1
+ node_modules/
2
+ dist/
3
+ *.tsbuildinfo
4
+ .env
5
+ .DS_Store
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "{{PROJECT_NAME}}",
3
+ "version": "{{VERSION}}",
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "tsx --watch src/index.ts",
7
+ "build": "tsup src/index.ts --format esm --out-dir dist",
8
+ "start": "node dist/index.js",
9
+ "typecheck": "tsc --noEmit"
10
+ },
11
+ "dependencies": {
12
+ "@modelcontextprotocol/sdk": "^1.0.0",
13
+ "zod": "^3.23.0"
14
+ },
15
+ "devDependencies": {
16
+ "@types/node": "^20.0.0",
17
+ "tsup": "^8.0.0",
18
+ "tsx": "^4.0.0",
19
+ "typescript": "^5.4.0"
20
+ },
21
+ "engines": {
22
+ "node": ">=18.0.0"
23
+ }
24
+ }
@@ -0,0 +1,21 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { z } from "zod";
4
+
5
+ const server = new McpServer({
6
+ name: "{{PROJECT_NAME}}",
7
+ version: "{{VERSION}}",
8
+ });
9
+
10
+ // Example tool — replace or extend this
11
+ server.tool(
12
+ "echo",
13
+ "Returns the input message unchanged",
14
+ { message: z.string().describe("The message to echo back") },
15
+ async ({ message }) => ({
16
+ content: [{ type: "text", text: message }],
17
+ })
18
+ );
19
+
20
+ const transport = new StdioServerTransport();
21
+ await server.connect(transport);
@@ -0,0 +1,12 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "outDir": "dist"
10
+ },
11
+ "include": ["src"]
12
+ }
@@ -0,0 +1,20 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main, dev]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - uses: actions/setup-node@v4
15
+ with:
16
+ node-version: 20
17
+ cache: npm
18
+ - run: npm install
19
+ - run: npm run typecheck
20
+ - run: npm test