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
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,14 @@ This MCP server implements the complete [Model Context Protocol](https://modelco
- **Resources** - Provides 100+ resources across 35+ ServiceNow metadata types (API specs, instructions, snippets, prompts)
- **Tools** - Exposes 10 ServiceNow SDK commands as MCP tools with full parameter validation
- **Prompts** - Offers development workflow templates for common ServiceNow tasks
- **Roots** - Supports MCP roots protocol for workspace-aware operations
- **Logging** - Structured logging for debugging and monitoring

### Extended Capabilities
### Client Capabilities (used by this server)

The server leverages these MCP client capabilities when available:

- **Roots** - Requests workspace roots from the client for context-aware operations
- Falls back to project root when client doesn't provide roots

- **Sampling** (MCP 2024-11-05) - Leverages client LLM for intelligent error analysis when SDK commands fail
- Automatically analyzes command errors >50 characters
Expand Down
1,481 changes: 747 additions & 734 deletions package-lock.json

Large diffs are not rendered by default.

26 changes: 13 additions & 13 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@modesty/fluent-mcp",
"version": "0.0.18",
"version": "0.0.19",
"description": "MCP server for Fluent (ServiceNow SDK)",
"keywords": [
"Servicenow SDK",
Expand Down Expand Up @@ -52,26 +52,26 @@
}
},
"dependencies": {
"@modelcontextprotocol/sdk": "1.21.1",
"@servicenow/sdk": "4.0.2",
"zod": "^3.25.76",
"zod-to-json-schema": "^3.24.6"
"@modelcontextprotocol/sdk": "1.25.1",
"@servicenow/sdk": "4.1.1",
"zod": "^4.3.5",
"zod-to-json-schema": "^3.25.1"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
"@eslint/js": "^9.39.2",
"@rollup/plugin-commonjs": "^29.0.0",
"@rollup/plugin-json": "^6.0.1",
"@rollup/plugin-node-resolve": "^16.0.3",
"@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-typescript": "^12.3.0",
"@types/jest": "^30.0.0",
"@types/node": "^24.10.0",
"@typescript-eslint/eslint-plugin": "^8.46.3",
"@typescript-eslint/parser": "^8.46.3",
"eslint": "^9.39.1",
"@types/node": "^25.0.3",
"@typescript-eslint/eslint-plugin": "^8.52.0",
"@typescript-eslint/parser": "^8.52.0",
"eslint": "^9.39.2",
"jest": "^30.2.0",
"rollup": "^4.53.1",
"ts-jest": "^29.4.5",
"rollup": "^4.55.1",
"ts-jest": "^29.4.6",
"ts-node": "^10.9.2",
"tslib": "^2.8.1",
"typescript": "~5.9.3"
Expand All @@ -84,4 +84,4 @@
"dist",
"res"
]
}
}
8 changes: 3 additions & 5 deletions src/prompts/promptManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,18 +210,16 @@ export class PromptManager {


// Register the prompt
// Note: PromptArgument in MCP SDK v1.25+ only supports name, description, and required
// The 'type' and 'items' properties are no longer part of the schema
const prompt: Prompt = {
name: promptName,
title: 'Coding in Fluent (ServiceNow SDK)',
description: 'Guide for coding in Fluent (ServiceNow SDK) with examples for specific metadata types',
arguments: [
{
name: 'metadata_list',
description: 'List of metadata types to include in the guide',
type: 'array',
items: {
type: 'string'
},
description: 'Comma-separated list of metadata types to include in the guide (e.g., "table,business-rule,script-include")',
required: true
}
]
Expand Down
3 changes: 0 additions & 3 deletions src/res/resourceManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ 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 @@ -134,7 +133,6 @@ 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 @@ -198,7 +196,6 @@ 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
8 changes: 3 additions & 5 deletions src/server/fluentMCPServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,12 @@ export class FluentMcpServer {
tools: {},
resources: {}, // Enable resources capability
logging: {}, // Enable logging capability
elicitation: {}, // Enable elicitation capability for structured data collection
sampling: {}, // Enable sampling capability for AI-powered features
// Note: 'elicitation', 'sampling', and 'roots' are ClientCapabilities, not ServerCapabilities
// in MCP SDK v1.25+. Servers don't declare these - clients do.
// The server can still USE these features by making requests to the client.
prompts: {
listChanged: true, // Enable prompt list change notifications
},
roots: {
listChanged: true, // Enable root list change notifications
},
},
}
);
Expand Down
2 changes: 1 addition & 1 deletion src/tools/toolsManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ export class ToolsManager {
description: command.description,
inputSchema: inputSchema
},
async (args: { [x: string]: any }, _extra) => {
async (args: { [x: string]: any }, _extra: unknown) => {
const result = await command.execute(args);
return {
content: [{ type: 'text' as const, text: result.output }],
Expand Down
85 changes: 46 additions & 39 deletions test/res/resourceManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,69 +113,72 @@ describe("ResourceManager", () => {
});
});

test("should register spec resources with required 'name' field", async () => {
test("should register spec resources with valid metadata", async () => {
await resourceManager.initialize();
resourceManager.registerAll();

// Get all registerResource calls
const calls = mockMcpServer.registerResource.mock.calls;

// Filter spec resource registrations
const specCalls = calls.filter((call: any[]) => call[0].startsWith('sn-spec-'));

expect(specCalls.length).toBeGreaterThan(0);

// Validate each spec resource has required 'name' field

// Validate each spec resource has valid metadata
// Note: In MCP SDK v1.25+, 'name' is passed as the first argument, not in metadata
specCalls.forEach((call: any[]) => {
const metadata = call[2]; // 3rd parameter is metadata
expect(metadata).toHaveProperty('name');
expect(metadata.name).toMatch(/^sn-spec-/);
const resourceName = call[0]; // 1st parameter is the name
const metadata = call[2]; // 3rd parameter is metadata (ResourceMetadata excludes 'name')
expect(resourceName).toMatch(/^sn-spec-/);
expect(metadata).toHaveProperty('title');
expect(metadata).toHaveProperty('description');
expect(metadata).toHaveProperty('mimeType', 'text/markdown');
});
});

test("should register snippet resources with required 'name' field", async () => {
test("should register snippet resources with valid metadata", async () => {
await resourceManager.initialize();
resourceManager.registerAll();

// Get all registerResource calls
const calls = mockMcpServer.registerResource.mock.calls;

// Filter snippet resource registrations
const snippetCalls = calls.filter((call: any[]) => call[0].startsWith('sn-snippet-'));

expect(snippetCalls.length).toBeGreaterThan(0);

// Validate each snippet resource has required 'name' field

// Validate each snippet resource has valid metadata
// Note: In MCP SDK v1.25+, 'name' is passed as the first argument, not in metadata
snippetCalls.forEach((call: any[]) => {
const metadata = call[2]; // 3rd parameter is metadata
expect(metadata).toHaveProperty('name');
expect(metadata.name).toMatch(/^sn-snippet-/);
const resourceName = call[0]; // 1st parameter is the name
const metadata = call[2]; // 3rd parameter is metadata (ResourceMetadata excludes 'name')
expect(resourceName).toMatch(/^sn-snippet-/);
expect(metadata).toHaveProperty('title');
expect(metadata).toHaveProperty('description');
expect(metadata).toHaveProperty('mimeType', 'text/markdown');
});
});

test("should register instruct resources with required 'name' field", async () => {
test("should register instruct resources with valid metadata", async () => {
await resourceManager.initialize();
resourceManager.registerAll();

// Get all registerResource calls
const calls = mockMcpServer.registerResource.mock.calls;

// Filter instruct resource registrations
const instructCalls = calls.filter((call: any[]) => call[0].startsWith('sn-instruct-'));

expect(instructCalls.length).toBeGreaterThan(0);

// Validate each instruct resource has required 'name' field

// Validate each instruct resource has valid metadata
// Note: In MCP SDK v1.25+, 'name' is passed as the first argument, not in metadata
instructCalls.forEach((call: any[]) => {
const metadata = call[2]; // 3rd parameter is metadata
expect(metadata).toHaveProperty('name');
expect(metadata.name).toMatch(/^sn-instruct-/);
const resourceName = call[0]; // 1st parameter is the name
const metadata = call[2]; // 3rd parameter is metadata (ResourceMetadata excludes 'name')
expect(resourceName).toMatch(/^sn-instruct-/);
expect(metadata).toHaveProperty('title');
expect(metadata).toHaveProperty('description');
expect(metadata).toHaveProperty('mimeType', 'text/markdown');
Expand All @@ -185,23 +188,27 @@ describe("ResourceManager", () => {
test("should ensure all registered resources have MCP-compliant metadata", async () => {
await resourceManager.initialize();
resourceManager.registerAll();

// Get all registerResource calls
const calls = mockMcpServer.registerResource.mock.calls;

expect(calls.length).toBeGreaterThan(0);

// Validate every registered resource has MCP-required fields
calls.forEach((call: any[], index: number) => {
const resourceId = call[0];
// Note: In MCP SDK v1.25+, 'name' is passed as the first argument to registerResource,
// not in the metadata object. ResourceMetadata = Omit<Resource, 'uri' | 'name'>
calls.forEach((call: any[]) => {
const resourceName = call[0]; // 1st parameter is the name
const metadata = call[2];

// Required fields for MCP protocol
expect(metadata).toHaveProperty('name');
expect(metadata.name).toBeDefined();

// The name should match the resource ID
expect(metadata.name).toBe(resourceId);

// Name should be a valid resource identifier
expect(resourceName).toBeDefined();
expect(typeof resourceName).toBe('string');
expect(resourceName).toMatch(/^sn-(spec|snippet|instruct)-/);

// Metadata should have title and description
expect(metadata).toHaveProperty('title');
expect(metadata).toHaveProperty('description');
});
});
});