Skip to content
Merged
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
464 changes: 139 additions & 325 deletions README.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@modesty/fluent-mcp",
"version": "0.0.17",
"version": "0.0.18",
"description": "MCP server for Fluent (ServiceNow SDK)",
"keywords": [
"Servicenow SDK",
Expand Down
27 changes: 27 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,14 @@ export interface McpServerConfig {
/** Default timeout for SDK CLI commands in milliseconds */
commandTimeoutMs: number;
};

/** Sampling configuration for AI-powered features */
sampling: {
/** Enable AI-powered error analysis using MCP Sampling capability */
enableErrorAnalysis: boolean;
/** Minimum error length to trigger analysis (avoid analyzing trivial errors) */
minErrorLength: number;
};
}

// Environment variable names
Expand All @@ -90,6 +98,8 @@ const ENV_VAR = {
RESOURCE_PATH_INSTRUCT: `${ENV_PREFIX}RESOURCE_PATH_INSTRUCT`,
SERVICENOW_SDK_CLI_PATH: `${ENV_PREFIX}SERVICENOW_SDK_CLI_PATH`,
COMMAND_TIMEOUT_MS: `${ENV_PREFIX}COMMAND_TIMEOUT_MS`,
ENABLE_ERROR_ANALYSIS: `${ENV_PREFIX}ENABLE_ERROR_ANALYSIS`,
MIN_ERROR_LENGTH: `${ENV_PREFIX}MIN_ERROR_LENGTH`,
};

/**
Expand All @@ -110,6 +120,10 @@ export const defaultConfig: McpServerConfig = {
cliPath: 'snc', // Default command name if installed globally
commandTimeoutMs: 30000, // 30 seconds default timeout
},
sampling: {
enableErrorAnalysis: true, // Enable by default
minErrorLength: 50, // Only analyze errors with 50+ characters
},
};

/**
Expand Down Expand Up @@ -149,6 +163,19 @@ export function getConfig(): McpServerConfig {
10
),
},
sampling: {
enableErrorAnalysis: getEnvVar(
ENV_VAR.ENABLE_ERROR_ANALYSIS,
defaultConfig.sampling.enableErrorAnalysis.toString()
) === 'true',
minErrorLength: parseInt(
getEnvVar(
ENV_VAR.MIN_ERROR_LENGTH,
defaultConfig.sampling.minErrorLength.toString()
),
10
),
},
};

// Add optional logFilePath if specified in environment
Expand Down
127 changes: 126 additions & 1 deletion src/res/resourceManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export class ResourceManager {
`sn-spec-${type}`,
template,
{
name: `sn-spec-${type}`,
title: `${type} API Specification for Fluent (ServiceNow SDK)`,
description: `API specification for Fluent (ServiceNow SDK) ${type}`,
mimeType: 'text/markdown'
Expand Down Expand Up @@ -133,6 +134,7 @@ export class ResourceManager {
`sn-snippet-${type}`,
template,
{
name: `sn-snippet-${type}`,
title: `${type} Code Snippets for Fluent (ServiceNow SDK)`,
description: `Example code snippets for Fluent (ServiceNow SDK) ${type}`,
mimeType: 'text/markdown'
Expand Down Expand Up @@ -196,6 +198,7 @@ export class ResourceManager {
`sn-instruct-${type}`,
template,
{
name: `sn-instruct-${type}`,
title: `${type} Instructions for Fluent (ServiceNow SDK)`,
description: `Development instructions for Fluent (ServiceNow SDK) ${type}`,
mimeType: 'text/markdown'
Expand Down Expand Up @@ -249,7 +252,7 @@ export class ResourceManager {
* Get all available resources for listing
* @returns List of resource objects
*/
async listResources(): Promise<{ uri: string, title: string, mimeType: string }[]> {
async listResources(): Promise<{ uri: string, name: string, title: string, mimeType: string }[]> {
try {
// Make sure metadata types are loaded
if (!this.metadataTypes || this.metadataTypes.length === 0) {
Expand All @@ -262,6 +265,7 @@ export class ResourceManager {
for (const type of this.metadataTypes) {
resources.push({
uri: `sn-spec://${type}`,
name: `sn-spec-${type}`,
title: `${type} API Specification for Fluent (ServiceNow SDK) `,
mimeType: 'text/markdown'
});
Expand All @@ -271,6 +275,7 @@ export class ResourceManager {
for (const type of this.metadataTypes) {
resources.push({
uri: `sn-instruct://${type}`,
name: `sn-instruct-${type}`,
title: `${type} Instructions for Fluent (ServiceNow SDK)`,
mimeType: 'text/markdown'
});
Expand All @@ -283,6 +288,7 @@ export class ResourceManager {
if (snippetIds.length > 0) {
resources.push({
uri: `sn-snippet://${type}/${snippetIds[0]}`,
name: `sn-snippet-${type}`,
title: `${type} Code Snippet for Fluent (ServiceNow SDK)`,
mimeType: 'text/markdown'
});
Expand Down Expand Up @@ -318,4 +324,123 @@ export class ResourceManager {
getMetadataTypes(): string[] {
return this.metadataTypes;
}

/**
* Read a resource by URI
* This method is called by the MCP server's resources/read handler
* @param uriString The resource URI to read
* @returns Resource contents
*/
async readResource(uriString: string): Promise<{ contents: Array<{ uri: string; text: string; mimeType?: string }> }> {
try {
const uri = new URL(uriString);
const scheme = uri.protocol.replace(':', '');
const metadataType = uri.host;

// Determine resource type from URI scheme
if (scheme === 'sn-spec') {
return await this.readSpecResource(uri, metadataType);
} else if (scheme === 'sn-snippet') {
return await this.readSnippetResource(uri, metadataType);
} else if (scheme === 'sn-instruct') {
return await this.readInstructResource(uri, metadataType);
}

throw new Error(`Unknown resource URI scheme: ${scheme}`);
} catch (error) {
logger.error('Error reading resource',
error instanceof Error ? error : new Error(String(error))
);
throw error;
}
}

/**
* Read a spec resource
*/
private async readSpecResource(uri: URL, metadataType: string) {
const result = await this.resourceLoader.getResource(
ResourceType.SPEC,
metadataType
);

if (!result.found) {
return {
contents: [{
uri: uri.href,
text: `API specification not found for ${metadataType}`,
mimeType: 'text/plain'
}]
};
}

return {
contents: [{
uri: uri.href,
text: result.content,
mimeType: 'text/markdown'
}]
};
}

/**
* Read a snippet resource
*/
private async readSnippetResource(uri: URL, metadataType: string) {
// Extract snippet ID from URI path
const pathParts = uri.pathname.split('/').filter(p => p);
const snippetId = pathParts.length > 0 ? pathParts[0] : undefined;

const result = await this.resourceLoader.getResource(
ResourceType.SNIPPET,
metadataType,
snippetId
);

if (!result.found) {
return {
contents: [{
uri: uri.href,
text: `Snippet not found for ${metadataType}`,
mimeType: 'text/plain'
}]
};
}

return {
contents: [{
uri: uri.href,
text: result.content,
mimeType: 'text/markdown'
}]
};
}

/**
* Read an instruct resource
*/
private async readInstructResource(uri: URL, metadataType: string) {
const result = await this.resourceLoader.getResource(
ResourceType.INSTRUCT,
metadataType
);

if (!result.found) {
return {
contents: [{
uri: uri.href,
text: `Instructions not found for ${metadataType}`,
mimeType: 'text/plain'
}]
};
}

return {
contents: [{
uri: uri.href,
text: result.content,
mimeType: 'text/markdown'
}]
};
}
}
Loading