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
4 changes: 2 additions & 2 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ export default [
caughtErrorsIgnorePattern: "^_",
},
],
"@typescript-eslint/no-explicit-any": "off", // Allow any for now in existing codebase
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-function-return-type": "error", // Require explicit return types
"@typescript-eslint/no-explicit-any": "error", // Disallow explicit any types
"@typescript-eslint/explicit-module-boundary-types": "off",

// General JavaScript/TypeScript rules - most important ones
Expand Down
30 changes: 15 additions & 15 deletions src/extract-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,18 @@
public parameters?: OpenAPIV3.ParameterObject[];
public responses: OpenAPIV3.ResponsesObject;

constructor(rawOperation: any) {
constructor(rawOperation: Partial<OpenAPIV3.OperationObject>) {
Object.assign(this, rawOperation);
this.deprecated = rawOperation.deprecated || false;
this.responses = rawOperation.responses || {};
this["x-ai-description"] = rawOperation["x-ai-description"] || "";

Check failure on line 37 in src/extract-tools.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

Property 'x-ai-description' does not exist on type 'Partial<{ tags?: string[] | undefined; summary?: string | undefined; description?: string | undefined; externalDocs?: ExternalDocumentationObject | undefined; ... 7 more ...; servers?: ServerObject[] | undefined; }>'. Did you mean 'description'?
}
}

