Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: Build and Publish Docker Image

on:
push:
tags:
- 'v*'

jobs:
build-and-push:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up QEMU
uses: docker/setup-qemu-action@v3

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: serkan-ozal/jaeger-mcp-server
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=ref,event=branch

- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
32 changes: 32 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
FROM node:18-slim

# Install supergateway globally
RUN npm install -g supergateway

# Set up jaeger-mcp
WORKDIR /app

# Copy jaeger-mcp package files
COPY package*.json ./
COPY scripts/ ./scripts/
COPY protos/ ./protos/

# Install dependencies
RUN npm ci

# Copy source files
COPY tsconfig.json ./
COPY src/ ./src/

# Build the application
RUN npm run build && chmod +x ./dist/index.js

# Environment variables
ENV NODE_ENV=production
ENV JAEGER_MCP_SSE_PORT=8000

# Expose port
EXPOSE ${JAEGER_MCP_SSE_PORT}

# Run supergateway with jaeger-mcp
ENTRYPOINT ["sh", "-c", "supergateway --stdio 'node /app/dist/index.js' --port ${JAEGER_MCP_SSE_PORT}"]
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ like VS Code, Claude, Cursor, Windsurf Github Copilot via the `jaeger-mcp-server
- `JAEGER_PORT`: HTTP or gRPC API port of the Jaeger instance to access. The default value is `16685` for the gRPC API and `16686` for the HTTP API.
- `JAEGER_AUTHORIZATION_HEADER`: `Authorization` HTTP header to be added into the requests for querying traces over Jaeger API (for ex. `Basic <Basic Auth Header>`)
- `JAEGER_PROTOCOL`: API protocol of the Jaeger instance to access. Valid values are `GRPC` and `HTTP`. The default value is `GRPC`. Valid
- `JAEGER_USE_DEFAULT_PORT`: If `false`, the Jaeger HTTP client won't attempt to use a port when `JAEGER_PORT` is not set. Default is `true`.
- `LOG_LEVEL`: Sets the log level from one of [`debug`, `info`, `warn`, `error`, `none`]. Default is `none`.

## Components

Expand Down Expand Up @@ -105,7 +107,7 @@ like VS Code, Claude, Cursor, Windsurf Github Copilot via the `jaeger-mcp-server
"stringAttribute": "str",
"integerAttribute": 123,
"doubleAttribute": 123.456,
"booleanAttribute": true,
"booleanAttribute": true
}
```
- `startTimeMin`:
Expand Down
6 changes: 6 additions & 0 deletions src/client/jaeger-grpc-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {
FindTracesResponse,
GetOperationsRequest,
GetOperationsResponse,
GetServiceGraphRequest,
GetServiceGraphResponse,
GetServicesRequest,
GetServicesResponse,
GetTraceRequest,
Expand Down Expand Up @@ -391,4 +393,8 @@ export class JaegerGrpcClient implements JaegerClient {
return this._handleError(err);
}
}

async getServiceGraph(request: GetServiceGraphRequest): Promise<GetServiceGraphResponse> {
return this._handleError('getServiceGraph not supported by gRPC');
}
}
48 changes: 32 additions & 16 deletions src/client/jaeger-http-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import {
FindTracesResponse,
GetOperationsRequest,
GetOperationsResponse,
GetServiceGraphRequest,
GetServiceGraphResponse,
GetServicesRequest,
GetServicesResponse,
GetTraceRequest,
Expand All @@ -22,19 +24,18 @@ const HTTP_STATUS_CODE_NOT_FOUND: number = 404;

export class JaegerHttpClient implements JaegerClient {
private readonly url: string;
private readonly port: number;
private readonly authorizationHeader: string | undefined;

constructor(clientConfigurations: ClientConfigurations) {
this.url = JaegerHttpClient._normalizeUrl(clientConfigurations.url);
this.port = JaegerHttpClient._normalizePort(
this.url,
clientConfigurations.port
this.url = JaegerHttpClient._normalizeUrl(
clientConfigurations.url,
clientConfigurations.port,
clientConfigurations.allowDefaultPort
);
this.authorizationHeader = clientConfigurations.authorizationHeader;
}

private static _normalizeUrl(url: string, port?: number): string {
private static _normalizeUrl(url: string, port?: number, allowDefaultPort?: boolean): string {
const schemaIdx: number = url.indexOf(URL_SCHEMA_SEPARATOR);
if (schemaIdx < 0) {
if (port === SECURE_URL_PORT) {
Expand All @@ -43,22 +44,22 @@ export class JaegerHttpClient implements JaegerClient {
url = `${INSECURE_URL_SCHEMA}${url}`;
}
}
return url;
}

private static _normalizePort(
normalizedUrl: string,
port?: number
): number {
if (normalizedUrl.startsWith(SECURE_URL_SCHEMA)) {
return port || SECURE_URL_PORT;
if (port) {
url = `${url}:${port}`
} else if (allowDefaultPort) {
port = url.startsWith(SECURE_URL_SCHEMA)
? SECURE_URL_PORT
: DEFAULT_PORT;
url = `${url}:${port}`;
}
return port || DEFAULT_PORT;

return url;
}

private async _get<R>(path: string, params?: any): Promise<R> {
const response: AxiosResponse = await axios.get(
`${this.url}:${this.port}/${path}`,
`${this.url}${path}`,
{
params,
headers: {
Expand Down Expand Up @@ -239,4 +240,19 @@ export class JaegerHttpClient implements JaegerClient {
return this._handleError(err);
}
}

async getServiceGraph(request: GetServiceGraphRequest): Promise<GetServiceGraphResponse> {
try {
const httpResponse: any = await this._get('/api/dependencies', {
'endTs': request.endTS,
'lookback': request.lookback,
});

return {
graphEdges: httpResponse.data
} as GetServiceGraphResponse;
} catch (err: any) {
return this._handleError(err);
}
}
}
4 changes: 4 additions & 0 deletions src/client/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import {
FindTracesResponse,
GetOperationsRequest,
GetOperationsResponse,
GetServiceGraphRequest,
GetServiceGraphResponse,
GetServicesRequest,
GetServicesResponse,
GetTraceRequest,
Expand All @@ -13,6 +15,7 @@ export type ClientConfigurations = {
url: string;
port?: number;
authorizationHeader?: string;
allowDefaultPort?: boolean;
};

export interface JaegerClient {
Expand All @@ -22,4 +25,5 @@ export interface JaegerClient {
): Promise<GetOperationsResponse>;
getTrace(request: GetTraceRequest): Promise<GetTraceResponse>;
findTraces(request: FindTracesRequest): Promise<FindTracesResponse>;
getServiceGraph(request: GetServiceGraphRequest): Promise<GetServiceGraphResponse>
}
6 changes: 6 additions & 0 deletions src/domain/commons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,9 @@ export type ResourceSpans = {
scopeSpans: ScopeSpans[];
schemaUrl?: string;
};

export type ServiceGraphEdge = {
parent: string;
child: string;
callCount: number;
};
5 changes: 5 additions & 0 deletions src/domain/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,8 @@ export type TraceQueryParameters = {
export type FindTracesRequest = {
query: TraceQueryParameters;
};

export type GetServiceGraphRequest = {
endTS: number;
lookback: number;
};
6 changes: 5 additions & 1 deletion src/domain/responses.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ResourceSpans, SpanKind } from './commons';
import { ResourceSpans, ServiceGraphEdge, SpanKind } from './commons';

export type Operation = {
name: string;
Expand All @@ -20,3 +20,7 @@ export type GetTraceResponse = {
export type FindTracesResponse = {
resourceSpans: ResourceSpans[];
};

export type GetServiceGraphResponse = {
graphEdges: ServiceGraphEdge[];
};
68 changes: 47 additions & 21 deletions src/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,33 @@ const BANNER_TEXT = '[JAEGER-MCP-SERVER]';
const BANNER_BG_COLOR = '#628816';
const BANNER_TEXT_COLOR = '#5ECAE0';

const DISABLED = true;
export enum LogLevel {
DEBUG = 0,
INFO = 1,
WARN = 2,
ERROR = 3,
NONE = 4
}

export function parseLogLevel(levelStr?: string): LogLevel {
if (!levelStr) {
return LogLevel.NONE;
}

const normalized = levelStr.toLowerCase().trim();

switch (normalized) {
case 'debug': return LogLevel.DEBUG;
case 'info': return LogLevel.INFO;
case 'warn': return LogLevel.WARN;
case 'error': return LogLevel.ERROR;
case 'none':
default: return LogLevel.NONE;
}
}

let debugEnabled = false;
// Initialize log level from environment
let currentLogLevel = parseLogLevel(process.env.LOG_LEVEL);

function _timeAsString(): string {
const date: Date = new Date();
Expand All @@ -20,7 +44,7 @@ function _timeAsString(): string {
}

function _normalizeArgs(...args: any[]): any[] {
if (isDebugEnabled()) {
if (currentLogLevel === LogLevel.DEBUG) {
return args;
} else {
return (args || []).map((arg) => {
Expand All @@ -39,34 +63,34 @@ function _normalizeArgs(...args: any[]): any[] {
}
}

export function isDebugEnabled(): boolean {
return debugEnabled;
export function getLogLevel(): LogLevel {
return currentLogLevel;
}

export function setDebugEnabled(enabled: boolean): void {
debugEnabled = enabled;
export function setLogLevel(level: LogLevel): void {
currentLogLevel = level;
}

export function debug(...args: any[]): void {
if (DISABLED) {
if (currentLogLevel > LogLevel.DEBUG) {
return;
}
if (isDebugEnabled()) {
console.debug(
chalk.bgHex(BANNER_BG_COLOR).hex(BANNER_TEXT_COLOR)(BANNER_TEXT),
_timeAsString(),
'|',
chalk.blue('DEBUG'),
'-',
..._normalizeArgs(...args)
);
}

console.debug(
chalk.bgHex(BANNER_BG_COLOR).hex(BANNER_TEXT_COLOR)(BANNER_TEXT),
_timeAsString(),
'|',
chalk.blue('DEBUG'),
'-',
..._normalizeArgs(...args)
);
}

export function info(...args: any[]): void {
if (DISABLED) {
if (currentLogLevel > LogLevel.INFO) {
return;
}

console.info(
chalk.bgHex(BANNER_BG_COLOR).hex(BANNER_TEXT_COLOR)(BANNER_TEXT),
_timeAsString(),
Expand All @@ -78,9 +102,10 @@ export function info(...args: any[]): void {
}

export function warn(...args: any[]): void {
if (DISABLED) {
if (currentLogLevel > LogLevel.WARN) {
return;
}

console.warn(
chalk.bgHex(BANNER_BG_COLOR).hex(BANNER_TEXT_COLOR)(BANNER_TEXT),
_timeAsString(),
Expand All @@ -92,9 +117,10 @@ export function warn(...args: any[]): void {
}

export function error(...args: any[]): void {
if (DISABLED) {
if (currentLogLevel > LogLevel.ERROR) {
return;
}

console.error(
chalk.bgHex(BANNER_BG_COLOR).hex(BANNER_TEXT_COLOR)(BANNER_TEXT),
_timeAsString(),
Expand Down
Loading