powerautomate-mcp 0.7.4 → 0.7.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -20,13 +20,16 @@ An MCP (Model Context Protocol) server that connects Claude to Microsoft Power A
20
20
  - Microsoft 365 work account with Power Automate access
21
21
  - **Linux only**: libsecret for secure token storage
22
22
  ```bash
23
- # Ubuntu/Debian
24
- sudo apt-get install libsecret-1-dev gnome-keyring
23
+ # Ubuntu/Debian runtime
24
+ sudo apt-get install libsecret-1-0 gnome-keyring
25
25
 
26
- # Fedora/RHEL
27
- sudo dnf install libsecret-devel gnome-keyring
26
+ # Fedora/RHEL runtime
27
+ sudo dnf install libsecret gnome-keyring
28
28
  ```
29
29
 
30
+ If setup fails with `libsecret-1.so.0: cannot open shared object file`, the
31
+ runtime package above is missing.
32
+
30
33
  ### Installation
31
34
 
32
35
  ```bash
@@ -215,7 +218,7 @@ npm run build
215
218
 
216
219
  This server implements defense-in-depth security:
217
220
 
218
- - **Secure Token Storage**: DPAPI (Windows), Keychain (macOS), libsecret (Linux) no plaintext fallback
221
+ - **Secure Token Storage**: DPAPI (Windows), Keychain (macOS), libsecret on Linux when available, with a 0o600 file-cache fallback when it is not
219
222
  - **Input Validation**: GUID validation on all IDs, OData injection protection (ASCII-only), path traversal blocking (including URL-encoded), SharePoint hostname allowlist
220
223
  - **SSRF Prevention**: Domain allowlists on Dataverse URLs, resource links, and token resources
221
224
  - **Injection Prevention**: Power Automate expression injection blocking (`@{`/`}@`), OData filter sanitization, command injection prevention (`execFile` over `exec`)
package/dist/index.js CHANGED
@@ -13,7 +13,6 @@ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/
13
13
  import { ListToolsRequestSchema, CallToolRequestSchema, ListResourceTemplatesRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema } from '@modelcontextprotocol/sdk/types.js';
14
14
  import { PublicClientApplication, LogLevel } from '@azure/msal-node';
15
15
  import { execFile } from 'child_process';
16
- import { PersistenceCreator, DataProtectionScope, PersistenceCachePlugin } from '@azure/msal-node-extensions';
17
16
  import { request } from 'undici';
18
17
  import Database from 'better-sqlite3';
19
18
  import { fileURLToPath } from 'url';
@@ -25,7 +24,13 @@ var EnvironmentSchema = z.object({
25
24
  /** Azure region (e.g., "unitedstates", "europe") */
26
25
  region: z.string().min(1),
27
26
  /** Human-readable name */
28
- displayName: z.string().optional()
27
+ displayName: z.string().optional(),
28
+ /**
29
+ * Optional override for the Dataverse hostname (e.g. "contoso.crm.dynamics.com").
30
+ * When omitted, the MCP resolves it at startup via the BAP admin API and falls
31
+ * back to a region-based guess if that call fails.
32
+ */
33
+ dataverseUrl: z.string().optional()
29
34
  });
30
35
  var AuthConfigSchema = z.object({
31
36
  /** Microsoft Entra application (client) ID */
@@ -423,7 +428,8 @@ function resolveEnvironment(config, envNameOrId) {
423
428
  name: envKey,
424
429
  id: envConfig.id,
425
430
  region: envConfig.region,
426
- displayName: envConfig.displayName
431
+ displayName: envConfig.displayName,
432
+ dataverseUrl: envConfig.dataverseUrl
427
433
  };
428
434
  }
429
435
  {
@@ -610,6 +616,19 @@ function validateRowId(id) {
610
616
  }
611
617
  return id;
612
618
  }
619
+ function validateFlowRunId(id) {
620
+ if (typeof id !== "string" || id.length < 10 || id.length > 128) {
621
+ throw new ConfigurationError(
622
+ `Invalid flow run ID format: expected alphanumeric identifier (10-128 chars)`
623
+ );
624
+ }
625
+ if (!/^[0-9A-Za-z]+$/.test(id)) {
626
+ throw new ConfigurationError(
627
+ `Invalid flow run ID format: expected alphanumeric identifier (10-128 chars)`
628
+ );
629
+ }
630
+ return id;
631
+ }
613
632
  function validateSharePointHostname(hostname) {
614
633
  const validPattern = /^[a-z0-9-]+\.sharepoint\.(com|us|de|cn)$/i;
615
634
  if (!validPattern.test(hostname)) {
@@ -725,6 +744,23 @@ function validateEnvironmentId(envId) {
725
744
  }
726
745
  return envId;
727
746
  }
747
+ var msalNodeExtensionsPromise = null;
748
+ async function loadMsalNodeExtensions() {
749
+ if (!msalNodeExtensionsPromise) {
750
+ msalNodeExtensionsPromise = import('@azure/msal-node-extensions').then((mod) => {
751
+ const moduleWithOptionalDefault = mod;
752
+ const resolved = moduleWithOptionalDefault.default ?? moduleWithOptionalDefault;
753
+ if (!resolved || !("PersistenceCreator" in resolved)) {
754
+ throw new Error("msal-node-extensions did not expose the expected exports");
755
+ }
756
+ return resolved;
757
+ });
758
+ }
759
+ return msalNodeExtensionsPromise;
760
+ }
761
+ function formatError(error) {
762
+ return error instanceof Error ? error.message : String(error);
763
+ }
728
764
  var NativeFileCachePlugin = class {
729
765
  cachePath;
730
766
  constructor(cachePath) {
@@ -765,6 +801,11 @@ async function createPersistentCachePlugin(cachePath) {
765
801
  logger.debug({ cacheLocation, platform: platform }, "Creating persistent token cache");
766
802
  if (platform === "win32" || platform === "darwin") {
767
803
  try {
804
+ const {
805
+ DataProtectionScope,
806
+ PersistenceCreator,
807
+ PersistenceCachePlugin
808
+ } = await loadMsalNodeExtensions();
768
809
  const persistence = await PersistenceCreator.createPersistence({
769
810
  cachePath: cacheLocation,
770
811
  dataProtectionScope: platform === "win32" ? DataProtectionScope.CurrentUser : void 0,
@@ -777,11 +818,15 @@ async function createPersistentCachePlugin(cachePath) {
777
818
  } catch (err) {
778
819
  logger.error({ err, cacheLocation }, "Failed to create persistent cache");
779
820
  throw new Error(
780
- `Failed to initialize token cache at ${cacheLocation}: ${err instanceof Error ? err.message : String(err)}`
821
+ `Failed to initialize token cache at ${cacheLocation}: ${formatError(err)}`
781
822
  );
782
823
  }
783
824
  }
784
825
  try {
826
+ const {
827
+ PersistenceCreator,
828
+ PersistenceCachePlugin
829
+ } = await loadMsalNodeExtensions();
785
830
  const persistence = await PersistenceCreator.createPersistence({
786
831
  cachePath: cacheLocation,
787
832
  serviceName: "powerautomate-mcp",
@@ -793,7 +838,7 @@ async function createPersistentCachePlugin(cachePath) {
793
838
  return new PersistenceCachePlugin(persistence);
794
839
  } catch (libsecretError) {
795
840
  logger.warn(
796
- { err: libsecretError instanceof Error ? libsecretError.message : String(libsecretError) },
841
+ { err: formatError(libsecretError) },
797
842
  "libsecret unavailable \u2014 using native file cache fallback (0o600 permissions)"
798
843
  );
799
844
  logger.info({ cacheLocation }, "Persistent token cache initialized (native file)");
@@ -1411,7 +1456,7 @@ var FlowManagementApi = class {
1411
1456
  async getFlowRunActions(flowId, runId, environmentId) {
1412
1457
  const envId = environmentId ?? this.defaultEnvironmentId;
1413
1458
  const safeFlowId = validateRowId(flowId);
1414
- const safeRunId = validateRowId(runId);
1459
+ const safeRunId = validateFlowRunId(runId);
1415
1460
  apiLogger.debug({ flowId: safeFlowId, runId: safeRunId, envId }, "Getting flow run actions");
1416
1461
  const url = this.buildUrl(envId, `/flows/${safeFlowId}/runs/${safeRunId}/actions`);
1417
1462
  const headers = await this.getAuthHeaders();
@@ -1489,7 +1534,7 @@ var FlowManagementApi = class {
1489
1534
  async resubmitRun(flowId, runId, environmentId) {
1490
1535
  const envId = environmentId ?? this.defaultEnvironmentId;
1491
1536
  const safeFlowId = validateRowId(flowId);
1492
- const safeRunId = validateRowId(runId);
1537
+ const safeRunId = validateFlowRunId(runId);
1493
1538
  apiLogger.info({ flowId: safeFlowId, runId: safeRunId, envId }, "Resubmitting flow run");
1494
1539
  const flow = await this.getFlow(safeFlowId, envId);
1495
1540
  const triggers = flow.properties.definition?.triggers ?? {};
@@ -1512,7 +1557,7 @@ var FlowManagementApi = class {
1512
1557
  async cancelRun(flowId, runId, environmentId) {
1513
1558
  const envId = environmentId ?? this.defaultEnvironmentId;
1514
1559
  const safeFlowId = validateRowId(flowId);
1515
- const safeRunId = validateRowId(runId);
1560
+ const safeRunId = validateFlowRunId(runId);
1516
1561
  apiLogger.info({ flowId: safeFlowId, runId: safeRunId, envId }, "Cancelling flow run");
1517
1562
  const url = this.buildUrl(envId, `/flows/${safeFlowId}/runs/${safeRunId}/cancel`);
1518
1563
  const headers = await this.getAuthHeaders();
@@ -3582,15 +3627,18 @@ var DataverseApi = class {
3582
3627
  const select = "$select=LogicalName,DisplayName,EntitySetName,Description,IsCustomEntity,IsManaged,SchemaName,PrimaryIdAttribute,PrimaryNameAttribute";
3583
3628
  const parts = [select];
3584
3629
  if (options.filter) parts.push(`$filter=${validateODataFilter(options.filter)}`);
3585
- parts.push(`$top=${options.top ?? 100}`);
3586
- parts.push("$orderby=LogicalName asc");
3587
3630
  const url = this.buildUrl("EntityDefinitions", parts.join("&"));
3588
3631
  const headers = await this.getAuthHeaders();
3632
+ const cap = options.top ?? 100;
3589
3633
  return wrapApiCall(async () => {
3590
3634
  const response = await request(url, { method: "GET", headers });
3591
3635
  const data = await this.handleResponse(response, "List tables");
3592
- logger2.info({ count: data.value.length }, "Listed Dataverse tables");
3593
- return data.value;
3636
+ const sorted = [...data.value].sort(
3637
+ (a, b) => a.LogicalName.localeCompare(b.LogicalName)
3638
+ );
3639
+ const limited = sorted.slice(0, cap);
3640
+ logger2.info({ count: limited.length, total: data.value.length }, "Listed Dataverse tables");
3641
+ return limited;
3594
3642
  }, "List tables");
3595
3643
  }
3596
3644
  /**
@@ -3722,8 +3770,12 @@ var DataverseApi = class {
3722
3770
  // Environment Discovery
3723
3771
  // ==========================================================================
3724
3772
  /**
3725
- * Discover Dataverse environment URL from Power Platform environment
3726
- * This is needed because Dataverse URL format differs from Flow API
3773
+ * Construct a best-guess Dataverse hostname from an environment GUID + region.
3774
+ *
3775
+ * WARNING: this is a last-resort fallback. The real Dataverse hostname is the
3776
+ * org's `domainName` (exposed as `linkedEnvironmentMetadata.instanceUrl` on the
3777
+ * BAP admin API) which usually does NOT match the environment GUID. Prefer
3778
+ * resolving instanceUrl from BAP, or set `dataverseUrl` in the env config.
3727
3779
  */
3728
3780
  static getDataverseUrl(environmentId, region = "crm") {
3729
3781
  const regionMap = {
@@ -14541,15 +14593,34 @@ async function createMcpServer(serverConfig) {
14541
14593
  });
14542
14594
  let dataverseApi = null;
14543
14595
  try {
14544
- const dataverseUrl = DataverseApi.getDataverseUrl(
14545
- defaultEnv.id,
14546
- defaultEnv.region
14547
- );
14596
+ let dataverseUrl;
14597
+ let source = "fallback";
14598
+ if (defaultEnv.dataverseUrl) {
14599
+ dataverseUrl = defaultEnv.dataverseUrl;
14600
+ source = "config";
14601
+ } else {
14602
+ try {
14603
+ const envDetail = await adminApi.getEnvironment(defaultEnv.id);
14604
+ const instanceUrl = envDetail.properties?.linkedEnvironmentMetadata?.instanceUrl;
14605
+ if (instanceUrl) {
14606
+ dataverseUrl = instanceUrl;
14607
+ source = "bap";
14608
+ }
14609
+ } catch (err) {
14610
+ logger.warn(
14611
+ { err: err instanceof Error ? err.message : String(err) },
14612
+ "BAP admin API did not return a Dataverse instanceUrl \u2014 falling back to region guess (may fail DNS)"
14613
+ );
14614
+ }
14615
+ }
14616
+ if (!dataverseUrl) {
14617
+ dataverseUrl = DataverseApi.getDataverseUrl(defaultEnv.id, defaultEnv.region);
14618
+ }
14548
14619
  dataverseApi = new DataverseApi({
14549
14620
  authProvider,
14550
14621
  environmentUrl: dataverseUrl
14551
14622
  });
14552
- logger.info({ dataverseUrl }, "Dataverse API initialized");
14623
+ logger.info({ dataverseUrl, source }, "Dataverse API initialized");
14553
14624
  } catch (error) {
14554
14625
  logger.warn({ error }, "Failed to initialize Dataverse API \u2014 Dataverse tools will return guidance");
14555
14626
  }
@@ -15134,6 +15205,7 @@ var c = {
15134
15205
  var icons = {
15135
15206
  check: `${c.green}\u2713${c.reset}`,
15136
15207
  cross: `${c.red}\u2717${c.reset}`,
15208
+ info: `${c.blue}i${c.reset}`,
15137
15209
  arrow: `${c.cyan}\u2192${c.reset}`,
15138
15210
  bullet: `${c.dim}\u2022${c.reset}`,
15139
15211
  star: `${c.yellow}\u2605${c.reset}`