We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/andychoi/mcp-strapi'
If you have feedback or need assistance with the MCP directory API, please join our Discord server
#!/usr/bin/env node
/**
* Test 11: Secure Search & RAG Endpoints
*
* Tests the secure-search plugin REST API:
* - POST /api/secure-search/search (semantic/metadata search)
* - GET /api/secure-search/search/facets (faceted counts)
* - POST /api/secure-search/search/rag (RAG answer generation)
* - Auth enforcement (401 without token)
* - Input validation (400 on missing query)
* - ABAC filtering on search results
*/
import {
requireStrapi,
section,
pass,
fail,
skip,
assert,
assertEqual,
assertStatus,
summary,
adminLogin,
getSuperAdminToken,
STRAPI_URL,
fetchJSON,
} from './helpers.js';
const SEARCH_URL = `${STRAPI_URL}/api/secure-search/search`;
const DOCS_URL = `${STRAPI_URL}/api/secure-documents/documents`;
// Demo users
const USERS = {
admin: { email: 'admin@example.com', password: 'Admin1234!' },
editor: { email: 'editor@example.com', password: 'Admin1234!' },
author: { email: 'author@example.com', password: 'Admin1234!' },
};
/**
* Upload a text document for search testing.
*/
async function uploadDoc(token, title, content, policy) {
const boundary = '----SearchTest' + Date.now() + Math.random().toString(36).slice(2);
const body = [
`--${boundary}`,
`Content-Disposition: form-data; name="file"; filename="${title.replace(/\s/g, '-').toLowerCase()}.txt"`,
'Content-Type: text/plain',
'',
content,
`--${boundary}`,
'Content-Disposition: form-data; name="policy"',
'',
JSON.stringify(policy),
`--${boundary}`,
'Content-Disposition: form-data; name="metadata"',
'',
JSON.stringify({ title, description: content.substring(0, 100) }),
`--${boundary}--`,
].join('\r\n');
const resp = await fetch(DOCS_URL, {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': `multipart/form-data; boundary=${boundary}`,
},
body,
});
const data = await resp.json().catch(() => null);
return { status: resp.status, documentId: data?.data?.documentId };
}
async function main() {
console.log('\x1b[1m=== Test 11: Secure Search & RAG ===\x1b[0m');
await requireStrapi();
// ── Setup ──────────────────────────────────────────────────────────
section('Setup');
const tokens = {};
for (const [role, creds] of Object.entries(USERS)) {
const result = await adminLogin(creds.email, creds.password);
if (result?.token) {
tokens[role] = result.token;
pass(`Login ${role}`);
} else {
fail(`Login ${role}`, 'Login failed');
}
}
if (!tokens.admin) {
fail('Admin login required', 'Cannot proceed');
summary();
process.exit(1);
}
const authHeaders = { Authorization: `Bearer ${tokens.admin}` };
// Upload test documents for search
const createdDocs = [];
const doc1 = await uploadDoc(
tokens.admin,
'Kubernetes Troubleshooting Guide',
'When a pod enters CrashLoopBackOff state, check the pod logs using kubectl logs. Common causes include missing environment variables, failed health checks, and OOM kills. Use kubectl describe pod to see events.',
{ classification: 'internal', min_clearance: 0 }
);
if (doc1.status === 201 && doc1.documentId) {
createdDocs.push(doc1.documentId);
pass('Upload search test doc 1 (kubernetes)');
} else {
skip('Upload search test doc 1', `HTTP ${doc1.status}`);
}
const doc2 = await uploadDoc(
tokens.admin,
'Database Failover Procedure',
'In case of primary database failure, the automated failover system will promote the standby replica. Verify replication lag before failover. Post-failover, update connection strings and notify the on-call team.',
{ classification: 'confidential', min_clearance: 2 }
);
if (doc2.status === 201 && doc2.documentId) {
createdDocs.push(doc2.documentId);
pass('Upload search test doc 2 (database, confidential)');
} else {
skip('Upload search test doc 2', `HTTP ${doc2.status}`);
}
// ── Auth enforcement ───────────────────────────────────────────────
section('Auth Enforcement');
{
const { status } = await fetchJSON(SEARCH_URL, {
method: 'POST',
body: JSON.stringify({ query: 'test' }),
});
assertStatus(status, 401, 'Search without auth returns 401');
}
{
const { status } = await fetchJSON(`${SEARCH_URL}/facets`);
assertStatus(status, 401, 'Facets without auth returns 401');
}
{
const { status } = await fetchJSON(`${SEARCH_URL}/rag`, {
method: 'POST',
body: JSON.stringify({ query: 'test' }),
});
assertStatus(status, 401, 'RAG without auth returns 401');
}
// ── Input validation ───────────────────────────────────────────────
section('Input Validation');
{
const { status } = await fetchJSON(SEARCH_URL, {
method: 'POST',
headers: authHeaders,
body: JSON.stringify({}),
});
assertStatus(status, 400, 'Search without query returns 400');
}
{
const { status } = await fetchJSON(SEARCH_URL, {
method: 'POST',
headers: authHeaders,
body: JSON.stringify({ query: 123 }),
});
assertStatus(status, 400, 'Search with non-string query returns 400');
}
{
const { status } = await fetchJSON(`${SEARCH_URL}/rag`, {
method: 'POST',
headers: authHeaders,
body: JSON.stringify({}),
});
// Should be 400 (missing query) or 400 (RAG not enabled)
assert(status === 400, 'RAG without query returns 400', `Got HTTP ${status}`);
}
// ── Search endpoint ────────────────────────────────────────────────
section('Search');
{
const { status, data } = await fetchJSON(SEARCH_URL, {
method: 'POST',
headers: authHeaders,
body: JSON.stringify({ query: 'kubernetes pod crash' }),
});
if (assertStatus(status, 200, 'Search returns 200')) {
assert(Array.isArray(data?.results), 'Search returns results array', `Got: ${typeof data?.results}`);
assert(typeof data?.total === 'number', 'Search returns total count');
assert(data?.query === 'kubernetes pod crash', 'Search echoes query');
// In metadata mode, should find doc by title/description match
if (data?.results?.length > 0) {
const result = data.results[0];
assert(result.documentId, 'Result has documentId');
assert(result.title, 'Result has title');
assert(typeof result.score === 'number', 'Result has score');
pass('Search result has expected shape');
} else {
// Vector mode may return 0 results if pgvector not available or embeddings not ready
skip('Search result shape', 'No results returned (vector search may not be available)');
}
}
}
// Search with limit
{
const { status, data } = await fetchJSON(SEARCH_URL, {
method: 'POST',
headers: authHeaders,
body: JSON.stringify({ query: 'database', limit: 1 }),
});
if (assertStatus(status, 200, 'Search with limit returns 200')) {
assert(data.results.length <= 1, 'Search respects limit', `Got ${data.results.length} results for limit=1`);
}
}
// Search with classification filter
{
const { status, data } = await fetchJSON(SEARCH_URL, {
method: 'POST',
headers: authHeaders,
body: JSON.stringify({
query: 'database',
filters: { classification: 'confidential' },
}),
});
if (assertStatus(status, 200, 'Search with classification filter returns 200')) {
// All results (if any) should be confidential
for (const r of (data.results || [])) {
assertEqual(r.classification, 'confidential', `Filtered result "${r.title}" is confidential`);
}
pass('Classification filter applied correctly');
}
}
// ── Search ABAC filtering ─────────────────────────────────────────
section('Search ABAC Filtering');
if (tokens.author) {
// Author (clearance 1) should NOT see confidential docs (clearance 2) in search
const { status, data } = await fetchJSON(SEARCH_URL, {
method: 'POST',
headers: { Authorization: `Bearer ${tokens.author}` },
body: JSON.stringify({ query: 'database failover' }),
});
if (assertStatus(status, 200, 'Author: search returns 200')) {
const titles = (data.results || []).map((r) => r.title);
assert(
!titles.includes('Database Failover Procedure'),
'Author: confidential doc filtered from search',
`Confidential doc found in author search results`
);
}
}
// ── Facets endpoint ────────────────────────────────────────────────
section('Facets');
{
const { status, data } = await fetchJSON(`${SEARCH_URL}/facets`, {
headers: authHeaders,
});
if (assertStatus(status, 200, 'Facets returns 200')) {
assert(typeof data?.classifications === 'object', 'Facets returns classifications', `Got: ${typeof data?.classifications}`);
assert(typeof data?.total === 'number', 'Facets returns total');
pass('Facets response has expected shape');
}
}
// Facets for author (should see fewer due to ABAC)
if (tokens.author) {
const { status: adminStatus, data: adminData } = await fetchJSON(`${SEARCH_URL}/facets`, {
headers: authHeaders,
});
const { status: authorStatus, data: authorData } = await fetchJSON(`${SEARCH_URL}/facets`, {
headers: { Authorization: `Bearer ${tokens.author}` },
});
if (adminStatus === 200 && authorStatus === 200) {
assert(
authorData.total <= adminData.total,
`Author facet total (${authorData.total}) <= admin total (${adminData.total})`,
`Author sees more than admin: ${authorData.total} vs ${adminData.total}`
);
}
}
// ── RAG endpoint ───────────────────────────────────────────────────
section('RAG');
{
const { status, data } = await fetchJSON(`${SEARCH_URL}/rag`, {
method: 'POST',
headers: authHeaders,
body: JSON.stringify({ query: 'How to troubleshoot CrashLoopBackOff?' }),
});
if (status === 200 && data?.answer) {
pass('RAG returns answer');
assert(typeof data.answer === 'string', 'RAG answer is a string');
assert(Array.isArray(data.sources), 'RAG returns sources array');
assert(data.query === 'How to troubleshoot CrashLoopBackOff?', 'RAG echoes query');
} else if (status === 400) {
// RAG might be disabled or require pgvector
skip('RAG answer', `RAG not available: ${data?.error || 'unknown'}`);
} else if (status === 500) {
// LLM provider (Ollama/Bedrock) might not be running
skip('RAG answer', 'LLM provider may not be running');
} else if (status === 501) {
skip('RAG answer', 'RAG not implemented yet');
} else {
fail('RAG endpoint', `HTTP ${status}: ${JSON.stringify(data)}`);
}
}
// ── Cleanup ────────────────────────────────────────────────────────
section('Cleanup');
for (const docId of createdDocs) {
try {
await fetch(`${DOCS_URL}/${docId}`, {
method: 'DELETE',
headers: authHeaders,
});
} catch {
// Ignore cleanup errors
}
}
pass(`Cleaned up ${createdDocs.length} test documents`);
// ── Summary ────────────────────────────────────────────────────────
section('Summary');
const results = summary();
process.exit(results.failed > 0 ? 1 : 0);
}
main().catch((err) => {
console.error('Fatal error:', err);
process.exit(1);
});