Skip to content
Open
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"@inquirer/prompts": "^7.10.1",
"@oclif/core": "^4",
"@oclif/multi-stage-output": "^0.8.36",
"@salesforce/agents": "^1.10.3",
"@salesforce/agents": "^1.11.0",
"@salesforce/core": "^8.28.3",
"@salesforce/kit": "^3.2.6",
"@salesforce/sf-plugins-core": "^12.2.6",
Expand Down
3 changes: 3 additions & 0 deletions schemas/agent-mcp-create.json
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,9 @@
},
"status": {
"type": "string"
},
"securityWarning": {
"type": "string"
}
},
"required": [
Expand Down
3 changes: 3 additions & 0 deletions schemas/agent-mcp-fetch.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@
},
"status": {
"type": "string"
},
"securityWarning": {
"type": "string"
}
},
"required": [
Expand Down
4 changes: 4 additions & 0 deletions src/commands/agent/mcp/asset/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,16 +62,20 @@ export default class ApiCatalogMcpServerAssetList extends SfCommand<ApiCatalogMc
data: (result.assets ?? []).map((a) => ({
id: a.id,
name: a.name,
label: a.label,
kind: a.kind,
active: a.active,
availableAsAgentAction: a.availableAsAgentAction,
description: a.description,
})),
columns: [
{ key: 'id', name: 'ID' },
{ key: 'name', name: 'Name' },
{ key: 'label', name: 'Label' },
{ key: 'kind', name: 'Kind' },
{ key: 'active', name: 'Active' },
{ key: 'availableAsAgentAction', name: 'Agent Action' },
{ key: 'description', name: 'Description' },
],
});
}
Expand Down
12 changes: 9 additions & 3 deletions src/commands/agent/mcp/asset/replace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,9 @@ export default class ApiCatalogMcpServerAssetReplace extends SfCommand<ApiCatalo
throw new SfError(messages.getMessage('error.invalidJson'), 'InvalidJson', [], 1);
}

const assets = (
Array.isArray(parsed) ? parsed : (parsed as { assets?: McpServerAssetReplaceItem[] }).assets
) as McpServerAssetReplaceItem[] | undefined;
const assets = (Array.isArray(parsed) ? parsed : (parsed as { assets?: McpServerAssetReplaceItem[] }).assets) as
| McpServerAssetReplaceItem[]
| undefined;
if (!assets || !Array.isArray(assets)) {
throw new SfError(messages.getMessage('error.invalidShape'), 'InvalidShape', [], 1);
}
Expand All @@ -101,14 +101,20 @@ export default class ApiCatalogMcpServerAssetReplace extends SfCommand<ApiCatalo
data: (result.assets ?? []).map((a) => ({
id: a.id,
name: a.name,
label: a.label,
kind: a.kind,
active: a.active,
availableAsAgentAction: a.availableAsAgentAction,
description: a.description,
})),
columns: [
{ key: 'id', name: 'ID' },
{ key: 'name', name: 'Name' },
{ key: 'label', name: 'Label' },
{ key: 'kind', name: 'Kind' },
{ key: 'active', name: 'Active' },
{ key: 'availableAsAgentAction', name: 'Agent Action' },
{ key: 'description', name: 'Description' },
],
});
}
Expand Down
22 changes: 22 additions & 0 deletions src/commands/agent/mcp/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,28 @@ export default class ApiCatalogMcpServerCreate extends SfCommand<McpServerCreate
if (!this.jsonEnabled()) {
this.log(`Created MCP server "${result.server.name}" (${result.server.id}).`);
this.log(`Discovered ${result.assets.length} asset(s).`);
if (result.assets.length > 0) {
this.table({
data: result.assets.map((a) => ({
name: a.name,
label: a.label,
kind: a.kind,
active: a.active,
availableAsAgentAction: a.availableAsAgentAction,
description: a.description,
securityWarning: a.securityWarning,
})),
columns: [
{ key: 'name', name: 'Name' },
{ key: 'label', name: 'Label' },
{ key: 'kind', name: 'Kind' },
{ key: 'active', name: 'Active' },
{ key: 'availableAsAgentAction', name: 'Agent Action' },
{ key: 'description', name: 'Description' },
{ key: 'securityWarning', name: 'Security Warning' },
],
});
}
}

return result;
Expand Down
14 changes: 7 additions & 7 deletions src/commands/agent/mcp/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,30 +48,30 @@ export default class ApiCatalogMcpServerFetch extends SfCommand<ApiCatalogMcpSer
result = await ApiCatalog.fetchMcpServer(connection, flags['mcp-server-id']);
} catch (error) {
const wrapped = SfError.wrap(error);
throw new SfError(
messages.getMessage('error.failed', [wrapped.message]),
'FetchMcpServerFailed',
[],
4,
wrapped
);
throw new SfError(messages.getMessage('error.failed', [wrapped.message]), 'FetchMcpServerFailed', [], 4, wrapped);
}

