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: 2 additions & 0 deletions apps/mcpserver/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
},
"dependencies": {
"@aws-sdk/client-bedrock-agent-runtime": "^3.583.0",
"@aws-sdk/client-s3": "^3.583.0",
"@aws-sdk/s3-request-presigner": "^3.583.0",
"@logtape/logtape": "catalog:",
"@modelcontextprotocol/sdk": "catalog:",
"@octokit/rest": "^22.0.0",
Expand Down
3 changes: 2 additions & 1 deletion apps/mcpserver/src/handlers/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,15 @@ export async function handleSearchEndpoint(
config.aws.rerankingEnabled,
);

// Format results with content, score, and source URL
// Format results with content, score, source URL, and source file URL
const formattedResults = results.map((result) => ({
content: result.content,
score: result.score,
source:
result.location?.type === "WEB"
? result.location.webLocation?.url
: undefined,
source_file_url: result.sourceFileUrl,
}));

sendJsonResponse(res, 200, {
Expand Down
65 changes: 64 additions & 1 deletion apps/mcpserver/src/services/bedrock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import {
RetrievalResultLocation,
RetrieveCommand,
} from "@aws-sdk/client-bedrock-agent-runtime";
import { GetObjectCommand, S3Client } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { getLogger } from "@logtape/logtape";

import { rerankingSupportedRegions } from "../config/aws.js";
Expand All @@ -12,6 +14,7 @@ export type QueryKnowledgeBasesOutput = {
content: string;
location?: RetrievalResultLocation;
score: number;
sourceFileUrl?: string;
};

type RerankingModelName = "AMAZON" | "COHERE";
Expand Down Expand Up @@ -77,15 +80,29 @@ export async function queryKnowledgeBase(
const results = response.retrievalResults || [];
const documents: QueryKnowledgeBasesOutput[] = [];

// Create S3 client for pre-signed URLs (reuse region from Bedrock client)
const s3Client = new S3Client({ region: clientRegion });

for (const result of results) {
if (result.content?.type === "IMAGE") {
logger.warn("Images are not supported at this time. Skipping...");
continue;
} else if (result.content?.text) {
// Generate pre-signed URL if S3 location is available
const preSignedUrl =
result.location?.type === "S3" && result.location.s3Location?.uri
? await generatePreSignedUrl(
result.location.s3Location.uri,
s3Client,
logger,
)
: undefined;

documents.push({
content: result.content.text,
location: resolveToWebsiteUrl(result.location),
score: result.score || -1.0,
sourceFileUrl: preSignedUrl,
});
}
}
Expand Down Expand Up @@ -145,15 +162,29 @@ export async function queryKnowledgeBaseStructured(
const results = response.retrievalResults || [];
const documents: QueryKnowledgeBasesOutput[] = [];

// Create S3 client for pre-signed URLs (reuse region from Bedrock client)
const s3Client = new S3Client({ region: clientRegion });

for (const result of results) {
if (result.content?.type === "IMAGE") {
logger.warn("Images are not supported at this time. Skipping...");
continue;
} else if (result.content?.text) {
// Generate pre-signed URL if S3 location is available
const preSignedUrl =
result.location?.type === "S3" && result.location.s3Location?.uri
? await generatePreSignedUrl(
result.location.s3Location.uri,
s3Client,
logger,
)
: undefined;

documents.push({
content: result.content.text,
location: resolveToWebsiteUrl(result.location),
score: result.score || -1.0,
sourceFileUrl: preSignedUrl,
});
}
}
Expand Down Expand Up @@ -224,6 +255,33 @@ export function resolveToWebsiteUrl(
return undefined;
}

/**
* Generates a pre-signed URL for an S3 object.
* @param s3Uri The S3 URI in format s3://bucket/key
* @param s3Client The S3 client to use
* @param logger Logger instance
* @returns Pre-signed URL or undefined if generation fails
*/
async function generatePreSignedUrl(
s3Uri: string,
s3Client: S3Client,
logger: ReturnType<typeof getLogger>,
): Promise<string | undefined> {
const match = s3Uri.match(/^s3:\/\/([^/]+)\/(.+)$/);
if (!match) {
return undefined;
}

const [, bucket, key] = match;
try {
const command = new GetObjectCommand({ Bucket: bucket, Key: key });
return await getSignedUrl(s3Client, command, { expiresIn: 3600 });
} catch (error) {
logger.warn(`Failed to generate pre-signed URL for ${s3Uri}: ${error}`);
return undefined;
}
}

/**
* Serializes an array of knowledge base query results into a formatted string.
* This method formats the results for better readability, including content, location, and score.
Expand Down Expand Up @@ -262,9 +320,14 @@ function serializeResults(results: QueryKnowledgeBasesOutput[]): string {
} else if (result.location) {
locationStr = JSON.stringify(result.location);
}

const sourceFileStr = result.sourceFileUrl
? `\nSource File: ${result.sourceFileUrl}`
: "";

return `Result ${index + 1} (Score: ${result.score.toFixed(4)}):\n${
result.content
}\nLocation: ${locationStr}\n`;
}\nLocation: ${locationStr}${sourceFileStr}\n`;
})
.join("\n\n");
}
6 changes: 3 additions & 3 deletions infra/resources/_modules/mcp_registry/tfmodules.lock.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"portal": {
"hash": "20d852cbfe3fbb963ec8cda6e518659f6beac4ce8d9efff9e8d67f6a7bae359d",
"version": "0.3.3",
"hash": "76adb1e139938bd35ad042c7b60c215d2f7c9fe74d4672d8d0d126c4a2233301",
"version": "0.4.0",
"name": "azure_cdn",
"source": "https://registry.terraform.io/modules/pagopa-dx/azure-cdn/azurerm/0.3.3"
"source": "https://registry.terraform.io/modules/pagopa-dx/azure-cdn/azurerm/0.4.0"
}
}
2 changes: 2 additions & 0 deletions infra/resources/_modules/mcp_server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ No modules.
| [aws_iam_policy.kb_data_sources_access](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource |
| [aws_iam_policy.kb_vector_store_access](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource |
| [aws_iam_policy.lambda_bedrock_access](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource |
| [aws_iam_policy.lambda_s3_access](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource |
| [aws_iam_role.api_gateway_cloudwatch](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource |
| [aws_iam_role.kb](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource |
| [aws_iam_role.server](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource |
Expand All @@ -50,6 +51,7 @@ No modules.
| [aws_iam_role_policy_attachment.kb_vector_store_access](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource |
| [aws_iam_role_policy_attachment.lambda_basic_execution](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource |
| [aws_iam_role_policy_attachment.lambda_bedrock_access](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource |
| [aws_iam_role_policy_attachment.lambda_s3_access](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource |
| [aws_lambda_function.server](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_function) | resource |
| [aws_lambda_permission.apigw](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_permission) | resource |
| [aws_s3_bucket.mcp_knowledge_base](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket) | resource |
Expand Down
6 changes: 3 additions & 3 deletions infra/resources/_modules/mcp_server/knowledge_base.tf
Original file line number Diff line number Diff line change
Expand Up @@ -59,19 +59,19 @@ resource "awscc_bedrock_data_source" "docs" {
# overlap_tokens = 60: Maintains context continuity between chunks.
# Small overlaps ensure smooth transitions between chunks while avoiding excessive redundancy.
# This helps preserve semantic coherence when chunks are retrieved independently.
overlap_tokens = 60
overlap_tokens = 150
level_configurations = [
{
# First level (coarse): max_tokens = 1500
# Creates larger chunks suitable for capturing high-level topics and broad concepts.
# Balances informativeness with manageability for semantic search.
max_tokens = 1500
max_tokens = 4000
},
{
# Second level (fine): max_tokens = 300
# Creates smaller, more granular chunks for detailed retrieval.
# Helps answer specific technical questions with precise, focused context.
max_tokens = 300
max_tokens = 500
}
]
}
Expand Down
37 changes: 37 additions & 0 deletions infra/resources/_modules/mcp_server/lambda.tf
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,40 @@ resource "aws_iam_role_policy_attachment" "lambda_bedrock_access" {
role = aws_iam_role.server.name
policy_arn = aws_iam_policy.lambda_bedrock_access.arn
}

# Defines an IAM policy to allow the Lambda function to access S3 for pre-signed URLs.
resource "aws_iam_policy" "lambda_s3_access" {
name = provider::awsdx::resource_name(merge(var.naming_config, {
name = "lambda-s3-access"
resource_type = "iam_policy"
}))

description = "IAM policy for Lambda to generate pre-signed URLs for S3 knowledge base objects"

policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"s3:GetObject"
]
Resource = "${aws_s3_bucket.mcp_knowledge_base.arn}/*"
},
{
Effect = "Allow"
Action = [
"s3:ListBucket"
]
Resource = aws_s3_bucket.mcp_knowledge_base.arn
}
]
})
tags = var.tags
}

# Attaches the S3 access policy to the Lambda's IAM role.
resource "aws_iam_role_policy_attachment" "lambda_s3_access" {
role = aws_iam_role.server.name
policy_arn = aws_iam_policy.lambda_s3_access.arn
}
Loading
Loading