@ampless/backend 0.2.0-alpha.8 → 0.2.0-alpha.9
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/dist/functions/mcp-handler.d.ts +33 -0
- package/dist/functions/mcp-handler.js +70 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +28 -2
- package/package.json +5 -1
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP HTTP endpoint Lambda. Phase 3: Bearer token validation only.
|
|
3
|
+
*
|
|
4
|
+
* Reads the KvStore table directly (PK = 'mcp-tokens', SK = SHA-256
|
|
5
|
+
* hash of the plaintext token) instead of going through AppSync — the
|
|
6
|
+
* Lambda IAM role only needs `dynamodb:GetItem` on that one table,
|
|
7
|
+
* which is much narrower than `appsync:GraphQL` and avoids the
|
|
8
|
+
* IAM-auth-mode complexity of letting Lambdas call AppSync as
|
|
9
|
+
* privileged identity. Phase 4 will add AppSync IAM auth when tool
|
|
10
|
+
* dispatch actually needs schema-aware access.
|
|
11
|
+
*
|
|
12
|
+
* Function URL event format: Lambda Function URLs emit API Gateway
|
|
13
|
+
* HTTP v2 events (https://docs.aws.amazon.com/lambda/latest/dg/urls-invocation.html#urls-payloads).
|
|
14
|
+
* Headers arrive lowercased.
|
|
15
|
+
*/
|
|
16
|
+
interface FunctionUrlEvent {
|
|
17
|
+
headers?: Record<string, string | undefined>;
|
|
18
|
+
body?: string;
|
|
19
|
+
isBase64Encoded?: boolean;
|
|
20
|
+
requestContext?: {
|
|
21
|
+
http?: {
|
|
22
|
+
method?: string;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
interface FunctionUrlResult {
|
|
27
|
+
statusCode: number;
|
|
28
|
+
headers?: Record<string, string>;
|
|
29
|
+
body: string;
|
|
30
|
+
}
|
|
31
|
+
declare const handler: (event: FunctionUrlEvent) => Promise<FunctionUrlResult>;
|
|
32
|
+
|
|
33
|
+
export { handler };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// src/functions/mcp-handler.ts
|
|
2
|
+
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
|
|
3
|
+
import { DynamoDBDocumentClient, GetCommand } from "@aws-sdk/lib-dynamodb";
|
|
4
|
+
import { createHash } from "crypto";
|
|
5
|
+
var TOKENS_PK = "mcp-tokens";
|
|
6
|
+
function requireEnv(name) {
|
|
7
|
+
const v = process.env[name];
|
|
8
|
+
if (!v) throw new Error(`[mcp-handler] missing required env var ${name}`);
|
|
9
|
+
return v;
|
|
10
|
+
}
|
|
11
|
+
var KV_TABLE = requireEnv("AMPLESS_KV_TABLE");
|
|
12
|
+
var ddb = DynamoDBDocumentClient.from(new DynamoDBClient({}));
|
|
13
|
+
function hashToken(plain) {
|
|
14
|
+
return createHash("sha256").update(plain).digest("hex");
|
|
15
|
+
}
|
|
16
|
+
function jsonResponse(statusCode, body) {
|
|
17
|
+
return {
|
|
18
|
+
statusCode,
|
|
19
|
+
headers: { "content-type": "application/json" },
|
|
20
|
+
body: JSON.stringify(body)
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
async function validateBearer(plaintext) {
|
|
24
|
+
const hash = hashToken(plaintext);
|
|
25
|
+
const res = await ddb.send(
|
|
26
|
+
new GetCommand({
|
|
27
|
+
TableName: KV_TABLE,
|
|
28
|
+
Key: { pk: TOKENS_PK, sk: hash }
|
|
29
|
+
})
|
|
30
|
+
);
|
|
31
|
+
const row = res.Item;
|
|
32
|
+
if (!row?.value) return null;
|
|
33
|
+
let meta;
|
|
34
|
+
try {
|
|
35
|
+
meta = JSON.parse(row.value);
|
|
36
|
+
} catch {
|
|
37
|
+
console.error("[mcp-handler] could not parse token row", { hash });
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
if (meta.revokedAt) return null;
|
|
41
|
+
if (meta.expiresAt && new Date(meta.expiresAt).getTime() <= Date.now()) return null;
|
|
42
|
+
return meta;
|
|
43
|
+
}
|
|
44
|
+
var handler = async (event) => {
|
|
45
|
+
if (event.requestContext?.http?.method === "OPTIONS") {
|
|
46
|
+
return { statusCode: 204, body: "" };
|
|
47
|
+
}
|
|
48
|
+
const authHeader = event.headers?.authorization ?? event.headers?.Authorization;
|
|
49
|
+
if (!authHeader) {
|
|
50
|
+
return jsonResponse(401, { error: "missing_authorization" });
|
|
51
|
+
}
|
|
52
|
+
const match = /^Bearer\s+(amk_[A-Za-z0-9_-]+)$/.exec(authHeader);
|
|
53
|
+
if (!match) {
|
|
54
|
+
return jsonResponse(401, { error: "invalid_authorization" });
|
|
55
|
+
}
|
|
56
|
+
const plaintext = match[1];
|
|
57
|
+
const meta = await validateBearer(plaintext);
|
|
58
|
+
if (!meta) {
|
|
59
|
+
return jsonResponse(401, { error: "invalid_token" });
|
|
60
|
+
}
|
|
61
|
+
return jsonResponse(200, {
|
|
62
|
+
ok: true,
|
|
63
|
+
tokenPrefix: meta.prefix,
|
|
64
|
+
scope: meta.scope,
|
|
65
|
+
note: "Phase 3 stub. JSON-RPC tool dispatch lands in Phase 4."
|
|
66
|
+
});
|
|
67
|
+
};
|
|
68
|
+
export {
|
|
69
|
+
handler
|
|
70
|
+
};
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { defineBackend } from "@aws-amplify/backend";
|
|
|
3
3
|
import { Effect, PolicyStatement, AnyPrincipal } from "aws-cdk-lib/aws-iam";
|
|
4
4
|
import { Duration, Stack } from "aws-cdk-lib";
|
|
5
5
|
import { Queue } from "aws-cdk-lib/aws-sqs";
|
|
6
|
-
import { StartingPosition } from "aws-cdk-lib/aws-lambda";
|
|
6
|
+
import { FunctionUrlAuthType, HttpMethod, StartingPosition } from "aws-cdk-lib/aws-lambda";
|
|
7
7
|
import { DynamoEventSource, SqsEventSource } from "aws-cdk-lib/aws-lambda-event-sources";
|
|
8
8
|
import { Rule, Schedule } from "aws-cdk-lib/aws-events";
|
|
9
9
|
import { LambdaFunction } from "aws-cdk-lib/aws-events-targets";
|
|
@@ -17,7 +17,8 @@ function defineAmplessBackend(opts) {
|
|
|
17
17
|
processorTrusted: opts.processorTrusted,
|
|
18
18
|
processorUntrusted: opts.processorUntrusted,
|
|
19
19
|
apiKeyRenewer: opts.apiKeyRenewer,
|
|
20
|
-
userAdmin: opts.userAdmin
|
|
20
|
+
userAdmin: opts.userAdmin,
|
|
21
|
+
mcpHandler: opts.mcpHandler
|
|
21
22
|
});
|
|
22
23
|
const cfnBucket = backend.storage.resources.cfnResources.cfnBucket;
|
|
23
24
|
cfnBucket.publicAccessBlockConfiguration = {
|
|
@@ -166,6 +167,31 @@ function defineAmplessBackend(opts) {
|
|
|
166
167
|
schedule: Schedule.cron({ minute: "0", hour: "3", day: "1", month: "*", year: "*" }),
|
|
167
168
|
targets: [new LambdaFunction(apiKeyRenewerFn)]
|
|
168
169
|
});
|
|
170
|
+
const mcpHandlerFn = backend.mcpHandler.resources.lambda;
|
|
171
|
+
mcpHandlerFn.addToRolePolicy(
|
|
172
|
+
new PolicyStatement({
|
|
173
|
+
effect: Effect.ALLOW,
|
|
174
|
+
actions: ["dynamodb:GetItem"],
|
|
175
|
+
resources: [kvTable.tableArn]
|
|
176
|
+
})
|
|
177
|
+
);
|
|
178
|
+
mcpHandlerFn.addEnvironment("AMPLESS_KV_TABLE", kvTable.tableName);
|
|
179
|
+
const mcpFunctionUrl = mcpHandlerFn.addFunctionUrl({
|
|
180
|
+
authType: FunctionUrlAuthType.NONE,
|
|
181
|
+
cors: {
|
|
182
|
+
allowedOrigins: ["*"],
|
|
183
|
+
allowedMethods: [HttpMethod.POST, HttpMethod.OPTIONS],
|
|
184
|
+
allowedHeaders: ["*"],
|
|
185
|
+
maxAge: Duration.hours(1)
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
backend.addOutput({
|
|
189
|
+
custom: {
|
|
190
|
+
mcp: {
|
|
191
|
+
endpoint: mcpFunctionUrl.url
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
});
|
|
169
195
|
return backend;
|
|
170
196
|
}
|
|
171
197
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ampless/backend",
|
|
3
|
-
"version": "0.2.0-alpha.
|
|
3
|
+
"version": "0.2.0-alpha.9",
|
|
4
4
|
"description": "Amplify Gen 2 backend factories for ampless: auth, data, storage, event processors, API key renewer",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -32,6 +32,10 @@
|
|
|
32
32
|
"./functions/api-key-renewer": {
|
|
33
33
|
"import": "./dist/functions/api-key-renewer.js",
|
|
34
34
|
"types": "./dist/functions/api-key-renewer.d.ts"
|
|
35
|
+
},
|
|
36
|
+
"./functions/mcp-handler": {
|
|
37
|
+
"import": "./dist/functions/mcp-handler.js",
|
|
38
|
+
"types": "./dist/functions/mcp-handler.d.ts"
|
|
35
39
|
}
|
|
36
40
|
},
|
|
37
41
|
"files": [
|