Skip to content

Commit 597c52c

Browse files
committed
chore: emit source/tools table upon startup
1 parent 82cdebf commit 597c52c

File tree

8 files changed

+183
-60
lines changed

8 files changed

+183
-60
lines changed

docs/plans/2025-12-03-startup-table-design.md

Lines changed: 0 additions & 37 deletions
This file was deleted.

src/connectors/manager.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,17 +39,13 @@ export class ConnectorManager {
3939
for (const source of sources) {
4040
await this.connectSource(source);
4141
}
42-
43-
console.error(`Successfully connected to ${sources.length} database source(s)`);
4442
}
4543

4644
/**
4745
* Connect to a single source (helper for connectWithSources)
4846
*/
4947
private async connectSource(source: SourceConfig): Promise<void> {
5048
const sourceId = source.id;
51-
console.error(`Connecting to source '${sourceId || "(default)"}' ...`);
52-
5349
// Build DSN from source config
5450
const dsn = buildDSNFromSource(source);
5551

@@ -148,8 +144,6 @@ export class ConnectorManager {
148144
options.readonly = source.readonly;
149145
}
150146
this.executeOptions.set(sourceId, options);
151-
152-
console.error(` Connected successfully`);
153147
}
154148

155149
/**

src/connectors/mariadb/index.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,9 +109,7 @@ export class MariaDBConnector implements Connector {
109109
this.pool = mariadb.createPool(connectionConfig);
110110

111111
// Test the connection
112-
console.error("Testing connection to MariaDB...");
113112
await this.pool.query("SELECT 1");
114-
console.error("Successfully connected to MariaDB database");
115113
} catch (err) {
116114
console.error("Failed to connect to MariaDB database:", err);
117115
throw err;

src/connectors/mysql/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,6 @@ export class MySQLConnector implements Connector {
112112

113113
// Test the connection
114114
const [rows] = await this.pool.query("SELECT 1");
115-
console.error("Successfully connected to MySQL database");
116115
} catch (err) {
117116
console.error("Failed to connect to MySQL database:", err);
118117
throw err;

src/connectors/postgres/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,6 @@ export class PostgresConnector implements Connector {
117117

118118
// Test the connection
119119
const client = await this.pool.connect();
120-
console.error("Successfully connected to PostgreSQL database");
121120
client.release();
122121
} catch (err) {
123122
console.error("Failed to connect to PostgreSQL database:", err);

src/connectors/sqlite/index.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,12 +132,10 @@ export class SQLiteConnector implements Connector {
132132
}
133133

134134
this.db = new Database(this.dbPath, dbOptions);
135-
console.error("Successfully connected to SQLite database");
136135

137136
// If an initialization script is provided, run it
138137
if (initScript) {
139138
this.db.exec(initScript);
140-
console.error("Successfully initialized database with script");
141139
}
142140
} catch (error) {
143141
console.error("Failed to connect to SQLite database:", error);

src/server.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import { buildDSNFromSource } from "./config/toml-loader.js";
1313
import { registerTools } from "./tools/index.js";
1414
import { listSources, getSource } from "./api/sources.js";
1515
import { listRequests } from "./api/requests.js";
16+
import { generateStartupTable, buildSourceDisplayInfo } from "./utils/startup-table.js";
17+
import { getToolsForSource } from "./utils/tool-metadata.js";
1618

1719
// Create __dirname equivalent for ES modules
1820
const __filename = fileURLToPath(import.meta.url);
@@ -124,13 +126,9 @@ See documentation for more details on configuring database connections.
124126

125127
// Resolve transport type (for MCP server)
126128
const transportData = resolveTransport();
127-
console.error(`MCP transport: ${transportData.type}`);
128129

129130
// Resolve port for HTTP server (only needed for http transport)
130-
const portData = transportData.type === "http" ? resolvePort() : null;
131-
if (portData) {
132-
console.error(`HTTP server port: ${portData.port} (source: ${portData.source})`);
133-
}
131+
const port = transportData.type === "http" ? resolvePort().port : null;
134132

135133
// Print ASCII art banner with version and slogan
136134
const readonly = isReadOnlyMode();
@@ -150,22 +148,24 @@ See documentation for more details on configuring database connections.
150148
modeDescriptions.push("only read only queries allowed");
151149
}
152150

153-
// Multi-source mode indicator
154-
if (sources.length > 1) {
155-
console.error(`Multi-source mode: ${sources.length} databases configured`);
156-
}
157-
158151
// Output mode information
159152
if (activeModes.length > 0) {
160153
console.error(`Running in ${activeModes.join(' and ')} mode - ${modeDescriptions.join(', ')}`);
161154
}
162155

163156
console.error(generateBanner(SERVER_VERSION, activeModes));
164157

158+
// Print sources and tools table
159+
const sourceDisplayInfos = buildSourceDisplayInfo(
160+
sources,
161+
(sourceId) => getToolsForSource(sourceId).map((t) => t.name),
162+
isDemo
163+
);
164+
console.error(generateStartupTable(sourceDisplayInfos));
165+
165166
// Set up transport-specific server
166167
if (transportData.type === "http") {
167168
// HTTP transport: Start Express server with MCP endpoint and admin console
168-
const port = portData!.port;
169169
const app = express();
170170

171171
// Enable JSON parsing

src/utils/startup-table.ts

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import type { SourceConfig } from "../types/config.js";
2+
3+
/**
4+
* Information about a source and its tools for display
5+
*/
6+
export interface SourceDisplayInfo {
7+
id: string;
8+
type: string;
9+
host: string;
10+
database: string;
11+
readonly: boolean;
12+
isDemo: boolean;
13+
tools: string[];
14+
}
15+
16+
/**
17+
* Unicode box drawing characters
18+
*/
19+
const BOX = {
20+
topLeft: "┌",
21+
topRight: "┐",
22+
bottomLeft: "└",
23+
bottomRight: "┘",
24+
horizontal: "─",
25+
vertical: "│",
26+
leftT: "├",
27+
rightT: "┤",
28+
bullet: "•",
29+
};
30+
31+
/**
32+
* Parse host and database from source config
33+
*/
34+
function parseHostAndDatabase(source: SourceConfig): { host: string; database: string } {
35+
// If DSN is provided, parse it
36+
if (source.dsn) {
37+
try {
38+
const url = new URL(source.dsn);
39+
const host = url.port ? `${url.hostname}:${url.port}` : url.hostname;
40+
const database = url.pathname.replace(/^\//, "") || "";
41+
return { host, database };
42+
} catch {
43+
return { host: "unknown", database: "" };
44+
}
45+
}
46+
47+
// Otherwise use individual connection params
48+
const host = source.host
49+
? source.port
50+
? `${source.host}:${source.port}`
51+
: source.host
52+
: "localhost";
53+
const database = source.database || "";
54+
55+
return { host, database };
56+
}
57+
58+
/**
59+
* Generate a horizontal line of specified width
60+
*/
61+
function horizontalLine(width: number, left: string, right: string): string {
62+
return left + BOX.horizontal.repeat(width - 2) + right;
63+
}
64+
65+
/**
66+
* Pad or truncate a string to fit a specific width
67+
*/
68+
function fitString(str: string, width: number): string {
69+
if (str.length > width) {
70+
return str.slice(0, width - 1) + "…";
71+
}
72+
return str.padEnd(width);
73+
}
74+
75+
/**
76+
* Generate the startup table showing sources and their tools
77+
*/
78+
export function generateStartupTable(sources: SourceDisplayInfo[]): string {
79+
if (sources.length === 0) {
80+
return "";
81+
}
82+
83+
// Calculate column widths based on content
84+
const idTypeWidth = Math.max(
85+
20,
86+
...sources.map((s) => `${s.id} (${s.type})`.length)
87+
);
88+
const hostDbWidth = Math.max(
89+
24,
90+
...sources.map((s) => {
91+
const hostDb = s.database ? `${s.host}/${s.database}` : s.host;
92+
return hostDb.length;
93+
})
94+
);
95+
const modeWidth = 10;
96+
97+
// Total width: 2 for borders + content + 2 spaces padding per column + 2 separators
98+
const totalWidth = 2 + idTypeWidth + 3 + hostDbWidth + 3 + modeWidth + 2;
99+
100+
const lines: string[] = [];
101+
102+
for (let i = 0; i < sources.length; i++) {
103+
const source = sources[i];
104+
const isFirst = i === 0;
105+
const isLast = i === sources.length - 1;
106+
107+
// Top border (only for first source)
108+
if (isFirst) {
109+
lines.push(horizontalLine(totalWidth, BOX.topLeft, BOX.topRight));
110+
}
111+
112+
// Source header row
113+
const idType = fitString(`${source.id} (${source.type})`, idTypeWidth);
114+
const hostDb = fitString(
115+
source.database ? `${source.host}/${source.database}` : source.host,
116+
hostDbWidth
117+
);
118+
119+
// Mode indicators
120+
const modes: string[] = [];
121+
if (source.isDemo) modes.push("DEMO");
122+
if (source.readonly) modes.push("READ-ONLY");
123+
const modeStr = fitString(modes.join(" "), modeWidth);
124+
125+
lines.push(
126+
`${BOX.vertical} ${idType} ${BOX.vertical} ${hostDb} ${BOX.vertical} ${modeStr} ${BOX.vertical}`
127+
);
128+
129+
// Separator after header
130+
lines.push(horizontalLine(totalWidth, BOX.leftT, BOX.rightT));
131+
132+
// Tool rows
133+
for (const tool of source.tools) {
134+
const toolLine = ` ${BOX.bullet} ${tool}`;
135+
lines.push(
136+
`${BOX.vertical} ${fitString(toolLine, totalWidth - 4)} ${BOX.vertical}`
137+
);
138+
}
139+
140+
// Bottom border or separator
141+
if (isLast) {
142+
lines.push(horizontalLine(totalWidth, BOX.bottomLeft, BOX.bottomRight));
143+
} else {
144+
lines.push(horizontalLine(totalWidth, BOX.leftT, BOX.rightT));
145+
}
146+
}
147+
148+
return lines.join("\n");
149+
}
150+
151+
/**
152+
* Build SourceDisplayInfo from source configs and tool names
153+
*/
154+
export function buildSourceDisplayInfo(
155+
sourceConfigs: SourceConfig[],
156+
getToolsForSource: (sourceId: string) => string[],
157+
isDemo: boolean
158+
): SourceDisplayInfo[] {
159+
return sourceConfigs.map((source) => {
160+
const { host, database } = parseHostAndDatabase(source);
161+
162+
return {
163+
id: source.id,
164+
type: source.type || "sqlite",
165+
host,
166+
database,
167+
readonly: source.readonly || false,
168+
isDemo,
169+
tools: getToolsForSource(source.id),
170+
};
171+
});
172+
}

0 commit comments

Comments
 (0)