diff --git a/IPython/kernel/adapter.py b/IPython/kernel/adapter.py
new file mode 100644
index 0000000..615ac47
--- /dev/null
+++ b/IPython/kernel/adapter.py
@@ -0,0 +1,295 @@
+"""Adapters for IPython msg spec versions."""
+
+# Copyright (c) IPython Development Team.
+# Distributed under the terms of the Modified BSD License.
+
+import json
+
+from IPython.core.release import kernel_protocol_version, kernel_protocol_version_info
+from IPython.utils.tokenutil import token_at_cursor
+
+
+def code_to_line(code, cursor_pos):
+    """Turn a multiline code block and cursor position into a single line
+    and new cursor position.
+    
+    For adapting complete_ and object_info_requests.
+    """
+    for line in code.splitlines(True):
+        n = len(line)
+        if cursor_pos > n:
+            cursor_pos -= n
+        else:
+            break
+    return line, cursor_pos
+
+
+class Adapter(object):
+    """Base class for adapting messages
+    
+    Override message_type(msg) methods to create adapters.
+    """
+    
+    msg_type_map = {}
+    
+    def update_header(self, msg):
+        return msg
+    
+    def update_metadata(self, msg):
+        return msg
+    
+    def update_msg_type(self, msg):
+        header = msg['header']
+        msg_type = header['msg_type']
+        if msg_type in self.msg_type_map:
+            msg['msg_type'] = header['msg_type'] = self.msg_type_map[msg_type]
+        return msg
+    
+    def __call__(self, msg):
+        msg = self.update_header(msg)
+        msg = self.update_metadata(msg)
+        msg = self.update_msg_type(msg)
+        header = msg['header']
+        
+        handler = getattr(self, header['msg_type'], None)
+        if handler is None:
+            return msg
+        return handler(msg)
+
+def _version_str_to_list(version):
+    """convert a version string to a list of ints
+    
+    non-int segments are excluded
+    """
+    v = []
+    for part in content[key].split('.'):
+        try:
+            v.append(int(part))
+        except ValueError:
+            pass
+    return v
+
+class V5toV4(Adapter):
+    """Adapt msg protocol v5 to v4"""
+    
+    version = '4.1'
+    
+    msg_type_map = {
+        'execute_result' : 'pyout',
+        'execute_input' : 'pyin',
+        'error' : 'pyerr',
+        'inspect_request' : 'object_info_request',
+        'inspect_reply' : 'object_info_reply',
+    }
+    
+    def update_header(self, msg):
+        msg['header'].pop('version', None)
+        return msg
+    
+    # shell channel
+    
+    def kernel_info_reply(self, msg):
+        content = msg['content']
+        content.pop('banner', None)
+        for key in ('language_version', 'protocol_version'):
+            if key in content:
+                content[key] = _version_str_to_list(content[key])
+        if content.pop('implementation', '') == 'ipython' \
+            and 'implementation_version' in content:
+            content['ipython_version'] = content.pop('implmentation_version')
+        content.pop('implementation_version', None)
+        content.setdefault("implmentation", content['language'])
+        return msg
+    
+    def execute_request(self, msg):
+        content = msg['content']
+        content.setdefault('user_variables', [])
+        return msg
+    
+    def execute_reply(self, msg):
+        content = msg['content']
+        content.setdefault('user_variables', {})
+        # TODO: handle payloads
+        return msg
+    
+    def complete_request(self, msg):
+        content = msg['content']
+        code = content['code']
+        cursor_pos = content['cursor_pos']
+        line, cursor_pos = code_to_line(code, cursor_pos)
+        
+        new_content = msg['content'] = {}
+        new_content['text'] = ''
+        new_content['line'] = line
+        new_content['blob'] = None
+        new_content['cursor_pos'] = cursor_pos
+        return msg
+    
+    def complete_reply(self, msg):
+        content = msg['content']
+        cursor_start = content.pop('cursor_start')
+        cursor_end = content.pop('cursor_end')
+        match_len = cursor_end - cursor_start
+        content['matched_text'] = content['matches'][0][:match_len]
+        content.pop('metadata', None)
+        return msg
+    
+    def object_info_request(self, msg):
+        content = msg['content']
+        code = content['code']
+        cursor_pos = content['cursor_pos']
+        line, cursor_pos = code_to_line(code, cursor_pos)
+        
+        new_content = msg['content'] = {}
+        new_content['name'] = token_at_cursor(code, cursor_pos)
+        new_content['detail_level'] = content['detail_level']
+        return msg
+    
+    def object_info_reply(self, msg):
+        """inspect_reply can't be easily backward compatible"""
+        msg['content'] = {'found' : False, 'name' : 'unknown'}
+        return msg
+    
+    # iopub channel
+    
+    def display_data(self, msg):
+        content = msg['content']
+        content.setdefault("source", "display")
+        return msg
+    
+    # stdin channel
+    
+    def input_request(self, msg):
+        msg['content'].pop('password', None)
+        return msg
+
+def _tuple_to_str(version):
+    return ".".join(map(str, version))
+
+class V4toV5(Adapter):
+    """Convert msg spec V4 to V5"""
+    version = kernel_protocol_version
+    
+    # invert message renames above
+    msg_type_map = {v:k for k,v in V5toV4.msg_type_map.items()}
+    
+    def update_header(self, msg):
+        msg['header']['version'] = self.version
+        return msg
+    
+    # shell channel
+    
+    def kernel_info_reply(self, msg):
+        content = msg['content']
+        for key in ('language_version', 'protocol_version', 'ipython_version'):
+            if key in content:
+                content[key] = ".".join(map(str, content[key]))
+        
+        if content['language'].startswith('python') and 'ipython_version' in content:
+            content['implementation'] = 'ipython'
+            content['implementation_version'] = content.pop('ipython_version')
+        
+        content['banner'] = ''
+        return msg
+    
+    def execute_request(self, msg):
+        content = msg['content']
+        user_variables = content.pop('user_variables', [])
+        user_expressions = content.setdefault('user_expressions', {})
+        for v in user_variables:
+            user_expressions[v] = v
+        return msg
+    
+    def execute_reply(self, msg):
+        content = msg['content']
+        user_expressions = content.setdefault('user_expressions', {})
+        user_variables = content.pop('user_variables', {})
+        if user_variables:
+            user_expressions.update(user_variables)
+        return msg
+    
+    def complete_request(self, msg):
+        old_content = msg['content']
+        
+        new_content = msg['content'] = {}
+        new_content['code'] = old_content['line']
+        new_content['cursor_pos'] = old_content['cursor_pos']
+        return msg
+    
+    def complete_reply(self, msg):
+        # TODO: complete_reply needs more context than we have
+        # Maybe strip common prefix and give magic cursor_start = cursor_end = 0?
+        content = msg['content'] = {}
+        content['matches'] = []
+        content['cursor_start'] = content['cursor_end'] = 0
+        content['metadata'] = {}
+        return msg
+    
+    def inspect_request(self, msg):
+        content = msg['content']
+        name = content['name']
+        
+        new_content = msg['content'] = {}
+        new_content['code'] = name
+        new_content['cursor_pos'] = len(name) - 1
+        new_content['detail_level'] = content['detail_level']
+        return msg
+    
+    def inspect_reply(self, msg):
+        """inspect_reply can't be easily backward compatible"""
+        msg['content'] = {'found' : False, 'name' : 'unknown'}
+        return msg
+    
+    # iopub channel
+    
+    def display_data(self, msg):
+        content = msg['content']
+        content.pop("source", None)
+        data = content['data']
+        if 'application/json' in data:
+            data['application/json'] = json.dumps(data['application/json'])
+        return msg
+    
+    # stdin channel
+    
+    def input_request(self, msg):
+        msg['content'].setdefault('password', False)
+        return msg
+    
+
+
+def adapt(msg, to_version=kernel_protocol_version_info[0]):
+    """Adapt a single message to a target version
+    
+    Parameters
+    ----------
+    
+    msg : dict
+        An IPython message.
+    to_version : int, optional
+        The target major version.
+        If unspecified, adapt to the current version for IPython.
+    
+    Returns
+    -------
+    
+    msg : dict
+        An IPython message appropriate in the new version.
+    """
+    header = msg['header']
+    if 'version' in header:
+        from_version = int(header['version'].split('.')[0])
+    else:
+        # assume last version before adding the key to the header
+        from_version = 4
+    adapter = adapters.get((from_version, to_version), None)
+    if adapter is None:
+        return msg
+    return adapter(msg)
+
+
+# one adapter per major version from,to
+adapters = {
+    (5,4) : V5toV4(),
+    (4,5) : V4toV5(),
+}
\ No newline at end of file