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

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 15 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ You can use the MCP server available in the npm registry. If you want to develop
You can run the MCP server with npx to expose each operation of the last deployed version of a decision service as a MCP tool:

```bash
npx -y di-mcp-server <CREDENTIALS> --url <RUNTIME_BASE_URL> --transport <TRANSPORT> --runtime <RUNTIME>
npx -y di-mcp-server <CREDENTIALS> --url <RUNTIME_BASE_URL> [--transport <TRANSPORT>] [--runtime <RUNTIME>] [--deploymentSpaces <DEPLOYMENT_SPACES>]
```

where
Expand All @@ -44,10 +44,11 @@ where
- `--username <USERNAME> --password <PASSWORD>` where `USERNAME` and `PASSWORD` are the basic authentication credentials to connect to the decision runtime of IBM Automation Decision Services.
- `--username <USERNAME> --apikey <ZEN_API_KEY>` where `USERNAME` and `ZEN_API_KEY` are the Zen API key credentials to access the decision runtime of IBM Automation Decision Services (see [Authorizing HTTP requests by using the Zen API key](https://www.ibm.com/docs/en/cloud-paks/cp-biz-automation/25.0.0?topic=administering-authorizing-http-requests-by-using-zen-api-key))
- `RUNTIME_BASE_URL` is the base URL of the decision runtime REST API. For IBM Decision Intelligence its pattern is: `https://<TENANT_NAME>.decision-prod-us-south.decision.saas.ibm.com/ads/runtime/api/v1` where TENANT_NAME is the name of the tenant.
- `TRANSPORT` is either `STDIO` (default) or `HTTP`.
- `RUNTIME` is either `DI` (default) or `ADS` for using the decision runtime of respectively IBM Decision Intelligence or IBM Automation Decision Services.
- `TRANSPORT` (optional) is the transport protocol, either `STDIO` (default) or `HTTP`.
- `RUNTIME` (optional) is the decision runtime, either `DI` (default) or `ADS` for using the decision runtime of respectively IBM Decision Intelligence or IBM Automation Decision Services.
- `DEPLOYMENT_SPACES` (optional) is a comma-separated list of deployment spaces to scan (defaults to `development`).

Example:
- Example:

```bash
npx -y di-mcp-server --apikey HRJcDNlNXZVWlk9 --url https://mytenant.decision-prod-us-south.decision.saas.ibm.com/ads/runtime/api/v1
Expand Down Expand Up @@ -343,15 +344,16 @@ APIKEY=<APIKEY> URL=<URL> npm run dev

## Environment variables

| Name | Description |
|-----------|----------------------------------------------------------------------------------------------------------------------------------|
| APIKEY | API key to access the decision runtime of either IBM Decision Intelligence or IBM Automation Decision Services |
| DEBUG | When the value is `true`, the debug messages are written to the `stderr` of the MCP server |
| PASSWORD | Password to access the decision runtime of IBM Automation Decision Services with basic authentication |
| RUNTIME | The target decision runtime: `DI` (default) or `ADS` |
| TRANSPORT | The transport protocol: `STDIO` (default) or `HTTP` |
| URL | Base URL of the decision runtime |
| USERNAME | Username to access the decision runtime of IBM Automation Decision Services either with basic authentication or Zen API key</br> |
| Name | Description |
|-------------------|----------------------------------------------------------------------------------------------------------------------------------|
| APIKEY | API key to access the decision runtime of either IBM Decision Intelligence or IBM Automation Decision Services |
| DEPLOYMENT_SPACES | Optional comma-separated list of deployment spaces to scan (default: `development`) |
| DEBUG | When the value is `true`, the debug messages are written to the `stderr` of the MCP server |
| PASSWORD | Password to access the decision runtime of IBM Automation Decision Services with basic authentication |
| RUNTIME | Optional target decision runtime: `DI` (default) or `ADS` |
| TRANSPORT | Optional transport protocol: `STDIO` (default) or `HTTP` |
| URL | Base URL of the decision runtime |
| USERNAME | Username to access the decision runtime of IBM Automation Decision Services either with basic authentication or Zen API key</br> |

## License
[Apache 2.0](LICENSE)
Expand Down
50 changes: 47 additions & 3 deletions src/command-line.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ export class Configuration {
public transport: StdioServerTransport | undefined,
public url: string,
public version: string,
public debugEnabled: boolean
public debugEnabled: boolean,
public deploymentSpaces: string[] = Configuration.defaultDeploymentSpaces()
) {
this.runtime = runtime || Configuration.defaultRuntime();
}
Expand All @@ -29,6 +30,10 @@ export class Configuration {
return Configuration.STDIO;
}

static defaultDeploymentSpaces(): string[] {
return ['development'];
}

isDiRuntime(): boolean {
return this.runtime === DecisionRuntime.DI;
}
Expand Down Expand Up @@ -85,6 +90,43 @@ function validateDecisionRuntime(runtime: string): DecisionRuntime {
return decisionRuntime;
}

function validateDeploymentSpaces(parseDeploymentSpaceOption: string | undefined): string[] {
debug("DEPLOYMENT SPACES=" + parseDeploymentSpaceOption);
const deploymentSpaces = parseDeploymentSpaces(parseDeploymentSpaceOption);
const invalidDeploymentSpaces: string[] = [];
const encodedDeploymentSpaces: string[] = [];

for (const deploymentSpace of deploymentSpaces) {
try {
encodedDeploymentSpaces.push(encodeURIComponent(deploymentSpace));
} catch {
invalidDeploymentSpaces.push(deploymentSpace);
}
}

const nbOfInvalidDeploymentSpaces = invalidDeploymentSpaces.length;
if (nbOfInvalidDeploymentSpaces > 0) {
if (nbOfInvalidDeploymentSpaces === 1) {
throw new Error(`Invalid deployment space '${invalidDeploymentSpaces[0]}' cannot be URI encoded.`);
}
throw new Error(`Invalid deployment spaces '${invalidDeploymentSpaces.join("', '")}' cannot be URI encoded.`);
}
return encodedDeploymentSpaces;
}

function parseDeploymentSpaces(deploymentSpaces: string | undefined): string[] {
if (deploymentSpaces !== undefined) {
const parsedDeploymentSpaces = deploymentSpaces
.split(',')
.map(ds => ds.trim())
.filter(ds => ds.length > 0);
if (parsedDeploymentSpaces.length > 0) {
return parsedDeploymentSpaces;
}
}
return Configuration.defaultDeploymentSpaces();
}

export function createConfiguration(cliArguments?: readonly string[]): Configuration {
const program = new Command();
const version = String(process.env.npm_package_version);
Expand All @@ -98,7 +140,8 @@ export function createConfiguration(cliArguments?: readonly string[]): Configura
.option('--username <string>', "Username for the decision runtime. Or set the 'USERNAME' environment variable")
.option('--password <string>', "Password for the decision runtime. Or set 'PASSWORD' environment variable)")
.option('--transport <transport>', "Transport mode: 'STDIO' or 'HTTP'")
.option("--runtime <runtime>", "Target decision runtime: 'DI' or 'ADS'. Default value is 'DI'");
.option("--runtime <runtime>", "Target decision runtime: 'DI' or 'ADS'. Default value is 'DI'")
.option('--deploymentSpaces <list>', "Comma-separated list of deployment spaces to scan (default: 'development')");

program.parse(cliArguments);

Expand All @@ -111,7 +154,8 @@ export function createConfiguration(cliArguments?: readonly string[]): Configura
const runtime = validateDecisionRuntime(options["runtime"] || process.env.RUNTIME);
const transport = validateTransport(options.transport || process.env.TRANSPORT);
const url = validateUrl(options.url || process.env.URL);
const deploymentSpaces = validateDeploymentSpaces(options.deploymentSpaces || process.env.DEPLOYMENT_SPACES);

// Create and return configuration object
return new Configuration(credentials, runtime, transport, url, version, debugFlag);
return new Configuration(credentials, runtime, transport, url, version, debugFlag, deploymentSpaces);
}
3 changes: 0 additions & 3 deletions src/constants.ts

This file was deleted.

26 changes: 13 additions & 13 deletions src/diruntimeclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import axios from 'axios';
import { OpenAPIV3_1 } from "openapi-types";
import {Configuration} from "./command-line.js";

export function executeDecision(configuration :Configuration, decisionId:string, operation:string, input:object|undefined) {
const url = configuration.url + "/deploymentSpaces/development/decisions/"
export function executeDecision(configuration: Configuration, deploymentSpace: string, decisionId: string, operation: string, input: object|undefined) {
const url = configuration.url + "/deploymentSpaces/" + deploymentSpace + "/decisions/"
+ encodeURIComponent(decisionId)
+ "/operations/"
+ encodeURIComponent(operation)
+"/execute";
+ "/execute";

return axios.post(url, input, { headers: getJsonContentTypeHeaders(configuration) })
.then(function (response) {
Expand All @@ -22,9 +22,9 @@ function getJsonContentTypeHeaders(configuration: Configuration) {
};
}

export function executeLastDeployedDecisionService(configuration: Configuration, serviceId:string, operation:string, input: object) {
const url = configuration.url + "/selectors/lastDeployedDecisionService/deploymentSpaces/development/operations/"
+ encodeURIComponent(operation)
export function executeLastDeployedDecisionService(configuration: Configuration, deploymentSpace: string, serviceId: string, operation: string, input: object) {
const url = configuration.url + "/selectors/lastDeployedDecisionService/deploymentSpaces/" + deploymentSpace
+ "/operations/" + encodeURIComponent(operation)
+ "/execute?decisionServiceId=" + encodeURIComponent(serviceId);

return axios.post(url, input, { headers: getJsonContentTypeHeaders(configuration) })
Expand All @@ -34,15 +34,15 @@ export function executeLastDeployedDecisionService(configuration: Configuration,
}

export async function getDecisionMetadata(configuration: Configuration, deploymentSpace: string, decisionId: string) {
const url = configuration.url + `/deploymentSpaces/${encodeURIComponent(deploymentSpace)}/decisions/${encodeURIComponent(decisionId)}/metadata`;
const url = configuration.url + `/deploymentSpaces/${deploymentSpace}/decisions/${encodeURIComponent(decisionId)}/metadata`;

const response = await axios.get(url, {headers: getHeaders(configuration)});
return response.data;
}

export function getMetadata(configuration: Configuration, deploymentSpace:string) {
const url = configuration.url + "/deploymentSpaces"
+ "/" + encodeURIComponent(deploymentSpace)
+ "/" + deploymentSpace
+ "/metadata?names=decisionServiceId";

return axios.get(url, { headers: getHeaders(configuration) })
Expand All @@ -65,9 +65,9 @@ export function getDecisionServiceIds(metadata: MetadataType[]): string[] {
return ids;
}

export function getDecisionOpenapi(configuration: Configuration, decisionId:string) {
const url = configuration.url + "/deploymentSpaces/development/decisions/"
+ encodeURIComponent(decisionId)
export function getDecisionOpenapi(configuration: Configuration, deploymentSpace: string, decisionId: string) {
const url = configuration.url + "/deploymentSpaces/" + deploymentSpace
+ "/decisions/" + encodeURIComponent(decisionId)
+ "/openapi";

return axios.get(url, { headers: getHeaders(configuration) })
Expand All @@ -85,8 +85,8 @@ function getHeaders(configuration: Configuration) {
};
}

export function getDecisionServiceOpenAPI(configuration: Configuration, decisionServiceId:string) {
const url = configuration.url + "/selectors/lastDeployedDecisionService/deploymentSpaces/development"
export function getDecisionServiceOpenAPI(configuration: Configuration, deploymentSpace: string, decisionServiceId: string) {
const url = configuration.url + "/selectors/lastDeployedDecisionService/deploymentSpaces/" + deploymentSpace
+ "/openapi?decisionServiceId="
+ encodeURIComponent(decisionServiceId) + "&outputFormat=JSON"
+ "/openapi";
Expand Down
5 changes: 2 additions & 3 deletions src/ditool.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import {debug} from "./debug.js";
import {getDecisionMetadata} from './diruntimeclient.js';
import {Constants} from "./constants.js";
import {Configuration} from "./command-line.js";

export async function getToolName(configuration: Configuration, info: Record<string, string>, operationId: string, decisionServiceId: string, toolNames: string[]): Promise<string> {
export async function getToolName(configuration: Configuration, deploymentSpace: string, info: Record<string, string>, operationId: string, decisionServiceId: string, toolNames: string[]): Promise<string> {
const serviceName = info["x-ibm-ads-decision-service-name"];
debug("decisionServiceName", serviceName);
const decisionId = info["x-ibm-ads-decision-id"];
Expand All @@ -20,7 +19,7 @@ export async function getToolName(configuration: Configuration, info: Record<str
[key: string]: MetadataEntry;
};

const metadata: { map: MetadataMap } = await getDecisionMetadata(configuration, Constants.DEVELOPMENT_DEPLOYMENT_SPACE, decisionId)
const metadata: { map: MetadataMap } = await getDecisionMetadata(configuration, deploymentSpace, decisionId)
debug("metadata", JSON.stringify(metadata, null, " "));

const metadataName = `mcpToolName.${operationId}`;
Expand Down
28 changes: 15 additions & 13 deletions src/mcp-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ function getToolDefinition(path: OpenAPIV3_1.PathItemObject, components: OpenAPI
};
}

async function registerTool(server: McpServer, configuration: Configuration, decisionOpenAPI: OpenAPIV3_1.Document, decisionServiceId: string, toolNames: string[]) {
async function registerTool(server: McpServer, configuration: Configuration, deploymentSpace: string, decisionOpenAPI: OpenAPIV3_1.Document, decisionServiceId: string, toolNames: string[]) {
for (const key in decisionOpenAPI.paths) {
debug("Found operationName", key);

Expand Down Expand Up @@ -86,7 +86,7 @@ async function registerTool(server: McpServer, configuration: Configuration, dec
debug("inputSchema", inputSchema);

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const toolName = await getToolName(configuration, (decisionOpenAPI as any).info, operationId, decisionServiceId, toolNames)
const toolName = await getToolName(configuration, deploymentSpace, (decisionOpenAPI as any).info, operationId, decisionServiceId, toolNames);
debug("toolName", toolName, toolNames);
toolNames.push(toolName);

Expand All @@ -96,7 +96,7 @@ async function registerTool(server: McpServer, configuration: Configuration, dec
async (input) => {
const decInput = input;
debug("Execute decision with", JSON.stringify(decInput, null, " "))
const str = await executeLastDeployedDecisionService(configuration, decisionServiceId, operationId, decInput);
const str = await executeLastDeployedDecisionService(configuration, deploymentSpace, decisionServiceId, operationId, decInput);
return {
content: [{ type: "text", text: str}]
};
Expand All @@ -112,17 +112,19 @@ export async function createMcpServer(name: string, configuration: Configuration
version: version
});

const spaceMetadata = await getMetadata(configuration, 'development');
debug("spaceMetadata", JSON.stringify(spaceMetadata, null, " "));
const serviceIds = getDecisionServiceIds(spaceMetadata);
debug("serviceIds", JSON.stringify(serviceIds, null, " "));

const toolNames: string[] = [];
for (const serviceId of serviceIds) {
debug("serviceId", serviceId);
const openapi = await getDecisionServiceOpenAPI(configuration, serviceId);

await registerTool(server, configuration, openapi, serviceId, toolNames);
for (const deploymentSpace of configuration.deploymentSpaces) {
debug("deploymentSpace", deploymentSpace);
const spaceMetadata = await getMetadata(configuration, deploymentSpace);
debug("spaceMetadata", JSON.stringify(spaceMetadata, null, " "));
const serviceIds = getDecisionServiceIds(spaceMetadata);
debug("serviceIds", JSON.stringify(serviceIds, null, " "));

for (const serviceId of serviceIds) {
debug("serviceId", serviceId);
const openapi = await getDecisionServiceOpenAPI(configuration, deploymentSpace, serviceId);
await registerTool(server, configuration, deploymentSpace, openapi, serviceId, toolNames);
}
}

if (configuration.isHttpTransport()) {
Expand Down
Loading