Skip to content

Commit 1210270

Browse files
authored
Add MCP non-media Agent API parity (#49)
## Summary - add grant-bound MCP service endpoints for owned record listing/detail, text-only revision, pending withdrawal, and deletion - add announcement unread/list/read delegation without exposing target agent UUIDs - add follow/unfollow, own follow listing, and following-feed delegation without exposing follow-edge agent UUIDs - reuse shared publication and follow logic so normal Agent API and MCP delegation keep identical lifecycle, idempotency, quota, and moderation behavior - keep MCP post media disabled and preserve the evergreen OAuth model with no new scope gate - normalize telemetry for the new service routes - extend Slice 1 regression coverage across the v0.5.1 parity surface ## Validation - `npm run check`: 0 errors / 0 warnings / 2 pre-existing hints - production Worker dry-run: passed - v0.5.1 MCP parity regression path passed; the only remaining local Slice 1 failures are the two known unrelated sign-in-trace `ECONNRESET` worker flakes
1 parent 578dd9c commit 1210270

3 files changed

Lines changed: 725 additions & 56 deletions

File tree

‎scripts/orbit-slice1-tests.ts‎

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1163,6 +1163,222 @@ let firstCredentialToken = '';
11631163
cookies: ownerCookies,
11641164
});
11651165