if (!this.jsonEnabled()) {
this.table({
data: (result.assets ?? []).map((a) => ({
name: a.name,
label: a.label,
kind: a.kind,
status: a.status,
active: a.active,
availableAsAgentAction: a.availableAsAgentAction,
description: a.description,
securityWarning: a.securityWarning,
})),
columns: [
{ key: 'name', name: 'Name' },
{ key: 'label', name: 'Label' },
{ key: 'kind', name: 'Kind' },
{ key: 'status', name: 'Status' },
{ key: 'active', name: 'Active' },
{ key: 'availableAsAgentAction', name: 'Agent Action' },
{ key: 'description', name: 'Description' },
{ key: 'securityWarning', name: 'Security Warning' },
],
});
}
Expand Down
22 changes: 9 additions & 13 deletions src/commands/agent/mcp/get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,22 +48,18 @@ export default class ApiCatalogMcpServerGet extends SfCommand<ApiCatalogMcpServe
result = await ApiCatalog.getMcpServer(connection, flags['mcp-server-id']);
} catch (error) {
const wrapped = SfError.wrap(error);
throw new SfError(
messages.getMessage('error.failed', [wrapped.message]),
'GetMcpServerFailed',
[],
4,
wrapped
);
throw new SfError(messages.getMessage('error.failed', [wrapped.message]), 'GetMcpServerFailed', [], 4, wrapped);
}

if (!this.jsonEnabled()) {
this.log(`Id: ${result.id}`);
this.log(`Name: ${result.name}`);
this.log(`Label: ${result.label ?? ''}`);
this.log(`Type: ${result.type}`);
this.log(`Status: ${result.status}`);
this.log(`Server URL: ${result.serverUrl ?? ''}`);
this.log(`Id: ${result.id}`);
this.log(`Name: ${result.name}`);
this.log(`Label: ${result.label ?? ''}`);
this.log(`Type: ${result.type}`);
this.log(`Status: ${result.status}`);
this.log(`Auth Type: ${result.authorization?.authType ?? ''}`);
this.log(`Server URL: ${result.serverUrl ?? ''}`);
this.log(`Description: ${result.description ?? ''}`);
}

return result;
Expand Down
21 changes: 13 additions & 8 deletions src/commands/agent/mcp/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,25 +58,30 @@ export default class ApiCatalogMcpServerList extends SfCommand<ApiCatalogMcpServ
});
} catch (error) {
const wrapped = SfError.wrap(error);
throw new SfError(
messages.getMessage('error.failed', [wrapped.message]),
'ListMcpServersFailed',
[],
4,
wrapped
);
throw new SfError(messages.getMessage('error.failed', [wrapped.message]), 'ListMcpServersFailed', [], 4, wrapped);
}

if (!this.jsonEnabled()) {
this.table({
data: result.mcpServers ?? [],
data: (result.mcpServers ?? []).map((s) => ({
id: s.id,
name: s.name,
label: s.label,
type: s.type,
status: s.status,
authType: s.authorization?.authType,
serverUrl: s.serverUrl,
description: s.description,
})),
columns: [
{ key: 'id', name: 'ID' },
{ key: 'name', name: 'Name' },
{ key: 'label', name: 'Label' },
{ key: 'type', name: 'Type' },
{ key: 'status', name: 'Status' },
{ key: 'authType', name: 'Auth Type' },
{ key: 'serverUrl', name: 'Server URL' },
{ key: 'description', name: 'Description' },
],
});
}
Expand Down
111 changes: 108 additions & 3 deletions test/commands/agent/mcp/mcp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,10 @@ import AgentMcpAssetReplace from '../../../../src/commands/agent/mcp/asset/repla

