tkeron 4.0.0-beta.16 → 4.0.0-beta.18

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/changelog.md CHANGED
@@ -1,3 +1,73 @@
1
+ # v4.0.0-beta.18
2
+
3
+ ## Code Quality & Architecture
4
+
5
+ ### Bug Fixes
6
+
7
+ - **Fixed**: Removed `process.exit()` from library code (`build.ts`, `develop.ts`, `init.ts`) — errors now propagate via `throw`, letting wrappers handle exit
8
+ - **Fixed**: Moved `reloadClients` Set inside `develop()` to prevent shared state across multiple server instances
9
+ - **Fixed**: Removed `await` on synchronous `path.resolve()` calls in `build.ts` and `develop.ts`
10
+ - **Fixed**: `log.error()` called without arguments in `processPre.ts` — now passes empty string
11
+
12
+ ### Code Style
13
+
14
+ - **Changed**: Converted all `function` declarations to arrow functions (`initWrapper`, `processComponents`, `processComponentsTs`)
15
+ - **Removed**: Comments in source code (`processCom.ts`, `processComTs.ts`)
16
+ - **Removed**: Dead re-export of `setupSigintHandler` from `develop.ts`
17
+
18
+ ### Tests
19
+
20
+ - **Fixed**: TS2532 errors in `getBuildResult.test.ts` (proper handling of possibly undefined values)
21
+ - **Updated**: Tests adapted to new error propagation (no more `process.exit` mocking in library tests)
22
+ - **Removed**: Redundant/dead test cases for removed behavior
23
+
24
+ ---
25
+
26
+ # v4.0.0-beta.17
27
+
28
+ ## MCP Server Updates
29
+
30
+ ### API Migration
31
+
32
+ - **Changed**: Migrated MCP server to new SDK API (`McpServer`, `registerResource`, `registerTool`)
33
+ - **Removed**: Legacy API (`Server`, `setRequestHandler`)
34
+ - **Benefit**: Better compatibility with MCP SDK 1.26.0, cleaner code structure
35
+
36
+ ### Tools Simplification
37
+
38
+ - **Removed**: `tkeron_init`, `tkeron_build`, `tkeron_develop` tools
39
+ - **Rationale**: MCP server now focuses on documentation and examples only
40
+ - **Alternative**: AI agents should use terminal commands (`tk init`, `tk build`, `tk dev`) instead
41
+ - **Kept**: `list_examples` and `get_example` tools (read-only)
42
+ - **Annotations**: Added `readOnlyHint: true` to remaining tools
43
+
44
+ ### New Testing Documentation
45
+
46
+ - **New**: `tkeron://testing` resource with comprehensive testing guide
47
+ - **Location**: `docs/testing.md`
48
+ - **Coverage**:
49
+ - How to use `getBuildResult()` for testing tkeron projects
50
+ - Proper test structure with `beforeAll()` for concurrent safety
51
+ - Testing patterns: file existence, DOM elements, bounded content, transformations
52
+ - Complete examples with best practices
53
+ - Rules summary (8 key rules)
54
+ - **Purpose**: Help users write reliable tests for their tkeron projects
55
+
56
+ ### Documentation Updates
57
+
58
+ - **Updated**: `docs/mcp-server.md` to reflect new tools approach
59
+ - **Clarified**: AI agents can access examples but must use terminal for project operations
60
+ - **Updated**: MCP server tests to include new `testing` resource
61
+
62
+ ### Tests
63
+
64
+ - **New**: `examples/basic_build/basic_build.test.ts` - complete test suite demonstrating testing patterns
65
+ - **Purpose**: Reference implementation for testing tkeron projects
66
+
67
+ ## Dependency Updates
68
+
69
+ - **Updated**: `@modelcontextprotocol/sdk` from `^1.25.3` to `^1.26.0`
70
+
1
71
  # v4.0.0-beta.16
2
72
 
3
73
  ## Improvements
@@ -99,6 +99,7 @@ The MCP server exposes the following documentation resources:
99
99
  | `tkeron://pre-rendering` | Transform HTML at build time with .pre.ts files |
100
100
  | `tkeron://cli-reference` | Complete command-line interface documentation |
101
101
  | `tkeron://best-practices` | Patterns, anti-patterns, and limitations |
102
+ | `tkeron://testing` | How to test tkeron projects with getBuildResult() |
102
103
 
103
104
  ## What AI Agents Can Do
104
105
 
@@ -111,25 +112,19 @@ With access to the MCP server, AI agents can:
111
112
  - ✅ Provide accurate CLI commands and options
112
113
  - ✅ Debug common issues with components and pre-rendering
113
114
  - ✅ Recommend best practices and avoid anti-patterns
114
- - ✅ **Initialize new Tkeron projects directly**
115
- - ✅ **Build and develop Tkeron projects**
116
115
  - ✅ **List and explore example projects**
117
116
  - ✅ **Retrieve example code and structures**
117
+ - ✅ **Initialize, build, and develop projects via terminal** (`tk init`, `tk build`, `tk dev`)
118
118
 
119
119
  ## Available Tools
120
120
 
121
121
  The MCP server provides the following tools for AI agents:
122
122
 
123
- ### Documentation Tool
124
-
125
- - **`get_tkeron_docs`**: Retrieve specific documentation topics
126
- - Parameters: `topic` (overview, getting-started, components-html, etc.)
127
- - Returns the full markdown content of the requested documentation
128
-
129
123
  ### Example Management Tools
130
124
 
131
125
  - **`list_examples`**: List all available Tkeron example projects
132
- - Shows example names, descriptions, and features
126
+ - No parameters required
127
+ - Returns example names, descriptions, and paths
133
128
  - Helps users discover what's possible with Tkeron
134
129
 
135
130
  - **`get_example`**: Get the complete source code of an example
@@ -137,22 +132,7 @@ The MCP server provides the following tools for AI agents:
137
132
  - Returns file structure and all source code
138
133
  - Useful for learning patterns and bootstrapping projects
139
134
 