1166+
const ownRecordsInitially = await postJson(
1167+
`/v1/mcp/grants/${encodeURIComponent(recipientGrant.grantId)}/agent/records`,
1168+
{ limit: 10 },
1169+
mcpServiceHeaders(),
1170+
NOW + 17_111,
1171+
);
1172+
assert.equal(ownRecordsInitially.status, 200, await ownRecordsInitially.clone().text());
1173+
assert.deepEqual((await ownRecordsInitially.json() as { records: unknown[] }).records, []);
1174+
1175+
const directPost = await postJson(
1176+
`/v1/mcp/grants/${encodeURIComponent(recipientGrant.grantId)}/records`,
1177+
{
1178+
bodyMarkdown: 'MCP v0.5.1 doğrudan kayıt yaşam döngüsü testi.',
1179+
projectSlug: null,
1180+
topicSlugs: [],
1181+
mediaId: null,
1182+
},
1183+
mcpServiceHeaders('mcp-v051-direct-post'),
1184+
NOW + 17_112,
1185+
);
1186+
assert.equal(directPost.status, 201, await directPost.clone().text());
1187+
const directPostBody = await directPost.json() as { record: { id: string; slug: string } };
1188+
1189+
const listedOwnRecords = await postJson(
1190+
`/v1/mcp/grants/${encodeURIComponent(recipientGrant.grantId)}/agent/records`,
1191+
{ state: 'published', kind: 'post', limit: 10 },
1192+
mcpServiceHeaders(),
1193+
NOW + 17_113,
1194+
);
1195+
assert.equal(listedOwnRecords.status, 200, await listedOwnRecords.clone().text());
1196+
const listedOwnRecordsBody = await listedOwnRecords.json() as {
1197+
records: Array<{ id: string; lifecycleState: string }>;
1198+
nextCursor: string | null;
1199+
};
1200+
assert.equal(listedOwnRecordsBody.records[0]?.id, directPostBody.record.id);
1201+
assert.equal(listedOwnRecordsBody.records[0]?.lifecycleState, 'published');
1202+
assert.equal(listedOwnRecordsBody.nextCursor, null);
1203+
1204+
const ownRecordDetail = await postJson(
1205+
`/v1/mcp/grants/${encodeURIComponent(recipientGrant.grantId)}/agent/records/${encodeURIComponent(directPostBody.record.id)}`,
1206+
{},
1207+
mcpServiceHeaders(),
1208+
NOW + 17_114,
1209+
);
1210+
assert.equal(ownRecordDetail.status, 200, await ownRecordDetail.clone().text());
1211+
const ownRecordDetailBody = await ownRecordDetail.json() as {
1212+
record: { id: string; currentRevision: { bodyMarkdown: string } | null };
1213+
};
1214+
assert.equal(ownRecordDetailBody.record.id, directPostBody.record.id);
1215+
assert.equal(ownRecordDetailBody.record.currentRevision?.bodyMarkdown, 'MCP v0.5.1 doğrudan kayıt yaşam döngüsü testi.');
1216+
1217+
const revisedDirectPost = await postJson(
1218+
`/v1/mcp/grants/${encodeURIComponent(recipientGrant.grantId)}/records/${encodeURIComponent(directPostBody.record.id)}/revise`,
1219+
{ bodyMarkdown: 'MCP v0.5.1 düzenlenmiş metin.', mediaId: null },
1220+
mcpServiceHeaders('mcp-v051-revise'),
1221+
NOW + 17_115,
1222+
);
1223+
assert.equal(revisedDirectPost.status, 200, await revisedDirectPost.clone().text());
1224+
assert.notEqual(revisedDirectPost.headers.get('idempotency-replayed'), 'true');
1225+
const replayedRevision = await postJson(
1226+
`/v1/mcp/grants/${encodeURIComponent(recipientGrant.grantId)}/records/${encodeURIComponent(directPostBody.record.id)}/revise`,
1227+
{ bodyMarkdown: 'MCP v0.5.1 düzenlenmiş metin.', mediaId: null },
1228+
mcpServiceHeaders('mcp-v051-revise'),
1229+
NOW + 17_116,
1230+
);
1231+
assert.equal(replayedRevision.status, 200, await replayedRevision.clone().text());
1232+
assert.equal(replayedRevision.headers.get('idempotency-replayed'), 'true');
1233+
1234+
const mediaRevisionDenied = await postJson(
1235+
`/v1/mcp/grants/${encodeURIComponent(recipientGrant.grantId)}/records/${encodeURIComponent(directPostBody.record.id)}/revise`,
1236+
{ bodyMarkdown: 'Medya henüz MCP üzerinden düzenlemeye eklenemez.', mediaId: '019f64d2-0109-7644-9a4e-a0d25df888e2' },
1237+
mcpServiceHeaders('mcp-v051-revise-media'),
1238+
NOW + 17_117,
1239+
);
1240+
assert.equal(mediaRevisionDenied.status, 403);
1241+
assert.equal((await mediaRevisionDenied.json() as { error: { code: string } }).error.code, 'mcp_media_scope_denied');
1242+
1243+
const withdrawn = await postJson(
1244+
`/v1/mcp/grants/${encodeURIComponent(full.grantId)}/records/${encodeURIComponent(createdReplyBody.record.id)}/withdraw`,
1245+
{},
1246+
mcpServiceHeaders('mcp-v051-withdraw'),
1247+
NOW + 17_118,
1248+
);
1249+
assert.equal(withdrawn.status, 200, await withdrawn.clone().text());
1250+
assert.equal((await withdrawn.json() as { record: { status: string } }).record.status, 'withdrawn');
1251+
1252+
const follow = await postJson(
1253+
`/v1/mcp/grants/${encodeURIComponent(full.grantId)}/follows/mcp-inbox-recipient/follow`,
1254+
{},
1255+
mcpServiceHeaders(),
1256+
NOW + 17_120,
1257+
);
1258+
assert.equal(follow.status, 200, await follow.clone().text());
1259+
assert.deepEqual(await follow.json(), { follow: { handle: 'mcp-inbox-recipient', following: true } });
1260+
1261+
const ownFollows = await postJson(
1262+
`/v1/mcp/grants/${encodeURIComponent(full.grantId)}/follows/list`,
1263+
{ box: 'following', limit: 10 },
1264+
mcpServiceHeaders(),
1265+
NOW + 17_121,
1266+
);
1267+
assert.equal(ownFollows.status, 200, await ownFollows.clone().text());
1268+
const ownFollowsText = await ownFollows.text();
1269+
assert.ok(!ownFollowsText.includes(recipientAgentId));
1270+
const ownFollowsBody = JSON.parse(ownFollowsText) as {
1271+
follows: Array<{ agent: { handle: string; id?: string } }>;
1272+
};
1273+
assert.equal(ownFollowsBody.follows[0]?.agent.handle, 'mcp-inbox-recipient');
1274+
assert.equal(ownFollowsBody.follows[0]?.agent.id, undefined);
1275+
1276+
const followingFeed = await postJson(
1277+
`/v1/mcp/grants/${encodeURIComponent(full.grantId)}/feed/following`,
1278+
{ limit: 10 },
1279+
mcpServiceHeaders(),
1280+
NOW + 17_122,
1281+
);
1282+
assert.equal(followingFeed.status, 200, await followingFeed.clone().text());
1283+
const followingFeedBody = await followingFeed.json() as { records: Array<{ id: string }> };
1284+
assert.ok(followingFeedBody.records.some((record) => record.id === directPostBody.record.id));
1285+
1286+
const unfollow = await postJson(
1287+
`/v1/mcp/grants/${encodeURIComponent(full.grantId)}/follows/mcp-inbox-recipient/unfollow`,
1288+
{},
1289+
mcpServiceHeaders(),
1290+
NOW + 17_123,
1291+
);
1292+
assert.equal(unfollow.status, 200, await unfollow.clone().text());
1293+
assert.deepEqual(await unfollow.json(), { follow: { handle: 'mcp-inbox-recipient', following: false } });
1294+
1295+
const announcementCreated = await postJson(
1296+
'/v1/admin/announcements',
1297+
{
1298+
title: 'MCP v0.5.1 duyuru testi',
1299+
bodyMarkdown: 'Bu duyuru yalnız parity regression testi içindir.',
1300+
severity: 'info',
1301+
audienceType: 'agent',
1302+
targetAgentId: recipientAgentId,
1303+
startsAt: NOW,
1304+
expiresAt: null,
1305+
},
1306+
authenticatedHeaders(ownerCookies, true),
1307+
NOW + 17_124,
1308+
);
1309+
assert.equal(announcementCreated.status, 201, await announcementCreated.clone().text());
1310+
const announcementId = (await announcementCreated.json() as { announcement: { id: string } }).announcement.id;
1311+
const announcementPublished = await postJson(
1312+
`/v1/admin/announcements/${encodeURIComponent(announcementId)}/publish`,
1313+
{},
1314+
authenticatedHeaders(ownerCookies, true),
1315+
NOW + 17_125,
1316+
);
1317+
assert.equal(announcementPublished.status, 200, await announcementPublished.clone().text());
1318+
1319+
const announcementUnread = await postJson(
1320+
`/v1/mcp/grants/${encodeURIComponent(recipientGrant.grantId)}/announcements/unread-count`,
1321+
{},
1322+
mcpServiceHeaders(),
1323+
NOW + 17_126,
1324+
);
1325+
assert.equal(announcementUnread.status, 200, await announcementUnread.clone().text());
1326+
assert.equal((await announcementUnread.json() as { unreadCount: number }).unreadCount, 1);
1327+
1328+
const announcements = await postJson(
1329+
`/v1/mcp/grants/${encodeURIComponent(recipientGrant.grantId)}/announcements/list`,
1330+
{ limit: 10 },
1331+
mcpServiceHeaders(),
1332+
NOW + 17_127,
1333+
);
1334+
assert.equal(announcements.status, 200, await announcements.clone().text());
1335+
const announcementsText = await announcements.text();
1336+
assert.ok(!announcementsText.includes(recipientAgentId));
1337+
const announcementsBody = JSON.parse(announcementsText) as {
1338+
announcements: Array<{ id: string; targetedToConnectedAgent: boolean; targetAgentId?: string }>;
1339+
};
1340+
assert.equal(announcementsBody.announcements[0]?.id, announcementId);
1341+
assert.equal(announcementsBody.announcements[0]?.targetedToConnectedAgent, true);
1342+
assert.equal(announcementsBody.announcements[0]?.targetAgentId, undefined);
1343+
1344+
const announcementRead = await postJson(
1345+
`/v1/mcp/grants/${encodeURIComponent(recipientGrant.grantId)}/announcements/${encodeURIComponent(announcementId)}/read`,
1346+
{},
1347+
mcpServiceHeaders(),
1348+
NOW + 17_128,
1349+
);
1350+
assert.equal(announcementRead.status, 200, await announcementRead.clone().text());
1351+
const announcementUnreadAfterRead = await postJson(
1352+
`/v1/mcp/grants/${encodeURIComponent(recipientGrant.grantId)}/announcements/unread-count`,
1353+
{},
1354+
mcpServiceHeaders(),
1355+
NOW + 17_129,
1356+
);
1357+
assert.deepEqual(await announcementUnreadAfterRead.json(), {
1358+
unreadCount: 0,
1359+
criticalCount: 0,
1360+
warningCount: 0,
1361+
infoCount: 0,
1362+
highestSeverity: null,
1363+
});
1364+
1365+
const deletedDirectPost = await postJson(
1366+
`/v1/mcp/grants/${encodeURIComponent(recipientGrant.grantId)}/records/${encodeURIComponent(directPostBody.record.id)}/delete`,
1367+
{ reason: 'v0.5.1 regression cleanup' },
1368+
mcpServiceHeaders('mcp-v051-delete'),
1369+
NOW + 17_130,
1370+
);
1371+
assert.equal(deletedDirectPost.status, 200, await deletedDirectPost.clone().text());
1372+
assert.equal((await deletedDirectPost.json() as { record: { status: string } }).record.status, 'deleted');
1373+
const deletedRecordDetail = await postJson(
1374+
`/v1/mcp/grants/${encodeURIComponent(recipientGrant.grantId)}/agent/records/${encodeURIComponent(directPostBody.record.id)}`,
1375+
{},
1376+
mcpServiceHeaders(),
1377+
NOW + 17_131,
1378+
);
1379+
assert.equal(deletedRecordDetail.status, 200, await deletedRecordDetail.clone().text());
1380+
assert.equal((await deletedRecordDetail.json() as { record: { lifecycleState: string } }).record.lifecycleState, 'deleted');
1381+
11661382
const initialUnread = await postJson(
11671383
`/v1/mcp/grants/${encodeURIComponent(recipientGrant.grantId)}/direct-messages/unread-count`,
11681384
{},

0 commit comments

Comments
 (0)