Skip to content

Commit d637a65

Browse files
committed
feat: add logging
1 parent 2b066e6 commit d637a65

File tree

4 files changed

+201
-48
lines changed

4 files changed

+201
-48
lines changed

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "mcp-framework",
3-
"version": "0.1.1",
3+
"version": "0.1.2",
44
"description": "Framework for building Model Context Protocol (MCP) servers in Typescript",
55
"type": "module",
66
"author": "Alex Andru <[email protected]>",

src/core/Logger.ts

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { createWriteStream, WriteStream } from "fs";
2+
import { join } from "path";
3+
import { mkdir } from "fs/promises";
4+
5+
export class Logger {
6+
private static instance: Logger;
7+
private logStream: WriteStream;
8+
private logFilePath: string;
9+
10+
private constructor() {
11+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
12+
const logDir = "logs";
13+
14+
// Ensure logs directory exists
15+
mkdir(logDir, { recursive: true }).catch((err) => {
16+
process.stderr.write(`Failed to create logs directory: ${err}\n`);
17+
});
18+
19+
this.logFilePath = join(logDir, `mcp-server-${timestamp}.log`);
20+
this.logStream = createWriteStream(this.logFilePath, { flags: "a" });
21+
22+
// Handle process termination
23+
process.on("exit", () => this.close());
24+
process.on("SIGINT", () => this.close());
25+
process.on("SIGTERM", () => this.close());
26+
}
27+
28+
public static getInstance(): Logger {
29+
if (!Logger.instance) {
30+
Logger.instance = new Logger();
31+
}
32+
return Logger.instance;
33+
}
34+
35+
private getTimestamp(): string {
36+
return new Date().toISOString();
37+
}
38+
39+
private formatMessage(level: string, message: string): string {
40+
return `[${this.getTimestamp()}] [${level}] ${message}\n`;
41+
}
42+
43+
public info(message: string): void {
44+
const formattedMessage = this.formatMessage("INFO", message);
45+
this.logStream.write(formattedMessage);
46+
process.stderr.write(formattedMessage);
47+
}
48+
49+
public log(message: string): void {
50+
this.info(message); // Alias for info
51+
}
52+
53+
public error(message: string): void {
54+
const formattedMessage = this.formatMessage("ERROR", message);
55+
this.logStream.write(formattedMessage);
56+
process.stderr.write(formattedMessage);
57+
}
58+
59+
public warn(message: string): void {
60+
const formattedMessage = this.formatMessage("WARN", message);
61+
this.logStream.write(formattedMessage);
62+
process.stderr.write(formattedMessage);
63+
}
64+
65+
public debug(message: string): void {
66+
const formattedMessage = this.formatMessage("DEBUG", message);
67+
this.logStream.write(formattedMessage);
68+
process.stderr.write(formattedMessage);
69+
}
70+
71+
public close(): void {
72+
if (this.logStream) {
73+
this.logStream.end();
74+
}
75+
}
76+
77+
public getLogPath(): string {
78+
return this.logFilePath;
79+
}
80+
}
81+
82+
export const logger = Logger.getInstance();

src/core/MCPServer.ts

+14-7
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ export class MCPServer {
2727
},
2828
{
2929
capabilities: {
30-
tools: {},
30+
tools: {
31+
enabled: true,
32+
},
3133
},
3234
}
3335
);
@@ -41,11 +43,13 @@ export class MCPServer {
4143
}
4244

4345
private setupHandlers() {
44-
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
45-
tools: Array.from(this.toolsMap.values()).map(
46-
(tool) => tool.toolDefinition
47-
),
48-
}));
46+
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
47+
return {
48+
tools: Array.from(this.toolsMap.values()).map(
49+
(tool) => tool.toolDefinition
50+
),
51+
};
52+
});
4953

5054
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
5155
const tool = this.toolsMap.get(request.params.name);
@@ -67,8 +71,11 @@ export class MCPServer {
6771

6872
const transport = new StdioServerTransport();
6973
await this.server.connect(transport);
74+
75+
// Write errors to stderr instead of stdout
76+
process.stderr.write(`Server started with ${tools.length} tools\n`);
7077
} catch (error) {
71-
console.error("Server initialization error:", error);
78+
process.stderr.write(`Server initialization error: ${error}\n`);
7279
throw error;
7380
}
7481
}

src/core/toolLoader.ts

+104-40
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { BaseTool } from "../tools/BaseTool.js";
2-
import { fileURLToPath } from "url";
32
import { dirname, join } from "path";
43
import { promises as fs } from "fs";
4+
import { statSync } from "fs";
5+
import { fileURLToPath } from "url";
56
import { cwd } from "process";
7+
import { logger } from "./Logger.js";
68

