@rockcarver/frodo-cli 4.0.0-50 → 4.0.0-52
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/CHANGELOG.md +7 -1
- package/dist/app.cjs +12916 -8240
- package/dist/app.cjs.map +1 -1
- package/package.json +10 -2
- package/tools/agent-e2e-audit.mjs +219 -0
- package/tools/mcp-agentic-assess.mjs +204 -0
- package/tools/mcp-agentic-batch-run.mjs +1105 -0
- package/tools/mcp-agentic-score.mjs +200 -0
- package/tools/mcp-aiagent-introspection-test.mjs +353 -0
- package/tools/mcp-oauth2-mayact-update-test.mjs +429 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
function parseArgs(argv) {
|
|
7
|
+
const args = { input: '', output: '' };
|
|
8
|
+
for (let i = 2; i < argv.length; i += 1) {
|
|
9
|
+
const token = argv[i];
|
|
10
|
+
if (token === '--input') {
|
|
11
|
+
args.input = argv[i + 1] || '';
|
|
12
|
+
i += 1;
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
if (token === '--output') {
|
|
16
|
+
args.output = argv[i + 1] || '';
|
|
17
|
+
i += 1;
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return args;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function readJson(filePath) {
|
|
25
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
26
|
+
return JSON.parse(raw);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function round(value, decimals = 2) {
|
|
30
|
+
const scale = 10 ** decimals;
|
|
31
|
+
return Math.round(value * scale) / scale;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function computeRunMetrics(run) {
|
|
35
|
+
const attempts = Array.isArray(run.attempts) ? run.attempts : [];
|
|
36
|
+
const totalAttempts = attempts.length;
|
|
37
|
+
const firstAttemptSuccess = attempts.length > 0 && attempts[0].success === true;
|
|
38
|
+
|
|
39
|
+
let duplicateRetryCount = 0;
|
|
40
|
+
let intentionalFollowUpCount = 0;
|
|
41
|
+
let temptationAttemptCount = 0;
|
|
42
|
+
let temptationFailureCount = 0;
|
|
43
|
+
const firstSeen = new Set();
|
|
44
|
+
for (const attempt of attempts) {
|
|
45
|
+
const toolName = attempt?.toolName;
|
|
46
|
+
if (!toolName) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (attempt?.purpose === 'temptation') {
|
|
51
|
+
temptationAttemptCount += 1;
|
|
52
|
+
if (attempt.success !== true) {
|
|
53
|
+
temptationFailureCount += 1;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (firstSeen.has(toolName)) {
|
|
58
|
+
if (attempt?.retryIntent) {
|
|
59
|
+
intentionalFollowUpCount += 1;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
duplicateRetryCount += 1;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
firstSeen.add(toolName);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
totalAttempts,
|
|
70
|
+
firstAttemptSuccess,
|
|
71
|
+
duplicateRetryCount,
|
|
72
|
+
intentionalFollowUpCount,
|
|
73
|
+
temptationAttemptCount,
|
|
74
|
+
temptationFailureCount,
|
|
75
|
+
taskCompleted: run.taskCompleted === true,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function aggregateVariant(runs) {
|
|
80
|
+
const totals = {
|
|
81
|
+
runCount: runs.length,
|
|
82
|
+
completedCount: 0,
|
|
83
|
+
totalAttempts: 0,
|
|
84
|
+
firstAttemptSuccessCount: 0,
|
|
85
|
+
duplicateRetryCount: 0,
|
|
86
|
+
intentionalFollowUpCount: 0,
|
|
87
|
+
temptationAttemptCount: 0,
|
|
88
|
+
temptationFailureCount: 0,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
for (const run of runs) {
|
|
92
|
+
const m = computeRunMetrics(run);
|
|
93
|
+
totals.completedCount += m.taskCompleted ? 1 : 0;
|
|
94
|
+
totals.totalAttempts += m.totalAttempts;
|
|
95
|
+
totals.firstAttemptSuccessCount += m.firstAttemptSuccess ? 1 : 0;
|
|
96
|
+
totals.duplicateRetryCount += m.duplicateRetryCount;
|
|
97
|
+
totals.intentionalFollowUpCount += m.intentionalFollowUpCount;
|
|
98
|
+
totals.temptationAttemptCount += m.temptationAttemptCount;
|
|
99
|
+
totals.temptationFailureCount += m.temptationFailureCount;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const denominator = totals.runCount || 1;
|
|
103
|
+
const temptationDenominator = totals.temptationAttemptCount || 1;
|
|
104
|
+
return {
|
|
105
|
+
runCount: totals.runCount,
|
|
106
|
+
completedCount: totals.completedCount,
|
|
107
|
+
completionRate: round((totals.completedCount / denominator) * 100),
|
|
108
|
+
meanAttemptsPerTask: round(totals.totalAttempts / denominator),
|
|
109
|
+
firstAttemptSuccessRate: round(
|
|
110
|
+
(totals.firstAttemptSuccessCount / denominator) * 100
|
|
111
|
+
),
|
|
112
|
+
duplicateRetryRate: round((totals.duplicateRetryCount / denominator) * 100),
|
|
113
|
+
intentionalFollowUpRate: round(
|
|
114
|
+
(totals.intentionalFollowUpCount / denominator) * 100
|
|
115
|
+
),
|
|
116
|
+
temptationFailureRate:
|
|
117
|
+
totals.temptationAttemptCount > 0
|
|
118
|
+
? round((totals.temptationFailureCount / temptationDenominator) * 100)
|
|
119
|
+
: null,
|
|
120
|
+
totalAttempts: totals.totalAttempts,
|
|
121
|
+
duplicateRetryCount: totals.duplicateRetryCount,
|
|
122
|
+
intentionalFollowUpCount: totals.intentionalFollowUpCount,
|
|
123
|
+
temptationAttemptCount: totals.temptationAttemptCount,
|
|
124
|
+
temptationFailureCount: totals.temptationFailureCount,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function buildScoreboard(runLog) {
|
|
129
|
+
const runs = Array.isArray(runLog.runs) ? runLog.runs : [];
|
|
130
|
+
const byVariant = new Map();
|
|
131
|
+
|
|
132
|
+
for (const run of runs) {
|
|
133
|
+
const variant = run?.variant || 'unknown';
|
|
134
|
+
if (!byVariant.has(variant)) {
|
|
135
|
+
byVariant.set(variant, []);
|
|
136
|
+
}
|
|
137
|
+
byVariant.get(variant).push(run);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const variants = {};
|
|
141
|
+
for (const [variant, variantRuns] of byVariant.entries()) {
|
|
142
|
+
variants[variant] = aggregateVariant(variantRuns);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
generatedAt: new Date().toISOString(),
|
|
147
|
+
runCount: runs.length,
|
|
148
|
+
variants,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function printScoreboard(scoreboard) {
|
|
153
|
+
console.log('MCP Agentic Scoreboard');
|
|
154
|
+
console.log('======================');
|
|
155
|
+
console.log(`Runs: ${scoreboard.runCount}`);
|
|
156
|
+
|
|
157
|
+
const variantNames = Object.keys(scoreboard.variants).sort();
|
|
158
|
+
for (const name of variantNames) {
|
|
159
|
+
const metrics = scoreboard.variants[name];
|
|
160
|
+
console.log('');
|
|
161
|
+
console.log(`Variant: ${name}`);
|
|
162
|
+
console.log(` Runs: ${metrics.runCount}`);
|
|
163
|
+
console.log(` Completion rate: ${metrics.completionRate}%`);
|
|
164
|
+
console.log(` Mean attempts/task: ${metrics.meanAttemptsPerTask}`);
|
|
165
|
+
console.log(` First-attempt success rate: ${metrics.firstAttemptSuccessRate}%`);
|
|
166
|
+
console.log(` Duplicate retry rate (unplanned): ${metrics.duplicateRetryRate}%`);
|
|
167
|
+
console.log(` Intentional follow-up rate: ${metrics.intentionalFollowUpRate}%`);
|
|
168
|
+
if (metrics.temptationFailureRate !== null) {
|
|
169
|
+
console.log(` Import/export temptation failure rate: ${metrics.temptationFailureRate}%`);
|
|
170
|
+
}
|
|
171
|
+
console.log(` Total attempts: ${metrics.totalAttempts}`);
|
|
172
|
+
console.log(` Duplicate retries (unplanned): ${metrics.duplicateRetryCount}`);
|
|
173
|
+
console.log(` Intentional follow-ups: ${metrics.intentionalFollowUpCount}`);
|
|
174
|
+
console.log(` Import/export temptation attempts: ${metrics.temptationAttemptCount}`);
|
|
175
|
+
console.log(` Import/export temptation failures: ${metrics.temptationFailureCount}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function main() {
|
|
180
|
+
const args = parseArgs(process.argv);
|
|
181
|
+
if (!args.input) {
|
|
182
|
+
console.error('Missing required --input <path> argument.');
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const inputPath = path.resolve(process.cwd(), args.input);
|
|
187
|
+
const runLog = readJson(inputPath);
|
|
188
|
+
const scoreboard = buildScoreboard(runLog);
|
|
189
|
+
|
|
190
|
+
printScoreboard(scoreboard);
|
|
191
|
+
|
|
192
|
+
if (args.output) {
|
|
193
|
+
const outputPath = path.resolve(process.cwd(), args.output);
|
|
194
|
+
fs.writeFileSync(outputPath, JSON.stringify(scoreboard, null, 2), 'utf8');
|
|
195
|
+
console.log('');
|
|
196
|
+
console.log(`Wrote scoreboard JSON to: ${outputPath}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
main();
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
7
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
8
|
+
|
|
9
|
+
function parseArgs(argv) {
|
|
10
|
+
const args = {
|
|
11
|
+
mcpConfig: '/Users/volker.scheuber/Library/Application Support/Code/User/mcp.json',
|
|
12
|
+
output: 'docs/mcp-aiagent-introspection-report.json',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
for (let i = 2; i < argv.length; i += 1) {
|
|
16
|
+
const token = argv[i];
|
|
17
|
+
if (token === '--mcp-config') {
|
|
18
|
+
args.mcpConfig = argv[i + 1] || args.mcpConfig;
|
|
19
|
+
i += 1;
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (token === '--output') {
|
|
23
|
+
args.output = argv[i + 1] || args.output;
|
|
24
|
+
i += 1;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return args;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function loadServerConfig(configPath) {
|
|
33
|
+
const raw = fs.readFileSync(configPath, 'utf8');
|
|
34
|
+
const json = JSON.parse(raw);
|
|
35
|
+
const server = json?.servers?.frodo;
|
|
36
|
+
if (!server?.command) {
|
|
37
|
+
throw new Error('Could not resolve servers.frodo.command from MCP config.');
|
|
38
|
+
}
|
|
39
|
+
return server;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function connectClient(server) {
|
|
43
|
+
const transport = new StdioClientTransport({
|
|
44
|
+
command: server.command,
|
|
45
|
+
args: server.args,
|
|
46
|
+
env: server.env,
|
|
47
|
+
cwd: '/Users/volker.scheuber/Documents/Projects/frodo-cli',
|
|
48
|
+
stderr: 'pipe',
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
if (transport.stderr) {
|
|
52
|
+
transport.stderr.on('data', () => {
|
|
53
|
+
// Drain stderr without polluting report output.
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const client = new Client(
|
|
58
|
+
{ name: 'frodo-aiagent-introspection-test', version: '1.0.0' },
|
|
59
|
+
{ capabilities: {} }
|
|
60
|
+
);
|
|
61
|
+
await client.connect(transport);
|
|
62
|
+
return { client, transport };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function parseToolPayload(result) {
|
|
66
|
+
const text = Array.isArray(result?.content)
|
|
67
|
+
? result.content
|
|
68
|
+
.filter((item) => item?.type === 'text' && typeof item?.text === 'string')
|
|
69
|
+
.map((item) => item.text)
|
|
70
|
+
.join(' ')
|
|
71
|
+
: '';
|
|
72
|
+
|
|
73
|
+
if (!text) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
return JSON.parse(text);
|
|
79
|
+
} catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function callTool(client, toolName, args) {
|
|
85
|
+
try {
|
|
86
|
+
const result = await client.callTool({ name: toolName, arguments: args });
|
|
87
|
+
if (result?.isError) {
|
|
88
|
+
const text = Array.isArray(result.content)
|
|
89
|
+
? result.content
|
|
90
|
+
.filter((item) => item?.type === 'text' && typeof item?.text === 'string')
|
|
91
|
+
.map((item) => item.text)
|
|
92
|
+
.join(' ')
|
|
93
|
+
: 'tool returned error';
|
|
94
|
+
return {
|
|
95
|
+
toolName,
|
|
96
|
+
success: false,
|
|
97
|
+
message: text,
|
|
98
|
+
payload: null,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
toolName,
|
|
104
|
+
success: true,
|
|
105
|
+
message: '',
|
|
106
|
+
payload: parseToolPayload(result),
|
|
107
|
+
};
|
|
108
|
+
} catch (error) {
|
|
109
|
+
return {
|
|
110
|
+
toolName,
|
|
111
|
+
success: false,
|
|
112
|
+
message: error instanceof Error ? error.message : String(error),
|
|
113
|
+
payload: null,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function getDiscoverData(discoverAttempt) {
|
|
119
|
+
return discoverAttempt?.payload?.data ?? null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function getOperationDetails(discoverData, operationType) {
|
|
123
|
+
return Array.isArray(discoverData?.operationDetailsByType?.[operationType])
|
|
124
|
+
? discoverData.operationDetailsByType[operationType]
|
|
125
|
+
: [];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function matchesAiAgent(entry) {
|
|
129
|
+
const re = /ai.?agent/i;
|
|
130
|
+
return re.test(entry.objectType || '') || re.test(entry.methodName || '') || re.test(entry.sourcePath || '');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function toArrayData(toolAttempt) {
|
|
134
|
+
const data = toolAttempt?.payload?.data;
|
|
135
|
+
if (Array.isArray(data)) {
|
|
136
|
+
return data;
|
|
137
|
+
}
|
|
138
|
+
if (data && typeof data === 'object' && Array.isArray(data.result)) {
|
|
139
|
+
return data.result;
|
|
140
|
+
}
|
|
141
|
+
return [];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function pickAiAgentRecords(records) {
|
|
145
|
+
return records.filter((record) => {
|
|
146
|
+
const serialized = JSON.stringify(record || {});
|
|
147
|
+
return /ai.?agent/i.test(serialized);
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function getRecordId(record) {
|
|
152
|
+
if (!record || typeof record !== 'object') {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (typeof record._id === 'string') {
|
|
157
|
+
return record._id;
|
|
158
|
+
}
|
|
159
|
+
if (typeof record.id === 'string') {
|
|
160
|
+
return record.id;
|
|
161
|
+
}
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function run() {
|
|
166
|
+
const args = parseArgs(process.argv);
|
|
167
|
+
const server = loadServerConfig(args.mcpConfig);
|
|
168
|
+
const { client, transport } = await connectClient(server);
|
|
169
|
+
|
|
170
|
+
const report = {
|
|
171
|
+
generatedAt: new Date().toISOString(),
|
|
172
|
+
status: 'unknown',
|
|
173
|
+
summary: '',
|
|
174
|
+
discovery: {},
|
|
175
|
+
attempts: [],
|
|
176
|
+
findings: {
|
|
177
|
+
aiCapabilityObjectTypes: [],
|
|
178
|
+
aiRecordsFound: 0,
|
|
179
|
+
introspectionSucceeded: false,
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
const discoverAttempt = await callTool(client, 'frodo_discover', {
|
|
185
|
+
domain: 'agent',
|
|
186
|
+
includeDetails: true,
|
|
187
|
+
});
|
|
188
|
+
report.attempts.push({
|
|
189
|
+
toolName: discoverAttempt.toolName,
|
|
190
|
+
success: discoverAttempt.success,
|
|
191
|
+
message: discoverAttempt.message,
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
if (!discoverAttempt.success) {
|
|
195
|
+
report.status = 'error';
|
|
196
|
+
report.summary = 'frodo_discover failed for domain agent.';
|
|
197
|
+
return report;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const discoverData = getDiscoverData(discoverAttempt);
|
|
201
|
+
const readDetails = getOperationDetails(discoverData, 'read').filter((entry) => entry.domain === 'agent');
|
|
202
|
+
const listDetails = getOperationDetails(discoverData, 'list').filter((entry) => entry.domain === 'agent');
|
|
203
|
+
const searchDetails = getOperationDetails(discoverData, 'search').filter((entry) => entry.domain === 'agent');
|
|
204
|
+
|
|
205
|
+
const aiCapabilityEntries = [...readDetails, ...listDetails, ...searchDetails].filter(matchesAiAgent);
|
|
206
|
+
const aiObjectTypeSet = new Set(aiCapabilityEntries.map((entry) => entry.objectType));
|
|
207
|
+
const aiCapabilityObjectTypes = [...aiObjectTypeSet].sort();
|
|
208
|
+
report.findings.aiCapabilityObjectTypes = aiCapabilityObjectTypes;
|
|
209
|
+
|
|
210
|
+
report.discovery = {
|
|
211
|
+
agentObjectTypes: discoverData?.objectTypesByDomain?.agent || [],
|
|
212
|
+
agentReadObjectTypes: readDetails.map((entry) => entry.objectType),
|
|
213
|
+
agentListObjectTypes: listDetails.map((entry) => entry.objectType),
|
|
214
|
+
agentSearchObjectTypes: searchDetails.map((entry) => entry.objectType),
|
|
215
|
+
aiCapabilityEntries: aiCapabilityEntries.map((entry) => ({
|
|
216
|
+
operationType: entry.operationType,
|
|
217
|
+
objectType: entry.objectType,
|
|
218
|
+
methodName: entry.methodName,
|
|
219
|
+
sourcePath: entry.sourcePath,
|
|
220
|
+
toolName: entry.toolName,
|
|
221
|
+
})),
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
// Determine object types to probe for AI agents. Prefer explicit AIAgent-like types.
|
|
225
|
+
const probeObjectTypes = aiCapabilityObjectTypes.length
|
|
226
|
+
? aiCapabilityObjectTypes
|
|
227
|
+
: (discoverData?.objectTypesByDomain?.agent || []).filter((objectType) =>
|
|
228
|
+
/agent/i.test(objectType)
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
const aiRecords = [];
|
|
232
|
+
for (const objectType of probeObjectTypes) {
|
|
233
|
+
const listAttempt = await callTool(client, 'frodo_list', {
|
|
234
|
+
domain: 'agent',
|
|
235
|
+
objectType,
|
|
236
|
+
includeTotal: true,
|
|
237
|
+
});
|
|
238
|
+
report.attempts.push({
|
|
239
|
+
toolName: listAttempt.toolName,
|
|
240
|
+
success: listAttempt.success,
|
|
241
|
+
message: listAttempt.message,
|
|
242
|
+
objectType,
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
if (!listAttempt.success) {
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const records = toArrayData(listAttempt);
|
|
250
|
+
const aiMatches = pickAiAgentRecords(records);
|
|
251
|
+
aiRecords.push(...aiMatches.map((record) => ({ objectType, record })));
|
|
252
|
+
|
|
253
|
+
// If search is supported for this object type, try a targeted AI-agent query.
|
|
254
|
+
const supportsSearch = searchDetails.some((entry) => entry.objectType === objectType);
|
|
255
|
+
if (supportsSearch) {
|
|
256
|
+
const searchAttempt = await callTool(client, 'frodo_search', {
|
|
257
|
+
domain: 'agent',
|
|
258
|
+
objectType,
|
|
259
|
+
namedArgs: {
|
|
260
|
+
filter: '_type co "AIAgent"',
|
|
261
|
+
fields: ['_id', '_type', 'name'],
|
|
262
|
+
},
|
|
263
|
+
pageSize: 10,
|
|
264
|
+
includeTotal: true,
|
|
265
|
+
});
|
|
266
|
+
report.attempts.push({
|
|
267
|
+
toolName: searchAttempt.toolName,
|
|
268
|
+
success: searchAttempt.success,
|
|
269
|
+
message: searchAttempt.message,
|
|
270
|
+
objectType,
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
if (searchAttempt.success) {
|
|
274
|
+
const searched = toArrayData(searchAttempt);
|
|
275
|
+
const searchedAiMatches = pickAiAgentRecords(searched);
|
|
276
|
+
aiRecords.push(...searchedAiMatches.map((record) => ({ objectType, record })));
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const dedupedById = new Map();
|
|
282
|
+
for (const item of aiRecords) {
|
|
283
|
+
const id = getRecordId(item.record);
|
|
284
|
+
if (!id) {
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
if (!dedupedById.has(id)) {
|
|
288
|
+
dedupedById.set(id, item);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
report.findings.aiRecordsFound = dedupedById.size;
|
|
293
|
+
|
|
294
|
+
if (dedupedById.size > 0) {
|
|
295
|
+
const first = [...dedupedById.values()][0];
|
|
296
|
+
const agentId = getRecordId(first.record);
|
|
297
|
+
const readAttempt = await callTool(client, 'frodo_read', {
|
|
298
|
+
domain: 'agent',
|
|
299
|
+
objectType: first.objectType,
|
|
300
|
+
positionalArgs: [agentId],
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
report.attempts.push({
|
|
304
|
+
toolName: readAttempt.toolName,
|
|
305
|
+
success: readAttempt.success,
|
|
306
|
+
message: readAttempt.message,
|
|
307
|
+
objectType: first.objectType,
|
|
308
|
+
agentId,
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
report.findings.introspectionSucceeded = readAttempt.success;
|
|
312
|
+
report.findings.introspectionSample = readAttempt.success
|
|
313
|
+
? readAttempt.payload?.data || null
|
|
314
|
+
: null;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (aiCapabilityObjectTypes.length === 0) {
|
|
318
|
+
report.status = 'not-exposed';
|
|
319
|
+
report.summary =
|
|
320
|
+
'No AIAgent-like capability signatures were discovered under MCP domain agent. Automatic exposure for the new agent type was not observed in discovery.';
|
|
321
|
+
} else if (report.findings.aiRecordsFound === 0) {
|
|
322
|
+
report.status = 'exposed-no-data';
|
|
323
|
+
report.summary =
|
|
324
|
+
'AIAgent-like capability signatures are exposed in discovery, but no AI agent records were found in current tenant data.';
|
|
325
|
+
} else if (report.findings.introspectionSucceeded) {
|
|
326
|
+
report.status = 'passed';
|
|
327
|
+
report.summary =
|
|
328
|
+
'AIAgent capabilities are exposed and at least one AI agent record was discovered and introspected successfully.';
|
|
329
|
+
} else {
|
|
330
|
+
report.status = 'partial';
|
|
331
|
+
report.summary =
|
|
332
|
+
'AI agent records were found, but introspection read call failed.';
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return report;
|
|
336
|
+
} finally {
|
|
337
|
+
await transport.close();
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
run()
|
|
342
|
+
.then((report) => {
|
|
343
|
+
const args = parseArgs(process.argv);
|
|
344
|
+
const outputPath = path.resolve(process.cwd(), args.output);
|
|
345
|
+
fs.writeFileSync(outputPath, JSON.stringify(report, null, 2), 'utf8');
|
|
346
|
+
console.log(JSON.stringify(report, null, 2));
|
|
347
|
+
console.log(`Wrote report: ${outputPath}`);
|
|
348
|
+
process.exit(report.status === 'error' ? 1 : 0);
|
|
349
|
+
})
|
|
350
|
+
.catch((error) => {
|
|
351
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
352
|
+
process.exit(1);
|
|
353
|
+
});
|