Skip to main content
Glama

Fountain Pen Ink MCP Server

lint-report.json35.3 kB
[{"filePath":"/Users/ewilderj/git/inks-mcp/src/index.ts","messages":[{"ruleId":"@typescript-eslint/consistent-type-imports","severity":1,"message":"All imports in the declaration are only used as types. Use `import type`.","line":11,"column":1,"nodeType":"ImportDeclaration","messageId":"typeOverValue","endLine":11,"endColumn":98,"fix":{"range":[399,399],"text":" type"}},{"ruleId":"@typescript-eslint/no-unused-vars","severity":2,"message":"'rgbToBgr' is defined but never used.","line":15,"column":3,"nodeType":null,"messageId":"unusedVar","endLine":15,"endColumn":11},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":2,"message":"Unsafe assignment of an `any` value.","line":56,"column":13,"nodeType":"VariableDeclarator","messageId":"anyAssignment","endLine":56,"endColumn":78},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":2,"message":"Unsafe assignment of an `any` value.","line":59,"column":7,"nodeType":"AssignmentExpression","messageId":"anyAssignment","endLine":62,"endColumn":10},{"ruleId":"@typescript-eslint/no-unsafe-call","severity":2,"message":"Unsafe call of a(n) `any` typed value.","line":59,"column":24,"nodeType":"MemberExpression","messageId":"unsafeCall","endLine":59,"endColumn":40},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access .map on an `any` value.","line":59,"column":37,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":59,"endColumn":40},{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":59,"column":47,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":59,"endColumn":50,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[1574,1577],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[1574,1577],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]},{"ruleId":"@typescript-eslint/no-unsafe-return","severity":2,"message":"Unsafe return of a value of type `any`.","line":59,"column":56,"nodeType":"ObjectExpression","messageId":"unsafeReturn","endLine":62,"endColumn":8},{"ruleId":"@typescript-eslint/no-unsafe-argument","severity":2,"message":"Unsafe argument of type `any` assigned to a parameter of type `[number, number, number]`.","line":61,"column":23,"nodeType":"MemberExpression","messageId":"unsafeArgument","endLine":61,"endColumn":30},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access .rgb on an `any` value.","line":61,"column":27,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":61,"endColumn":30},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":2,"message":"Unsafe assignment of an `any` value.","line":66,"column":7,"nodeType":"AssignmentExpression","messageId":"anyAssignment","endLine":66,"endColumn":79},{"ruleId":"@typescript-eslint/require-await","severity":2,"message":"Async arrow function has no 'await' expression.","line":89,"column":68,"nodeType":"ArrowFunctionExpression","messageId":"missingAwait","endLine":89,"endColumn":70,"suggestions":[{"messageId":"removeAsync","fix":{"range":[2547,2553],"text":""},"desc":"Remove 'async'."}]},{"ruleId":"@typescript-eslint/no-unsafe-return","severity":2,"message":"Unsafe return of a value of type `any`.","line":245,"column":13,"nodeType":"ReturnStatement","messageId":"unsafeReturn","endLine":245,"endColumn":101},{"ruleId":"@typescript-eslint/no-unsafe-return","severity":2,"message":"Unsafe return of a value of type `any`.","line":248,"column":13,"nodeType":"ReturnStatement","messageId":"unsafeReturn","endLine":252,"endColumn":15},{"ruleId":"@typescript-eslint/no-unsafe-argument","severity":2,"message":"Unsafe argument of type `any` assigned to a parameter of type `\"complementary\" | \"analogous\" | \"triadic\" | \"split-complementary\" | undefined`.","line":251,"column":15,"nodeType":"TSAsExpression","messageId":"unsafeArgument","endLine":251,"endColumn":34},{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":251,"column":31,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":251,"endColumn":34,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[8339,8342],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[8339,8342],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]},{"ruleId":"@typescript-eslint/require-await","severity":2,"message":"Async method 'searchInksByName' has no 'await' expression.","line":270,"column":3,"nodeType":"FunctionExpression","messageId":"missingAwait","endLine":270,"endColumn":33,"suggestions":[{"messageId":"removeAsync","fix":{"range":[8705,8711],"text":""},"desc":"Remove 'async'."}]},{"ruleId":"@typescript-eslint/require-await","severity":2,"message":"Async method 'searchInksByColor' has no 'await' expression.","line":301,"column":3,"nodeType":"FunctionExpression","messageId":"missingAwait","endLine":301,"endColumn":34,"suggestions":[{"messageId":"removeAsync","fix":{"range":[9443,9449],"text":""},"desc":"Remove 'async'."}]},{"ruleId":"@typescript-eslint/no-unused-vars","severity":2,"message":"'error' is defined but never used.","line":328,"column":14,"nodeType":null,"messageId":"unusedVar","endLine":328,"endColumn":19},{"ruleId":"@typescript-eslint/require-await","severity":2,"message":"Async method 'getInkDetails' has no 'await' expression.","line":333,"column":3,"nodeType":"FunctionExpression","messageId":"missingAwait","endLine":333,"endColumn":30,"suggestions":[{"messageId":"removeAsync","fix":{"range":[10356,10362],"text":""},"desc":"Remove 'async'."}]},{"ruleId":"@typescript-eslint/require-await","severity":2,"message":"Async method 'getInksByMaker' has no 'await' expression.","line":362,"column":3,"nodeType":"FunctionExpression","messageId":"missingAwait","endLine":362,"endColumn":31,"suggestions":[{"messageId":"removeAsync","fix":{"range":[11098,11104],"text":""},"desc":"Remove 'async'."}]},{"ruleId":"@typescript-eslint/require-await","severity":2,"message":"Async method 'analyzeColor' has no 'await' expression.","line":395,"column":3,"nodeType":"FunctionExpression","messageId":"missingAwait","endLine":395,"endColumn":29,"suggestions":[{"messageId":"removeAsync","fix":{"range":[11903,11977],"text":"analyzeColor(colorHex: string, maxResults: number = 5): any"},"desc":"Remove 'async'."}]},{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":395,"column":81,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":395,"endColumn":84,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[11973,11976],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[11973,11976],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]},{"ruleId":"@typescript-eslint/no-unused-vars","severity":2,"message":"'error' is defined but never used.","line":421,"column":14,"nodeType":null,"messageId":"unusedVar","endLine":421,"endColumn":19},{"ruleId":"@typescript-eslint/require-await","severity":2,"message":"Async method 'getColorPalette' has no 'await' expression.","line":426,"column":3,"nodeType":"FunctionExpression","messageId":"missingAwait","endLine":426,"endColumn":32,"suggestions":[{"messageId":"removeAsync","fix":{"range":[12855,13020],"text":"getColorPalette(\n theme: string,\n paletteSize: number,\n harmony?: 'complementary' | 'analogous' | 'triadic' | 'split-complementary',\n ): any"},"desc":"Remove 'async'."}]},{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":430,"column":14,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":430,"endColumn":17,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[13016,13019],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[13016,13019],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]},{"ruleId":"@typescript-eslint/no-unused-vars","severity":2,"message":"'error' is defined but never used.","line":535,"column":16,"nodeType":null,"messageId":"unusedVar","endLine":535,"endColumn":21},{"ruleId":"@typescript-eslint/no-unused-vars","severity":2,"message":"'error' is defined but never used.","line":543,"column":16,"nodeType":null,"messageId":"unusedVar","endLine":543,"endColumn":21}],"suppressedMessages":[],"errorCount":27,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":1,"source":"#!/usr/bin/env node\n\nimport { Server } from '@modelcontextprotocol/sdk/server/index.js';\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\nimport { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';\nimport Fuse from 'fuse.js';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { fileURLToPath } from 'url';\n\nimport { InkColor, InkSearchData, SearchResult, ColorAnalysis, PaletteResult } from './types.js';\nimport {\n hexToRgb,\n bgrToRgb,\n rgbToBgr,\n rgbToHex,\n findClosestInks,\n getColorFamily,\n getColorDescription,\n createSearchResult,\n rgbToHsl,\n hslToRgb,\n generateHarmonyColors,\n} from './utils.js';\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\n\nclass InkMCPServer {\n private server: Server;\n private inkColors: InkColor[] = [];\n private inkSearchData: InkSearchData[] = [];\n private fuse!: Fuse<InkSearchData>;\n\n constructor() {\n this.server = new Server(\n {\n name: 'fountain-pen-ink-server',\n version: '1.0.0',\n },\n {\n capabilities: {\n tools: {},\n },\n },\n );\n\n this.setupToolHandlers();\n this.loadData();\n }\n\n private loadData() {\n try {\n // Load ink colors and convert BGR to RGB immediately\n const inkColorsPath = path.join(__dirname, '../data/ink-colors.json');\n const rawInkColors = JSON.parse(fs.readFileSync(inkColorsPath, 'utf8'));\n\n // Convert BGR to RGB at load time\n this.inkColors = rawInkColors.map((ink: any) => ({\n ...ink,\n rgb: bgrToRgb(ink.rgb), // Convert BGR data to true RGB\n }));\n\n // Load search metadata\n const searchDataPath = path.join(__dirname, '../data/search.json');\n this.inkSearchData = JSON.parse(fs.readFileSync(searchDataPath, 'utf8'));\n\n // Setup fuzzy search\n this.fuse = new Fuse(this.inkSearchData, {\n keys: ['fullname', 'name', 'maker'],\n threshold: 0.3,\n minMatchCharLength: 2,\n ignoreLocation: true,\n });\n\n console.error(\n `Loaded ${this.inkColors.length} inks and ${this.inkSearchData.length} search entries (BGR→RGB converted)`,\n );\n } catch (error) {\n console.error('Error loading ink data:', error);\n }\n }\n\n private getInkMetadata(inkId: string): InkSearchData | undefined {\n return this.inkSearchData.find((item) => item.ink_id === inkId);\n }\n\n private setupToolHandlers() {\n this.server.setRequestHandler(ListToolsRequestSchema, async () => {\n return {\n tools: [\n {\n name: 'search_inks_by_name',\n description: 'Search for fountain pen inks by name using fuzzy matching',\n inputSchema: {\n type: 'object',\n properties: {\n query: {\n type: 'string',\n description: 'Search term for ink name',\n },\n max_results: {\n type: 'number',\n description: 'Maximum number of results to return (default: 20)',\n default: 20,\n },\n },\n required: ['query'],\n },\n },\n {\n name: 'search_inks_by_color',\n description: 'Find inks similar to a given color using RGB matching',\n inputSchema: {\n type: 'object',\n properties: {\n color: {\n type: 'string',\n description: 'Hex color code (e.g., \"#FF5733\")',\n },\n max_results: {\n type: 'number',\n description: 'Maximum number of results to return (default: 20)',\n default: 20,\n },\n },\n required: ['color'],\n },\n },\n {\n name: 'get_ink_details',\n description: 'Get complete information about a specific ink',\n inputSchema: {\n type: 'object',\n properties: {\n ink_id: {\n type: 'string',\n description: 'The unique identifier for the ink',\n },\n },\n required: ['ink_id'],\n },\n },\n {\n name: 'get_inks_by_maker',\n description: 'List all inks from a specific manufacturer',\n inputSchema: {\n type: 'object',\n properties: {\n maker: {\n type: 'string',\n description: 'Manufacturer name (e.g., \"sailor\", \"diamine\")',\n },\n max_results: {\n type: 'number',\n description: 'Maximum number of results to return (default: 50)',\n default: 50,\n },\n },\n required: ['maker'],\n },\n },\n {\n name: 'analyze_color',\n description: 'Analyze a color and provide ink knowledge context',\n inputSchema: {\n type: 'object',\n properties: {\n color: {\n type: 'string',\n description: 'Hex color code (e.g., \"#FF5733\")',\n },\n max_results: {\n type: 'number',\n description: 'Maximum number of closest inks to return (default: 5)',\n default: 5,\n },\n },\n required: ['color'],\n },\n },\n {\n name: 'get_color_palette',\n description:\n 'Generate a themed or harmony-based palette of inks. Supports three modes: 1) Predefined themes (warm, cool, earth, ocean, autumn, spring, summer, winter, pastel, vibrant, monochrome, sunset, forest), 2) Custom hex color lists (comma-separated), 3) Color harmony generation from a base hex color.',\n inputSchema: {\n type: 'object',\n properties: {\n theme: {\n type: 'string',\n description:\n 'Theme name (e.g., \"warm\", \"ocean\"), comma-separated hex colors (e.g., \"#FF0000,#00FF00\"), or single hex color for harmony generation (e.g., \"#FF0000\").',\n },\n palette_size: {\n type: 'number',\n description: 'Number of inks in the palette (default: 5)',\n default: 5,\n },\n harmony: {\n type: 'string',\n description:\n 'Color harmony rule to apply when theme is a single hex color. Options: \"complementary\", \"analogous\", \"triadic\", \"split-complementary\". Requires theme to be a valid hex color.',\n enum: ['complementary', 'analogous', 'triadic', 'split-complementary'],\n },\n },\n required: ['theme'],\n additionalProperties: false,\n },\n },\n ],\n };\n });\n\n this.server.setRequestHandler(CallToolRequestSchema, async (request) => {\n const { name, arguments: args } = request.params;\n\n if (!args) {\n throw new Error('Missing arguments');\n }\n\n try {\n switch (name) {\n case 'search_inks_by_name':\n return await this.searchInksByName(\n args.query as string,\n (args.max_results as number) || 20,\n );\n\n case 'search_inks_by_color':\n return await this.searchInksByColor(\n args.color as string,\n (args.max_results as number) || 20,\n );\n\n case 'get_ink_details':\n return await this.getInkDetails(args.ink_id as string);\n\n case 'get_inks_by_maker':\n return await this.getInksByMaker(\n args.maker as string,\n (args.max_results as number) || 50,\n );\n\n case 'analyze_color':\n return await this.analyzeColor(args.color as string, (args.max_results as number) || 5);\n\n case 'get_color_palette':\n return await this.getColorPalette(\n args.theme as string,\n (args.palette_size as number) || 5,\n args.harmony as any,\n );\n\n default:\n throw new Error(`Unknown tool: ${name}`);\n }\n } catch (error) {\n return {\n content: [\n {\n type: 'text',\n text: `Error: ${error instanceof Error ? error.message : String(error)}`,\n },\n ],\n };\n }\n });\n }\n\n private async searchInksByName(query: string, maxResults: number) {\n const searchResults = this.fuse.search(query);\n const results: SearchResult[] = [];\n\n for (const result of searchResults.slice(0, maxResults)) {\n const metadata = result.item;\n const inkColor = this.inkColors.find((ink) => ink.ink_id === metadata.ink_id);\n\n if (inkColor) {\n results.push(createSearchResult(inkColor, metadata));\n }\n }\n\n return {\n content: [\n {\n type: 'text',\n text: JSON.stringify(\n {\n query,\n results_count: results.length,\n results,\n },\n null,\n 2,\n ),\n },\n ],\n };\n }\n\n private async searchInksByColor(colorHex: string, maxResults: number) {\n try {\n const targetRgb = hexToRgb(colorHex);\n const closestInks = findClosestInks(targetRgb, this.inkColors, maxResults);\n\n const results: SearchResult[] = closestInks.map((ink) => {\n const metadata = this.getInkMetadata(ink.ink_id);\n return createSearchResult(ink, metadata, ink.distance);\n });\n\n return {\n content: [\n {\n type: 'text',\n text: JSON.stringify(\n {\n target_color: colorHex,\n target_rgb: targetRgb,\n results_count: results.length,\n results,\n },\n null,\n 2,\n ),\n },\n ],\n };\n } catch (error) {\n throw new Error(`Invalid color format: ${colorHex}. Please use hex format like #FF5733`);\n }\n }\n\n private async getInkDetails(inkId: string) {\n const inkColor = this.inkColors.find((ink) => ink.ink_id === inkId);\n const metadata = this.getInkMetadata(inkId);\n\n if (!inkColor) {\n throw new Error(`Ink not found: ${inkId}`);\n }\n\n const result = createSearchResult(inkColor, metadata);\n\n return {\n content: [\n {\n type: 'text',\n text: JSON.stringify(\n {\n ink_details: result,\n hex_color: rgbToHex(inkColor.rgb), // Now actually RGB!\n color_family: getColorFamily(inkColor.rgb),\n color_description: getColorDescription(inkColor.rgb),\n },\n null,\n 2,\n ),\n },\n ],\n };\n }\n\n private async getInksByMaker(maker: string, maxResults: number) {\n const makerLower = maker.toLowerCase();\n const makerInks = this.inkSearchData\n .filter((item) => item.maker.toLowerCase() === makerLower)\n .slice(0, maxResults);\n\n const results: SearchResult[] = [];\n\n for (const metadata of makerInks) {\n const inkColor = this.inkColors.find((ink) => ink.ink_id === metadata.ink_id);\n if (inkColor) {\n results.push(createSearchResult(inkColor, metadata));\n }\n }\n\n return {\n content: [\n {\n type: 'text',\n text: JSON.stringify(\n {\n maker,\n results_count: results.length,\n results,\n },\n null,\n 2,\n ),\n },\n ],\n };\n }\n\n private async analyzeColor(colorHex: string, maxResults: number = 5): Promise<any> {\n try {\n const rgb = hexToRgb(colorHex);\n const closestInks = findClosestInks(rgb, this.inkColors, maxResults);\n\n const results: SearchResult[] = closestInks.map((ink) => {\n const metadata = this.getInkMetadata(ink.ink_id);\n return createSearchResult(ink, metadata, ink.distance);\n });\n\n const analysis: ColorAnalysis = {\n hex: colorHex,\n rgb,\n closest_inks: results,\n color_family: getColorFamily(rgb), // No conversion needed\n description: getColorDescription(rgb), // No conversion needed\n };\n\n return {\n content: [\n {\n type: 'text',\n text: JSON.stringify(analysis, null, 2),\n },\n ],\n };\n } catch (error) {\n throw new Error(`Invalid color format: ${colorHex}. Please use hex format like #FF5733`);\n }\n }\n\n private async getColorPalette(\n theme: string,\n paletteSize: number,\n harmony?: 'complementary' | 'analogous' | 'triadic' | 'split-complementary',\n ): Promise<any> {\n const themeColors: { [key: string]: [number, number, number][] } = {\n warm: [\n [255, 100, 50],\n [255, 150, 0],\n [200, 80, 80],\n [180, 120, 60],\n [220, 180, 100],\n ],\n cool: [\n [50, 150, 255],\n [100, 200, 200],\n [150, 100, 255],\n [80, 180, 150],\n [120, 120, 200],\n ],\n earth: [\n [139, 69, 19],\n [160, 82, 45],\n [210, 180, 140],\n [107, 142, 35],\n [85, 107, 47],\n ],\n ocean: [\n [0, 119, 190],\n [0, 150, 136],\n [72, 201, 176],\n [135, 206, 235],\n [25, 25, 112],\n ],\n autumn: [\n [255, 140, 0],\n [255, 69, 0],\n [220, 20, 60],\n [184, 134, 11],\n [139, 69, 19],\n ],\n spring: [\n [154, 205, 50],\n [124, 252, 0],\n [173, 255, 47],\n [50, 205, 50],\n [0, 255, 127],\n ],\n summer: [\n [255, 235, 59],\n [255, 193, 7],\n [76, 175, 80],\n [139, 195, 74],\n [3, 169, 244],\n ],\n winter: [\n [224, 224, 224],\n [144, 164, 174],\n [96, 125, 139],\n [33, 150, 243],\n [0, 0, 128],\n ],\n pastel: [\n [255, 204, 204],\n [204, 255, 204],\n [204, 204, 255],\n [255, 255, 204],\n [255, 204, 255],\n ],\n vibrant: [\n [255, 0, 0],\n [0, 255, 0],\n [0, 0, 255],\n [255, 255, 0],\n [255, 0, 255],\n ],\n monochrome: [\n [255, 255, 255],\n [224, 224, 224],\n [192, 192, 192],\n [128, 128, 128],\n [64, 64, 64],\n [0, 0, 0],\n ],\n sunset: [\n [255, 224, 130],\n [255, 170, 85],\n [255, 110, 80],\n [200, 80, 120],\n [100, 60, 110],\n ],\n forest: [\n [34, 85, 34],\n [20, 60, 20],\n [60, 100, 60],\n [100, 140, 100],\n [140, 180, 140],\n ],\n };\n\n let targetColors: [number, number, number][];\n const lowerCaseTheme = theme.toLowerCase();\n\n if (harmony) {\n try {\n const baseRgb = hexToRgb(theme);\n const baseHsl = rgbToHsl(baseRgb);\n const harmonyHsl = generateHarmonyColors(baseHsl, harmony);\n targetColors = harmonyHsl.map((hsl) => hslToRgb(hsl));\n } catch (error) {\n throw new Error('Invalid base color for harmony rule. Please use a single valid hex code.');\n }\n } else if (themeColors[lowerCaseTheme]) {\n targetColors = themeColors[lowerCaseTheme];\n } else if (theme.startsWith('#') || theme.includes(',')) {\n try {\n targetColors = theme.split(',').map((hex) => hexToRgb(hex.trim()));\n } catch (error) {\n throw new Error(\n 'Invalid custom palette format. Please use a comma-separated list of hex codes, e.g., \"#FF0000,#00FF00,#0000FF\"',\n );\n }\n } else {\n throw new Error(\n `Unknown theme: \"${theme}\". Available themes are: ${Object.keys(themeColors).join(', ')}`,\n );\n }\n\n const paletteInks: SearchResult[] = [];\n const usedInkIds = new Set<string>();\n\n for (let i = 0; i < Math.min(paletteSize, targetColors.length); i++) {\n const targetRgb = targetColors[i];\n const closestInks = findClosestInks(targetRgb, this.inkColors, 5).filter(\n (ink) => !usedInkIds.has(ink.ink_id),\n );\n\n if (closestInks.length > 0) {\n const ink = closestInks[0];\n usedInkIds.add(ink.ink_id);\n const metadata = this.getInkMetadata(ink.ink_id);\n paletteInks.push(createSearchResult(ink, metadata, ink.distance));\n }\n }\n\n const palette: PaletteResult = {\n theme,\n inks: paletteInks,\n description: `A curated palette of ${paletteInks.length} fountain pen inks matching the ${theme} theme.`,\n };\n\n return {\n content: [\n {\n type: 'text',\n text: JSON.stringify(palette, null, 2),\n },\n ],\n };\n }\n\n async run() {\n const transport = new StdioServerTransport();\n await this.server.connect(transport);\n }\n}\n\nconst server = new InkMCPServer();\nserver.run().catch(console.error);\n","usedDeprecatedRules":[]},{"filePath":"/Users/ewilderj/git/inks-mcp/src/types.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/Users/ewilderj/git/inks-mcp/src/utils.ts","messages":[{"ruleId":"@typescript-eslint/consistent-type-imports","severity":1,"message":"All imports in the declaration are only used as types. Use `import type`.","line":1,"column":1,"nodeType":"ImportDeclaration","messageId":"typeOverValue","endLine":1,"endColumn":85,"fix":{"range":[6,6],"text":" type"}},{"ruleId":"prefer-const","severity":2,"message":"'l' is never reassigned. Use 'const' instead.","line":173,"column":5,"nodeType":"Identifier","messageId":"useConst","endLine":173,"endColumn":6},{"ruleId":"prefer-const","severity":2,"message":"'s' is never reassigned. Use 'const' instead.","line":201,"column":11,"nodeType":"Identifier","messageId":"useConst","endLine":201,"endColumn":12},{"ruleId":"prefer-const","severity":2,"message":"'l' is never reassigned. Use 'const' instead.","line":201,"column":14,"nodeType":"Identifier","messageId":"useConst","endLine":201,"endColumn":15}],"suppressedMessages":[],"errorCount":3,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":1,"source":"import { InkColor, InkSearchData, InkWithDistance, SearchResult } from './types.js';\n\n/**\n * Convert hex color to RGB array\n */\nexport function hexToRgb(hex: string): [number, number, number] {\n const cleanHex = hex.replace(/^#/, '');\n if (cleanHex.length !== 6) {\n throw new Error('Invalid hex color string');\n }\n\n const bigint = parseInt(cleanHex, 16);\n const r = (bigint >> 16) & 255;\n const g = (bigint >> 8) & 255;\n const b = bigint & 255;\n\n return [r, g, b];\n}\n\n/**\n * Convert BGR array (as stored in ink-colors.json) to RGB array\n * Used at data load time to convert the source BGR data to RGB format\n */\nexport function bgrToRgb(bgr: [number, number, number]): [number, number, number] {\n const [b, g, r] = bgr;\n return [r, g, b];\n}\n\n/**\n * Convert RGB array to BGR array (for comparison with ink data)\n * @deprecated No longer needed - data is converted at load time\n */\nexport function rgbToBgr(rgb: [number, number, number]): [number, number, number] {\n const [r, g, b] = rgb;\n return [b, g, r];\n}\n\n/**\n * Convert RGB array to hex string\n */\nexport function rgbToHex(rgb: [number, number, number]): string {\n const [r, g, b] = rgb;\n return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;\n}\n\n/**\n * Calculate Euclidean distance between two RGB colors\n * Now simplified - both inputs are RGB format\n */\nexport function calculateColorDistance(\n rgb1: [number, number, number],\n rgb2: [number, number, number],\n): number {\n const [r1, g1, b1] = rgb1;\n const [r2, g2, b2] = rgb2;\n\n return Math.sqrt(Math.pow(r1 - r2, 2) + Math.pow(g1 - g2, 2) + Math.pow(b1 - b2, 2));\n}\n\n/**\n * Find inks closest to a given color\n * Now simplified - both target and ink colors are RGB\n */\nexport function findClosestInks(\n targetRgb: [number, number, number],\n inkColors: InkColor[],\n maxResults: number = 20,\n): InkWithDistance[] {\n const distances = inkColors.map((ink) => ({\n ...ink,\n distance: calculateColorDistance(targetRgb, ink.rgb), // Now both are RGB!\n }));\n\n // Sort by distance (closest first)\n distances.sort((a, b) => a.distance - b.distance);\n\n return distances.slice(0, maxResults);\n}\n\n/**\n * Determine color family based on RGB values\n * Now simplified - input is already RGB format\n */\nexport function getColorFamily(rgb: [number, number, number]): string {\n const [r, g, b] = rgb; // No conversion needed!\n\n // Determine dominant color component\n const max = Math.max(r, g, b);\n const min = Math.min(r, g, b);\n\n // Check for grayscale - tightened threshold from 30 to 22\n if (max - min < 22) {\n return 'gray';\n }\n\n // Check for primary colors\n if (r === max && r > g + 20 && r > b + 20) {\n return 'red';\n }\n if (g === max && g > r + 20 && g > b + 20) {\n return 'green';\n }\n if (b === max && b > r + 20 && b > g + 20) {\n return 'blue';\n }\n\n // Check for secondary colors\n if (r > 150 && g > 150 && b < 100) {\n return 'yellow';\n }\n if (r > 150 && b > 150 && g < 100) {\n return 'magenta';\n }\n if (g > 150 && b > 150 && r < 100) {\n return 'cyan';\n }\n\n // More nuanced color detection\n if (r > g && r > b) {\n if (g > b + 30) return 'orange';\n if (b > g + 30) return 'purple';\n return 'red';\n }\n\n if (g > r && g > b) {\n if (b > r + 30) return 'teal';\n if (r > b + 30) return 'yellow-green';\n return 'green';\n }\n\n if (b > r && b > g) {\n if (r > g + 30) return 'purple';\n if (g > r + 30) return 'blue-green';\n return 'blue';\n }\n\n return 'mixed';\n}\n\n/**\n * Generate a color description based on RGB values\n * Now simplified - input is already RGB format\n */\nexport function getColorDescription(rgb: [number, number, number]): string {\n const [r, g, b] = rgb; // No conversion needed!\n const brightness = (r + g + b) / 3;\n const colorFamily = getColorFamily(rgb);\n\n let brightnessDesc = '';\n if (brightness < 85) brightnessDesc = 'dark ';\n else if (brightness > 170) brightnessDesc = 'light ';\n\n const saturation = Math.max(r, g, b) - Math.min(r, g, b);\n let saturationDesc = '';\n if (saturation < 30) saturationDesc = 'muted ';\n else if (saturation > 150) saturationDesc = 'vibrant ';\n\n return `${brightnessDesc}${saturationDesc}${colorFamily}`.trim();\n}\n\n/**\n * Convert RGB to HSL\n */\nexport function rgbToHsl(rgb: [number, number, number]): [number, number, number] {\n let [r, g, b] = rgb;\n r /= 255;\n g /= 255;\n b /= 255;\n const max = Math.max(r, g, b);\n const min = Math.min(r, g, b);\n let h = 0,\n s = 0,\n l = (max + min) / 2;\n\n if (max === min) {\n h = s = 0; // achromatic\n } else {\n const d = max - min;\n s = l > 0.5 ? d / (2 - max - min) : d / (max + min);\n switch (max) {\n case r:\n h = (g - b) / d + (g < b ? 6 : 0);\n break;\n case g:\n h = (b - r) / d + 2;\n break;\n case b:\n h = (r - g) / d + 4;\n break;\n }\n h /= 6;\n }\n\n return [h * 360, s, l];\n}\n\n/**\n * Convert HSL to RGB\n */\nexport function hslToRgb(hsl: [number, number, number]): [number, number, number] {\n let [h, s, l] = hsl;\n h /= 360;\n let r, g, b;\n\n if (s === 0) {\n r = g = b = l; // achromatic\n } else {\n const hue2rgb = (p: number, q: number, t: number) => {\n if (t < 0) t += 1;\n if (t > 1) t -= 1;\n if (t < 1 / 6) return p + (q - p) * 6 * t;\n if (t < 1 / 2) return q;\n if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;\n return p;\n };\n const q = l < 0.5 ? l * (1 + s) : l + s - l * s;\n const p = 2 * l - q;\n r = hue2rgb(p, q, h + 1 / 3);\n g = hue2rgb(p, q, h);\n b = hue2rgb(p, q, h - 1 / 3);\n }\n\n return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];\n}\n\n/**\n * Generate a set of harmony colors from a base color\n */\nexport function generateHarmonyColors(\n baseHsl: [number, number, number],\n harmony: 'complementary' | 'analogous' | 'triadic' | 'split-complementary',\n): [number, number, number][] {\n const [h, s, l] = baseHsl;\n const harmonyHues: { [key: string]: number[] } = {\n complementary: [h, (h + 180) % 360],\n analogous: [h, (h + 30) % 360, (h + 330) % 360],\n triadic: [h, (h + 120) % 360, (h + 240) % 360],\n 'split-complementary': [h, (h + 150) % 360, (h + 210) % 360],\n };\n\n const hues = harmonyHues[harmony] || [h];\n return hues.map((hue) => [hue, s, l]);\n}\n\n/**\n * Create a SearchResult from ink data\n */\nexport function createSearchResult(\n ink: InkColor,\n metadata?: InkSearchData,\n distance?: number,\n): SearchResult {\n return {\n ink,\n metadata,\n distance,\n url: `https://wilderwrites.ink/ink/${ink.ink_id}`,\n image_url: `https://wilderwrites.ink/images/inks/${ink.ink_id}-sq.jpg`,\n };\n}\n","usedDeprecatedRules":[]}]

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ewilderj/inks-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server