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.
Files changed (40) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +26 -2
  3. package/dist/index.js +6777 -126216
  4. package/index.ts +287 -265
  5. package/package.json +4 -3
  6. package/tools/bap/generate.test.ts +17 -16
  7. package/tools/index.ts +8 -3
  8. package/tools/ordinals/getInscription.ts +15 -65
  9. package/tools/ordinals/getTokenByIdOrTicker.ts +14 -80
  10. package/tools/ordinals/index.ts +8 -7
  11. package/tools/ordinals/marketListings.ts +21 -188
  12. package/tools/ordinals/marketSales.ts +21 -127
  13. package/tools/ordinals/searchInscriptions.ts +21 -108
  14. package/tools/wallet/brc100.ts +572 -0
  15. package/tools/wallet/cancelListing.ts +80 -0
  16. package/tools/wallet/createOrdinals.ts +62 -143
  17. package/tools/wallet/getAddress.ts +19 -13
  18. package/tools/wallet/getBalance.ts +29 -25
  19. package/tools/wallet/getBsv21Balances.ts +28 -0
  20. package/tools/wallet/getLockData.ts +25 -0
  21. package/tools/wallet/getOrdinals.ts +35 -0
  22. package/tools/wallet/listOrdinal.ts +86 -0
  23. package/tools/wallet/listTokens.ts +28 -0
  24. package/tools/wallet/lockBsv.ts +69 -0
  25. package/tools/wallet/opnsDeregister.ts +80 -0
  26. package/tools/wallet/opnsRegister.ts +80 -0
  27. package/tools/wallet/purchaseListing.ts +89 -290
  28. package/tools/wallet/refreshUtxos.ts +21 -32
  29. package/tools/wallet/schemas.ts +7 -11
  30. package/tools/wallet/sendAllBsv.ts +64 -0
  31. package/tools/wallet/sendToAddress.ts +46 -47
  32. package/tools/wallet/signBsm.ts +29 -0
  33. package/tools/wallet/sweepBsv.ts +70 -0
  34. package/tools/wallet/sweepBsv21.ts +71 -0
  35. package/tools/wallet/sweepOrdinals.ts +69 -0
  36. package/tools/wallet/tools.ts +50 -88
  37. package/tools/wallet/transferOrdToken.ts +137 -129
  38. package/tools/wallet/unlockBsv.ts +57 -0
  39. package/tools/wallet/a2bPublishAgent.ts +0 -307
  40. 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 { BunSSEServerTransport } from "./transports/sse.ts";
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
- server = new McpServer(
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, // Pass payment key for auth
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
- // Create a compatibility wrapper for existing code
888
- wallet = integratedWallet.getLocalWallet(); // This will be undefined in Droplet mode
889
-
890
- // In Droplet mode, we need to disable tools that require local keys
891
- effectiveConfig.loadMneeTools = false; // MNEE requires local wallet
892
- effectiveConfig.loadBapTools = false; // BAP requires identity key
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
- // Register MCP App tools and resource (interactive UI)
970
- registerMcpAppTools(server, wallet);
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
- // Register prompts if enabled
973
- if (CONFIG.loadPrompts) {
974
- registerAllPrompts(server);
975
- }
1013
+ const serverFactoryOpts: ServerFactoryOptions = {
1014
+ toolsConfig,
1015
+ wallet,
1016
+ loadPrompts: effectiveConfig.loadPrompts,
1017
+ loadResources: effectiveConfig.loadResources,
1018
+ };
976
1019
 
977
- // Register resources if enabled
978
- if (CONFIG.loadResources) {
979
- registerResources(server);
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
- // Initialize JWT validator if OAuth is enabled
1040
+ // JWT validator for OAuth
996
1041
  const jwtValidator = CONFIG.enableOAuth
997
1042
  ? createMCPJWTValidator(resourceUrl)
998
1043
  : null;
999
1044
 
1000
- logFunc(`Starting BSV MCP Server in HTTP/SSE mode on port ${port}...`);
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: port,
1095
+ port,
1009
1096
  async fetch(req: Request): Promise<Response> {
1010
1097
  const url = new URL(req.url);
1011
1098
 
1012
- // Skip auth for discovery endpoints
1013
- const isDiscoveryEndpoint =
1014
- url.pathname === "/.well-known/oauth-protected-resource" ||
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
- // Handle message POST endpoint
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
- const authServer =
1132
- process.env.OAUTH_ISSUER || "https://auth.sigmaidentity.com";
1133
-
1134
- return new Response(
1135
- JSON.stringify(
1136
- {
1137
- issuer: authServer,
1138
- authorization_endpoint: `${authServer}/api/oauth/authorize`,
1139
- token_endpoint: `${authServer}/api/oauth/token`,
1140
- userinfo_endpoint: `${authServer}/api/oauth/userinfo`,
1141
- jwks_uri: `${authServer}/.well-known/jwks.json`,
1142
- registration_endpoint: `${authServer}/api/oauth/register`,
1143
- scopes_supported: [
1144
- "openid",
1145
- "profile",
1146
- "email",
1147
- "offline_access",
1148
- "bsv:tools",
1149
- "bsv:wallet",
1150
- "bsv:ordinals",
1151
- "bsv:tokens",
1152
- ],
1153
- response_types_supported: ["code"],
1154
- grant_types_supported: ["authorization_code", "refresh_token"],
1155
- token_endpoint_auth_methods_supported: ["none"],
1156
- code_challenge_methods_supported: ["S256"],
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
- // Handle OAuth 2.1 Protected Resource Metadata (RFC 9728)
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
- const resourceUrl =
1178
- process.env.RESOURCE_URL || `http://localhost:${port}`;
1179
- const authServer =
1180
- process.env.OAUTH_ISSUER || "https://auth.sigmaidentity.com";
1181
-
1182
- return new Response(
1183
- JSON.stringify(
1184
- {
1185
- resource: resourceUrl,
1186
- authorization_servers: [authServer],
1187
- scopes_supported: [
1188
- "openid",
1189
- "profile",
1190
- "email",
1191
- "bsv:tools",
1192
- "bsv:wallet",
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
- // Handle CORS preflight for OAuth discovery
1217
- if (
1218
- req.method === "OPTIONS" &&
1219
- (url.pathname === "/.well-known/oauth-protected-resource" ||
1220
- url.pathname === "/.well-known/oauth-authorization-server")
1221
- ) {
1222
- return new Response(null, {
1223
- status: 204,
1224
- headers: {
1225
- "Access-Control-Allow-Origin": "*",
1226
- "Access-Control-Allow-Methods": "GET",
1227
- "Access-Control-Allow-Headers": "Content-Type, Authorization",
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
- ` Bun server started successfully. Listening on http://localhost:${port} `,
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