describe('agent mcp commands', () => {
const $$ = new TestContext();
let sfCommandStubs: ReturnType<typeof stubSfCommandUx>;

beforeEach(() => {
stubSfCommandUx($$.SANDBOX);
sfCommandStubs = stubSfCommandUx($$.SANDBOX);
});

afterEach(() => {
Expand Down Expand Up @@ -275,7 +276,14 @@ describe('agent mcp commands', () => {
return Promise.resolve({ assets: [] });
};

await AgentMcpAssetReplace.run(['--target-org', testOrg.username, '--mcp-server-id', '0XS1', '--assets-file', file]);
await AgentMcpAssetReplace.run([
'--target-org',
testOrg.username,
'--mcp-server-id',
'0XS1',
'--assets-file',
file,
]);

expect(JSON.parse(captured.body as string).assets[0].name).to.equal('tool-b');
});
Expand Down Expand Up @@ -338,7 +346,14 @@ describe('agent mcp commands', () => {
$$.fakeConnectionRequest = () => Promise.resolve({} as any);

try {
await AgentMcpAssetReplace.run(['--target-org', testOrg.username, '--mcp-server-id', '0XS1', '--assets-file', file]);
await AgentMcpAssetReplace.run([
'--target-org',
testOrg.username,
'--mcp-server-id',
'0XS1',
'--assets-file',
file,
]);
expect.fail('should have thrown');
} catch (err) {
expect((err as { name: string }).name).to.equal('InvalidShape');
Expand Down Expand Up @@ -407,4 +422,94 @@ describe('agent mcp commands', () => {
expect(captured.url).to.match(/\/mcp-servers\/0XS1\/assets$/);
expect(result.assets[0].name).to.equal('McpTool__add');
});

it('asset list renders the tool description in the human-readable table', async () => {
const testOrg = new MockTestOrgData();
await $$.stubAuths(testOrg);
$$.fakeConnectionRequest = () =>
Promise.resolve({
assets: [{ id: '1', name: 'McpTool__add', kind: 'MCP_TOOL', active: true, description: 'Add two numbers' }],
});

await AgentMcpAssetList.run(['--target-org', testOrg.username, '--mcp-server-id', '0XS1']);

// The table must expose a Description column so a user can review a tool before approving it.
const tableArg = sfCommandStubs.table.firstCall.firstArg as {
data: Array<Record<string, unknown>>;
columns: Array<{ key: string; name: string }>;
};
expect(tableArg.columns.map((c) => c.key)).to.include('description');
expect(tableArg.data[0].description).to.equal('Add two numbers');
});

it('create renders the tool description and security warning in the discovered-asset table', async () => {
const testOrg = new MockTestOrgData();
await $$.stubAuths(testOrg);
// The auto-fetch on create returns the same live tool metadata as /fetch,
// including securityWarning — so the discovered-asset table must surface it.
$$.fakeConnectionRequest = () =>
Promise.resolve({
server: { id: '0XS1', name: 'my-server', type: 'EXTERNAL', status: 'ACTIVE' },
assets: [
{
name: 'McpTool__add',
kind: 'MCP_TOOL',
active: false,
description: 'Add two numbers',
securityWarning: 'Tool description contains text in multiple languages or scripts.',
},
],
});

await AgentMcpCreate.run([
'--target-org',
testOrg.username,
'--name',
'my-server',
'--server-url',
'https://example.com/mcp',
]);

const tableArg = sfCommandStubs.table.firstCall.firstArg as {
data: Array<Record<string, unknown>>;
columns: Array<{ key: string; name: string }>;
};
const keys = tableArg.columns.map((c) => c.key);
expect(keys).to.include('description');
expect(keys).to.include('securityWarning');
expect(tableArg.data[0].description).to.equal('Add two numbers');
expect(tableArg.data[0].securityWarning).to.equal(
'Tool description contains text in multiple languages or scripts.'
);
});

it('fetch renders the tool description and security warning in the human-readable table', async () => {
const testOrg = new MockTestOrgData();
await $$.stubAuths(testOrg);
// securityWarning is only populated by the live /fetch introspection.
$$.fakeConnectionRequest = () =>
Promise.resolve({
assets: [
{
name: 'McpTool__add',
kind: 'MCP_TOOL',
status: 'IN_SYNC',
description: 'Add two numbers',
securityWarning: 'Tool description was modified since last approval.',
},
],
});

await AgentMcpFetch.run(['--target-org', testOrg.username, '--mcp-server-id', '0XS1']);

const tableArg = sfCommandStubs.table.firstCall.firstArg as {
data: Array<Record<string, unknown>>;
columns: Array<{ key: string; name: string }>;
};
const keys = tableArg.columns.map((c) => c.key);
expect(keys).to.include('description');
expect(keys).to.include('securityWarning');
expect(tableArg.data[0].description).to.equal('Add two numbers');
expect(tableArg.data[0].securityWarning).to.equal('Tool description was modified since last approval.');
});
});
8 changes: 4 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1411,10 +1411,10 @@
resolved "https://registry.yarnpkg.com/@rtsao/scc/-/scc-1.1.0.tgz#927dd2fae9bc3361403ac2c7a00c32ddce9ad7e8"
integrity sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==

"@salesforce/agents@^1.10.3":
version "1.10.3"
resolved "https://registry.yarnpkg.com/@salesforce/agents/-/agents-1.10.3.tgz#5f78f25624c26ca0679b7654c60808d2e612297f"
integrity sha512-Cww6DizTMcPGi9oK8JrVh2Yt2Mg00l7qUOPRS2U/oRFkXXoMBzzotlA6aj44EuX9PAHR93N1DVUM4WCPTzaSQg==
"@salesforce/agents@^1.11.0":
version "1.11.0"
resolved "https://registry.yarnpkg.com/@salesforce/agents/-/agents-1.11.0.tgz#069145a3c0af97377dfb7382dbebd6731873d5d5"
integrity sha512-uH7/xFTLpdRB7v8A+PCY3hLdJTSLazNGsl9XmfC52ADlSdnBoxigvOwV05gMbjfhqml0y3USmiuYr+MkLYteeg==
dependencies:
"@salesforce/core" "^8.31.0"
"@salesforce/kit" "^3.2.6"
Expand Down