140
- ### Project Management Tools
141
-
142
- - **`tkeron_init`**: Initialize a new Tkeron project
143
- - Parameters: `projectPath` (absolute path), `force` (optional boolean)
144
- - Creates the standard Tkeron structure (websrc/ with sample files)
145
- - AI agents can create projects without user intervention
146
-
147
- - **`tkeron_build`**: Build a Tkeron project
148
- - Parameters: `sourceDir`, `targetDir` (optional)
149
- - Processes all `.pre.ts`, `.com.html`, and `.com.ts` files
150
- - Generates production-ready output
151
-
152
- - **`tkeron_develop`**: Start the development server
153
- - Parameters: `sourceDir`, `outputDir`, `port`, `host` (all optional)
154
- - Launches hot-reload development server
155
- - AI agents can test projects immediately after creation
135
+ > **Note:** The MCP server only provides documentation and examples. It does NOT modify files. The AI agent should use terminal commands (`tk init`, `tk build`, `tk dev`) to manage projects.
156
136
 
157
137
  ## Benefits
158
138
 
@@ -0,0 +1,331 @@
1
+ # Testing Tkeron Projects
2
+
3
+ ## Overview
4
+
5
+ Tkeron provides a `getBuildResult()` function that lets you test your built projects programmatically. This is useful for verifying that your components, pre-rendering scripts, and build output work correctly.
6
+
7
+ **Important:** These tests validate YOUR project's build output — not tkeron internals. You're testing that your HTML, components, and scripts produce the expected result after the build.
8
+
9
+ ## Setup
10
+
11
+ Install tkeron as a dependency in your project:
12
+
13
+ ```bash
14
+ bun add -d tkeron
15
+ ```
16
+
17
+ ## API
18
+
19
+ ```typescript
20
+ import { getBuildResult, type BuildResult } from "tkeron";
21
+
22
+ const result: BuildResult = await getBuildResult("./websrc");
23
+ ```
24
+
25
+ ### BuildResult
26
+
27
+ `getBuildResult()` returns a `Record<string, FileInfo>` indexed by relative path:
28
+
29
+ ```typescript
30
+ type BuildResult = Record<string, FileInfo>;
31
+
32
+ interface FileInfo {
33
+ fileName: string; // "index.html"
34
+ filePath: string; // "styles" or "" for root files
35
+ path: string; // "styles/main.css" (the key)
36
+ type: string; // MIME type: "text/html", "text/css", etc.
37
+ size: number; // size in bytes
38
+ fileHash: string; // SHA-256 hash
39
+ getContentAsString?: () => string; // text files only
40
+ dom?: Document; // HTML files only - parsed DOM
41
+ }
42
+ ```
43
+
44
+ - `dom` is only present on `.html` files. It provides full DOM API: `querySelector()`, `getElementById()`, `querySelectorAll()`, etc.
45
+ - `getContentAsString()` is only present on text files (html, css, js, json, txt, svg, xml, ts, mjs, cjs, md, map).
46
+ - Binary files (images, fonts) only have metadata — no `dom` or `getContentAsString`.
47
+
48
+ ## Test Structure
49
+
50
+ ### Single Build with beforeAll
51
+
52
+ **Always** build once and share the result across tests. This is:
53
+
54
+ - Concurrent-safe (`bun test --concurrent`)
55
+ - Efficient (1 build instead of N builds)
56
+ - Reliable (no race conditions)
57
+
58
+ ```typescript
59
+ import { describe, it, expect, beforeAll } from "bun:test";
60
+ import { getBuildResult, type BuildResult } from "tkeron";
61
+ import { join } from "path";
62
+
63
+ describe("my project", () => {
64
+ const sourcePath = join(import.meta.dir, "src");
65
+ let result: BuildResult;
66
+
67
+ beforeAll(async () => {
68
+ result = await getBuildResult(sourcePath);
69
+ });
70
+
71
+ it("should generate index.html", () => {
72
+ expect(result?.["index.html"]).toBeDefined();
73
+ });
74
+ });
75
+ ```
76
+
77
+ **Never** call `getBuildResult()` inside each `it()` — this causes race conditions in concurrent mode.
78
+
79
+ ## Testing Patterns
80
+
81
+ ### 1. Verify File Existence
82
+
83
+ Check that expected output files exist:
84
+
85
+ ```typescript
86
+ it("should generate expected files", () => {
87
+ expect(result?.["index.html"]).toBeDefined();
88
+ expect(result?.["index.js"]).toBeDefined();
89
+ expect(result?.["styles/main.css"]).toBeDefined();
90
+ });
91
+ ```
92
+
93
+ ### 2. Verify Element Existence (Always Check First)
94
+
95
+ **Rule:** Always verify an element exists BEFORE checking its properties.
96
+
97
+ ```typescript
98
+ // ✅ CORRECT: check existence first, then properties
99
+ it("should have login button", () => {
100
+ const dom = result?.["index.html"]?.dom;
101
+
102
+ const button = dom?.querySelector("#login-button");
103
+ expect(button).toBeDefined();
104
+ expect(button!.textContent).toBe("Log In");
105
+ });
106
+
107
+ // ❌ WRONG: accessing properties without checking existence
108
+ it("should have login button", () => {
109
+ const dom = result?.["index.html"]?.dom;
110
+ expect(dom!.querySelector("#login-button")!.textContent).toBe("Log In");
111
+ });
112
+ ```
113
+
114
+ ### 3. Verify Bounded Content
115
+
116
+ Test specific, short, predictable values — not full output strings.
117
+
118
+ ```typescript
119
+ // ✅ GOOD: specific, bounded values
120
+ it("should have correct title", () => {
121
+ const dom = result?.["index.html"]?.dom;
122
+
123
+ const title = dom?.querySelector("title");
124
+ expect(title).toBeDefined();
125
+ expect(title!.textContent).toBe("My App");
126
+ });
127
+
128
+ // ✅ GOOD: keyword presence in compiled JS
129
+ it("should contain event handling logic", () => {
130
+ const js = result?.["index.js"]?.getContentAsString!();
131
+ expect(js).toContain("addEventListener");
132
+ });
133
+
134
+ // ❌ BAD: comparing full minified JS output (fragile, breaks on any optimization change)
135
+ it("should match exact JS", () => {
136
+ const js = result?.["index.js"]?.getContentAsString!();
137
+ expect(js).toBe('var t=0,e=document.querySelector("button")...');
138
+ });
139
+
140
+ // ❌ BAD: comparing full HTML output
141
+ it("should match exact HTML", () => {
142
+ const html = result?.["index.html"]?.getContentAsString!();
143
+ expect(html).toBe("<!doctype html><html>...");
144
+ });
145
+ ```
146
+
147
+ **Why bounded:** Build output changes with optimizations, minification, and bundling. Test the **meaning**, not the **exact format**.
148
+
149
+ ### 4. Verify DOM Elements (Specific Queries)
150
+
151
+ Test specific elements that matter to your project:
152
+
153
+ ```typescript
154
+ it("should have navigation with correct links", () => {
155
+ const dom = result?.["index.html"]?.dom;
156
+
157
+ const homeLink = dom?.querySelector('nav a[href="/"]');
158
+ expect(homeLink).toBeDefined();
159
+ expect(homeLink!.textContent).toBe("Home");
160
+
161
+ const aboutLink = dom?.querySelector('nav a[href="/about"]');
162
+ expect(aboutLink).toBeDefined();
163
+ });
164
+
165
+ it("should have user profile section", () => {
166
+ const dom = result?.["index.html"]?.dom;
167
+
168
+ const profile = dom?.querySelector("#user-profile");
169
+ expect(profile).toBeDefined();
170
+
171
+ const avatar = profile?.querySelector("img.avatar");
172
+ expect(avatar).toBeDefined();
173
+ expect(avatar!.getAttribute("alt")).toBe("User avatar");
174
+ });
175
+ ```
176
+
177
+ ### 5. Verify Tkeron Transformations
178
+
179
+ Test that tkeron's build features work correctly in your project:
180
+
181
+ **Script injection (TypeScript → JavaScript):**
182
+
183
+ ```typescript
184
+ it("should have compiled script in head", () => {
185
+ const dom = result?.["index.html"]?.dom;
186
+
187
+ const script = dom?.querySelector('script[type="module"]');
188
+ expect(script).toBeDefined();
189
+ expect(script!.getAttribute("src")).toBe("./index.js");
190
+ expect(script!.parentElement!.tagName).toBe("HEAD");
191
+ });
192
+ ```
193
+
194
+ **Component substitution (.com.html):**
195
+
196
+ ```typescript
197
+ it("should have processed nav component", () => {
198
+ const dom = result?.["index.html"]?.dom;
199
+
200
+ // The <nav-menu> tag should be replaced by its .com.html content
201
+ const nav = dom?.querySelector("nav");
202
+ expect(nav).toBeDefined();
203
+
204
+ const links = dom?.querySelectorAll("nav a");
205
+ expect(links).toBeDefined();
206
+ expect(links!.length).toBeGreaterThan(0);
207
+ });
208
+ ```
209
+
210
+ **TypeScript component (.com.ts):**
211
+
212
+ ```typescript
213
+ it("should have dynamically generated list", () => {
214
+ const dom = result?.["index.html"]?.dom;
215
+
216
+ const list = dom?.querySelector("#item-list");
217
+ expect(list).toBeDefined();
218
+
219
+ const items = list?.querySelectorAll("li");
220
+ expect(items).toBeDefined();
221
+ expect(items!.length).toBe(5);
222
+ });
223
+ ```
224
+
225
+ **Pre-rendering (.pre.ts):**
226
+
227
+ ```typescript
228
+ it("should have pre-rendered content", () => {
229
+ const dom = result?.["index.html"]?.dom;
230
+
231
+ const generated = dom?.querySelector("#generated-content");
232
+ expect(generated).toBeDefined();
233
+ expect(generated!.textContent).not.toBe("");
234
+ });
235
+ ```
236
+
237
+ ### 6. Verify File Metadata
238
+
239
+ ```typescript
240
+ it("should have correct metadata", () => {
241
+ expect(result?.["index.html"]?.type).toBe("text/html");
242
+ expect(result?.["index.html"]?.size).toBeGreaterThan(0);
243
+ expect(result?.["index.html"]?.fileHash).toBeDefined();
244
+ });
245
+
246
+ it("should not have DOM on non-HTML files", () => {
247
+ expect(result?.["index.js"]?.dom).toBeUndefined();
248
+ });
249
+
250
+ it("should have correct path info for nested files", () => {
251
+ expect(result?.["pages/about.html"]?.fileName).toBe("about.html");
252
+ expect(result?.["pages/about.html"]?.filePath).toBe("pages");
253
+ });
254
+ ```
255
+
256
+ ## Complete Example
257
+
258
+ Here's a complete test file for a tkeron project with components:
259
+
260
+ ```typescript
261
+ import { describe, it, expect, beforeAll } from "bun:test";
262
+ import { getBuildResult, type BuildResult } from "tkeron";
263
+ import { join } from "path";
264
+
265
+ describe("my-website build", () => {
266
+ const sourcePath = join(import.meta.dir, "websrc");
267
+ let result: BuildResult;
268
+
269
+ beforeAll(async () => {
270
+ result = await getBuildResult(sourcePath);
271
+ });
272
+
273
+ it("should generate expected files", () => {
274
+ expect(result?.["index.html"]).toBeDefined();
275
+ expect(result?.["index.js"]).toBeDefined();
276
+ expect(result?.["about.html"]).toBeDefined();
277
+ });
278
+
279
+ it("should have page title", () => {
280
+ const dom = result?.["index.html"]?.dom;
281
+
282
+ const title = dom?.querySelector("title");
283
+ expect(title).toBeDefined();
284
+ expect(title!.textContent).toBe("My Website");
285
+ });
286
+
287
+ it("should have navigation from component", () => {
288
+ const dom = result?.["index.html"]?.dom;
289
+
290
+ const nav = dom?.querySelector("nav");
291
+ expect(nav).toBeDefined();
292
+
293
+ const homeLink = dom?.querySelector('nav a[href="/"]');
294
+ expect(homeLink).toBeDefined();
295
+ expect(homeLink!.textContent).toBe("Home");
296
+ });
297
+
298
+ it("should have footer from component", () => {
299
+ const dom = result?.["index.html"]?.dom;
300
+
301
+ const footer = dom?.querySelector("footer");
302
+ expect(footer).toBeDefined();
303
+ });
304
+
305
+ it("should have compiled JavaScript", () => {
306
+ const js = result?.["index.js"]?.getContentAsString!();
307
+
308
+ expect(js).toBeDefined();
309
+ expect(js!.length).toBeGreaterThan(0);
310
+ });
311
+
312
+ it("should have script injected in head", () => {
313
+ const dom = result?.["index.html"]?.dom;
314
+
315
+ const script = dom?.querySelector('head script[type="module"]');
316
+ expect(script).toBeDefined();
317
+ expect(script!.getAttribute("src")).toBe("./index.js");
318
+ });
319
+ });
320
+ ```
321
+
322
+ ## Rules Summary
323
+
324
+ 1. **Build once** with `beforeAll()` — never inside individual tests
325
+ 2. **Check existence first** — always `expect(element).toBeDefined()` before accessing properties
326
+ 3. **Use optional chaining** (`?.`) for safe property access on result entries
327
+ 4. **Test bounded content** — specific values, keywords, attributes; not full output strings
328
+ 5. **Test specific elements** — query for elements that matter to your project
329
+ 6. **Verify transformations** — components processed, scripts injected, pre-rendering applied
330
+ 7. **Don't test the parser** — you're testing your project, not tkeron internals
331
+ 8. **Concurrent-safe** — tests must work with `bun test --concurrent`
package/mcp-server.ts CHANGED
@@ -1,57 +1,33 @@
1
1
  #!/usr/bin/env bun