/**
* Normalize a value to boolean if it looks like a boolean; otherwise undefined.
*/
export function normalizeBoolean(value: any): boolean | undefined {
export function normalizeBoolean(value: unknown): boolean | undefined {
if (typeof value === "boolean") return value;
if (typeof value === "string") {
const normalized = value.trim().toLowerCase();
Expand All @@ -62,7 +62,7 @@
operation: AAPOperationObject,
defaultInclude = true,
): boolean {
const opRaw = (operation as any)["x-mcp"];
const opRaw = (operation as Record<string, unknown>)["x-mcp"];

Check failure on line 65 in src/extract-tools.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

Conversion of type 'AAPOperationObject' to type 'Record<string, unknown>' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
const opVal = normalizeBoolean(opRaw);
if (typeof opVal !== "undefined") return opVal;
if (typeof opRaw !== "undefined") {
Expand All @@ -72,7 +72,7 @@
`-> expected boolean or 'true'/'false'. Falling back to path/root/default.`,
);
}
const pathRaw = (pathItem as any)["x-mcp"];
const pathRaw = (pathItem as Record<string, unknown>)["x-mcp"];
const pathVal = normalizeBoolean(pathRaw);
if (typeof pathVal !== "undefined") return pathVal;
if (typeof pathRaw !== "undefined") {
Expand All @@ -82,7 +82,7 @@
`-> expected boolean or 'true'/'false'. Falling back to root/default.`,
);
}
const rootRaw = (api as any)["x-mcp"];
const rootRaw = (api as Record<string, unknown>)["x-mcp"];

Check failure on line 85 in src/extract-tools.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

Conversion of type 'Document<{}>' to type 'Record<string, unknown>' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
const rootVal = normalizeBoolean(rootRaw);
if (typeof rootVal !== "undefined") return rootVal;
if (typeof rootRaw !== "undefined") {
Expand Down Expand Up @@ -148,14 +148,14 @@
// Detect cycles
if (seen.has(schema)) {
console.warn(
`Cycle detected in schema${(schema as any).title ? ` "${(schema as any).title}"` : ""}, returning generic object to break recursion.`,
`Cycle detected in schema${(schema as Record<string, unknown>).title ? ` "${(schema as Record<string, unknown>).title}"` : ""}, returning generic object to break recursion.`,
);
return { type: "object" };
}
seen.add(schema);
try {
// Create a copy of the schema to modify
const jsonSchema: any = { ...schema };
const jsonSchema: Record<string, unknown> = { ...schema };
// Convert integer type to number (JSON Schema compatible)
if (schema.type === "integer") jsonSchema.type = "number";
// Remove OpenAPI-specific properties that aren't in JSON Schema
Expand All @@ -167,7 +167,7 @@
delete jsonSchema.readOnly;
delete jsonSchema.writeOnly;
// Handle nullable properties by adding null to the type
if ((schema as any).nullable) {
if ((schema as Record<string, unknown>).nullable) {
if (Array.isArray(jsonSchema.type)) {
if (!jsonSchema.type.includes("null")) jsonSchema.type.push("null");
} else if (typeof jsonSchema.type === "string") {
Expand All @@ -178,11 +178,11 @@
}
// Recursively process object properties
if (jsonSchema.type === "object" && jsonSchema.properties) {
const mappedProps: any = {};
const mappedProps: Record<string, JSONSchema7 | boolean> = {};
for (const [key, propSchema] of Object.entries(jsonSchema.properties)) {
if (typeof propSchema === "object" && propSchema !== null) {
mappedProps[key] = mapOpenApiSchemaToJsonSchema(
propSchema as any,
propSchema as OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject,
seen,
);
} else if (typeof propSchema === "boolean") {
Expand All @@ -198,7 +198,7 @@
jsonSchema.items !== null
) {
jsonSchema.items = mapOpenApiSchemaToJsonSchema(
jsonSchema.items as any,
jsonSchema.items as OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject,
seen,
);
}
Expand Down Expand Up @@ -358,9 +358,9 @@
} catch (error) {
const loc = operation.operationId || `${method} ${path}`;
const extVal =
(operation as any)["x-mcp"] ??
(pathItem as any)["x-mcp"] ??
(api as any)["x-mcp"];
(operation as Record<string, unknown>)["x-mcp"] ??

Check failure on line 361 in src/extract-tools.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

Conversion of type 'AAPOperationObject' to type 'Record<string, unknown>' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
(pathItem as Record<string, unknown>)["x-mcp"] ??
(api as Record<string, unknown>)["x-mcp"];

Check failure on line 363 in src/extract-tools.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

Conversion of type 'Document<{}>' to type 'Record<string, unknown>' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
let extPreview: string;
try {
extPreview = JSON.stringify(extVal);
Expand Down Expand Up @@ -438,7 +438,7 @@
(value) => value[1],
);
const missingDescriptions = propertiesEntries.filter(
(p: any) => p && typeof p === "object" && !p.description,
(p: JSONSchema7 | boolean) => p && typeof p === "object" && !(p as JSONSchema7).description,
);
if (missingDescriptions.length) {
logs.push({
Expand Down
49 changes: 31 additions & 18 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,19 @@
}

// TypeScript interfaces
interface UserInfo {
is_superuser: boolean;
is_platform_auditor: boolean;
}

interface UserApiResponse {
results: UserInfo[];
}

interface ExtendedTransport extends StreamableHTTPServerTransport {
categoryOverride?: string;
userAgent?: string;
}

interface SessionData {
[sessionId: string]: {
Expand Down Expand Up @@ -197,7 +210,7 @@
);
}

const data = (await response.json()) as any;
const data = (await response.json()) as UserApiResponse;

if (
!data.results ||
Expand All @@ -207,7 +220,7 @@
throw new Error("Invalid response format from /api/gateway/v1/me/");
}

const userInfo = data.results[0] as any;
const userInfo = data.results[0];
return {
is_superuser: userInfo.is_superuser || false,
is_platform_auditor: userInfo.is_platform_auditor || false,
Expand Down Expand Up @@ -307,7 +320,7 @@

try {
const tools = extractToolsFromApi(
bundledSpec as any,
bundledSpec as OpenAPIV3.Document,

Check failure on line 323 in src/index.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

Cannot find namespace 'OpenAPIV3'.
) as AAPMcpToolDefinition[];
const filteredTools = tools.filter((tool) => {
tool.service = spec.service; // Add service information to each tool
Expand Down Expand Up @@ -446,9 +459,7 @@

// Get category override from transport if available
const transport = sessionId ? transports[sessionId] : null;
const categoryOverride = transport
? (transport as any).categoryOverride
: undefined;
const categoryOverride = transport?.categoryOverride;

// Determine user category based on category override
const category = getUserCategory(categoryOverride);
Expand Down Expand Up @@ -503,15 +514,15 @@
// Get user-agent from transport (if available)
let userAgent = "unknown";
if (sessionId && transports[sessionId]) {
const transport = transports[sessionId] as any;
const transport = transports[sessionId];
userAgent = transport.userAgent || "unknown";
}

// Get the Bearer token for this session
const bearerToken = getBearerTokenForSession(sessionId);

// Execute the tool by making HTTP request
let result: any;
let result: unknown;
let response: Response | undefined;
let fullUrl: string = `${CONFIG.BASE_URL}${tool.pathTemplate}`;
let requestOptions: RequestInit | undefined;
Expand Down Expand Up @@ -590,7 +601,7 @@
method: tool.method.toUpperCase(),
userAgent: userAgent,
},
result,

Check failure on line 604 in src/index.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

Argument of type 'unknown' is not assignable to parameter of type 'Record<string, unknown>'.
response.status,
_startTime,
);
Expand Down Expand Up @@ -645,7 +656,7 @@
});

// Global state management
const transports: Record<string, StreamableHTTPServerTransport> = {};
const transports: Record<string, ExtendedTransport> = {};
const sessionData: SessionData = {};

const app = express();
Expand All @@ -664,7 +675,7 @@
req: express.Request,
res: express.Response,
categoryOverride?: string,
) => {
): Promise<void> => {
const sessionId = req.headers["mcp-session-id"] as string;
const authHeader = req.headers["authorization"] as string;

Expand All @@ -683,16 +694,18 @@
} else if (!sessionId && isInitializeRequest(req.body)) {
// New initialization request
transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
onsessioninitialized: async (sessionId: string) => {
sessionIdGenerator: (): string => randomUUID(),
onsessioninitialized: async (sessionId: string): Promise<void> => {
console.log(
`${getTimestamp()} Session initialized${categoryOverride ? ` with category override: ${categoryOverride}` : ""}`,
);
transports[sessionId] = transport;

// Store category override and user-agent in transport for later access
(transport as any).categoryOverride = categoryOverride;
(transport as any).userAgent = req.headers["user-agent"] || "unknown";
const extendedTransport = transport as ExtendedTransport;
extendedTransport.categoryOverride = categoryOverride;
extendedTransport.userAgent = req.headers["user-agent"] || "unknown";

transports[sessionId] = extendedTransport;

// Extract and validate the bearer token
const token = extractBearerToken(authHeader);
Expand All @@ -718,7 +731,7 @@
});

// Set up onclose handler to clean up transport when closed
transport.onclose = () => {
transport.onclose = (): void => {
const sid = transport.sessionId;
if (sid && transports[sid]) {
console.log(
Expand Down Expand Up @@ -772,7 +785,7 @@
req: express.Request,
res: express.Response,
_categoryOverride?: string,
) => {
): Promise<void> => {
const sessionId = req.headers["mcp-session-id"] as string;
const _authHeader = req.headers["authorization"] as string;

Expand Down Expand Up @@ -801,7 +814,7 @@
req: express.Request,
res: express.Response,
_categoryOverride?: string,
) => {
): Promise<void> => {
const sessionId = req.headers["mcp-session-id"] as string;

if (!sessionId || !transports[sessionId]) {
Expand Down Expand Up @@ -1130,7 +1143,7 @@
lastEntries = lastEntries.filter((entry) => {
const entryUserAgent = entry.payload?.userAgent || "unknown";
return entryUserAgent
.toLowerCase()

Check failure on line 1146 in src/index.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

Property 'toLowerCase' does not exist on type '{}'.
.includes(userAgentFilter.toLowerCase());
});
}
Expand All @@ -1156,7 +1169,7 @@
const userAgentSummary = lastEntries.reduce(
(acc, entry) => {
const userAgent = entry.payload?.userAgent || "unknown";
acc[userAgent] = (acc[userAgent] || 0) + 1;

Check failure on line 1172 in src/index.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

Type '{}' cannot be used as an index type.
return acc;
},
{} as Record<string, number>,
Expand Down
10 changes: 5 additions & 5 deletions src/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@ import { metricsService } from "./metrics.js";
export interface LogEntry {
timestamp: string;
endpoint: string;
payload: any;
response: any;
payload: Record<string, unknown>;
response: Record<string, unknown>;
return_code: number;
}

export interface Tool {
name: string;
service?: string;
category?: string;
[key: string]: any;
[key: string]: unknown;
}

export class ToolLogger {
Expand All @@ -36,8 +36,8 @@ export class ToolLogger {
async logToolAccess(
tool: Tool,
endpoint: string,
payload: any,
response: any,
payload: Record<string, unknown>,
response: Record<string, unknown>,
returnCode: number,
startTime?: number,
_sessionId?: string,
Expand Down
6 changes: 3 additions & 3 deletions src/views/logs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ interface LogsEntry {
toolName: string;
return_code: number;
endpoint: string;
payload?: any;
payload?: Record<string, unknown>;
userAgent?: string;
}

Expand Down Expand Up @@ -35,12 +35,12 @@ export const renderLogs = (data: LogsData): string => {
} = data;

// Helper function to format timestamp for display
const formatTimestamp = (timestamp: string) => {
const formatTimestamp = (timestamp: string): string => {
return new Date(timestamp).toLocaleString();
};

// Helper function to get status color
const getStatusColor = (code: number) => {
const getStatusColor = (code: number): string => {
if (code >= 200 && code < 300) return "#28a745"; // green
if (code >= 300 && code < 400) return "#ffc107"; // yellow
if (code >= 400 && code < 500) return "#fd7e14"; // orange
Expand Down
18 changes: 9 additions & 9 deletions src/views/tool-details.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ interface LogEntry {
timestamp: string;
return_code: number;
endpoint: string;
response?: any;
response?: Record<string, unknown>;
}

interface CategoryWithAccess {
Expand Down Expand Up @@ -35,12 +35,12 @@ export const renderToolDetails = (data: ToolDetailsData): string => {
} = data;

// Helper function to format timestamp for display
const formatTimestamp = (timestamp: string) => {
const formatTimestamp = (timestamp: string): string => {
return new Date(timestamp).toLocaleString();
};

// Helper function to get status color
const getStatusColor = (code: number) => {
const getStatusColor = (code: number): string => {
if (code >= 200 && code < 300) return "#28a745"; // green
if (code >= 300 && code < 400) return "#ffc107"; // yellow
if (code >= 400 && code < 500) return "#fd7e14"; // orange
Expand All @@ -49,7 +49,7 @@ export const renderToolDetails = (data: ToolDetailsData): string => {
};

// Helper function to get status text
const getStatusText = (code: number) => {
const getStatusText = (code: number): string => {
if (code >= 200 && code < 300) return "Success";
if (code >= 300 && code < 400) return "Redirect";
if (code >= 400 && code < 500) return "Client Error";
Expand All @@ -58,17 +58,17 @@ export const renderToolDetails = (data: ToolDetailsData): string => {
};

// Format the input schema for display
const formatSchema = (schema: any, level = 0): string => {
const formatSchema = (schema: unknown, level = 0): string => {
if (!schema) return "No schema defined";

const indent = " ".repeat(level);
let result = "";

if (schema.type === "object" && schema.properties) {
if ((schema as Record<string, unknown>).type === "object" && (schema as Record<string, unknown>).properties) {
result += "{\n";
for (const [key, value] of Object.entries(schema.properties)) {
const prop = value as any;
const required = schema.required?.includes(key) ? " (required)" : "";
for (const [key, value] of Object.entries((schema as Record<string, unknown>).properties as Record<string, unknown>)) {
const prop = value as Record<string, unknown>;
const required = ((schema as Record<string, unknown>).required as string[])?.includes(key) ? " (required)" : "";
result += `${indent} "${key}"${required}: `;
if (prop.type === "object") {
result += formatSchema(prop, level + 1);
Expand Down
Loading