bsv-mcp 0.2.9 → 0.2.11
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 +19 -0
- package/README.md +26 -2
- package/dist/index.js +6777 -126216
- package/index.ts +287 -265
- package/package.json +4 -3
- package/tools/bap/generate.test.ts +17 -16
- package/tools/index.ts +8 -3
- package/tools/ordinals/getInscription.ts +15 -65
- package/tools/ordinals/getTokenByIdOrTicker.ts +14 -80
- package/tools/ordinals/index.ts +8 -7
- package/tools/ordinals/marketListings.ts +21 -188
- package/tools/ordinals/marketSales.ts +21 -127
- package/tools/ordinals/searchInscriptions.ts +21 -108
- package/tools/wallet/brc100.ts +572 -0
- package/tools/wallet/cancelListing.ts +80 -0
- package/tools/wallet/createOrdinals.ts +62 -143
- package/tools/wallet/getAddress.ts +19 -13
- package/tools/wallet/getBalance.ts +29 -25
- package/tools/wallet/getBsv21Balances.ts +28 -0
- package/tools/wallet/getLockData.ts +25 -0
- package/tools/wallet/getOrdinals.ts +35 -0
- package/tools/wallet/listOrdinal.ts +86 -0
- package/tools/wallet/listTokens.ts +28 -0
- package/tools/wallet/lockBsv.ts +69 -0
- package/tools/wallet/opnsDeregister.ts +80 -0
- package/tools/wallet/opnsRegister.ts +80 -0
- package/tools/wallet/purchaseListing.ts +89 -290
- package/tools/wallet/refreshUtxos.ts +21 -32
- package/tools/wallet/schemas.ts +7 -11
- package/tools/wallet/sendAllBsv.ts +64 -0
- package/tools/wallet/sendToAddress.ts +46 -47
- package/tools/wallet/signBsm.ts +29 -0
- package/tools/wallet/sweepBsv.ts +70 -0
- package/tools/wallet/sweepBsv21.ts +71 -0
- package/tools/wallet/sweepOrdinals.ts +69 -0
- package/tools/wallet/tools.ts +50 -88
- package/tools/wallet/transferOrdToken.ts +137 -129
- package/tools/wallet/unlockBsv.ts +57 -0
- package/tools/wallet/a2bPublishAgent.ts +0 -307
- package/tools/wallet/sendOrdinals.ts +0 -160
package/index.ts
CHANGED
|
@@ -35,7 +35,8 @@ import { getBsvPriceWithCache } from "./tools/bsv/getPrice.ts";
|
|
|
35
35
|
import { registerAllTools, type ToolsConfig } from "./tools/index.ts";
|
|
36
36
|
import { IntegratedWallet } from "./tools/wallet/integratedWallet.ts";
|
|
37
37
|
import { Wallet } from "./tools/wallet/wallet.ts";
|
|
38
|
-
import {
|
|
38
|
+
import { initWallet, destroyWallet } from "./utils/walletInit.ts";
|
|
39
|
+
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
39
40
|
import {
|
|
40
41
|
type BSVJWTPayload,
|
|
41
42
|
createMCPJWTValidator,
|
|
@@ -44,9 +45,48 @@ import {
|
|
|
44
45
|
import { SecureKeyManager } from "./utils/keyManager.ts";
|
|
45
46
|
import { setServerInstance } from "./utils/passphrasePrompt.ts";
|
|
46
47
|
|
|
47
|
-
// Initialize server variable
|
|
48
|
+
// Initialize server variable (used for stdio mode and passphrase detection)
|
|
48
49
|
let server: McpServer | undefined;
|
|
49
50
|
|
|
51
|
+
/** Options for createConfiguredServer */
|
|
52
|
+
interface ServerFactoryOptions {
|
|
53
|
+
toolsConfig: ToolsConfig;
|
|
54
|
+
wallet?: Wallet;
|
|
55
|
+
loadPrompts: boolean;
|
|
56
|
+
loadResources: boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Creates a fully configured McpServer with all tools, prompts, and resources registered.
|
|
61
|
+
* Used to create per-session server instances for HTTP mode and the single instance for stdio.
|
|
62
|
+
*/
|
|
63
|
+
function createConfiguredServer(opts: ServerFactoryOptions): McpServer {
|
|
64
|
+
const srv = new McpServer(
|
|
65
|
+
{ name: packageJson.name, version: packageJson.version },
|
|
66
|
+
{
|
|
67
|
+
capabilities: {
|
|
68
|
+
prompts: {},
|
|
69
|
+
resources: {},
|
|
70
|
+
tools: {},
|
|
71
|
+
experimental: {
|
|
72
|
+
"io.modelcontextprotocol/ui": { version: "0.1" },
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
instructions: `
|
|
76
|
+
This server exposes Bitcoin SV helpers.
|
|
77
|
+
Tools are idempotent unless marked destructive.
|
|
78
|
+
`,
|
|
79
|
+
},
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
registerAllTools(srv, opts.toolsConfig);
|
|
83
|
+
registerMcpAppTools(srv, opts.wallet);
|
|
84
|
+
if (opts.loadPrompts) registerAllPrompts(srv);
|
|
85
|
+
if (opts.loadResources) registerResources(srv);
|
|
86
|
+
|
|
87
|
+
return srv;
|
|
88
|
+
}
|
|
89
|
+
|
|
50
90
|
/**
|
|
51
91
|
* Configuration options from environment variables
|
|
52
92
|
*/
|
|
@@ -835,29 +875,11 @@ Authentication:
|
|
|
835
875
|
logFunc("------------------------------------\n");
|
|
836
876
|
// --- End of Logging Block ---
|
|
837
877
|
|
|
838
|
-
|
|
839
|
-
{ name: packageJson.name, version: packageJson.version },
|
|
840
|
-
{
|
|
841
|
-
capabilities: {
|
|
842
|
-
prompts: {},
|
|
843
|
-
resources: {},
|
|
844
|
-
tools: {},
|
|
845
|
-
experimental: {
|
|
846
|
-
"io.modelcontextprotocol/ui": { version: "0.1" },
|
|
847
|
-
},
|
|
848
|
-
},
|
|
849
|
-
instructions: `
|
|
850
|
-
This server exposes Bitcoin SV helpers.
|
|
851
|
-
Tools are idempotent unless marked destructive.
|
|
852
|
-
`,
|
|
853
|
-
},
|
|
854
|
-
);
|
|
855
|
-
|
|
856
|
-
// Set server instance for transport detection
|
|
857
|
-
setServerInstance(server);
|
|
858
|
-
|
|
878
|
+
// --- Initialize Wallet & Tools Config ---
|
|
859
879
|
let wallet: Wallet | undefined;
|
|
860
880
|
let integratedWallet: IntegratedWallet | undefined;
|
|
881
|
+
let remoteCtx: import("@1sat/actions").OneSatContext | undefined;
|
|
882
|
+
let remoteServices: import("@1sat/wallet-remote").OneSatServices | undefined;
|
|
861
883
|
|
|
862
884
|
if (CONFIG.loadTools) {
|
|
863
885
|
// Check if we should use Droplet API mode
|
|
@@ -870,9 +892,8 @@ Authentication:
|
|
|
870
892
|
dropletConfig: {
|
|
871
893
|
apiUrl: CONFIG.dropletApiUrl,
|
|
872
894
|
faucetName: CONFIG.dropletFaucetName,
|
|
873
|
-
// Note: authKey will be set by IntegratedWallet if paymentKey is provided
|
|
874
895
|
},
|
|
875
|
-
paymentKey: payPk,
|
|
896
|
+
paymentKey: payPk,
|
|
876
897
|
identityKey: identityPk,
|
|
877
898
|
});
|
|
878
899
|
logFunc(
|
|
@@ -884,14 +905,12 @@ Authentication:
|
|
|
884
905
|
logFunc(
|
|
885
906
|
"\x1b[33mNOTE: Local keys are ignored in Droplet API mode\x1b[0m",
|
|
886
907
|
);
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
effectiveConfig.
|
|
892
|
-
effectiveConfig.
|
|
893
|
-
effectiveConfig.loadA2bTools = false; // A2B requires identity key
|
|
894
|
-
effectiveConfig.loadBsocialTools = false; // BSocial requires local wallet
|
|
908
|
+
wallet = integratedWallet.getLocalWallet();
|
|
909
|
+
|
|
910
|
+
effectiveConfig.loadMneeTools = false;
|
|
911
|
+
effectiveConfig.loadBapTools = false;
|
|
912
|
+
effectiveConfig.loadA2bTools = false;
|
|
913
|
+
effectiveConfig.loadBsocialTools = false;
|
|
895
914
|
} catch (e) {
|
|
896
915
|
logFunc(
|
|
897
916
|
`\x1b[31mERROR: Failed to initialize Droplet API mode: ${e instanceof Error ? e.message : String(e)}. Wallet-dependent tools will be unavailable.\x1b[0m`,
|
|
@@ -903,7 +922,6 @@ Authentication:
|
|
|
903
922
|
}
|
|
904
923
|
}
|
|
905
924
|
} else if (payPk) {
|
|
906
|
-
// Initialize wallet with the payPk if wallet tools are enabled
|
|
907
925
|
if (CONFIG.loadWalletTools) {
|
|
908
926
|
try {
|
|
909
927
|
wallet = new Wallet(payPk, identityPk);
|
|
@@ -934,9 +952,23 @@ Authentication:
|
|
|
934
952
|
effectiveConfig.loadMneeTools = false;
|
|
935
953
|
effectiveConfig.loadBapTools = false;
|
|
936
954
|
}
|
|
955
|
+
|
|
956
|
+
// Initialize remote BRC-100 wallet alongside the local wallet
|
|
957
|
+
try {
|
|
958
|
+
const chain = (process.env.BSV_CHAIN as "main" | "test") ?? "main";
|
|
959
|
+
const remoteResult = await initWallet(payPk.toWif(), chain);
|
|
960
|
+
remoteCtx = remoteResult.ctx;
|
|
961
|
+
remoteServices = remoteResult.services;
|
|
962
|
+
logFunc(
|
|
963
|
+
`\x1b[32mINFO: Remote BRC-100 wallet initialized. Deposit address: ${remoteResult.depositAddress}\x1b[0m`,
|
|
964
|
+
);
|
|
965
|
+
} catch (e) {
|
|
966
|
+
logFunc(
|
|
967
|
+
`\x1b[33mWARN: Remote BRC-100 wallet initialization failed: ${e instanceof Error ? e.message : String(e)}. Falling back to local wallet only.\x1b[0m`,
|
|
968
|
+
);
|
|
969
|
+
}
|
|
937
970
|
}
|
|
938
971
|
|
|
939
|
-
// Disable MNEE tools if wallet is not available
|
|
940
972
|
if (effectiveConfig.loadMneeTools && !wallet && CONFIG.loadWalletTools) {
|
|
941
973
|
logFunc(
|
|
942
974
|
"\x1b[33mWARN: MNEE tools require a wallet but wallet initialization failed. MNEE tools disabled.\x1b[0m",
|
|
@@ -944,224 +976,164 @@ Authentication:
|
|
|
944
976
|
effectiveConfig.loadMneeTools = false;
|
|
945
977
|
}
|
|
946
978
|
}
|
|
947
|
-
|
|
948
|
-
// Register all other tools based on configuration
|
|
949
|
-
const toolsConfig: ToolsConfig = {
|
|
950
|
-
enableBsvTools: effectiveConfig.loadBsvTools,
|
|
951
|
-
enableOrdinalsTools: effectiveConfig.loadOrdinalsTools,
|
|
952
|
-
enableUtilsTools: effectiveConfig.loadUtilsTools,
|
|
953
|
-
enableA2bTools: effectiveConfig.loadA2bTools,
|
|
954
|
-
enableBapTools: effectiveConfig.loadBapTools,
|
|
955
|
-
enableBsocialTools: effectiveConfig.loadBsocialTools,
|
|
956
|
-
enableWalletTools: effectiveConfig.loadWalletTools,
|
|
957
|
-
enableMneeTools: effectiveConfig.loadMneeTools,
|
|
958
|
-
identityPk,
|
|
959
|
-
payPk,
|
|
960
|
-
xprv,
|
|
961
|
-
wallet,
|
|
962
|
-
integratedWallet,
|
|
963
|
-
disableBroadcasting: effectiveConfig.disableBroadcasting,
|
|
964
|
-
};
|
|
965
|
-
|
|
966
|
-
registerAllTools(server, toolsConfig);
|
|
967
979
|
}
|
|
968
980
|
|
|
969
|
-
//
|
|
970
|
-
|
|
981
|
+
// Build the shared tools config (used by server factory for each session)
|
|
982
|
+
const toolsConfig: ToolsConfig = CONFIG.loadTools
|
|
983
|
+
? {
|
|
984
|
+
enableBsvTools: effectiveConfig.loadBsvTools,
|
|
985
|
+
enableOrdinalsTools: effectiveConfig.loadOrdinalsTools,
|
|
986
|
+
enableUtilsTools: effectiveConfig.loadUtilsTools,
|
|
987
|
+
enableA2bTools: effectiveConfig.loadA2bTools,
|
|
988
|
+
enableBapTools: effectiveConfig.loadBapTools,
|
|
989
|
+
enableBsocialTools: effectiveConfig.loadBsocialTools,
|
|
990
|
+
enableWalletTools: effectiveConfig.loadWalletTools,
|
|
991
|
+
enableMneeTools: effectiveConfig.loadMneeTools,
|
|
992
|
+
identityPk,
|
|
993
|
+
payPk,
|
|
994
|
+
xprv,
|
|
995
|
+
wallet,
|
|
996
|
+
integratedWallet,
|
|
997
|
+
disableBroadcasting: effectiveConfig.disableBroadcasting,
|
|
998
|
+
ctx: remoteCtx,
|
|
999
|
+
services: remoteServices,
|
|
1000
|
+
}
|
|
1001
|
+
: {
|
|
1002
|
+
enableBsvTools: false,
|
|
1003
|
+
enableOrdinalsTools: false,
|
|
1004
|
+
enableUtilsTools: false,
|
|
1005
|
+
enableA2bTools: false,
|
|
1006
|
+
enableBapTools: false,
|
|
1007
|
+
enableBsocialTools: false,
|
|
1008
|
+
enableWalletTools: false,
|
|
1009
|
+
enableMneeTools: false,
|
|
1010
|
+
disableBroadcasting: true,
|
|
1011
|
+
};
|
|
971
1012
|
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
1013
|
+
const serverFactoryOpts: ServerFactoryOptions = {
|
|
1014
|
+
toolsConfig,
|
|
1015
|
+
wallet,
|
|
1016
|
+
loadPrompts: effectiveConfig.loadPrompts,
|
|
1017
|
+
loadResources: effectiveConfig.loadResources,
|
|
1018
|
+
};
|
|
976
1019
|
|
|
977
|
-
//
|
|
978
|
-
|
|
979
|
-
|
|
1020
|
+
// Clean up remote wallet on shutdown
|
|
1021
|
+
for (const sig of ["SIGINT", "SIGTERM"] as const) {
|
|
1022
|
+
process.once(sig, () => {
|
|
1023
|
+
destroyWallet().catch(() => {});
|
|
1024
|
+
});
|
|
980
1025
|
}
|
|
981
1026
|
|
|
982
1027
|
// Start the server based on transport mode
|
|
983
1028
|
if (CONFIG.transportMode === "stdio") {
|
|
1029
|
+
// Stdio: single server instance, single transport
|
|
1030
|
+
server = createConfiguredServer(serverFactoryOpts);
|
|
1031
|
+
setServerInstance(server);
|
|
984
1032
|
const transport = new StdioServerTransport();
|
|
985
1033
|
await server.connect(transport);
|
|
986
1034
|
logFunc("BSV MCP Server running on stdio");
|
|
987
1035
|
} else {
|
|
1036
|
+
// --- HTTP: Streamable HTTP transport (MCP 2025-03-26 spec) ---
|
|
988
1037
|
const port = CONFIG.port;
|
|
989
|
-
const messageEndpoint = "/messages";
|
|
990
|
-
const activeTransports = new Map<string, BunSSEServerTransport>();
|
|
991
|
-
|
|
992
|
-
// Set up resource URL for OAuth
|
|
993
1038
|
const resourceUrl = CONFIG.resourceUrl || `http://localhost:${port}`;
|
|
994
1039
|
|
|
995
|
-
//
|
|
1040
|
+
// JWT validator for OAuth
|
|
996
1041
|
const jwtValidator = CONFIG.enableOAuth
|
|
997
1042
|
? createMCPJWTValidator(resourceUrl)
|
|
998
1043
|
: null;
|
|
999
1044
|
|
|
1000
|
-
|
|
1045
|
+
// Session tracking: sessionId -> { server, transport }
|
|
1046
|
+
const sessions = new Map<
|
|
1047
|
+
string,
|
|
1048
|
+
{
|
|
1049
|
+
server: McpServer;
|
|
1050
|
+
transport: WebStandardStreamableHTTPServerTransport;
|
|
1051
|
+
}
|
|
1052
|
+
>();
|
|
1053
|
+
|
|
1054
|
+
logFunc(`Starting BSV MCP Server in Streamable HTTP mode on port ${port}...`);
|
|
1001
1055
|
if (CONFIG.enableOAuth) {
|
|
1002
1056
|
logFunc(`OAuth 2.1 authentication enabled`);
|
|
1003
1057
|
logFunc(` Issuer: ${CONFIG.oauthIssuer}`);
|
|
1004
1058
|
logFunc(` Resource: ${resourceUrl}`);
|
|
1005
1059
|
}
|
|
1006
1060
|
|
|
1061
|
+
const authServer =
|
|
1062
|
+
process.env.OAUTH_ISSUER || "https://auth.sigmaidentity.com";
|
|
1063
|
+
|
|
1064
|
+
/** CORS headers for the /mcp endpoint */
|
|
1065
|
+
const corsHeaders = {
|
|
1066
|
+
"Access-Control-Allow-Origin": "*",
|
|
1067
|
+
"Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
|
|
1068
|
+
"Access-Control-Allow-Headers":
|
|
1069
|
+
"Content-Type, Authorization, mcp-session-id, Last-Event-ID, mcp-protocol-version",
|
|
1070
|
+
"Access-Control-Expose-Headers":
|
|
1071
|
+
"mcp-session-id, mcp-protocol-version",
|
|
1072
|
+
} as const;
|
|
1073
|
+
|
|
1074
|
+
/**
|
|
1075
|
+
* Validate OAuth JWT token from request.
|
|
1076
|
+
* Returns null if auth disabled or no token. Throws on invalid token.
|
|
1077
|
+
*/
|
|
1078
|
+
async function validateAuth(req: Request): Promise<BSVJWTPayload | null> {
|
|
1079
|
+
if (!CONFIG.enableOAuth || !jwtValidator) return null;
|
|
1080
|
+
|
|
1081
|
+
const userContext = await jwtValidator.validateFromRequest(req);
|
|
1082
|
+
if (!userContext) {
|
|
1083
|
+
const err = new Error("Authentication required");
|
|
1084
|
+
(err as Error & { status: number }).status = 401;
|
|
1085
|
+
throw err;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
logFunc(
|
|
1089
|
+
`Authenticated: ${userContext.sub} (pubkey: ${userContext.pubkey?.substring(0, 20)}...)`,
|
|
1090
|
+
);
|
|
1091
|
+
return userContext;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1007
1094
|
Bun.serve({
|
|
1008
|
-
port
|
|
1095
|
+
port,
|
|
1009
1096
|
async fetch(req: Request): Promise<Response> {
|
|
1010
1097
|
const url = new URL(req.url);
|
|
1011
1098
|
|
|
1012
|
-
//
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
url.pathname === "/.well-known/oauth-authorization-server";
|
|
1016
|
-
|
|
1017
|
-
// Validate JWT for protected endpoints (if OAuth enabled)
|
|
1018
|
-
let userContext: BSVJWTPayload | null = null;
|
|
1019
|
-
if (CONFIG.enableOAuth && jwtValidator && !isDiscoveryEndpoint) {
|
|
1020
|
-
try {
|
|
1021
|
-
userContext = await jwtValidator.validateFromRequest(req);
|
|
1022
|
-
|
|
1023
|
-
if (!userContext) {
|
|
1024
|
-
// No token provided - return 401 with WWW-Authenticate
|
|
1025
|
-
return new Response(
|
|
1026
|
-
JSON.stringify({
|
|
1027
|
-
error: "unauthorized",
|
|
1028
|
-
message: "Authentication required",
|
|
1029
|
-
}),
|
|
1030
|
-
{
|
|
1031
|
-
status: 401,
|
|
1032
|
-
headers: {
|
|
1033
|
-
"Content-Type": "application/json",
|
|
1034
|
-
"WWW-Authenticate": generateWWWAuthenticate(
|
|
1035
|
-
resourceUrl,
|
|
1036
|
-
"invalid_token",
|
|
1037
|
-
"Authentication required",
|
|
1038
|
-
),
|
|
1039
|
-
"Access-Control-Expose-Headers": "WWW-Authenticate",
|
|
1040
|
-
"Access-Control-Allow-Origin": "*",
|
|
1041
|
-
},
|
|
1042
|
-
},
|
|
1043
|
-
);
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
|
-
// Token validated successfully
|
|
1047
|
-
logFunc(
|
|
1048
|
-
`Authenticated request from user: ${userContext.sub} (pubkey: ${userContext.pubkey?.substring(0, 20)}...)`,
|
|
1049
|
-
);
|
|
1050
|
-
} catch (error) {
|
|
1051
|
-
// Token provided but invalid
|
|
1052
|
-
const errorMessage =
|
|
1053
|
-
error instanceof Error
|
|
1054
|
-
? error.message
|
|
1055
|
-
: "Token validation failed";
|
|
1056
|
-
|
|
1057
|
-
logFunc(`JWT validation error: ${errorMessage}`);
|
|
1058
|
-
|
|
1059
|
-
return new Response(
|
|
1060
|
-
JSON.stringify({
|
|
1061
|
-
error: "invalid_token",
|
|
1062
|
-
message: errorMessage,
|
|
1063
|
-
}),
|
|
1064
|
-
{
|
|
1065
|
-
status: 401,
|
|
1066
|
-
headers: {
|
|
1067
|
-
"Content-Type": "application/json",
|
|
1068
|
-
"WWW-Authenticate": generateWWWAuthenticate(
|
|
1069
|
-
resourceUrl,
|
|
1070
|
-
"invalid_token",
|
|
1071
|
-
errorMessage,
|
|
1072
|
-
),
|
|
1073
|
-
"Access-Control-Expose-Headers": "WWW-Authenticate",
|
|
1074
|
-
"Access-Control-Allow-Origin": "*",
|
|
1075
|
-
},
|
|
1076
|
-
},
|
|
1077
|
-
);
|
|
1078
|
-
}
|
|
1079
|
-
}
|
|
1080
|
-
|
|
1081
|
-
// Handle SSE connection endpoint
|
|
1082
|
-
if (req.headers.get("accept") === "text/event-stream") {
|
|
1083
|
-
const transport = new BunSSEServerTransport(messageEndpoint);
|
|
1084
|
-
activeTransports.set(transport.sessionId, transport);
|
|
1085
|
-
logFunc(`New SSE connection: ${transport.sessionId}`);
|
|
1086
|
-
|
|
1087
|
-
try {
|
|
1088
|
-
// Connect server instance to THIS transport
|
|
1089
|
-
if (!server) throw new Error("Server not initialized");
|
|
1090
|
-
await server.connect(transport);
|
|
1091
|
-
logFunc(
|
|
1092
|
-
`Server connected to transport for session: ${transport.sessionId}`,
|
|
1093
|
-
);
|
|
1094
|
-
|
|
1095
|
-
return await transport.createResponse();
|
|
1096
|
-
} catch (connectError) {
|
|
1097
|
-
logFunc(
|
|
1098
|
-
`Error connecting server to transport ${transport.sessionId}: ${connectError}`,
|
|
1099
|
-
);
|
|
1100
|
-
activeTransports.delete(transport.sessionId); // Clean up if connect fails
|
|
1101
|
-
return new Response("Failed to establish SSE connection", {
|
|
1102
|
-
status: 500,
|
|
1103
|
-
});
|
|
1104
|
-
}
|
|
1099
|
+
// --- CORS preflight ---
|
|
1100
|
+
if (req.method === "OPTIONS") {
|
|
1101
|
+
return new Response(null, { status: 204, headers: corsHeaders });
|
|
1105
1102
|
}
|
|
1106
1103
|
|
|
1107
|
-
//
|
|
1108
|
-
if (req.method === "POST" && url.pathname === messageEndpoint) {
|
|
1109
|
-
const sessionId = url.searchParams.get("sessionId");
|
|
1110
|
-
if (!sessionId) {
|
|
1111
|
-
return new Response("Missing sessionId query parameter", {
|
|
1112
|
-
status: 400,
|
|
1113
|
-
});
|
|
1114
|
-
}
|
|
1115
|
-
const transport = activeTransports.get(sessionId);
|
|
1116
|
-
if (!transport) {
|
|
1117
|
-
return new Response(`Invalid or expired sessionId: ${sessionId}`, {
|
|
1118
|
-
status: 404,
|
|
1119
|
-
});
|
|
1120
|
-
}
|
|
1121
|
-
// Let the specific transport instance handle the message
|
|
1122
|
-
return await transport.handlePostMessage(req);
|
|
1123
|
-
}
|
|
1124
|
-
|
|
1125
|
-
// Handle OAuth 2.1 Authorization Server Metadata (MCP spec requirement)
|
|
1126
|
-
// MCP clients will request this to discover sigma-auth endpoints
|
|
1104
|
+
// --- OAuth 2.1 Authorization Server Metadata ---
|
|
1127
1105
|
if (
|
|
1128
1106
|
req.method === "GET" &&
|
|
1129
1107
|
url.pathname === "/.well-known/oauth-authorization-server"
|
|
1130
1108
|
) {
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
{
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
},
|
|
1158
|
-
null,
|
|
1159
|
-
2,
|
|
1160
|
-
),
|
|
1109
|
+
return Response.json(
|
|
1110
|
+
{
|
|
1111
|
+
issuer: authServer,
|
|
1112
|
+
authorization_endpoint: `${authServer}/api/oauth/authorize`,
|
|
1113
|
+
token_endpoint: `${authServer}/api/oauth/token`,
|
|
1114
|
+
userinfo_endpoint: `${authServer}/api/oauth/userinfo`,
|
|
1115
|
+
jwks_uri: `${authServer}/.well-known/jwks.json`,
|
|
1116
|
+
registration_endpoint: `${authServer}/api/oauth/register`,
|
|
1117
|
+
scopes_supported: [
|
|
1118
|
+
"openid",
|
|
1119
|
+
"profile",
|
|
1120
|
+
"email",
|
|
1121
|
+
"offline_access",
|
|
1122
|
+
"bsv:tools",
|
|
1123
|
+
"bsv:wallet",
|
|
1124
|
+
"bsv:ordinals",
|
|
1125
|
+
"bsv:tokens",
|
|
1126
|
+
],
|
|
1127
|
+
response_types_supported: ["code"],
|
|
1128
|
+
grant_types_supported: [
|
|
1129
|
+
"authorization_code",
|
|
1130
|
+
"refresh_token",
|
|
1131
|
+
],
|
|
1132
|
+
token_endpoint_auth_methods_supported: ["none"],
|
|
1133
|
+
code_challenge_methods_supported: ["S256"],
|
|
1134
|
+
},
|
|
1161
1135
|
{
|
|
1162
|
-
status: 200,
|
|
1163
1136
|
headers: {
|
|
1164
|
-
"Content-Type": "application/json",
|
|
1165
1137
|
"Access-Control-Allow-Origin": "*",
|
|
1166
1138
|
"Cache-Control": "public, max-age=3600",
|
|
1167
1139
|
},
|
|
@@ -1169,67 +1141,118 @@ Authentication:
|
|
|
1169
1141
|
);
|
|
1170
1142
|
}
|
|
1171
1143
|
|
|
1172
|
-
//
|
|
1144
|
+
// --- OAuth 2.1 Protected Resource Metadata (RFC 9728) ---
|
|
1173
1145
|
if (
|
|
1174
1146
|
req.method === "GET" &&
|
|
1175
1147
|
url.pathname === "/.well-known/oauth-protected-resource"
|
|
1176
1148
|
) {
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
"bsv:ordinals",
|
|
1194
|
-
"bsv:tokens",
|
|
1195
|
-
],
|
|
1196
|
-
bearer_methods_supported: ["header"],
|
|
1197
|
-
resource_signing_alg_values_supported: ["RS256", "ES256"],
|
|
1198
|
-
},
|
|
1199
|
-
null,
|
|
1200
|
-
2,
|
|
1201
|
-
),
|
|
1149
|
+
return Response.json(
|
|
1150
|
+
{
|
|
1151
|
+
resource: resourceUrl,
|
|
1152
|
+
authorization_servers: [authServer],
|
|
1153
|
+
scopes_supported: [
|
|
1154
|
+
"openid",
|
|
1155
|
+
"profile",
|
|
1156
|
+
"email",
|
|
1157
|
+
"bsv:tools",
|
|
1158
|
+
"bsv:wallet",
|
|
1159
|
+
"bsv:ordinals",
|
|
1160
|
+
"bsv:tokens",
|
|
1161
|
+
],
|
|
1162
|
+
bearer_methods_supported: ["header"],
|
|
1163
|
+
resource_signing_alg_values_supported: ["RS256", "ES256"],
|
|
1164
|
+
},
|
|
1202
1165
|
{
|
|
1203
|
-
status: 200,
|
|
1204
1166
|
headers: {
|
|
1205
|
-
"Content-Type": "application/json",
|
|
1206
1167
|
"Access-Control-Allow-Origin": "*",
|
|
1207
|
-
"Access-Control-Allow-Methods": "GET",
|
|
1208
|
-
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
|
1209
|
-
"Access-Control-Expose-Headers": "WWW-Authenticate",
|
|
1210
1168
|
"Cache-Control": "public, max-age=3600",
|
|
1211
1169
|
},
|
|
1212
1170
|
},
|
|
1213
1171
|
);
|
|
1214
1172
|
}
|
|
1215
1173
|
|
|
1216
|
-
//
|
|
1217
|
-
if (
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1174
|
+
// --- MCP Streamable HTTP endpoint ---
|
|
1175
|
+
if (url.pathname === "/mcp") {
|
|
1176
|
+
// Validate OAuth
|
|
1177
|
+
let authInfo: { token: string; clientId: string; scopes: string[] } | undefined;
|
|
1178
|
+
try {
|
|
1179
|
+
const userCtx = await validateAuth(req);
|
|
1180
|
+
if (userCtx) {
|
|
1181
|
+
authInfo = {
|
|
1182
|
+
token: req.headers.get("Authorization")?.substring(7) || "",
|
|
1183
|
+
clientId: userCtx.sub,
|
|
1184
|
+
scopes: userCtx.scope?.split(" ") || [],
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1187
|
+
} catch (error) {
|
|
1188
|
+
const msg =
|
|
1189
|
+
error instanceof Error
|
|
1190
|
+
? error.message
|
|
1191
|
+
: "Token validation failed";
|
|
1192
|
+
return new Response(
|
|
1193
|
+
JSON.stringify({ error: "invalid_token", message: msg }),
|
|
1194
|
+
{
|
|
1195
|
+
status: 401,
|
|
1196
|
+
headers: {
|
|
1197
|
+
"Content-Type": "application/json",
|
|
1198
|
+
"WWW-Authenticate": generateWWWAuthenticate(
|
|
1199
|
+
resourceUrl,
|
|
1200
|
+
"invalid_token",
|
|
1201
|
+
msg,
|
|
1202
|
+
),
|
|
1203
|
+
...corsHeaders,
|
|
1204
|
+
},
|
|
1205
|
+
},
|
|
1206
|
+
);
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
// Route to existing session or create new one
|
|
1210
|
+
const sessionId = req.headers.get("mcp-session-id");
|
|
1211
|
+
|
|
1212
|
+
if (sessionId) {
|
|
1213
|
+
const session = sessions.get(sessionId);
|
|
1214
|
+
if (!session) {
|
|
1215
|
+
return new Response(
|
|
1216
|
+
JSON.stringify({
|
|
1217
|
+
jsonrpc: "2.0",
|
|
1218
|
+
error: { code: -32001, message: "Session not found" },
|
|
1219
|
+
id: null,
|
|
1220
|
+
}),
|
|
1221
|
+
{ status: 404, headers: { "Content-Type": "application/json", ...corsHeaders } },
|
|
1222
|
+
);
|
|
1223
|
+
}
|
|
1224
|
+
const response = await session.transport.handleRequest(req, { authInfo });
|
|
1225
|
+
// Add CORS headers to transport response
|
|
1226
|
+
for (const [k, v] of Object.entries(corsHeaders)) {
|
|
1227
|
+
if (!response.headers.has(k)) response.headers.set(k, v);
|
|
1228
|
+
}
|
|
1229
|
+
return response;
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
// No session ID — new session (initialization request)
|
|
1233
|
+
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
1234
|
+
sessionIdGenerator: () => crypto.randomUUID(),
|
|
1235
|
+
onsessioninitialized: (id: string) => {
|
|
1236
|
+
sessions.set(id, { server: mcpServer, transport });
|
|
1237
|
+
logFunc(`New MCP session: ${id}`);
|
|
1238
|
+
},
|
|
1239
|
+
onsessionclosed: (id: string) => {
|
|
1240
|
+
sessions.delete(id);
|
|
1241
|
+
logFunc(`MCP session closed: ${id}`);
|
|
1228
1242
|
},
|
|
1229
1243
|
});
|
|
1244
|
+
|
|
1245
|
+
const mcpServer = createConfiguredServer(serverFactoryOpts);
|
|
1246
|
+
await mcpServer.connect(transport);
|
|
1247
|
+
|
|
1248
|
+
const response = await transport.handleRequest(req, { authInfo });
|
|
1249
|
+
// Add CORS headers
|
|
1250
|
+
for (const [k, v] of Object.entries(corsHeaders)) {
|
|
1251
|
+
if (!response.headers.has(k)) response.headers.set(k, v);
|
|
1252
|
+
}
|
|
1253
|
+
return response;
|
|
1230
1254
|
}
|
|
1231
1255
|
|
|
1232
|
-
// Default response for other paths/methods
|
|
1233
1256
|
return new Response("Not Found", { status: 404 });
|
|
1234
1257
|
},
|
|
1235
1258
|
error(error: Error): Response {
|
|
@@ -1237,13 +1260,12 @@ Authentication:
|
|
|
1237
1260
|
return new Response("Internal Server Error", { status: 500 });
|
|
1238
1261
|
},
|
|
1239
1262
|
});
|
|
1263
|
+
|
|
1240
1264
|
logFunc(
|
|
1241
|
-
`
|
|
1242
|
-
);
|
|
1243
|
-
logFunc(" SSE Endpoint: /sse");
|
|
1244
|
-
logFunc(
|
|
1245
|
-
` Message Endpoint: ${messageEndpoint} (POST with ?sessionId=...)`,
|
|
1265
|
+
`Bun server listening on http://localhost:${port}`,
|
|
1246
1266
|
);
|
|
1267
|
+
logFunc(" MCP Endpoint: /mcp (Streamable HTTP)");
|
|
1268
|
+
logFunc(" OAuth Discovery: /.well-known/oauth-protected-resource");
|
|
1247
1269
|
}
|
|
1248
1270
|
}
|
|
1249
1271
|
|