2
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
- import {
5
- ListResourcesRequestSchema,
6
- ReadResourceRequestSchema,
7
- ListToolsRequestSchema,
8
- CallToolRequestSchema,
9
- } from "@modelcontextprotocol/sdk/types.js";
10
4
  import { readFileSync, existsSync, readdirSync, statSync } from "fs";
11
5
  import { join, dirname } from "path";
12
6
  import { fileURLToPath } from "url";
13
- import { init } from "./src/init";
14
- import { build } from "./src/build";
15
- import { develop } from "./src/develop";
16
7
 
17
- // Resolve the docs directory - handle both direct execution and symlink execution
18
- const getDocsDir = () => {
19
- // Try import.meta.dir first (Bun specific)
8
+ const getBaseDir = () => {
20
9
  if (import.meta.dir) {
21
- const docsPath = join(import.meta.dir, "docs");
22
- if (existsSync(docsPath)) {
23
- return docsPath;
24
- }
10
+ return import.meta.dir;
25
11
  }
26
-
27
- // Fallback: resolve from the actual file location
28
12
  const currentFile = import.meta.url
29
13
  ? fileURLToPath(import.meta.url)
30
14
  : __filename;
31
- const currentDir = dirname(currentFile);
32
- return join(currentDir, "docs");
15
+ return dirname(currentFile);
33
16
  };
34
17
 
35
- const DOCS_DIR = getDocsDir();
18
+ const BASE_DIR = getBaseDir();
19
+ const DOCS_DIR = join(BASE_DIR, "docs");
20
+ const EXAMPLES_DIR = join(BASE_DIR, "examples");
36
21
 
37
- // Get examples directory
38
- const getExamplesDir = () => {
39
- if (import.meta.dir) {
40
- const examplesPath = join(import.meta.dir, "examples");
41
- if (existsSync(examplesPath)) {
42
- return examplesPath;
43
- }
22
+ const getVersion = (): string => {
23
+ const pkgPath = join(BASE_DIR, "package.json");
24
+ if (existsSync(pkgPath)) {
25
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
26
+ return pkg.version || "0.0.0";
44
27
  }
45
-
46
- const currentFile = import.meta.url
47
- ? fileURLToPath(import.meta.url)
48
- : __filename;
49
- const currentDir = dirname(currentFile);
50
- return join(currentDir, "examples");
28
+ return "0.0.0";
51
29
  };
52
30
 
53
- const EXAMPLES_DIR = getExamplesDir();
54
-
55
31
  const DOCS = [
56
32
  {
57
33
  uri: "tkeron://overview",
@@ -95,466 +71,191 @@ const DOCS = [
95
71
  description: "Patterns, anti-patterns, and limitations",
96
72
  file: "best-practices.md",
97
73
  },
98
- ];
99
-
100
- const server = new Server(
101
74
  {
102
- name: "tkeron-mcp",
103
- version: "1.0.0",
75
+ uri: "tkeron://testing",
76
+ name: "Testing Tkeron Projects",
77
+ description:
78
+ "How to test tkeron projects using getBuildResult(), DOM assertions, and bounded content verification",
79
+ file: "testing.md",
104
80
  },
105
- {
106
- capabilities: {
107
- resources: {},
108
- tools: {},
109
- },
110
- },
111
- );
112
-
113
- server.setRequestHandler(ListResourcesRequestSchema, async () => {
114
- return {
115
- resources: DOCS.map((doc) => ({
116
- uri: doc.uri,
117
- name: doc.name,
118
- description: doc.description,
119
- mimeType: "text/markdown",
120
- })),
121
- };
122
- });
123
- server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
124
- try {
125
- const uri = request.params.uri;
126
- const doc = DOCS.find((d) => d.uri === uri);
127
-
128
- if (!doc) {
129
- throw new Error(`Unknown resource: ${uri}`);
130
- }
81
+ ];
131
82
 
132
- const filePath = join(DOCS_DIR, doc.file);
83
+ const EXAMPLE_DESCRIPTIONS: { [key: string]: string } = {
84
+ init_sample:
85
+ "Complete starter template with all features (used by 'tk init')",
86
+ basic_build: "Simple HTML + TypeScript bundling",
87
+ with_assets: "HTML in multiple directories with asset references",
88
+ with_pre: "Pre-rendering HTML at build time with .pre.ts files",
89
+ with_com_html_priority:
90
+ "Static HTML components (.com.html) with local override priority",
91
+ with_com_ts: "TypeScript components (.com.ts) generating HTML at build time",
92
+ with_com_ts_priority:
93
+ "TypeScript components (.com.ts) with local override priority",
94
+ with_com_mixed_priority:
95
+ "Mixed .com.html and .com.ts showing processing order",
96
+ with_component_iteration:
97
+ "Component iteration: .com.ts with logic, .com.html for templates",
98
+ };
133
99
 
134
- // Check if file exists before reading
135
- if (!existsSync(filePath)) {
136
- throw new Error(`Documentation file not found: ${doc.file}`);
100
+ const readDirRecursive = (
101
+ dir: string,
102
+ baseDir: string = dir,
103
+ ): { [key: string]: string } => {
104
+ const files: { [key: string]: string } = {};
105
+ const items = readdirSync(dir);
106
+
107
+ for (const item of items) {
108
+ const fullPath = join(dir, item);
109
+ const relativePath = fullPath.substring(baseDir.length + 1);
110
+
111
+ if (statSync(fullPath).isDirectory()) {
112
+ Object.assign(files, readDirRecursive(fullPath, baseDir));
113
+ } else {
114
+ files[relativePath] = readFileSync(fullPath, "utf-8");
137
115
  }
116
+ }
138
117
 
139
- const content = readFileSync(filePath, "utf-8");
118
+ return files;
119
+ };
140
120
 
141
- return {
142
- contents: [
143
- {
144
- uri: doc.uri,
145
- mimeType: "text/markdown",
146
- text: content,
147
- },
148
- ],
149
- };
150
- } catch (error) {
151
- throw error;
152
- }
121
+ const server = new McpServer({
122
+ name: "tkeron-mcp",
123
+ version: getVersion(),
153
124
  });
154
125
 
155
- // Tools handler - provide a simple tool to keep VS Code happy
156
- server.setRequestHandler(ListToolsRequestSchema, async () => {
157
- return {
158
- tools: [
159
- {
160
- name: "get_tkeron_docs",
161
- description:
162
- "Get Tkeron documentation. Available topics: overview, getting-started, components-html, components-typescript, pre-rendering, cli-reference, best-practices",
163
- inputSchema: {
164
- type: "object",
165
- properties: {
166
- topic: {
167
- type: "string",
168
- description: "Documentation topic to retrieve",
169
- enum: [
170
- "overview",
171
- "getting-started",
172
- "components-html",
173
- "components-typescript",
174
- "pre-rendering",
175
- "cli-reference",
176
- "best-practices",
177
- ],
178
- },
179
- },
180
- required: ["topic"],
181
- },
182
- },
183
- {
184
- name: "list_examples",
185
- description:
186
- "List all available Tkeron example projects with their descriptions",
187
- inputSchema: {
188
- type: "object",
189
- properties: {},
190
- required: [],
191
- },
192
- },
193
- {
194
- name: "get_example",
195
- description:
196
- "Get the source code and structure of a specific Tkeron example project",
197
- inputSchema: {
198
- type: "object",
199
- properties: {
200
- example: {
201
- type: "string",
202
- description:
203
- "Example name (e.g., 'basic_build', 'with_component_iteration')",
204
- },
205
- },
206
- required: ["example"],
207
- },
208
- },
209
- {
210
- name: "tkeron_init",
211
- description:
212
- "Initialize a new Tkeron project with the standard structure (websrc/ directory with sample files)",
213
- inputSchema: {
214
- type: "object",
215
- properties: {
216
- projectPath: {
217
- type: "string",
218
- description: "Absolute path where the project will be created",
219
- },
220
- force: {
221
- type: "boolean",
222
- description: "Overwrite existing files if they exist",
223
- default: false,
224
- },
225
- },
226
- required: ["projectPath"],
227
- },
228
- },
229
- {
230
- name: "tkeron_build",
231
- description:
232
- "Build a Tkeron project (processes .pre.ts, .com.html, and .com.ts files)",
233
- inputSchema: {
234
- type: "object",
235
- properties: {
236
- sourceDir: {
237
- type: "string",
238
- description:
239
- "Absolute path to source directory (default: websrc/)",
240
- },
241
- targetDir: {
242
- type: "string",
243
- description: "Absolute path to output directory (default: web/)",
244
- },
245
- },
246
- required: [],
247
- },
248
- },
249
- {
250
- name: "tkeron_develop",
251
- description: "Start Tkeron development server with hot reload",
252
- inputSchema: {
253
- type: "object",
254
- properties: {
255
- sourceDir: {
256
- type: "string",
257
- description:
258
- "Absolute path to source directory (default: websrc/)",
259
- },
260
- outputDir: {
261
- type: "string",
262
- description: "Absolute path to output directory (default: web/)",
263
- },
264
- port: {
265
- type: "number",
266
- description: "Port number for dev server (default: 3000)",
267
- default: 3000,
268
- },
269
- host: {
270
- type: "string",
271
- description: "Host for dev server (default: localhost)",
272
- default: "localhost",
273
- },
274
- },
275
- required: [],
276
- },
277
- },
278
- ],
279
- };
280
- });
126
+ for (const doc of DOCS) {
127
+ server.registerResource(
128
+ doc.name,
129
+ doc.uri,
130
+ { description: doc.description, mimeType: "text/markdown" },
131
+ () => {
132
+ const filePath = join(DOCS_DIR, doc.file);
281
133
 
282
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
283
- const { name, arguments: args } = request.params;
134
+ if (!existsSync(filePath)) {
135
+ throw new Error(`Documentation file not found: ${doc.file}`);
136
+ }
284
137
 
285
- if (name === "get_tkeron_docs") {
286
- const topic = (args as any).topic;
287
- const doc = DOCS.find((d) => d.uri === `tkeron://${topic}`);
138
+ const content = readFileSync(filePath, "utf-8");
288
139
 
289
- if (!doc) {
290
140
  return {
291
- content: [
141
+ contents: [
292
142
  {
293
- type: "text",
294
- text: `Unknown topic: ${topic}`,
143
+ uri: doc.uri,
144
+ mimeType: "text/markdown",
145
+ text: content,
295
146
  },
296
147
  ],
297
148
  };
298
- }
149
+ },
150
+ );
151
+ }
299
152
 
300
- const filePath = join(DOCS_DIR, doc.file);
301
- const content = readFileSync(filePath, "utf-8");
153
+ server.registerTool(
154
+ "list_examples",
155
+ {
156
+ description:
157
+ "List all available Tkeron example projects with their descriptions",
158
+ annotations: { readOnlyHint: true },
159
+ },
160
+ () => {
161
+ const examples = readdirSync(EXAMPLES_DIR).filter((name) => {
162
+ const examplePath = join(EXAMPLES_DIR, name);
163
+ return (
164
+ statSync(examplePath).isDirectory() &&
165
+ (existsSync(join(examplePath, "src")) ||
166
+ existsSync(join(examplePath, "websrc")))
167
+ );
168
+ });
169
+
170
+ const exampleList = examples.map((name) => ({
171
+ name,
172
+ description: EXAMPLE_DESCRIPTIONS[name] || "No description available",
173
+ path: `examples/${name}`,
174
+ }));
302
175
 
303
176
  return {
304
177
  content: [
305
178
  {
306
- type: "text",
307
- text: content,
179
+ type: "text" as const,
180
+ text: JSON.stringify(exampleList, null, 2),
308
181
  },
309
182
  ],
310
183
  };
311
- }
312
-
313
- if (name === "list_examples") {
314
- try {
315
- const examples = readdirSync(EXAMPLES_DIR).filter((name) => {
316
- const examplePath = join(EXAMPLES_DIR, name);
317
- // Accept both src/ and websrc/ directories (init_sample uses websrc/)
318
- return (
319
- statSync(examplePath).isDirectory() &&
320
- (existsSync(join(examplePath, "src")) ||
321
- existsSync(join(examplePath, "websrc")))
322
- );
323
- });
324
-
325
- const exampleDescriptions: { [key: string]: string } = {
326
- init_sample:
327
- "Complete starter template with all features (used by 'tk init')",
328
- basic_build: "Simple HTML + TypeScript bundling",
329
- with_assets: "HTML in multiple directories with asset references",
330
- with_pre: "Pre-rendering HTML at build time with .pre.ts files",
331
- with_com_html_priority:
332
- "Static HTML components (.com.html) with local override priority",
333
- with_com_ts_priority:
334
- "TypeScript components (.com.ts) with local override priority",
335
- with_com_mixed_priority:
336
- "Mixed .com.html and .com.ts showing processing order",
337
- with_component_iteration:
338
- "Component iteration: .com.ts with logic, .com.html for templates",
339
- };
340
-
341
- const exampleList = examples.map((name) => ({
342
- name,
343
- description: exampleDescriptions[name] || "No description available",
344
- path: `examples/${name}`,
345
- }));
346
-
347
- return {
348
- content: [
349
- {
350
- type: "text",
351
- text: JSON.stringify(exampleList, null, 2),
352
- },
353
- ],
354
- };
355
- } catch (error) {
356
- return {
357
- content: [
358
- {
359
- type: "text",
360
- text: `Error listing examples: ${error instanceof Error ? error.message : String(error)}`,
361
- },
362
- ],
363
- };
364
- }
365
- }
184
+ },
185
+ );
366
186
 
367
- if (name === "get_example") {
368
- const exampleName = (args as any).example;
187
+ server.registerTool(
188
+ "get_example",
189
+ {
190
+ description:
191
+ "Get the source code and structure of a specific Tkeron example project",
192
+ inputSchema: {
193
+ example: {
194
+ type: "string",
195
+ description:
196
+ "Example name (e.g., 'basic_build', 'with_component_iteration')",
197
+ },
198
+ } as any,
199
+ annotations: { readOnlyHint: true },
200
+ },
201
+ ({ example: exampleName }: { example: string }) => {
369
202
  const examplePath = join(EXAMPLES_DIR, exampleName);
370
203
 
371
204
  if (!existsSync(examplePath)) {
372
205
  return {
373
206
  content: [
374
207
  {
375
- type: "text",
376
- text: `Example not found: ${exampleName}`,
208
+ type: "text" as const,
209
+ text: `Example not found: ${exampleName}. Use the list_examples tool to see available examples.`,
377
210
  },
378
211
  ],
212
+ isError: true,
379
213
  };
380
214
  }
381
215
 
382
- try {
383
- // init_sample uses websrc/, others use src/
384
- let srcPath = join(examplePath, "src");
385
- if (!existsSync(srcPath)) {
386
- srcPath = join(examplePath, "websrc");
387
- }
388
-
389
- if (!existsSync(srcPath)) {
390
- return {
391
- content: [
392
- {
393
- type: "text",
394
- text: `Example ${exampleName} does not have a src or websrc directory`,
395
- },
396
- ],
397
- };
398
- }
399
-
400
- // Read all files in src directory recursively
401
- const readDirRecursive = (
402
- dir: string,
403
- baseDir: string = dir,
404
- ): { [key: string]: string } => {
405
- const files: { [key: string]: string } = {};
406
- const items = readdirSync(dir);
407
-
408
- for (const item of items) {
409
- const fullPath = join(dir, item);
410
- const relativePath = fullPath.substring(baseDir.length + 1);
411
-
412
- if (statSync(fullPath).isDirectory()) {
413
- Object.assign(files, readDirRecursive(fullPath, baseDir));
414
- } else {
415
- files[relativePath] = readFileSync(fullPath, "utf-8");
416
- }
417
- }
418
-
419
- return files;
420
- };
421
-
422
- const sourceFiles = readDirRecursive(srcPath);
423
-
424
- const result = {
425
- example: exampleName,
426
- path: `examples/${exampleName}`,
427
- files: sourceFiles,
428
- };
429
-
430
- return {
431
- content: [
432
- {
433
- type: "text",
434
- text: JSON.stringify(result, null, 2),
435
- },
436
- ],
437
- };
438
- } catch (error) {
439
- return {
440
- content: [
441
- {
442
- type: "text",
443
- text: `Error reading example: ${error instanceof Error ? error.message : String(error)}`,
444
- },
445
- ],
446
- };
216
+ let srcPath = join(examplePath, "src");
217
+ if (!existsSync(srcPath)) {
218
+ srcPath = join(examplePath, "websrc");
447
219
  }
448
- }
449
-
450
- if (name === "tkeron_init") {
451
- const projectPath = (args as any).projectPath;
452
- const force = (args as any).force || false;
453
-
454
- try {
455
- await init({
456
- projectName: projectPath,
457
- force,
458
- promptFn: async () => force, // Auto-confirm if force is true
459
- });
460
220
 
221
+ if (!existsSync(srcPath)) {
461
222
  return {
462
223
  content: [
463
224
  {
464
- type: "text",
465
- text: `✅ Tkeron project initialized at ${projectPath}`,
466
- },
467
- ],
468
- };
469
- } catch (error) {
470
- return {
471
- content: [
472
- {
473
- type: "text",
474
- text: `Error initializing project: ${error instanceof Error ? error.message : String(error)}`,
225
+ type: "text" as const,
226
+ text: `Example ${exampleName} does not have a src or websrc directory`,
475
227
  },
476
228
  ],
229
+ isError: true,
477
230
  };
478
231
  }
479
- }
480
-
481
- if (name === "tkeron_build") {
482
- const sourceDir = (args as any).sourceDir;
483
- const targetDir = (args as any).targetDir;
484
232
 
485
- try {
486
- await build({ sourceDir, targetDir });
233
+ const sourceFiles = readDirRecursive(srcPath);
487
234
 
488
- return {
489
- content: [
490
- {
491
- type: "text",
492
- text: `✅ Build complete! Output: ${targetDir || "web/"}`,
493
- },
494
- ],
495
- };
496
- } catch (error) {
497
- return {
498
- content: [
499
- {
500
- type: "text",
501
- text: `Error building project: ${error instanceof Error ? error.message : String(error)}`,
502
- },
503
- ],
504
- };
505
- }
506
- }
507
-
508
- if (name === "tkeron_develop") {
509
- const sourceDir = (args as any).sourceDir;
510
- const outputDir = (args as any).outputDir;
511
- const port = (args as any).port || 3000;
512
- const host = (args as any).host || "localhost";
513
-
514
- try {
515
- const server = await develop({ sourceDir, outputDir, port, host });
516
-
517
- return {
518
- content: [
519
- {
520
- type: "text",
521
- text: `✅ Development server started at http://${server.host}:${server.port}\n\n⚠️ Note: Server is running in the background. Use the returned server object to stop it later.`,
522
- },
523
- ],
524
- };
525
- } catch (error) {
526
- return {
527
- content: [
528
- {
529
- type: "text",
530
- text: `Error starting development server: ${error instanceof Error ? error.message : String(error)}`,
531
- },
532
- ],
533
- };
534
- }
535
- }
536
-
537
- throw new Error(`Unknown tool: ${name}`);
538
- });
539
-
540
- async function main() {
541
- try {
542
- console.error(`[tkeron-mcp] Starting server...`);
543
- console.error(`[tkeron-mcp] Docs directory: ${DOCS_DIR}`);
544
- console.error(`[tkeron-mcp] Available resources: ${DOCS.length}`);
235
+ const result = {
236
+ example: exampleName,
237
+ path: `examples/${exampleName}`,
238
+ files: sourceFiles,
239
+ };
545
240
 
546
- const transport = new StdioServerTransport();
547
- await server.connect(transport);
241
+ return {
242
+ content: [
243
+ {
244
+ type: "text" as const,
245
+ text: JSON.stringify(result, null, 2),
246
+ },
247
+ ],
248
+ };
249
+ },
250
+ );
548
251
 
549
- console.error(`[tkeron-mcp] Server started successfully`);
550
- } catch (error) {
551
- const errorMessage = error instanceof Error ? error.message : String(error);
552
- console.error(`[tkeron-mcp] Fatal error:`, errorMessage);
553
- process.exit(1);
554
- }
555
- }
252
+ const main = async () => {
253
+ const transport = new StdioServerTransport();
254
+ await server.connect(transport);
255
+ console.error(`[tkeron-mcp] Server v${getVersion()} started`);
256
+ };
556
257
 
557
258
  main().catch((error) => {
558
- console.error(`[tkeron-mcp] Unhandled error:`, error);
259
+ console.error(`[tkeron-mcp] Fatal error:`, error);
559
260
  process.exit(1);
560
261
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tkeron",
3
- "version": "4.0.0-beta.16",
3
+ "version": "4.0.0-beta.18",
4
4
  "description": "CLI tool for backend-driven frontend development with TypeScript.",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
package/src/build.ts CHANGED
@@ -24,10 +24,10 @@ export const build = async (options: BuildOptions) => {
24
24
  return;
25
25
  }
26
26
 
27
- const source = await resolve(options.sourceDir || "websrc");
27
+ const source = resolve(options.sourceDir || "websrc");
28
28
  const target = options.targetDir
29
- ? await resolve(options.targetDir)
30
- : await resolve(dirname(source), "web");
29
+ ? resolve(options.targetDir)
30
+ : resolve(dirname(source), "web");
31
31
 
32
32
  const sourceParent = dirname(source);
33
33
  const tempDir = join(
@@ -67,14 +67,6 @@ export const build = async (options: BuildOptions) => {
67
67
  `\n💡 Tip: Create the directory first, check the path, or run 'tk init' to create a new project.`,
68
68
  );
69
69
  log.error(` Expected: ${source}\n`);
70
- process.exit(1);
71
- }
72
-
73
- if (
74
- error.message &&
75
- error.message.includes("must contain at least one hyphen")
76
- ) {
77
- process.exit(1);
78
70
  }
79
71
 
80
72
  throw error;
package/src/develop.ts CHANGED
@@ -13,8 +13,6 @@ export interface TkeronDevOptions {
13
13
  logger?: Logger;
14
14
  }
15
15
 
16
- const reloadClients = new Set<ReadableStreamDefaultController>();
17
-
18
16
  export interface DevelopServer {
19
17
  stop: () => Promise<void>;
20
18
  port: number;
@@ -28,6 +26,8 @@ export const develop = async (
28
26
  throw new Error("Invalid options provided for develop");
29
27
  }
30
28
 
29
+ const reloadClients = new Set<ReadableStreamDefaultController>();
30
+
31
31
  const {
32
32
  port = 3000,
33
33
  host = "localhost",
@@ -36,28 +36,13 @@ export const develop = async (
36
36
  logger: log = defaultLogger,
37
37
  } = options;
38
38
 
39
- const source = await resolve(sourceDir || "websrc");
39
+ const source = resolve(sourceDir || "websrc");
40
40
  const target = outputDir
41
- ? await resolve(outputDir)
42
- : await resolve(dirname(source), "web");
41
+ ? resolve(outputDir)
42
+ : resolve(dirname(source), "web");
43
43
 
44
44
  log.log("🔨 Building project...");
45
- try {
46
- await build({ sourceDir: source, targetDir: target, logger: log });
47
- } catch (error: any) {
48
- if (error.code === "ENOENT") {
49
- const actualSourceDir = sourceDir || "websrc";
50
- log.error(
51
- `\n❌ Error: Source directory "${actualSourceDir}" does not exist.`,
52
- );
53
- log.error(
54
- `\n💡 Tip: Create the directory first, check the path, or run 'tk init' to create a new project.`,
55
- );
56
- log.error(` Expected: ${source}\n`);
57
- process.exit(1);
58
- }
59
- throw error;
60
- }
45
+ await build({ sourceDir: source, targetDir: target, logger: log });
61
46
  log.log("✅ Build complete!");
62
47
 
63
48
  const server = Bun.serve({
@@ -182,5 +167,3 @@ export const develop = async (
182
167
  host: server.hostname ?? host,
183
168
  };
184
169
  };
185
-
186
- export { setupSigintHandler } from "./setupSigintHandler";
package/src/init.ts CHANGED
@@ -57,7 +57,7 @@ export const init = async (options: InitOptions) => {
57
57
 
58
58
  if (!shouldContinue) {
59
59
  log.log("\n❌ Operation cancelled.");
60
- process.exit(0);
60
+ return;
61
61
  }
62
62
  }
63
63
 
@@ -9,7 +9,7 @@ export interface InitWrapperOptions {
9
9
  [key: string]: any;
10
10
  }
11
11
 
12
- export async function initWrapper(options: InitWrapperOptions) {
12
+ export const initWrapper = async (options: InitWrapperOptions) => {
13
13
  const log = options?.logger || defaultLogger;
14
14
 
15
15
  if (!options || typeof options !== "object") {
@@ -60,4 +60,4 @@ export async function initWrapper(options: InitWrapperOptions) {
60
60
  }
61
61
  process.exit(1);
62
62
  }
63
- }
63
+ };
package/src/processCom.ts CHANGED
@@ -60,14 +60,14 @@ export const processCom = async (
60
60
  return hasChanges;
61
61
  };
62
62
 
63
- async function processComponents(
63
+ const processComponents = async (
64
64
  element: any,
65
65
  currentDir: string,
66
66
  rootDir: string,
67
67
  componentStack: string[],
68
68
  depth: number = 0,
69
69
  log: Logger = silentLogger,
70
- ): Promise<boolean> {
70
+ ): Promise<boolean> => {
71
71
  let hasChanges = false;
72
72
  const MAX_DEPTH = 50;
73
73
  if (depth > MAX_DEPTH) {
@@ -183,7 +183,6 @@ async function processComponents(
183
183
  }
184
184
  }
185
185
 
186
- // Recursively process child elements
187
186
  const childChanged = await processComponents(
188
187
  child,
189
188
  currentDir,
@@ -196,4 +195,4 @@ async function processComponents(
196
195
  }
197
196
 
198
197
  return hasChanges;
199
- }
198
+ };
@@ -61,7 +61,7 @@ export const processComTs = async (
61
61
  return hasChanges;
62
62
  };
63
63
 
64
- async function processComponentsTs(
64
+ const processComponentsTs = async (
65
65
  element: any,
66
66
  currentDir: string,
67
67
  rootDir: string,
@@ -69,7 +69,7 @@ async function processComponentsTs(
69
69
  depth: number = 0,
70
70
  log: Logger = silentLogger,
71
71
  options: ProcessComTsOptions = {},
72
- ): Promise<boolean> {
72
+ ): Promise<boolean> => {
73
73
  let hasChanges = false;
74
74
  const MAX_DEPTH = 50;
75
75
  if (depth > MAX_DEPTH) {
@@ -251,7 +251,6 @@ ${codeWithoutImports}
251
251
  }
252
252
  }
253
253
 
254
- // Recursively process child elements
255
254
  const childChanged = await processComponentsTs(
256
255
  child,
257
256
  currentDir,
@@ -265,4 +264,4 @@ ${codeWithoutImports}
265
264
  }
266
265
 
267
266
  return hasChanges;
268
- }
267
+ };
package/src/processPre.ts CHANGED
@@ -89,7 +89,7 @@ export const processPre = async (
89
89
  if (stdout) {
90
90
  log.error(`\nOutput:\n${stdout}`);
91
91
  }
92
- log.error();
92
+ log.error("");
93
93
  throw new Error(`Pre-rendering failed for ${preFile}`);
94
94
  }
95
95
  }
@@ -11,7 +11,7 @@ const hashString = (str: string): number => {
11
11
  for (let i = 0; i < str.length; i++) {
12
12
  const char = str.charCodeAt(i);
13
13
  hash = (hash << 5) - hash + char;
14
- hash = hash & hash; // Convert to 32bit integer
14
+ hash = hash & hash;
15
15
  }
16
16
  return Math.abs(hash);
17
17
  };
@@ -22,7 +22,7 @@ export const getTestResources = (
22
22
  testName: string,
23
23
  ): { port: number; dir: string } => {
24
24
  const hash = hashString(testName);
25
- const port = 9000 + (hash % 10000); // Port range: 9000-18999
25
+ const port = 9000 + (hash % 10000);
26
26
  const uniqueId = `${hash}-${resourceCounter++}`;
27
27
  const dir = join(tmpdir(), `tkeron-test-${uniqueId}`);
28
28