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 +8 -5
- package/dist/index.js +91 -19
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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-
|
|
23
|
+
# Ubuntu/Debian runtime
|
|
24
|
+
sudo apt-get install libsecret-1-0 gnome-keyring
|
|
25
25
|
|
|
26
|
-
# Fedora/RHEL
|
|
27
|
-
sudo dnf install libsecret
|
|
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
|
|
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}: ${
|
|
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:
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
3593
|
-
|
|
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
|
-
*
|
|
3726
|
-
*
|
|
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
|
-
|
|
14545
|
-
|
|
14546
|
-
|
|
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}`
|