79
export interface ToolLoaderOptions {
810
toolsDir?: string;
@@ -15,75 +17,137 @@ export class ToolLoader {
1517

1618
constructor(options: ToolLoaderOptions = {}) {
1719
this.exclude = options.exclude || ["BaseTool.js", "*.test.js", "*.spec.js"];
18-
this.toolsDir = options.toolsDir || this.findDefaultToolsDir();
20+
this.toolsDir = this.resolveToolsDir(options.toolsDir);
21+
logger.debug(`Initialized ToolLoader with directory: ${this.toolsDir}`);
1922
}
2023

21-
private findDefaultToolsDir(): string {
22-
// Use current working directory + dist/tools as default
23-
return join(cwd(), "dist", "tools");
24+
private resolveToolsDir(toolsDir?: string): string {
25+
if (toolsDir) {
26+
logger.debug(`Using provided tools directory: ${toolsDir}`);
27+
return toolsDir;
28+
}
29+
30+
const currentFilePath = fileURLToPath(import.meta.url);
31+
const currentDir = dirname(currentFilePath);
32+
const possiblePaths = [
33+
join(currentDir, "..", "tools"),
34+
join(currentDir, "..", "..", "tools"),
35+
join(cwd(), "dist", "tools"),
36+
join(cwd(), "build", "tools"),
37+
join(cwd(), "tools"),
38+
];
39+
40+
logger.debug(
41+
`Searching for tools in possible paths:\n${possiblePaths.join("\n")}`
42+
);
43+
44+
for (const path of possiblePaths) {
45+
try {
46+
if (statSync(path).isDirectory()) {
47+
logger.debug(`Found existing tools directory: ${path}`);
48+
return path;
49+
}
50+
} catch (e) {
51+
logger.debug(`Path ${path} not accessible`);
52+
}
53+
}
54+
55+
const fallbackPath = join(cwd(), "dist", "tools");
56+
logger.debug(
57+
`No valid tools directory found, falling back to: ${fallbackPath}`
58+
);
59+
return fallbackPath;
2460
}
2561

2662
private isToolFile(file: string): boolean {
2763
if (!file.endsWith(".js")) return false;
28-
return !this.exclude.some((pattern) => {
64+
const isExcluded = this.exclude.some((pattern) => {
2965
if (pattern.includes("*")) {
3066
const regex = new RegExp(pattern.replace("*", ".*"));
3167
return regex.test(file);
3268
}
3369
return file === pattern;
3470
});
71+
72+
logger.debug(
73+
`Checking file ${file}: ${isExcluded ? "excluded" : "included"}`
74+
);
75+
return !isExcluded;
3576
}
3677

3778
private validateTool(tool: any): tool is BaseTool {
38-
return Boolean(
79+
const isValid = Boolean(
3980
tool &&
4081
typeof tool.name === "string" &&
4182
tool.toolDefinition &&
4283
typeof tool.toolCall === "function"
4384
);
85+
86+
if (isValid) {
87+
logger.debug(`Validated tool: ${tool.name}`);
88+
} else {
89+
logger.warn(`Invalid tool found: missing required properties`);
90+
}
91+
92+
return isValid;
4493
}
4594

4695
async loadTools(): Promise<BaseTool[]> {
4796
try {
48-
console.log(`Loading tools from directory: ${this.toolsDir}`);
97+
logger.debug(`Attempting to load tools from: ${this.toolsDir}`);
98+
99+
let stats;
100+
try {
101+
stats = await fs.stat(this.toolsDir);
102+
} catch (error) {
103+
logger.error(`Error accessing tools directory: ${error}`);
104+
return [];
105+
}
106+
107+
if (!stats.isDirectory()) {
108+
logger.error(`Path is not a directory: ${this.toolsDir}`);
109+
return [];
110+
}
111+
49112
const files = await fs.readdir(this.toolsDir);
50-
console.log(`Found files: ${files.join(", ")}`);
51-
52-
const toolPromises = files
53-
.filter((file) => this.isToolFile(file))
54-
.map(async (file) => {
55-
try {
56-
const fullPath = join(this.toolsDir, file);
57-
console.log(`Loading tool from: ${fullPath}`);
58-
const { default: ToolClass } = await import(`file://${fullPath}`);
59-
60-
if (!ToolClass) {
61-
console.log(`No default export found in ${file}`);
62-
return null;
63-
}
64-
65-
const tool = new ToolClass();
66-
if (this.validateTool(tool)) {
67-
console.log(`Successfully loaded tool: ${tool.name}`);
68-
return tool;
69-
}
70-
console.log(`Invalid tool found in ${file}`);
71-
return null;
72-
} catch (error) {
73-
console.error(`Error loading tool ${file}:`, error);
74-
return null;
113+
logger.debug(`Found files in directory: ${files.join(", ")}`);
114+
115+
const tools: BaseTool[] = [];
116+
117+
for (const file of files) {
118+
if (!this.isToolFile(file)) {
119+
continue;
120+
}
121+
122+
try {
123+
const fullPath = join(this.toolsDir, file);
124+
logger.debug(`Attempting to load tool from: ${fullPath}`);
125+
126+
const importPath = `file://${fullPath}`;
127+
const { default: ToolClass } = await import(importPath);
128+
129+
if (!ToolClass) {
130+
logger.warn(`No default export found in ${file}`);
131+
continue;
75132
}
76-
});
77133

78-
const tools = (await Promise.all(toolPromises)).filter(
79-
Boolean
80-
) as BaseTool[];
81-
console.log(
82-
`Loaded ${tools.length} tools: ${tools.map((t) => t.name).join(", ")}`
134+
const tool = new ToolClass();
135+
if (this.validateTool(tool)) {
136+
tools.push(tool);
137+
}
138+
} catch (error) {
139+
logger.error(`Error loading tool ${file}: ${error}`);
140+
}
141+
}
142+
143+
logger.debug(
144+
`Successfully loaded ${tools.length} tools: ${tools
145+
.map((t) => t.name)
146+
.join(", ")}`
83147
);
84148
return tools;
85149
} catch (error) {
86-
console.error(`Failed to load tools from ${this.toolsDir}:`, error);
150+
logger.error(`Failed to load tools: ${error}`);
87151
return [];
88152
}
89153
}

0 commit comments

Comments
 (0)